shithub: riscv

Download patch

ref: dff78ef64a0f064e74108394b76a92889f8a2d54
parent: 4bcd247b909898766d549c3990c230cbeb3d17a3
parent: 963cfc9a6f6e721f52aa949e6d1af0c3e8dc2ecc
author: cinap_lenrek <[email protected]>
date: Sun Mar 12 13:20:13 EDT 2017

merge

--- a/acme/mail/guide
+++ /dev/null
@@ -1,4 +1,0 @@
-Mail stored
-plumb /mail/box/$user/names
-mail -'x' someaddress
-mkbox /mail/box/$user/new_box
--- a/acme/mail/mkbox
+++ /dev/null
@@ -1,11 +1,0 @@
-#!/bin/rc
-
-for(i){
-	if(! test -e $i){
-		if(cp /dev/null $i){
-			chmod 600 $i
-			chmod +al $i
-		}
-	}
-	if not echo $i already exists
-}
--- a/acme/mail/readme
+++ /dev/null
@@ -1,57 +1,0 @@
-The Acme Mail program uses upas/fs to parse the mail box, and then
-presents a file-browser-like user interface to reading and sending
-messages.  The Mail window presents each numbered message like the
-contents of a directory presented one per line.  If a message has a
-Subject: line, that is shown indented on the following line.
-Multipart MIME-encoded messages are presented in the obvious
-hierarchical format.
-
-Mail uses upas/fs to access the mail box.  By default it reads "mbox",
-the standard user mail box.  If Mail is given an argument, it is
-passed to upas/fs as the name of the mail box (or upas/fs directory)
-to open.
-
-Although Mail works if the plumber is not running, it's designed to be
-run with plumbing enabled and many of its features work best if it is.
-
-The mailbox window has a few commands: Put writes back the mailbox;
-Mail creates a new window in which to compose a message; and Delmesg
-deletes messages by number.  The number may be given as argument or
-indicated by selecting the header line in the mailbox window.
-(Delmesg does not expand null selections, in the interest of safety.)
-
-Clicking the right button on a message number opens it; clicking on
-any of the subparts of a message opens that (and also opens the
-message itself).  Each message window has a few commands in the tag
-with obvious names: Reply, Delmsg, etc.  "Reply" replies to the single
-sender of the message, "Reply all" or "Replyall" replies to everyone
-in the From:, To:, and CC: lines.
-
-Message parts with recognized MIME types such as image/jpeg are sent
-to the plumber for further dispatch.  Acme Mail also listens to
-messages on the seemail and showmail plumbing ports, to report the
-arrival of new messages (highlighting the entry; right-click on the
-entry to open the message) and open them if you right-click on the
-face in the faces window.
-
-When composing a mail message or replying to a message, the first line
-of the text is a list of recipients of the message.  To:, and CC:, and BCC:
-lines are interpreted in the usual way. Two other header lines are
-special to Acme Mail:
-        Include: file places a copy of file in the message as an
-		inline MIME attachment.
-        Attach: file places a copy of file in the message as a regular
-		MIME attachment.
-
-Acme Mail uses these conventions when replying to messages,
-constructing headers for the default behavior.  You may edit these to
-change behavior.  Most important, when replying to a message Mail will
-always Include: the original message; delete that line if you don't
-want to include it.
-
-If the mailbox
-	/mail/box/$user/outgoing
-exists, Acme Mail will save your a copy of your outgoing messages
-there.  Attachments are described in the copy but not included.
-
-The -m mntpoint flag specifies a different mount point for /upas/fs.
--- a/acme/mail/src/dat.h
+++ /dev/null
@@ -1,164 +1,0 @@
-typedef struct Event Event;
-typedef struct Exec Exec;
-typedef struct Message Message;
-typedef struct Window Window;
-
-enum
-{
-	STACK		= 8192,
-	EVENTSIZE	= 256,
-	NEVENT		= 5,
-};
-
-struct Event
-{
-	int	c1;
-	int	c2;
-	int	q0;
-	int	q1;
-	int	flag;
-	int	nb;
-	int	nr;
-	char	b[EVENTSIZE*UTFmax+1];
-	Rune	r[EVENTSIZE+1];
-};
-
-struct Window
-{
-	/* file descriptors */
-	int		ctl;
-	int		event;
-	int		addr;
-	int		data;
-	Biobuf	*body;
-
-	/* event input */
-	char		buf[512];
-	char		*bufp;
-	int		nbuf;
-	Event	e[NEVENT];
-
-	int		id;
-	int		open;
-	Channel	*cevent;
-};
-
-struct Message
-{
-	Window	*w;
-	int		ctlfd;
-	char		*name;
-	char		*replyname;
-	uchar	opened;
-	uchar	dirty;
-	uchar	isreply;
-	uchar	deleted;
-	uchar	writebackdel;
-	uchar	tagposted;
-	uchar	recursed;
-	uchar	level;
-
-	/* header info */
-	char		*fromcolon;	/* from header file; all rest are from info file */
-	char		*from;
-	char		*to;
-	char		*cc;
-	char		*replyto;
-	char		*date;
-	char		*subject;
-	char		*type;
-	char		*disposition;
-	char		*filename;
-	char		*digest;
-
-	Message	*next;	/* next in this mailbox */
-	Message	*prev;	/* prev in this mailbox */
-	Message	*head;	/* first subpart */
-	Message	*tail;		/* last subpart */
-};
-
-enum
-{
-	NARGS		= 100,
-	NARGCHAR	= 8*1024,
-	EXECSTACK 	= STACK+(NARGS+1)*sizeof(char*)+NARGCHAR
-};
-
-struct Exec
-{
-	char		*prog;
-	char		**argv;
-	int		p[2];	/* p[1] is write to program; p[0] set to prog fd 0*/
-	int		q[2];	/* q[0] is read from program; q[1] set to prog fd 1 */
-	Channel	*sync;
-};
-
-extern	Window*	newwindow(void);
-extern	int		winopenfile(Window*, char*);
-extern	void		winopenbody(Window*, int);
-extern	void		winclosebody(Window*);
-extern	void		wintagwrite(Window*, char*, int);
-extern	void		winname(Window*, char*);
-extern	void		winwriteevent(Window*, Event*);
-extern	void		winread(Window*, uint, uint, char*);
-extern	int		windel(Window*, int);
-extern	void		wingetevent(Window*, Event*);
-extern	void		wineventproc(void*);
-extern	void		winwritebody(Window*, char*, int);
-extern	void		winclean(Window*);
-extern	int		winselect(Window*, char*, int);
-extern	char*	winselection(Window*);
-extern	int		winsetaddr(Window*, char*, int);
-extern	char*	winreadbody(Window*, int*);
-extern	void		windormant(Window*);
-extern	void		winsetdump(Window*, char*, char*);
-
-extern	void		readmbox(Message*, char*, char*);
-extern	void		rewritembox(Window*, Message*);
-
-extern	void		mkreply(Message*, char*, char*, Plumbattr*, char*);
-extern	void		delreply(Message*);
-
-extern	int		mesgadd(Message*, char*, Dir*, char*);
-extern	void		mesgmenu(Window*, Message*);
-extern	void		mesgmenunew(Window*, Message*);
-extern	int		mesgopen(Message*, char*, char*, Message*, int, char*);
-extern	void		mesgctl(void*);
-extern	void		mesgsend(Message*);
-extern	void		mesgdel(Message*, Message*);
-extern	void		mesgmenudel(Window*, Message*, Message*);
-extern	void		mesgmenumark(Window*, char*, char*);
-extern	void		mesgmenumarkdel(Window*, Message*, Message*, int);
-extern	Message*	mesglookup(Message*, char*, char*);
-extern	Message*	mesglookupfile(Message*, char*, char*);
-extern	void		mesgfreeparts(Message*);
-
-extern	char*	readfile(char*, char*, int*);
-extern	char*	readbody(char*, char*, int*);
-extern	void		ctlprint(int, char*, ...);
-extern	void*	emalloc(uint);
-extern	void*	erealloc(void*, uint);
-extern	char*	estrdup(char*);
-extern	char*	estrstrdup(char*, char*);
-extern	char*	egrow(char*, char*, char*);
-extern	char*	eappend(char*, char*, char*);
-extern	void		error(char*, ...);
-extern	int		tokenizec(char*, char**, int, char*);
-extern	void		execproc(void*);
-
-#pragma	varargck	argpos	error	1
-#pragma	varargck	argpos	ctlprint	2
-
-extern	Window	*wbox;
-extern	Message	mbox;
-extern	Message	replies;
-extern	char		*fsname;
-extern	int		plumbsendfd;
-extern	int		plumbseemailfd;
-extern	char		*home;
-extern	char		*outgoing;
-extern	char		*mailboxdir;
-extern	char		*user;
-extern	char		deleted[];
-extern	int		wctlfd;
-extern	int		shortmenu;
--- a/acme/mail/src/html.c
+++ /dev/null
@@ -1,75 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <thread.h>
-#include <ctype.h>
-#include <plumb.h>
-#include "dat.h"
-
-
-char*
-formathtml(char *body, int *np)
-{
-	int i, j, p[2], q[2];
-	Exec *e;
-	char buf[1024];
-	Channel *sync;
-
-	e = emalloc(sizeof(struct Exec));
-	if(pipe(p) < 0 || pipe(q) < 0)
-		error("can't create pipe: %r");
-
-	e->p[0] = p[0];
-	e->p[1] = p[1];
-	e->q[0] = q[0];
-	e->q[1] = q[1];
-	e->argv = emalloc(3*sizeof(char*));
-	e->argv[0] = estrdup("htmlfmt");
-	e->argv[1] = estrdup("-cutf-8");
-	e->argv[2] = nil;
-	e->prog = "/bin/htmlfmt";
-	sync = chancreate(sizeof(int), 0);
-	e->sync = sync;
-	proccreate(execproc, e, EXECSTACK);
-	recvul(sync);
-	close(p[0]);
-	close(q[1]);
-
-	if((i=write(p[1], body, *np)) != *np){
-		fprint(2, "Mail: warning: htmlfmt failed: wrote %d of %d: %r\n", i, *np);
-		close(p[1]);
-		close(q[0]);
-		return body;
-	}
-	close(p[1]);
-
-	free(body);
-	body = nil;
-	i = 0;
-	for(;;){
-		j = read(q[0], buf, sizeof buf);
-		if(j <= 0)
-			break;
-		body = realloc(body, i+j+1);
-		if(body == nil)
-			error("realloc failed: %r");
-		memmove(body+i, buf, j);
-		i += j;
-		body[i] = '\0';
-	}
-	close(q[0]);
-
-	*np = i;
-	return body;
-}
-
-char*
-readbody(char *type, char *dir, int *np)
-{
-	char *body;
-	
-	body = readfile(dir, "body", np);
-	if(body != nil && strcmp(type, "text/html") == 0)
-		return formathtml(body, np);
-	return body;
-}
--- a/acme/mail/src/mail.c
+++ /dev/null
@@ -1,550 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <thread.h>
-#include <plumb.h>
-#include <ctype.h>
-#include "dat.h"
-
-char	*maildir = "/mail/fs/";			/* mountpoint of mail file system */
-char	*mailtermdir = "/mnt/term/mail/fs/";	/* alternate mountpoint */
-char *mboxname = "mbox";			/* mailboxdir/mboxname is mail spool file */
-char	*mailboxdir = nil;				/* nil == /mail/box/$user */
-char *fsname;						/* filesystem for mailboxdir/mboxname is at maildir/fsname */
-char	*user;
-char	*outgoing;
-
-Window	*wbox;
-Message	mbox;
-Message	replies;
-char		*home;
-int		plumbsendfd;
-int		plumbseemailfd;
-int		plumbshowmailfd;
-int		plumbsendmailfd;
-Channel	*cplumb;
-Channel	*cplumbshow;
-Channel	*cplumbsend;
-int		wctlfd;
-void		mainctl(void*);
-void		plumbproc(void*);
-void		plumbshowproc(void*);
-void		plumbsendproc(void*);
-void		plumbthread(void);
-void		plumbshowthread(void*);
-void		plumbsendthread(void*);
-
-int			shortmenu;
-
-void
-usage(void)
-{
-	fprint(2, "usage: Mail [-sS] [-o outgoing] [mailboxname [directoryname]]\n");
-	threadexitsall("usage");
-}
-
-void
-removeupasfs(void)
-{
-	char buf[256];
-
-	if(strcmp(mboxname, "mbox") == 0)
-		return;
-	snprint(buf, sizeof buf, "close %s", mboxname);
-	write(mbox.ctlfd, buf, strlen(buf));
-}
-
-int
-ismaildir(char *s)
-{
-	char buf[256];
-	Dir *d;
-	int ret;
-
-	snprint(buf, sizeof buf, "%s%s", maildir, s);
-	d = dirstat(buf);
-	if(d == nil)
-		return 0;
-	ret = d->qid.type & QTDIR;
-	free(d);
-	return ret;
-}
-
-void
-threadmain(int argc, char *argv[])
-{
-	char *s, *name;
-	char err[ERRMAX], *cmd;
-	int i, newdir;
-	Fmt fmt;
-
-	doquote = needsrcquote;
-	quotefmtinstall();
-
-	/* open these early so we won't miss notification of new mail messages while we read mbox */
-	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
-	plumbseemailfd = plumbopen("seemail", OREAD|OCEXEC);
-	plumbshowmailfd = plumbopen("showmail", OREAD|OCEXEC);
-
-	shortmenu = 0;
-	ARGBEGIN{
-	case 's':
-		shortmenu = 1;
-		break;
-	case 'S':
-		shortmenu = 2;
-		break;
-	case 'o':
-		outgoing = EARGF(usage());
-		break;
-	case 'm':
-		smprint(maildir, "%s/", EARGF(usage()));
-		break;
-	default:
-		usage();
-	}ARGEND
-
-	name = "mbox";
-
-	/* bind the terminal /mail/fs directory over the local one */
-	if(access(maildir, 0)<0 && access(mailtermdir, 0)==0)
-		bind(mailtermdir, maildir, MAFTER);
-
-	newdir = 1;
-	if(argc > 0){
-		i = strlen(argv[0]);
-		if(argc>2 || i==0)
-			usage();
-		/* see if the name is that of an existing /mail/fs directory */
-		if(argc==1 && strchr(argv[0], '/')==0 && ismaildir(argv[0])){
-			name = argv[0];
-			mboxname = eappend(estrdup(maildir), "", name);
-			newdir = 0;
-		}else{
-			if(argv[0][i-1] == '/')
-				argv[0][i-1] = '\0';
-			s = strrchr(argv[0], '/');
-			if(s == nil)
-				mboxname = estrdup(argv[0]);
-			else{
-				*s++ = '\0';
-				if(*s == '\0')
-					usage();
-				mailboxdir = argv[0];
-				mboxname = estrdup(s);
-			}
-			if(argc > 1)
-				name = argv[1];
-			else
-				name = mboxname;
-		}
-	}
-
-	user = getenv("user");
-	if(user == nil)
-		user = "none";
-	if(mailboxdir == nil)
-		mailboxdir = estrstrdup("/mail/box/", user);
-	if(outgoing == nil)
-		outgoing = estrstrdup(mailboxdir, "/outgoing");
-
-	s = estrstrdup(maildir, "ctl");
-	mbox.ctlfd = open(s, ORDWR|OCEXEC);
-	if(mbox.ctlfd < 0)
-		error("can't open %s: %r", s);
-
-	fsname = estrdup(name);
-	if(newdir && argc > 0){
-		s = emalloc(5+strlen(mailboxdir)+strlen(mboxname)+strlen(name)+10+1);
-		for(i=0; i<10; i++){
-			sprint(s, "open %s/%s %s", mailboxdir, mboxname, fsname);
-			if(write(mbox.ctlfd, s, strlen(s)) >= 0)
-				break;
-			err[0] = '\0';
-			errstr(err, sizeof err);
-			if(strstr(err, "mbox name in use") == nil)
-				error("can't create directory %s for mail: %s", name, err);
-			free(fsname);
-			fsname = emalloc(strlen(name)+10);
-			sprint(fsname, "%s-%d", name, i);
-		}
-		if(i == 10)
-			error("can't open %s/%s: %r", mailboxdir, mboxname);
-		free(s);
-	}
-
-	s = estrstrdup(fsname, "/");
-	mbox.name = estrstrdup(maildir, s);
-	mbox.level= 0;
-	readmbox(&mbox, maildir, s);
-	home = getenv("home");
-	if(home == nil)
-		home = "/";
-
-	wbox = newwindow();
-	winname(wbox, mbox.name);
-	wintagwrite(wbox, "Put Mail Delmesg ", 3+1+4+1+7+1);
-	threadcreate(mainctl, wbox, STACK);
-
-	fmtstrinit(&fmt);
-	fmtprint(&fmt, "Mail");
-	if(shortmenu)
-		fmtprint(&fmt, " -%c", "sS"[shortmenu-1]);
-	if(outgoing)
-		fmtprint(&fmt, " -o %s", outgoing);
-	fmtprint(&fmt, " %s", name);
-	cmd = fmtstrflush(&fmt);
-	if(cmd == nil)
-		sysfatal("out of memory");
-	winsetdump(wbox, "/acme/mail", cmd);
-	mbox.w = wbox;
-
-	mesgmenu(wbox, &mbox);
-	winclean(wbox);
-
-	wctlfd = open("/dev/wctl", OWRITE|OCEXEC);	/* for acme window */
-	cplumb = chancreate(sizeof(Plumbmsg*), 0);
-	cplumbshow = chancreate(sizeof(Plumbmsg*), 0);
-	if(strcmp(name, "mbox") == 0){
-		/*
-		 * Avoid creating multiple windows to send mail by only accepting
-		 * sendmail plumb messages if we're reading the main mailbox.
-		 */
-		plumbsendmailfd = plumbopen("sendmail", OREAD|OCEXEC);
-		cplumbsend = chancreate(sizeof(Plumbmsg*), 0);
-		proccreate(plumbsendproc, nil, STACK);
-		threadcreate(plumbsendthread, nil, STACK);
-	}
-	/* start plumb reader as separate proc ... */
-	proccreate(plumbproc, nil, STACK);
-	proccreate(plumbshowproc, nil, STACK);
-	threadcreate(plumbshowthread, nil, STACK);
-	/* ... and use this thread to read the messages */
-	plumbthread();
-}
-
-void
-plumbproc(void*)
-{
-	Plumbmsg *m;
-
-	threadsetname("plumbproc");
-	for(;;){
-		m = plumbrecv(plumbseemailfd);
-		sendp(cplumb, m);
-		if(m == nil)
-			threadexits(nil);
-	}
-}
-
-void
-plumbshowproc(void*)
-{
-	Plumbmsg *m;
-
-	threadsetname("plumbshowproc");
-	for(;;){
-		m = plumbrecv(plumbshowmailfd);
-		sendp(cplumbshow, m);
-		if(m == nil)
-			threadexits(nil);
-	}
-}
-
-void
-plumbsendproc(void*)
-{
-	Plumbmsg *m;
-
-	threadsetname("plumbsendproc");
-	for(;;){
-		m = plumbrecv(plumbsendmailfd);
-		sendp(cplumbsend, m);
-		if(m == nil)
-			threadexits(nil);
-	}
-}
-
-void
-newmesg(char *name, char *digest)
-{
-	Dir *d;
-
-	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
-		return;	/* message is about another mailbox */
-	if(mesglookupfile(&mbox, name, digest) != nil)
-		return;
-	d = dirstat(name);
-	if(d == nil)
-		return;
-	if(mesgadd(&mbox, mbox.name, d, digest))
-		mesgmenunew(wbox, &mbox);
-	free(d);
-}
-
-void
-showmesg(char *name, char *digest)
-{
-	char *n;
-
-	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
-		return;	/* message is about another mailbox */
-	n = estrdup(name+strlen(mbox.name));
-	if(n[strlen(n)-1] != '/')
-		n = egrow(n, "/", nil);
-	mesgopen(&mbox, mbox.name, name+strlen(mbox.name), nil, 1, digest);
-	free(n);
-}
-
-void
-delmesg(char *name, char *digest, int dodel)
-{
-	Message *m;
-
-	m = mesglookupfile(&mbox, name, digest);
-	if(m != nil){
-		mesgmenumarkdel(wbox, &mbox, m, 0);
-		if(dodel)
-			m->writebackdel = 1;
-	}
-}
-
-void
-plumbthread(void)
-{
-	Plumbmsg *m;
-	Plumbattr *a;
-	char *type, *digest;
-
-	threadsetname("plumbthread");
-	while((m = recvp(cplumb)) != nil){
-		a = m->attr;
-		digest = plumblookup(a, "digest");
-		type = plumblookup(a, "mailtype");
-		if(type == nil)
-			fprint(2, "Mail: plumb message with no mailtype attribute\n");
-		else if(strcmp(type, "new") == 0)
-			newmesg(m->data, digest);
-		else if(strcmp(type, "delete") == 0)
-			delmesg(m->data, digest, 0);
-		else
-			fprint(2, "Mail: unknown plumb attribute %s\n", type);
-		plumbfree(m);
-	}
-	threadexits(nil);
-}
-
-void
-plumbshowthread(void*)
-{
-	Plumbmsg *m;
-
-	threadsetname("plumbshowthread");
-	while((m = recvp(cplumbshow)) != nil){
-		showmesg(m->data, plumblookup(m->attr, "digest"));
-		plumbfree(m);
-	}
-	threadexits(nil);
-}
-
-void
-plumbsendthread(void*)
-{
-	Plumbmsg *m;
-
-	threadsetname("plumbsendthread");
-	while((m = recvp(cplumbsend)) != nil){
-		mkreply(nil, "Mail", m->data, m->attr, nil);
-		plumbfree(m);
-	}
-	threadexits(nil);
-}
-
-int
-mboxcommand(Window *w, char *s)
-{
-	char *args[10], **targs;
-	Message *m, *next;
-	int ok, nargs, i, j;
-	char buf[128];
-
-	nargs = tokenize(s, args, nelem(args));
-	if(nargs == 0)
-		return 0;
-	if(strcmp(args[0], "Mail") == 0){
-		if(nargs == 1)
-			mkreply(nil, "Mail", "", nil, nil);
-		else
-			mkreply(nil, "Mail", args[1], nil, nil);
-		return 1;
-	}
-	if(strcmp(s, "Del") == 0){
-		if(mbox.dirty){
-			mbox.dirty = 0;
-			fprint(2, "mail: mailbox not written\n");
-			return 1;
-		}
-		ok = 1;
-		for(m=mbox.head; m!=nil; m=next){
-			next = m->next;
-			if(m->w){
-				if(windel(m->w, 0))
-					m->w = nil;
-				else
-					ok = 0;
-			}
-		}
-		for(m=replies.head; m!=nil; m=next){
-			next = m->next;
-			if(m->w){
-				if(windel(m->w, 0))
-					m->w = nil;
-				else
-					ok = 0;
-			}
-		}
-		if(ok){
-			windel(w, 1);
-			removeupasfs();
-			threadexitsall(nil);
-		}
-		return 1;
-	}
-	if(strcmp(s, "Put") == 0){
-		rewritembox(wbox, &mbox);
-		return 1;
-	}
-	if(strcmp(s, "Delmesg") == 0){
-		if(nargs > 1){
-			for(i=1; i<nargs; i++){
-				snprint(buf, sizeof buf, "%s%s", mbox.name, args[i]);
-				delmesg(buf, nil, 1);
-			}
-		}
-		s = winselection(w);
-		if(s == nil)
-			return 1;
-		nargs = 1;
-		for(i=0; s[i]; i++)
-			if(s[i] == '\n')
-				nargs++;
-		targs = emalloc(nargs*sizeof(char*));	/* could be too many for a local array */
-		nargs = getfields(s, targs, nargs, 1, "\n");
-		for(i=0; i<nargs; i++){
-			if(!isdigit(targs[i][0]))
-				continue;
-			j = atoi(targs[i]);	/* easy way to parse the number! */
-			if(j == 0)
-				continue;
-			snprint(buf, sizeof buf, "%s%d", mbox.name, j);
-			delmesg(buf, nil, 1);
-		}
-		free(s);
-		free(targs);
-		return 1;
-	}
-	return 0;
-}
-
-void
-mainctl(void *v)
-{
-	Window *w;
-	Event *e, *e2, *eq, *ea;
-	int na, nopen;
-	char *s, *t, *buf;
-
-	w = v;
-	proccreate(wineventproc, w, STACK);
-
-	for(;;){
-		e = recvp(w->cevent);
-		switch(e->c1){
-		default:
-		Unknown:
-			print("unknown message %c%c\n", e->c1, e->c2);
-			break;
-	
-		case 'E':	/* write to body; can't affect us */
-			break;
-	
-		case 'F':	/* generated by our actions; ignore */
-			break;
-	
-		case 'K':	/* type away; we don't care */
-			break;
-	
-		case 'M':
-			switch(e->c2){
-			case 'x':
-			case 'X':
-				ea = nil;
-				e2 = nil;
-				if(e->flag & 2)
-					e2 = recvp(w->cevent);
-				if(e->flag & 8){
-					ea = recvp(w->cevent);
-					na = ea->nb;
-					recvp(w->cevent);
-				}else
-					na = 0;
-				s = e->b;
-				/* if it's a known command, do it */
-				if((e->flag&2) && e->nb==0)
-					s = e2->b;
-				if(na){
-					t = emalloc(strlen(s)+1+na+1);
-					sprint(t, "%s %s", s, ea->b);
-					s = t;
-				}
-				/* if it's a long message, it can't be for us anyway */
-				if(!mboxcommand(w, s))	/* send it back */
-					winwriteevent(w, e);
-				if(na)
-					free(s);
-				break;
-	
-			case 'l':
-			case 'L':
-				buf = nil;
-				eq = e;
-				if(e->flag & 2){
-					e2 = recvp(w->cevent);
-					eq = e2;
-				}
-				s = eq->b;
-				if(eq->q1>eq->q0 && eq->nb==0){
-					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
-					winread(w, eq->q0, eq->q1, buf);
-					s = buf;
-				}
-				nopen = 0;
-				do{
-					/* skip 'deleted' string if present' */
-					if(strncmp(s, deleted, strlen(deleted)) == 0)
-						s += strlen(deleted);
-					/* skip mail box name if present */
-					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
-						s += strlen(mbox.name);
-					nopen += mesgopen(&mbox, mbox.name, s, nil, 0, nil);
-					while(*s!='\0' && *s++!='\n')
-						;
-				}while(*s);
-				if(nopen == 0)	/* send it back */
-					winwriteevent(w, e);
-				free(buf);
-				break;
-	
-			case 'I':	/* modify away; we don't care */
-			case 'D':
-			case 'd':
-			case 'i':
-				break;
-	
-			default:
-				goto Unknown;
-			}
-		}
-	}
-}
-
--- a/acme/mail/src/mesg.c
+++ /dev/null
@@ -1,1322 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <thread.h>
-#include <ctype.h>
-#include <plumb.h>
-#include "dat.h"
-
-enum
-{
-	DIRCHUNK = 32*sizeof(Dir)
-};
-
-char	regexchars[] = "\\/[].+?()*^$";
-char	deleted[] = "(deleted)-";
-char	deletedrx[] = "\\(deleted\\)-";
-char	deletedrx01[] = "(\\(deleted\\)-)?";
-char	deletedaddr[] = "-#0;/^\\(deleted\\)-/";
-
-struct{
-	char	*type;
-	char	*port;
-	char *suffix;
-} ports[] = {
-	"text/",			"edit",		".txt",
-	/* text must be first for plumbport() */
-	"image/gif",			"image",	".gif",
-	"image/jpeg",			"image",	".jpg",
-	"image/jpeg",			"image",	".jpeg",
-	"image/png",			"image",	".png",
-	"image/tiff",			"image",	".tif",
-	"application/postscript",	"postscript",	".ps",
-	"application/pdf",		"postscript",	".pdf",
-	"application/msword",		"msword",	".doc",
-	"application/rtf",		"msword",	".rtf",
-	"audio/x-wav",			"wav",		".wav",
-	nil,	nil
-};
-
-char *goodtypes[] = {
-	"text",
-	"text/plain",
-	"message/rfc822",
-	"text/richtext",
-	"text/tab-separated-values",
-	"application/octet-stream",
-	nil,
-};
-
-struct{
-	char *type;
-	char	*ext;
-} exts[] = {
-	"image/gif",	".gif",
-	"image/jpeg",	".jpg",
-	nil, nil
-};
-
-char *okheaders[] =
-{
-	"From:",
-	"Date:",
-	"To:",
-	"CC:",
-	"Subject:",
-	nil
-};
-
-char *extraheaders[] =
-{
-	"Resent-From:",
-	"Resent-To:",
-	"Sort:",
-	nil,
-};
-
-char*
-line(char *data, char **pp)
-{
-	char *p, *q;
-
-	for(p=data; *p!='\0' && *p!='\n'; p++)
-		;
-	if(*p == '\n')
-		*pp = p+1;
-	else
-		*pp = p;
-	q = emalloc(p-data + 1);
-	memmove(q, data, p-data);
-	return q;
-}
-
-void
-scanheaders(Message *m, char *dir)
-{
-	char *s, *t, *u, *f;
-
-	s = f = readfile(dir, "header", nil);
-	if(s != nil)
-		while(*s){
-			t = line(s, &s);
-			if(strncmp(t, "From: ", 6) == 0){
-				m->fromcolon = estrdup(t+6);
-				/* remove all quotes; they're ugly and irregular */
-				for(u=m->fromcolon; *u; u++)
-					if(*u == '"')
-						memmove(u, u+1, strlen(u));
-			}
-			if(strncmp(t, "Subject: ", 9) == 0)
-				m->subject = estrdup(t+9);
-			free(t);
-		}
-	if(m->fromcolon == nil)
-		m->fromcolon = estrdup(m->from);
-	free(f);
-}
-
-int
-loadinfo(Message *m, char *dir)
-{
-	int n;
-	char *data, *p, *s;
-
-	data = readfile(dir, "info", &n);
-	if(data == nil)
-		return 0;
-	m->from = line(data, &p);
-	scanheaders(m, dir);	/* depends on m->from being set */
-	m->to = line(p, &p);
-	m->cc = line(p, &p);
-	m->replyto = line(p, &p);
-	m->date = line(p, &p);
-	s = line(p, &p);
-	if(m->subject == nil)
-		m->subject = s;
-	else
-		free(s);
-	m->type = line(p, &p);
-	m->disposition = line(p, &p);
-	m->filename = line(p, &p);
-	m->digest = line(p, &p);
-	free(data);
-	return 1;
-}
-
-int
-isnumeric(char *s)
-{
-	while(*s){
-		if(!isdigit(*s))
-			return 0;
-		s++;
-	}
-	return 1;
-}
-
-Dir*
-loaddir(char *name, int *np)
-{
-	int fd;
-	Dir *dp;
-
-	fd = open(name, OREAD);
-	if(fd < 0)
-		return nil;
-	*np = dirreadall(fd, &dp);
-	close(fd);
-	return dp;
-}
-
-void
-readmbox(Message *mbox, char *dir, char *subdir)
-{
-	char *name;
-	Dir *d, *dirp;
-	int i, n;
-
-	name = estrstrdup(dir, subdir);
-	dirp = loaddir(name, &n);
-	mbox->recursed = 1;
-	if(dirp)
-		for(i=0; i<n; i++){
-			d = &dirp[i];
-			if(isnumeric(d->name))
-				mesgadd(mbox, name, d, nil);
-		}
-	free(dirp);
-	free(name);
-}
-
-/* add message to box, in increasing numerical order */
-int
-mesgadd(Message *mbox, char *dir, Dir *d, char *digest)
-{
-	Message *m;
-	char *name;
-	int loaded;
-
-	m = emalloc(sizeof(Message));
-	m->name = estrstrdup(d->name, "/");
-	m->next = nil;
-	m->prev = mbox->tail;
-	m->level= mbox->level+1;
-	m->recursed = 0;
-	name = estrstrdup(dir, m->name);
-	loaded = loadinfo(m, name);
-	free(name);
-	/* if two upas/fs are running, we can get misled, so check digest before accepting message */
-	if(loaded==0 || (digest!=nil && m->digest!=nil && strcmp(digest, m->digest)!=0)){
-		mesgfreeparts(m);
-		free(m);
-		return 0;
-	}
-	if(mbox->tail != nil)
-		mbox->tail->next = m;
-	mbox->tail = m;
-	if(mbox->head == nil)
-		mbox->head = m;
-
-	if (m->level != 1){
-		m->recursed = 1;
-		readmbox(m, dir, m->name); 
-	}
-	return 1;
-}
-
-int
-thisyear(char *year)
-{
-	static char now[10];
-	char *s;
-
-	if(now[0] == '\0'){
-		s = ctime(time(nil));
-		strcpy(now, s+24);
-	}
-	return strncmp(year, now, 4) == 0;
-}
-
-char*
-stripdate(char *as)
-{
-	int n;
-	char *s, *fld[10];
-
-	as = estrdup(as);
-	s = estrdup(as);
-	n = tokenize(s, fld, 10);
-	if(n > 5){
-		sprint(as, "%.3s ", fld[0]);	/* day */
-		/* some dates have 19 Apr, some Apr 19 */
-		if(strlen(fld[1])<4 && isnumeric(fld[1]))
-			sprint(as+strlen(as), "%.3s %.3s ", fld[1], fld[2]);	/* date, month */
-		else
-			sprint(as+strlen(as), "%.3s %.3s ", fld[2], fld[1]);	/* date, month */
-		/* do we use time or year?  depends on whether year matches this one */
-		if(thisyear(fld[5])){
-			if(strchr(fld[3], ':') != nil)
-				sprint(as+strlen(as), "%.5s ", fld[3]);	/* time */
-			else if(strchr(fld[4], ':') != nil)
-				sprint(as+strlen(as), "%.5s ", fld[4]);	/* time */
-		}else
-			sprint(as+strlen(as), "%.4s ", fld[5]);	/* year */
-	}
-	free(s);
-	return as;
-}
-
-char*
-readfile(char *dir, char *name, int *np)
-{
-	char *file, *data;
-	int fd, len;
-	Dir *d;
-
-	if(np != nil)
-		*np = 0;
-	file = estrstrdup(dir, name);
-	fd = open(file, OREAD);
-	if(fd < 0)
-		return nil;
-	d = dirfstat(fd);
-	free(file);
-	len = 0;
-	if(d != nil)
-		len = d->length;
-	free(d);
-	data = emalloc(len+1);
-	read(fd, data, len);
-	close(fd);
-	if(np != nil)
-		*np = len;
-	return data;
-}
-
-char*
-info(Message *m, int ind, int ogf)
-{
-	char *i;
-	int j, len, lens;
-	char *p;
-	char fmt[80], s[80];
-
-	if (ogf)
-		p=m->to;
-	else
-		p=m->fromcolon;
-
-	if(ind==0 && shortmenu){
-		len = 30;
-		lens = 30;
-		if(shortmenu > 1){
-			len = 10;
-			lens = 25;
-		}
-		if(ind==0 && m->subject[0]=='\0'){
-			snprint(fmt, sizeof fmt, " %%-%d.%ds", len, len);
-			snprint(s, sizeof s, fmt, p);
-		}else{
-			snprint(fmt, sizeof fmt, " %%-%d.%ds  %%-%d.%ds", len, len, lens, lens);
-			snprint(s, sizeof s, fmt, p, m->subject);
-		}
-		i = estrdup(s);
-
-		return i;
-	} 
-
-	i = estrdup("");
-	i = eappend(i, "\t", p);
-	i = egrow(i, "\t", stripdate(m->date));
-	if(ind == 0){
-		if(strcmp(m->type, "text")!=0 && strncmp(m->type, "text/", 5)!=0 && 
-		   strncmp(m->type, "multipart/", 10)!=0)
-			i = egrow(i, "\t(", estrstrdup(m->type, ")"));
-	}else if(strncmp(m->type, "multipart/", 10) != 0)
-		i = egrow(i, "\t(", estrstrdup(m->type, ")"));
-	if(m->subject[0] != '\0'){
-		i = eappend(i, "\n", nil);
-		for(j=0; j<ind; j++)
-			i = eappend(i, "\t", nil);
-		i = eappend(i, "\t", m->subject);
-	}
-	return i;
-}
-
-void
-mesgmenu0(Window *w, Message *mbox, char *realdir, char *dir, int ind, Biobuf *fd, int onlyone, int dotail)
-{
-	int i;
-	Message *m;
-	char *name, *tmp;
-	int ogf=0;
-
-	if(strstr(realdir, "outgoing") != nil)
-		ogf=1;
-
-	/* show mail box in reverse order, pieces in forward order */
-	if(ind > 0)
-		m = mbox->head;
-	else
-		m = mbox->tail;
-	while(m != nil){
-		for(i=0; i<ind; i++)
-			Bprint(fd, "\t");
-		if(ind != 0)
-			Bprint(fd, "  ");
-		name = estrstrdup(dir, m->name);
-		tmp = info(m, ind, ogf);
-		Bprint(fd, "%s%s\n", name, tmp);
-		free(tmp);
-		if(dotail && m->tail)
-			mesgmenu0(w, m, realdir, name, ind+1, fd, 0, dotail);
-		free(name);
-		if(ind)
-			m = m->next;
-		else
-			m = m->prev;
-		if(onlyone)
-			m = nil;
-	}
-}
-
-void
-mesgmenu(Window *w, Message *mbox)
-{
-	winopenbody(w, OWRITE);
-	mesgmenu0(w, mbox, mbox->name, "", 0, w->body, 0, !shortmenu);
-	winclosebody(w);
-}
-
-/* one new message has arrived, as mbox->tail */
-void
-mesgmenunew(Window *w, Message *mbox)
-{
-	Biobuf *b;
-
-	winselect(w, "0", 0);
-	w->data = winopenfile(w, "data");
-	b = emalloc(sizeof(Biobuf));
-	Binit(b, w->data, OWRITE);
-	mesgmenu0(w, mbox, mbox->name, "", 0, b, 1, !shortmenu);
-	Bterm(b);
-	free(b);
-	if(!mbox->dirty)
-		winclean(w);
-	/* select tag line plus following indented lines, but not final newline (it's distinctive) */
-	winselect(w, "0/.*\\n((\t.*\\n)*\t.*)?/", 1);
-	close(w->addr);
-	close(w->data);
-	w->addr = -1;
-	w->data = -1;
-}
-
-char*
-name2regexp(char *prefix, char *s)
-{
-	char *buf, *p, *q;
-
-	buf = emalloc(strlen(prefix)+2*strlen(s)+50);	/* leave room to append more */
-	p = buf;
-	*p++ = '0';
-	*p++ = '/';
-	*p++ = '^';
-	strcpy(p, prefix);
-	p += strlen(prefix);
-	for(q=s; *q!='\0'; q++){
-		if(strchr(regexchars, *q) != nil)
-			*p++ = '\\';
-		*p++ = *q;
-	}
-	*p++ = '/';
-	*p = '\0';
-	return buf;
-}
-
-void
-mesgmenumarkdel(Window *w, Message *mbox, Message *m, int writeback)
-{
-	char *buf;
-
-
-	if(m->deleted)
-		return;
-	m->writebackdel = writeback;
-	if(w->data < 0)
-		w->data = winopenfile(w, "data");
-	buf = name2regexp("", m->name);
-	strcat(buf, "-#0");
-	if(winselect(w, buf, 1))
-		write(w->data, deleted, 10);
-	free(buf);
-	close(w->data);
-	close(w->addr);
-	w->addr = w->data = -1;
-	mbox->dirty = 1;
-	m->deleted = 1;
-}
-
-void
-mesgmenumarkundel(Window *w, Message*, Message *m)
-{
-	char *buf;
-
-	if(m->deleted == 0)
-		return;
-	if(w->data < 0)
-		w->data = winopenfile(w, "data");
-	buf = name2regexp(deletedrx, m->name);
-	if(winselect(w, buf, 1))
-		if(winsetaddr(w, deletedaddr, 1))
-			write(w->data, "", 0);
-	free(buf);
-	close(w->data);
-	close(w->addr);
-	w->addr = w->data = -1;
-	m->deleted = 0;
-}
-
-void
-mesgmenudel(Window *w, Message *mbox, Message *m)
-{
-	char *buf;
-
-	if(w->data < 0)
-		w->data = winopenfile(w, "data");
-	buf = name2regexp(deletedrx, m->name);
-	if(winsetaddr(w, buf, 1) && winsetaddr(w, ".,./.*\\n(\t.*\\n)*/", 1))
-		write(w->data, "", 0);
-	free(buf);
-	close(w->data);
-	close(w->addr);
-	w->addr = w->data = -1;
-	mbox->dirty = 1;
-	m->deleted = 1;
-}
-
-void
-mesgmenumark(Window *w, char *which, char *mark)
-{
-	char *buf;
-
-	if(w->data < 0)
-		w->data = winopenfile(w, "data");
-	buf = name2regexp(deletedrx01, which);
-	if(winsetaddr(w, buf, 1) && winsetaddr(w, "+0-#1", 1))	/* go to end of line */
-		write(w->data, mark, strlen(mark));
-	free(buf);
-	close(w->data);
-	close(w->addr);
-	w->addr = w->data = -1;
-	if(!mbox.dirty)
-		winclean(w);
-}
-
-void
-mesgfreeparts(Message *m)
-{
-	free(m->name);
-	free(m->replyname);
-	free(m->fromcolon);
-	free(m->from);
-	free(m->to);
-	free(m->cc);
-	free(m->replyto);
-	free(m->date);
-	free(m->subject);
-	free(m->type);
-	free(m->disposition);
-	free(m->filename);
-	free(m->digest);
-}
-
-void
-mesgdel(Message *mbox, Message *m)
-{
-	Message *n, *next;
-
-	if(m->opened)
-		error("internal error: deleted message still open in mesgdel");
-	/* delete subparts */
-	for(n=m->head; n!=nil; n=next){
-		next = n->next;
-		mesgdel(m, n);
-	}
-	/* remove this message from list */
-	if(m->next)
-		m->next->prev = m->prev;
-	else
-		mbox->tail = m->prev;
-	if(m->prev)
-		m->prev->next = m->next;
-	else
-		mbox->head = m->next;
-
-	mesgfreeparts(m);
-}
-
-int
-mesgsave(Message *m, char *s)
-{
-	int ofd, n, k, ret;
-	char *t, *raw, *unixheader, *all;
-
-	t = estrstrdup(mbox.name, m->name);
-	raw = readfile(t, "raw", &n);
-	unixheader = readfile(t, "unixheader", &k);
-	if(raw==nil || unixheader==nil){
-		fprint(2, "Mail: can't read %s: %r\n", t);
-		free(t);
-		return 0;
-	}
-	free(t);
-
-	all = emalloc(n+k+1);
-	memmove(all, unixheader, k);
-	memmove(all+k, raw, n);
-	memmove(all+k+n, "\n", 1);
-	n = k+n+1;
-	free(unixheader);
-	free(raw);
-	ret = 1;
-	s = estrdup(s);
-	if(s[0] != '/')
-		s = egrow(estrdup(mailboxdir), "/", s);
-	ofd = open(s, OWRITE);
-	if(ofd < 0){
-		fprint(2, "Mail: can't open %s: %r\n", s);
-		ret = 0;
-	}else if(seek(ofd, 0LL, 2)<0 || write(ofd, all, n)!=n){
-		fprint(2, "Mail: save failed: can't write %s: %r\n", s);
-		ret = 0;
-	}
-	free(all);
-	close(ofd);
-	free(s);
-	return ret;
-}
-
-int
-mesgcommand(Message *m, char *cmd)
-{
-	char *s;
-	char *args[10];
-	int ok, ret, nargs;
-
-	s = cmd;
-	ret = 1;
-	nargs = tokenize(s, args, nelem(args));
-	if(nargs == 0)
-		return 0;
-	if(strcmp(args[0], "Post") == 0){
-		mesgsend(m);
-		goto Return;
-	}
-	if(strncmp(args[0], "Save", 4) == 0){
-		if(m->isreply)
-			goto Return;
-		s = estrdup("\t[saved");
-		if(nargs==1 || strcmp(args[1], "")==0){
-			ok = mesgsave(m, "stored");
-		}else{
-			ok = mesgsave(m, args[1]);
-			s = eappend(s, " ", args[1]);
-		}
-		if(ok){
-			s = egrow(s, "]", nil);
-			mesgmenumark(mbox.w, m->name, s);
-		}
-		free(s);
-		goto Return;
-	}
-	if(strcmp(args[0], "Reply")==0){
-		if(nargs>=2 && strcmp(args[1], "all")==0)
-			mkreply(m, "Replyall", nil, nil, nil);
-		else
-			mkreply(m, "Reply", nil, nil, nil);
-		goto Return;
-	}
-	if(strcmp(args[0], "Q") == 0){
-		s = winselection(m->w);	/* will be freed by mkreply */
-		if(nargs>=3 && strcmp(args[1], "Reply")==0 && strcmp(args[2], "all")==0)
-			mkreply(m, "QReplyall", nil, nil, s);
-		else
-			mkreply(m, "QReply", nil, nil, s);
-		goto Return;
-	}
-	if(strcmp(args[0], "Del") == 0){
-		if(windel(m->w, 0)){
-			chanfree(m->w->cevent);
-			free(m->w);
-			m->w = nil;
-			if(m->isreply)
-				delreply(m);
-			else{
-				m->opened = 0;
-				m->tagposted = 0;
-			}
-			free(cmd);
-			threadexits(nil);
-		}
-		goto Return;
-	}
-	if(strcmp(args[0], "Delmesg") == 0){
-		if(!m->isreply){
-			mesgmenumarkdel(wbox, &mbox, m, 1);
-			free(cmd);	/* mesgcommand might not return */
-			mesgcommand(m, estrdup("Del"));
-			return 1;
-		}
-		goto Return;
-	}
-	if(strcmp(args[0], "UnDelmesg") == 0){
-		if(!m->isreply && m->deleted)
-			mesgmenumarkundel(wbox, &mbox, m);
-		goto Return;
-	}
-//	if(strcmp(args[0], "Headers") == 0){
-//		m->showheaders();
-//		return True;
-//	}
-
-	ret = 0;
-
-    Return:
-	free(cmd);
-	return ret;
-}
-
-void
-mesgtagpost(Message *m)
-{
-	if(m->tagposted)
-		return;
-	wintagwrite(m->w, " Post", 5);
-	m->tagposted = 1;
-}
-
-/* need to expand selection more than default word */
-#pragma varargck argpos eval 2
-
-long
-eval(Window *w, char *s, ...)
-{
-	char buf[64];
-	va_list arg;
-
-	va_start(arg, s);
-	vsnprint(buf, sizeof buf, s, arg);
-	va_end(arg);
-
-	if(winsetaddr(w, buf, 1)==0)
-		return -1;
-
-	if(pread(w->addr, buf, 24, 0) != 24)
-		return -1;
-	return strtol(buf, 0, 10);
-}
-
-int
-isemail(char *s)
-{
-	int nat;
-
-	nat = 0;
-	for(; *s; s++)
-		if(*s == '@')
-			nat++;
-		else if(!isalpha(*s) && !isdigit(*s) && !strchr("_.-+/", *s))
-			return 0;
-	return nat==1;
-}
-
-char addrdelim[] =  "/[ \t\\n<>()\\[\\]]/";
-char*
-expandaddr(Window *w, Event *e)
-{
-	char *s;
-	long q0, q1;
-
-	if(e->q0 != e->q1)	/* cannot happen */
-		return nil;
-
-	q0 = eval(w, "#%d-%s", e->q0, addrdelim);
-	if(q0 == -1)	/* bad char not found */
-		q0 = 0;
-	else			/* increment past bad char */
-		q0++;
-
-	q1 = eval(w, "#%d+%s", e->q0, addrdelim);
-	if(q1 < 0){
-		q1 = eval(w, "$");
-		if(q1 < 0)
-			return nil;
-	}
-	if(q0 >= q1)
-		return nil;
-	s = emalloc((q1-q0)*UTFmax+1);
-	winread(w, q0, q1, s);
-	return s;
-}
-
-int
-replytoaddr(Window *w, Message *m, Event *e, char *s)
-{
-	int did;
-	char *buf;
-	Plumbmsg *pm;
-
-	buf = nil;
-	did = 0;
-	if(e->flag & 2){
-		/* autoexpanded; use our own bigger expansion */
-		buf = expandaddr(w, e);
-		if(buf == nil)
-			return 0;
-		s = buf;
-	}
-	if(isemail(s)){
-		did = 1;
-		pm = emalloc(sizeof(Plumbmsg));
-		pm->src = estrdup("Mail");
-		pm->dst = estrdup("sendmail");
-		pm->data = estrdup(s);
-		pm->ndata = -1;
-		if(m->subject && m->subject[0]){
-			pm->attr = emalloc(sizeof(Plumbattr));
-			pm->attr->name = estrdup("Subject");
-			if(tolower(m->subject[0]) != 'r' || tolower(m->subject[1]) != 'e' || m->subject[2] != ':')
-				pm->attr->value = estrstrdup("Re: ", m->subject);
-			else
-				pm->attr->value = estrdup(m->subject);
-			pm->attr->next = nil;
-		}
-		if(plumbsend(plumbsendfd, pm) < 0)
-			fprint(2, "error writing plumb message: %r\n");
-		plumbfree(pm);
-	}
-	free(buf);
-	return did;
-}
-
-
-void
-mesgctl(void *v)
-{
-	Message *m;
-	Window *w;
-	Event *e, *eq, *e2, *ea;
-	int na, nopen, i, j;
-	char *os, *s, *t, *buf;
-
-	m = v;
-	w = m->w;
-	threadsetname("mesgctl");
-	proccreate(wineventproc, w, STACK);
-	for(;;){
-		e = recvp(w->cevent);
-		switch(e->c1){
-		default:
-		Unk:
-			print("unknown message %c%c\n", e->c1, e->c2);
-			break;
-
-		case 'E':	/* write to body; can't affect us */
-			break;
-
-		case 'F':	/* generated by our actions; ignore */
-			break;
-
-		case 'K':	/* type away; we don't care */
-		case 'M':
-			switch(e->c2){
-			case 'x':	/* mouse only */
-			case 'X':
-				ea = nil;
-				eq = e;
-				if(e->flag & 2){
-					e2 = recvp(w->cevent);
-					eq = e2;
-				}
-				if(e->flag & 8){
-					ea = recvp(w->cevent);
-					recvp(w->cevent);
-					na = ea->nb;
-				}else
-					na = 0;
-				if(eq->q1>eq->q0 && eq->nb==0){
-					s = emalloc((eq->q1-eq->q0)*UTFmax+1);
-					winread(w, eq->q0, eq->q1, s);
-				}else
-					s = estrdup(eq->b);
-				if(na){
-					t = emalloc(strlen(s)+1+na+1);
-					sprint(t, "%s %s", s, ea->b);
-					free(s);
-					s = t;
-				}
-				if(!mesgcommand(m, s))	/* send it back */
-					winwriteevent(w, e);
-				break;
-
-			case 'l':	/* mouse only */
-			case 'L':
-				buf = nil;
-				eq = e;
-				if(e->flag & 2){
-					e2 = recvp(w->cevent);
-					eq = e2;
-				}
-				s = eq->b;
-				if(eq->q1>eq->q0 && eq->nb==0){
-					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
-					winread(w, eq->q0, eq->q1, buf);
-					s = buf;
-				}
-				os = s;
-				nopen = 0;
-				do{
-					/* skip mail box name if present */
-					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
-						s += strlen(mbox.name);
-					if(strstr(s, "body") != nil){
-						/* strip any known extensions */
-						for(i=0; exts[i].ext!=nil; i++){
-							j = strlen(exts[i].ext);
-							if(strlen(s)>j && strcmp(s+strlen(s)-j, exts[i].ext)==0){
-								s[strlen(s)-j] = '\0';
-								break;
-							}
-						}
-						if(strlen(s)>5 && strcmp(s+strlen(s)-5, "/body")==0)
-							s[strlen(s)-4] = '\0';	/* leave / in place */
-					}
-					nopen += mesgopen(&mbox, mbox.name, s, m, 0, nil);
-					while(*s!=0 && *s++!='\n')
-						;
-				}while(*s);
-				if(nopen == 0 && e->c1 == 'L')
-					nopen += replytoaddr(w, m, e, os);
-				if(nopen == 0)
-					winwriteevent(w, e);
-				free(buf);
-				break;
-
-			case 'I':	/* modify away; we don't care */
-			case 'D':
-				mesgtagpost(m);
-				/* fall through */
-			case 'd':
-			case 'i':
-				break;
-
-			default:
-				goto Unk;
-			}
-		}
-	}
-}
-
-void
-mesgline(Message *m, char *header, char *value)
-{
-	if(strlen(value) > 0)
-		Bprint(m->w->body, "%s: %s\n", header, value);
-}
-
-int
-isprintable(char *type)
-{
-	int i;
-
-	for(i=0; goodtypes[i]!=nil; i++)
-		if(strcmp(type, goodtypes[i])==0)
-			return 1;
-	return 0;
-}
-
-char*
-ext(char *type)
-{
-	int i;
-
-	for(i=0; exts[i].type!=nil; i++)
-		if(strcmp(type, exts[i].type)==0)
-			return exts[i].ext;
-	return "";
-}
-
-void
-mimedisplay(Message *m, char *name, char *rootdir, Window *w, int fileonly)
-{
-	char *dest;
-
-	if(strcmp(m->disposition, "file")==0 || strlen(m->filename)!=0){
-		if(strlen(m->filename) == 0){
-			dest = estrdup(m->name);
-			dest[strlen(dest)-1] = '\0';
-		}else
-			dest = estrdup(m->filename);
-		if(m->filename[0] != '/')
-			dest = egrow(estrdup(home), "/", dest);
-		Bprint(w->body, "\tcp %s%sbody%s %q\n", rootdir, name, ext(m->type), dest);
-		free(dest);
-	}else if(!fileonly)
-		Bprint(w->body, "\tfile is %s%sbody%s\n", rootdir, name, ext(m->type));
-}
-
-void
-printheader(char *dir, Biobuf *b, char **okheaders)
-{
-	char *s;
-	char *lines[100];
-	int i, j, n;
-
-	s = readfile(dir, "header", nil);
-	if(s == nil)
-		return;
-	n = getfields(s, lines, nelem(lines), 0, "\n");
-	for(i=0; i<n; i++)
-		for(j=0; okheaders[j]; j++)
-			if(cistrncmp(lines[i], okheaders[j], strlen(okheaders[j])) == 0)
-				Bprint(b, "%s\n", lines[i]);
-	free(s);
-}
-
-void
-mesgload(Message *m, char *rootdir, char *file, Window *w)
-{
-	char *s, *subdir, *name, *dir;
-	Message *mp, *thisone;
-	int n;
-
-	dir = estrstrdup(rootdir, file);
-
-	if(strcmp(m->type, "message/rfc822") != 0){	/* suppress headers of envelopes */
-		if(strlen(m->from) > 0){
-			Bprint(w->body, "From: %s\n", m->from);
-			mesgline(m, "Date", m->date);
-			mesgline(m, "To", m->to);
-			mesgline(m, "CC", m->cc);
-			mesgline(m, "Subject", m->subject);
-			printheader(dir, w->body, extraheaders);
-		}else{
-			printheader(dir, w->body, okheaders);
-			printheader(dir, w->body, extraheaders);
-		}
-		Bprint(w->body, "\n");
-	}
-
-	if(m->level == 1 && m->recursed == 0){
-		m->recursed = 1;
-		readmbox(m, rootdir, m->name);
-	}
-	if(m->head == nil){	/* single part message */
-		if(strcmp(m->type, "text")==0 || strncmp(m->type, "text/", 5)==0){
-			mimedisplay(m, m->name, rootdir, w, 1);
-			s = readbody(m->type, dir, &n);
-			winwritebody(w, s, n);
-			free(s);
-		}else
-			mimedisplay(m, m->name, rootdir, w, 0);
-	}else{
-		/* multi-part message, either multipart/* or message/rfc822 */
-		thisone = nil;
-		if(strcmp(m->type, "multipart/alternative") == 0){
-			thisone = m->head;	/* in case we can't find a good one */
-			for(mp=m->head; mp!=nil; mp=mp->next)
-				if(isprintable(mp->type)){
-					thisone = mp;
-					break;
-				}
-		}
-		for(mp=m->head; mp!=nil; mp=mp->next){
-			if(thisone!=nil && mp!=thisone)
-				continue;
-			subdir = estrstrdup(dir, mp->name);
-			name = estrstrdup(file, mp->name);
-			/* skip first element in name because it's already in window name */
-			if(mp != m->head)
-				Bprint(w->body, "\n===> %s (%s) [%s]\n", strchr(name, '/')+1, mp->type, mp->disposition);
-			if(strcmp(mp->type, "text")==0 || strncmp(mp->type, "text/", 5)==0){
-				mimedisplay(mp, name, rootdir, w, 1);
-				printheader(subdir, w->body, okheaders);
-				printheader(subdir, w->body, extraheaders);
-				winwritebody(w, "\n", 1);
-				s = readbody(mp->type, subdir, &n);
-				winwritebody(w, s, n);
-				free(s);
-			}else{
-				if(strncmp(mp->type, "multipart/", 10)==0 || strcmp(mp->type, "message/rfc822")==0){
-					mp->w = w;
-					mesgload(mp, rootdir, name, w);
-					mp->w = nil;
-				}else
-					mimedisplay(mp, name, rootdir, w, 0);
-			}
-			free(name);
-			free(subdir);
-		}
-	}
-	free(dir);
-}
-
-int
-tokenizec(char *str, char **args, int max, char *splitc)
-{
-	int na;
-	int intok = 0;
-
-	if(max <= 0)
-		return 0;	
-	for(na=0; *str != '\0';str++){
-		if(strchr(splitc, *str) == nil){
-			if(intok)
-				continue;
-			args[na++] = str;
-			intok = 1;
-		}else{
-			/* it's a separator/skip character */
-			*str = '\0';
-			if(intok){
-				intok = 0;
-				if(na >= max)
-					break;
-			}
-		}
-	}
-	return na;
-}
-
-Message*
-mesglookup(Message *mbox, char *name, char *digest)
-{
-	int n;
-	Message *m;
-	char *t;
-
-	if(digest){
-		/* can find exactly */
-		for(m=mbox->head; m!=nil; m=m->next)
-			if(strcmp(digest, m->digest) == 0)
-				break;
-		return m;
-	}
-
-	n = strlen(name);
-	if(n == 0)
-		return nil;
-	if(name[n-1] == '/')
-		t = estrdup(name);
-	else
-		t = estrstrdup(name, "/");
-	for(m=mbox->head; m!=nil; m=m->next)
-		if(strcmp(t, m->name) == 0)
-			break;
-	free(t);
-	return m;
-}
-
-/*
- * Find plumb port, knowing type is text, given file name (by extension)
- */
-int
-plumbportbysuffix(char *file)
-{
-	char *suf;
-	int i, nsuf, nfile;
-
-	nfile = strlen(file);
-	for(i=0; ports[i].type!=nil; i++){
-		suf = ports[i].suffix;
-		nsuf = strlen(suf);
-		if(nfile > nsuf)
-			if(cistrncmp(file+nfile-nsuf, suf, nsuf) == 0)
-				return i;
-	}
-	return 0;
-}
-
-/*
- * Find plumb port using type and file name (by extension)
- */
-int
-plumbport(char *type, char *file)
-{
-	int i;
-
-	for(i=0; ports[i].type!=nil; i++)
-		if(strncmp(type, ports[i].type, strlen(ports[i].type)) == 0)
-			return i;
-	/* see if it's a text type */
-	for(i=0; goodtypes[i]!=nil; i++)
-		if(strncmp(type, goodtypes[i], strlen(goodtypes[i])) == 0)
-			return plumbportbysuffix(file);
-	return -1;
-}
-
-void
-plumb(Message *m, char *dir)
-{
-	int i;
-	char *port;
-	Plumbmsg *pm;
-
-	if(strlen(m->type) == 0)
-		return;
-	i = plumbport(m->type, m->filename);
-	if(i < 0)
-		fprint(2, "can't find destination for message subpart\n");
-	else{
-		port = ports[i].port;
-		pm = emalloc(sizeof(Plumbmsg));
-		pm->src = estrdup("Mail");
-		if(port)
-			pm->dst = estrdup(port);
-		else
-			pm->dst = nil;
-		pm->wdir = nil;
-		pm->type = estrdup("text");
-		pm->ndata = -1;
-		pm->data = estrstrdup(dir, "body");
-		pm->data = eappend(pm->data, "", ports[i].suffix);
-		if(plumbsend(plumbsendfd, pm) < 0)
-			fprint(2, "error writing plumb message: %r\n");
-		plumbfree(pm);
-	}
-}
-
-int
-mesgopen(Message *mbox, char *dir, char *s, Message *mesg, int plumbed, char *digest)
-{
-	char *t, *u, *v;
-	Message *m;
-	char *direlem[10];
-	int i, ndirelem, reuse;
-
-	/* find white-space-delimited first word */
-	for(t=s; *t!='\0' && !isspace(*t); t++)
-		;
-	u = emalloc(t-s+1);
-	memmove(u, s, t-s);
-	/* separate it on slashes */
-	ndirelem = tokenizec(u, direlem, nelem(direlem), "/");
-	if(ndirelem <= 0){
-    Error:
-		free(u);
-		return 0;
-	}
-	if(plumbed){
-		write(wctlfd, "top", 3);
-		write(wctlfd, "current", 7);
-	}
-	/* open window for message */
-	m = mesglookup(mbox, direlem[0], digest);
-	if(m == nil)
-		goto Error;
-	if(mesg!=nil && m!=mesg)	/* string looked like subpart but isn't part of this message */
-		goto Error;
-	if(m->opened == 0){
-		if(m->w == nil){
-			reuse = 0;
-			m->w = newwindow();
-		}else{
-			reuse = 1;
-			/* re-use existing window */
-			if(winsetaddr(m->w, "0,$", 1)){
-				if(m->w->data < 0)
-					m->w->data = winopenfile(m->w, "data");
-				write(m->w->data, "", 0);
-			}
-		}
-		v = estrstrdup(mbox->name, m->name);
-		winname(m->w, v);
-		free(v);
-		if(!reuse){
-			if(m->deleted)
-				wintagwrite(m->w, "Q Reply all UnDelmesg Save ", 2+6+4+10+5);
-			else
-				wintagwrite(m->w, "Q Reply all Delmesg Save ", 2+6+4+8+5);
-		}
-		threadcreate(mesgctl, m, STACK);
-		winopenbody(m->w, OWRITE);
-		mesgload(m, dir, m->name, m->w);
-		winclosebody(m->w);
-		winclean(m->w);
-		m->opened = 1;
-		if(ndirelem == 1){
-			free(u);
-			return 1;
-		}
-	}
-	if(ndirelem == 1 && plumbport(m->type, m->filename) <= 0){
-		/* make sure dot is visible */
-		ctlprint(m->w->ctl, "show\n");
-		return 0;
-	}
-	/* walk to subpart */
-	dir = estrstrdup(dir, m->name);
-	for(i=1; i<ndirelem; i++){
-		m = mesglookup(m, direlem[i], digest);
-		if(m == nil)
-			break;
-		dir = egrow(dir, m->name, nil);
-	}
-	if(m != nil && plumbport(m->type, m->filename) > 0)
-		plumb(m, dir);
-	free(dir);
-	free(u);
-	return 1;
-}
-
-void
-rewritembox(Window *w, Message *mbox)
-{
-	Message *m, *next;
-	char *deletestr, *t;
-	int nopen;
-
-	deletestr = estrstrdup("delete ", fsname);
-
-	nopen = 0;
-	for(m=mbox->head; m!=nil; m=next){
-		next = m->next;
-		if(m->deleted == 0)
-			continue;
-		if(m->opened){
-			nopen++;
-			continue;
-		}
-		if(m->writebackdel){
-			/* messages deleted by plumb message are not removed again */
-			t = estrdup(m->name);
-			if(strlen(t) > 0)
-				t[strlen(t)-1] = '\0';
-			deletestr = egrow(deletestr, " ", t);
-		}
-		mesgmenudel(w, mbox, m);
-		mesgdel(mbox, m);
-	}
-	if(write(mbox->ctlfd, deletestr, strlen(deletestr)) < 0)
-		fprint(2, "Mail: warning: error removing mail message files: %r\n");
-	free(deletestr);
-	winselect(w, "0", 0);
-	if(nopen == 0)
-		winclean(w);
-	mbox->dirty = 0;
-}
-
-/* name is a full file name, but it might not belong to us */
-Message*
-mesglookupfile(Message *mbox, char *name, char *digest)
-{
-	int k, n;
-
-	k = strlen(name);
-	n = strlen(mbox->name);
-	if(k==0 || strncmp(name, mbox->name, n) != 0){
-//		fprint(2, "Mail: message %s not in this mailbox\n", name);
-		return nil;
-	}
-	return mesglookup(mbox, name+n, digest);
-}
--- a/acme/mail/src/mkfile
+++ /dev/null
@@ -1,30 +1,0 @@
-</$objtype/mkfile
-
-TARG=Mail
-OFILES=\
-		html.$O\
-		mail.$O\
-		mesg.$O\
-		reply.$O\
-		util.$O\
-		win.$O
-
-HFILES=dat.h
-LIB=
-
-BIN=/acme/bin/$objtype
-
-UPDATE=\
-	mkfile\
-	$HFILES\
-	${OFILES:%.$O=%.c}\
-
-</sys/src/cmd/mkone
-
-$O.out: $OFILES
-	$LD -o $target  $LDFLAGS $OFILES
-
-syms:V:
-	8c -a mail.c	>syms
-	8c -aa mesg.c reply.c util.c win.c 	>>syms
-
--- a/acme/mail/src/reply.c
+++ /dev/null
@@ -1,567 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <thread.h>
-#include <ctype.h>
-#include <plumb.h>
-#include "dat.h"
-
-static int	replyid;
-
-int
-quote(Message *m, Biobuf *b, char *dir, char *quotetext)
-{
-	char *body, *type;
-	int i, n, nlines;
-	char **lines;
-
-	if(quotetext){
-		body = quotetext;
-		n = strlen(body);
-		type = nil;
-	}else{
-		/* look for first textual component to quote */
-		type = readfile(dir, "type", &n);
-		if(type == nil){
-			print("no type in %s\n", dir);
-			return 0;
-		}
-		if(strncmp(type, "multipart/", 10)==0 || strncmp(type, "message/", 8)==0){
-			dir = estrstrdup(dir, "1/");
-			if(quote(m, b, dir, nil)){
-				free(type);
-				free(dir);
-				return 1;
-			}
-			free(dir);
-		}
-		if(strncmp(type, "text", 4) != 0){
-			free(type);
-			return 0;
-		}
-		body = readbody(m->type, dir, &n);
-		if(body == nil)
-			return 0;
-	}
-	nlines = 0;
-	for(i=0; i<n; i++)
-		if(body[i] == '\n')
-			nlines++;
-	nlines++;
-	lines = emalloc(nlines*sizeof(char*));
-	nlines = getfields(body, lines, nlines, 0, "\n");
-	/* delete leading and trailing blank lines */
-	i = 0;
-	while(i<nlines && lines[i][0]=='\0')
-		i++;
-	while(i<nlines && lines[nlines-1][0]=='\0')
-		nlines--;
-	while(i < nlines){
-		Bprint(b, ">%s%s\n", lines[i][0]=='>'? "" : " ", lines[i]);
-		i++;
-	}
-	free(lines);
-	free(body);	/* will free quotetext if non-nil */
-	free(type);
-	return 1;
-}
-
-void
-mkreply(Message *m, char *label, char *to, Plumbattr *attr, char *quotetext)
-{
-	Message *r;
-	char *dir, *t;
-	int quotereply;
-	Plumbattr *a;
-
-	quotereply = (label[0] == 'Q');
-	r = emalloc(sizeof(Message));
-	r->isreply = 1;
-	if(m != nil)
-		r->replyname = estrdup(m->name);
-	r->next = replies.head;
-	r->prev = nil;
-	if(replies.head != nil)
-		replies.head->prev = r;
-	replies.head = r;
-	if(replies.tail == nil)
-		replies.tail = r;
-	r->name = emalloc(strlen(mbox.name)+strlen(label)+10);
-	sprint(r->name, "%s%s%d", mbox.name, label, ++replyid);
-	r->w = newwindow();
-	winname(r->w, r->name);
-	ctlprint(r->w->ctl, "cleartag");
-	wintagwrite(r->w, "fmt Look Post Undo", 4+5+5+4);
-	r->tagposted = 1;
-	threadcreate(mesgctl, r, STACK);
-	winopenbody(r->w, OWRITE);
-	if(to!=nil && to[0]!='\0')
-		Bprint(r->w->body, "%s\n", to);
-	for(a=attr; a; a=a->next)
-		Bprint(r->w->body, "%s: %s\n", a->name, a->value);
-	dir = nil;
-	if(m != nil){
-		dir = estrstrdup(mbox.name, m->name);
-		if(to == nil && attr == nil){
-			/* Reply goes to replyto; Reply all goes to From and To and CC */
-			if(strstr(label, "all") == nil)
-				Bprint(r->w->body, "To: %s\n", m->replyto);
-			else{	/* Replyall */
-				if(strlen(m->from) > 0)
-					Bprint(r->w->body, "To: %s\n", m->from);
-				if(strlen(m->to) > 0)
-					Bprint(r->w->body, "To: %s\n", m->to);
-				if(strlen(m->cc) > 0)
-					Bprint(r->w->body, "CC: %s\n", m->cc);
-			}
-		}
-		if(strlen(m->subject) > 0){
-			t = "Subject: Re: ";
-			if(strlen(m->subject) >= 3)
-				if(tolower(m->subject[0])=='r' && tolower(m->subject[1])=='e' && m->subject[2]==':')
-					t = "Subject: ";
-			Bprint(r->w->body, "%s%s\n", t, m->subject);
-		}
-		if(!quotereply){
-			Bprint(r->w->body, "Include: %sraw\n", dir);
-			free(dir);
-		}
-	}
-	Bprint(r->w->body, "\n");
-	if(m == nil)
-		Bprint(r->w->body, "\n");
-	else if(quotereply){
-		quote(m, r->w->body, dir, quotetext);
-		free(dir);
-	}
-	winclosebody(r->w);
-	if(m==nil && (to==nil || to[0]=='\0'))
-		winselect(r->w, "0", 0);
-	else
-		winselect(r->w, "$", 0);
-	winclean(r->w);
-	windormant(r->w);
-}
-
-void
-delreply(Message *m)
-{
-	if(m->next == nil)
-		replies.tail = m->prev;
-	else
-		m->next->prev = m->prev;
-	if(m->prev == nil)
-		replies.head = m->next;
-	else
-		m->prev->next = m->next;
-	mesgfreeparts(m);
-	free(m);
-}
-
-/* copy argv to stack and free the incoming strings, so we don't leak argument vectors */
-void
-buildargv(char **inargv, char *argv[NARGS+1], char args[NARGCHAR])
-{
-	int i, n;
-	char *s, *a;
-
-	s = args;
-	for(i=0; i<NARGS; i++){
-		a = inargv[i];
-		if(a == nil)
-			break;
-		n = strlen(a)+1;
-		if((s-args)+n >= NARGCHAR)	/* too many characters */
-			break;
-		argv[i] = s;
-		memmove(s, a, n);
-		s += n;
-		free(a);
-	}
-	argv[i] = nil;
-}
-
-void
-execproc(void *v)
-{
-	struct Exec *e;
-	int p[2], q[2];
-	char *prog;
-	char *argv[NARGS+1], args[NARGCHAR];
-
-	e = v;
-	p[0] = e->p[0];
-	p[1] = e->p[1];
-	q[0] = e->q[0];
-	q[1] = e->q[1];
-	prog = e->prog;	/* known not to be malloc'ed */
-	rfork(RFFDG);
-	sendul(e->sync, 1);
-	buildargv(e->argv, argv, args);
-	free(e->argv);
-	chanfree(e->sync);
-	free(e);
-	dup(p[0], 0);
-	close(p[0]);
-	close(p[1]);
-	if(q[0]){
-		dup(q[1], 1);
-		close(q[0]);
-		close(q[1]);
-	}
-	procexec(nil, prog, argv);
-//fprint(2, "exec: %s", e->prog);
-//{int i;
-//for(i=0; argv[i]; i++) print(" '%s'", argv[i]);
-//print("\n");
-//}
-//argv[0] = "cat";
-//argv[1] = nil;
-//procexec(nil, "/bin/cat", argv);
-	fprint(2, "Mail: can't exec %s: %r\n", prog);
-	threadexits("can't exec");
-}
-
-enum{
-	ATTACH,
-	BCC,
-	CC,
-	FROM,
-	INCLUDE,
-	TO,
-};
-
-char *headers[] = {
-	"attach:",
-	"bcc:",
-	"cc:",
-	"from:",
-	"include:",
-	"to:",
-	nil,
-};
-
-int
-whichheader(char *h)
-{
-	int i;
-
-	for(i=0; headers[i]!=nil; i++)
-		if(cistrcmp(h, headers[i]) == 0)
-			return i;
-	return -1;
-}
-
-char *tolist[200];
-char	*cclist[200];
-char	*bcclist[200];
-int ncc, nbcc, nto;
-char	*attlist[200];
-char	included[200];
-
-int
-addressed(char *name)
-{
-	int i;
-
-	for(i=0; i<nto; i++)
-		if(strcmp(name, tolist[i]) == 0)
-			return 1;
-	for(i=0; i<ncc; i++)
-		if(strcmp(name, cclist[i]) == 0)
-			return 1;
-	for(i=0; i<nbcc; i++)
-		if(strcmp(name, bcclist[i]) == 0)
-			return 1;
-	return 0;
-}
-
-char*
-skipbl(char *s, char *e)
-{
-	while(s < e){
-		if(*s!=' ' && *s!='\t' && *s!=',')
-			break;
-		s++;
-	}
-	return s;
-}
-
-char*
-findbl(char *s, char *e)
-{
-	while(s < e){
-		if(*s==' ' || *s=='\t' || *s==',')
-			break;
-		s++;
-	}
-	return s;
-}
-
-/*
- * comma-separate possibly blank-separated strings in line; e points before newline
- */
-void
-commas(char *s, char *e)
-{
-	char *t;
-
-	/* may have initial blanks */
-	s = skipbl(s, e);
-	while(s < e){
-		s = findbl(s, e);
-		if(s == e)
-			break;
-		t = skipbl(s, e);
-		if(t == e)	/* no more words */
-			break;
-		/* patch comma */
-		*s++ = ',';
-		while(s < t)
-			*s++ = ' ';
-	}
-}
-
-int
-print2(int fd, int ofd, char *fmt, ...)
-{
-	int m, n;
-	char *s;
-	va_list arg;
-
-	va_start(arg, fmt);
-	s = vsmprint(fmt, arg);
-	va_end(arg);
-	if(s == nil)
-		return -1;
-	m = strlen(s);
-	n = write(fd, s, m);
-	if(ofd > 0)
-		write(ofd, s, m);
-	return n;
-}
-
-void
-write2(int fd, int ofd, char *buf, int n, int nofrom)
-{
-	char *from, *p;
-	int m;
-
-	write(fd, buf, n);
-
-	if(ofd <= 0)
-		return;
-
-	if(nofrom == 0){
-		write(ofd, buf, n);
-		return;
-	}
-
-	/* need to escape leading From lines to avoid corrupting 'outgoing' mailbox */
-	for(p=buf; *p; p+=m){
-		from = cistrstr(p, "from");
-		if(from == nil)
-			m = n;
-		else
-			m = from - p;
-		if(m > 0)
-			write(ofd, p, m);
-		if(from){
-			if(p==buf || from[-1]=='\n')
-				write(ofd, " ", 1);	/* escape with space if From is at start of line */
-			write(ofd, from, 4);
-			m += 4;
-		}
-		n -= m;
-	}
-}
-
-void
-mesgsend(Message *m)
-{
-	char *s, *body, *to;
-	int i, j, h, n, natt, p[2];
-	struct Exec *e;
-	Channel *sync;
-	int first, nfld, delit, ofd;
-	char *copy, *fld[100], *now;
-
-	body = winreadbody(m->w, &n);
-	/* assemble to: list from first line, to: line, and cc: line */
-	nto = 0;
-	natt = 0;
-	ncc = 0;
-	nbcc = 0;
-	first = 1;
-	to = body;
-	for(;;){
-		for(s=to; *s!='\n'; s++)
-			if(*s == '\0'){
-				free(body);
-				return;
-			}
-		if(s++ == to)	/* blank line */
-			break;
-		/* make copy of line to tokenize */
-		copy = emalloc(s-to);
-		memmove(copy, to, s-to);
-		copy[s-to-1] = '\0';
-		nfld = tokenizec(copy, fld, nelem(fld), ", \t");
-		if(nfld == 0){
-			free(copy);
-			break;
-		}
-		n -= s-to;
-		switch(h = whichheader(fld[0])){
-		case TO:
-		case FROM:
-			delit = 1;
-			commas(to+strlen(fld[0]), s-1);
-			for(i=1; i<nfld && nto<nelem(tolist); i++)
-				if(!addressed(fld[i]))
-					tolist[nto++] = estrdup(fld[i]);
-			break;
-		case BCC:
-			delit = 1;
-			commas(to+strlen(fld[0]), s-1);
-			for(i=1; i<nfld && nbcc<nelem(bcclist); i++)
-				if(!addressed(fld[i]))
-					bcclist[nbcc++] = estrdup(fld[i]);
-			break;
-		case CC:
-			delit = 1;
-			commas(to+strlen(fld[0]), s-1);
-			for(i=1; i<nfld && ncc<nelem(cclist); i++)
-				if(!addressed(fld[i]))
-					cclist[ncc++] = estrdup(fld[i]);
-			break;
-		case ATTACH:
-		case INCLUDE:
-			delit = 1;
-			for(i=1; i<nfld && natt<nelem(attlist); i++){
-				attlist[natt] = estrdup(fld[i]);
-				included[natt++] = (h == INCLUDE);
-			}
-			break;
-		default:
-			if(first){
-				delit = 1;
-				for(i=0; i<nfld && nto<nelem(tolist); i++)
-					tolist[nto++] = estrdup(fld[i]);
-			}else	/* ignore it */
-				delit = 0;
-			break;
-		}
-		if(delit){
-			/* delete line from body */
-			memmove(to, s, n+1);
-		}else
-			to = s;
-		free(copy);
-		first = 0;
-	}
-
-	ofd = open(outgoing, OWRITE|OCEXEC);	/* no error check necessary */
-	if(ofd > 0){
-		/* From dhog Fri Aug 24 22:13:00 EDT 2001 */
-		now = ctime(time(0));
-		fprint(ofd, "From %s %s", user, now);
-		fprint(ofd, "From: %s\n", user);
-		fprint(ofd, "Date: %s", now);
-		for(i=0; i<natt; i++)
-			if(included[i])
-				fprint(ofd, "Include: %s\n", attlist[i]);
-			else
-				fprint(ofd, "Attach: %s\n", attlist[i]);
-		/* needed because mail is by default Latin-1 */
-		fprint(ofd, "Content-Type: text/plain; charset=\"UTF-8\"\n");
-		fprint(ofd, "Content-Transfer-Encoding: 8bit\n");
-	}
-
-	e = emalloc(sizeof(struct Exec));
-	if(pipe(p) < 0)
-		error("can't create pipe: %r");
-	e->p[0] = p[0];
-	e->p[1] = p[1];
-	e->prog = "/bin/upas/marshal";
-	e->argv = emalloc((1+1+2+4*natt+1)*sizeof(char*));
-	e->argv[0] = estrdup("marshal");
-	e->argv[1] = estrdup("-8");
-	j = 2;
-	if(m->replyname){
-		e->argv[j++] = estrdup("-R");
-		e->argv[j++] = estrstrdup(mbox.name, m->replyname);
-	}
-	for(i=0; i<natt; i++){
-		if(included[i])
-			e->argv[j++] = estrdup("-A");
-		else
-			e->argv[j++] = estrdup("-a");
-		e->argv[j++] = estrdup(attlist[i]);
-	}
-	sync = chancreate(sizeof(int), 0);
-	e->sync = sync;
-	proccreate(execproc, e, EXECSTACK);
-	recvul(sync);
-	close(p[0]);
-
-	/* using marshal -8, so generate rfc822 headers */
-	if(nto > 0){
-		print2(p[1], ofd, "To: ");
-		for(i=0; i<nto-1; i++)
-			print2(p[1], ofd, "%s, ", tolist[i]);
-		print2(p[1], ofd, "%s\n", tolist[i]);
-	}
-	if(ncc > 0){
-		print2(p[1], ofd, "CC: ");
-		for(i=0; i<ncc-1; i++)
-			print2(p[1], ofd, "%s, ", cclist[i]);
-		print2(p[1], ofd, "%s\n", cclist[i]);
-	}
-	if(nbcc > 0){
-		print2(p[1], ofd, "BCC: ");
-		for(i=0; i<nbcc-1; i++)
-			print2(p[1], ofd, "%s, ", bcclist[i]);
-		print2(p[1], ofd, "%s\n", bcclist[i]);
-	}
-
-	i = strlen(body);
-	if(i > 0)
-		write2(p[1], ofd, body, i, 1);
-
-	/* guarantee a blank line, to ensure attachments are separated from body */
-	if(i==0 || body[i-1]!='\n')
-		write2(p[1], ofd, "\n\n", 2, 0);
-	else if(i>1 && body[i-2]!='\n')
-		write2(p[1], ofd, "\n", 1, 0);
-
-	/* these look like pseudo-attachments in the "outgoing" box */
-	if(ofd>0 && natt>0){
-		for(i=0; i<natt; i++)
-			if(included[i])
-				fprint(ofd, "=====> Include: %s\n", attlist[i]);
-			else
-				fprint(ofd, "=====> Attach: %s\n", attlist[i]);
-	}
-	if(ofd > 0)
-		write(ofd, "\n", 1);
-
-	for(i=0; i<natt; i++)
-		free(attlist[i]);
-	close(ofd);
-	close(p[1]);
-	free(body);
-
-	if(m->replyname != nil)
-		mesgmenumark(mbox.w, m->replyname, "\t[replied]");
-	if(m->name[0] == '/')
-		s = estrdup(m->name);
-	else
-		s = estrstrdup(mbox.name, m->name);
-	s = egrow(s, "-R", nil);
-	winname(m->w, s);
-	free(s);
-	winclean(m->w);
-	/* mark message unopened because it's no longer the original message */
-	m->opened = 0;
-}
--- a/acme/mail/src/util.c
+++ /dev/null
@@ -1,105 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <thread.h>
-#include <plumb.h>
-#include "dat.h"
-
-void*
-emalloc(uint n)
-{
-	void *p;
-
-	p = malloc(n);
-	if(p == nil)
-		error("can't malloc: %r");
-	memset(p, 0, n);
-	setmalloctag(p, getcallerpc(&n));
-	return p;
-}
-
-void*
-erealloc(void *p, uint n)
-{
-	p = realloc(p, n);
-	if(p == nil)
-		error("can't realloc: %r");
-	setmalloctag(p, getcallerpc(&n));
-	return p;
-}
-
-char*
-estrdup(char *s)
-{
-	char *t;
-
-	t = emalloc(strlen(s)+1);
-	strcpy(t, s);
-	return t;
-}
-
-char*
-estrstrdup(char *s, char *t)
-{
-	char *u;
-
-	u = emalloc(strlen(s)+strlen(t)+1);
-	strcpy(u, s);
-	strcat(u, t);
-	return u;
-}
-
-char*
-eappend(char *s, char *sep, char *t)
-{
-	char *u;
-
-	if(t == nil)
-		u = estrstrdup(s, sep);
-	else{
-		u = emalloc(strlen(s)+strlen(sep)+strlen(t)+1);
-		strcpy(u, s);
-		strcat(u, sep);
-		strcat(u, t);
-	}
-	free(s);
-	return u;
-}
-
-char*
-egrow(char *s, char *sep, char *t)
-{
-	s = eappend(s, sep, t);
-	free(t);
-	return s;
-}
-
-void
-error(char *fmt, ...)
-{
-	Fmt f;
-	char buf[64];
-	va_list arg;
-
-	fmtfdinit(&f, 2, buf, sizeof buf);
-	fmtprint(&f, "Mail: ");
-	va_start(arg, fmt);
-	fmtvprint(&f, fmt, arg);
-	va_end(arg);
-	fmtprint(&f, "\n");
-	fmtfdflush(&f);
-	threadexitsall(buf);
-}
-
-void
-ctlprint(int fd, char *fmt, ...)
-{
-	int n;
-	va_list arg;
-
-	va_start(arg, fmt);
-	n = vfprint(fd, fmt, arg);
-	va_end(arg);
-	if(n <= 0)
-		error("control file write error: %r");
-}
--- a/acme/mail/src/win.c
+++ /dev/null
@@ -1,341 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <thread.h>
-#include <plumb.h>
-#include "dat.h"
-
-Window*
-newwindow(void)
-{
-	char buf[12];
-	Window *w;
-
-	w = emalloc(sizeof(Window));
-	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
-	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
-		error("can't open window ctl file: %r");
-	ctlprint(w->ctl, "noscroll\n");
-	w->id = atoi(buf);
-	w->event = winopenfile(w, "event");
-	w->addr = -1;	/* will be opened when needed */
-	w->body = nil;
-	w->data = -1;
-	w->cevent = chancreate(sizeof(Event*), 0);
-	return w;
-}
-
-void
-winsetdump(Window *w, char *dir, char *cmd)
-{
-	if(dir != nil)
-		ctlprint(w->ctl, "dumpdir %s\n", dir);
-	if(cmd != nil)
-		ctlprint(w->ctl, "dump %s\n", cmd);
-}
-
-void
-wineventproc(void *v)
-{
-	Window *w;
-	int i;
-
-	w = v;
-	for(i=0; ; i++){
-		if(i >= NEVENT)
-			i = 0;
-		wingetevent(w, &w->e[i]);
-		sendp(w->cevent, &w->e[i]);
-	}
-}
-
-static int
-winopenfile1(Window *w, char *f, int m)
-{
-	char buf[64];
-	int fd;
-
-	sprint(buf, "/mnt/wsys/%d/%s", w->id, f);
-	fd = open(buf, m|OCEXEC);
-	if(fd < 0)
-		error("can't open window file %s: %r", f);
-	return fd;
-}
-
-int
-winopenfile(Window *w, char *f)
-{
-	return winopenfile1(w, f, ORDWR);
-}
-
-void
-wintagwrite(Window *w, char *s, int n)
-{
-	int fd;
-
-	fd = winopenfile(w, "tag");
-	if(write(fd, s, n) != n)
-		error("tag write: %r");
-	close(fd);
-}
-
-void
-winname(Window *w, char *s)
-{
-	ctlprint(w->ctl, "name %s\n", s);
-}
-
-void
-winopenbody(Window *w, int mode)
-{
-	char buf[256];
-
-	sprint(buf, "/mnt/wsys/%d/body", w->id);
-	w->body = Bopen(buf, mode|OCEXEC);
-	if(w->body == nil)
-		error("can't open window body file: %r");
-}
-
-void
-winclosebody(Window *w)
-{
-	if(w->body != nil){
-		Bterm(w->body);
-		w->body = nil;
-	}
-}
-
-void
-winwritebody(Window *w, char *s, int n)
-{
-	if(w->body == nil)
-		winopenbody(w, OWRITE);
-	if(Bwrite(w->body, s, n) != n)
-		error("write error to window: %r");
-}
-
-int
-wingetec(Window *w)
-{
-	if(w->nbuf == 0){
-		w->nbuf = read(w->event, w->buf, sizeof w->buf);
-		if(w->nbuf <= 0){
-			/* probably because window has exited, and only called by wineventproc, so just shut down */
-			threadexits(nil);
-		}
-		w->bufp = w->buf;
-	}
-	w->nbuf--;
-	return *w->bufp++;
-}
-
-int
-wingeten(Window *w)
-{
-	int n, c;
-
-	n = 0;
-	while('0'<=(c=wingetec(w)) && c<='9')
-		n = n*10+(c-'0');
-	if(c != ' ')
-		error("event number syntax");
-	return n;
-}
-
-int
-wingeter(Window *w, char *buf, int *nb)
-{
-	Rune r;
-	int n;
-
-	r = wingetec(w);
-	buf[0] = r;
-	n = 1;
-	if(r >= Runeself) {
-		while(!fullrune(buf, n))
-			buf[n++] = wingetec(w);
-		chartorune(&r, buf);
-	} 
-	*nb = n;
-	return r;
-}
-
-void
-wingetevent(Window *w, Event *e)
-{
-	int i, nb;
-
-	e->c1 = wingetec(w);
-	e->c2 = wingetec(w);
-	e->q0 = wingeten(w);
-	e->q1 = wingeten(w);
-	e->flag = wingeten(w);
-	e->nr = wingeten(w);
-	if(e->nr > EVENTSIZE)
-		error("event string too long");
-	e->nb = 0;
-	for(i=0; i<e->nr; i++){
-		e->r[i] = wingeter(w, e->b+e->nb, &nb);
-		e->nb += nb;
-	}
-	e->r[e->nr] = 0;
-	e->b[e->nb] = 0;
-	if(wingetec(w) != '\n')
-		error("event syntax error");
-}
-
-void
-winwriteevent(Window *w, Event *e)
-{
-	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
-}
-
-void
-winread(Window *w, uint q0, uint q1, char *data)
-{
-	int m, n, nr;
-	char buf[256];
-
-	if(w->addr < 0)
-		w->addr = winopenfile(w, "addr");
-	if(w->data < 0)
-		w->data = winopenfile(w, "data");
-	m = q0;
-	while(m < q1){
-		n = sprint(buf, "#%d", m);
-		if(write(w->addr, buf, n) != n)
-			error("error writing addr: %r");
-		n = read(w->data, buf, sizeof buf);
-		if(n <= 0)
-			error("reading data: %r");
-		nr = utfnlen(buf, n);
-		while(m+nr >q1){
-			do; while(n>0 && (buf[--n]&0xC0)==0x80);
-			--nr;
-		}
-		if(n == 0)
-			break;
-		memmove(data, buf, n);
-		data += n;
-		*data = 0;
-		m += nr;
-	}
-}
-
-void
-windormant(Window *w)
-{
-	if(w->addr >= 0){
-		close(w->addr);
-		w->addr = -1;
-	}
-	if(w->body != nil){
-		Bterm(w->body);
-		w->body = nil;
-	}
-	if(w->data >= 0){
-		close(w->data);
-		w->data = -1;
-	}
-}
-
-
-int
-windel(Window *w, int sure)
-{
-	if(sure)
-		write(w->ctl, "delete\n", 7);
-	else if(write(w->ctl, "del\n", 4) != 4)
-		return 0;
-	/* event proc will die due to read error from event file */
-	windormant(w);
-	close(w->ctl);
-	w->ctl = -1;
-	close(w->event);
-	w->event = -1;
-	return 1;
-}
-
-void
-winclean(Window *w)
-{
-	if(w->body)
-		Bflush(w->body);
-	ctlprint(w->ctl, "clean\n");
-}
-
-int
-winsetaddr(Window *w, char *addr, int errok)
-{
-	if(w->addr < 0)
-		w->addr = winopenfile(w, "addr");
-	if(write(w->addr, addr, strlen(addr)) < 0){
-		if(!errok)
-			error("error writing addr(%s): %r", addr);
-		return 0;
-	}
-	return 1;
-}
-
-int
-winselect(Window *w, char *addr, int errok)
-{
-	if(winsetaddr(w, addr, errok)){
-		ctlprint(w->ctl, "dot=addr\n");
-		return 1;
-	}
-	return 0;
-}
-
-char*
-winreadbody(Window *w, int *np)	/* can't use readfile because acme doesn't report the length */
-{
-	char *s;
-	int m, na, n;
-
-	if(w->body != nil)
-		winclosebody(w);
-	winopenbody(w, OREAD);
-	s = nil;
-	na = 0;
-	n = 0;
-	for(;;){
-		if(na < n+512){
-			na += 1024;
-			s = realloc(s, na+1);
-		}
-		m = Bread(w->body, s+n, na-n);
-		if(m <= 0)
-			break;
-		n += m;
-	}
-	s[n] = 0;
-	winclosebody(w);
-	*np = n;
-	return s;
-}
-
-char*
-winselection(Window *w)
-{
-	int fd, m, n;
-	char *buf;
-	char tmp[256];
-
-	fd = winopenfile1(w, "rdsel", OREAD);
-	if(fd < 0)
-		error("can't open rdsel: %r");
-	n = 0;
-	buf = nil;
-	for(;;){
-		m = read(fd, tmp, sizeof tmp);
-		if(m <= 0)
-			break;
-		buf = erealloc(buf, n+m+1);
-		memmove(buf+n, tmp, m);
-		n += m;
-		buf[n] = '\0';
-	}
-	close(fd);
-	return buf;
-}
--- a/acme/mkfile
+++ b/acme/mkfile
@@ -5,6 +5,5 @@
 
 all install clean nuke installall update:V:
 	@{cd bin/source; mk $target}
-	@{cd mail/src; mk $target}
 	@{cd news/src; mk $target}
 	@{cd wiki/src; mk $target}
--- a/sys/doc/mkfile
+++ b/sys/doc/mkfile
@@ -40,6 +40,7 @@
 	spin\
 	port\
 	colophon\
+	nupas/nupas\
 
 ALLPS=${ALL:%=%.ps}
 HTML=${ALL:%=%.html} release3.html release4.html
--- /dev/null
+++ b/sys/doc/nupas/macros.ms
@@ -1,0 +1,92 @@
+.de F1
+.nr OI \\n(.iu
+.nr PW 1v
+.KF
+.sp 0.3v
+..
+.de T1
+.F1
+..
+.de F2
+.ds Fp Figure\ \\n(Fi
+.ds Fn Figure\ \\n+(Fi
+.ds Fq \\*(Fp
+.F0
+..
+.de T2
+.ds Tp Table\ \\n(Ti
+.ds Tn Table\ \\n+(Ti
+.ds Tq \\*(Tp
+.T0
+..
+.de F0
+.nr BD 1
+.if t .ps \\n(PS-1
+.ie \\n(VS>=41 .vs \\n(VSu-1p
+.el .vs \\n(VSp-1p
+.ft 1
+.di DD
+.ll \\n(.lu*3u/4u
+.in 0
+.fi
+.ad b
+.sp 0.5v
+\f3\\*(Fq\f1\ \ \c
+..
+.de T0
+.nr BD 1
+.if t .ps \\n(PS-1
+.ie \\n(VS>=41 .vs \\n(VSu-1p
+.el .vs \\n(VSp-1p
+.ft 1
+.di DD
+.ll \\n(.lu*3u/4u
+.in 0
+.fi
+.ad b
+.sp 0.5v
+\f3\\*(Tq\f1\ \ \c
+..
+.de F3
+.sp 0.5v
+.di
+.br
+.ll \\n(.lu*4u/3u
+.if \\n(dl>\\n(BD .nr BD \\n(dl
+.if \\n(BD<\\n(.l .in (\\n(.lu-\\n(BDu)/2u
+.nf
+.DD
+.in \\n(OIu
+.nr BD 0
+.fi
+.KE
+.ie \\n(VS>=41 .vs \\n(VSu
+.el .vs \\n(VSp
+..
+.de T3
+.F3
+..
+.de EX
+.P1
+\s-4
+..
+.de EE
+\s+4
+.P2
+..
+.nr Fi 1 +1
+.nr Ti 1 +1
+.ds Fn Figure\ \\n(Fi
+.ds Tn Table\ \\n(Ti
+.nr XP 2	\" delta point size for program
+.nr XV 2p	\" delta vertical for programs
+.nr XT 4	\" delta tab stop for programs
+.nr DV .5v	\" space before start of program
+.FP lucidasans
+.nr PS 11
+.nr VS 13
+.nr LL 6.6i
+.nr PI 0	\" paragraph indent
+.nr PD 4p	\" extra space between paragraphs
+.pl 11i
+.rm CH
--- /dev/null
+++ b/sys/doc/nupas/mkfile
@@ -1,0 +1,20 @@
+TARG=nupas.ps nupas.pdf
+
+all:V: $TARG
+
+%.ps:DQ:	%.ms
+	eval `{doctype macros.ms $stem.ms} | \
+	lp -m.9 -dstdout >$target
+
+%.pdf:DQ: %.ps
+	cat ../docfonts $stem.ps >_$stem.ps
+	ps2pdf _$stem.ps $stem.pdf && rm -f _$stem.ps
+
+%.show:VQ:	%.ps
+	page -w $stem.ps
+
+#install:VQ: fs4.man fs.man fsrecover.man fsconfig.man
+#	cp fs4.man /sys/man/4/fs
+#	cp fs.man /sys/man/8/fs
+#	cp fsconfig.man /sys/man/8/fsconfig
+#	cp fsrecover.man /sys/man/8/fsrecover
--- /dev/null
+++ b/sys/doc/nupas/nupas.ms
@@ -1,0 +1,354 @@
+.EQ
+delim $$
+.EN
+.TL
+Scaling Upas
+.AU
+Erik Quanstrom
[email protected]
+.AB
+The Plan 9 email system, Upas, uses traditional methods of delivery to
+.UX
+mail boxes while using a user-level file system, Upas/fs, to
+translate mail boxes of various formats into a single, convenient format for access.
+Unfortunately, it does not do so efficiently.  Upas/fs
+reads entire folders into core.  When deleting email from mail boxes,
+the entire mail box is rewritten.  I describe how Upas/fs has been
+adapted to use caching, indexing and a new mail box format (mdir) to
+limit I/O, reduce core size and eliminate the need to rewrite mail
+boxes.
+.AE
+.NH
+Introduction
+.LP
+.DS I
+Chained at his root two scion demons dwell
+.br
+	– Erasmus Darwin, The Botanic Garden
+.DE
+.LP
+At Coraid, email is the largest resource user in the system by orders
+of magnitude.  As of July, 2007, rewriting mail boxes was using
+300MB/day on the WORM and several users required more than 400MB of
+core.  As of July, 2008, rewriting mail boxes was using 800MB/day on
+the WORM and several users required more than 1.2GB of core to read
+email.  Clearly these are difficult to sustain levels of growth, even
+without growth of the company.  We needed to limit the amount of disk
+space used and, more urgently, reduce Upas/fs' core size.
+.LP
+The techniques employed are simple.  Mail is now stored in a directory
+with one message per file.  This eliminates the need to rewrite mail
+boxes.  Upas/fs now maintains an index which allows it to present
+complete message summaries without reading indexed messages.
+Combining the two techniques allows Upas/fs to read only new or
+referenced messages.  Finally, caching limits both the total number of
+in-core messages and their total size.
+.NH
+Mdir Format
+.LP
+In addition to meeting our urgent operational requirements of reducing
+memory and disk footprint, to meet the expectations of our users we
+require a solution that is able to handle folders up to ten thousand
+messages, open folders quickly, list the contents of folders quickly
+and support the current set of mail readers.
+.LP
+There are several potential styles of mail boxes.  The maildir[1] format
+has some attractive properties.  Mail can be delivered to or deleted
+from a mail box without locking.  New mail or deleted mail may be
+detected with a directory scan.  When used with WORM storage, the
+amount of storage required is no more than the size of new mail
+received.  Mbox format can require that a new copy of the inbox be
+stored every day.  Even with storage that coalesces duplicate blocks
+such as Venti, deleting a message will generally require new storage
+since messages are not disk-block aligned.  Maildir does not reduce
+the cost of the common task of a summary listing of mail such as
+generated by acme Mail.
+.LP
+The mails[2] format proposes a directory per mail.  A copy of
+the mail as delivered is stored and each mime part is decoded
+in such a way that a mail reader could display the file directly.
+Command line tools in the style of MH[3] are used to display and
+process mail.  Upas/fs is not necessary for reading local mail.
+Mails has the potential to reduce memory footprint below that
+offered by mdirs for native email reading.  However all of the
+largest mail boxes at our site are served exclusively through IMAP.
+The preformatting by mails would be unnecessary for such accounts.
+.LP
+Other mail servers such as Lotus Notes[4] store email in a custom
+database format which allows for fielded and full-text searching
+of mail folders.  Such a format provides very quick mail
+listings and good search capabilities.  Such a solution would not
+lend itself well to a tool-based environment,  nor would it be simple.
+.LP
+Maildir format seemed the best basic format but its particulars are
+tied to the
+.UX
+environment; mdir is a descendant.  A mdir folder
+is a directory with the name of the folder.  Messages in the mdir
+folder are stored in a file named
+.I "utime.seq" .
+.I Utime
+is defined as the decimal
+.UX
+seconds when the message was added to
+the folder.  For the inbox, this time will correspond to the
+.UX
+“From ” line.
+.I Seq
+is a two-digit sequence number starting with
+.CW "00."
+The lowest available sequence number is used to store the message.
+Thus, the first email possible would be named
+.CW "0.00."
+To prevent accidents, message files are stored with
+the append-only and exclusive-access bits turned on.
+The message is stored in the same format it would be in mbox
+format; each message is a valid mbox folder with a single message.
+.NH
+Indexing
+.LP
+When upas/fs finds an unindexed message, it is added to the index.
+The index is a file named
+.I "foldername" .idx
+and consists a signature and one line per MIME part.  Each line
+contains the SHA1 checksum of the message (or a place holder for
+subparts), one field per entry in the
+.I "messageid/info"
+file, flags and the number of subparts.  The flags are currently a
+superset of the standard IMAP flags.  They provide the similar
+functionality to maildir's modified file names.  Thus the `S'
+(answered) flag remains set between invocations of mail readers.
+Other mutable information about a message may be stored in a similar
+way.
+.LP
+Since the
+.I info
+file is read by all the mail readers to produce mail listings,
+mail boxes may be listed without opening any mail files when no new
+mail has arrived.  Similarly, opening a new mail box requires reading
+the index and checking new mail.  Index files are typically between
+0.5% and 5% the size of the full mail box.  Each time the index is
+generated, it is fully rewritten.
+.NH
+Caching
+.LP
+Upas/fs stores each message in a
+.CW "Message"
+structure.  To enable caching, this structure was split
+into four parts: The 
+.CW "Idx" 
+(or index), message subparts, information on the cache state of the
+message and a set of pointers into the processed header and body.
+Only the pointers to the processed header and body are subject to
+caching.  The available cache states are
+.CW "Cidx" ,
+.CW "Cheader"
+and 
+.CW "Cbody" .
+.LP
+When the header and body are not present, the average message with
+subparts takes roughly 3KB of memory.  Thus a 10,000 message mail box
+would require roughly 30MB of core in addition to any cached
+messages.  Reads of the
+.CW "info"
+or
+.CW "subject"
+files can be satisfied from the information in the 
+.CW "Idx"
+structure.
+.LP
+Since there are a fair number of very large messages, requests that
+can be satisfied by reading the message headers do not result in the
+full message being read.  Reads of the
+.CW "header"
+or
+.CW "rawheader"
+files of top-level messages are satisfied in this way.  Reading the
+same files for subparts, however, results in the entire message being
+read.  Caching the header results in the
+.CW "Cheader"
+cache state.
+.LP
+Similarly, reading the
+.CW "body"
+requires the body to be read, processed and results in
+the
+.CW "Cbody"
+cache state.  Reading from MIME subparts also results
+in the
+.CW "Cbody"
+cache state.
+.LP
+The cache has a simple LRU replacement policy.  Each time a cached
+member of a message is accessed, it is moved to the head of the list.
+The cache contains a maximum number of messages and a maximum size.
+While the maximum number of messages may not be exceeded, the maximum
+cache size may be exceeded if the sum of all the currently referenced
+messages is greater than the size of the cache.  In this case all
+unreferenced messages will be freed.  When removing a message
+from the cache all of the cacheable information is freed.
+.NH
+Collateral damage
+.LP
+.DS I
+Each new user of a new system uncovers a new class of bugs.
+.br
+	— Brian Kernighan
+.DE
+.LP
+In addition to upas/fs, programs that have assumptions about how
+mail boxes are structured needed to be modified.  Programs which
+deliver mail to mail boxes (deliver, marshal, ml, smtp) and append messages to
+folders were given a common (nedmail) function to call.  Since this
+was done by modifying functions in the Upas common library, this
+presented a problem for programs not traditionally part of Upas
+such as acme Mail and imap4d.  Rather than fold these programs
+into Upas, a new program, mbappend, was added to Upas.
+.LP
+Imap4d also requires the ability to rename and remove folders.
+While an external program would work for this as well, that
+approach has some drawbacks.  Most importantly, IMAP folders
+can't be moved or renamed in the same way without reimplementing
+functionality that is already in upas/fs.  It also emphasises the
+asymmetry between reading and deleting email and other folder
+actions.  Folder renaming and removal were added to upas/fs.  
+It is intended that mbappend will be removed soon
+and replaced with equivalent upas/fs functionality —
+at least for non-delivery programs.
+.LP
+Mdirs also expose an oddity about file permissions.  An append-only
+file that is mode
+.CW 0622
+may be appended to by anyone, but is readable only by the owner.
+With a directory, such a setup is not directly possible as write permission
+to a directory implies permission to remove.  There are a number of
+solutions to this problem.  Delivery could be made asymmetrical—incoming
+files could be written to a mbox. Or, following the example of the outbound
+mail queue, each user could deliver to a directory owned by that user.
+In many BSD-derived 
+.UX
+systems, the “sticky bit” on directories is used to modify
+the meaning of the
+.CW w
+bit for users matching only the other bits.  For them, the
+.CW w
+bit gives permission to create but not to remove.
+.LP
+While this is somewhat of a one-off situation, I chose to implement
+a version of the “sticky bit” using the existing append-only bit on our
+file server.  This was implemented as an extra permission check when
+removing files.  Fewer than 10 lines of code were required.
+.NH
+Performance
+.LP
+A representative local mail box was used to generate some rough
+performance numbers.  The mail box is 110MB and contains 868 messages.
+These figures are shown in table 1.  In the worse case—an unindexed
+mail box—the new upas/fs uses 18% of the memory of the original while
+using 13% more cpu.  In the best case, it uses only 5% of the memory
+while using only 13% of the cpu.  Clearly, a larger mail box will make
+these ratios more attractive.  In the two months since the snapshot was
+taken, that same mail box has grown to 220MB and contains 1814
+messages.
+.ps -2
+.DS C
+.TS
+box, tab(:);
+c s s s s
+c | c | c | c | c
+a | n | n | n | n.
+Table 1 – Performance
+_
+action:user:system:real:core size:
+:s:s:s:MB:
+_
+old fs read:1.69:0.84:6.07:135
+_
+initial read:1.65:0.90:6.90:25
+_
+indexed read:0.64:0.03:0.77:6.5
+.TE
+.DE
+.NL
+.NH
+Future Work
+.LP
+While Upas' memory usage has been drastically reduced,
+it is still a work-in-progress.  Caching and indexing are
+adequate but primitive.  Upas/fs is still inconsistently
+bypassed for appending messages to mail boxes.  There
+are also some features which remain incomplete.  Finally,
+the small increase in scale brings some new questions about
+the organization  of email.
+.LP
+It may be useful for mail boxes with very large numbers
+of messages to divide the index into fixed-size chunks.
+Then messages could be read into a fixed-sized pool of
+structures as needed.  However it is currently hard to
+see how clients could easily interface a mail box large
+enough for this technique to be useful.  Currently, all
+clients assume that it is reasonable to allocate an
+in-core data structure for each message in a mail box.
+To take advantage of a chunked index, clients (or the
+server) would need a way of limiting the number of
+messages considered at a time.  Also, for such large
+mail boxes, it would be important to separate the
+incoming messages from older messages to limit the work
+required to scan for new messages.
+.LP
+Caching is particularly unsatisfactory.  Files should
+be read in fixed-sized buffers so maximum memory usage
+does not depend on the size of the largest file in the
+mail box.  Unfortunately, current data structures do not readily
+support this.  In practice, this limitation has not yet
+been noticeable.
+.LP
+There are also a few features that need to be completed.
+Tracking of references has been added to marshal and
+upas/fs.  In addition, the index provides a place to store
+mutable information about a message.  These capabilities
+should be built upon to provide general threading and
+tagging capabilities.
+.NH
+Speculation
+.LP
+Freed from the limitation that all messages in a
+mail box must be read and stored in memory before a
+single message may be accessed, it is interesting to
+speculate on a few further possibilites.
+.LP
+For example, it may be
+useful to replace separate mail boxes with a single
+collection of messages assigned to one or more virtual
+mail boxes.  The association between a message and a
+mail box would be a “tag.” A message could be added to
+or removed from one or more mail boxes without modifying
+the mdir file.  If threads were implemented by tagging
+each message with its references, it would be possible
+to follow threads across mail boxes, even to messages
+removed from all mail boxes, provided the underlying
+file were not also removed.  If a facility for adding
+arbitrary, automatic tags were enabled, it would be
+possible to tag messages with the email address in
+the SMTP From line.
+.NH
+References
+.IP [1]
+D. Bernstein, “Using maildir format”,
+published online at
+.br
+http://cr.yp.to/proto/maildir.html
+.IP [2]
+F. Ballesteros
+.IR mails (1),
+published online at
+http://lsub.org/magic/man2html/1/mails
+.IP [3]
+MH Wikipedia entry,
+http://en.wikipedia.org/wiki/MH_Message_Handling_System
+.IP [4]
+Lotus Notes Wikipedia entry,
+http://en.wikipedia.org/wiki/Lotus_Notes
+.IP [5]
+D. Presotto, “Upas—a Simpler Approach to Network Mail”,
+Proceedings of the 10th Usenix conference, 1985.
--- a/sys/man/8/pop3
+++ b/sys/man/8/pop3
@@ -20,7 +20,7 @@
 .B -p
 ]
 .PP
-.B ip/imap4d
+.B upas/imap4d
 .RB [ -acpv ]
 .RB [ -d
 .IR smtpdomain ]
@@ -142,7 +142,7 @@
 .SH SOURCE
 .B /sys/src/cmd/upas/pop3
 .br
-.B /sys/src/cmd/ip/imap4d
+.B /sys/src/cmd/upas/imap4d
 .SH "SEE ALSO"
 .IR aliasmail (8),
 .IR faces (1),
--- a/sys/src/cmd/ip/imap4d/auth.c
+++ /dev/null
@@ -1,184 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <auth.h>
-#include <libsec.h>
-#include <bio.h>
-#include "imap4d.h"
-
-/*
- * hack to allow smtp forwarding.
- * hide the peer IP address under a rock in the ratifier FS.
- */
-void
-enableForwarding(void)
-{
-	char buf[64], peer[64], *p;
-	static ulong last;
-	ulong now;
-	int fd;
-
-	if(remote == nil)
-		return;
-
-	now = time(0);
-	if(now < last + 5*60)
-		return;
-	last = now;
-
-	fd = open("/srv/ratify", ORDWR);
-	if(fd < 0)
-		return;
-	if(!mount(fd, -1, "/mail/ratify", MBEFORE, "")){
-		close(fd);
-		return;
-	}
-	close(fd);
-
-	strncpy(peer, remote, sizeof(peer));
-	peer[sizeof(peer) - 1] = '\0';
-	p = strchr(peer, '!');
-	if(p != nil)
-		*p = '\0';
-
-	snprint(buf, sizeof(buf), "/mail/ratify/trusted/%s#32", peer);
-
-	/*
-	 * if the address is already there and the user owns it,
-	 * remove it and recreate it to give him a new time quanta.
-	 */
-	if(access(buf, 0) >= 0 && remove(buf) < 0)
-		return;
-
-	fd = create(buf, OREAD, 0666);
-	if(fd >= 0)
-		close(fd);
-}
-
-void
-setupuser(AuthInfo *ai)
-{
-	Waitmsg *w;
-	int pid;
-
-	if(ai){
-		strecpy(username, username+sizeof username, ai->cuid);
-
-		if(auth_chuid(ai, nil) < 0)
-			bye("user auth failed: %r");
-		auth_freeAI(ai);
-	}else
-		strecpy(username, username+sizeof username, getuser());
-
-	if(newns(username, 0) < 0)
-		bye("user login failed: %r");
-
-	/*
-	 * hack to allow access to outgoing smtp forwarding
-	 */
-	enableForwarding();
-
-	snprint(mboxDir, MboxNameLen, "/mail/box/%s", username);
-	if(myChdir(mboxDir) < 0)
-		bye("can't open user's mailbox");
-
-	switch(pid = fork()){
-	case -1:
-		bye("can't initialize mail system");
-		break;
-	case 0:
-		execl("/bin/upas/fs", "upas/fs", "-np", nil);
-_exits("rob1");
-		_exits(0);
-		break;
-	default:
-		break;
-	}
-	if((w=wait()) == nil || w->pid != pid || w->msg[0] != '\0')
-		bye("can't initialize mail system");
-	free(w);
-}
-
-static char*
-authresp(void)
-{
-	char *s, *t;
-	int n;
-
-	t = Brdline(&bin, '\n');
-	n = Blinelen(&bin);
-	if(n < 2)
-		return nil;
-	n--;
-	if(t[n-1] == '\r')
-		n--;
-	t[n] = '\0';
-	if(n == 0 || strcmp(t, "*") == 0)
-		return nil;
-
-	s = binalloc(&parseBin, n + 1, 0);
-	n = dec64((uchar*)s, n, t, n);
-	s[n] = '\0';
-	return s;
-}
-
-/*
- * rfc 2195 cram-md5 authentication
- */
-char*
-cramauth(void)
-{
-	AuthInfo *ai;
-	Chalstate *cs;
-	char *s, *t;
-
-	if((cs = auth_challenge("proto=cram role=server")) == nil)
-		return "couldn't get cram challenge";
-
-	Bprint(&bout, "+ %.*[\r\n", cs->nchal, cs->chal);
-	if(Bflush(&bout) < 0)
-		writeErr();
-
-	s = authresp();
-	if(s == nil)
-		return "client cancelled authentication";
-
-	t = strchr(s, ' ');
-	if(t == nil)
-		bye("bad auth response");
-	*t++ = '\0';
-	strncpy(username, s, UserNameLen);
-	username[UserNameLen-1] = '\0';
-
-	cs->user = username;
-	cs->resp = t;
-	cs->nresp = strlen(t);
-	if((ai = auth_response(cs)) == nil)
-		return "login failed";
-	auth_freechal(cs);
-	setupuser(ai);
-	return nil;
-}
-
-AuthInfo*
-passLogin(char *user, char *secret)
-{
-	AuthInfo *ai;
-	Chalstate *cs;
-	uchar digest[MD5dlen];
-	char response[2*MD5dlen+1];
-
-	if((cs = auth_challenge("proto=cram role=server")) == nil)
-		return nil;
-
-	hmac_md5((uchar*)cs->chal, strlen(cs->chal),
-		(uchar*)secret, strlen(secret), digest,
-		nil);
-	snprint(response, sizeof(response), "%.*H", MD5dlen, digest);
-
-	cs->user = user;
-	cs->resp = response;
-	cs->nresp = strlen(response);
-	ai = auth_response(cs);
-	auth_freechal(cs);
-	return ai;
-}
--- a/sys/src/cmd/ip/imap4d/copy.c
+++ /dev/null
@@ -1,259 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include <libsec.h>
-#include "imap4d.h"
-
-static int	saveMsg(char *dst, char *digest, int flags, char *head, int nhead, Biobuf *b, long n);
-static int	saveb(int fd, DigestState *dstate, char *buf, int nr, int nw);
-static long	appSpool(Biobuf *bout, Biobuf *bin, long n);
-
-/*
- * check if the message exists
- */
-int
-copyCheck(Box *box, Msg *m, int uids, void *v)
-{
-	int fd;
-
-	USED(box);
-	USED(uids);
-	USED(v);
-
-	if(m->expunged)
-		return 0;
-	fd = msgFile(m, "raw");
-	if(fd < 0){
-		msgDead(m);
-		return 0;
-	}
-	close(fd);
-	return 1;
-}
-
-int
-copySave(Box *box, Msg *m, int uids, void *vs)
-{
-	Dir *d;
-	Biobuf b;
-	vlong length;
-	char *head;
-	int ok, hfd, bfd, nhead;
-
-	USED(box);
-	USED(uids);
-
-	if(m->expunged)
-		return 0;
-
-	hfd = msgFile(m, "unixheader");
-	if(hfd < 0){
-		msgDead(m);
-		return 0;
-	}
-	head = readFile(hfd);
-	if(head == nil){
-		close(hfd);
-		return 0;
-	}
-
-	/*
-	 * clear out the header if it doesn't end in a newline,
-	 * since it is a runt and the "header" will show up in the raw file.
-	 */
-	nhead = strlen(head);
-	if(nhead > 0 && head[nhead-1] != '\n')
-		nhead = 0;
-
-	bfd = msgFile(m, "raw");
-	close(hfd);
-	if(bfd < 0){
-		msgDead(m);
-		return 0;
-	}
-
-	d = dirfstat(bfd);
-	if(d == nil){
-		close(bfd);
-		return 0;
-	}
-	length = d->length;
-	free(d);
-
-	Binit(&b, bfd, OREAD);
-	ok = saveMsg(vs, m->info[IDigest], m->flags, head, nhead, &b, length);
-	Bterm(&b);
-	close(bfd);
-	return ok;
-}
-
-/*
- * first spool the input into a temorary file,
- * and massage the input in the process.
- * then save to real box.
- */
-int
-appendSave(char *mbox, int flags, char *head, Biobuf *b, long n)
-{
-	Biobuf btmp;
-	int fd, ok;
-
-	fd = imapTmp();
-	if(fd < 0)
-		return 0;
-	Bprint(&bout, "+ Ready for literal data\r\n");
-	if(Bflush(&bout) < 0)
-		writeErr();
-	Binit(&btmp, fd, OWRITE);
-	n = appSpool(&btmp, b, n);
-	Bterm(&btmp);
-	if(n < 0){
-		close(fd);
-		return 0;
-	}
-
-	seek(fd, 0, 0);
-	Binit(&btmp, fd, OREAD);
-	ok = saveMsg(mbox, nil, flags, head, strlen(head), &btmp, n);
-	Bterm(&btmp);
-	close(fd);
-	return ok;
-}
-
-/*
- * copy from bin to bout,
- * mapping "\r\n" to "\n" and "\nFrom " to "\n From "
- * return the number of bytes in the mapped file.
- *
- * exactly n bytes must be read from the input,
- * unless an input error occurs.
- */
-static long
-appSpool(Biobuf *bout, Biobuf *bin, long n)
-{
-	int i, c;
-
-	c = '\n';
-	while(n > 0){
-		if(c == '\n' && n >= STRLEN("From ")){
-			for(i = 0; i < STRLEN("From "); i++){
-				c = Bgetc(bin);
-				if(c != "From "[i]){
-					if(c < 0)
-						return -1;
-					Bungetc(bin);
-					break;
-				}
-				n--;
-			}
-			if(i == STRLEN("From "))
-				Bputc(bout, ' ');
-			Bwrite(bout, "From ", i);
-		}
-		c = Bgetc(bin);
-		n--;
-		if(c == '\r' && n-- > 0){
-			c = Bgetc(bin);
-			if(c != '\n')
-				Bputc(bout, '\r');
-		}
-		if(c < 0)
-			return -1;
-		if(Bputc(bout, c) < 0)
-			return -1;
-	}
-	if(c != '\n')
-		Bputc(bout, '\n');
-	if(Bflush(bout) < 0)
-		return -1;
-	return Boffset(bout);
-}
-
-static int
-saveMsg(char *dst, char *digest, int flags, char *head, int nhead, Biobuf *b, long n)
-{
-	DigestState *dstate;
-	MbLock *ml;
-	uchar shadig[SHA1dlen];
-	char buf[BufSize + 1], digbuf[NDigest + 1];
-	int i, fd, nr, nw, ok;
-
-	ml = mbLock();
-	if(ml == nil)
-		return 0;
-	fd = openLocked(mboxDir, dst, OWRITE);
-	if(fd < 0){
-		mbUnlock(ml);
-		return 0;
-	}
-	seek(fd, 0, 2);
-
-	dstate = nil;
-	if(digest == nil)
-		dstate = sha1(nil, 0, nil, nil);
-	if(!saveb(fd, dstate, head, nhead, nhead)){
-		if(dstate != nil)
-			sha1(nil, 0, shadig, dstate);
-		mbUnlock(ml);
-		close(fd);
-		return 0;
-	}
-	ok = 1;
-	if(n == 0)
-		ok = saveb(fd, dstate, "\n", 0, 1);
-	while(n > 0){
-		nr = n;
-		if(nr > BufSize)
-			nr = BufSize;
-		nr = Bread(b, buf, nr);
-		if(nr <= 0){
-			saveb(fd, dstate, "\n\n", 0, 2);
-			ok = 0;
-			break;
-		}
-		n -= nr;
-		nw = nr;
-		if(n == 0){
-			if(buf[nw - 1] != '\n')
-				buf[nw++] = '\n';
-			buf[nw++] = '\n';
-		}
-		if(!saveb(fd, dstate, buf, nr, nw)){
-			ok = 0;
-			break;
-		}
-		mbLockRefresh(ml);
-	}
-	close(fd);
-
-	if(dstate != nil){
-		digest = digbuf;
-		sha1(nil, 0, shadig, dstate);
-		for(i = 0; i < SHA1dlen; i++)
-			snprint(digest+2*i, NDigest+1-2*i, "%2.2ux", shadig[i]);
-	}
-	if(ok){
-		fd = cdOpen(mboxDir, impName(dst), OWRITE);
-		if(fd < 0)
-			fd = emptyImp(dst);
-		if(fd >= 0){
-			seek(fd, 0, 2);
-			wrImpFlags(buf, flags, 1);
-			fprint(fd, "%.*s %.*lud %s\n", NDigest, digest, NUid, 0UL, buf);
-			close(fd);
-		}
-	}
-	mbUnlock(ml);
-	return 1;
-}
-
-static int
-saveb(int fd, DigestState *dstate, char *buf, int nr, int nw)
-{
-	if(dstate != nil)
-		sha1((uchar*)buf, nr, nil, dstate);
-	if(write(fd, buf, nw) != nw)
-		return 0;
-	return 1;
-}
--- a/sys/src/cmd/ip/imap4d/csquery.c
+++ /dev/null
@@ -1,44 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-/*
- *  query the connection server
- */
-char*
-csquery(char *attr, char *val, char *rattr)
-{
-	char token[64+4];
-	char buf[256], *p, *sp;
-	int fd, n;
-
-	if(val == nil || val[0] == 0)
-		return nil;
-	fd = open("/net/cs", ORDWR);
-	if(fd < 0)
-		return nil;
-	fprint(fd, "!%s=%s", attr, val);
-	seek(fd, 0, 0);
-	snprint(token, sizeof(token), "%s=", rattr);
-	for(;;){
-		n = read(fd, buf, sizeof(buf)-1);
-		if(n <= 0)
-			break;
-		buf[n] = 0;
-		p = strstr(buf, token);
-		if(p != nil && (p == buf || *(p-1) == 0)){
-			close(fd);
-			sp = strchr(p, ' ');
-			if(sp)
-				*sp = 0;
-			p = strchr(p, '=');
-			if(p == nil)
-				return nil;
-			return strdup(p+1);
-		}
-	}
-	close(fd);
-	return nil;
-}
--- a/sys/src/cmd/ip/imap4d/date.c
+++ /dev/null
@@ -1,308 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-char *
-wdayname[7] =
-{
-	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
-};
-
-char *
-monname[12] =
-{
-	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
-};
-
-static void	time2tm(Tm *tm, char *s);
-static void	zone2tm(Tm *tm, char *s);
-static int	dateindex(char *d, char **tab, int n);
-
-int
-rfc822date(char *s, int n, Tm *tm)
-{
-	char *plus;
-	int m;
-
-	plus = "+";
-	if(tm->tzoff < 0)
-		plus = "";
-	m = 0;
-	if(0 <= tm->wday && tm->wday < 7){
-		m = snprint(s, n, "%s, ", wdayname[tm->wday]);
-		if(m < 0)
-			return m;
-	}
-	return snprint(s+m, n-m, "%.2d %s %.4d %.2d:%.2d:%.2d %s%.4d",
-		tm->mday, monname[tm->mon], tm->year+1900, tm->hour, tm->min, tm->sec,
-		plus, (tm->tzoff/3600)*100 + (tm->tzoff/60)%60);
-}
-
-int
-imap4date(char *s, int n, Tm *tm)
-{
-	char *plus;
-
-	plus = "+";
-	if(tm->tzoff < 0)
-		plus = "";
-	return snprint(s, n, "%2d-%s-%.4d %2.2d:%2.2d:%2.2d %s%4.4d",
-		tm->mday, monname[tm->mon], tm->year+1900, tm->hour, tm->min, tm->sec, plus, (tm->tzoff/3600)*100 + (tm->tzoff/60)%60);
-}
-
-int
-imap4Date(Tm *tm, char *date)
-{
-	char *flds[4];
-
-	if(getfields(date, flds, 3, 0, "-") != 3)
-		return 0;
-
-	tm->mday = strtol(flds[0], nil, 10);
-	tm->mon = dateindex(flds[1], monname, 12);
-	tm->year = strtol(flds[2], nil, 10) - 1900;
-	return 1;
-}
-
-/*
- * parse imap4 dates
- */
-ulong
-imap4DateTime(char *date)
-{
-	Tm tm;
-	char *flds[4], *sflds[4];
-	ulong t;
-
-	if(getfields(date, flds, 4, 0, " ") != 3)
-		return ~0;
-
-	if(!imap4Date(&tm, flds[0]))
-		return ~0;
-
-	if(getfields(flds[1], sflds, 3, 0, ":") != 3)
-		return ~0;
-
-	tm.hour = strtol(sflds[0], nil, 10);
-	tm.min = strtol(sflds[1], nil, 10);
-	tm.sec = strtol(sflds[2], nil, 10);
-
-	strcpy(tm.zone, "GMT");
-	tm.yday = 0;
-	t = tm2sec(&tm);
-	zone2tm(&tm, flds[2]);
-	t -= tm.tzoff;
-	return t;
-}
-
-/*
- * parse dates of formats
- * [Wkd[,]] DD Mon YYYY HH:MM:SS zone
- * [Wkd] Mon ( D|DD) HH:MM:SS zone YYYY
- * plus anything similar
- * return nil for a failure
- */
-Tm*
-date2tm(Tm *tm, char *date)
-{
-	Tm gmt, *atm;
-	char *flds[7], *s, dstr[64];
-	int n;
-
-	/*
-	 * default date is Thu Jan  1 00:00:00 GMT 1970
-	 */
-	tm->wday = 4;
-	tm->mday = 1;
-	tm->mon = 1;
-	tm->hour = 0;
-	tm->min = 0;
-	tm->sec = 0;
-	tm->year = 70;
-	strcpy(tm->zone, "GMT");
-	tm->tzoff = 0;
-
-	strncpy(dstr, date, sizeof(dstr));
-	dstr[sizeof(dstr)-1] = '\0';
-	n = tokenize(dstr, flds, 7);
-	if(n != 6 && n != 5)
-		return nil;
-
-	if(n == 5){
-		for(n = 5; n >= 1; n--)
-			flds[n] = flds[n - 1];
-		n = 5;
-	}else{
-		/*
-		 * Wday[,]
-		 */
-		s = strchr(flds[0], ',');
-		if(s != nil)
-			*s = '\0';
-		tm->wday = dateindex(flds[0], wdayname, 7);
-		if(tm->wday < 0)
-			return nil;
-	}
-
-	/*
-	 * check for the two major formats:
-	 * Month first or day first
-	 */
-	tm->mon = dateindex(flds[1], monname, 12);
-	if(tm->mon >= 0){
-		tm->mday = strtoul(flds[2], nil, 10);
-		time2tm(tm, flds[3]);
-		zone2tm(tm, flds[4]);
-		tm->year = strtoul(flds[5], nil, 10);
-		if(strlen(flds[5]) > 2)
-			tm->year -= 1900;
-	}else{
-		tm->mday = strtoul(flds[1], nil, 10);
-		tm->mon = dateindex(flds[2], monname, 12);
-		tm->year = strtoul(flds[3], nil, 10);
-		if(strlen(flds[3]) > 2)
-			tm->year -= 1900;
-		time2tm(tm, flds[4]);
-		zone2tm(tm, flds[5]);
-	}
-
-	if(n == 5){
-		gmt = *tm;
-		strncpy(gmt.zone, "", 4);
-		gmt.tzoff = 0;
-		atm = gmtime(tm2sec(&gmt));
-		tm->wday = atm->wday;
-	}else{
-		/*
-		 * Wday[,]
-		 */
-		s = strchr(flds[0], ',');
-		if(s != nil)
-			*s = '\0';
-		tm->wday = dateindex(flds[0], wdayname, 7);
-		if(tm->wday < 0)
-			return nil;
-	}
-	return tm;
-}
-
-/*
- * zone	: [A-Za-z][A-Za-z][A-Za-z]	some time zone names
- *	| [A-IK-Z]			military time; rfc1123 says the rfc822 spec is wrong.
- *	| "UT"				universal time
- *	| [+-][0-9][0-9][0-9][0-9]
- * zones is the rfc-822 list of time zone names
- */
-static NamedInt zones[] =
-{
-	{"A",	-1 * 3600},
-	{"B",	-2 * 3600},
-	{"C",	-3 * 3600},
-	{"CDT", -5 * 3600},
-	{"CST", -6 * 3600},
-	{"D",	-4 * 3600},
-	{"E",	-5 * 3600},
-	{"EDT", -4 * 3600},
-	{"EST", -5 * 3600},
-	{"F",	-6 * 3600},
-	{"G",	-7 * 3600},
-	{"GMT", 0},
-	{"H",	-8 * 3600},
-	{"I",	-9 * 3600},
-	{"K",	-10 * 3600},
-	{"L",	-11 * 3600},
-	{"M",	-12 * 3600},
-	{"MDT", -6 * 3600},
-	{"MST", -7 * 3600},
-	{"N",	+1 * 3600},
-	{"O",	+2 * 3600},
-	{"P",	+3 * 3600},
-	{"PDT", -7 * 3600},
-	{"PST", -8 * 3600},
-	{"Q",	+4 * 3600},
-	{"R",	+5 * 3600},
-	{"S",	+6 * 3600},
-	{"T",	+7 * 3600},
-	{"U",	+8 * 3600},
-	{"UT",	0},
-	{"V",	+9 * 3600},
-	{"W",	+10 * 3600},
-	{"X",	+11 * 3600},
-	{"Y",	+12 * 3600},
-	{"Z",	0},
-	{nil,	0}
-};
-
-static void
-zone2tm(Tm *tm, char *s)
-{
-	Tm aux, *atm;
-	int i;
-
-	if(*s == '+' || *s == '-'){
-		i = strtol(s, &s, 10);
-		tm->tzoff = (i / 100) * 3600 + i % 100;
-		strncpy(tm->zone, "", 4);
-		return;
-	}
-
-	/*
-	 * look it up in the standard rfc822 table
-	 */
-	strncpy(tm->zone, s, 3);
-	tm->zone[3] = '\0';
-	tm->tzoff = 0;
-	for(i = 0; zones[i].name != nil; i++){
-		if(cistrcmp(zones[i].name, s) == 0){
-			tm->tzoff = zones[i].v;
-			return;
-		}
-	}
-
-	/*
-	 * one last try: look it up in the current local timezone
-	 * probe a couple of times to get daylight/standard time change.
-	 */
-	aux = *tm;
-	memset(aux.zone, 0, 4);
-	aux.hour--;
-	for(i = 0; i < 2; i++){
-		atm = localtime(tm2sec(&aux));
-		if(cistrcmp(tm->zone, atm->zone) == 0){
-			tm->tzoff = atm->tzoff;
-			return;
-		}
-		aux.hour++;
-	}
-
-	strncpy(tm->zone, "GMT", 4);
-	tm->tzoff = 0;
-}
-
-/*
- * hh[:mm[:ss]]
- */
-static void
-time2tm(Tm *tm, char *s)
-{
-	tm->hour = strtoul(s, &s, 10);
-	if(*s++ != ':')
-		return;
-	tm->min = strtoul(s, &s, 10);
-	if(*s++ != ':')
-		return;
-	tm->sec = strtoul(s, &s, 10);
-}
-
-static int
-dateindex(char *d, char **tab, int n)
-{
-	int i;
-
-	for(i = 0; i < n; i++)
-		if(cistrcmp(d, tab[i]) == 0)
-			return i;
-	return -1;
-}
--- a/sys/src/cmd/ip/imap4d/debug.c
+++ /dev/null
@@ -1,108 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <auth.h>
-#include <bio.h>
-#include "imap4d.h"
-
-void
-debuglog(char *fmt, ...)
-{
-	va_list arg;
-	static int logfd;
-
-	if(debug == 0)
-		return;
-	if(logfd == 0)
-		logfd = open("/sys/log/imap4d", OWRITE);
-	if(logfd > 0){
-		va_start(arg, fmt);
-		fprint(logfd, "%s: ", username);
-		vfprint(logfd, fmt, arg);
-		va_end(arg);
-	}
-}
-
-void
-boxVerify(Box *box)
-{
-	Msg *m;
-	ulong seq, uid, recent;
-
-	if(box == nil)
-		return;
-	recent = 0;
-	seq = 0;
-	uid = 0;
-	for(m = box->msgs; m != nil; m = m->next){
-		if(m->seq == 0)
-			fprint(2, "m->seq == 0: m->seq=%lud\n", m->seq);
-		else if(m->seq <= seq)
-			fprint(2, "m->seq=%lud out of order: last=%lud\n", m->seq, seq);
-		seq = m->seq;
-
-		if(m->uid == 0)
-			fprint(2, "m->uid == 0: m->seq=%lud\n", m->seq);
-		else if(m->uid <= uid)
-			fprint(2, "m->uid=%lud out of order: last=%lud\n", m->uid, uid);
-		uid = m->uid;
-
-		if(m->flags & MRecent)
-			recent++;
-	}
-	if(seq != box->max)
-		fprint(2, "max=%lud, should be %lud\n", box->max, seq);
-	if(uid >= box->uidnext)
-		fprint(2, "uidnext=%lud, maxuid=%lud\n", box->uidnext, uid);
-	if(recent != box->recent)
-		fprint(2, "recent=%lud, should be %lud\n", box->recent, recent);
-}
-
-void
-openfiles(void)
-{
-	Dir *d;
-	int i;
-
-	for(i = 0; i < 20; i++){
-		d = dirfstat(i);
-		if(d != nil){
-			fprint(2, "fd[%d]='%s' type=%c dev=%d user='%s group='%s'\n", i, d->name, d->type, d->dev, d->uid, d->gid);
-			free(d);
-		}
-	}
-}
-
-void
-ls(char *file)
-{
-	Dir *d;
-	int fd, i, nd;
-
-	fd = open(file, OREAD);
-	if(fd < 0)
-		return;
-
-	/*
-	 * read box to find all messages
-	 * each one has a directory, and is in numerical order
-	 */
-	d = dirfstat(fd);
-	if(d == nil){
-		close(fd);
-		return;
-	}
-	if(!(d->mode & DMDIR)){
-		fprint(2, "file %s\n", file);
-		free(d);
-		close(fd);
-		return;
-	}
-	free(d);
-	while((nd = dirread(fd, &d)) > 0){
-		for(i = 0; i < nd; i++){
-			fprint(2, "%s/%s %c\n", file, d[i].name, "-d"[(d[i].mode & DMDIR) == DMDIR]);
-		}
-		free(d);
-	}
-	close(fd);
-}
--- a/sys/src/cmd/ip/imap4d/fetch.c
+++ /dev/null
@@ -1,625 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-char *fetchPartNames[FPMax] =
-{
-	"",
-	"HEADER",
-	"HEADER.FIELDS",
-	"HEADER.FIELDS.NOT",
-	"MIME",
-	"TEXT",
-};
-
-/*
- * implicitly set the \seen flag.  done in a separate pass
- * so the .imp file doesn't need to be open while the
- * messages are sent to the client.
- */
-int
-fetchSeen(Box *box, Msg *m, int uids, void *vf)
-{
-	Fetch *f;
-
-	USED(uids);
-
-	if(m->expunged)
-		return uids;
-	for(f = vf; f != nil; f = f->next){
-		switch(f->op){
-		case FRfc822:
-		case FRfc822Text:
-		case FBodySect:
-			msgSeen(box, m);
-			goto breakout;
-		}
-	}
-breakout:
-
-	return 1;
-}
-
-/*
- * fetch messages
- *
- * imap4 body[] requestes get translated to upas/fs files as follows
- *	body[id.header] == id/rawheader file + extra \r\n
- *	body[id.text] == id/rawbody
- *	body[id.mime] == id/mimeheader + extra \r\n
- *	body[id] === body[id.header] + body[id.text]
-*/
-int
-fetchMsg(Box *, Msg *m, int uids, void *vf)
-{
-	Tm tm;
-	Fetch *f;
-	char *sep;
-	int todo;
-
-	if(m->expunged)
-		return uids;
-
-	todo = 0;
-	for(f = vf; f != nil; f = f->next){
-		switch(f->op){
-		case FFlags:
-			todo = 1;
-			break;
-		case FUid:
-			todo = 1;
-			break;
-		case FInternalDate:
-		case FEnvelope:
-		case FRfc822:
-		case FRfc822Head:
-		case FRfc822Size:
-		case FRfc822Text:
-		case FBodySect:
-		case FBodyPeek:
-		case FBody:
-		case FBodyStruct:
-			todo = 1;
-			if(!msgStruct(m, 1)){
-				msgDead(m);
-				return uids;
-			}
-			break;
-		default:
-			bye("bad implementation of fetch");
-			return 0;
-		}
-	}
-
-	if(m->expunged)
-		return uids;
-	if(!todo)
-		return 1;
-
-	/*
-	 * note: it is allowed to send back the responses one at a time
-	 * rather than all together.  this is exploited to send flags elsewhere.
-	 */
-	Bprint(&bout, "* %lud FETCH (", m->seq);
-	sep = "";
-	if(uids){
-		Bprint(&bout, "UID %lud", m->uid);
-		sep = " ";
-	}
-	for(f = vf; f != nil; f = f->next){
-		switch(f->op){
-		default:
-			bye("bad implementation of fetch");
-			break;
-		case FFlags:
-			Bprint(&bout, "%sFLAGS (", sep);
-			writeFlags(&bout, m, 1);
-			Bprint(&bout, ")");
-			break;
-		case FUid:
-			if(uids)
-				continue;
-			Bprint(&bout, "%sUID %lud", sep, m->uid);
-			break;
-		case FEnvelope:
-			Bprint(&bout, "%sENVELOPE ", sep);
-			fetchEnvelope(m);
-			break;
-		case FInternalDate:
-			Bprint(&bout, "%sINTERNALDATE ", sep);
-			Bimapdate(&bout, date2tm(&tm, m->unixDate));
-			break;
-		case FBody:
-			Bprint(&bout, "%sBODY ", sep);
-			fetchBodyStruct(m, &m->head, 0);
-			break;
-		case FBodyStruct:
-			Bprint(&bout, "%sBODYSTRUCTURE ", sep);
-			fetchBodyStruct(m, &m->head, 1);
-			break;
-		case FRfc822Size:
-			Bprint(&bout, "%sRFC822.SIZE %lud", sep, msgSize(m));
-			break;
-		case FRfc822:
-			f->part = FPAll;
-			Bprint(&bout, "%sRFC822", sep);
-			fetchBody(m, f);
-			break;
-		case FRfc822Head:
-			f->part = FPHead;
-			Bprint(&bout, "%sRFC822.HEADER", sep);
-			fetchBody(m, f);
-			break;
-		case FRfc822Text:
-			f->part = FPText;
-			Bprint(&bout, "%sRFC822.TEXT", sep);
-			fetchBody(m, f);
-			break;
-		case FBodySect:
-		case FBodyPeek:
-			Bprint(&bout, "%sBODY", sep);
-			fetchBody(fetchSect(m, f), f);
-			break;
-		}
-		sep = " ";
-	}
-	Bprint(&bout, ")\r\n");
-
-	return 1;
-}
-
-/*
- * print out section, part, headers;
- * find and return message section
- */
-Msg *
-fetchSect(Msg *m, Fetch *f)
-{
-	Bputc(&bout, '[');
-	BNList(&bout, f->sect, ".");
-	if(f->part != FPAll){
-		if(f->sect != nil)
-			Bputc(&bout, '.');
-		Bprint(&bout, "%s", fetchPartNames[f->part]);
-		if(f->hdrs != nil){
-			Bprint(&bout, " (");
-			BSList(&bout, f->hdrs, " ");
-			Bputc(&bout, ')');
-		}
-	}
-	Bprint(&bout, "]");
-	return findMsgSect(m, f->sect);
-}
-
-/*
- * actually return the body pieces
- */
-void
-fetchBody(Msg *m, Fetch *f)
-{
-	Pair p;
-	char *s, *t, *e, buf[BufSize + 2];
-	ulong n, start, stop, pos;
-	int fd, nn;
-
-	if(m == nil){
-		fetchBodyStr(f, "", 0);
-		return;
-	}
-	switch(f->part){
-	case FPHeadFields:
-	case FPHeadFieldsNot:
-		n = m->head.size + 3;
-		s = emalloc(n);
-		n = selectFields(s, n, m->head.buf, f->hdrs, f->part == FPHeadFields);
-		fetchBodyStr(f, s, n);
-		free(s);
-		return;
-	case FPHead:
-		fetchBodyStr(f, m->head.buf, m->head.size);
-		return;
-	case FPMime:
-		fetchBodyStr(f, m->mime.buf, m->mime.size);
-		return;
-	case FPAll:
-		fd = msgFile(m, "rawbody");
-		if(fd < 0){
-			msgDead(m);
-			fetchBodyStr(f, "", 0);
-			return;
-		}
-		p = fetchBodyPart(f, msgSize(m));
-		start = p.start;
-		if(start < m->head.size){
-			stop = p.stop;
-			if(stop > m->head.size)
-				stop = m->head.size;
-			Bwrite(&bout, &m->head.buf[start], stop - start);
-			start = 0;
-			stop = p.stop;
-			if(stop <= m->head.size){
-				close(fd);
-				return;
-			}
-		}else
-			start -= m->head.size;
-		stop = p.stop - m->head.size;
-		break;
-	case FPText:
-		fd = msgFile(m, "rawbody");
-		if(fd < 0){
-			msgDead(m);
-			fetchBodyStr(f, "", 0);
-			return;
-		}
-		p = fetchBodyPart(f, m->size);
-		start = p.start;
-		stop = p.stop;
-		break;
-	default:
-		fetchBodyStr(f, "", 0);
-		return;
-	}
-
-	/*
-	 * read in each block, convert \n without \r to \r\n.
-	 * this means partial fetch requires fetching everything
-	 * through stop, since we don't know how many \r's will be added
-	 */
-	buf[0] = ' ';
-	for(pos = 0; pos < stop; ){
-		n = BufSize;
-		if(n > stop - pos)
-			n = stop - pos;
-		n = read(fd, &buf[1], n);
-		if(n <= 0){
-			fetchBodyFill(stop - pos);
-			break;
-		}
-		e = &buf[n + 1];
-		*e = '\0';
-		for(s = &buf[1]; s < e && pos < stop; s = t + 1){
-			t = memchr(s, '\n', e - s);
-			if(t == nil)
-				t = e;
-			n = t - s;
-			if(pos < start){
-				if(pos + n <= start){
-					s = t;
-					pos += n;
-				}else{
-					s += start - pos;
-					pos = start;
-				}
-				n = t - s;
-			}
-			nn = n;
-			if(pos + nn > stop)
-				nn = stop - pos;
-			if(Bwrite(&bout, s, nn) != nn)
-				writeErr();
-			pos += n;
-			if(*t == '\n'){
-				if(t[-1] != '\r'){
-					if(pos >= start && pos < stop)
-						Bputc(&bout, '\r');
-					pos++;
-				}
-				if(pos >= start && pos < stop)
-					Bputc(&bout, '\n');
-				pos++;
-			}
-		}
-		buf[0] = e[-1];
-	}
-	close(fd);
-}
-
-/*
- * resolve the actual bounds of any partial fetch,
- * and print out the bounds & size of string returned
- */
-Pair
-fetchBodyPart(Fetch *f, ulong size)
-{
-	Pair p;
-	ulong start, stop;
-
-	start = 0;
-	stop = size;
-	if(f->partial){
-		start = f->start;
-		if(start > size)
-			start = size;
-		stop = start + f->size;
-		if(stop > size)
-			stop = size;
-		Bprint(&bout, "<%lud>", start);
-	}
-	Bprint(&bout, " {%lud}\r\n", stop - start);
-	p.start = start;
-	p.stop = stop;
-	return p;
-}
-
-/*
- * something went wrong fetching data
- * produce fill bytes for what we've committed to produce
- */
-void
-fetchBodyFill(ulong n)
-{
-	while(n-- > 0)
-		if(Bputc(&bout, ' ') < 0)
-			writeErr();
-}
-
-/*
- * return a simple string
- */
-void
-fetchBodyStr(Fetch *f, char *buf, ulong size)
-{
-	Pair p;
-
-	p = fetchBodyPart(f, size);
-	Bwrite(&bout, &buf[p.start], p.stop-p.start);
-}
-
-char*
-printnlist(NList *sect)
-{
-	static char buf[100];
-	char *p;
-
-	for(p= buf; sect; sect=sect->next){
-		p += sprint(p, "%ld", sect->n);
-		if(sect->next)
-			*p++ = '.';
-	}
-	*p = '\0';
-	return buf;
-}
-
-/*
- * find the numbered sub-part of the message
- */
-Msg*
-findMsgSect(Msg *m, NList *sect)
-{
-	ulong id;
-
-	for(; sect != nil; sect = sect->next){
-		id = sect->n;
-#ifdef HACK
-		/* HACK to solve extra level of structure not visible from upas/fs  */
-		if(m->kids == 0 && id == 1 && sect->next == nil){
-			if(m->mime.type->s && strcmp(m->mime.type->s, "message")==0)
-			if(m->mime.type->t && strcmp(m->mime.type->t, "rfc822")==0)
-			if(m->head.type->s && strcmp(m->head.type->s, "text")==0)
-			if(m->head.type->t && strcmp(m->head.type->t, "plain")==0)
-				break;
-		}
-		/* end of HACK */
-#endif HACK
-		for(m = m->kids; m != nil; m = m->next)
-			if(m->id == id)
-				break;
-		if(m == nil)
-			return nil;
-	}
-	return m;
-}
-
-void
-fetchEnvelope(Msg *m)
-{
-	Tm tm;
-
-	Bputc(&bout, '(');
-	Brfc822date(&bout, date2tm(&tm, m->info[IDate]));
-	Bputc(&bout, ' ');
-	Bimapstr(&bout, m->info[ISubject]);
-	Bputc(&bout, ' ');
-	Bimapaddr(&bout, m->from);
-	Bputc(&bout, ' ');
-	Bimapaddr(&bout, m->sender);
-	Bputc(&bout, ' ');
-	Bimapaddr(&bout, m->replyTo);
-	Bputc(&bout, ' ');
-	Bimapaddr(&bout, m->to);
-	Bputc(&bout, ' ');
-	Bimapaddr(&bout, m->cc);
-	Bputc(&bout, ' ');
-	Bimapaddr(&bout, m->bcc);
-	Bputc(&bout, ' ');
-	Bimapstr(&bout, m->info[IInReplyTo]);
-	Bputc(&bout, ' ');
-	Bimapstr(&bout, m->info[IMessageId]);
-	Bputc(&bout, ')');
-}
-
-void
-fetchBodyStruct(Msg *m, Header *h, int extensions)
-{
-	Msg *k;
-	ulong len;
-
-	if(msgIsMulti(h)){
-		Bputc(&bout, '(');
-		for(k = m->kids; k != nil; k = k->next)
-			fetchBodyStruct(k, &k->mime, extensions);
-
-		Bputc(&bout, ' ');
-		Bimapstr(&bout, h->type->t);
-
-		if(extensions){
-			Bputc(&bout, ' ');
-			BimapMimeParams(&bout, h->type->next);
-			fetchStructExt(h);
-		}
-
-		Bputc(&bout, ')');
-		return;
-	}
-
-	Bputc(&bout, '(');
-	if(h->type != nil){
-		Bimapstr(&bout, h->type->s);
-		Bputc(&bout, ' ');
-		Bimapstr(&bout, h->type->t);
-		Bputc(&bout, ' ');
-		BimapMimeParams(&bout, h->type->next);
-	}else
-		Bprint(&bout, "\"text\" \"plain\" NIL");
-
-	Bputc(&bout, ' ');
-	if(h->id != nil)
-		Bimapstr(&bout, h->id->s);
-	else
-		Bprint(&bout, "NIL");
-
-	Bputc(&bout, ' ');
-	if(h->description != nil)
-		Bimapstr(&bout, h->description->s);
-	else
-		Bprint(&bout, "NIL");
-
-	Bputc(&bout, ' ');
-	if(h->encoding != nil)
-		Bimapstr(&bout, h->encoding->s);
-	else
-		Bprint(&bout, "NIL");
-
-	/*
-	 * this is so strange: return lengths for a body[text] response,
-	 * except in the case of a multipart message, when return lengths for a body[] response
-	 */
-	len = m->size;
-	if(h == &m->mime)
-		len += m->head.size;
-	Bprint(&bout, " %lud", len);
-
-	len = m->lines;
-	if(h == &m->mime)
-		len += m->head.lines;
-
-	if(h->type == nil || cistrcmp(h->type->s, "text") == 0){
-		Bprint(&bout, " %lud", len);
-	}else if(msgIsRfc822(h)){
-		Bputc(&bout, ' ');
-		k = m;
-		if(h != &m->mime)
-			k = m->kids;
-		if(k == nil)
-			Bprint(&bout, "(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) (\"text\" \"plain\" NIL NIL NIL NIL 0 0) 0");
-		else{
-			fetchEnvelope(k);
-			Bputc(&bout, ' ');
-			fetchBodyStruct(k, &k->head, extensions);
-			Bprint(&bout, " %lud", len);
-		}
-	}
-
-	if(extensions){
-		Bputc(&bout, ' ');
-
-		/*
-		 * don't have the md5 laying around,
-		 * since the header & body have added newlines.
-		 */
-		Bprint(&bout, "NIL");
-
-		fetchStructExt(h);
-	}
-	Bputc(&bout, ')');
-}
-
-/*
- * common part of bodystructure extensions
- */
-void
-fetchStructExt(Header *h)
-{
-	Bputc(&bout, ' ');
-	if(h->disposition != nil){
-		Bputc(&bout, '(');
-		Bimapstr(&bout, h->disposition->s);
-		Bputc(&bout, ' ');
-		BimapMimeParams(&bout, h->disposition->next);
-		Bputc(&bout, ')');
-	}else
-		Bprint(&bout, "NIL");
-	Bputc(&bout, ' ');
-	if(h->language != nil){
-		if(h->language->next != nil)
-			BimapMimeParams(&bout, h->language->next);
-		else
-			Bimapstr(&bout, h->language->s);
-	}else
-		Bprint(&bout, "NIL");
-}
-
-int
-BimapMimeParams(Biobuf *b, MimeHdr *mh)
-{
-	char *sep;
-	int n;
-
-	if(mh == nil)
-		return Bprint(b, "NIL");
-
-	n = Bputc(b, '(');
-
-	sep = "";
-	for(; mh != nil; mh = mh->next){
-		n += Bprint(b, sep);
-		n += Bimapstr(b, mh->s);
-		n += Bputc(b, ' ');
-		n += Bimapstr(b, mh->t);
-		sep = " ";
-	}
-
-	n += Bputc(b, ')');
-	return n;
-}
-
-/*
- * print a list of addresses;
- * each address is printed as '(' personalName AtDomainList mboxName hostName ')'
- * the AtDomainList is always NIL
- */
-int
-Bimapaddr(Biobuf *b, MAddr *a)
-{
-	char *host, *sep;
-	int n;
-
-	if(a == nil)
-		return Bprint(b, "NIL");
-
-	n = Bputc(b, '(');
-	sep = "";
-	for(; a != nil; a = a->next){
-		n += Bprint(b, "%s(", sep);
-		n += Bimapstr(b, a->personal);
-		n += Bprint(b," NIL ");
-		n += Bimapstr(b, a->box);
-		n += Bputc(b, ' ');
-
-		/*
-		 * can't send NIL as hostName, since that is code for a group
-		 */
-		host = a->host;
-		if(host == nil)
-			host = "";
-		n += Bimapstr(b, host);
-
-		n += Bputc(b, ')');
-		sep = " ";
-	}
-	n += Bputc(b, ')');
-	return n;
-}
--- a/sys/src/cmd/ip/imap4d/fns.h
+++ /dev/null
@@ -1,125 +1,0 @@
-/*
- * sorted by 4,/^$/|sort -bd +1
- */
-int	fqid(int fd, Qid *qid);
-int	BNList(Biobuf *b, NList *nl, char *sep);
-int	BSList(Biobuf *b, SList *sl, char *sep);
-int	BimapMimeParams(Biobuf *b, MimeHdr *mh);
-int	Bimapaddr(Biobuf *b, MAddr *a);
-int	Bimapdate(Biobuf *b, Tm *tm);
-int	Bimapstr(Biobuf *b, char *s);
-int	Brfc822date(Biobuf *b, Tm *tm);
-int	appendSave(char *mbox, int flags, char *head, Biobuf *b, long n);
-void	bye(char *fmt, ...);
-int	cdCreate(char *dir, char *file, int mode, ulong perm);
-int	cdExists(char *dir, char *file);
-Dir	*cdDirstat(char *dir, char *file);
-int	cdDirwstat(char *dir, char *file, Dir *d);
-int	cdOpen(char *dir, char *file, int mode);
-int	cdRemove(char *dir, char *file);
-MbLock	*checkBox(Box *box, int imped);
-int	ciisprefix(char *pre, char *s);
-int	cistrcmp(char*, char*);
-int	cistrncmp(char*, char*, int);
-char	*cistrstr(char *s, char *sub);
-void	closeBox(Box *box, int opened);
-void	closeImp(Box *box, MbLock *ml);
-int	copyBox(char *from, char *to, int doremove);
-int	copyCheck(Box *box, Msg *m, int uids, void *v);
-int	copySave(Box *box, Msg *m, int uids, void *vs);
-char	*cramauth(void);
-int	createBox(char *mbox, int dir);
-Tm	*date2tm(Tm *tm, char *date);
-int	decmutf7(char *out, int nout, char *in);
-int	deleteMsgs(Box *box);
-void	debuglog(char *fmt, ...);
-void	*emalloc(ulong);
-int	emptyImp(char *mbox);
-void	enableForwarding(void);
-int	encmutf7(char *out, int nout, char *in);
-void	*erealloc(void*, ulong);
-char	*estrdup(char*);
-int	expungeMsgs(Box *box, int send);
-void	*ezmalloc(ulong);
-void	fetchBodyFill(ulong n);
-void	fetchBody(Msg *m, Fetch *f);
-Pair	fetchBodyPart(Fetch *f, ulong size);
-void	fetchBodyStr(Fetch *f, char *buf, ulong size);
-void	fetchBodyStruct(Msg *m, Header *h, int extensions);
-void	fetchEnvelope(Msg *m);
-int	fetchMsg(Box *box, Msg *m, int uids, void *fetch);
-Msg	*fetchSect(Msg *m, Fetch *f);
-int	fetchSeen(Box *box, Msg *m, int uids, void *vf);
-void	fetchStructExt(Header *h);
-Msg	*findMsgSect(Msg *m, NList *sect);
-int	forMsgs(Box *box, MsgSet *ms, ulong max, int uids, int (*f)(Box*, Msg*, int, void*), void *rock);
-void	freeMsg(Msg *m);
-ulong	imap4DateTime(char *date);
-int	imap4Date(Tm *tm, char *date);
-int	imap4date(char *s, int n, Tm *tm);
-int	imapTmp(void);
-char	*impName(char *name);
-int	infoIsNil(char *s);
-int	isdotdot(char*);
-int	isprefix(char *pre, char *s);
-int	issuffix(char *suf, char *s);
-int	listBoxes(char *cmd, char *ref, char *pat);
-char	*loginauth(void);
-int	lsubBoxes(char *cmd, char *ref, char *pat);
-char	*maddrStr(MAddr *a);
-ulong	mapFlag(char *name);
-ulong	mapInt(NamedInt *map, char *name);
-void	mbLockRefresh(MbLock *ml);
-int	mbLocked(void);
-MbLock	*mbLock(void);
-void	mbUnlock(MbLock *ml);
-char	*mboxName(char*);
-Fetch	*mkFetch(int op, Fetch *next);
-NList	*mkNList(ulong n, NList *next);
-SList	*mkSList(char *s, SList *next);
-Store	*mkStore(int sign, int op, int flags);
-int	moveBox(char *from, char *to);
-void	msgDead(Msg *m);
-int	msgFile(Msg *m, char *f);
-int	msgInfo(Msg *m);
-int	msgIsMulti(Header *h);
-int	msgIsRfc822(Header *h);
-int	msgSeen(Box *box, Msg *m);
-ulong	msgSize(Msg *m);
-int	msgStruct(Msg *m, int top);
-char	*mutf7str(char*);
-int	myChdir(char *dir);
-int	okMbox(char *mbox);
-Box	*openBox(char *name, char *fsname, int writable);
-int	openLocked(char *dir, char *file, int mode);
-void	parseErr(char *msg);
-AuthInfo	*passLogin(char*, char*);
-char	*readFile(int fd);
-void	resetCurDir(void);
-Fetch	*revFetch(Fetch *f);
-NList	*revNList(NList *s);
-SList	*revSList(SList *s);
-int	rfc822date(char *s, int n, Tm *tm);
-int	searchMsg(Msg *m, Search *s);
-long	selectFields(char *dst, long n, char *hdr, SList *fields, int matches);
-void	sendFlags(Box *box, int uids);
-void	setFlags(Box *box, Msg *m, int f);
-void	setupuser(AuthInfo*);
-int	storeMsg(Box *box, Msg *m, int uids, void *fetch);
-char	*strmutf7(char*);
-void	strrev(char *s, char *e);
-int	subscribe(char *mbox, int how);
-void	wrImpFlags(char *buf, int flags, int killRecent);
-void	writeErr(void);
-void	writeFlags(Biobuf *b, Msg *m, int recentOk);
-
-#pragma	varargck argpos	bye		1
-#pragma	varargck argpos	debuglog	1
-
-#define	MK(t)		((t*)emalloc(sizeof(t)))
-#define	MKZ(t)		((t*)ezmalloc(sizeof(t)))
-#define	MKN(t,n)	((t*)emalloc((n)*sizeof(t)))
-#define	MKNZ(t,n)	((t*)ezmalloc((n)*sizeof(t)))
-#define	MKNA(t,at,n)	((t*)emalloc(sizeof(t) + (n)*sizeof(at)))
-
-#define STRLEN(cs)	(sizeof(cs)-1)
--- a/sys/src/cmd/ip/imap4d/folder.c
+++ /dev/null
@@ -1,398 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-static	int	copyData(int ffd, int tfd, MbLock *ml);
-static	MbLock	mLock =
-{
-	.fd = -1
-};
-
-static char curDir[MboxNameLen];
-
-void
-resetCurDir(void)
-{
-	curDir[0] = '\0';
-}
-
-int
-myChdir(char *dir)
-{
-	if(strcmp(dir, curDir) == 0)
-		return 0;
-	if(dir[0] != '/' || strlen(dir) > MboxNameLen)
-		return -1;
-	strcpy(curDir, dir);
-	if(chdir(dir) < 0){
-		werrstr("mychdir failed: %r");
-		return -1;
-	}
-	return 0;
-}
-
-int
-cdCreate(char *dir, char *file, int mode, ulong perm)
-{
-	if(myChdir(dir) < 0)
-		return -1;
-	return create(file, mode, perm);
-}
-
-Dir*
-cdDirstat(char *dir, char *file)
-{
-	if(myChdir(dir) < 0)
-		return nil;
-	return dirstat(file);
-}
-
-int
-cdExists(char *dir, char *file)
-{
-	Dir *d;
-
-	d = cdDirstat(dir, file);
-	if(d == nil)
-		return 0;
-	free(d);
-	return 1;
-}
-
-int
-cdDirwstat(char *dir, char *file, Dir *d)
-{
-	if(myChdir(dir) < 0)
-		return -1;
-	return dirwstat(file, d);
-}
-
-int
-cdOpen(char *dir, char *file, int mode)
-{
-	if(myChdir(dir) < 0)
-		return -1;
-	return open(file, mode);
-}
-
-int
-cdRemove(char *dir, char *file)
-{
-	if(myChdir(dir) < 0)
-		return -1;
-	return remove(file);
-}
-
-/*
- * open the one true mail lock file
- */
-MbLock*
-mbLock(void)
-{
-	int i;
-
-	if(mLock.fd >= 0)
-		bye("mail lock deadlock");
-	for(i = 0; i < 5; i++){
-		mLock.fd = openLocked(mboxDir, "L.mbox", OREAD);
-		if(mLock.fd >= 0)
-			return &mLock;
-		sleep(1000);
-	}
-	return nil;
-}
-
-void
-mbUnlock(MbLock *ml)
-{
-	if(ml != &mLock)
-		bye("bad mail unlock");
-	if(ml->fd < 0)
-		bye("mail unlock when not locked");
-	close(ml->fd);
-	ml->fd = -1;
-}
-
-void
-mbLockRefresh(MbLock *ml)
-{
-	char buf[1];
-
-	seek(ml->fd, 0, 0);
-	read(ml->fd, buf, 1);
-}
-
-int
-mbLocked(void)
-{
-	return mLock.fd >= 0;
-}
-
-char*
-impName(char *name)
-{
-	char *s;
-	int n;
-
-	if(cistrcmp(name, "inbox") == 0)
-		if(access("msgs", AEXIST) == 0)
-			name = "msgs";
-		else
-			name = "mbox";
-	n = strlen(name) + STRLEN(".imp") + 1;
-	s = binalloc(&parseBin, n, 0);
-	if(s == nil)
-		return nil;
-	snprint(s, n, "%s.imp", name);
-	return s;
-}
-
-/*
- * massage the mailbox name into something valid
- * eliminates all .', and ..',s, redundatant and trailing /'s.
- */
-char *
-mboxName(char *s)
-{
-	char *ss;
-
-	ss = mutf7str(s);
-	if(ss == nil)
-		return nil;
-	cleanname(ss);
-	return ss;
-}
-
-char *
-strmutf7(char *s)
-{
-	char *m;
-	int n;
-
-	n = strlen(s) * MUtf7Max + 1;
-	m = binalloc(&parseBin, n, 0);
-	if(m == nil)
-		return nil;
-	if(encmutf7(m, n, s) < 0)
-		return nil;
-	return m;
-}
-
-char *
-mutf7str(char *s)
-{
-	char *m;
-	int n;
-
-	/*
-	 * n = strlen(s) * UTFmax / (2.67) + 1
-	 * UTFMax / 2.67 == 3 / (8/3) == 9 / 8
-	 */
-	n = strlen(s);
-	n = (n * 9 + 7) / 8 + 1;
-	m = binalloc(&parseBin, n, 0);
-	if(m == nil)
-		return nil;
-	if(decmutf7(m, n, s) < 0)
-		return nil;
-	return m;
-}
-
-void
-splitr(char *s, int c, char **left, char **right)
-{
-	char *d;
-	int n;
-
-	n = strlen(s);
-	d = binalloc(&parseBin, n + 1, 0);
-	if(d == nil)
-		parseErr("out of memory");
-	strcpy(d, s);
-	s = strrchr(d, c);
-	if(s != nil){
-		*left = d;
-		*s++ = '\0';
-		*right = s;
-	}else{
-		*right = d;
-		*left = d + n;
-	}
-}
-
-/*
- * create the mailbox and all intermediate components
- * a trailing / implies the new mailbox is a directory;
- * otherwise, it's a file.
- *
- * return with the file open for write, or directory open for read.
- */
-int
-createBox(char *mbox, int dir)
-{
-	char *m;
-	int fd;
-
-	fd = -1;
-	for(m = mbox; *m; m++){
-		if(*m == '/'){
-			*m = '\0';
-			if(access(mbox, AEXIST) < 0){
-				if(fd >= 0)
-					close(fd);
-				fd = cdCreate(mboxDir, mbox, OREAD, DMDIR|0775);
-				if(fd < 0)
-					return -1;
-			}
-			*m = '/';
-		}
-	}
-	if(dir)
-		fd = cdCreate(mboxDir, mbox, OREAD, DMDIR|0775);
-	else
-		fd = cdCreate(mboxDir, mbox, OWRITE, 0664);
-	return fd;
-}
-
-/*
- * move one mail folder to another
- * destination mailbox doesn't exist.
- * the source folder may be a directory or a mailbox,
- * and may be in the same directory as the destination,
- * or a completely different directory.
- */
-int
-moveBox(char *from, char *to)
-{
-	Dir *d;
-	char *fd, *fe, *td, *te, *fimp;
-
-	splitr(from, '/', &fd, &fe);
-	splitr(to, '/', &td, &te);
-
-	/*
-	 * in the same directory: try rename
-	 */
-	d = cdDirstat(mboxDir, from);
-	if(d == nil)
-		return 0;
-	if(strcmp(fd, td) == 0){
-		nulldir(d);
-		d->name = te;
-		if(cdDirwstat(mboxDir, from, d) >= 0){
-			fimp = impName(from);
-			d->name = impName(te);
-			cdDirwstat(mboxDir, fimp, d);
-			free(d);
-			return 1;
-		}
-	}
-
-	/*
-	 * directory copy is too hard for now
-	 */
-	if(d->mode & DMDIR)
-		return 0;
-	free(d);
-
-	return copyBox(from, to, 1);
-}
-
-/*
- * copy the contents of one mailbox to another
- * either truncates or removes the source box if it succeeds.
- */
-int
-copyBox(char *from, char *to, int doremove)
-{
-	MbLock *ml;
-	char *fimp, *timp;
-	int ffd, tfd, ok;
-
-	if(cistrcmp(from, "inbox") == 0)
-		if(access("msgs", AEXIST) == 0)
-			from = "msgs";
-		else
-			from = "mbox";
-
-	ml = mbLock();
-	if(ml == nil)
-		return 0;
-	ffd = openLocked(mboxDir, from, OREAD);
-	if(ffd < 0){
-		mbUnlock(ml);
-		return 0;
-	}
-	tfd = createBox(to, 0);
-	if(tfd < 0){
-		mbUnlock(ml);
-		close(ffd);
-		return 0;
-	}
-
-	ok = copyData(ffd, tfd, ml);
-	close(ffd);
-	close(tfd);
-	if(!ok){
-		mbUnlock(ml);
-		return 0;
-	}
-
-	fimp = impName(from);
-	timp = impName(to);
-	if(fimp != nil && timp != nil){
-		ffd = cdOpen(mboxDir, fimp, OREAD);
-		if(ffd >= 0){
-			tfd = cdCreate(mboxDir, timp, OWRITE, 0664);
-			if(tfd >= 0){
-				copyData(ffd, tfd, ml);
-				close(tfd);
-			}
-			close(ffd);
-		}
-	}
-	cdRemove(mboxDir, fimp);
-	if(doremove)
-		cdRemove(mboxDir, from);
-	else
-		close(cdOpen(mboxDir, from, OWRITE|OTRUNC));
-	mbUnlock(ml);
-	return 1;
-}
-
-/*
- * copies while holding the mail lock,
- * then tries to copy permissions and group ownership
- */
-static int
-copyData(int ffd, int tfd, MbLock *ml)
-{
-	Dir *fd, td;
-	char buf[BufSize];
-	int n;
-
-	for(;;){
-		n = read(ffd, buf, BufSize);
-		if(n <= 0){
-			if(n < 0)
-				return 0;
-			break;
-		}
-		if(write(tfd, buf, n) != n)
-			return 0;
-		mbLockRefresh(ml);
-	}
-	fd = dirfstat(ffd);
-	if(fd != nil){
-		nulldir(&td);
-		td.mode = fd->mode;
-		if(dirfwstat(tfd, &td) >= 0){
-			nulldir(&td);
-			td.gid = fd->gid;
-			dirfwstat(tfd, &td);
-		}
-	}
-	return 1;
-}
--- a/sys/src/cmd/ip/imap4d/imap4d.c
+++ /dev/null
@@ -1,2102 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <auth.h>
-#include <bio.h>
-#include "imap4d.h"
-
-/*
- * these should be in libraries
- */
-char	*csquery(char *attr, char *val, char *rattr);
-
-/*
- * /lib/rfc/rfc2060 imap4rev1
- * /lib/rfc/rfc2683 is implementation advice
- * /lib/rfc/rfc2342 is namespace capability
- * /lib/rfc/rfc2222 is security protocols
- * /lib/rfc/rfc1731 is security protocols
- * /lib/rfc/rfc2221 is LOGIN-REFERRALS
- * /lib/rfc/rfc2193 is MAILBOX-REFERRALS
- * /lib/rfc/rfc2177 is IDLE capability
- * /lib/rfc/rfc2195 is CRAM-MD5 authentication
- * /lib/rfc/rfc2088 is LITERAL+ capability
- * /lib/rfc/rfc1760 is S/Key authentication
- *
- * outlook uses "Secure Password Authentication" aka ntlm authentication
- *
- * capabilities from nslocum
- * CAPABILITY IMAP4 IMAP4REV1 NAMESPACE IDLE SCAN SORT MAILBOX-REFERRALS LOGIN-REFERRALS AUTH=LOGIN THREAD=ORDEREDSUBJECT
- */
-
-typedef struct	ParseCmd	ParseCmd;
-
-enum
-{
-	UlongMax	= 4294967295,
-};
-
-struct ParseCmd
-{
-	char	*name;
-	void	(*f)(char *tg, char *cmd);
-};
-
-static	void	appendCmd(char *tg, char *cmd);
-static	void	authenticateCmd(char *tg, char *cmd);
-static	void	capabilityCmd(char *tg, char *cmd);
-static	void	closeCmd(char *tg, char *cmd);
-static	void	copyCmd(char *tg, char *cmd);
-static	void	createCmd(char *tg, char *cmd);
-static	void	deleteCmd(char *tg, char *cmd);
-static	void	expungeCmd(char *tg, char *cmd);
-static	void	fetchCmd(char *tg, char *cmd);
-static	void	idleCmd(char *tg, char *cmd);
-static	void	listCmd(char *tg, char *cmd);
-static	void	loginCmd(char *tg, char *cmd);
-static	void	logoutCmd(char *tg, char *cmd);
-static	void	namespaceCmd(char *tg, char *cmd);
-static	void	noopCmd(char *tg, char *cmd);
-static	void	renameCmd(char *tg, char *cmd);
-static	void	searchCmd(char *tg, char *cmd);
-static	void	selectCmd(char *tg, char *cmd);
-static	void	statusCmd(char *tg, char *cmd);
-static	void	storeCmd(char *tg, char *cmd);
-static	void	subscribeCmd(char *tg, char *cmd);
-static	void	uidCmd(char *tg, char *cmd);
-static	void	unsubscribeCmd(char *tg, char *cmd);
-
-static	void	copyUCmd(char *tg, char *cmd, int uids);
-static	void	fetchUCmd(char *tg, char *cmd, int uids);
-static	void	searchUCmd(char *tg, char *cmd, int uids);
-static	void	storeUCmd(char *tg, char *cmd, int uids);
-
-static	void	imap4(int);
-static	void	status(int expungeable, int uids);
-static	void	cleaner(void);
-static	void	check(void);
-static	int	catcher(void*, char*);
-
-static	Search	*searchKey(int first);
-static	Search	*searchKeys(int first, Search *tail);
-static	char	*astring(void);
-static	char	*atomString(char *disallowed, char *initial);
-static	char	*atom(void);
-static	void	badsyn(void);
-static	void	clearcmd(void);
-static	char	*command(void);
-static	void	crnl(void);
-static	Fetch	*fetchAtt(char *s, Fetch *f);
-static	Fetch	*fetchWhat(void);
-static	int	flagList(void);
-static	int	flags(void);
-static	int	getc(void);
-static	char	*listmbox(void);
-static	char	*literal(void);
-static	ulong	litlen(void);
-static	MsgSet	*msgSet(int);
-static	void	mustBe(int c);
-static	ulong	number(int nonzero);
-static	int	peekc(void);
-static	char	*quoted(void);
-static	void	sectText(Fetch *f, int mimeOk);
-static	ulong	seqNo(void);
-static	Store	*storeWhat(void);
-static	char	*tag(void);
-static	ulong	uidNo(void);
-static	void	ungetc(void);
-
-static	ParseCmd	SNonAuthed[] =
-{
-	{"capability",		capabilityCmd},
-	{"logout",		logoutCmd},
-	{"x-exit",		logoutCmd},
-	{"noop",		noopCmd},
-
-	{"login",		loginCmd},
-	{"authenticate",	authenticateCmd},
-
-	nil
-};
-
-static	ParseCmd	SAuthed[] =
-{
-	{"capability",		capabilityCmd},
-	{"logout",		logoutCmd},
-	{"x-exit",		logoutCmd},
-	{"noop",		noopCmd},
-
-	{"append",		appendCmd},
-	{"create",		createCmd},
-	{"delete",		deleteCmd},
-	{"examine",		selectCmd},
-	{"select",		selectCmd},
-	{"idle",		idleCmd},
-	{"list",		listCmd},
-	{"lsub",		listCmd},
-	{"namespace",		namespaceCmd},
-	{"rename",		renameCmd},
-	{"status",		statusCmd},
-	{"subscribe",		subscribeCmd},
-	{"unsubscribe",		unsubscribeCmd},
-
-	nil
-};
-
-static	ParseCmd	SSelected[] =
-{
-	{"capability",		capabilityCmd},
-	{"logout",		logoutCmd},
-	{"x-exit",		logoutCmd},
-	{"noop",		noopCmd},
-
-	{"append",		appendCmd},
-	{"create",		createCmd},
-	{"delete",		deleteCmd},
-	{"examine",		selectCmd},
-	{"select",		selectCmd},
-	{"idle",		idleCmd},
-	{"list",		listCmd},
-	{"lsub",		listCmd},
-	{"namespace",		namespaceCmd},
-	{"rename",		renameCmd},
-	{"status",		statusCmd},
-	{"subscribe",		subscribeCmd},
-	{"unsubscribe",		unsubscribeCmd},
-
-	{"check",		noopCmd},
-	{"close",		closeCmd},
-	{"copy",		copyCmd},
-	{"expunge",		expungeCmd},
-	{"fetch",		fetchCmd},
-	{"search",		searchCmd},
-	{"store",		storeCmd},
-	{"uid",			uidCmd},
-
-	nil
-};
-
-static	char		*atomStop = "(){%*\"\\";
-static	Chalstate	*chal;
-static	int		chaled;
-static	ParseCmd	*imapState;
-static	jmp_buf		parseJmp;
-static	char		*parseMsg;
-static	int		allowPass;
-static	int		allowCR;
-static	int		exiting;
-static	QLock		imaplock;
-static	int		idlepid = -1;
-
-Biobuf	bout;
-Biobuf	bin;
-char	username[UserNameLen];
-char	mboxDir[MboxNameLen];
-char	*servername;
-char	*site;
-char	*remote;
-Box	*selected;
-Bin	*parseBin;
-int	debug;
-
-void
-main(int argc, char *argv[])
-{
-	char *s, *t;
-	int preauth, n;
-
-	Binit(&bin, 0, OREAD);
-	Binit(&bout, 1, OWRITE);
-
-	/* for auth */
-	fmtinstall('H', encodefmt);
-	fmtinstall('[', encodefmt);
-
-	preauth = 0;
-	allowPass = 0;
-	allowCR = 0;
-	ARGBEGIN{
-	case 'a':
-		preauth = 1;
-		break;
-	case 'd':
-		site = ARGF();
-		break;
-	case 'c':
-		allowCR = 1;
-		break;
-	case 'p':
-		allowPass = 1;
-		break;
-	case 'r':
-		remote = ARGF();
-		break;
-	case 's':
-		servername = ARGF();
-		break;
-	case 'v':
-		debug = 1;
-		debuglog("imap4d debugging enabled\n");
-		break;
-	default:
-		fprint(2, "usage: ip/imap4d [-acpv] [-d site] [-r remotehost] [-s servername]\n");
-		bye("usage");
-		break;
-	}ARGEND
-
-	if(allowPass && allowCR){
-		fprint(2, "%s: -c and -p are mutually exclusive\n", argv0);
-		bye("usage");
-	}
-
-	if(preauth)
-		setupuser(nil);
-
-	if(servername == nil){
-		servername = csquery("sys", sysname(), "dom");
-		if(servername == nil)
-			servername = sysname();
-		if(servername == nil){
-			fprint(2, "ip/imap4d can't find server name: %r\n");
-			bye("can't find system name");
-		}
-	}
-	if(site == nil){
-		t = getenv("site");
-		if(t == nil)
-			site = servername;
-		else{
-			n = strlen(t);
-			s = strchr(servername, '.');
-			if(s == nil)
-				s = servername;
-			else
-				s++;
-			n += strlen(s) + 2;
-			site = emalloc(n);
-			snprint(site, n, "%s.%s", t, s);
-		}
-	}
-
-	rfork(RFNOTEG|RFREND);
-
-	atnotify(catcher, 1);
-	qlock(&imaplock);
-	atexit(cleaner);
-	imap4(preauth);
-}
-
-static void
-imap4(int preauth)
-{
-	char *volatile tg;
-	char *volatile cmd;
-	ParseCmd *st;
-
-	if(preauth){
-		Bprint(&bout, "* preauth %s IMAP4rev1 server ready user %s authenticated\r\n", servername, username);
-		imapState = SAuthed;
-	}else{
-		Bprint(&bout, "* OK %s IMAP4rev1 server ready\r\n", servername);
-		imapState = SNonAuthed;
-	}
-	if(Bflush(&bout) < 0)
-		writeErr();
-
-	chaled = 0;
-
-	tg = nil;
-	cmd = nil;
-	if(setjmp(parseJmp)){
-		if(tg == nil)
-			Bprint(&bout, "* bad empty command line: %s\r\n", parseMsg);
-		else if(cmd == nil)
-			Bprint(&bout, "%s BAD no command: %s\r\n", tg, parseMsg);
-		else
-			Bprint(&bout, "%s BAD %s %s\r\n", tg, cmd, parseMsg);
-		clearcmd();
-		if(Bflush(&bout) < 0)
-			writeErr();
-		binfree(&parseBin);
-	}
-	for(;;){
-		if(mbLocked())
-			bye("internal error: mailbox lock held");
-		tg = nil;
-		cmd = nil;
-		tg = tag();
-		mustBe(' ');
-		cmd = atom();
-
-		/*
-		 * note: outlook express is broken: it requires echoing the
-		 * command as part of matching response
-		 */
-		for(st = imapState; st->name != nil; st++){
-			if(cistrcmp(cmd, st->name) == 0){
-				(*st->f)(tg, cmd);
-				break;
-			}
-		}
-		if(st->name == nil){
-			clearcmd();
-			Bprint(&bout, "%s BAD %s illegal command\r\n", tg, cmd);
-		}
-
-		if(Bflush(&bout) < 0)
-			writeErr();
-		binfree(&parseBin);
-	}
-}
-
-void
-bye(char *fmt, ...)
-{
-	va_list arg;
-
-	va_start(arg, fmt);
-	Bprint(&bout, "* bye ");
-	Bvprint(&bout, fmt, arg);
-	Bprint(&bout, "\r\n");
-	Bflush(&bout);
-exits("rob2");
-	exits(0);
-}
-
-void
-parseErr(char *msg)
-{
-	parseMsg = msg;
-	longjmp(parseJmp, 1);
-}
-
-/*
- * an error occured while writing to the client
- */
-void
-writeErr(void)
-{
-	cleaner();
-	_exits("connection closed");
-}
-
-static int
-catcher(void *v, char *msg)
-{
-	USED(v);
-	if(strstr(msg, "closed pipe") != nil)
-		return 1;
-	return 0;
-}
-
-/*
- * wipes out the idleCmd backgroung process if it is around.
- * this can only be called if the current proc has qlocked imaplock.
- * it must be the last piece of imap4d code executed.
- */
-static void
-cleaner(void)
-{
-	int i;
-
-	if(idlepid < 0)
-		return;
-	exiting = 1;
-	close(0);
-	close(1);
-	close(2);
-
-	/*
-	 * the other proc is either stuck in a read, a sleep,
-	 * or is trying to lock imap4lock.
-	 * get him out of it so he can exit cleanly
-	 */
-	qunlock(&imaplock);
-	for(i = 0; i < 4; i++)
-		postnote(PNGROUP, getpid(), "die");
-}
-
-/*
- * send any pending status updates to the client
- * careful: shouldn't exit, because called by idle polling proc
- *
- * can't always send pending info
- * in particular, can't send expunge info
- * in response to a fetch, store, or search command.
- * 
- * rfc2060 5.2:	server must send mailbox size updates
- * rfc2060 5.2:	server may send flag updates
- * rfc2060 5.5:	servers prohibited from sending expunge while fetch, store, search in progress
- * rfc2060 7:	in selected state, server checks mailbox for new messages as part of every command
- * 		sends untagged EXISTS and RECENT respsonses reflecting new size of the mailbox
- * 		should also send appropriate untagged FETCH and EXPUNGE messages if another agent
- * 		changes the state of any message flags or expunges any messages
- * rfc2060 7.4.1	expunge server response must not be sent when no command is in progress,
- * 		nor while responding to a fetch, stort, or search command (uid versions are ok)
- * 		command only "in progress" after entirely parsed.
- *
- * strategy for third party deletion of messages or of a mailbox
- *
- * deletion of a selected mailbox => act like all message are expunged
- *	not strictly allowed by rfc2180, but close to method 3.2.
- *
- * renaming same as deletion
- *
- * copy
- *	reject iff a deleted message is in the request
- *
- * search, store, fetch operations on expunged messages
- *	ignore the expunged messages
- *	return tagged no if referenced
- */
-static void
-status(int expungeable, int uids)
-{
-	int tell;
-
-	if(!selected)
-		return;
-	tell = 0;
-	if(expungeable)
-		tell = expungeMsgs(selected, 1);
-	if(selected->sendFlags)
-		sendFlags(selected, uids);
-	if(tell || selected->toldMax != selected->max){
-		Bprint(&bout, "* %lud EXISTS\r\n", selected->max);
-		selected->toldMax = selected->max;
-	}
-	if(tell || selected->toldRecent != selected->recent){
-		Bprint(&bout, "* %lud RECENT\r\n", selected->recent);
-		selected->toldRecent = selected->recent;
-	}
-	if(tell)
-		closeImp(selected, checkBox(selected, 1));
-}
-
-/*
- * careful: can't exit, because called by idle polling proc
- */
-static void
-check(void)
-{
-	if(!selected)
-		return;
-	checkBox(selected, 0);
-	status(1, 0);
-}
-
-static void
-appendCmd(char *tg, char *cmd)
-{
-	char *mbox, head[128];
-	ulong t, n, now;
-	int flags, ok;
-
-	mustBe(' ');
-	mbox = astring();
-	mustBe(' ');
-	flags = 0;
-	if(peekc() == '('){
-		flags = flagList();
-		mustBe(' ');
-	}
-	now = time(nil);
-	if(peekc() == '"'){
-		t = imap4DateTime(quoted());
-		if(t == ~0)
-			parseErr("illegal date format");
-		mustBe(' ');
-		if(t > now)
-			t = now;
-	}else
-		t = now;
-	n = litlen();
-
-	mbox = mboxName(mbox);
-	if(mbox == nil || !okMbox(mbox)){
-		check();
-		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
-		return;
-	}
-	if(!cdExists(mboxDir, mbox)){
-		check();
-		Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd);
-		return;
-	}
-
-	snprint(head, sizeof(head), "From %s %s", username, ctime(t));
-	ok = appendSave(mbox, flags, head, &bin, n);
-	crnl();
-	check();
-	if(ok)
-		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-	else
-		Bprint(&bout, "%s NO %s message save failed\r\n", tg, cmd);
-}
-
-static void
-authenticateCmd(char *tg, char *cmd)
-{
-	char *s, *t;
-
-	mustBe(' ');
-	s = atom();
-	crnl();
-	auth_freechal(chal);
-	chal = nil;
-	if(cistrcmp(s, "cram-md5") == 0){
-		t = cramauth();
-		if(t == nil){
-			Bprint(&bout, "%s OK %s\r\n", tg, cmd);
-			imapState = SAuthed;
-		}else
-			Bprint(&bout, "%s NO %s failed %s\r\n", tg, cmd, t);
-	}else
-		Bprint(&bout, "%s NO %s unsupported authentication protocol\r\n", tg, cmd);
-}
-
-static void
-capabilityCmd(char *tg, char *cmd)
-{
-	crnl();
-	check();
-// nslocum's capabilities
-//	Bprint(&bout, "* CAPABILITY IMAP4 IMAP4REV1 NAMESPACE IDLE SCAN SORT MAILBOX-REFERRALS LOGIN-REFERRALS AUTH=LOGIN THREAD=ORDEREDSUBJECT\r\n");
-	Bprint(&bout, "* CAPABILITY IMAP4REV1 IDLE NAMESPACE AUTH=CRAM-MD5\r\n");
-	Bprint(&bout, "%s OK %s\r\n", tg, cmd);
-}
-
-static void
-closeCmd(char *tg, char *cmd)
-{
-	crnl();
-	imapState = SAuthed;
-	closeBox(selected, 1);
-	selected = nil;
-	Bprint(&bout, "%s OK %s mailbox closed, now in authenticated state\r\n", tg, cmd);
-}
-
-/*
- * note: message id's are before any pending expunges
- */
-static void
-copyCmd(char *tg, char *cmd)
-{
-	copyUCmd(tg, cmd, 0);
-}
-
-static void
-copyUCmd(char *tg, char *cmd, int uids)
-{
-	MsgSet *ms;
-	char *uid, *mbox;
-	ulong max;
-	int ok;
-
-	mustBe(' ');
-	ms = msgSet(uids);
-	mustBe(' ');
-	mbox = astring();
-	crnl();
-
-	uid = "";
-	if(uids)
-		uid = "uid ";
-
-	mbox = mboxName(mbox);
-	if(mbox == nil || !okMbox(mbox)){
-		status(1, uids);
-		Bprint(&bout, "%s NO %s%s bad mailbox\r\n", tg, uid, cmd);
-		return;
-	}
-	if(!cdExists(mboxDir, mbox)){
-		check();
-		Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd);
-		return;
-	}
-
-	max = selected->max;
-	checkBox(selected, 0);
-	ok = forMsgs(selected, ms, max, uids, copyCheck, nil);
-	if(ok)
-		ok = forMsgs(selected, ms, max, uids, copySave, mbox);
-
-	status(1, uids);
-	if(ok)
-		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
-	else
-		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
-}
-
-static void
-createCmd(char *tg, char *cmd)
-{
-	char *mbox, *m;
-	int fd, slash;
-
-	mustBe(' ');
-	mbox = astring();
-	crnl();
-	check();
-
-	m = strchr(mbox, '\0');
-	slash = m != mbox && m[-1] == '/';
-	mbox = mboxName(mbox);
-	if(mbox == nil || !okMbox(mbox)){
-		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
-		return;
-	}
-	if(cistrcmp(mbox, "inbox") == 0){
-		Bprint(&bout, "%s NO %s cannot remotely create INBOX\r\n", tg, cmd);
-		return;
-	}
-	if(access(mbox, AEXIST) >= 0){
-		Bprint(&bout, "%s NO %s mailbox already exists\r\n", tg, cmd);
-		return;
-	}
-
-	fd = createBox(mbox, slash);
-	close(fd);
-	if(fd < 0)
-		Bprint(&bout, "%s NO %s cannot create mailbox %s\r\n", tg, cmd, mbox);
-	else
-		Bprint(&bout, "%s OK %s %s completed\r\n", tg, mbox, cmd);
-}
-
-static void
-deleteCmd(char *tg, char *cmd)
-{
-	char *mbox, *imp;
-
-	mustBe(' ');
-	mbox = astring();
-	crnl();
-	check();
-
-	mbox = mboxName(mbox);
-	if(mbox == nil || !okMbox(mbox)){
-		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
-		return;
-	}
-
-	imp = impName(mbox);
-	if(cistrcmp(mbox, "inbox") == 0
-	|| imp != nil && cdRemove(mboxDir, imp) < 0 && cdExists(mboxDir, imp)
-	|| cdRemove(mboxDir, mbox) < 0)
-		Bprint(&bout, "%s NO %s cannot delete mailbox %s\r\n", tg, cmd, mbox);
-	else
-		Bprint(&bout, "%s OK %s %s completed\r\n", tg, mbox, cmd);
-}
-
-static void
-expungeCmd(char *tg, char *cmd)
-{
-	int ok;
-
-	crnl();
-	ok = deleteMsgs(selected);
-	check();
-	if(ok)
-		Bprint(&bout, "%s OK %s messages erased\r\n", tg, cmd);
-	else
-		Bprint(&bout, "%s NO %s some messages not expunged\r\n", tg, cmd);
-}
-
-static void
-fetchCmd(char *tg, char *cmd)
-{
-	fetchUCmd(tg, cmd, 0);
-}
-
-static void
-fetchUCmd(char *tg, char *cmd, int uids)
-{
-	Fetch *f;
-	MsgSet *ms;
-	MbLock *ml;
-	char *uid;
-	ulong max;
-	int ok;
-
-	mustBe(' ');
-	ms = msgSet(uids);
-	mustBe(' ');
-	f = fetchWhat();
-	crnl();
-	uid = "";
-	if(uids)
-		uid = "uid ";
-	max = selected->max;
-	ml = checkBox(selected, 1);
-	if(ml != nil)
-		forMsgs(selected, ms, max, uids, fetchSeen, f);
-	closeImp(selected, ml);
-	ok = ml != nil && forMsgs(selected, ms, max, uids, fetchMsg, f);
-	status(uids, uids);
-	if(ok)
-		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
-	else
-		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
-}
-
-static void
-idleCmd(char *tg, char *cmd)
-{
-	int c, pid;
-
-	crnl();
-	Bprint(&bout, "+ idling, waiting for done\r\n");
-	if(Bflush(&bout) < 0)
-		writeErr();
-
-	if(idlepid < 0){
-		pid = rfork(RFPROC|RFMEM|RFNOWAIT);
-		if(pid == 0){
-			for(;;){
-				qlock(&imaplock);
-				if(exiting)
-					break;
-
-				/*
-				 * parent may have changed curDir, but it doesn't change our .
-				 */
-				resetCurDir();
-
-				check();
-				if(Bflush(&bout) < 0)
-					writeErr();
-				qunlock(&imaplock);
-				sleep(15*1000);
-				enableForwarding();
-			}
-_exits("rob3");
-			_exits(0);
-		}
-		idlepid = pid;
-	}
-
-	qunlock(&imaplock);
-
-	/*
-	 * clear out the next line, which is supposed to contain (case-insensitive)
-	 * done\n
-	 * this is special code since it has to dance with the idle polling proc
-	 * and handle exiting correctly.
-	 */
-	for(;;){
-		c = getc();
-		if(c < 0){
-			qlock(&imaplock);
-			if(!exiting)
-				cleaner();
-_exits("rob4");
-			_exits(0);
-		}
-		if(c == '\n')
-			break;
-	}
-
-	qlock(&imaplock);
-	if(exiting)
-{_exits("rob5");
-		_exits(0);
-}
-
-	/*
-	 * child may have changed curDir, but it doesn't change our .
-	 */
-	resetCurDir();
-
-	check();
-	Bprint(&bout, "%s OK %s terminated\r\n", tg, cmd);
-}
-
-static void
-listCmd(char *tg, char *cmd)
-{
-	char *s, *t, *ss, *ref, *mbox;
-	int n;
-
-	mustBe(' ');
-	s = astring();
-	mustBe(' ');
-	t = listmbox();
-	crnl();
-	check();
-	ref = mutf7str(s);
-	mbox = mutf7str(t);
-	if(ref == nil || mbox == nil){
-		Bprint(&bout, "%s BAD %s mailbox name not in modified utf-7\r\n", tg, cmd);
-		return;
-	}
-
-	/*
-	 * special request for hierarchy delimiter and root name
-	 * root name appears to be name up to and including any delimiter,
-	 * or the empty string, if there is no delimiter.
-	 *
-	 * this must change if the # namespace convention is supported.
-	 */
-	if(*mbox == '\0'){
-		s = strchr(ref, '/');
-		if(s == nil)
-			ref = "";
-		else
-			s[1] = '\0';
-		Bprint(&bout, "* %s (\\Noselect) \"/\" \"%s\"\r\n", cmd, ref);
-		Bprint(&bout, "%s OK %s\r\n", tg, cmd);
-		return;
-	}
-
-
-	/*
-	 * massage the listing name:
-	 * clean up the components individually,
-	 * then rip off componenets from the ref to
-	 * take care of leading ..'s in the mbox.
-	 *
-	 * the cleanup can wipe out * followed by a ..
-	 * tough luck if such a stupid pattern is given.
-	 */
-	cleanname(mbox);
-	if(strcmp(mbox, ".") == 0)
-		*mbox = '\0';
-	if(mbox[0] == '/')
-		*ref = '\0';
-	else if(*ref != '\0'){
-		cleanname(ref);
-		if(strcmp(ref, ".") == 0)
-			*ref = '\0';
-	}else
-		*ref = '\0';
-	while(*ref && isdotdot(mbox)){
-		s = strrchr(ref, '/');
-		if(s == nil)
-			s = ref;
-		if(isdotdot(s))
-			break;
-		*s = '\0';
-		mbox += 2;
-		if(*mbox == '/')
-			mbox++;
-	}
-	if(*ref == '\0'){
-		s = mbox;
-		ss = s;
-	}else{
-		n = strlen(ref) + strlen(mbox) + 2;
-		t = binalloc(&parseBin, n, 0);
-		if(t == nil)
-			parseErr("out of memory");
-		snprint(t, n, "%s/%s", ref, mbox);
-		s = t;
-		ss = s + strlen(ref);
-	}
-
-	/*
-	 * only allow activity in /mail/box
-	 */
-	if(s[0] == '/' || isdotdot(s)){
-		Bprint(&bout, "%s NO illegal mailbox pattern\r\n", tg);
-		return;
-	}
-
-	if(cistrcmp(cmd, "lsub") == 0)
-		lsubBoxes(cmd, s, ss);
-	else
-		listBoxes(cmd, s, ss);
-	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-}
-
-static char*
-passCR(char*u, char*p)
-{
-	static char Ebadch[] = "can't get challenge";
-	static char nchall[64];
-	static char response[64];
-	static Chalstate *ch = nil;
-	AuthInfo *ai;
-
-again:
-	if (ch == nil){
-		if(!(ch = auth_challenge("proto=p9cr role=server user=%q", u)))
-			return Ebadch;
-		snprint(nchall, 64, " encrypt challenge: %s", ch->chal);
-		return nchall;
-	} else {
-		strncpy(response, p, 64);
-		ch->resp = response;
-		ch->nresp = strlen(response);
-		ai = auth_response(ch);
-		auth_freechal(ch);
-		ch = nil;
-		if (ai == nil)
-			goto again;
-		setupuser(ai);
-		return nil;
-	}
-		
-}
-
-static void
-loginCmd(char *tg, char *cmd)
-{
-	char *s, *t;
-	AuthInfo *ai;
-	char*r;
-	mustBe(' ');
-	s = astring();	/* uid */
-	mustBe(' ');
-	t = astring();	/* password */
-	crnl();
-	if(allowCR){
-		if ((r = passCR(s, t)) == nil){
-			Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd);
-			imapState = SAuthed;
-		} else {
-			Bprint(&bout, "* NO [ALERT] %s\r\n", r);
-			Bprint(&bout, "%s NO %s succeeded\r\n", tg, cmd);
-		}
-		return;
-	}
-	else if(allowPass){
-		if(ai = passLogin(s, t)){
-			setupuser(ai);
-			Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd);
-			imapState = SAuthed;
-		}else
-			Bprint(&bout, "%s NO %s failed check\r\n", tg, cmd);
-		return;
-	}
-	Bprint(&bout, "%s NO %s plaintext passwords disallowed\r\n", tg, cmd);
-}
-
-/*
- * logout or x-exit, which doesn't expunge the mailbox
- */
-static void
-logoutCmd(char *tg, char *cmd)
-{
-	crnl();
-
-	if(cmd[0] != 'x' && selected){
-		closeBox(selected, 1);
-		selected = nil;
-	}
-	Bprint(&bout, "* bye\r\n");
-	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-exits("rob6");
-	exits(0);
-}
-
-static void
-namespaceCmd(char *tg, char *cmd)
-{
-	crnl();
-	check();
-
-	/*
-	 * personal, other users, shared namespaces
-	 * send back nil or descriptions of (prefix heirarchy-delim) for each case
-	 */
-	Bprint(&bout, "* NAMESPACE ((\"\" \"/\")) nil nil\r\n");
-	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-}
-
-static void
-noopCmd(char *tg, char *cmd)
-{
-	crnl();
-	check();
-	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-	enableForwarding();
-}
-
-/*
- * this is only a partial implementation
- * should copy files to other directories,
- * and copy & truncate inbox
- */
-static void
-renameCmd(char *tg, char *cmd)
-{
-	char *from, *to;
-	int ok;
-
-	mustBe(' ');
-	from = astring();
-	mustBe(' ');
-	to = astring();
-	crnl();
-	check();
-
-	to = mboxName(to);
-	if(to == nil || !okMbox(to) || cistrcmp(to, "inbox") == 0){
-		Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd);
-		return;
-	}
-	if(access(to, AEXIST) >= 0){
-		Bprint(&bout, "%s NO %s mailbox already exists\r\n", tg, cmd);
-		return;
-	}
-	from = mboxName(from);
-	if(from == nil || !okMbox(from)){
-		Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd);
-		return;
-	}
-	if(cistrcmp(from, "inbox") == 0)
-		ok = copyBox(from, to, 0);
-	else
-		ok = moveBox(from, to);
-
-	if(ok)
-		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-	else
-		Bprint(&bout, "%s NO %s failed\r\n", tg, cmd);
-}
-
-static void
-searchCmd(char *tg, char *cmd)
-{
-	searchUCmd(tg, cmd, 0);
-}
-
-static void
-searchUCmd(char *tg, char *cmd, int uids)
-{
-	Search rock;
-	Msg *m;
-	char *uid;
-	ulong id;
-
-	mustBe(' ');
-	rock.next = nil;
-	searchKeys(1, &rock);
-	crnl();
-	uid = "";
-	if(uids)
-		uid = "uid ";
-	if(rock.next != nil && rock.next->key == SKCharset){
-		if(cistrcmp(rock.next->s, "utf-8") != 0
-		&& cistrcmp(rock.next->s, "us-ascii") != 0){
-			Bprint(&bout, "%s NO [BADCHARSET] (\"US-ASCII\" \"UTF-8\") %s%s failed\r\n", tg, uid, cmd);
-			checkBox(selected, 0);
-			status(uids, uids);
-			return;
-		}
-		rock.next = rock.next->next;
-	}
-	Bprint(&bout, "* search");
-	for(m = selected->msgs; m != nil; m = m->next)
-		m->matched = searchMsg(m, rock.next);
-	for(m = selected->msgs; m != nil; m = m->next){
-		if(m->matched){
-			if(uids)
-				id = m->uid;
-			else
-				id = m->seq;
-			Bprint(&bout, " %lud", id);
-		}
-	}
-	Bprint(&bout, "\r\n");
-	checkBox(selected, 0);
-	status(uids, uids);
-	Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
-}
-
-static void
-selectCmd(char *tg, char *cmd)
-{
-	Msg *m;
-	char *s, *mbox;
-
-	mustBe(' ');
-	mbox = astring();
-	crnl();
-
-	if(selected){
-		imapState = SAuthed;
-		closeBox(selected, 1);
-		selected = nil;
-	}
-
-	mbox = mboxName(mbox);
-	if(mbox == nil || !okMbox(mbox)){
-		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
-		return;
-	}
-
-	selected = openBox(mbox, "imap", cistrcmp(cmd, "select") == 0);
-	if(selected == nil){
-		Bprint(&bout, "%s NO %s can't open mailbox %s: %r\r\n", tg, cmd, mbox);
-		return;
-	}
-
-	imapState = SSelected;
-
-	Bprint(&bout, "* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n");
-	Bprint(&bout, "* %lud EXISTS\r\n", selected->max);
-	selected->toldMax = selected->max;
-	Bprint(&bout, "* %lud RECENT\r\n", selected->recent);
-	selected->toldRecent = selected->recent;
-	for(m = selected->msgs; m != nil; m = m->next){
-		if(!m->expunged && (m->flags & MSeen) != MSeen){
-			Bprint(&bout, "* OK [UNSEEN %ld]\r\n", m->seq);
-			break;
-		}
-	}
-	Bprint(&bout, "* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Draft \\Deleted)]\r\n");
-	Bprint(&bout, "* OK [UIDNEXT %ld]\r\n", selected->uidnext);
-	Bprint(&bout, "* OK [UIDVALIDITY %ld]\r\n", selected->uidvalidity);
-	s = "READ-ONLY";
-	if(selected->writable)
-		s = "READ-WRITE";
-	Bprint(&bout, "%s OK [%s] %s %s completed\r\n", tg, s, cmd, mbox);
-}
-
-static NamedInt	statusItems[] =
-{
-	{"MESSAGES",	SMessages},
-	{"RECENT",	SRecent},
-	{"UIDNEXT",	SUidNext},
-	{"UIDVALIDITY",	SUidValidity},
-	{"UNSEEN",	SUnseen},
-	{nil,		0}
-};
-
-static void
-statusCmd(char *tg, char *cmd)
-{
-	Box *box;
-	Msg *m;
-	char *s, *mbox;
-	ulong v;
-	int si, i;
-
-	mustBe(' ');
-	mbox = astring();
-	mustBe(' ');
-	mustBe('(');
-	si = 0;
-	for(;;){
-		s = atom();
-		i = mapInt(statusItems, s);
-		if(i == 0)
-			parseErr("illegal status item");
-		si |= i;
-		if(peekc() == ')')
-			break;
-		mustBe(' ');
-	}
-	mustBe(')');
-	crnl();
-
-	mbox = mboxName(mbox);
-	if(mbox == nil || !okMbox(mbox)){
-		check();
-		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
-		return;
-	}
-
-	box = openBox(mbox, "status", 1);
-	if(box == nil){
-		check();
-		Bprint(&bout, "%s NO [TRYCREATE] %s can't open mailbox %s: %r\r\n", tg, cmd, mbox);
-		return;
-	}
-
-	Bprint(&bout, "* STATUS %s (", mbox);
-	s = "";
-	for(i = 0; statusItems[i].name != nil; i++){
-		if(si & statusItems[i].v){
-			v = 0;
-			switch(statusItems[i].v){
-			case SMessages:
-				v = box->max;
-				break;
-			case SRecent:
-				v = box->recent;
-				break;
-			case SUidNext:
-				v = box->uidnext;
-				break;
-			case SUidValidity:
-				v = box->uidvalidity;
-				break;
-			case SUnseen:
-				v = 0;
-				for(m = box->msgs; m != nil; m = m->next)
-					if((m->flags & MSeen) != MSeen)
-						v++;
-				break;
-			default:
-				Bprint(&bout, ")");
-				bye("internal error: status item not implemented");
-				break;
-			}
-			Bprint(&bout, "%s%s %lud", s, statusItems[i].name, v);
-			s = " ";
-		}
-	}
-	Bprint(&bout, ")\r\n");
-	closeBox(box, 1);
-
-	check();
-	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-}
-
-static void
-storeCmd(char *tg, char *cmd)
-{
-	storeUCmd(tg, cmd, 0);
-}
-
-static void
-storeUCmd(char *tg, char *cmd, int uids)
-{
-	Store *st;
-	MsgSet *ms;
-	MbLock *ml;
-	char *uid;
-	ulong max;
-	int ok;
-
-	mustBe(' ');
-	ms = msgSet(uids);
-	mustBe(' ');
-	st = storeWhat();
-	crnl();
-	uid = "";
-	if(uids)
-		uid = "uid ";
-	max = selected->max;
-	ml = checkBox(selected, 1);
-	ok = ml != nil && forMsgs(selected, ms, max, uids, storeMsg, st);
-	closeImp(selected, ml);
-	status(uids, uids);
-	if(ok)
-		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
-	else
-		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
-}
-
-/*
- * minimal implementation of subscribe
- * all folders are automatically subscribed,
- * and can't be unsubscribed
- */
-static void
-subscribeCmd(char *tg, char *cmd)
-{
-	Box *box;
-	char *mbox;
-	int ok;
-
-	mustBe(' ');
-	mbox = astring();
-	crnl();
-	check();
-	mbox = mboxName(mbox);
-	ok = 0;
-	if(mbox != nil && okMbox(mbox)){
-		box = openBox(mbox, "subscribe", 0);
-		if(box != nil){
-			ok = subscribe(mbox, 's');
-			closeBox(box, 1);
-		}
-	}
-	if(!ok)
-		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
-	else
-		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-}
-
-static void
-uidCmd(char *tg, char *cmd)
-{
-	char *sub;
-
-	mustBe(' ');
-	sub = atom();
-	if(cistrcmp(sub, "copy") == 0)
-		copyUCmd(tg, sub, 1);
-	else if(cistrcmp(sub, "fetch") == 0)
-		fetchUCmd(tg, sub, 1);
-	else if(cistrcmp(sub, "search") == 0)
-		searchUCmd(tg, sub, 1);
-	else if(cistrcmp(sub, "store") == 0)
-		storeUCmd(tg, sub, 1);
-	else{
-		clearcmd();
-		Bprint(&bout, "%s BAD %s illegal uid command %s\r\n", tg, cmd, sub);
-	}
-}
-
-static void
-unsubscribeCmd(char *tg, char *cmd)
-{
-	char *mbox;
-
-	mustBe(' ');
-	mbox = astring();
-	crnl();
-	check();
-	mbox = mboxName(mbox);
-	if(mbox == nil || !okMbox(mbox) || !subscribe(mbox, 'u'))
-		Bprint(&bout, "%s NO %s can't unsubscribe\r\n", tg, cmd);
-	else
-		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
-}
-
-static void
-badsyn(void)
-{
-	parseErr("bad syntax");
-}
-
-static void
-clearcmd(void)
-{
-	int c;
-
-	for(;;){
-		c = getc();
-		if(c < 0)
-			bye("end of input");
-		if(c == '\n')
-			return;
-	}
-}
-
-static void
-crnl(void)
-{
-	int c;
-
-	c = getc();
-	if(c == '\n')
-		return;
-	if(c != '\r' || getc() != '\n')
-		badsyn();
-}
-
-static void
-mustBe(int c)
-{
-	if(getc() != c){
-		ungetc();
-		badsyn();
-	}
-}
-
-/*
- * flaglist	: '(' ')' | '(' flags ')'
- */
-static int
-flagList(void)
-{
-	int f;
-
-	mustBe('(');
-	f = 0;
-	if(peekc() != ')')
-		f = flags();
-
-	mustBe(')');
-	return f;
-}
-
-/*
- * flags	: flag | flags ' ' flag
- * flag		: '\' atom | atom
- */
-static int
-flags(void)
-{
-	int ff, flags;
-	char *s;
-	int c;
-
-	flags = 0;
-	for(;;){
-		c = peekc();
-		if(c == '\\'){
-			mustBe('\\');
-			s = atomString(atomStop, "\\");
-		}else if(strchr(atomStop, c) != nil)
-			s = atom();
-		else
-			break;
-		ff = mapFlag(s);
-		if(ff == 0)
-			parseErr("flag not supported");
-		flags |= ff;
-		if(peekc() != ' ')
-			break;
-		mustBe(' ');
-	}
-	if(flags == 0)
-		parseErr("no flags given");
-	return flags;
-}
-
-/*
- * storeWhat	: osign 'FLAGS' ' ' storeflags
- *		| osign 'FLAGS.SILENT' ' ' storeflags
- * osign	:
- *		| '+' | '-'
- * storeflags	: flagList | flags
- */
-static Store*
-storeWhat(void)
-{
-	int f;
-	char *s;
-	int c, w;
-
-	c = peekc();
-	if(c == '+' || c == '-')
-		mustBe(c);
-	else
-		c = 0;
-	s = atom();
-	w = 0;
-	if(cistrcmp(s, "flags") == 0)
-		w = STFlags;
-	else if(cistrcmp(s, "flags.silent") == 0)
-		w = STFlagsSilent;
-	else
-		parseErr("illegal store attribute");
-	mustBe(' ');
-	if(peekc() == '(')
-		f = flagList();
-	else
-		f = flags();
-	return mkStore(c, w, f);
-}
-
-/*
- * fetchWhat	: "ALL" | "FULL" | "FAST" | fetchAtt | '(' fetchAtts ')'
- * fetchAtts	: fetchAtt | fetchAtts ' ' fetchAtt
- */
-static char *fetchAtom	= "(){}%*\"\\[]";
-static Fetch*
-fetchWhat(void)
-{
-	Fetch *f;
-	char *s;
-
-	if(peekc() == '('){
-		getc();
-		f = nil;
-		for(;;){
-			s = atomString(fetchAtom, "");
-			f = fetchAtt(s, f);
-			if(peekc() == ')')
-				break;
-			mustBe(' ');
-		}
-		getc();
-		return revFetch(f);
-	}
-
-	s = atomString(fetchAtom, "");
-	if(cistrcmp(s, "all") == 0)
-		f = mkFetch(FFlags, mkFetch(FInternalDate, mkFetch(FRfc822Size, mkFetch(FEnvelope, nil))));
-	else if(cistrcmp(s, "fast") == 0)
-		f = mkFetch(FFlags, mkFetch(FInternalDate, mkFetch(FRfc822Size, nil)));
-	else if(cistrcmp(s, "full") == 0)
-		f = mkFetch(FFlags, mkFetch(FInternalDate, mkFetch(FRfc822Size, mkFetch(FEnvelope, mkFetch(FBody, nil)))));
-	else
-		f = fetchAtt(s, nil);
-	return f;
-}
-
-/*
- * fetchAtt	: "ENVELOPE" | "FLAGS" | "INTERNALDATE"
- *		| "RFC822" | "RFC822.HEADER" | "RFC822.SIZE" | "RFC822.TEXT"
- *		| "BODYSTRUCTURE"
- *		| "UID"
- *		| "BODY"
- *		| "BODY" bodysubs
- *		| "BODY.PEEK" bodysubs
- * bodysubs	: sect
- *		| sect '<' number '.' nz-number '>'
- * sect		: '[' sectSpec ']'
- * sectSpec	: sectMsgText
- *		| sectPart
- *		| sectPart '.' sectText
- * sectPart	: nz-number
- *		| sectPart '.' nz-number
- */
-static Fetch*
-fetchAtt(char *s, Fetch *f)
-{
-	NList *sect;
-	int c;
-
-	if(cistrcmp(s, "envelope") == 0)
-		return mkFetch(FEnvelope, f);
-	if(cistrcmp(s, "flags") == 0)
-		return mkFetch(FFlags, f);
-	if(cistrcmp(s, "internaldate") == 0)
-		return mkFetch(FInternalDate, f);
-	if(cistrcmp(s, "RFC822") == 0)
-		return mkFetch(FRfc822, f);
-	if(cistrcmp(s, "RFC822.header") == 0)
-		return mkFetch(FRfc822Head, f);
-	if(cistrcmp(s, "RFC822.size") == 0)
-		return mkFetch(FRfc822Size, f);
-	if(cistrcmp(s, "RFC822.text") == 0)
-		return mkFetch(FRfc822Text, f);
-	if(cistrcmp(s, "bodystructure") == 0)
-		return mkFetch(FBodyStruct, f);
-	if(cistrcmp(s, "uid") == 0)
-		return mkFetch(FUid, f);
-
-	if(cistrcmp(s, "body") == 0){
-		if(peekc() != '[')
-			return mkFetch(FBody, f);
-		f = mkFetch(FBodySect, f);
-	}else if(cistrcmp(s, "body.peek") == 0)
-		f = mkFetch(FBodyPeek, f);
-	else
-		parseErr("illegal fetch attribute");
-
-	mustBe('[');
-	c = peekc();
-	if(c >= '1' && c <= '9'){
-		sect = mkNList(number(1), nil);
-		while(peekc() == '.'){
-			getc();
-			c = peekc();
-			if(c >= '1' && c <= '9'){
-				sect = mkNList(number(1), sect);
-			}else{
-				break;
-			}
-		}
-		f->sect = revNList(sect);
-	}
-	if(peekc() != ']')
-		sectText(f, f->sect != nil);
-	mustBe(']');
-
-	if(peekc() != '<')
-		return f;
-
-	f->partial = 1;
-	mustBe('<');
-	f->start = number(0);
-	mustBe('.');
-	f->size = number(1);
-	mustBe('>');
-	return f;
-}
-
-/*
- * sectText	: sectMsgText | "MIME"
- * sectMsgText	: "HEADER"
- *		| "TEXT"
- *		| "HEADER.FIELDS" ' ' hdrList
- *		| "HEADER.FIELDS.NOT" ' ' hdrList
- * hdrList	: '(' hdrs ')'
- * hdrs:	: astring
- *		| hdrs ' ' astring
- */
-static void
-sectText(Fetch *f, int mimeOk)
-{
-	SList *h;
-	char *s;
-
-	s = atomString(fetchAtom, "");
-	if(cistrcmp(s, "header") == 0){
-		f->part = FPHead;
-		return;
-	}
-	if(cistrcmp(s, "text") == 0){
-		f->part = FPText;
-		return;
-	}
-	if(mimeOk && cistrcmp(s, "mime") == 0){
-		f->part = FPMime;
-		return;
-	}
-	if(cistrcmp(s, "header.fields") == 0)
-		f->part = FPHeadFields;
-	else if(cistrcmp(s, "header.fields.not") == 0)
-		f->part = FPHeadFieldsNot;
-	else
-		parseErr("illegal fetch section text");
-	mustBe(' ');
-	mustBe('(');
-	h = nil;
-	for(;;){
-		h = mkSList(astring(), h);
-		if(peekc() == ')')
-			break;
-		mustBe(' ');
-	}
-	mustBe(')');
-	f->hdrs = revSList(h);
-}
-
-/*
- * searchWhat	: "CHARSET" ' ' astring searchkeys | searchkeys
- * searchkeys	: searchkey | searchkeys ' ' searchkey
- * searchkey	: "ALL" | "ANSWERED" | "DELETED" | "FLAGGED" | "NEW" | "OLD" | "RECENT"
- *		| "SEEN" | "UNANSWERED" | "UNDELETED" | "UNFLAGGED" | "DRAFT" | "UNDRAFT"
- *		| astrkey ' ' astring
- *		| datekey ' ' date
- *		| "KEYWORD" ' ' flag | "UNKEYWORD" flag
- *		| "LARGER" ' ' number | "SMALLER" ' ' number
- * 		| "HEADER" astring ' ' astring
- *		| set | "UID" ' ' set
- *		| "NOT" ' ' searchkey
- *		| "OR" ' ' searchkey ' ' searchkey
- *		| '(' searchkeys ')'
- * astrkey	: "BCC" | "BODY" | "CC" | "FROM" | "SUBJECT" | "TEXT" | "TO"
- * datekey	: "BEFORE" | "ON" | "SINCE" | "SENTBEFORE" | "SENTON" | "SENTSINCE"
- */
-static NamedInt searchMap[] =
-{
-	{"ALL",		SKAll},
-	{"ANSWERED",	SKAnswered},
-	{"DELETED",	SKDeleted},
-	{"FLAGGED",	SKFlagged},
-	{"NEW",		SKNew},
-	{"OLD",		SKOld},
-	{"RECENT",	SKRecent},
-	{"SEEN",	SKSeen},
-	{"UNANSWERED",	SKUnanswered},
-	{"UNDELETED",	SKUndeleted},
-	{"UNFLAGGED",	SKUnflagged},
-	{"DRAFT",	SKDraft},
-	{"UNDRAFT",	SKUndraft},
-	{"UNSEEN",	SKUnseen},
-	{nil,		0}
-};
-
-static NamedInt searchMapStr[] =
-{
-	{"CHARSET",	SKCharset},
-	{"BCC",		SKBcc},
-	{"BODY",	SKBody},
-	{"CC",		SKCc},
-	{"FROM",	SKFrom},
-	{"SUBJECT",	SKSubject},
-	{"TEXT",	SKText},
-	{"TO",		SKTo},
-	{nil,		0}
-};
-
-static NamedInt searchMapDate[] =
-{
-	{"BEFORE",	SKBefore},
-	{"ON",		SKOn},
-	{"SINCE",	SKSince},
-	{"SENTBEFORE",	SKSentBefore},
-	{"SENTON",	SKSentOn},
-	{"SENTSINCE",	SKSentSince},
-	{nil,		0}
-};
-
-static NamedInt searchMapFlag[] =
-{
-	{"KEYWORD",	SKKeyword},
-	{"UNKEYWORD",	SKUnkeyword},
-	{nil,		0}
-};
-
-static NamedInt searchMapNum[] =
-{
-	{"SMALLER",	SKSmaller},
-	{"LARGER",	SKLarger},
-	{nil,		0}
-};
-
-static Search*
-searchKeys(int first, Search *tail)
-{
-	Search *s;
-
-	for(;;){
-		if(peekc() == '('){
-			getc();
-			tail = searchKeys(0, tail);
-			mustBe(')');
-		}else{
-			s = searchKey(first);
-			tail->next = s;
-			tail = s;
-		}
-		first = 0;
-		if(peekc() != ' ')
-			break;
-		getc();
-	}
-	return tail;
-}
-
-static Search*
-searchKey(int first)
-{
-	Search *sr, rock;
-	Tm tm;
-	char *a;
-	int i, c;
-
-	sr = binalloc(&parseBin, sizeof(Search), 1);
-	if(sr == nil)
-		parseErr("out of memory");
-
-	c = peekc();
-	if(c >= '0' && c <= '9'){
-		sr->key = SKSet;
-		sr->set = msgSet(0);
-		return sr;
-	}
-
-	a = atom();
-	if(i = mapInt(searchMap, a))
-		sr->key = i;
-	else if(i = mapInt(searchMapStr, a)){
-		if(!first && i == SKCharset)
-			parseErr("illegal search key");
-		sr->key = i;
-		mustBe(' ');
-		sr->s = astring();
-	}else if(i = mapInt(searchMapDate, a)){
-		sr->key = i;
-		mustBe(' ');
-		c = peekc();
-		if(c == '"')
-			getc();
-		a = atom();
-		if(!imap4Date(&tm, a))
-			parseErr("bad date format");
-		sr->year = tm.year;
-		sr->mon = tm.mon;
-		sr->mday = tm.mday;
-		if(c == '"')
-			mustBe('"');
-	}else if(i = mapInt(searchMapFlag, a)){
-		sr->key = i;
-		mustBe(' ');
-		c = peekc();
-		if(c == '\\'){
-			mustBe('\\');
-			a = atomString(atomStop, "\\");
-		}else
-			a = atom();
-		i = mapFlag(a);
-		if(i == 0)
-			parseErr("flag not supported");
-		sr->num = i;
-	}else if(i = mapInt(searchMapNum, a)){
-		sr->key = i;
-		mustBe(' ');
-		sr->num = number(0);
-	}else if(cistrcmp(a, "HEADER") == 0){
-		sr->key = SKHeader;
-		mustBe(' ');
-		sr->hdr = astring();
-		mustBe(' ');
-		sr->s = astring();
-	}else if(cistrcmp(a, "UID") == 0){
-		sr->key = SKUid;
-		mustBe(' ');
-		sr->set = msgSet(0);
-	}else if(cistrcmp(a, "NOT") == 0){
-		sr->key = SKNot;
-		mustBe(' ');
-		rock.next = nil;
-		searchKeys(0, &rock);
-		sr->left = rock.next;
-	}else if(cistrcmp(a, "OR") == 0){
-		sr->key = SKOr;
-		mustBe(' ');
-		rock.next = nil;
-		searchKeys(0, &rock);
-		sr->left = rock.next;
-		mustBe(' ');
-		rock.next = nil;
-		searchKeys(0, &rock);
-		sr->right = rock.next;
-	}else
-		parseErr("illegal search key");
-	return sr;
-}
-
-/*
- * set	: seqno
- *	| seqno ':' seqno
- *	| set ',' set
- * seqno: nz-number
- *	| '*'
- *
- */
-static MsgSet*
-msgSet(int uids)
-{
-	MsgSet head, *last, *ms;
-	ulong from, to;
-
-	last = &head;
-	head.next = nil;
-	for(;;){
-		from = uids ? uidNo() : seqNo();
-		to = from;
-		if(peekc() == ':'){
-			getc();
-			to = uids ? uidNo() : seqNo();
-		}
-		ms = binalloc(&parseBin, sizeof(MsgSet), 0);
-		if(ms == nil)
-			parseErr("out of memory");
-		ms->from = from;
-		ms->to = to;
-		ms->next = nil;
-		last->next = ms;
-		last = ms;
-		if(peekc() != ',')
-			break;
-		getc();
-	}
-	return head.next;
-}
-
-static ulong
-seqNo(void)
-{
-	if(peekc() == '*'){
-		getc();
-		return ~0UL;
-	}
-	return number(1);
-}
-
-static ulong
-uidNo(void)
-{
-	if(peekc() == '*'){
-		getc();
-		return ~0UL;
-	}
-	return number(0);
-}
-
-/*
- * 7 bit, non-ctl chars, no (){%*"\
- * NIL is special case for nstring or parenlist
- */
-static char *
-atom(void)
-{
-	return atomString(atomStop, "");
-}
-
-/*
- * like an atom, but no +
- */
-static char *
-tag(void)
-{
-	return atomString("+(){%*\"\\", "");
-}
-
-/*
- * string or atom allowing %*
- */
-static char *
-listmbox(void)
-{
-	int c;
-
-	c = peekc();
-	if(c == '{')
-		return literal();
-	if(c == '"')
-		return quoted();
-	return atomString("(){\"\\", "");
-}
-
-/*
- * string or atom
- */
-static char *
-astring(void)
-{
-	int c;
-
-	c = peekc();
-	if(c == '{')
-		return literal();
-	if(c == '"')
-		return quoted();
-	return atom();
-}
-
-/*
- * 7 bit, non-ctl chars, none from exception list
- */
-static char *
-atomString(char *disallowed, char *initial)
-{
-	char *s;
-	int c, ns, as;
-
-	ns = strlen(initial);
-	s = binalloc(&parseBin, ns + StrAlloc, 0);
-	if(s == nil)
-		parseErr("out of memory");
-	strcpy(s, initial);
-	as = ns + StrAlloc;
-	for(;;){
-		c = getc();
-		if(c <= ' ' || c >= 0x7f || strchr(disallowed, c) != nil){
-			ungetc();
-			break;
-		}
-		s[ns++] = c;
-		if(ns >= as){
-			s = bingrow(&parseBin, s, as, as + StrAlloc, 0);
-			if(s == nil)
-				parseErr("out of memory");
-			as += StrAlloc;
-		}
-	}
-	if(ns == 0)
-		badsyn();
-	s[ns] = '\0';
-	return s;
-}
-
-/*
- * quoted: '"' chars* '"'
- * chars:	1-128 except \r and \n
- */
-static char *
-quoted(void)
-{
-	char *s;
-	int c, ns, as;
-
-	mustBe('"');
-	s = binalloc(&parseBin, StrAlloc, 0);
-	if(s == nil)
-		parseErr("out of memory");
-	as = StrAlloc;
-	ns = 0;
-	for(;;){
-		c = getc();
-		if(c == '"')
-			break;
-		if(c < 1 || c > 0x7f || c == '\r' || c == '\n')
-			badsyn();
-		if(c == '\\'){
-			c = getc();
-			if(c != '\\' && c != '"')
-				badsyn();
-		}
-		s[ns++] = c;
-		if(ns >= as){
-			s = bingrow(&parseBin, s, as, as + StrAlloc, 0);
-			if(s == nil)
-				parseErr("out of memory");
-			as += StrAlloc;
-		}
-	}
-	s[ns] = '\0';
-	return s;
-}
-
-/*
- * litlen: {number}\r\n
- */
-static ulong
-litlen(void)
-{
-	ulong v;
-
-	mustBe('{');
-	v = number(0);
-	mustBe('}');
-	crnl();
-	return v;
-}
-
-/*
- * literal: litlen data<0:litlen>
- */
-static char *
-literal(void)
-{
-	char *s;
-	ulong v;
-
-	v = litlen();
-	s = binalloc(&parseBin, v+1, 0);
-	if(s == nil)
-		parseErr("out of memory");
-	Bprint(&bout, "+ Ready for literal data\r\n");
-	if(Bflush(&bout) < 0)
-		writeErr();
-	if(v != 0 && Bread(&bin, s, v) != v)
-		badsyn();
-	s[v] = '\0';
-	return s;
-}
-
-/*
- * digits; number is 32 bits
- */
-static ulong
-number(int nonzero)
-{
-	ulong v;
-	int c, first;
-
-	v = 0;
-	first = 1;
-	for(;;){
-		c = getc();
-		if(c < '0' || c > '9'){
-			ungetc();
-			if(first)
-				badsyn();
-			break;
-		}
-		if(nonzero && first && c == '0')
-			badsyn();
-		c -= '0';
-		first = 0;
-		if(v > UlongMax/10 || v == UlongMax/10 && c > UlongMax%10)
-			parseErr("number out of range\r\n");
-		v = v * 10 + c;
-	}
-	return v;
-}
-
-static int
-getc(void)
-{
-	return Bgetc(&bin);
-}
-
-static void
-ungetc(void)
-{
-	Bungetc(&bin);
-}
-
-static int
-peekc(void)
-{
-	int c;
-
-	c = Bgetc(&bin);
-	Bungetc(&bin);
-	return c;
-}
-
--- a/sys/src/cmd/ip/imap4d/imap4d.h
+++ /dev/null
@@ -1,378 +1,0 @@
-/*
- * mailbox and message representations
- *
- * these structures are allocated with emalloc and must be explicitly freed
- */
-typedef struct Box	Box;
-typedef struct Header	Header;
-typedef struct MAddr	MAddr;
-typedef struct MbLock	MbLock;
-typedef struct MimeHdr	MimeHdr;
-typedef struct Msg	Msg;
-typedef struct NamedInt	NamedInt;
-typedef struct Pair	Pair;
-
-enum
-{
-	StrAlloc	= 32,		/* characters allocated at a time */
-	BufSize		= 8*1024,	/* size of transfer block */
-	NDigest		= 40,		/* length of digest string */
-	NUid		= 10,		/* length of .imp uid string */
-	NFlags		= 8,		/* length of .imp flag string */
-	LockSecs	= 5 * 60,	/* seconds to wait for acquiring a locked file */
-	MboxNameLen	= 256,		/* max. length of upas/fs mbox name */
-	MsgNameLen	= 32,		/* max. length of a file in a upas/fs mbox */
-	UserNameLen	= 64,		/* max. length of user's name */
-
-	MUtf7Max	= 6,		/* max length for a modified utf7 character: &bbbb- */
-
-	/*
-	 * message flags
-	 */
-	MSeen		= 1 << 0,
-	MAnswered	= 1 << 1,
-	MFlagged	= 1 << 2,
-	MDeleted	= 1 << 3,
-	MDraft		= 1 << 4,
-	MRecent		= 1 << 5,
-
-	/*
-	 * message bogus flags
-	 */
-	NotBogus	= 0,	/* the message is displayable */
-	BogusHeader	= 1,	/* the header had bad characters */
-	BogusBody	= 2,	/* the body had bad characters */
-	BogusTried	= 4,	/* attempted to open the fake message */
-};
-
-struct Box
-{
-	char	*name;		/* path name of mailbox */
-	char	*fs;		/* fs name of mailbox */
-	char	*fsDir;		/* /mail/fs/box->fs */
-	char	*imp;		/* path name of .imp file */
-	uchar	writable;	/* can write back messages? */
-	uchar	dirtyImp;	/* .imp file needs to be written? */
-	uchar	sendFlags;	/* need flags update */
-	Qid	qid;		/* qid of fs mailbox */
-	Qid	impQid;		/* qid of .imp when last synched */
-	long	mtime;		/* file mtime when last read */
-	ulong	max;		/* maximum msgs->seq, same as number of messages */
-	ulong	toldMax;	/* last value sent to client */
-	ulong	recent;		/* number of recently received messaged */
-	ulong	toldRecent;	/* last value sent to client */
-	ulong	uidnext;	/* next uid value assigned to a message */
-	ulong	uidvalidity;	/* uid of mailbox */
-	Msg	*msgs;
-};
-
-/*
- * fields of Msg->info
- */
-enum
-{
-	/*
-	 * read from upasfs
-	 */
-	IFrom,
-	ITo,
-	ICc,
-	IReplyTo,
-	IUnixDate,
-	ISubject,
-	IType,
-	IDisposition,
-	IFilename,
-	IDigest,
-	IBcc,
-	IInReplyTo,	/* aka internal date */
-	IDate,
-	ISender,
-	IMessageId,
-	ILines,		/* number of lines of raw body */
-
-	IMax
-};
-
-struct Header
-{
-	char	*buf;		/* header, including terminating \r\n */
-	ulong	size;		/* strlen(buf) */
-	ulong	lines;		/* number of \n characters in buf */
-
-	/*
-	 * pre-parsed mime headers
-	 */
-	MimeHdr	*type;		/* content-type */
-	MimeHdr	*id;		/* content-id */
-	MimeHdr	*description;	/* content-description */
-	MimeHdr	*encoding;	/* content-transfer-encoding */
-	MimeHdr	*md5;		/* content-md5 */
-	MimeHdr	*disposition;	/* content-disposition */
-	MimeHdr	*language;	/* content-language */
-};
-
-struct Msg
-{
-	Msg	*next;
-	Msg	*prev;
-	Msg	*kids;
-	Msg	*parent;
-	char	*fsDir;		/* box->fsDir of enclosing message */
-	Header	head;		/* message header */
-	Header	mime;		/* mime header from enclosing multipart spec */
-	int	flags;
-	uchar	sendFlags;	/* flags value needs to be sent to client */
-	uchar	expunged;	/* message actually expunged, but not yet reported to client */
-	uchar	matched;	/* search succeeded? */
-	uchar	bogus;		/* implies the message is invalid, ie contains nulls; see flags above */
-	ulong	uid;		/* imap unique identifier */
-	ulong	seq;		/* position in box; 1 is oldest */
-	ulong	id;		/* number of message directory in upas/fs */
-	char	*fs;		/* name of message directory */
-	char	*efs;		/* pointer after / in fs; enough space for file name */
-
-	ulong	size;		/* size of fs/rawbody, in bytes, with \r added before \n */
-	ulong	lines;		/* number of lines in rawbody */
-
-	char	*iBuf;
-	char	*info[IMax];	/* all info about message */
-
-	char	*unixDate;
-	MAddr	*unixFrom;
-
-	MAddr	*to;		/* parsed out address lines */
-	MAddr	*from;
-	MAddr	*replyTo;
-	MAddr	*sender;
-	MAddr	*cc;
-	MAddr	*bcc;
-};
-
-/*
- * pre-parsed header lines
- */
-struct MAddr
-{
-	char	*personal;
-	char	*box;
-	char	*host;
-	MAddr	*next;
-};
-
-struct MimeHdr
-{
-	char	*s;
-	char	*t;
-	MimeHdr	*next;
-};
-
-/*
- * mapping of integer & names
- */
-struct NamedInt
-{
-	char	*name;
-	int	v;
-};
-
-/*
- * lock for all mail file operations
- */
-struct MbLock
-{
-	int	fd;
-};
-
-/*
- * parse nodes for imap4rev1 protocol
- *
- * important: all of these items are allocated
- * in one can, so they can be tossed out at the same time.
- * this allows leakless parse error recovery by simply tossing the can away.
- * however, it means these structures cannot be mixed with the mailbox structures
- */
-
-typedef struct Fetch	Fetch;
-typedef struct NList	NList;
-typedef struct SList	SList;
-typedef struct MsgSet	MsgSet;
-typedef struct Store	Store;
-typedef struct Search	Search;
-
-/*
- * parse tree for fetch command
- */
-enum
-{
-	FEnvelope,
-	FFlags,
-	FInternalDate,
-	FRfc822,
-	FRfc822Head,
-	FRfc822Size,
-	FRfc822Text,
-	FBodyStruct,
-	FUid,
-	FBody,			/* BODY */
-	FBodySect,		/* BODY [...] */
-	FBodyPeek,
-
-	FMax
-};
-
-enum
-{
-	FPAll,
-	FPHead,
-	FPHeadFields,
-	FPHeadFieldsNot,
-	FPMime,
-	FPText,
-
-	FPMax
-};
-
-struct Fetch
-{
-	uchar	op;		/* F.* operator */
-	uchar	part;		/* FP.* subpart for body[] & body.peek[]*/
-	uchar	partial;	/* partial fetch? */
-	long	start;		/* partial fetch amounts */
-	long	size;
-	NList	*sect;
-	SList	*hdrs;
-	Fetch	*next;
-};
-
-/*
- * status items
- */
-enum{
-	SMessages	= 1 << 0,
-	SRecent		= 1 << 1,
-	SUidNext	= 1 << 2,
-	SUidValidity	= 1 << 3,
-	SUnseen		= 1 << 4,
-};
-
-/*
- * parse tree for store command
- */
-enum
-{
-	STFlags,
-	STFlagsSilent,
-
-	STMax
-};
-
-struct Store
-{
-	uchar	sign;
-	uchar	op;
-	int	flags;
-};
-
-/*
- * parse tree for search command
- */
-enum
-{
-	SKNone,
-
-	SKCharset,
-
-	SKAll,
-	SKAnswered,
-	SKBcc,
-	SKBefore,
-	SKBody,
-	SKCc,
-	SKDeleted,
-	SKDraft,
-	SKFlagged,
-	SKFrom,
-	SKHeader,
-	SKKeyword,
-	SKLarger,
-	SKNew,
-	SKNot,
-	SKOld,
-	SKOn,
-	SKOr,
-	SKRecent,
-	SKSeen,
-	SKSentBefore,
-	SKSentOn,
-	SKSentSince,
-	SKSet,
-	SKSince,
-	SKSmaller,
-	SKSubject,
-	SKText,
-	SKTo,
-	SKUid,
-	SKUnanswered,
-	SKUndeleted,
-	SKUndraft,
-	SKUnflagged,
-	SKUnkeyword,
-	SKUnseen,
-
-	SKMax
-};
-
-struct Search
-{
-	int	key;
-	char	*s;
-	char	*hdr;
-	ulong	num;
-	int	year;
-	int	mon;
-	int	mday;
-	MsgSet	*set;
-	Search	*left;
-	Search	*right;
-	Search	*next;
-};
-
-struct NList
-{
-	ulong	n;
-	NList	*next;
-};
-
-struct SList
-{
-	char	*s;
-	SList	*next;
-};
-
-struct MsgSet
-{
-	ulong	from;
-	ulong	to;
-	MsgSet	*next;
-};
-
-struct Pair
-{
-	ulong	start;
-	ulong	stop;
-};
-
-#include "bin.h"
-
-extern	Bin	*parseBin;
-extern	Biobuf	bout;
-extern	Biobuf	bin;
-extern	char	username[UserNameLen];
-extern	char	mboxDir[MboxNameLen];
-extern	char	*fetchPartNames[FPMax];
-extern	char	*site;
-extern	char	*remote;
-extern	int	debug;
-
-#include "fns.h"
--- a/sys/src/cmd/ip/imap4d/list.c
+++ /dev/null
@@ -1,412 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-#define SUBSCRIBED	"imap.subscribed"
-
-static int	matches(char *ref, char *pat, char *name);
-static int	mayMatch(char *pat, char *name, int star);
-static int	checkMatch(char *cmd, char *ref, char *pat, char *mbox, long mtime, int isdir);
-static int	listAll(char *cmd, char *ref, char *pat, char *mbox, long mtime);
-static int	listMatch(char *cmd, char *ref, char *pat, char *mbox, char *mm);
-static int	mkSubscribed(void);
-
-static long
-listMtime(char *file)
-{
-	Dir *d;
-	long mtime;
-
-	d = cdDirstat(mboxDir, file);
-	if(d == nil)
-		return 0;
-	mtime = d->mtime;
-	free(d);
-	return mtime;
-}
-
-/*
- * check for subscribed mailboxes
- * each line is either a comment starting with #
- * or is a subscribed mailbox name
- */
-int
-lsubBoxes(char *cmd, char *ref, char *pat)
-{
-	MbLock *mb;
-	Dir *d;
-	Biobuf bin;
-	char *s;
-	long mtime;
-	int fd, ok, isdir;
-
-	mb = mbLock();
-	if(mb == nil)
-		return 0;
-	fd = cdOpen(mboxDir, SUBSCRIBED, OREAD);
-	if(fd < 0)
-		fd = mkSubscribed();
-	if(fd < 0){
-		mbUnlock(mb);
-		return 0;
-	}
-	ok = 0;
-	Binit(&bin, fd, OREAD);
-	while(s = Brdline(&bin, '\n')){
-		s[Blinelen(&bin) - 1] = '\0';
-		if(s[0] == '#')
-			continue;
-		isdir = 1;
-		if(cistrcmp(s, "INBOX") == 0){
-			if(access("msgs", AEXIST) == 0)
-				mtime = listMtime("msgs");
-			else
-				mtime = listMtime("mbox");
-			isdir = 0;
-		}else{
-			d = cdDirstat(mboxDir, s);
-			if(d != nil){
-				mtime = d->mtime;
-				if(!(d->mode & DMDIR))
-					isdir = 0;
-				free(d);
-			}else
-				mtime = 0;
-		}
-		ok |= checkMatch(cmd, ref, pat, s, mtime, isdir);
-	}
-	Bterm(&bin);
-	close(fd);
-	mbUnlock(mb);
-	return ok;
-}
-
-static int
-mkSubscribed(void)
-{
-	int fd;
-
-	fd = cdCreate(mboxDir, SUBSCRIBED, ORDWR, 0664);
-	if(fd < 0)
-		return -1;
-	fprint(fd, "#imap4 subscription list\nINBOX\n");
-	seek(fd, 0, 0);
-	return fd;
-}
-
-/*
- * either subscribe or unsubscribe to a mailbox
- */
-int
-subscribe(char *mbox, int how)
-{
-	MbLock *mb;
-	char *s, *in, *ein;
-	int fd, tfd, ok, nmbox;
-
-	if(cistrcmp(mbox, "inbox") == 0)
-		mbox = "INBOX";
-	mb = mbLock();
-	if(mb == nil)
-		return 0;
-	fd = cdOpen(mboxDir, SUBSCRIBED, ORDWR);
-	if(fd < 0)
-		fd = mkSubscribed();
-	if(fd < 0){
-		mbUnlock(mb);
-		return 0;
-	}
-	in = readFile(fd);
-	if(in == nil){
-		mbUnlock(mb);
-		return 0;
-	}
-	nmbox = strlen(mbox);
-	s = strstr(in, mbox);
-	while(s != nil && (s != in && s[-1] != '\n' || s[nmbox] != '\n'))
-		s = strstr(s+1, mbox);
-	ok = 0;
-	if(how == 's' && s == nil){
-		if(fprint(fd, "%s\n", mbox) > 0)
-			ok = 1;
-	}else if(how == 'u' && s != nil){
-		ein = strchr(s, '\0');
-		memmove(s, &s[nmbox+1], ein - &s[nmbox+1]);
-		ein -= nmbox+1;
-		tfd = cdOpen(mboxDir, SUBSCRIBED, OWRITE|OTRUNC);
-		if(tfd >= 0 && seek(fd, 0, 0) >= 0 && write(fd, in, ein-in) == ein-in)
-			ok = 1;
-		if(tfd > 0)
-			close(tfd);
-	}else
-		ok = 1;
-	close(fd);
-	mbUnlock(mb);
-	return ok;
-}
-
-/*
- * stupidly complicated so that % doesn't read entire directory structure
- * yet * works
- * note: in most places, inbox is case-insensitive,
- * but here INBOX is checked for a case-sensitve match.
- */
-int
-listBoxes(char *cmd, char *ref, char *pat)
-{
-	int ok;
-
-	ok = checkMatch(cmd, ref, pat, "INBOX", listMtime("mbox"), 0);
-	return ok | listMatch(cmd, ref, pat, ref, pat);
-}
-
-/*
- * look for all messages which may match the pattern
- * punt when a * is reached
- */
-static int
-listMatch(char *cmd, char *ref, char *pat, char *mbox, char *mm)
-{
-	Dir *dir, *dirs;
-	char *mdir, *m, *mb, *wc;
-	long mode;
-	int c, i, nmb, nmdir, nd, ok, fd;
-
-	mdir = nil;
-	for(m = mm; c = *m; m++){
-		if(c == '%' || c == '*'){
-			if(mdir == nil){
-				fd = cdOpen(mboxDir, ".", OREAD);
-				if(fd < 0)
-					return 0;
-				mbox = "";
-				nmdir = 0;
-			}else{
-				*mdir = '\0';
-				fd = cdOpen(mboxDir, mbox, OREAD);
-				*mdir = '/';
-				nmdir = mdir - mbox + 1;
-				if(fd < 0)
-					return 0;
-				dir = dirfstat(fd);
-				if(dir == nil){
-					close(fd);
-					return 0;
-				}
-				mode = dir->mode;
-				free(dir);
-				if(!(mode & DMDIR))
-					break;
-			}
-			wc = m;
-			for(; c = *m; m++)
-				if(c == '/')
-					break;
-			nmb = nmdir + strlen(m) + MboxNameLen + 3;
-			mb = emalloc(nmb);
-			strncpy(mb, mbox, nmdir);
-			ok = 0;
-			while((nd = dirread(fd, &dirs)) > 0){
-				for(i = 0; i < nd; i++){
-					if(strcmp(mbox, "") == 0 &&
-					    !okMbox(dirs[i].name))
-						continue;
-					/* Safety: ignore message dirs */
-					if(strstr(dirs[i].name, "mails") != 0 ||
-					   strcmp(dirs[i].name, "out") == 0 ||
-					   strcmp(dirs[i].name, "obox") == 0 ||
-					   strcmp(dirs[i].name, "ombox") == 0)
-						continue;
-					if(strcmp(dirs[i].name, "msgs") == 0)
-						dirs[i].mode &= ~DMDIR;
-					if(*wc == '*' && dirs[i].mode & DMDIR &&
-					    mayMatch(mm, dirs[i].name, 1)){
-						snprint(mb+nmdir, nmb-nmdir,
-							"%s", dirs[i].name);
-						ok |= listAll(cmd, ref, pat, mb,
-							dirs[i].mtime);
-					}else if(mayMatch(mm, dirs[i].name, 0)){
-						snprint(mb+nmdir, nmb-nmdir,
-							"%s%s", dirs[i].name, m);
-						if(*m == '\0')
-							ok |= checkMatch(cmd,
-								ref, pat, mb,
-								dirs[i].mtime,
-								dirs[i].mode &
-								DMDIR);
-						else if(dirs[i].mode & DMDIR)
-							ok |= listMatch(cmd,
-								ref, pat, mb, mb
-								+ nmdir + strlen(
-								dirs[i].name));
-					}
-				}
-				free(dirs);
-			}
-			close(fd);
-			free(mb);
-			return ok;
-		}
-		if(c == '/'){
-			mdir = m;
-			mm = m + 1;
-		}
-	}
-	m = mbox;
-	if(*mbox == '\0')
-		m = ".";
-	dir = cdDirstat(mboxDir, m);
-	if(dir == nil)
-		return 0;
-	ok = checkMatch(cmd, ref, pat, mbox, dir->mtime, (dir->mode & DMDIR) == DMDIR);
-	free(dir);
-	return ok;
-}
-
-/*
- * too hard: recursively list all files rooted at mbox,
- * and list checkMatch figure it out
- */
-static int
-listAll(char *cmd, char *ref, char *pat, char *mbox, long mtime)
-{
-	Dir *dirs;
-	char *mb;
-	int i, nmb, nd, ok, fd;
-
-	ok = checkMatch(cmd, ref, pat, mbox, mtime, 1);
-	fd = cdOpen(mboxDir, mbox, OREAD);
-	if(fd < 0)
-		return ok;
-
-	nmb = strlen(mbox) + MboxNameLen + 2;
-	mb = emalloc(nmb);
-	while((nd = dirread(fd, &dirs)) > 0){
-		for(i = 0; i < nd; i++){
-			snprint(mb, nmb, "%s/%s", mbox, dirs[i].name);
-			/* safety: do not recurr */
-			if(0 && dirs[i].mode & DMDIR)
-				ok |= listAll(cmd, ref, pat, mb, dirs[i].mtime);
-			else
-				ok |= checkMatch(cmd, ref, pat, mb, dirs[i].mtime, 0);
-		}
-		free(dirs);
-	}
-	close(fd);
-	free(mb);
-	return ok;
-}
-
-static int
-mayMatch(char *pat, char *name, int star)
-{
-	Rune r;
-	int i, n;
-
-	for(; *pat && *pat != '/'; pat += n){
-		r = *(uchar*)pat;
-		if(r < Runeself)
-			n = 1;
-		else
-			n = chartorune(&r, pat);
-
-		if(r == '*' || r == '%'){
-			pat += n;
-			if(r == '*' && star || *pat == '\0' || *pat == '/')
-				return 1;
-			while(*name){
-				if(mayMatch(pat, name, star))
-					return 1;
-				name += chartorune(&r, name);
-			}
-			return 0;
-		}
-		for(i = 0; i < n; i++)
-			if(name[i] != pat[i])
-				return 0;
-		name += n;
-	}
-	if(*name == '\0')
-		return 1;
-	return 0;
-}
-
-/*
- * mbox is a mailbox name which might match pat.
- * verify the match
- * generates response
- */
-static int
-checkMatch(char *cmd, char *ref, char *pat, char *mbox, long mtime, int isdir)
-{
-	char *s, *flags;
-
-	if(!matches(ref, pat, mbox) || !okMbox(mbox))
-		return 0;
-	if(strcmp(mbox, ".") == 0)
-		mbox = "";
-
-	if(isdir)
-		flags = "(\\Noselect)";
-	else{
-		s = impName(mbox);
-		if(s != nil && listMtime(s) < mtime)
-			flags = "(\\Noinferiors \\Marked)";
-		else
-			flags = "(\\Noinferiors)";
-	}
-
-	s = strmutf7(mbox);
-	if(s != nil)
-		Bprint(&bout, "* %s %s \"/\" \"%s\"\r\n", cmd, flags, s);
-	return 1;
-}
-
-static int
-matches(char *ref, char *pat, char *name)
-{
-	Rune r;
-	int i, n;
-
-	while(ref != pat)
-		if(*name++ != *ref++)
-			return 0;
-	for(; *pat; pat += n){
-		r = *(uchar*)pat;
-		if(r < Runeself)
-			n = 1;
-		else
-			n = chartorune(&r, pat);
-
-		if(r == '*'){
-			pat += n;
-			if(*pat == '\0')
-				return 1;
-			while(*name){
-				if(matches(pat, pat, name))
-					return 1;
-				name += chartorune(&r, name);
-			}
-			return 0;
-		}
-		if(r == '%'){
-			pat += n;
-			while(*name && *name != '/'){
-				if(matches(pat, pat, name))
-					return 1;
-				name += chartorune(&r, name);
-			}
-			pat -= n;
-			continue;
-		}
-		for(i = 0; i < n; i++)
-			if(name[i] != pat[i])
-				return 0;
-		name += n;
-	}
-	if(*name == '\0')
-		return 1;
-	return 0;
-}
--- a/sys/src/cmd/ip/imap4d/mbox.c
+++ /dev/null
@@ -1,863 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-static NamedInt	flagChars[NFlags] =
-{
-	{"s",	MSeen},
-	{"a",	MAnswered},
-	{"f",	MFlagged},
-	{"D",	MDeleted},
-	{"d",	MDraft},
-	{"r",	MRecent},
-};
-
-static	int	fsCtl = -1;
-
-static	void	boxFlags(Box *box);
-static	int	createImp(Box *box, Qid *qid);
-static	void	fsInit(void);
-static	void	mboxGone(Box *box);
-static	MbLock	*openImp(Box *box, int new);
-static	int	parseImp(Biobuf *b, Box *box);
-static	int	readBox(Box *box);
-static	ulong	uidRenumber(Msg *m, ulong uid, int force);
-static	int	impFlags(Box *box, Msg *m, char *flags);
-
-/*
- * strategy:
- * every mailbox file has an associated .imp file
- * which maps upas/fs message digests to uids & message flags.
- *
- * the .imp files are locked by /mail/fs/usename/L.mbox.
- * whenever the flags can be modified, the lock file
- * should be opened, thereby locking the uid & flag state.
- * for example, whenever new uids are assigned to messages,
- * and whenever flags are changed internally, the lock file
- * should be open and locked.  this means the file must be
- * opened during store command, and when changing the \seen
- * flag for the fetch command.
- *
- * if no .imp file exists, a null one must be created before
- * assigning uids.
- *
- * the .imp file has the following format
- * imp		: "imap internal mailbox description\n"
- * 			uidvalidity " " uidnext "\n"
- *			messageLines
- *
- * messageLines	:
- *		| messageLines digest " " uid " " flags "\n"
- *
- * uid, uidnext, and uidvalidity are 32 bit decimal numbers
- * printed right justified in a field NUid characters long.
- * the 0 uid implies that no uid has been assigned to the message,
- * but the flags are valid. note that message lines are in mailbox
- * order, except possibly for 0 uid messages.
- *
- * digest is an ascii hex string NDigest characters long.
- *
- * flags has a character for each of NFlag flag fields.
- * if the flag is clear, it is represented by a "-".
- * set flags are represented as a unique single ascii character.
- * the currently assigned flags are, in order:
- *	MSeen		s
- *	MAnswered	a
- *	MFlagged	f
- *	MDeleted	D
- *	MDraft		d
- */
-Box*
-openBox(char *name, char *fsname, int writable)
-{
-	Box *box;
-	MbLock *ml;
-	int n, new;
-
-	if(cistrcmp(name, "inbox") == 0)
-		if(access("msgs", AEXIST) == 0)
-			name = "msgs";
-		else
-			name = "mbox";
-	fsInit();
-	debuglog("imap4d open %s %s\n", name, fsname);
-
-	if(fprint(fsCtl, "open '/mail/box/%s/%s' %s", username, name, fsname) < 0){
-//ZZZ
-		char err[ERRMAX];
-
-		rerrstr(err, sizeof err);
-		if(strstr(err, "file does not exist") == nil)
-			fprint(2,
-		"imap4d at %lud: upas/fs open %s/%s as %s failed: '%s' %s",
-			time(nil), username, name, fsname, err,
-			ctime(time(nil)));  /* NB: ctime result ends with \n */
-		fprint(fsCtl, "close %s", fsname);
-		return nil;
-	}
-
-	/*
-	 * read box to find all messages
-	 * each one has a directory, and is in numerical order
-	 */
-	box = MKZ(Box);
-	box->writable = writable;
-
-	n = strlen(name) + 1;
-	box->name = emalloc(n);
-	strcpy(box->name, name);
-
-	n += STRLEN(".imp");
-	box->imp = emalloc(n);
-	snprint(box->imp, n, "%s.imp", name);
-
-	n = strlen(fsname) + 1;
-	box->fs = emalloc(n);
-	strcpy(box->fs, fsname);
-
-	n = STRLEN("/mail/fs/") + strlen(fsname) + 1;
-	box->fsDir = emalloc(n);
-	snprint(box->fsDir, n, "/mail/fs/%s", fsname);
-
-	box->uidnext = 1;
-	new = readBox(box);
-	if(new >= 0){
-		ml = openImp(box, new);
-		if(ml != nil){
-			closeImp(box, ml);
-			return box;
-		}
-	}
-	closeBox(box, 0);
-	return nil;
-}
-
-/*
- * check mailbox
- * returns fd of open .imp file if imped.
- * otherwise, return value is insignificant
- *
- * careful: called by idle polling proc
- */
-MbLock*
-checkBox(Box *box, int imped)
-{
-	MbLock *ml;
-	Dir *d;
-	int new;
-
-	if(box == nil)
-		return nil;
-
-	/*
-	 * if stat fails, mailbox must be gone
-	 */
-	d = cdDirstat(box->fsDir, ".");
-	if(d == nil){
-		mboxGone(box);
-		return nil;
-	}
-	new = 0;
-	if(box->qid.path != d->qid.path || box->qid.vers != d->qid.vers
-	|| box->mtime != d->mtime){
-		new = readBox(box);
-		if(new < 0){
-			free(d);
-			return nil;
-		}
-	}
-	free(d);
-	ml = openImp(box, new);
-	if(ml == nil)
-		box->writable = 0;
-	else if(!imped){
-		closeImp(box, ml);
-		ml = nil;
-	}
-	return ml;
-}
-
-/*
- * mailbox is unreachable, so mark all messages expunged
- * clean up .imp files as well.
- */
-static void
-mboxGone(Box *box)
-{
-	Msg *m;
-
-	if(cdExists(mboxDir, box->name) < 0)
-		cdRemove(mboxDir, box->imp);
-	for(m = box->msgs; m != nil; m = m->next)
-		m->expunged = 1;
-	box->writable = 0;
-}
-
-/*
- * read messages in the mailbox
- * mark message that no longer exist as expunged
- * returns -1 for failure, 0 if no new messages, 1 if new messages.
- */
-static int
-readBox(Box *box)
-{
-	Msg *msgs, *m, *last;
-	Dir *d;
-	char *s;
-	long max, id;
-	int i, nd, fd, new;
-
-	fd = cdOpen(box->fsDir, ".", OREAD);
-	if(fd < 0){
-		syslog(0, "mail",
-		    "imap4d at %lud: upas/fs stat of %s/%s aka %s failed: %r",
-			time(nil), username, box->name, box->fsDir);
-		mboxGone(box);
-		return -1;
-	}
-
-	/*
-	 * read box to find all messages
-	 * each one has a directory, and is in numerical order
-	 */
-	d = dirfstat(fd);
-	if(d == nil){
-		close(fd);
-		return -1;
-	}
-	box->mtime = d->mtime;
-	box->qid = d->qid;
-	last = nil;
-	msgs = box->msgs;
-	max = 0;
-	new = 0;
-	free(d);
-	while((nd = dirread(fd, &d)) > 0){
-		for(i = 0; i < nd; i++){
-			s = d[i].name;
-			id = strtol(s, &s, 10);
-			if(id <= max || *s != '\0'
-			|| (d[i].mode & DMDIR) != DMDIR)
-				continue;
-
-			max = id;
-
-			while(msgs != nil){
-				last = msgs;
-				msgs = msgs->next;
-				if(last->id == id)
-					goto continueDir;
-				last->expunged = 1;
-			}
-
-			new = 1;
-			m = MKZ(Msg);
-			m->id = id;
-			m->fsDir = box->fsDir;
-			m->fs = emalloc(2 * (MsgNameLen + 1));
-			m->efs = seprint(m->fs, m->fs + (MsgNameLen + 1), "%lud/", id);
-			m->size = ~0UL;
-			m->lines = ~0UL;
-			m->prev = last;
-			m->flags = MRecent;
-			if(!msgInfo(m))
-				freeMsg(m);
-			else{
-				if(last == nil)
-					box->msgs = m;
-				else
-					last->next = m;
-				last = m;
-			}
-	continueDir:;
-		}
-		free(d);
-	}
-	close(fd);
-	for(; msgs != nil; msgs = msgs->next)
-		msgs->expunged = 1;
-
-	/*
-	 * make up the imap message sequence numbers
-	 */
-	id = 1;
-	for(m = box->msgs; m != nil; m = m->next){
-		if(m->seq && m->seq != id)
-			bye("internal error assigning message numbers");
-		m->seq = id++;
-	}
-	box->max = id - 1;
-
-	return new;
-}
-
-/*
- * read in the .imp file, or make one if it doesn't exist.
- * make sure all flags and uids are consistent.
- * return the mailbox lock.
- */
-#define IMPMAGIC	"imap internal mailbox description\n"
-static MbLock*
-openImp(Box *box, int new)
-{
-	Qid qid;
-	Biobuf b;
-	MbLock *ml;
-	int fd;
-//ZZZZ
-	int once;
-
-	ml = mbLock();
-	if(ml == nil)
-		return nil;
-	fd = cdOpen(mboxDir, box->imp, OREAD);
-	once = 0;
-ZZZhack:
-	if(fd < 0 || fqid(fd, &qid) < 0){
-		if(fd < 0){
-			char buf[ERRMAX];
-
-			errstr(buf, sizeof buf);
-			if(cistrstr(buf, "does not exist") == nil)
-				fprint(2, "imap4d at %lud: imp open failed: %s\n", time(nil), buf);
-			if(!once && cistrstr(buf, "locked") != nil){
-				once = 1;
-				fprint(2, "imap4d at %lud: imp %s/%s %s locked when it shouldn't be; spinning\n", time(nil), username, box->name, box->imp);
-				fd = openLocked(mboxDir, box->imp, OREAD);
-				goto ZZZhack;
-			}
-		}
-		if(fd >= 0)
-			close(fd);
-		fd = createImp(box, &qid);
-		if(fd < 0){
-			mbUnlock(ml);
-			return nil;
-		}
-		box->dirtyImp = 1;
-		if(box->uidvalidity == 0)
-			box->uidvalidity = box->mtime;
-		box->impQid = qid;
-		new = 1;
-	}else if(qid.path != box->impQid.path || qid.vers != box->impQid.vers){
-		Binit(&b, fd, OREAD);
-		if(!parseImp(&b, box)){
-			box->dirtyImp = 1;
-			if(box->uidvalidity == 0)
-				box->uidvalidity = box->mtime;
-		}
-		Bterm(&b);
-		box->impQid = qid;
-		new = 1;
-	}
-	if(new)
-		boxFlags(box);
-	close(fd);
-	return ml;
-}
-
-/*
- * close the .imp file, after writing out any changes
- */
-void
-closeImp(Box *box, MbLock *ml)
-{
-	Msg *m;
-	Qid qid;
-	Biobuf b;
-	char buf[NFlags+1];
-	int fd;
-
-	if(ml == nil)
-		return;
-	if(!box->dirtyImp){
-		mbUnlock(ml);
-		return;
-	}
-
-	fd = cdCreate(mboxDir, box->imp, OWRITE, 0664);
-	if(fd < 0){
-		mbUnlock(ml);
-		return;
-	}
-	Binit(&b, fd, OWRITE);
-
-	box->dirtyImp = 0;
-	Bprint(&b, "%s", IMPMAGIC);
-	Bprint(&b, "%.*lud %.*lud\n", NUid, box->uidvalidity, NUid, box->uidnext);
-	for(m = box->msgs; m != nil; m = m->next){
-		if(m->expunged)
-			continue;
-		wrImpFlags(buf, m->flags, strcmp(box->fs, "imap") == 0);
-		Bprint(&b, "%.*s %.*lud %s\n", NDigest, m->info[IDigest], NUid, m->uid, buf);
-	}
-	Bterm(&b);
-
-	if(fqid(fd, &qid) >= 0)
-		box->impQid = qid;
-	close(fd);
-	mbUnlock(ml);
-}
-
-void
-wrImpFlags(char *buf, int flags, int killRecent)
-{
-	int i;
-
-	for(i = 0; i < NFlags; i++){
-		if((flags & flagChars[i].v)
-		&& (flagChars[i].v != MRecent || !killRecent))
-			buf[i] = flagChars[i].name[0];
-		else
-			buf[i] = '-';
-	}
-	buf[i] = '\0';
-}
-
-int
-emptyImp(char *mbox)
-{
-	Dir *d;
-	long mode;
-	int fd;
-
-	fd = cdCreate(mboxDir, impName(mbox), OWRITE, 0664);
-	if(fd < 0)
-		return -1;
-	d = cdDirstat(mboxDir, mbox);
-	if(d == nil){
-		close(fd);
-		return -1;
-	}
-	fprint(fd, "%s%.*lud %.*lud\n", IMPMAGIC, NUid, d->mtime, NUid, 1UL);
-	mode = d->mode & 0777;
-	nulldir(d);
-	d->mode = mode;
-	dirfwstat(fd, d);
-	free(d);
-	return fd;
-}
-
-/*
- * try to match permissions with mbox
- */
-static int
-createImp(Box *box, Qid *qid)
-{
-	Dir *d;
-	long mode;
-	int fd;
-
-	fd = cdCreate(mboxDir, box->imp, OREAD, 0664);
-	if(fd < 0)
-		return -1;
-	d = cdDirstat(mboxDir, box->name);
-	if(d != nil){
-		mode = d->mode & 0777;
-		nulldir(d);
-		d->mode = mode;
-		dirfwstat(fd, d);
-		free(d);
-	}
-	if(fqid(fd, qid) < 0){
-		close(fd);
-		return -1;
-	}
-
-	return fd;
-}
-
-/*
- * read or re-read a .imp file.
- * this is tricky:
- *	messages can be deleted by another agent
- *	we might still have a Msg for an expunged message,
- *		because we haven't told the client yet.
- *	we can have a Msg without a .imp entry.
- *	flag information is added at the end of the .imp by copy & append
- *	there can be duplicate messages (same digests).
- *
- * look up existing messages based on uid.
- * look up new messages based on in order digest matching.
- *
- * note: in the face of duplicate messages, one of which is deleted,
- * two active servers may decide different ones are valid, and so return
- * different uids for the messages.  this situation will stablize when the servers exit.
- */
-static int
-parseImp(Biobuf *b, Box *box)
-{
-	Msg *m, *mm;
-	char *s, *t, *toks[3];
-	ulong uid, u;
-	int match, n;
-
-	m = box->msgs;
-	s = Brdline(b, '\n');
-	if(s == nil || Blinelen(b) != STRLEN(IMPMAGIC)
-	|| strncmp(s, IMPMAGIC, STRLEN(IMPMAGIC)) != 0)
-		return 0;
-
-	s = Brdline(b, '\n');
-	if(s == nil || Blinelen(b) != 2*NUid + 2)
-		return 0;
-	s[2*NUid + 1] = '\0';
-	u = strtoul(s, &t, 10);
-	if(u != box->uidvalidity && box->uidvalidity != 0)
-		return 0;
-	box->uidvalidity = u;
-	if(*t != ' ' || t != s + NUid)
-		return 0;
-	t++;
-	u = strtoul(t, &t, 10);
-	if(box->uidnext > u)
-		return 0;
-	box->uidnext = u;
-	if(t != s + 2*NUid+1 || box->uidnext == 0)
-		return 0;
-
-	uid = ~0;
-	while(m != nil){
-		s = Brdline(b, '\n');
-		if(s == nil)
-			break;
-		n = Blinelen(b) - 1;
-		if(n != NDigest + NUid + NFlags + 2
-		|| s[NDigest] != ' ' || s[NDigest + NUid + 1] != ' ')
-			return 0;
-		toks[0] = s;
-		s[NDigest] = '\0';
-		toks[1] = s + NDigest + 1;
-		s[NDigest + NUid + 1] = '\0';
-		toks[2] = s + NDigest + NUid + 2;
-		s[n] = '\0';
-		t = toks[1];
-		u = strtoul(t, &t, 10);
-		if(*t != '\0' || uid != ~0 && (uid >= u && u || u && !uid))
-			return 0;
-		uid = u;
-
-		/*
-		 * zero uid => added by append or copy, only flags valid
-		 * can only match messages without uids, but this message
-		 * may not be the next one, and may have been deleted.
-		 */
-		if(!uid){
-			for(; m != nil && m->uid; m = m->next)
-				;
-			for(mm = m; mm != nil; mm = mm->next){
-				if(mm->info[IDigest] != nil &&
-				    strcmp(mm->info[IDigest], toks[0]) == 0){
-					if(!mm->uid)
-						mm->flags = 0;
-					if(!impFlags(box, mm, toks[2]))
-						return 0;
-					m = mm->next;
-					break;
-				}
-			}
-			continue;
-		}
-
-		/*
-		 * ignore expunged messages,
-		 * and messages already assigned uids which don't match this uid.
-		 * such messages must have been deleted by another imap server,
-		 * which updated the mailbox and .imp file since we read the mailbox,
-		 * or because upas/fs got confused by consecutive duplicate messages,
-		 * the first of which was deleted by another imap server.
-		 */
-		for(; m != nil && (m->expunged || m->uid && m->uid < uid); m = m->next)
-			;
-		if(m == nil)
-			break;
-
-		/*
-		 * only check for digest match on the next message,
-		 * since it comes before all other messages, and therefore
-		 * must be in the .imp file if they should be.
-		 */
-		match = m->info[IDigest] != nil &&
-			strcmp(m->info[IDigest], toks[0]) == 0;
-		if(uid && (m->uid == uid || !m->uid && match)){
-			if(!match)
-				bye("inconsistent uid");
-
-			/*
-			 * wipe out recent flag if some other server saw this new message.
-			 * it will be read from the .imp file if is really should be set,
-			 * ie the message was only seen by a status command.
-			 */
-			if(!m->uid)
-				m->flags = 0;
-
-			if(!impFlags(box, m, toks[2]))
-				return 0;
-			m->uid = uid;
-			m = m->next;
-		}
-	}
-	return 1;
-}
-
-/*
- * parse .imp flags
- */
-static int
-impFlags(Box *box, Msg *m, char *flags)
-{
-	int i, f;
-
-	f = 0;
-	for(i = 0; i < NFlags; i++){
-		if(flags[i] == '-')
-			continue;
-		if(flags[i] != flagChars[i].name[0])
-			return 0;
-		f |= flagChars[i].v;
-	}
-
-	/*
-	 * recent flags are set until the first time message's box is selected or examined.
-	 * it may be stored in the file as a side effect of a status or subscribe command;
-	 * if so, clear it out.
-	 */
-	if((f & MRecent) && strcmp(box->fs, "imap") == 0)
-		box->dirtyImp = 1;
-	f |= m->flags & MRecent;
-
-	/*
-	 * all old messages with changed flags should be reported to the client
-	 */
-	if(m->uid && m->flags != f){
-		box->sendFlags = 1;
-		m->sendFlags = 1;
-	}
-	m->flags = f;
-	return 1;
-}
-
-/*
- * assign uids to any new messages
- * which aren't already in the .imp file.
- * sum up totals for flag values.
- */
-static void
-boxFlags(Box *box)
-{
-	Msg *m;
-
-	box->recent = 0;
-	for(m = box->msgs; m != nil; m = m->next){
-		if(m->uid == 0){
-			box->dirtyImp = 1;
-			box->uidnext = uidRenumber(m, box->uidnext, 0);
-		}
-		if(m->flags & MRecent)
-			box->recent++;
-	}
-}
-
-static ulong
-uidRenumber(Msg *m, ulong uid, int force)
-{
-	for(; m != nil; m = m->next){
-		if(!force && m->uid != 0)
-			bye("uid renumbering with a valid uid");
-		m->uid = uid++;
-	}
-	return uid;
-}
-
-void
-closeBox(Box *box, int opened)
-{
-	Msg *m, *next;
-
-	/*
-	 * make sure to leave the mailbox directory so upas/fs can close the mailbox
-	 */
-	myChdir(mboxDir);
-
-	if(box->writable){
-		deleteMsgs(box);
-		if(expungeMsgs(box, 0))
-			closeImp(box, checkBox(box, 1));
-	}
-
-	if(fprint(fsCtl, "close %s", box->fs) < 0 && opened)
-		bye("can't talk to mail server");
-	for(m = box->msgs; m != nil; m = next){
-		next = m->next;
-		freeMsg(m);
-	}
-	free(box->name);
-	free(box->fs);
-	free(box->fsDir);
-	free(box->imp);
-	free(box);
-}
-
-int
-deleteMsgs(Box *box)
-{
-	Msg *m;
-	char buf[BufSize], *p, *start;
-	int ok;
-
-	if(!box->writable)
-		return 0;
-
-	/*
-	 * first pass: delete messages; gang the writes together for speed.
-	 */
-	ok = 1;
-	start = seprint(buf, buf + sizeof(buf), "delete %s", box->fs);
-	p = start;
-	for(m = box->msgs; m != nil; m = m->next){
-		if((m->flags & MDeleted) && !m->expunged){
-			m->expunged = 1;
-			p = seprint(p, buf + sizeof(buf), " %lud", m->id);
-			if(p + 32 >= buf + sizeof(buf)){
-				if(write(fsCtl, buf, p - buf) < 0)
-					bye("can't talk to mail server");
-				p = start;
-			}
-		}
-	}
-	if(p != start && write(fsCtl, buf, p - buf) < 0)
-		bye("can't talk to mail server");
-
-	return ok;
-}
-
-/*
- * second pass: remove the message structure,
- * and renumber message sequence numbers.
- * update messages counts in mailbox.
- * returns true if anything changed.
- */
-int
-expungeMsgs(Box *box, int send)
-{
-	Msg *m, *next, *last;
-	ulong n;
-
-	n = 0;
-	last = nil;
-	for(m = box->msgs; m != nil; m = next){
-		m->seq -= n;
-		next = m->next;
-		if(m->expunged){
-			if(send)
-				Bprint(&bout, "* %lud expunge\r\n", m->seq);
-			if(m->flags & MRecent)
-				box->recent--;
-			n++;
-			if(last == nil)
-				box->msgs = next;
-			else
-				last->next = next;
-			freeMsg(m);
-		}else
-			last = m;
-	}
-	if(n){
-		box->max -= n;
-		box->dirtyImp = 1;
-	}
-	return n;
-}
-
-static void
-fsInit(void)
-{
-	if(fsCtl >= 0)
-		return;
-	fsCtl = open("/mail/fs/ctl", ORDWR);
-	if(fsCtl < 0)
-		bye("can't open mail file system");
-	if(fprint(fsCtl, "close mbox") < 0)
-		bye("can't initialize mail file system");
-}
-
-static char *stoplist[] =
-{
-	"mbox",
-	"pipeto",
-	"forward",
-	"names",
-	"pipefrom",
-	"headers",
-	"imap.ok",
-	0
-};
-
-enum {
-	Maxokbytes	= 4096,
-	Maxfolders	= Maxokbytes / 4,
-};
-
-static char *folders[Maxfolders];
-static char *folderbuff;
-
-static void
-readokfolders(void)
-{
-	int fd, nr;
-
-	fd = open("imap.ok", OREAD);
-	if(fd < 0)
-		return;
-	folderbuff = malloc(Maxokbytes);
-	if(folderbuff == nil) {
-		close(fd);
-		return;
-	}
-	nr = read(fd, folderbuff, Maxokbytes-1);	/* once is ok */
-	close(fd);
-	if(nr < 0){
-		free(folderbuff);
-		folderbuff = nil;
-		return;
-	}
-	folderbuff[nr] = 0;
-	tokenize(folderbuff, folders, nelem(folders));
-}
-
-/*
- * reject bad mailboxes based on mailbox name
- */
-int
-okMbox(char *path)
-{
-	char *name;
-	int i;
-
-	if(folderbuff == nil && access("imap.ok", AREAD) == 0)
-		readokfolders();
-	name = strrchr(path, '/');
-	if(name == nil)
-		name = path;
-	else
-		name++;
-	if(folderbuff != nil){
-		for(i = 0; i < nelem(folders) && folders[i] != nil; i++)
-			if(cistrcmp(folders[i], name) == 0)
-				return 1;
-		return 0;
-	}
-	if(strlen(name) + STRLEN(".imp") >= MboxNameLen)
-		return 0;
-	for(i = 0; stoplist[i]; i++)
-		if(strcmp(name, stoplist[i]) == 0)
-			return 0;
-	if(isprefix("L.", name) || isprefix("imap-tmp.", name)
-	|| issuffix(".imp", name)
-	|| strcmp("imap.subscribed", name) == 0
-	|| isdotdot(name) || name[0] == '/')
-		return 0;
-	return 1;
-}
--- a/sys/src/cmd/ip/imap4d/mkfile
+++ /dev/null
@@ -1,31 +1,0 @@
-</$objtype/mkfile
-
-OFILES=\
-	auth.$O\
-	copy.$O\
-	csquery.$O\
-	date.$O\
-	fetch.$O\
-	imap4d.$O\
-	list.$O\
-	mbox.$O\
-	msg.$O\
-	mutf7.$O\
-	nodes.$O\
-	folder.$O\
-	search.$O\
-	store.$O\
-	utils.$O\
-	debug.$O\
-
-HFILES=imap4d.h\
-	fns.h\
-
-TARG=imap4d
-BIN=/$objtype/bin/ip
-UPDATE=\
-	mkfile\
-	$HFILES\
-	${OFILES:%.$O=%.c}\
-
-</sys/src/cmd/mkone
--- a/sys/src/cmd/ip/imap4d/msg.c
+++ /dev/null
@@ -1,1782 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <libsec.h>
-#include <auth.h>
-#include <fcall.h>
-#include "imap4d.h"
-
-static void	body64(int in, int out);
-static void	bodystrip(int in, int out);
-static void	cleanupHeader(Header *h);
-static char	*domBang(char *s);
-static void	freeMAddr(MAddr *a);
-static void	freeMimeHdr(MimeHdr *mh);
-static char	*headAddrSpec(char *e, char *w);
-static MAddr	*headAddresses(void);
-static MAddr	*headAddress(void);
-static char	*headAtom(char *disallowed);
-static int	headChar(int eat);
-static char	*headDomain(char *e);
-static MAddr	*headMAddr(MAddr *old);
-static char	*headPhrase(char *e, char *w);
-static char	*headQuoted(int start, int stop);
-static char	*headSkipWhite(int);
-static void	headSkip(void);
-static char	*headSubDomain(void);
-static char	*headText(void);
-static void	headToEnd(void);
-static char	*headWord(void);
-static void	mimeDescription(Header *h);
-static void	mimeDisposition(Header *h);
-static void	mimeEncoding(Header *h);
-static void	mimeId(Header *h);
-static void	mimeLanguage(Header *h);
-static void	mimeMd5(Header *h);
-static MimeHdr	*mimeParams(void);
-static void	mimeType(Header *h);
-static MimeHdr	*mkMimeHdr(char *s, char *t, MimeHdr *next);
-static void	msgAddDate(Msg *m);
-static void	msgAddHead(Msg *m, char *head, char *body);
-static int	msgBodySize(Msg *m);
-static int	msgHeader(Msg *m, Header *h, char *file);
-static long	msgReadFile(Msg *m, char *file, char **ss);
-static int	msgUnix(Msg *m, int top);
-static void	stripQuotes(char *q);
-static MAddr	*unixFrom(char *s);
-
-
-static char bogusBody[] = 
-	"This message contains null characters, so it cannot be displayed correctly.\r\n"
-	"Most likely you were sent a bogus message or a binary file.\r\n"
-	"\r\n"
-	"Each of the following attachments has a different version of the message.\r\n"
-	"The first is inlined with all non-printable characters stripped.\r\n"
-	"The second contains the message as it was stored in your mailbox.\r\n"
-	"The third has the initial header stripped.\r\n";
-
-static char bogusMimeText[] =
-	"Content-Disposition: inline\r\n"
-	"Content-Type: text/plain; charset=\"US-ASCII\"\r\n"
-	"Content-Transfer-Encoding: 7bit\r\n";
-
-static char bogusMimeBinary[] =
-	"Content-Disposition: attachment\r\n"
-	"Content-Type: application/octet-stream\r\n"
-	"Content-Transfer-Encoding: base64\r\n";
-
-/*
- * stop list for header fields
- */
-static char	*headFieldStop = ":";
-static char	*mimeTokenStop = "()<>@,;:\\\"/[]?=";
-static char	*headAtomStop = "()<>@,;:\\\".[]";
-static uchar	*headStr;
-static uchar	*lastWhite;
-
-long
-selectFields(char *dst, long n, char *hdr, SList *fields, int matches)
-{
-	SList *f;
-	uchar *start;
-	char *s;
-	long m, nf;
-
-	headStr = (uchar*)hdr;
-	m = 0;
-	for(;;){
-		start = headStr;
-		s = headAtom(headFieldStop);
-		if(s == nil)
-			break;
-		headSkip();
-		for(f = fields; f != nil; f = f->next){
-			if(cistrcmp(s, f->s) == !matches){
-				nf = headStr - start;
-				if(m + nf > n)
-					return 0;
-				memmove(&dst[m], start, nf);
-				m += nf;
-			}
-		}
-		free(s);
-	}
-	if(m + 3 > n)
-		return 0;
-	dst[m++] = '\r';
-	dst[m++] = '\n';
-	dst[m] = '\0';
-	return m;
-}
-
-void
-freeMsg(Msg *m)
-{
-	Msg *k, *last;
-
-	free(m->iBuf);
-	freeMAddr(m->to);
-	if(m->replyTo != m->from)
-		freeMAddr(m->replyTo);
-	if(m->sender != m->from)
-		freeMAddr(m->sender);
-	if(m->from != m->unixFrom)
-		freeMAddr(m->from);
-	freeMAddr(m->unixFrom);
-	freeMAddr(m->cc);
-	freeMAddr(m->bcc);
-	free(m->unixDate);
-	cleanupHeader(&m->head);
-	cleanupHeader(&m->mime);
-	for(k = m->kids; k != nil; ){
-		last = k;
-		k = k->next;
-		freeMsg(last);
-	}
-	free(m->fs);
-	free(m);
-}
-
-ulong
-msgSize(Msg *m)
-{
-	return m->head.size + m->size;
-}
-
-int
-infoIsNil(char *s)
-{
-	return s == nil || s[0] == '\0';
-}
-
-char*
-maddrStr(MAddr *a)
-{
-	char *host, *addr;
-	int n;
-
-	host = a->host;
-	if(host == nil)
-		host = "";
-	n = strlen(a->box) + strlen(host) + 2;
-	if(a->personal != nil)
-		n += strlen(a->personal) + 3;
-	addr = emalloc(n);
-	if(a->personal != nil)
-		snprint(addr, n, "%s <%s@%s>", a->personal, a->box, host);
-	else
-		snprint(addr, n, "%s@%s", a->box, host);
-	return addr;
-}
-
-/*
- * return actual name of f in m's fs directory
- * this is special cased when opening m/rawbody, m/mimeheader, or m/rawheader,
- * if the message was corrupted.  in that case,
- * a temporary file is made to hold the base64 encoding of m/raw.
- */
-int
-msgFile(Msg *m, char *f)
-{
-	Msg *parent, *p;
-	Dir d;
-	Tm tm;
-	char buf[64], nbuf[2];
-	uchar dbuf[64];
-	int i, n, fd, fd1, fd2;
-
-	if(!m->bogus
-	|| strcmp(f, "") != 0 && strcmp(f, "rawbody") != 0
-	&& strcmp(f, "rawheader") != 0 && strcmp(f, "mimeheader") != 0
-	&& strcmp(f, "info") != 0 && strcmp(f, "unixheader") != 0){
-		if(strlen(f) > MsgNameLen)
-			bye("internal error: msgFile name too long");
-		strcpy(m->efs, f);
-		return cdOpen(m->fsDir, m->fs, OREAD);
-	}
-
-	/*
-	 * walk up the stupid runt message parts for non-multipart messages
-	 */
-	parent = m->parent;
-	if(parent != nil && parent->parent != nil){
-		m = parent;
-		parent = m->parent;
-	}
-	p = m;
-	if(parent != nil)
-		p = parent;
-
-	if(strcmp(f, "info") == 0 || strcmp(f, "unixheader") == 0){
-		strcpy(p->efs, f);
-		return cdOpen(p->fsDir, p->fs, OREAD);
-	}
-
-	fd = imapTmp();
-	if(fd < 0)
-		return -1;
-
-	/*
-	 * craft the message parts for bogus messages
-	 */
-	if(strcmp(f, "") == 0){
-		/*
-		 * make a fake directory for each kid
-		 * all we care about is the name
-		 */
-		if(parent == nil){
-			nulldir(&d);
-			d.mode = DMDIR|0600;
-			d.qid.type = QTDIR;
-			d.name = nbuf;
-			nbuf[1] = '\0';
-			for(i = '1'; i <= '4'; i++){
-				nbuf[0] = i;
-				n = convD2M(&d, dbuf, sizeof(dbuf));
-				if(n <= BIT16SZ)
-					fprint(2, "bad convD2M %d\n", n);
-				write(fd, dbuf, n);
-			}
-		}
-	}else if(strcmp(f, "mimeheader") == 0){
-		if(parent != nil){
-			switch(m->id){
-			case 1:
-			case 2:
-				fprint(fd, "%s", bogusMimeText);
-				break;
-			case 3:
-			case 4:
-				fprint(fd, "%s", bogusMimeBinary);
-				break;
-			}
-		}
-	}else if(strcmp(f, "rawheader") == 0){
-		if(parent == nil){
-			date2tm(&tm, m->unixDate);
-			rfc822date(buf, sizeof(buf), &tm);
-			fprint(fd,
-				"Date: %s\r\n"
-				"From: imap4 daemon <%s@%s>\r\n"
-				"To: <%s@%s>\r\n"
-				"Subject: This message was illegal or corrupted\r\n"
-				"MIME-Version: 1.0\r\n"
-				"Content-Type: multipart/mixed;\r\n\tboundary=\"upas-%s\"\r\n",
-					buf, username, site, username, site, m->info[IDigest]);
-		}
-	}else if(strcmp(f, "rawbody") == 0){
-		fd1 = msgFile(p, "raw");
-		strcpy(p->efs, "rawbody");
-		fd2 = cdOpen(p->fsDir, p->fs, OREAD);
-		if(fd1 < 0 || fd2 < 0){
-			close(fd);
-			close(fd1);
-			close(fd2);
-			return -1;
-		}
-		if(parent == nil){
-			fprint(fd,
-				"This is a multi-part message in MIME format.\r\n"
-				"--upas-%s\r\n"
-				"%s"
-				"\r\n"
-				"%s"
-				"\r\n",
-					m->info[IDigest], bogusMimeText, bogusBody);
-
-			fprint(fd,
-				"--upas-%s\r\n"
-				"%s"
-				"\r\n",
-					m->info[IDigest], bogusMimeText);
-			bodystrip(fd1, fd);
-
-			fprint(fd,
-				"--upas-%s\r\n"
-				"%s"
-				"\r\n",
-					m->info[IDigest], bogusMimeBinary);
-			seek(fd1, 0, 0);
-			body64(fd1, fd);
-
-			fprint(fd,
-				"--upas-%s\r\n"
-				"%s"
-				"\r\n",
-					m->info[IDigest], bogusMimeBinary);
-			body64(fd2, fd);
-
-			fprint(fd, "--upas-%s--\r\n", m->info[IDigest]);
-		}else{
-			switch(m->id){
-			case 1:
-				fprint(fd, "%s", bogusBody);
-				break;
-			case 2:
-				bodystrip(fd1, fd);
-				break;
-			case 3:
-				body64(fd1, fd);
-				break;
-			case 4:
-				body64(fd2, fd);
-				break;
-			}
-		}
-		close(fd1);
-		close(fd2);
-	}
-	seek(fd, 0, 0);
-	return fd;
-}
-
-int
-msgIsMulti(Header *h)
-{
-	return h->type != nil && cistrcmp("multipart", h->type->s) == 0;
-}
-
-int
-msgIsRfc822(Header *h)
-{
-	return h->type != nil && cistrcmp("message", h->type->s) == 0 && cistrcmp("rfc822", h->type->t) == 0;
-}
-
-/*
- * check if a message has been deleted by someone else
- */
-void
-msgDead(Msg *m)
-{
-	if(m->expunged)
-		return;
-	*m->efs = '\0';
-	if(!cdExists(m->fsDir, m->fs))
-		m->expunged = 1;
-}
-
-/*
- * make sure the message has valid associated info
- * used for ISubject, IDigest, IInReplyTo, IMessageId.
- */
-int
-msgInfo(Msg *m)
-{
-	char *s;
-	int i;
-
-	if(m->info[0] != nil)
-		return 1;
-
-	i = msgReadFile(m, "info", &m->iBuf);
-	if(i < 0)
-		return 0;
-
-	s = m->iBuf;
-	for(i = 0; i < IMax; i++){
-		m->info[i] = s;
-		s = strchr(s, '\n');
-		if(s == nil)
-			break;
-		*s++ = '\0';
-	}
-	for(; i < IMax; i++)
-		m->info[i] = nil;
-
-	for(i = 0; i < IMax; i++)
-		if(infoIsNil(m->info[i]))
-			m->info[i] = nil;
-
-	return 1;
-}
-
-/*
- * make sure the message has valid mime structure
- * and sub-messages
- */
-int
-msgStruct(Msg *m, int top)
-{
-	Msg *k, head, *last;
-	Dir *d;
-	char *s;
-	ulong max, id;
-	int i, nd, fd, ns;
-
-	if(m->kids != nil)
-		return 1;
-
-	if(m->expunged
-	|| !msgInfo(m)
-	|| !msgUnix(m, top)
-	|| !msgBodySize(m)
-	|| !msgHeader(m, &m->mime, "mimeheader")
-	|| (top || msgIsRfc822(&m->mime) || msgIsMulti(&m->mime)) && !msgHeader(m, &m->head, "rawheader")){
-		if(top && m->bogus && !(m->bogus & BogusTried)){
-			m->bogus |= BogusTried;
-			return msgStruct(m, top);
-		}
-		msgDead(m);
-		return 0;
-	}
-
-	/*
-	 * if a message has no kids, it has a kid which is just the body of the real message
-	 */
-	if(!msgIsMulti(&m->head) && !msgIsMulti(&m->mime) && !msgIsRfc822(&m->head) && !msgIsRfc822(&m->mime)){
-		k = MKZ(Msg);
-		k->id = 1;
-		k->fsDir = m->fsDir;
-		k->bogus = m->bogus;
-		k->parent = m->parent;
-		ns = m->efs - m->fs;
-		k->fs = emalloc(ns + (MsgNameLen + 1));
-		memmove(k->fs, m->fs, ns);
-		k->efs = k->fs + ns;
-		*k->efs = '\0';
-		k->size = m->size;
-		m->kids = k;
-		return 1;
-	}
-
-	/*
-	 * read in all child messages messages
-	 */
-	fd = msgFile(m, "");
-	if(fd < 0){
-		msgDead(m);
-		return 0;
-	}
-
-	max = 0;
-	head.next = nil;
-	last = &head;
-	while((nd = dirread(fd, &d)) > 0){
-		for(i = 0; i < nd; i++){
-			s = d[i].name;
-			id = strtol(s, &s, 10);
-			if(id <= max || *s != '\0'
-			|| (d[i].mode & DMDIR) != DMDIR)
-				continue;
-
-			max = id;
-
-			k = MKZ(Msg);
-			k->id = id;
-			k->fsDir = m->fsDir;
-			k->bogus = m->bogus;
-			k->parent = m;
-			ns = strlen(m->fs);
-			k->fs = emalloc(ns + 2 * (MsgNameLen + 1));
-			k->efs = seprint(k->fs, k->fs + ns + (MsgNameLen + 1), "%s%lud/", m->fs, id);
-			k->prev = last;
-			k->size = ~0UL;
-			k->lines = ~0UL;
-			last->next = k;
-			last = k;
-		}
-	}
-	close(fd);
-	m->kids = head.next;
-
-	/*
-	 * if kids fail, just whack them
-	 */
-	top = top && (msgIsRfc822(&m->head) || msgIsMulti(&m->head));
-	for(k = m->kids; k != nil; k = k->next){
-		if(!msgStruct(k, top)){
-			for(k = m->kids; k != nil; ){
-				last = k;
-				k = k->next;
-				freeMsg(last);
-			}
-			m->kids = nil;
-			break;
-		}
-	}
-	return 1;
-}
-
-static long
-msgReadFile(Msg *m, char *file, char **ss)
-{
-	Dir *d;
-	char *s, buf[BufSize];
-	vlong length;
-	long n, nn;
-	int fd;
-
-	fd = msgFile(m, file);
-	if(fd < 0){
-		msgDead(m);
-		return -1;
-	}
-
-	n = read(fd, buf, BufSize);
-	if(n < BufSize){
-		close(fd);
-		if(n < 0){
-			*ss = nil;
-			return -1;
-		}
-		s = emalloc(n + 1);
-		memmove(s, buf, n);
-		s[n] = '\0';
-		*ss = s;
-		return n;
-	}
-
-	d = dirfstat(fd);
-	if(d == nil){
-		close(fd);
-		return -1;
-	}
-	length = d->length;
-	free(d);
-	nn = length;
-	s = emalloc(nn + 1);
-	memmove(s, buf, n);
-	if(nn > n)
-		nn = readn(fd, s+n, nn-n) + n;
-	close(fd);
-	if(nn != length){
-		free(s);
-		return -1;
-	}
-	s[nn] = '\0';
-	*ss = s;
-	return nn;
-}
-
-static void
-freeMAddr(MAddr *a)
-{
-	MAddr *p;
-
-	while(a != nil){
-		p = a;
-		a = a->next;
-		free(p->personal);
-		free(p->box);
-		free(p->host);
-		free(p);
-	}
-}
-
-/*
- * the message is corrupted or illegal.
- * reset message fields.  msgStruct will reparse the message,
- * relying on msgFile to make up corrected body parts.
- */
-static int
-msgBogus(Msg *m, int flags)
-{
-	if(!(m->bogus & flags))
-		m->bogus |= flags;
-	m->lines = ~0;
-	free(m->head.buf);
-	free(m->mime.buf);
-	memset(&m->head, 0, sizeof(Header));
-	memset(&m->mime, 0, sizeof(Header));
-	return 0;
-}
-
-/*
- *  stolen from upas/marshal; base64 encodes from one fd to another.
- *
- *  the size of buf is very important to enc64.  Anything other than
- *  a multiple of 3 will cause enc64 to output a termination sequence.
- *  To ensure that a full buf corresponds to a multiple of complete lines,
- *  we make buf a multiple of 3*18 since that's how many enc64 sticks on
- *  a single line.  This avoids short lines in the output which is pleasing
- *  but not necessary.
- */
-static int
-enc64x18(char *out, int lim, uchar *in, int n)
-{
-	int m, mm, nn;
-
-	for(nn = 0; n > 0; n -= m, nn += mm){
-		m = 18 * 3;
-		if(m > n)
-			m = n;
-		nn += 2;	/* \r\n */
-		assert(nn < lim);
-		mm = enc64(out, lim - nn, in, m);
-		assert(mm > 0);
-		in += m;
-		out += mm;
-		*out++ = '\r';
-		*out++ = '\n';
-	}
-	return nn;
-}
-
-static void
-body64(int in, int out)
-{
-	uchar buf[3*18*54];
-	char obuf[3*18*54*2];
-	int m, n;
-
-	for(;;){
-		n = readn(in, buf, sizeof(buf));
-		if(n < 0)
-			return;
-		if(n == 0)
-			break;
-		m = enc64x18(obuf, sizeof(obuf), buf, n);
-		if(write(out, obuf, m) < 0)
-			return;
-	}
-}
-
-/*
- * strip all non-printable characters from a file
- */
-static void
-bodystrip(int in, int out)
-{
-	uchar buf[3*18*54];
-	int m, n, i, c;
-
-	for(;;){
-		n = read(in, buf, sizeof(buf));
-		if(n < 0)
-			return;
-		if(n == 0)
-			break;
-		m = 0;
-		for(i = 0; i < n; i++){
-			c = buf[i];
-			if(c > 0x1f && c < 0x7f		/* normal characters */
-			|| c >= 0x9 && c <= 0xd)	/* \t, \n, vertical tab, form feed, \r */
-				buf[m++] = c;
-		}
-
-		if(m && write(out, buf, m) < 0)
-			return;
-	}
-}
-
-/*
- * read in the message body to count \n without a preceding \r
- */
-static int
-msgBodySize(Msg *m)
-{
-	Dir *d;
-	char buf[BufSize + 2], *s, *se;
-	vlong length;
-	ulong size, lines, bad;
-	int n, fd, c;
-
-	if(m->lines != ~0UL)
-		return 1;
-	fd = msgFile(m, "rawbody");
-	if(fd < 0)
-		return 0;
-	d = dirfstat(fd);
-	if(d == nil){
-		close(fd);
-		return 0;
-	}
-	length = d->length;
-	free(d);
-
-	size = 0;
-	lines = 0;
-	bad = 0;
-	buf[0] = ' ';
-	for(;;){
-		n = read(fd, &buf[1], BufSize);
-		if(n <= 0)
-			break;
-		size += n;
-		se = &buf[n + 1];
-		for(s = &buf[1]; s < se; s++){
-			c = *s;
-			if(c == '\0'){
-				close(fd);
-				return msgBogus(m, BogusBody);
-			}
-			if(c != '\n')
-				continue;
-			if(s[-1] != '\r')
-				bad++;
-			lines++;
-		}
-		buf[0] = buf[n];
-	}
-	if(size != length)
-		bye("bad length reading rawbody");
-	size += bad;
-	m->size = size;
-	m->lines = lines;
-	close(fd);
-	return 1;
-}
-
-/*
- * retrieve information from the unixheader file
- */
-static int
-msgUnix(Msg *m, int top)
-{
-	Tm tm;
-	char *s, *ss;
-
-	if(m->unixDate != nil)
-		return 1;
-
-	if(!top){
-bogus:
-		m->unixDate = estrdup("");
-		m->unixFrom = unixFrom(nil);
-		return 1;
-	}
-
-	if(msgReadFile(m, "unixheader", &ss) < 0)
-		return 0;
-	s = ss;
-	s = strchr(s, ' ');
-	if(s == nil){
-		free(ss);
-		goto bogus;
-	}
-	s++;
-	m->unixFrom = unixFrom(s);
-	s = (char*)headStr;
-	if(date2tm(&tm, s) == nil)
-		s = m->info[IUnixDate];
-	if(s == nil){
-		free(ss);
-		goto bogus;
-	}
-	m->unixDate = estrdup(s);
-	free(ss);
-	return 1;
-}
-
-/*
- * parse the address in the unix header
- * last line of defence, so must return something
- */
-static MAddr *
-unixFrom(char *s)
-{
-	MAddr *a;
-	char *e, *t;
-
-	if(s == nil)
-		return nil;
-	headStr = (uchar*)s;
-	t = emalloc(strlen(s) + 2);
-	e = headAddrSpec(t, nil);
-	if(e == nil)
-		a = nil;
-	else{
-		if(*e != '\0')
-			*e++ = '\0';
-		else
-			e = site;
-		a = MKZ(MAddr);
-
-		a->box = estrdup(t);
-		a->host = estrdup(e);
-	}
-	free(t);
-	return a;
-}
-
-/*
- * read in the entire header,
- * and parse out any existing mime headers
- */
-static int
-msgHeader(Msg *m, Header *h, char *file)
-{
-	char *s, *ss, *t, *te;
-	ulong lines, n, nn;
-	long ns;
-	int dated, c;
-
-	if(h->buf != nil)
-		return 1;
-
-	ns = msgReadFile(m, file, &ss);
-	if(ns < 0)
-		return 0;
-	s = ss;
-	n = ns;
-
-	/*
-	 * count lines ending with \n and \r\n
-	 * add an extra line at the end, since upas/fs headers
-	 * don't have a terminating \r\n
-	 */
-	lines = 1;
-	te = s + ns;
-	for(t = s; t < te; t++){
-		c = *t;
-		if(c == '\0')
-			return msgBogus(m, BogusHeader);
-		if(c != '\n')
-			continue;
-		if(t == s || t[-1] != '\r')
-			n++;
-		lines++;
-	}
-	if(t > s && t[-1] != '\n'){
-		if(t[-1] != '\r')
-			n++;
-		n++;
-	}
-	n += 2;
-	h->buf = emalloc(n + 1);
-	h->size = n;
-	h->lines = lines;
-
-	/*
-	 * make sure all headers end in \r\n
-	 */
-	nn = 0;
-	for(t = s; t < te; t++){
-		c = *t;
-		if(c == '\n'){
-			if(!nn || h->buf[nn - 1] != '\r')
-				h->buf[nn++] = '\r';
-			lines++;
-		}
-		h->buf[nn++] = c;
-	}
-	if(nn && h->buf[nn-1] != '\n'){
-		if(h->buf[nn-1] != '\r')
-			h->buf[nn++] = '\r';
-		h->buf[nn++] = '\n';
-	}
-	h->buf[nn++] = '\r';
-	h->buf[nn++] = '\n';
-	h->buf[nn] = '\0';
-	if(nn != n)
-		bye("misconverted header %ld %ld", nn, n);
-	free(s);
-
-	/*
-	 * and parse some mime headers
-	 */
-	headStr = (uchar*)h->buf;
-	dated = 0;
-	while(s = headAtom(headFieldStop)){
-		if(cistrcmp(s, "content-type") == 0)
-			mimeType(h);
-		else if(cistrcmp(s, "content-transfer-encoding") == 0)
-			mimeEncoding(h);
-		else if(cistrcmp(s, "content-id") == 0)
-			mimeId(h);
-		else if(cistrcmp(s, "content-description") == 0)
-			mimeDescription(h);
-		else if(cistrcmp(s, "content-disposition") == 0)
-			mimeDisposition(h);
-		else if(cistrcmp(s, "content-md5") == 0)
-			mimeMd5(h);
-		else if(cistrcmp(s, "content-language") == 0)
-			mimeLanguage(h);
-		else if(h == &m->head && cistrcmp(s, "from") == 0)
-			m->from = headMAddr(m->from);
-		else if(h == &m->head && cistrcmp(s, "to") == 0)
-			m->to = headMAddr(m->to);
-		else if(h == &m->head && cistrcmp(s, "reply-to") == 0)
-			m->replyTo = headMAddr(m->replyTo);
-		else if(h == &m->head && cistrcmp(s, "sender") == 0)
-			m->sender = headMAddr(m->sender);
-		else if(h == &m->head && cistrcmp(s, "cc") == 0)
-			m->cc = headMAddr(m->cc);
-		else if(h == &m->head && cistrcmp(s, "bcc") == 0)
-			m->bcc = headMAddr(m->bcc);
-		else if(h == &m->head && cistrcmp(s, "date") == 0)
-			dated = 1;
-		headSkip();
-		free(s);
-	}
-
-	if(h == &m->head){
-		if(m->from == nil){
-			m->from = m->unixFrom;
-			if(m->from != nil){
-				s = maddrStr(m->from);
-				msgAddHead(m, "From", s);
-				free(s);
-			}
-		}
-		if(m->sender == nil)
-			m->sender = m->from;
-		if(m->replyTo == nil)
-			m->replyTo = m->from;
-
-		if(infoIsNil(m->info[IDate]))
-			m->info[IDate] = m->unixDate;
-		if(!dated && m->from != nil)
-			msgAddDate(m);
-	}
-	return 1;
-}
-
-/*
- * prepend head: body to the cached header
- */
-static void
-msgAddHead(Msg *m, char *head, char *body)
-{
-	char *s;
-	long size, n;
-
-	n = strlen(head) + strlen(body) + 4;
-	size = m->head.size + n;
-	s = emalloc(size + 1);
-	snprint(s, size + 1, "%s: %s\r\n%s", head, body, m->head.buf);
-	free(m->head.buf);
-	m->head.buf = s;
-	m->head.size = size;
-	m->head.lines++;
-}
-
-static void
-msgAddDate(Msg *m)
-{
-	Tm tm;
-	char buf[64];
-
-	/* don't bother if we don't have a date */
-	if(infoIsNil(m->info[IDate]))
-		return;
-
-	date2tm(&tm, m->info[IDate]);
-	rfc822date(buf, sizeof(buf), &tm);
-	msgAddHead(m, "Date", buf);
-}
-
-static MimeHdr*
-mkMimeHdr(char *s, char *t, MimeHdr *next)
-{
-	MimeHdr *mh;
-
-	mh = MK(MimeHdr);
-	mh->s = s;
-	mh->t = t;
-	mh->next = next;
-	return mh;
-}
-
-static void
-freeMimeHdr(MimeHdr *mh)
-{
-	MimeHdr *last;
-
-	while(mh != nil){
-		last = mh;
-		mh = mh->next;
-		free(last->s);
-		free(last->t);
-		free(last);
-	}
-}
-
-static void
-cleanupHeader(Header *h)
-{
-	freeMimeHdr(h->type);
-	freeMimeHdr(h->id);
-	freeMimeHdr(h->description);
-	freeMimeHdr(h->encoding);
-	freeMimeHdr(h->md5);
-	freeMimeHdr(h->disposition);
-	freeMimeHdr(h->language);
-}
-
-/*
- * parser for rfc822 & mime header fields
- */
-
-/*
- * type		: 'content-type' ':' token '/' token params
- */
-static void
-mimeType(Header *h)
-{
-	char *s, *t;
-
-	if(headChar(1) != ':')
-		return;
-	s = headAtom(mimeTokenStop);
-	if(s == nil || headChar(1) != '/'){
-		free(s);
-		return;
-	}
-	t = headAtom(mimeTokenStop);
-	if(t == nil){
-		free(s);
-		return;
-	}
-	h->type = mkMimeHdr(s, t, mimeParams());
-}
-
-/*
- * params	:
- *		| params ';' token '=' token
- * 		| params ';' token '=' quoted-str
- */
-static MimeHdr*
-mimeParams(void)
-{
-	MimeHdr head, *last;
-	char *s, *t;
-
-	head.next = nil;
-	last = &head;
-	for(;;){
-		if(headChar(1) != ';')
-			break;
-		s = headAtom(mimeTokenStop);
-		if(s == nil || headChar(1) != '='){
-			free(s);
-			break;
-		}
-		if(headChar(0) == '"'){
-			t = headQuoted('"', '"');
-			stripQuotes(t);
-		}else
-			t = headAtom(mimeTokenStop);
-		if(t == nil){
-			free(s);
-			break;
-		}
-		last->next = mkMimeHdr(s, t, nil);
-		last = last->next;
-	}
-	return head.next;
-}
-
-/*
- * encoding	: 'content-transfer-encoding' ':' token
- */
-static void
-mimeEncoding(Header *h)
-{
-	char *s;
-
-	if(headChar(1) != ':')
-		return;
-	s = headAtom(mimeTokenStop);
-	if(s == nil)
-		return;
-	h->encoding = mkMimeHdr(s, nil, nil);
-}
-
-/*
- * mailaddr	: ':' addresses
- */
-static MAddr*
-headMAddr(MAddr *old)
-{
-	MAddr *a;
-
-	if(headChar(1) != ':')
-		return old;
-
-	if(headChar(0) == '\n')
-		return old;
-
-	a = headAddresses();
-	if(a == nil)
-		return old;
-
-	freeMAddr(old);
-	return a;
-}
-
-/*
- * addresses	: address | addresses ',' address
- */
-static MAddr*
-headAddresses(void)
-{
-	MAddr *addr, *tail, *a;
-
-	addr = headAddress();
-	if(addr == nil)
-		return nil;
-	tail = addr;
-	while(headChar(0) == ','){
-		headChar(1);
-		a = headAddress();
-		if(a == nil){
-			freeMAddr(addr);
-			return nil;
-		}
-		tail->next = a;
-		tail = a;
-	}
-	return addr;
-}
-
-/*
- * address	: mailbox | group
- * group	: phrase ':' mboxes ';' | phrase ':' ';'
- * mailbox	: addr-spec
- *		| optphrase '<' addr-spec '>'
- *		| optphrase '<' route ':' addr-spec '>'
- * optphrase	: | phrase
- * route	: '@' domain
- *		| route ',' '@' domain
- * personal names are the phrase before '<',
- * or a comment before or after a simple addr-spec
- */
-static MAddr*
-headAddress(void)
-{
-	MAddr *addr;
-	uchar *hs;
-	char *s, *e, *w, *personal;
-	int c;
-
-	s = emalloc(strlen((char*)headStr) + 2);
-	e = s;
-	personal = headSkipWhite(1);
-	c = headChar(0);
-	if(c == '<')
-		w = nil;
-	else{
-		w = headWord();
-		c = headChar(0);
-	}
-	if(c == '.' || c == '@' || c == ',' || c == '\n' || c == '\0'){
-		lastWhite = headStr;
-		e = headAddrSpec(s, w);
-		if(personal == nil){
-			hs = headStr;
-			headStr = lastWhite;
-			personal = headSkipWhite(1);
-			headStr = hs;
-		}
-	}else{
-		if(c != '<' || w != nil){
-			free(personal);
-			if(!headPhrase(e, w)){
-				free(s);
-				return nil;
-			}
-
-			/*
-			 * ignore addresses with groups,
-			 * so the only thing left if <
-			 */
-			c = headChar(1);
-			if(c != '<'){
-				free(s);
-				return nil;
-			}
-			personal = estrdup(s);
-		}else
-			headChar(1);
-
-		/*
-		 * after this point, we need to free personal before returning.
-		 * set e to nil to everything afterwards fails.
-		 *
-		 * ignore routes, they are useless, and heavily discouraged in rfc1123.
-		 * imap4 reports them up to, but not including, the terminating :
-		 */
-		e = s;
-		c = headChar(0);
-		if(c == '@'){
-			for(;;){
-				c = headChar(1);
-				if(c != '@'){
-					e = nil;
-					break;
-				}
-				headDomain(e);
-				c = headChar(1);
-				if(c != ','){
-					e = s;
-					break;
-				}
-			}
-			if(c != ':')
-				e = nil;
-		}
-
-		if(e != nil)
-			e = headAddrSpec(s, nil);
-		if(headChar(1) != '>')
-			e = nil;
-	}
-
-	/*
-	 * e points to @host, or nil if an error occured
-	 */
-	if(e == nil){
-		free(personal);
-		addr = nil;
-	}else{
-		if(*e != '\0')
-			*e++ = '\0';
-		else
-			e = site;
-		addr = MKZ(MAddr);
-
-		addr->personal = personal;
-		addr->box = estrdup(s);
-		addr->host = estrdup(e);
-	}
-	free(s);
-	return addr;
-}
-
-/*
- * phrase	: word
- *		| phrase word
- * w is the optional initial word of the phrase
- * returns the end of the phrase, or nil if a failure occured
- */
-static char*
-headPhrase(char *e, char *w)
-{
-	int c;
-
-	for(;;){
-		if(w == nil){
-			w = headWord();
-			if(w == nil)
-				return nil;
-		}
-		if(w[0] == '"')
-			stripQuotes(w);
-		strcpy(e, w);
-		free(w);
-		w = nil;
-		e = strchr(e, '\0');
-		c = headChar(0);
-		if(c <= ' ' || strchr(headAtomStop, c) != nil && c != '"')
-			break;
-		*e++ = ' ';
-		*e = '\0';
-	}
-	return e;
-}
-
-/*
- * addr-spec	: local-part '@' domain
- *		| local-part			extension to allow ! and local names
- * local-part	: word
- *		| local-part '.' word
- *
- * if no '@' is present, rewrite d!e!f!u as @d,@e:u@f,
- * where d, e, f are valid domain components.
- * the @d,@e: is ignored, since routes are ignored.
- * perhaps they should be rewritten as e!f!u@d, but that is inconsistent with upas.
- *
- * returns a pointer to '@', the end if none, or nil if there was an error
- */
-static char*
-headAddrSpec(char *e, char *w)
-{
-	char *s, *at, *b, *bang, *dom;
-	int c;
-
-	s = e;
-	for(;;){
-		if(w == nil){
-			w = headWord();
-			if(w == nil)
-				return nil;
-		}
-		strcpy(e, w);
-		free(w);
-		w = nil;
-		e = strchr(e, '\0');
-		lastWhite = headStr;
-		c = headChar(0);
-		if(c != '.')
-			break;
-		headChar(1);
-		*e++ = '.';
-		*e = '\0';
-	}
-
-	if(c != '@'){
-		/*
-		 * extenstion: allow name without domain
-		 * check for domain!xxx
-		 */
-		bang = domBang(s);
-		if(bang == nil)
-			return e;
-
-		/*
-		 * if dom1!dom2!xxx, ignore dom1!
-		 */
-		dom = s;
-		for(; b = domBang(bang + 1); bang = b)
-			dom = bang + 1;
-
-		/*
-		 * convert dom!mbox into mbox@dom
-		 */
-		*bang = '@';
-		strrev(dom, bang);
-		strrev(bang+1, e);
-		strrev(dom, e);
-		bang = &dom[e - bang - 1];
-		if(dom > s){
-			bang -= dom - s;
-			for(e = s; *e = *dom; e++)
-				dom++;
-		}
-
-		/*
-		 * eliminate a trailing '.'
-		 */
-		if(e[-1] == '.')
-			e[-1] = '\0';
-		return bang;
-	}
-	headChar(1);
-
-	at = e;
-	*e++ = '@';
-	*e = '\0';
-	if(!headDomain(e))
-		return nil;
-	return at;
-}
-
-/*
- * find the ! in domain!rest, where domain must have at least
- * one internal '.'
- */
-static char*
-domBang(char *s)
-{
-	int dot, c;
-
-	dot = 0;
-	for(; c = *s; s++){
-		if(c == '!'){
-			if(!dot || dot == 1 && s[-1] == '.' || s[1] == '\0')
-				return nil;
-			return s;
-		}
-		if(c == '"')
-			break;
-		if(c == '.')
-			dot++;
-	}
-	return nil;
-}
-
-/*
- * domain	: sub-domain
- *		| domain '.' sub-domain
- * returns the end of the domain, or nil if a failure occured
- */
-static char*
-headDomain(char *e)
-{
-	char *w;
-
-	for(;;){
-		w = headSubDomain();
-		if(w == nil)
-			return nil;
-		strcpy(e, w);
-		free(w);
-		e = strchr(e, '\0');
-		lastWhite = headStr;
-		if(headChar(0) != '.')
-			break;
-		headChar(1);
-		*e++ = '.';
-		*e = '\0';
-	}
-	return e;
-}
-
-/*
- * id		: 'content-id' ':' msg-id
- * msg-id	: '<' addr-spec '>'
- */
-static void
-mimeId(Header *h)
-{
-	char *s, *e, *w;
-
-	if(headChar(1) != ':')
-		return;
-	if(headChar(1) != '<')
-		return;
-
-	s = emalloc(strlen((char*)headStr) + 3);
-	e = s;
-	*e++ = '<';
-	e = headAddrSpec(e, nil);
-	if(e == nil || headChar(1) != '>'){
-		free(s);
-		return;
-	}
-	e = strchr(e, '\0');
-	*e++ = '>';
-	e[0] = '\0';
-	w = strdup(s);
-	free(s);
-	h->id = mkMimeHdr(w, nil, nil);
-}
-
-/*
- * description	: 'content-description' ':' *text
- */
-static void
-mimeDescription(Header *h)
-{
-	if(headChar(1) != ':')
-		return;
-	headSkipWhite(0);
-	h->description = mkMimeHdr(headText(), nil, nil);
-}
-
-/*
- * disposition	: 'content-disposition' ':' token params
- */
-static void
-mimeDisposition(Header *h)
-{
-	char *s;
-
-	if(headChar(1) != ':')
-		return;
-	s = headAtom(mimeTokenStop);
-	if(s == nil)
-		return;
-	h->disposition = mkMimeHdr(s, nil, mimeParams());
-}
-
-/*
- * md5		: 'content-md5' ':' token
- */
-static void
-mimeMd5(Header *h)
-{
-	char *s;
-
-	if(headChar(1) != ':')
-		return;
-	s = headAtom(mimeTokenStop);
-	if(s == nil)
-		return;
-	h->md5 = mkMimeHdr(s, nil, nil);
-}
-
-/*
- * language	: 'content-language' ':' langs
- * langs	: token
- *		| langs commas token
- * commas	: ','
- *		| commas ','
- */
-static void
-mimeLanguage(Header *h)
-{
-	MimeHdr head, *last;
-	char *s;
-
-	head.next = nil;
-	last = &head;
-	for(;;){
-		s = headAtom(mimeTokenStop);
-		if(s == nil)
-			break;
-		last->next = mkMimeHdr(s, nil, nil);
-		last = last->next;
-		while(headChar(0) != ',')
-			headChar(1);
-	}
-	h->language = head.next;
-}
-
-/*
- * token	: 1*<char 33-255, except "()<>@,;:\\\"/[]?=" aka mimeTokenStop>
- * atom		: 1*<chars 33-255, except "()<>@,;:\\\".[]" aka headAtomStop>
- * note this allows 8 bit characters, which occur in utf.
- */
-static char*
-headAtom(char *disallowed)
-{
-	char *s;
-	int c, ns, as;
-
-	headSkipWhite(0);
-
-	s = emalloc(StrAlloc);
-	as = StrAlloc;
-	ns = 0;
-	for(;;){
-		c = *headStr++;
-		if(c <= ' ' || strchr(disallowed, c) != nil){
-			headStr--;
-			break;
-		}
-		s[ns++] = c;
-		if(ns >= as){
-			as += StrAlloc;
-			s = erealloc(s, as);
-		}
-	}
-	if(ns == 0){
-		free(s);
-		return 0;
-	}
-	s[ns] = '\0';
-	return s;
-}
-
-/*
- * sub-domain	: atom | domain-lit
- */
-static char *
-headSubDomain(void)
-{
-	if(headChar(0) == '[')
-		return headQuoted('[', ']');
-	return headAtom(headAtomStop);
-}
-
-/*
- * word	: atom | quoted-str
- */
-static char *
-headWord(void)
-{
-	if(headChar(0) == '"')
-		return headQuoted('"', '"');
-	return headAtom(headAtomStop);
-}
-
-/*
- * q is a quoted string.  remove enclosing " and and \ escapes
- */
-static void
-stripQuotes(char *q)
-{
-	char *s;
-	int c;
-
-	if(q == nil)
-		return;
-	s = q++;
-	while(c = *q++){
-		if(c == '\\'){
-			c = *q++;
-			if(!c)
-				return;
-		}
-		*s++ = c;
-	}
-	s[-1] = '\0';
-}
-
-/*
- * quoted-str	: '"' *(any char but '"\\\r', or '\' any char, or linear-white-space) '"'
- * domain-lit	: '[' *(any char but '[]\\\r', or '\' any char, or linear-white-space) ']'
- */
-static char *
-headQuoted(int start, int stop)
-{
-	char *s;
-	int c, ns, as;
-
-	if(headChar(1) != start)
-		return nil;
-	s = emalloc(StrAlloc);
-	as = StrAlloc;
-	ns = 0;
-	s[ns++] = start;
-	for(;;){
-		c = *headStr;
-		if(c == stop){
-			headStr++;
-			break;
-		}
-		if(c == '\0'){
-			free(s);
-			return nil;
-		}
-		if(c == '\r'){
-			headStr++;
-			continue;
-		}
-		if(c == '\n'){
-			headStr++;
-			while(*headStr == ' ' || *headStr == '\t' || *headStr == '\r' || *headStr == '\n')
-				headStr++;
-			c = ' ';
-		}else if(c == '\\'){
-			headStr++;
-			s[ns++] = c;
-			c = *headStr;
-			if(c == '\0'){
-				free(s);
-				return nil;
-			}
-			headStr++;
-		}else
-			headStr++;
-		s[ns++] = c;
-		if(ns + 1 >= as){	/* leave room for \c or "0 */
-			as += StrAlloc;
-			s = erealloc(s, as);
-		}
-	}
-	s[ns++] = stop;
-	s[ns] = '\0';
-	return s;
-}
-
-/*
- * headText	: contents of rest of header line
- */
-static char *
-headText(void)
-{
-	uchar *v;
-	char *s;
-
-	v = headStr;
-	headToEnd();
-	s = emalloc(headStr - v + 1);
-	memmove(s, v, headStr - v);
-	s[headStr - v] = '\0';
-	return s;
-}
-
-/*
- * white space is ' ' '\t' or nested comments.
- * skip white space.
- * if com and a comment is seen,
- * return it's contents and stop processing white space.
- */
-static char*
-headSkipWhite(int com)
-{
-	char *s;
-	int c, incom, as, ns;
-
-	s = nil;
-	as = StrAlloc;
-	ns = 0;
-	if(com)
-		s = emalloc(StrAlloc);
-	incom = 0;
-	for(; c = *headStr; headStr++){
-		switch(c){
-		case ' ':
-		case '\t':
-		case '\r':
-			c = ' ';
-			break;
-		case '\n':
-			c = headStr[1];
-			if(c != ' ' && c != '\t')
-				goto breakout;
-			c = ' ';
-			break;
-		case '\\':
-			if(com && incom)
-				s[ns++] = c;
-			c = headStr[1];
-			if(c == '\0')
-				goto breakout;
-			headStr++;
-			break;
-		case '(':
-			incom++;
-			if(incom == 1)
-				continue;
-			break;
-		case ')':
-			incom--;
-			if(com && !incom){
-				s[ns] = '\0';
-				return s;
-			}
-			break;
-		default:
-			if(!incom)
-				goto breakout;
-			break;
-		}
-		if(com && incom && (c != ' ' || ns > 0 && s[ns-1] != ' ')){
-			s[ns++] = c;
-			if(ns + 1 >= as){	/* leave room for \c or 0 */
-				as += StrAlloc;
-				s = erealloc(s, as);
-			}
-		}
-	}
-breakout:;
-	free(s);
-	return nil;
-}
-
-/*
- * return the next non-white character
- */
-static int
-headChar(int eat)
-{
-	int c;
-
-	headSkipWhite(0);
-	c = *headStr;
-	if(eat && c != '\0' && c != '\n')
-		headStr++;
-	return c;
-}
-
-static void
-headToEnd(void)
-{
-	uchar *s;
-	int c;
-
-	for(;;){
-		s = headStr;
-		c = *s++;
-		while(c == '\r')
-			c = *s++;
-		if(c == '\n'){
-			c = *s++;
-			if(c != ' ' && c != '\t')
-				return;
-		}
-		if(c == '\0')
-			return;
-		headStr = s;
-	}
-}
-
-static void
-headSkip(void)
-{
-	int c;
-
-	while(c = *headStr){
-		headStr++;
-		if(c == '\n'){
-			c = *headStr;
-			if(c == ' ' || c == '\t')
-				continue;
-			return;
-		}
-	}
-}
--- a/sys/src/cmd/ip/imap4d/mutf7.c
+++ /dev/null
@@ -1,174 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-/*
- * modified utf-7, as per imap4 spec
- * like utf-7, but substitues , for / in base 64,
- * does not allow escaped ascii characters.
- *
- * /lib/rfc/rfc2152 is utf-7
- * /lib/rfc/rfc1642 is obsolete utf-7
- *
- * test sequences from rfc1642
- *	'A≢Α.'		'A&ImIDkQ-.'
- *	'Hi Mom ☺!"	'Hi Mom &Jjo-!'
- *	'日本語'		'&ZeVnLIqe-'
- */
-
-static uchar mt64d[256];
-static char mt64e[64];
-
-static void
-initm64(void)
-{
-	int c, i;
-
-	memset(mt64d, 255, 256);
-	memset(mt64e, '=', 64);
-	i = 0;
-	for(c = 'A'; c <= 'Z'; c++){
-		mt64e[i] = c;
-		mt64d[c] = i++;
-	}
-	for(c = 'a'; c <= 'z'; c++){
-		mt64e[i] = c;
-		mt64d[c] = i++;
-	}
-	for(c = '0'; c <= '9'; c++){
-		mt64e[i] = c;
-		mt64d[c] = i++;
-	}
-	mt64e[i] = '+';
-	mt64d['+'] = i++;
-	mt64e[i] = ',';
-	mt64d[','] = i;
-}
-
-int
-encmutf7(char *out, int lim, char *in)
-{
-	Rune rr;
-	ulong r, b;
-	char *start = out;
-	char *e = out + lim;
-	int nb;
-
-	if(mt64e[0] == 0)
-		initm64();
-	for(;;){
-		r = *(uchar*)in;
-
-		if(r < ' ' || r >= Runeself){
-			if(r == '\0')
-				break;
-			if(out + 1 >= e)
-				return -1;
-			*out++ = '&';
-			b = 0;
-			nb = 0;
-			for(;;){
-				in += chartorune(&rr, in);
-				r = rr;
-				if(r == '\0' || r >= ' ' && r < Runeself)
-					break;
-				b = (b << 16) | r;
-				for(nb += 16; nb >= 6; nb -= 6){
-					if(out + 1 >= e)
-						return -1;
-					*out++ = mt64e[(b>>(nb-6))&0x3f];
-				}
-			}
-			for(; nb >= 6; nb -= 6){
-				if(out + 1 >= e)
-					return -1;
-				*out++ = mt64e[(b>>(nb-6))&0x3f];
-			}
-			if(nb){
-				if(out + 1 >= e)
-					return -1;
-				*out++ = mt64e[(b<<(6-nb))&0x3f];
-			}
-
-			if(out + 1 >= e)
-				return -1;
-			*out++ = '-';
-			if(r == '\0')
-				break;
-		}else
-			in++;
-		if(out + 1 >= e)
-			return -1;
-		*out = r;
-		out++;
-		if(r == '&')
-			*out++ = '-';
-	}
-
-	if(out >= e)
-		return -1;
-	*out = '\0';
-	return out - start;
-}
-
-int
-decmutf7(char *out, int lim, char *in)
-{
-	Rune rr;
-	char *start = out;
-	char *e = out + lim;
-	int c, b, nb;
-
-	if(mt64e[0] == 0)
-		initm64();
-	for(;;){
-		c = *in;
-
-		if(c < ' ' || c >= Runeself){
-			if(c == '\0')
-				break;
-			return -1;
-		}
-		if(c != '&'){
-			if(out + 1 >= e)
-				return -1;
-			*out++ = c;
-			in++;
-			continue;
-		}
-		in++;
-		if(*in == '-'){
-			if(out + 1 >= e)
-				return -1;
-			*out++ = '&';
-			in++;
-			continue;
-		}
-
-		b = 0;
-		nb = 0;
-		while((c = *in++) != '-'){
-			c = mt64d[c];
-			if(c >= 64)
-				return -1;
-			b = (b << 6) | c;
-			nb += 6;
-			if(nb >= 16){
-				rr = b >> (nb - 16);
-				nb -= 16;
-				if(out + UTFmax + 1 >= e && out + runelen(rr) + 1 >= e)
-					return -1;
-				out += runetochar(out, &rr);
-			}
-		}
-		if(b & ((1 << nb) - 1))
-			return -1;
-	}
-
-	if(out >= e)
-		return -1;
-	*out = '\0';
-	return out - start;
-}
--- a/sys/src/cmd/ip/imap4d/nodes.c
+++ /dev/null
@@ -1,213 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-/*
- * iterated over all of the items in the message set.
- * errors are accumulated, but processing continues.
- * if uids, then ignore non-existent messages.
- * otherwise, that's an error
- */
-int
-forMsgs(Box *box, MsgSet *ms, ulong max, int uids, int (*f)(Box*, Msg*, int, void*), void *rock)
-{
-	Msg *m;
-	ulong id;
-	int ok, rok;
-
-	ok = 1;
-	for(; ms != nil; ms = ms->next){
-		id = ms->from;
-		rok = 0;
-		for(m = box->msgs; m != nil && m->seq <= max; m = m->next){
-			if(!uids && m->seq > id
-			|| uids && m->uid > ms->to)
-				break;
-			if(!uids && m->seq == id
-			|| uids && m->uid >= id){
-				if(!(*f)(box, m, uids, rock))
-					ok = 0;
-				if(uids)
-					id = m->uid;
-				if(id >= ms->to){
-					rok = 1;
-					break;
-				}
-				if(ms->to == ~0UL)
-					rok = 1;
-				id++;
-			}
-		}
-		if(!uids && !rok)
-			ok = 0;
-	}
-	return ok;
-}
-
-Store *
-mkStore(int sign, int op, int flags)
-{
-	Store *st;
-
-	st = binalloc(&parseBin, sizeof(Store), 1);
-	if(st == nil)
-		parseErr("out of memory");
-	st->sign = sign;
-	st->op = op;
-	st->flags = flags;
-	return st;
-}
-
-Fetch *
-mkFetch(int op, Fetch *next)
-{
-	Fetch *f;
-
-	f = binalloc(&parseBin, sizeof(Fetch), 1);
-	if(f == nil)
-		parseErr("out of memory");
-	f->op = op;
-	f->next = next;
-	return f;
-}
-
-Fetch*
-revFetch(Fetch *f)
-{
-	Fetch *last, *next;
-
-	last = nil;
-	for(; f != nil; f = next){
-		next = f->next;
-		f->next = last;
-		last = f;
-	}
-	return last;
-}
-
-NList*
-mkNList(ulong n, NList *next)
-{
-	NList *nl;
-
-	nl = binalloc(&parseBin, sizeof(NList), 0);
-	if(nl == nil)
-		parseErr("out of memory");
-	nl->n = n;
-	nl->next = next;
-	return nl;
-}
-
-NList*
-revNList(NList *nl)
-{
-	NList *last, *next;
-
-	last = nil;
-	for(; nl != nil; nl = next){
-		next = nl->next;
-		nl->next = last;
-		last = nl;
-	}
-	return last;
-}
-
-SList*
-mkSList(char *s, SList *next)
-{
-	SList *sl;
-
-	sl = binalloc(&parseBin, sizeof(SList), 0);
-	if(sl == nil)
-		parseErr("out of memory");
-	sl->s = s;
-	sl->next = next;
-	return sl;
-}
-
-SList*
-revSList(SList *sl)
-{
-	SList *last, *next;
-
-	last = nil;
-	for(; sl != nil; sl = next){
-		next = sl->next;
-		sl->next = last;
-		last = sl;
-	}
-	return last;
-}
-
-int
-BNList(Biobuf *b, NList *nl, char *sep)
-{
-	char *s;
-	int n;
-
-	s = "";
-	n = 0;
-	for(; nl != nil; nl = nl->next){
-		n += Bprint(b, "%s%lud", s, nl->n);
-		s = sep;
-	}
-	return n;
-}
-
-int
-BSList(Biobuf *b, SList *sl, char *sep)
-{
-	char *s;
-	int n;
-
-	s = "";
-	n = 0;
-	for(; sl != nil; sl = sl->next){
-		n += Bprint(b, "%s", s);
-		n += Bimapstr(b, sl->s);
-		s = sep;
-	}
-	return n;
-}
-
-int
-Bimapdate(Biobuf *b, Tm *tm)
-{
-	char buf[64];
-
-	if(tm == nil)
-		tm = localtime(time(nil));
-	imap4date(buf, sizeof(buf), tm);
-	return Bimapstr(b, buf);
-}
-
-int
-Brfc822date(Biobuf *b, Tm *tm)
-{
-	char buf[64];
-
-	if(tm == nil)
-		tm = localtime(time(nil));
-	rfc822date(buf, sizeof(buf), tm);
-	return Bimapstr(b, buf);
-}
-
-int
-Bimapstr(Biobuf *b, char *s)
-{
-	char *t;
-	int c;
-
-	if(s == nil)
-		return Bprint(b, "NIL");
-	for(t = s; ; t++){
-		c = *t;
-		if(c == '\0')
-			return Bprint(b, "\"%s\"", s);
-		if(t - s > 64 || c >= 0x7f || strchr("\"\\\r\n", c) != nil)
-			break;
-	}
-	return Bprint(b, "{%lud}\r\n%s", strlen(s), s);
-}
--- a/sys/src/cmd/ip/imap4d/search.c
+++ /dev/null
@@ -1,244 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-static int	dateCmp(char *date, Search *s);
-static int	addrSearch(MAddr *a, char *s);
-static int	fileSearch(Msg *m, char *file, char *pat);
-static int	headerSearch(Msg *m, char *hdr, char *pat);
-
-/*
- * free to exit, parseErr, since called before starting any client reply
- *
- * the header and envelope searches should convert mime character set escapes.
- */
-int
-searchMsg(Msg *m, Search *s)
-{
-	MsgSet *ms;
-	int ok;
-
-	if(!msgStruct(m, 1) || m->expunged)
-		return 0;
-	for(ok = 1; ok && s != nil; s = s->next){
-		switch(s->key){
-		default:
-			ok = 0;
-			break;
-		case SKNot:
-			ok = !searchMsg(m, s->left);
-			break;
-		case SKOr:
-			ok = searchMsg(m, s->left) || searchMsg(m, s->right);
-			break;
-		case SKAll:
-			ok = 1;
-			break;
-		case SKAnswered:
-			ok = (m->flags & MAnswered) == MAnswered;
-			break;
-		case SKDeleted:
-			ok = (m->flags & MDeleted) == MDeleted;
-			break;
-		case SKDraft:
-			ok = (m->flags & MDraft) == MDraft;
-			break;
-		case SKFlagged:
-			ok = (m->flags & MFlagged) == MFlagged;
-			break;
-		case SKKeyword:
-			ok = (m->flags & s->num) == s->num;
-			break;
-		case SKNew:
-			ok = (m->flags & (MRecent|MSeen)) == MRecent;
-			break;
-		case SKOld:
-			ok = (m->flags & MRecent) != MRecent;
-			break;
-		case SKRecent:
-			ok = (m->flags & MRecent) == MRecent;
-			break;
-		case SKSeen:
-			ok = (m->flags & MSeen) == MSeen;
-			break;
-		case SKUnanswered:
-			ok = (m->flags & MAnswered) != MAnswered;
-			break;
-		case SKUndeleted:
-			ok = (m->flags & MDeleted) != MDeleted;
-			break;
-		case SKUndraft:
-			ok = (m->flags & MDraft) != MDraft;
-			break;
-		case SKUnflagged:
-			ok = (m->flags & MFlagged) != MFlagged;
-			break;
-		case SKUnkeyword:
-			ok = (m->flags & s->num) != s->num;
-			break;
-		case SKUnseen:
-			ok = (m->flags & MSeen) != MSeen;
-			break;
-
-		case SKLarger:
-			ok = msgSize(m) > s->num;
-			break;
-		case SKSmaller:
-			ok = msgSize(m) < s->num;
-			break;
-
-		case SKBcc:
-			ok = addrSearch(m->bcc, s->s);
-			break;
-		case SKCc:
-			ok = addrSearch(m->cc, s->s);
-			break;
-		case SKFrom:
-			ok = addrSearch(m->from, s->s);
-			break;
-		case SKTo:
-			ok = addrSearch(m->to, s->s);
-			break;
-		case SKSubject:
-			ok = 0;
-			if(m->info[ISubject])
-				ok = cistrstr(m->info[ISubject], s->s) != nil;
-			break;
-
-		case SKBefore:
-			ok = dateCmp(m->unixDate, s) < 0;
-			break;
-		case SKOn:
-			ok = dateCmp(m->unixDate, s) == 0;
-			break;
-		case SKSince:
-			ok = dateCmp(m->unixDate, s) > 0;
-			break;
-		case SKSentBefore:
-			ok = dateCmp(m->info[IDate], s) < 0;
-			break;
-		case SKSentOn:
-			ok = dateCmp(m->info[IDate], s) == 0;
-			break;
-		case SKSentSince:
-			ok = dateCmp(m->info[IDate], s) > 0;
-			break;
-
-		case SKUid:
-		case SKSet:
-			for(ms = s->set; ms != nil; ms = ms->next)
-				if(s->key == SKUid && m->uid >= ms->from && m->uid <= ms->to
-				|| s->key == SKSet && m->seq >= ms->from && m->seq <= ms->to)
-					break;
-			ok = ms != nil;
-			break;
-
-		case SKHeader:
-			ok = headerSearch(m, s->hdr, s->s);
-			break;
-
-		case SKBody:
-		case SKText:
-			if(s->key == SKText && cistrstr(m->head.buf, s->s)){
-				ok = 1;
-				break;
-			}
-			ok = fileSearch(m, "body", s->s);
-			break;
-		}
-	}
-	return ok;
-}
-
-static int
-fileSearch(Msg *m, char *file, char *pat)
-{
-	char buf[BufSize + 1];
-	int n, nbuf, npat, fd, ok;
-
-	npat = strlen(pat);
-	if(npat >= BufSize / 2)
-		return 0;
-
-	fd = msgFile(m, file);
-	if(fd < 0)
-		return 0;
-	ok = 0;
-	nbuf = 0;
-	for(;;){
-		n = read(fd, &buf[nbuf], BufSize - nbuf);
-		if(n <= 0)
-			break;
-		nbuf += n;
-		buf[nbuf] = '\0';
-		if(cistrstr(buf, pat) != nil){
-			ok = 1;
-			break;
-		}
-		if(nbuf > npat){
-			memmove(buf, &buf[nbuf - npat], npat);
-			nbuf = npat;
-		}
-	}
-	close(fd);
-	return ok;
-}
-
-static int
-headerSearch(Msg *m, char *hdr, char *pat)
-{
-	SList hdrs;
-	char *s, *t;
-	int ok, n;
-
-	n = m->head.size + 3;
-	s = emalloc(n);
-	hdrs.next = nil;
-	hdrs.s = hdr;
-	ok = 0;
-	if(selectFields(s, n, m->head.buf, &hdrs, 1) > 0){
-		t = strchr(s, ':');
-		if(t != nil && cistrstr(t+1, pat) != nil)
-			ok = 1;
-	}
-	free(s);
-	return ok;
-}
-
-static int
-addrSearch(MAddr *a, char *s)
-{
-	char *ok, *addr;
-
-	for(; a != nil; a = a->next){
-		addr = maddrStr(a);
-		ok = cistrstr(addr, s);
-		free(addr);
-		if(ok != nil)
-			return 1;
-	}
-	return 0;
-}
-
-static int
-dateCmp(char *date, Search *s)
-{
-	Tm tm;
-
-	date2tm(&tm, date);
-	if(tm.year < s->year)
-		return -1;
-	if(tm.year > s->year)
-		return 1;
-	if(tm.mon < s->mon)
-		return -1;
-	if(tm.mon > s->mon)
-		return 1;
-	if(tm.mday < s->mday)
-		return -1;
-	if(tm.mday > s->mday)
-		return 1;
-	return 0;
-}
--- a/sys/src/cmd/ip/imap4d/store.c
+++ /dev/null
@@ -1,127 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-static NamedInt	flagMap[] =
-{
-	{"\\Seen",	MSeen},
-	{"\\Answered",	MAnswered},
-	{"\\Flagged",	MFlagged},
-	{"\\Deleted",	MDeleted},
-	{"\\Draft",	MDraft},
-	{"\\Recent",	MRecent},
-	{nil,		0}
-};
-
-int
-storeMsg(Box *box, Msg *m, int uids, void *vst)
-{
-	Store *st;
-	int f, flags;
-
-	USED(uids);
-
-	if(m->expunged)
-		return uids;
-
-	st = vst;
-	flags = st->flags;
-
-	f = m->flags;
-	if(st->sign == '+')
-		f |= flags;
-	else if(st->sign == '-')
-		f &= ~flags;
-	else
-		f = flags;
-
-	/*
-	 * not allowed to change the recent flag
-	 */
-	f = (f & ~MRecent) | (m->flags & MRecent);
-	setFlags(box, m, f);
-
-	if(st->op != STFlagsSilent){
-		m->sendFlags = 1;
-		box->sendFlags = 1;
-	}
-
-	return 1;
-}
-
-/*
- * update flags & global flag counts in box
- */
-void
-setFlags(Box *box, Msg *m, int f)
-{
-	if(f == m->flags)
-		return;
-
-	box->dirtyImp = 1;
-	if((f & MRecent) != (m->flags & MRecent)){
-		if(f & MRecent)
-			box->recent++;
-		else
-			box->recent--;
-	}
-	m->flags = f;
-}
-
-void
-sendFlags(Box *box, int uids)
-{
-	Msg *m;
-
-	if(!box->sendFlags)
-		return;
-
-	box->sendFlags = 0;
-	for(m = box->msgs; m != nil; m = m->next){
-		if(!m->expunged && m->sendFlags){
-			Bprint(&bout, "* %lud FETCH (", m->seq);
-			if(uids)
-				Bprint(&bout, "uid %lud ", m->uid);
-			Bprint(&bout, "FLAGS (");
-			writeFlags(&bout, m, 1);
-			Bprint(&bout, "))\r\n");
-			m->sendFlags = 0;
-		}
-	}
-}
-
-void
-writeFlags(Biobuf *b, Msg *m, int recentOk)
-{
-	char *sep;
-	int f;
-
-	sep = "";
-	for(f = 0; flagMap[f].name != nil; f++){
-		if((m->flags & flagMap[f].v)
-		&& (flagMap[f].v != MRecent || recentOk)){
-			Bprint(b, "%s%s", sep, flagMap[f].name);
-			sep = " ";
-		}
-	}
-}
-
-int
-msgSeen(Box *box, Msg *m)
-{
-	if(m->flags & MSeen)
-		return 0;
-	m->flags |= MSeen;
-	box->sendFlags = 1;
-	m->sendFlags = 1;
-	box->dirtyImp = 1;
-	return 1;
-}
-
-ulong
-mapFlag(char *name)
-{
-	return mapInt(flagMap, name);
-}
--- a/sys/src/cmd/ip/imap4d/utils.c
+++ /dev/null
@@ -1,182 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <auth.h>
-#include "imap4d.h"
-
-/*
- * reverse string [s:e) in place
- */
-void
-strrev(char *s, char *e)
-{
-	int c;
-
-	while(--e > s){
-		c = *s;
-		*s++ = *e;
-		*e = c;
-	}
-}
-
-int
-isdotdot(char *s)
-{
-	return s[0] == '.' && s[1] == '.' && (s[2] == '/' || s[2] == '\0');
-}
-
-int
-issuffix(char *suf, char *s)
-{
-	int n;
-
-	n = strlen(s) - strlen(suf);
-	if(n < 0)
-		return 0;
-	return strcmp(s + n, suf) == 0;
-}
-
-int
-isprefix(char *pre, char *s)
-{
-	return strncmp(pre, s, strlen(pre)) == 0;
-}
-
-int
-ciisprefix(char *pre, char *s)
-{
-	return cistrncmp(pre, s, strlen(pre)) == 0;
-}
-
-char*
-readFile(int fd)
-{
-	Dir *d;
-	long length;
-	char *s;
-
-	d = dirfstat(fd);
-	if(d == nil)
-		return nil;
-	length = d->length;
-	free(d);
-	s = binalloc(&parseBin, length + 1, 0);
-	if(s == nil || read(fd, s, length) != length)
-		return nil;
-	s[length] = '\0';
-	return s;
-}
-
-/*
- * create the imap tmp file.
- * it just happens that we don't need multiple temporary files.
- */
-int
-imapTmp(void)
-{
-	char buf[ERRMAX], name[MboxNameLen];
-	int tries, fd;
-
-	snprint(name, sizeof(name), "/mail/box/%s/mbox.tmp.imp", username);
-	for(tries = 0; tries < LockSecs*2; tries++){
-		fd = create(name, ORDWR|ORCLOSE|OCEXEC, DMEXCL|0600);
-		if(fd >= 0)
-			return fd;
-		errstr(buf, sizeof buf);
-		if(cistrstr(buf, "locked") == nil)
-			break;
-		sleep(500);
-	}
-	return -1;
-}
-
-/*
- * open a file which might be locked.
- * if it is, spin until available
- */
-int
-openLocked(char *dir, char *file, int mode)
-{
-	char buf[ERRMAX];
-	int tries, fd;
-
-	for(tries = 0; tries < LockSecs*2; tries++){
-		fd = cdOpen(dir, file, mode);
-		if(fd >= 0)
-			return fd;
-		errstr(buf, sizeof buf);
-		if(cistrstr(buf, "locked") == nil)
-			break;
-		sleep(500);
-	}
-	return -1;
-}
-
-int
-fqid(int fd, Qid *qid)
-{
-	Dir *d;
-
-	d = dirfstat(fd);
-	if(d == nil)
-		return -1;
-	*qid = d->qid;
-	free(d);
-	return 0;
-}
-
-ulong
-mapInt(NamedInt *map, char *name)
-{
-	int i;
-
-	for(i = 0; map[i].name != nil; i++)
-		if(cistrcmp(map[i].name, name) == 0)
-			break;
-	return map[i].v;
-}
-
-char*
-estrdup(char *s)
-{
-	char *t;
-
-	t = emalloc(strlen(s) + 1);
-	strcpy(t, s);
-	return t;
-}
-
-void*
-emalloc(ulong n)
-{
-	void *p;
-
-	p = malloc(n);
-	if(p == nil)
-		bye("server out of memory");
-	setmalloctag(p, getcallerpc(&n));
-	return p;
-}
-
-void*
-ezmalloc(ulong n)
-{
-	void *p;
-
-	p = malloc(n);
-	if(p == nil)
-		bye("server out of memory");
-	setmalloctag(p, getcallerpc(&n));
-	memset(p, 0, n);
-	return p;
-}
-
-void*
-erealloc(void *p, ulong n)
-{
-	p = realloc(p, n);
-	if(p == nil)
-		bye("server out of memory");
-	setrealloctag(p, getcallerpc(&p));
-	return p;
-}
--- a/sys/src/cmd/ip/mkfile
+++ b/sys/src/cmd/ip/mkfile
@@ -27,7 +27,7 @@
 	socksd\
 	wol\
 
-DIRS=ftpfs cifsd dhcpd httpd ipconfig ppp imap4d snoopy
+DIRS=ftpfs cifsd dhcpd httpd ipconfig ppp snoopy
 
 BIN=/$objtype/bin/ip
 HFILES=dhcp.h arp.h glob.h icmp.h telnet.h
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/dat.h
@@ -1,0 +1,166 @@
+typedef struct Event Event;
+typedef struct Exec Exec;
+typedef struct Message Message;
+typedef struct Window Window;
+
+enum
+{
+	STACK		= 8192,
+	EVENTSIZE	= 256,
+	NEVENT		= 5,
+};
+
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	b[EVENTSIZE*UTFmax+1];
+	Rune	r[EVENTSIZE+1];
+};
+
+struct Window
+{
+	/* file descriptors */
+	int		ctl;
+	int		event;
+	int		addr;
+	int		data;
+	Biobuf	*body;
+
+	/* event input */
+	char		buf[512];
+	char		*bufp;
+	int		nbuf;
+	Event	e[NEVENT];
+
+	int		id;
+	int		open;
+	Channel	*cevent;
+};
+
+struct Message
+{
+	Window	*w;
+	int		ctlfd;
+	char		*name;
+	char		*replyname;
+	uchar	opened;
+	uchar	dirty;
+	uchar	isreply;
+	uchar	deleted;
+	uchar	writebackdel;
+	uchar	tagposted;
+	uchar	recursed;
+	uchar	level;
+
+	/* header info */
+	char		*fromcolon;	/* from header file; all rest are from info file */
+	char		*from;
+	char		*to;
+	char		*cc;
+	char		*replyto;
+	char		*date;
+	char		*subject;
+	char		*type;
+	char		*disposition;
+	char		*filename;
+	char		*digest;
+
+	Message	*next;	/* next in this mailbox */
+	Message	*prev;	/* prev in this mailbox */
+	Message	*head;	/* first subpart */
+	Message	*tail;		/* last subpart */
+};
+
+enum
+{
+	NARGS		= 100,
+	NARGCHAR	= 8*1024,
+	EXECSTACK 	= STACK+(NARGS+1)*sizeof(char*)+NARGCHAR
+};
+
+struct Exec
+{
+	char		*prog;
+	char		**argv;
+	int		p[2];	/* p[1] is write to program; p[0] set to prog fd 0*/
+	int		q[2];	/* q[0] is read from program; q[1] set to prog fd 1 */
+	Channel	*sync;
+};
+
+extern	Window*	newwindow(void);
+extern	int		winopenfile(Window*, char*);
+extern	void		winopenbody(Window*, int);
+extern	void		winclosebody(Window*);
+extern	void		wintagwrite(Window*, char*, int);
+extern	void		winname(Window*, char*);
+extern	void		winwriteevent(Window*, Event*);
+extern	void		winread(Window*, uint, uint, char*);
+extern	int		windel(Window*, int);
+extern	void		wingetevent(Window*, Event*);
+extern	void		wineventproc(void*);
+extern	void		winwritebody(Window*, char*, int);
+extern	void		winclean(Window*);
+extern	int		winselect(Window*, char*, int);
+extern	char*	winselection(Window*);
+extern	int		winsetaddr(Window*, char*, int);
+extern	char*	winreadbody(Window*, int*);
+extern	void		windormant(Window*);
+extern	void		winsetdump(Window*, char*, char*);
+
+extern	void		readmbox(Message*, char*, char*);
+extern	void		rewritembox(Window*, Message*);
+
+extern	void		mkreply(Message*, char*, char*, Plumbattr*, char*);
+extern	void		delreply(Message*);
+extern	int		write2(int, int, char*, int, int);
+
+extern	int		mesgadd(Message*, char*, Dir*, char*);
+extern	void		mesgmenu(Window*, Message*);
+extern	void		mesgmenunew(Window*, Message*);
+extern	int		mesgopen(Message*, char*, char*, Message*, int, char*);
+extern	void		mesgctl(void*);
+extern	void		mesgsend(Message*);
+extern	void		mesgdel(Message*, Message*);
+extern	void		mesgmenudel(Window*, Message*, Message*);
+extern	void		mesgmenumark(Window*, char*, char*);
+extern	void		mesgmenumarkdel(Window*, Message*, Message*, int);
+extern	Message*	mesglookup(Message*, char*, char*);
+extern	Message*	mesglookupfile(Message*, char*, char*);
+extern	void		mesgfreeparts(Message*);
+
+extern	char*	readfile(char*, char*, int*);
+extern	char*	readbody(char*, char*, int*);
+extern	void		ctlprint(int, char*, ...);
+extern	void*	emalloc(uint);
+extern	void*	erealloc(void*, uint);
+extern	char*	estrdup(char*);
+extern	char*	estrstrdup(char*, char*);
+extern	char*	egrow(char*, char*, char*);
+extern	char*	eappend(char*, char*, char*);
+extern	void		error(char*, ...);
+extern	int		tokenizec(char*, char**, int, char*);
+extern	void		execproc(void*);
+
+#pragma	varargck	argpos	error	1
+#pragma	varargck	argpos	ctlprint	2
+
+extern	Window	*wbox;
+extern	Message	mbox;
+extern	Message	replies;
+extern	char		*fsname;
+extern	int		plumbsendfd;
+extern	int		plumbseemailfd;
+extern	char		*home;
+extern	char		*outgoing;
+extern	char		*mailboxdir;
+extern	char		*user;
+extern	char		deleted[];
+extern	int		wctlfd;
+extern	int		shortmenu;
+extern	int		altmenu;
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/html.c
@@ -1,0 +1,75 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <ctype.h>
+#include <plumb.h>
+#include "dat.h"
+
+
+char*
+formathtml(char *body, int *np)
+{
+	int i, j, p[2], q[2];
+	Exec *e;
+	char buf[1024];
+	Channel *sync;
+
+	e = emalloc(sizeof(struct Exec));
+	if(pipe(p) < 0 || pipe(q) < 0)
+		error("can't create pipe: %r");
+
+	e->p[0] = p[0];
+	e->p[1] = p[1];
+	e->q[0] = q[0];
+	e->q[1] = q[1];
+	e->argv = emalloc(3*sizeof(char*));
+	e->argv[0] = estrdup("htmlfmt");
+	e->argv[1] = estrdup("-cutf-8");
+	e->argv[2] = nil;
+	e->prog = "/bin/htmlfmt";
+	sync = chancreate(sizeof(int), 0);
+	e->sync = sync;
+	proccreate(execproc, e, EXECSTACK);
+	recvul(sync);
+	close(p[0]);
+	close(q[1]);
+
+	if((i=write(p[1], body, *np)) != *np){
+		fprint(2, "Mail: warning: htmlfmt failed: wrote %d of %d: %r\n", i, *np);
+		close(p[1]);
+		close(q[0]);
+		return body;
+	}
+	close(p[1]);
+
+	free(body);
+	body = nil;
+	i = 0;
+	for(;;){
+		j = read(q[0], buf, sizeof buf);
+		if(j <= 0)
+			break;
+		body = realloc(body, i+j+1);
+		if(body == nil)
+			error("realloc failed: %r");
+		memmove(body+i, buf, j);
+		i += j;
+		body[i] = '\0';
+	}
+	close(q[0]);
+
+	*np = i;
+	return body;
+}
+
+char*
+readbody(char *type, char *dir, int *np)
+{
+	char *body;
+	
+	body = readfile(dir, "body", np);
+	if(body != nil && strcmp(type, "text/html") == 0)
+		return formathtml(body, np);
+	return body;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/mail.c
@@ -1,0 +1,620 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include <ctype.h>
+#include "dat.h"
+
+char	*maildir = "/mail/fs/";			/* mountpoint of mail file system */
+char	*mailtermdir = "/mnt/term/mail/fs/";	/* alternate mountpoint */
+char *mboxname = "mbox";			/* mailboxdir/mboxname is mail spool file */
+char	*mailboxdir = nil;				/* nil == /mail/box/$user */
+char *fsname;						/* filesystem for mailboxdir/mboxname is at maildir/fsname */
+char	*user;
+char	*outgoing;
+
+Window	*wbox;
+Message	mbox;
+Message	replies;
+char		*home;
+int		plumbsendfd;
+int		plumbseemailfd;
+int		plumbshowmailfd;
+int		plumbsendmailfd;
+Channel	*cplumb;
+Channel	*cplumbshow;
+Channel	*cplumbsend;
+int		wctlfd;
+void		mainctl(void*);
+void		plumbproc(void*);
+void		plumbshowproc(void*);
+void		plumbsendproc(void*);
+void		plumbthread(void);
+void		plumbshowthread(void*);
+void		plumbsendthread(void*);
+
+int		shortmenu;
+int		altmenu;
+
+void
+usage(void)
+{
+	fprint(2, "usage: Mail [-sS] [-o outgoing] [mailboxname [directoryname]]\n");
+	threadexitsall("usage");
+}
+
+void
+removeupasfs(void)
+{
+	char buf[256];
+
+	if(strcmp(mboxname, "mbox") == 0)
+		return;
+	snprint(buf, sizeof buf, "close %s", mboxname);
+	write(mbox.ctlfd, buf, strlen(buf));
+}
+
+int
+ismaildir(char *s)
+{
+	char *path;
+	Dir *d;
+	int ret;
+
+	path = smprint("%s%s", maildir, s);
+	d = dirstat(path);
+	free(path);
+	if(d == nil)
+		return 0;
+	ret = d->qid.type & QTDIR;
+	free(d);
+	return ret;
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char *s, *name;
+	char err[ERRMAX], *cmd;
+	int i, newdir;
+	Fmt fmt;
+
+	doquote = needsrcquote;
+	quotefmtinstall();
+
+	/* open these early so we won't miss notification of new mail messages while we read mbox */
+	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
+	plumbseemailfd = plumbopen("seemail", OREAD|OCEXEC);
+	plumbshowmailfd = plumbopen("showmail", OREAD|OCEXEC);
+
+	shortmenu = 0;
+	altmenu = 0;
+	ARGBEGIN{
+	case 's':
+		shortmenu = 1;
+		break;
+	case 'S':
+		shortmenu = 2;
+		break;
+	case 'A':
+		altmenu = 1;
+		break;
+	case 'o':
+		outgoing = EARGF(usage());
+		break;
+	case 'm':
+		smprint(maildir, "%s/", EARGF(usage()));
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	name = "mbox";
+
+	/* bind the terminal /mail/fs directory over the local one */
+	if(access(maildir, 0)<0 && access(mailtermdir, 0)==0)
+		bind(mailtermdir, maildir, MAFTER);
+
+	newdir = 1;
+	if(argc > 0){
+		i = strlen(argv[0]);
+		if(argc>2 || i==0)
+			usage();
+		/* see if the name is that of an existing /mail/fs directory */
+		if(argc==1 && strchr(argv[0], '/')==0 && ismaildir(argv[0])){
+			name = argv[0];
+			mboxname = eappend(estrdup(maildir), "", name);
+			newdir = 0;
+		}else{
+			if(argv[0][i-1] == '/')
+				argv[0][i-1] = '\0';
+			s = strrchr(argv[0], '/');
+			if(s == nil)
+				mboxname = estrdup(argv[0]);
+			else{
+				*s++ = '\0';
+				if(*s == '\0')
+					usage();
+				mailboxdir = argv[0];
+				mboxname = estrdup(s);
+			}
+			if(argc > 1)
+				name = argv[1];
+			else
+				name = mboxname;
+		}
+	}
+
+	user = getenv("user");
+	if(user == nil)
+		user = "none";
+	if(mailboxdir == nil)
+		mailboxdir = estrstrdup("/mail/box/", user);
+	if(outgoing == nil)
+		outgoing = estrstrdup(mailboxdir, "/outgoing");
+
+	s = estrstrdup(maildir, "ctl");
+	mbox.ctlfd = open(s, ORDWR|OCEXEC);
+	if(mbox.ctlfd < 0)
+		error("can't open %s: %r", s);
+
+	fsname = estrdup(name);
+	if(newdir && argc > 0){
+		s = emalloc(5+strlen(mailboxdir)+strlen(mboxname)+strlen(name)+10+1);
+		for(i=0; i<10; i++){
+			sprint(s, "open %s/%s %s", mailboxdir, mboxname, fsname);
+			if(write(mbox.ctlfd, s, strlen(s)) >= 0)
+				break;
+			err[0] = '\0';
+			errstr(err, sizeof err);
+			if(strstr(err, "mbox name in use") == nil)
+				error("can't create directory %s for mail: %s", name, err);
+			free(fsname);
+			fsname = emalloc(strlen(name)+10);
+			sprint(fsname, "%s-%d", name, i);
+		}
+		if(i == 10)
+			error("can't open %s/%s: %r", mailboxdir, mboxname);
+		free(s);
+	}
+
+	s = estrstrdup(fsname, "/");
+	mbox.name = estrstrdup(maildir, s);
+	mbox.level= 0;
+	readmbox(&mbox, maildir, s);
+	home = getenv("home");
+	if(home == nil)
+		home = "/";
+
+	wbox = newwindow();
+	winname(wbox, mbox.name);
+	wintagwrite(wbox, "Put Mail Delmesg Save ", 3+1+4+1+7+1+4+1);
+	threadcreate(mainctl, wbox, STACK);
+
+	fmtstrinit(&fmt);
+	fmtprint(&fmt, "Mail");
+	if(shortmenu)
+		fmtprint(&fmt, " -%c", "sS"[shortmenu-1]);
+	if(altmenu)
+		fmtprint(&fmt, " -A");
+	if(outgoing)
+		fmtprint(&fmt, " -o %s", outgoing);
+	fmtprint(&fmt, " %s", name);
+	cmd = fmtstrflush(&fmt);
+	if(cmd == nil)
+		sysfatal("out of memory");
+	winsetdump(wbox, "/acme/mail", cmd);
+	mbox.w = wbox;
+
+	mesgmenu(wbox, &mbox);
+	winclean(wbox);
+
+	wctlfd = open("/dev/wctl", OWRITE|OCEXEC);	/* for acme window */
+	cplumb = chancreate(sizeof(Plumbmsg*), 0);
+	cplumbshow = chancreate(sizeof(Plumbmsg*), 0);
+	if(strcmp(name, "mbox") == 0){
+		/*
+		 * Avoid creating multiple windows to send mail by only accepting
+		 * sendmail plumb messages if we're reading the main mailbox.
+		 */
+		plumbsendmailfd = plumbopen("sendmail", OREAD|OCEXEC);
+		cplumbsend = chancreate(sizeof(Plumbmsg*), 0);
+		proccreate(plumbsendproc, nil, STACK);
+		threadcreate(plumbsendthread, nil, STACK);
+	}
+	/* start plumb reader as separate proc ... */
+	proccreate(plumbproc, nil, STACK);
+	proccreate(plumbshowproc, nil, STACK);
+	threadcreate(plumbshowthread, nil, STACK);
+	/* ... and use this thread to read the messages */
+	plumbthread();
+}
+
+void
+plumbproc(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbproc");
+	for(;;){
+		m = plumbrecv(plumbseemailfd);
+		sendp(cplumb, m);
+		if(m == nil)
+			threadexits(nil);
+	}
+}
+
+void
+plumbshowproc(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbshowproc");
+	for(;;){
+		m = plumbrecv(plumbshowmailfd);
+		sendp(cplumbshow, m);
+		if(m == nil)
+			threadexits(nil);
+	}
+}
+
+void
+plumbsendproc(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbsendproc");
+	for(;;){
+		m = plumbrecv(plumbsendmailfd);
+		sendp(cplumbsend, m);
+		if(m == nil)
+			threadexits(nil);
+	}
+}
+
+void
+newmesg(char *name, char *digest)
+{
+	Dir *d;
+
+	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
+		return;	/* message is about another mailbox */
+	if(mesglookupfile(&mbox, name, digest) != nil)
+		return;
+	d = dirstat(name);
+	if(d == nil)
+		return;
+	if(mesgadd(&mbox, mbox.name, d, digest))
+		mesgmenunew(wbox, &mbox);
+	free(d);
+}
+
+void
+showmesg(char *name, char *digest)
+{
+	char *n;
+
+	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
+		return;	/* message is about another mailbox */
+	n = estrdup(name+strlen(mbox.name));
+	if(n[strlen(n)-1] != '/')
+		n = egrow(n, "/", nil);
+	mesgopen(&mbox, mbox.name, name+strlen(mbox.name), nil, 1, digest);
+	free(n);
+}
+
+void
+delmesg(char *name, char *digest, int dodel)
+{
+	Message *m;
+
+	m = mesglookupfile(&mbox, name, digest);
+	if(m != nil){
+		mesgmenumarkdel(wbox, &mbox, m, 0);
+		if(dodel)
+			m->writebackdel = 1;
+	}
+}
+
+extern int mesgsave(Message*, char*);
+void
+savemesg(char *box, char *name, char *digest)
+{
+	char *s;
+	int ok;
+	Message *m;
+
+	m = mesglookupfile(&mbox, name, digest);
+	if(!m || m->isreply)
+		return;
+	s = estrdup("\t[saved");
+	if(!box[0])
+		ok = mesgsave(m, "stored");
+	else{
+		ok = mesgsave(m, box);
+		s = eappend(s, " ", box);
+	}
+	if(ok){
+		s = egrow(s, "]", nil);
+		mesgmenumark(mbox.w, m->name, s);
+	}
+	free(s);
+	
+}
+
+void
+plumbthread(void)
+{
+	Plumbmsg *m;
+	Plumbattr *a;
+	char *type, *digest;
+
+	threadsetname("plumbthread");
+	while((m = recvp(cplumb)) != nil){
+		a = m->attr;
+		digest = plumblookup(a, "digest");
+		type = plumblookup(a, "mailtype");
+		if(type == nil)
+			fprint(2, "Mail: plumb message with no mailtype attribute\n");
+		else if(strcmp(type, "new") == 0)
+			newmesg(m->data, digest);
+		else if(strcmp(type, "delete") == 0)
+			delmesg(m->data, digest, 0);
+		else
+			fprint(2, "Mail: unknown plumb attribute %s\n", type);
+		plumbfree(m);
+	}
+	threadexits(nil);
+}
+
+void
+plumbshowthread(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbshowthread");
+	while((m = recvp(cplumbshow)) != nil){
+		showmesg(m->data, plumblookup(m->attr, "digest"));
+		plumbfree(m);
+	}
+	threadexits(nil);
+}
+
+void
+plumbsendthread(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbsendthread");
+	while((m = recvp(cplumbsend)) != nil){
+		mkreply(nil, "Mail", m->data, m->attr, nil);
+		plumbfree(m);
+	}
+	threadexits(nil);
+}
+
+int
+mboxcommand(Window *w, char *s)
+{
+	char *args[10], **targs, *r, *box;
+	Message *m, *next;
+	int ok, nargs, i, j;
+	char buf[128];
+
+	nargs = tokenize(s, args, nelem(args));
+	if(nargs == 0)
+		return 0;
+	if(strcmp(args[0], "Mail") == 0){
+		if(nargs == 1)
+			mkreply(nil, "Mail", "", nil, nil);
+		else
+			mkreply(nil, "Mail", args[1], nil, nil);
+		return 1;
+	}
+	if(strcmp(s, "Del") == 0){
+		if(mbox.dirty){
+			mbox.dirty = 0;
+			fprint(2, "mail: mailbox not written\n");
+			return 1;
+		}
+		ok = 1;
+		for(m=mbox.head; m!=nil; m=next){
+			next = m->next;
+			if(m->w){
+				if(windel(m->w, 0))
+					m->w = nil;
+				else
+					ok = 0;
+			}
+		}
+		for(m=replies.head; m!=nil; m=next){
+			next = m->next;
+			if(m->w){
+				if(windel(m->w, 0))
+					m->w = nil;
+				else
+					ok = 0;
+			}
+		}
+		if(ok){
+			windel(w, 1);
+			removeupasfs();
+			threadexitsall(nil);
+		}
+		return 1;
+	}
+	if(strcmp(s, "Put") == 0){
+		rewritembox(wbox, &mbox);
+		return 1;
+	}
+	if(strcmp(s, "Delmesg") == 0){
+		if(nargs > 1){
+			for(i=1; i<nargs; i++){
+				snprint(buf, sizeof buf, "%s%s", mbox.name, args[i]);
+				delmesg(buf, nil, 1);
+			}
+		}
+		s = winselection(w);
+		if(s == nil)
+			return 1;
+		nargs = 1;
+		for(i=0; s[i]; i++)
+			if(s[i] == '\n')
+				nargs++;
+		targs = emalloc(nargs*sizeof(char*));	/* could be too many for a local array */
+		nargs = getfields(s, targs, nargs, 1, "\n");
+		for(i=0; i<nargs; i++){
+			if(!isdigit(targs[i][0]))
+				continue;
+			j = atoi(targs[i]);	/* easy way to parse the number! */
+			if(j == 0)
+				continue;
+			snprint(buf, sizeof buf, "%s%d", mbox.name, j);
+			delmesg(buf, nil, 1);
+		}
+		free(s);
+		free(targs);
+		return 1;
+	}
+	if(strncmp(args[0], "Save", 4) == 0){
+		box = "";
+		i = 1;
+		if(nargs > 1 && !mesglookupfile(&mbox, args[1], nil)){
+			box = args[1];
+			i++;
+			nargs--;
+		}
+		if(nargs > 1){
+			for(; i<nargs; i++){
+				snprint(buf, sizeof buf, "%s%s", mbox.name, args[i]);
+				savemesg(box, buf, nil);
+			}
+		}
+		s = winselection(w);
+		if(s == nil)
+			return 1;
+		nargs = 1;
+		for(i=0; s[i]; i++)
+			if(s[i] == '\n')
+				nargs++;
+		targs = emalloc(nargs*sizeof(char*));	/* could be too many for a local array */
+		nargs = getfields(s, targs, nargs, 1, "\n");
+		for(i=0; i<nargs; i++){
+			if(!isdigit(targs[i][0]))
+				continue;
+			j = strtoul(targs[i], &r, 10);
+			if(j == 0 || *r != '/')
+				continue;
+			snprint(buf, sizeof buf, "%s%d", mbox.name, j);
+			savemesg(box, buf, nil);
+		}
+		free(s);
+		free(targs);
+		return 1;
+	}
+	return 0;
+}
+
+void
+mainctl(void *v)
+{
+	Window *w;
+	Event *e, *e2, *eq, *ea;
+	int na, nopen;
+	char *s, *t, *buf;
+
+	w = v;
+	proccreate(wineventproc, w, STACK);
+
+	for(;;){
+		e = recvp(w->cevent);
+		switch(e->c1){
+		default:
+		Unknown:
+			print("unknown message %c%c\n", e->c1, e->c2);
+			break;
+	
+		case 'E':	/* write to body; can't affect us */
+			break;
+	
+		case 'F':	/* generated by our actions; ignore */
+			break;
+	
+		case 'K':	/* type away; we don't care */
+			break;
+	
+		case 'M':
+			switch(e->c2){
+			case 'x':
+			case 'X':
+				ea = nil;
+				e2 = nil;
+				if(e->flag & 2)
+					e2 = recvp(w->cevent);
+				if(e->flag & 8){
+					ea = recvp(w->cevent);
+					na = ea->nb;
+					recvp(w->cevent);
+				}else
+					na = 0;
+				s = e->b;
+				/* if it's a known command, do it */
+				if((e->flag&2) && e->nb==0)
+					s = e2->b;
+				if(na){
+					t = emalloc(strlen(s)+1+na+1);
+					sprint(t, "%s %s", s, ea->b);
+					s = t;
+				}
+				/* if it's a long message, it can't be for us anyway */
+				if(!mboxcommand(w, s))	/* send it back */
+					winwriteevent(w, e);
+				if(na)
+					free(s);
+				break;
+	
+			case 'l':
+			case 'L':
+				buf = nil;
+				eq = e;
+				if(e->flag & 2){
+					e2 = recvp(w->cevent);
+					eq = e2;
+				}
+				s = eq->b;
+				if(eq->q1>eq->q0 && eq->nb==0){
+					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
+					winread(w, eq->q0, eq->q1, buf);
+					s = buf;
+				}
+				nopen = 0;
+				do{
+					/* skip 'deleted' string if present' */
+					if(strncmp(s, deleted, strlen(deleted)) == 0)
+						s += strlen(deleted);
+					/* skip mail box name if present */
+					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
+						s += strlen(mbox.name);
+					nopen += mesgopen(&mbox, mbox.name, s, nil, 0, nil);
+					while(*s!='\0' && *s++!='\n')
+						;
+				}while(*s);
+				if(nopen == 0)	/* send it back */
+					winwriteevent(w, e);
+				free(buf);
+				break;
+	
+			case 'I':	/* modify away; we don't care */
+			case 'D':
+			case 'd':
+			case 'i':
+				break;
+	
+			default:
+				goto Unknown;
+			}
+		}
+	}
+}
+
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/mesg.c
@@ -1,0 +1,1390 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <ctype.h>
+#include <plumb.h>
+#include "dat.h"
+
+enum
+{
+	DIRCHUNK = 32*sizeof(Dir)
+};
+
+char	regexchars[] = "\\/[].+?()*^$";
+char	deleted[] = "(deleted)-";
+char	deletedrx[] = "\\(deleted\\)-";
+char	deletedrx01[] = "(\\(deleted\\)-)?";
+char	deletedaddr[] = "-#0;/^\\(deleted\\)-/";
+
+struct{
+	char	*type;
+	char	*port;
+	char *suffix;
+} ports[] = {
+	"text/",				"edit",	".txt", /* must be first for plumbport() */
+	/* text must be first for plumbport() */
+	"image/gif",			"image",	".gif",
+	"image/jpeg",			"image",	".jpg",
+	"image/jpeg",			"image",	".jpeg",
+	"image/png",			"image",	".png",
+	"image/tiff",			"image",	".tif",
+	"application/postscript",	"postscript",	".ps",
+	"application/pdf",	"postscript",	".pdf",
+	"application/msword",	"msword",	".doc",
+	"application/rtf",	"msword",	".rtf",
+	"audio/x-wav",			"wav",		".wav",
+	nil,	nil
+};
+
+char *goodtypes[] = {
+	"text",
+	"text/plain",
+	"message/rfc822",
+	"text/richtext",
+	"text/tab-separated-values",
+	"application/octet-stream",
+	nil,
+};
+
+struct{
+	char *type;
+	char	*ext;
+} exts[] = {
+	"image/gif",	".gif",
+	"image/jpeg",	".jpg",
+	nil, nil
+};
+
+char *okheaders[] =
+{
+	"From:",
+	"Date:",
+	"To:",
+	"CC:",
+	"Subject:",
+	nil
+};
+
+char *extraheaders[] =
+{
+	"Resent-From:",
+	"Resent-To:",
+	"Sort:",
+	nil,
+};
+
+char*
+line(char *data, char **pp)
+{
+	char *p, *q;
+
+	for(p=data; *p!='\0' && *p!='\n'; p++)
+		;
+	if(*p == '\n')
+		*pp = p+1;
+	else
+		*pp = p;
+	q = emalloc(p-data + 1);
+	memmove(q, data, p-data);
+	return q;
+}
+
+char*
+fc(Message *m, char *s)
+{
+	char *r;
+
+	if(*s && *m->from){
+		r = smprint("%s <%s>", s, m->from);
+		free(s);
+		return r;
+	}else if(*s)
+		return s;
+	else if(*m->from)
+		return estrdup(m->from);
+	return estrdup("??");
+}
+
+int
+loadinfo(Message *m, char *dir)
+{
+	int n;
+	char *data, *p;
+
+	data = readfile(dir, "info", &n);
+	if(data == nil)
+		return 0;
+	m->from = line(data, &p);
+	m->to = line(p, &p);
+	m->cc = line(p, &p);
+	m->replyto = line(p, &p);
+	m->date = line(p, &p);
+	m->subject = line(p, &p);
+	m->type = line(p, &p);
+	m->disposition = line(p, &p);
+	m->filename = line(p, &p);
+	m->digest = line(p, &p);
+	/* m->bcc = */ free(line(p, &p));
+	/* m->inreplyto = */ free(line(p, &p));
+	/* m->date = */ free(line(p, &p));
+	/* m->sender = */ free(line(p, &p));
+	/* m->messageid = */ free(line(p, &p));
+	/* m->lines = */ free(line(p, &p));
+	/* m->size = */ free(line(p, &p));
+	/* m->flags = */ free(line(p, &p));
+	/* m->fileid = */ free(line(p, &p));
+	m->fromcolon = fc(m, line(p, &p));
+
+	free(data);
+	return 1;
+}
+
+int
+isnumeric(char *s)
+{
+	while(*s){
+		if(!isdigit(*s))
+			return 0;
+		s++;
+	}
+	return 1;
+}
+
+Dir*
+loaddir(char *name, int *np)
+{
+	int fd;
+	Dir *dp;
+
+	fd = open(name, OREAD);
+	if(fd < 0)
+		return nil;
+	*np = dirreadall(fd, &dp);
+	close(fd);
+	return dp;
+}
+
+void
+readmbox(Message *mbox, char *dir, char *subdir)
+{
+	char *name;
+	Dir *d, *dirp;
+	int i, n;
+
+	name = estrstrdup(dir, subdir);
+	dirp = loaddir(name, &n);
+	mbox->recursed = 1;
+	if(dirp)
+		for(i=0; i<n; i++){
+			d = &dirp[i];
+			if(isnumeric(d->name))
+				mesgadd(mbox, name, d, nil);
+		}
+	free(dirp);
+	free(name);
+}
+
+/* add message to box, in increasing numerical order */
+int
+mesgadd(Message *mbox, char *dir, Dir *d, char *digest)
+{
+	Message *m;
+	char *name;
+	int loaded;
+
+	m = emalloc(sizeof(Message));
+	m->name = estrstrdup(d->name, "/");
+	m->next = nil;
+	m->prev = mbox->tail;
+	m->level= mbox->level+1;
+	m->recursed = 0;
+	name = estrstrdup(dir, m->name);
+	loaded = loadinfo(m, name);
+	free(name);
+	/* if two upas/fs are running, we can get misled, so check digest before accepting message */
+	if(loaded==0 || (digest!=nil && m->digest!=nil && strcmp(digest, m->digest)!=0)){
+		mesgfreeparts(m);
+		free(m);
+		return 0;
+	}
+	if(mbox->tail != nil)
+		mbox->tail->next = m;
+	mbox->tail = m;
+	if(mbox->head == nil)
+		mbox->head = m;
+
+	if (m->level != 1){
+		m->recursed = 1;
+		readmbox(m, dir, m->name); 
+	}
+	return 1;
+}
+
+int
+thisyear(char *year)
+{
+	static char now[10];
+	char *s;
+
+	if(now[0] == '\0'){
+		s = ctime(time(nil));
+		strcpy(now, s+24);
+	}
+	return strncmp(year, now, 4) == 0;
+}
+
+char*
+stripdate(char *as)
+{
+	int n;
+	char *s, *fld[10];
+
+	as = estrdup(as);
+	s = estrdup(as);
+	n = tokenize(s, fld, 10);
+	if(n > 5){
+		sprint(as, "%.3s ", fld[0]);	/* day */
+		/* some dates have 19 Apr, some Apr 19 */
+		if(strlen(fld[1])<4 && isnumeric(fld[1]))
+			sprint(as+strlen(as), "%.3s %.3s ", fld[1], fld[2]);	/* date, month */
+		else
+			sprint(as+strlen(as), "%.3s %.3s ", fld[2], fld[1]);	/* date, month */
+		/* do we use time or year?  depends on whether year matches this one */
+		if(thisyear(fld[5])){
+			if(strchr(fld[3], ':') != nil)
+				sprint(as+strlen(as), "%.5s ", fld[3]);	/* time */
+			else if(strchr(fld[4], ':') != nil)
+				sprint(as+strlen(as), "%.5s ", fld[4]);	/* time */
+		}else
+			sprint(as+strlen(as), "%.4s ", fld[5]);	/* year */
+	}
+	free(s);
+	return as;
+}
+
+char*
+readfile(char *dir, char *name, int *np)
+{
+	char *file, *data;
+	int fd, len;
+	Dir *d;
+
+	if(np != nil)
+		*np = 0;
+	file = estrstrdup(dir, name);
+	fd = open(file, OREAD);
+	if(fd < 0)
+		return nil;
+	d = dirfstat(fd);
+	free(file);
+	len = 0;
+	if(d != nil)
+		len = d->length;
+	free(d);
+	data = emalloc(len+1);
+	read(fd, data, len);
+	close(fd);
+	if(np != nil)
+		*np = len;
+	return data;
+}
+
+int
+writefile(char *dir, char *name, char *s)
+{
+	char *e, *file;
+	int fd, n;
+
+	file = estrstrdup(dir, name);
+//	fprint(2, "writefile %s [%s]\n", file, s);
+	fd = open(file, OWRITE);
+	if(fd < 0)
+		return -1;
+	for(e = s + strlen(s); e - s > 0; s += n)
+		if((n = write(fd, s, e - s)) <= 0)
+			break;
+	close(fd);
+	return s == e? 0: -1;
+}
+
+void
+setflags(Message *m, char *f)
+{
+	char *t;
+
+	t = smprint("%s/%s", mbox.name, m->name);
+	writefile(t, "flags", f);
+	free(t);
+}
+
+char*
+info(Message *m, int ind, int ogf)
+{
+	char *i;
+	int j, len, lens;
+	char *p;
+	char fmt[80], s[80];
+
+	if (ogf)
+		p=m->to;
+	else
+		p=m->fromcolon;
+
+	if(ind==0 && altmenu){
+		len = 12;
+		lens = 20;
+
+		if(ind==0 && m->subject[0]=='\0'){
+			snprint(fmt, sizeof fmt,
+				"\t%%-%d.%ds\t%%-12.12s\t", len, len);
+			snprint(s, sizeof s, fmt, p, stripdate(m->date) + 4);
+		}else{
+			snprint(fmt, sizeof fmt,
+				"\t%%-%d.%ds\t%%-12.12s\t%%-%d.%ds", len, len, lens, lens);
+			snprint(s, sizeof s, fmt, p, stripdate(m->date) + 4, m->subject);
+		}
+		i = estrdup(s);
+
+		return i;
+	}
+
+	if(ind==0 && shortmenu){
+		len = 30;
+		lens = 30;
+		if(shortmenu > 1){
+			len = 10;
+			lens = 25;
+		}
+		if(ind==0 && m->subject[0]=='\0'){
+			snprint(fmt, sizeof fmt, " %%-%d.%ds", len, len);
+			snprint(s, sizeof s, fmt, p);
+		}else{
+			snprint(fmt, sizeof fmt, " %%-%d.%ds  %%-%d.%ds", len, len, lens, lens);
+			snprint(s, sizeof s, fmt, p, m->subject);
+		}
+		i = estrdup(s);
+
+		return i;
+	} 
+
+	i = estrdup("");
+	i = eappend(i, "\t", p);
+	i = egrow(i, "\t", stripdate(m->date));
+	if(ind == 0){
+		if(strcmp(m->type, "text")!=0 && strncmp(m->type, "text/", 5)!=0 && 
+		   strncmp(m->type, "multipart/", 10)!=0)
+			i = egrow(i, "\t(", estrstrdup(m->type, ")"));
+	}else if(strncmp(m->type, "multipart/", 10) != 0)
+		i = egrow(i, "\t(", estrstrdup(m->type, ")"));
+	if(m->subject[0] != '\0'){
+		i = eappend(i, "\n", nil);
+		for(j=0; j<ind; j++)
+			i = eappend(i, "\t", nil);
+		i = eappend(i, "\t", m->subject);
+	}
+	return i;
+}
+
+void
+mesgmenu0(Window *w, Message *mbox, char *realdir, char *dir, int ind, Biobuf *fd, int onlyone, int dotail)
+{
+	int i;
+	Message *m;
+	char *name, *tmp;
+	int ogf=0;
+
+	if(strstr(realdir, "outgoing") != nil)
+		ogf=1;
+
+	/* show mail box in reverse order, pieces in forward order */
+	if(ind > 0)
+		m = mbox->head;
+	else
+		m = mbox->tail;
+	while(m != nil){
+		for(i=0; i<ind; i++)
+			Bprint(fd, "\t");
+		if(ind != 0)
+			Bprint(fd, "  ");
+		name = estrstrdup(dir, m->name);
+		tmp = info(m, ind, ogf);
+		Bprint(fd, "%s%s\n", name, tmp);
+		free(tmp);
+		if(dotail && m->tail)
+			mesgmenu0(w, m, realdir, name, ind+1, fd, 0, dotail);
+		free(name);
+		if(ind)
+			m = m->next;
+		else
+			m = m->prev;
+		if(onlyone)
+			m = nil;
+	}
+}
+
+void
+mesgmenu(Window *w, Message *mbox)
+{
+	winopenbody(w, OWRITE);
+	mesgmenu0(w, mbox, mbox->name, "", 0, w->body, 0, !shortmenu);
+	winclosebody(w);
+}
+
+/* one new message has arrived, as mbox->tail */
+void
+mesgmenunew(Window *w, Message *mbox)
+{
+	Biobuf *b;
+
+	winselect(w, "0", 0);
+	w->data = winopenfile(w, "data");
+	b = emalloc(sizeof(Biobuf));
+	Binit(b, w->data, OWRITE);
+	mesgmenu0(w, mbox, mbox->name, "", 0, b, 1, !shortmenu);
+	Bterm(b);
+	free(b);
+	if(!mbox->dirty)
+		winclean(w);
+	/* select tag line plus following indented lines, but not final newline (it's distinctive) */
+	winselect(w, "0/.*\\n((\t.*\\n)*\t.*)?/", 1);
+	close(w->addr);
+	close(w->data);
+	w->addr = -1;
+	w->data = -1;
+}
+
+char*
+name2regexp(char *prefix, char *s)
+{
+	char *buf, *p, *q;
+
+	buf = emalloc(strlen(prefix)+2*strlen(s)+50);	/* leave room to append more */
+	p = buf;
+	*p++ = '0';
+	*p++ = '/';
+	*p++ = '^';
+	strcpy(p, prefix);
+	p += strlen(prefix);
+	for(q=s; *q!='\0'; q++){
+		if(strchr(regexchars, *q) != nil)
+			*p++ = '\\';
+		*p++ = *q;
+	}
+	*p++ = '/';
+	*p = '\0';
+	return buf;
+}
+
+void
+mesgmenumarkdel(Window *w, Message *mbox, Message *m, int writeback)
+{
+	char *buf;
+
+
+	if(m->deleted)
+		return;
+	m->writebackdel = writeback;
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp("", m->name);
+	strcat(buf, "-#0");
+	if(winselect(w, buf, 1))
+		write(w->data, deleted, 10);
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	mbox->dirty = 1;
+	m->deleted = 1;
+}
+
+void
+mesgmenumarkundel(Window *w, Message*, Message *m)
+{
+	char *buf;
+
+	if(m->deleted == 0)
+		return;
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp(deletedrx, m->name);
+	if(winselect(w, buf, 1))
+		if(winsetaddr(w, deletedaddr, 1))
+			write(w->data, "", 0);
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	m->deleted = 0;
+}
+
+void
+mesgmenudel(Window *w, Message *mbox, Message *m)
+{
+	char *buf;
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp(deletedrx, m->name);
+	if(winsetaddr(w, buf, 1) && winsetaddr(w, ".,./.*\\n(\t.*\\n)*/", 1))
+		write(w->data, "", 0);
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	mbox->dirty = 1;
+	m->deleted = 1;
+}
+
+void
+mesgmenumark(Window *w, char *which, char *mark)
+{
+	char *buf;
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp(deletedrx01, which);
+	if(winsetaddr(w, buf, 1) && winsetaddr(w, "+0-#1", 1))	/* go to end of line */
+		write(w->data, mark, strlen(mark));
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	if(!mbox.dirty)
+		winclean(w);
+}
+
+void
+mesgfreeparts(Message *m)
+{
+	free(m->name);
+	free(m->replyname);
+	free(m->fromcolon);
+	free(m->from);
+	free(m->to);
+	free(m->cc);
+	free(m->replyto);
+	free(m->date);
+	free(m->subject);
+	free(m->type);
+	free(m->disposition);
+	free(m->filename);
+	free(m->digest);
+}
+
+void
+mesgdel(Message *mbox, Message *m)
+{
+	Message *n, *next;
+
+	if(m->opened)
+		error("internal error: deleted message still open in mesgdel");
+	/* delete subparts */
+	for(n=m->head; n!=nil; n=next){
+		next = n->next;
+		mesgdel(m, n);
+	}
+	/* remove this message from list */
+	if(m->next)
+		m->next->prev = m->prev;
+	else
+		mbox->tail = m->prev;
+	if(m->prev)
+		m->prev->next = m->next;
+	else
+		mbox->head = m->next;
+
+	mesgfreeparts(m);
+}
+
+int
+deliver(char *folder, char *file)
+{
+	char *av[4];
+	int pid, wpid, nz;
+	Waitmsg *w;
+
+	pid = fork();
+	switch(pid){
+	case -1:
+		return -1;
+	case 0:
+		av[0] = "mbappend";
+		av[1] = folder;
+		av[2] = file;
+		av[3] = 0;
+		exec("/bin/upas/mbappend", av);
+		_exits("b0rked");
+		return -1;
+	default:
+		while(w = wait()){
+			nz = !w->msg || !w->msg[0];
+			if(!nz)
+				werrstr("%s", w->msg);
+			wpid = w->pid;
+			free(w);
+			if(wpid == pid)
+				return nz? 0: -1;
+		}
+		return -1;
+	}
+}
+
+int
+mesgsave(Message *m, char *s)
+{
+	char *t;
+	int ret;
+
+	t = smprint("%s/%s/rawunix", mbox.name, m->name);
+	if(s[0] != '/')
+		s = estrdup(s);
+	else
+		s = smprint("%s/%s", mailboxdir, s);
+	ret = 1;
+	if(deliver(s, t) == -1){
+		fprint(2, "Mail: save failed: can't write %s: %r\n", s);
+		ret = 0;
+	}
+	setflags(m, "S");
+	free(s);
+	free(t);
+	return ret;
+}
+
+int
+mesgcommand(Message *m, char *cmd)
+{
+	char *s;
+	char *args[10];
+	int ok, ret, nargs;
+
+	s = cmd;
+	ret = 1;
+	nargs = tokenize(s, args, nelem(args));
+	if(nargs == 0)
+		return 0;
+	if(strcmp(args[0], "Post") == 0){
+		mesgsend(m);
+		goto Return;
+	}
+	if(strncmp(args[0], "Save", 4) == 0){
+		if(m->isreply)
+			goto Return;
+		s = estrdup("\t[saved");
+		if(nargs==1 || strcmp(args[1], "")==0){
+			ok = mesgsave(m, "stored");
+		}else{
+			ok = mesgsave(m, args[1]);
+			s = eappend(s, " ", args[1]);
+		}
+		if(ok){
+			s = egrow(s, "]", nil);
+			mesgmenumark(mbox.w, m->name, s);
+		}
+		free(s);
+		setflags(m, "S");
+		goto Return;
+	}
+	if(strcmp(args[0], "Reply")==0){
+		if(nargs>=2 && strcmp(args[1], "all")==0)
+			mkreply(m, "Replyall", nil, nil, nil);
+		else
+			mkreply(m, "Reply", nil, nil, nil);
+//		setflags(m, "a");
+		goto Return;
+	}
+	if(strcmp(args[0], "Q") == 0){
+		s = winselection(m->w);	/* will be freed by mkreply */
+		if(nargs>=3 && strcmp(args[1], "Reply")==0 && strcmp(args[2], "all")==0)
+			mkreply(m, "QReplyall", nil, nil, s);
+		else
+			mkreply(m, "QReply", nil, nil, s);
+//		setflags(m, "a");
+		goto Return;
+	}
+	if(strcmp(args[0], "Del") == 0){
+		if(windel(m->w, 0)){
+			chanfree(m->w->cevent);
+			free(m->w);
+			m->w = nil;
+			if(m->isreply)
+				delreply(m);
+			else{
+				m->opened = 0;
+				m->tagposted = 0;
+			}
+			free(cmd);
+			threadexits(nil);
+		}
+		goto Return;
+	}
+	if(strcmp(args[0], "Delmesg") == 0){
+		if(!m->isreply){
+			mesgmenumarkdel(wbox, &mbox, m, 1);
+			free(cmd);	/* mesgcommand might not return */
+			mesgcommand(m, estrdup("Del"));
+			return 1;
+		}
+//		setflags(m, "d");
+		goto Return;
+	}
+	if(strcmp(args[0], "UnDelmesg") == 0){
+		if(!m->isreply && m->deleted)
+			mesgmenumarkundel(wbox, &mbox, m);
+//		setflags(m, "-d");
+		goto Return;
+	}
+//	if(strcmp(args[0], "Headers") == 0){
+//		m->showheaders();
+//		return True;
+//	}
+
+	ret = 0;
+
+    Return:
+	free(cmd);
+	return ret;
+}
+
+void
+mesgtagpost(Message *m)
+{
+	if(m->tagposted)
+		return;
+	wintagwrite(m->w, " Post", 5);
+	m->tagposted = 1;
+}
+
+/* need to expand selection more than default word */
+#pragma varargck argpos eval 2
+
+long
+eval(Window *w, char *s, ...)
+{
+	char buf[64];
+	va_list arg;
+
+	va_start(arg, s);
+	vsnprint(buf, sizeof buf, s, arg);
+	va_end(arg);
+
+	if(winsetaddr(w, buf, 1)==0)
+		return -1;
+
+	if(pread(w->addr, buf, 24, 0) != 24)
+		return -1;
+	return strtol(buf, 0, 10);
+}
+
+int
+isemail(char *s)
+{
+	int nat;
+
+	nat = 0;
+	for(; *s; s++)
+		if(*s == '@')
+			nat++;
+		else if(!isalpha(*s) && !isdigit(*s) && !strchr("_.-+/", *s))
+			return 0;
+	return nat==1;
+}
+
+char addrdelim[] =  "/[ \t\\n<>()\\[\\]]/";
+char*
+expandaddr(Window *w, Event *e)
+{
+	char *s;
+	long q0, q1;
+
+	if(e->q0 != e->q1)	/* cannot happen */
+		return nil;
+
+	q0 = eval(w, "#%d-%s", e->q0, addrdelim);
+	if(q0 == -1)	/* bad char not found */
+		q0 = 0;
+	else			/* increment past bad char */
+		q0++;
+
+	q1 = eval(w, "#%d+%s", e->q0, addrdelim);
+	if(q1 < 0){
+		q1 = eval(w, "$");
+		if(q1 < 0)
+			return nil;
+	}
+	if(q0 >= q1)
+		return nil;
+	s = emalloc((q1-q0)*UTFmax+1);
+	winread(w, q0, q1, s);
+	return s;
+}
+
+int
+replytoaddr(Window *w, Message *m, Event *e, char *s)
+{
+	int did;
+	char *buf;
+	Plumbmsg *pm;
+
+	buf = nil;
+	did = 0;
+	if(e->flag & 2){
+		/* autoexpanded; use our own bigger expansion */
+		buf = expandaddr(w, e);
+		if(buf == nil)
+			return 0;
+		s = buf;
+	}
+	if(isemail(s)){
+		did = 1;
+		pm = emalloc(sizeof(Plumbmsg));
+		pm->src = estrdup("Mail");
+		pm->dst = estrdup("sendmail");
+		pm->data = estrdup(s);
+		pm->ndata = -1;
+		if(m->subject && m->subject[0]){
+			pm->attr = emalloc(sizeof(Plumbattr));
+			pm->attr->name = estrdup("Subject");
+			if(tolower(m->subject[0]) != 'r' || tolower(m->subject[1]) != 'e' || m->subject[2] != ':')
+				pm->attr->value = estrstrdup("Re: ", m->subject);
+			else
+				pm->attr->value = estrdup(m->subject);
+			pm->attr->next = nil;
+		}
+		if(plumbsend(plumbsendfd, pm) < 0)
+			fprint(2, "error writing plumb message: %r\n");
+		plumbfree(pm);
+	}
+	free(buf);
+	return did;
+}
+
+
+void
+mesgctl(void *v)
+{
+	Message *m;
+	Window *w;
+	Event *e, *eq, *e2, *ea;
+	int na, nopen, i, j;
+	char *os, *s, *t, *buf;
+
+	m = v;
+	w = m->w;
+	threadsetname("mesgctl");
+	proccreate(wineventproc, w, STACK);
+	for(;;){
+		e = recvp(w->cevent);
+		switch(e->c1){
+		default:
+		Unk:
+			print("unknown message %c%c\n", e->c1, e->c2);
+			break;
+
+		case 'E':	/* write to body; can't affect us */
+			break;
+
+		case 'F':	/* generated by our actions; ignore */
+			break;
+
+		case 'K':	/* type away; we don't care */
+		case 'M':
+			switch(e->c2){
+			case 'x':	/* mouse only */
+			case 'X':
+				ea = nil;
+				eq = e;
+				if(e->flag & 2){
+					e2 = recvp(w->cevent);
+					eq = e2;
+				}
+				if(e->flag & 8){
+					ea = recvp(w->cevent);
+					recvp(w->cevent);
+					na = ea->nb;
+				}else
+					na = 0;
+				if(eq->q1>eq->q0 && eq->nb==0){
+					s = emalloc((eq->q1-eq->q0)*UTFmax+1);
+					winread(w, eq->q0, eq->q1, s);
+				}else
+					s = estrdup(eq->b);
+				if(na){
+					t = emalloc(strlen(s)+1+na+1);
+					sprint(t, "%s %s", s, ea->b);
+					free(s);
+					s = t;
+				}
+				if(!mesgcommand(m, s))	/* send it back */
+					winwriteevent(w, e);
+				break;
+
+			case 'l':	/* mouse only */
+			case 'L':
+				buf = nil;
+				eq = e;
+				if(e->flag & 2){
+					e2 = recvp(w->cevent);
+					eq = e2;
+				}
+				s = eq->b;
+				if(eq->q1>eq->q0 && eq->nb==0){
+					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
+					winread(w, eq->q0, eq->q1, buf);
+					s = buf;
+				}
+				os = s;
+				nopen = 0;
+				do{
+					/* skip mail box name if present */
+					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
+						s += strlen(mbox.name);
+					if(strstr(s, "body") != nil){
+						/* strip any known extensions */
+						for(i=0; exts[i].ext!=nil; i++){
+							j = strlen(exts[i].ext);
+							if(strlen(s)>j && strcmp(s+strlen(s)-j, exts[i].ext)==0){
+								s[strlen(s)-j] = '\0';
+								break;
+							}
+						}
+						if(strlen(s)>5 && strcmp(s+strlen(s)-5, "/body")==0)
+							s[strlen(s)-4] = '\0';	/* leave / in place */
+					}
+					nopen += mesgopen(&mbox, mbox.name, s, m, 0, nil);
+					while(*s!=0 && *s++!='\n')
+						;
+				}while(*s);
+				if(nopen == 0 && e->c1 == 'L')
+					nopen += replytoaddr(w, m, e, os);
+				if(nopen == 0)
+					winwriteevent(w, e);
+				free(buf);
+				break;
+
+			case 'I':	/* modify away; we don't care */
+			case 'D':
+				mesgtagpost(m);
+				/* fall through */
+			case 'd':
+			case 'i':
+				break;
+
+			default:
+				goto Unk;
+			}
+		}
+	}
+}
+
+void
+mesgline(Message *m, char *header, char *value)
+{
+	if(strlen(value) > 0)
+		Bprint(m->w->body, "%s: %s\n", header, value);
+}
+
+int
+isprintable(char *type)
+{
+	int i;
+
+	for(i=0; goodtypes[i]!=nil; i++)
+		if(strcmp(type, goodtypes[i])==0)
+			return 1;
+	return 0;
+}
+
+char*
+ext(char *type)
+{
+	int i;
+
+	for(i=0; exts[i].type!=nil; i++)
+		if(strcmp(type, exts[i].type)==0)
+			return exts[i].ext;
+	return "";
+}
+
+void
+mimedisplay(Message *m, char *name, char *rootdir, Window *w, int fileonly)
+{
+	char *dest, *maildest;
+
+	if(strcmp(m->disposition, "file")==0 || strlen(m->filename)!=0){
+		if(strlen(m->filename) == 0){
+			dest = estrdup(m->name);
+			dest[strlen(dest)-1] = '\0';
+		}else
+			dest = estrdup(m->filename);
+		if(maildest = getenv("maildest")){
+			maildest = eappend(maildest, "/", dest);
+			Bprint(w->body, "\tcp %s%sbody%s %q\n", rootdir, name, ext(m->type), maildest);
+			free(maildest);
+		}
+		if(m->filename[0] != '/')
+			dest = egrow(estrdup(home), "/", dest);
+		Bprint(w->body, "\tcp %s%sbody%s %q\n", rootdir, name, ext(m->type), dest);
+		free(dest);
+	}else if(!fileonly)
+		Bprint(w->body, "\tfile is %s%sbody%s\n", rootdir, name, ext(m->type));
+}
+
+void
+printheader(char *dir, Biobuf *b, char **okheaders)
+{
+	char *s;
+	char *lines[100];
+	int i, j, n;
+
+	s = readfile(dir, "header", nil);
+	if(s == nil)
+		return;
+	n = getfields(s, lines, nelem(lines), 0, "\n");
+	for(i=0; i<n; i++)
+		for(j=0; okheaders[j]; j++)
+			if(cistrncmp(lines[i], okheaders[j], strlen(okheaders[j])) == 0)
+				Bprint(b, "%s\n", lines[i]);
+	free(s);
+}
+
+void
+mesgload(Message *m, char *rootdir, char *file, Window *w)
+{
+	char *s, *subdir, *name, *dir;
+	Message *mp, *thisone;
+	int n;
+
+	dir = estrstrdup(rootdir, file);
+
+	if(strcmp(m->type, "message/rfc822") != 0){	/* suppress headers of envelopes */
+		if(strlen(m->from) > 0){
+			Bprint(w->body, "From: %s\n", m->from);
+			mesgline(m, "Date", m->date);
+			mesgline(m, "To", m->to);
+			mesgline(m, "CC", m->cc);
+			mesgline(m, "Subject", m->subject);
+			printheader(dir, w->body, extraheaders);
+		}else{
+			printheader(dir, w->body, okheaders);
+			printheader(dir, w->body, extraheaders);
+		}
+		Bprint(w->body, "\n");
+	}
+
+	if(m->level == 1 && m->recursed == 0){
+		m->recursed = 1;
+		readmbox(m, rootdir, m->name);
+	}
+	if(m->head == nil){	/* single part message */
+		if(strcmp(m->type, "text")==0 || strncmp(m->type, "text/", 5)==0){
+			mimedisplay(m, m->name, rootdir, w, 1);
+			s = readbody(m->type, dir, &n);
+			winwritebody(w, s, n);
+			free(s);
+		}else
+			mimedisplay(m, m->name, rootdir, w, 0);
+	}else{
+		/* multi-part message, either multipart/* or message/rfc822 */
+		thisone = nil;
+		if(strcmp(m->type, "multipart/alternative") == 0){
+			thisone = m->head;	/* in case we can't find a good one */
+			for(mp=m->head; mp!=nil; mp=mp->next)
+				if(isprintable(mp->type)){
+					thisone = mp;
+					break;
+				}
+		}
+		for(mp=m->head; mp!=nil; mp=mp->next){
+			if(thisone!=nil && mp!=thisone)
+				continue;
+			subdir = estrstrdup(dir, mp->name);
+			name = estrstrdup(file, mp->name);
+			/* skip first element in name because it's already in window name */
+			if(mp != m->head)
+				Bprint(w->body, "\n===> %s (%s) [%s]\n", strchr(name, '/')+1, mp->type, mp->disposition);
+			if(strcmp(mp->type, "text")==0 || strncmp(mp->type, "text/", 5)==0){
+				mimedisplay(mp, name, rootdir, w, 1);
+				printheader(subdir, w->body, okheaders);
+				printheader(subdir, w->body, extraheaders);
+				winwritebody(w, "\n", 1);
+				s = readbody(mp->type, subdir, &n);
+				winwritebody(w, s, n);
+				free(s);
+			}else{
+				if(strncmp(mp->type, "multipart/", 10)==0 || strcmp(mp->type, "message/rfc822")==0){
+					mp->w = w;
+					mesgload(mp, rootdir, name, w);
+					mp->w = nil;
+				}else
+					mimedisplay(mp, name, rootdir, w, 0);
+			}
+			free(name);
+			free(subdir);
+		}
+	}
+	free(dir);
+}
+
+int
+tokenizec(char *str, char **args, int max, char *splitc)
+{
+	int na;
+	int intok = 0;
+
+	if(max <= 0)
+		return 0;	
+	for(na=0; *str != '\0';str++){
+		if(strchr(splitc, *str) == nil){
+			if(intok)
+				continue;
+			args[na++] = str;
+			intok = 1;
+		}else{
+			/* it's a separator/skip character */
+			*str = '\0';
+			if(intok){
+				intok = 0;
+				if(na >= max)
+					break;
+			}
+		}
+	}
+	return na;
+}
+
+Message*
+mesglookup(Message *mbox, char *name, char *digest)
+{
+	int n;
+	Message *m;
+	char *t;
+
+	if(digest){
+		/* can find exactly */
+		for(m=mbox->head; m!=nil; m=m->next)
+			if(strcmp(digest, m->digest) == 0)
+				break;
+		return m;
+	}
+
+	n = strlen(name);
+	if(n == 0)
+		return nil;
+	if(name[n-1] == '/')
+		t = estrdup(name);
+	else
+		t = estrstrdup(name, "/");
+	for(m=mbox->head; m!=nil; m=m->next)
+		if(strcmp(t, m->name) == 0)
+			break;
+	free(t);
+	return m;
+}
+
+/*
+ * Find plumb port, knowing type is text, given file name (by extension)
+ */
+int
+plumbportbysuffix(char *file)
+{
+	char *suf;
+	int i, nsuf, nfile;
+
+	nfile = strlen(file);
+	for(i=0; ports[i].type!=nil; i++){
+		suf = ports[i].suffix;
+		nsuf = strlen(suf);
+		if(nfile > nsuf)
+			if(cistrncmp(file+nfile-nsuf, suf, nsuf) == 0)
+				return i;
+	}
+	return 0;
+}
+
+/*
+ * Find plumb port using type and file name (by extension)
+ */
+int
+plumbport(char *type, char *file)
+{
+	int i;
+
+	for(i=0; ports[i].type!=nil; i++)
+		if(strncmp(type, ports[i].type, strlen(ports[i].type)) == 0)
+			return i;
+	/* see if it's a text type */
+	for(i=0; goodtypes[i]!=nil; i++)
+		if(strncmp(type, goodtypes[i], strlen(goodtypes[i])) == 0)
+			return plumbportbysuffix(file);
+	return -1;
+}
+
+void
+plumb(Message *m, char *dir)
+{
+	int i;
+	char *port;
+	Plumbmsg *pm;
+
+	if(strlen(m->type) == 0)
+		return;
+	i = plumbport(m->type, m->filename);
+	if(i < 0)
+		fprint(2, "can't find destination for message subpart\n");
+	else{
+		port = ports[i].port;
+		pm = emalloc(sizeof(Plumbmsg));
+		pm->src = estrdup("Mail");
+		if(port)
+			pm->dst = estrdup(port);
+		else
+			pm->dst = nil;
+		pm->wdir = nil;
+		pm->type = estrdup("text");
+		pm->ndata = -1;
+		pm->data = estrstrdup(dir, "body");
+		pm->data = eappend(pm->data, "", ports[i].suffix);
+		if(plumbsend(plumbsendfd, pm) < 0)
+			fprint(2, "error writing plumb message: %r\n");
+		plumbfree(pm);
+	}
+}
+
+int
+mesgopen(Message *mbox, char *dir, char *s, Message *mesg, int plumbed, char *digest)
+{
+	char *t, *u, *v;
+	Message *m;
+	char *direlem[10];
+	int i, ndirelem, reuse;
+
+	/* find white-space-delimited first word */
+	for(t=s; *t!='\0' && !isspace(*t); t++)
+		;
+	u = emalloc(t-s+1);
+	memmove(u, s, t-s);
+	/* separate it on slashes */
+	ndirelem = tokenizec(u, direlem, nelem(direlem), "/");
+	if(ndirelem <= 0){
+    Error:
+		free(u);
+		return 0;
+	}
+	if(plumbed){
+		write(wctlfd, "top", 3);
+		write(wctlfd, "current", 7);
+	}
+	/* open window for message */
+	m = mesglookup(mbox, direlem[0], digest);
+	if(m == nil)
+		goto Error;
+	if(mesg!=nil && m!=mesg)	/* string looked like subpart but isn't part of this message */
+		goto Error;
+	if(m->opened == 0){
+		if(m->w == nil){
+			reuse = 0;
+			m->w = newwindow();
+		}else{
+			reuse = 1;
+			/* re-use existing window */
+			if(winsetaddr(m->w, "0,$", 1)){
+				if(m->w->data < 0)
+					m->w->data = winopenfile(m->w, "data");
+				write(m->w->data, "", 0);
+			}
+		}
+		v = estrstrdup(mbox->name, m->name);
+		winname(m->w, v);
+		free(v);
+		if(!reuse){
+			if(m->deleted)
+				wintagwrite(m->w, "Q Reply all UnDelmesg Save ", 2+6+4+10+5);
+			else
+				wintagwrite(m->w, "Q Reply all Delmesg Save ", 2+6+4+8+5);
+		}
+		threadcreate(mesgctl, m, STACK);
+		winopenbody(m->w, OWRITE);
+		mesgload(m, dir, m->name, m->w);
+		winclosebody(m->w);
+		winclean(m->w);
+		m->opened = 1;
+		setflags(m, "s");
+		if(ndirelem == 1){
+			free(u);
+			return 1;
+		}
+	}
+	if(ndirelem == 1 && plumbport(m->type, m->filename) <= 0){
+		/* make sure dot is visible */
+		ctlprint(m->w->ctl, "show\n");
+		return 0;
+	}
+	/* walk to subpart */
+	dir = estrstrdup(dir, m->name);
+	for(i=1; i<ndirelem; i++){
+		m = mesglookup(m, direlem[i], digest);
+		if(m == nil)
+			break;
+		dir = egrow(dir, m->name, nil);
+	}
+	if(m != nil && plumbport(m->type, m->filename) > 0)
+		plumb(m, dir);
+	free(dir);
+	free(u);
+	return 1;
+}
+
+void
+rewritembox(Window *w, Message *mbox)
+{
+	Message *m, *next;
+	char *deletestr, *t;
+	int nopen;
+
+	deletestr = estrstrdup("delete ", fsname);
+
+	nopen = 0;
+	for(m=mbox->head; m!=nil; m=next){
+		next = m->next;
+		if(m->deleted == 0)
+			continue;
+		if(m->opened){
+			nopen++;
+			continue;
+		}
+		if(m->writebackdel){
+			/* messages deleted by plumb message are not removed again */
+			t = estrdup(m->name);
+			if(strlen(t) > 0)
+				t[strlen(t)-1] = '\0';
+			deletestr = egrow(deletestr, " ", t);
+		}
+		mesgmenudel(w, mbox, m);
+		mesgdel(mbox, m);
+	}
+	if(write(mbox->ctlfd, deletestr, strlen(deletestr)) < 0)
+		fprint(2, "Mail: warning: error removing mail message files: %r\n");
+	free(deletestr);
+	winselect(w, "0", 0);
+	if(nopen == 0)
+		winclean(w);
+	mbox->dirty = 0;
+}
+
+/* name is a full file name, but it might not belong to us */
+Message*
+mesglookupfile(Message *mbox, char *name, char *digest)
+{
+	int k, n;
+
+	k = strlen(name);
+	n = strlen(mbox->name);
+	if(k==0 || strncmp(name, mbox->name, n) != 0){
+//		fprint(2, "Mail: message %s not in this mailbox\n", name);
+		return nil;
+	}
+	return mesglookup(mbox, name+n, digest);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/mkfile
@@ -1,0 +1,32 @@
+</$objtype/mkfile
+<../mkupas
+
+TARG=Mail
+OFILES=\
+		html.$O\
+		mail.$O\
+		mesg.$O\
+		reply.$O\
+		util.$O\
+		win.$O
+
+HFILES=dat.h
+LIB=
+
+# BIN=/acme/bin/$objtype
+BIN=$ABIN
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
+
+$O.out: $OFILES
+	$LD -o $target  $LDFLAGS $OFILES
+
+syms:V:
+	$CC -a mail.c	>syms
+	$CC -aa mesg.c reply.c util.c win.c 	>>syms
+
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/reply.c
@@ -1,0 +1,568 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <ctype.h>
+#include <plumb.h>
+#include "dat.h"
+
+static int	replyid;
+
+int
+quote(Message *m, Biobuf *b, char *dir, char *quotetext)
+{
+	char *body, *type;
+	int i, n, nlines;
+	char **lines;
+
+	if(quotetext){
+		body = quotetext;
+		n = strlen(body);
+		type = nil;
+	}else{
+		/* look for first textual component to quote */
+		type = readfile(dir, "type", &n);
+		if(type == nil){
+			print("no type in %s\n", dir);
+			return 0;
+		}
+		if(strncmp(type, "multipart/", 10)==0 || strncmp(type, "message/", 8)==0){
+			dir = estrstrdup(dir, "1/");
+			if(quote(m, b, dir, nil)){
+				free(type);
+				free(dir);
+				return 1;
+			}
+			free(dir);
+		}
+		if(strncmp(type, "text", 4) != 0){
+			free(type);
+			return 0;
+		}
+		body = readbody(m->type, dir, &n);
+		if(body == nil)
+			return 0;
+	}
+	nlines = 0;
+	for(i=0; i<n; i++)
+		if(body[i] == '\n')
+			nlines++;
+	nlines++;
+	lines = emalloc(nlines*sizeof(char*));
+	nlines = getfields(body, lines, nlines, 0, "\n");
+	/* delete leading and trailing blank lines */
+	i = 0;
+	while(i<nlines && lines[i][0]=='\0')
+		i++;
+	while(i<nlines && lines[nlines-1][0]=='\0')
+		nlines--;
+	while(i < nlines){
+		Bprint(b, ">%s%s\n", lines[i][0]=='>'? "" : " ", lines[i]);
+		i++;
+	}
+	free(lines);
+	free(body);	/* will free quotetext if non-nil */
+	free(type);
+	return 1;
+}
+
+void
+mkreply(Message *m, char *label, char *to, Plumbattr *attr, char *quotetext)
+{
+	Message *r;
+	char *dir, *t;
+	int quotereply;
+	Plumbattr *a;
+
+	quotereply = (label[0] == 'Q');
+	r = emalloc(sizeof(Message));
+	r->isreply = 1;
+	if(m != nil)
+		r->replyname = estrdup(m->name);
+	r->next = replies.head;
+	r->prev = nil;
+	if(replies.head != nil)
+		replies.head->prev = r;
+	replies.head = r;
+	if(replies.tail == nil)
+		replies.tail = r;
+	r->name = emalloc(strlen(mbox.name)+strlen(label)+10);
+	sprint(r->name, "%s%s%d", mbox.name, label, ++replyid);
+	r->w = newwindow();
+	winname(r->w, r->name);
+	ctlprint(r->w->ctl, "cleartag");
+	wintagwrite(r->w, "fmt Look Post Undo", 4+5+5+4);
+	r->tagposted = 1;
+	threadcreate(mesgctl, r, STACK);
+	winopenbody(r->w, OWRITE);
+	if(to!=nil && to[0]!='\0')
+		Bprint(r->w->body, "%s\n", to);
+	for(a=attr; a; a=a->next)
+		Bprint(r->w->body, "%s: %s\n", a->name, a->value);
+	dir = nil;
+	if(m != nil){
+		dir = estrstrdup(mbox.name, m->name);
+		if(to == nil && attr == nil){
+			/* Reply goes to replyto; Reply all goes to From and To and CC */
+			if(strstr(label, "all") == nil)
+				Bprint(r->w->body, "To: %s\n", m->replyto);
+			else{	/* Replyall */
+				if(strlen(m->from) > 0)
+					Bprint(r->w->body, "To: %s\n", m->from);
+				if(strlen(m->to) > 0)
+					Bprint(r->w->body, "To: %s\n", m->to);
+				if(strlen(m->cc) > 0)
+					Bprint(r->w->body, "CC: %s\n", m->cc);
+			}
+		}
+		if(strlen(m->subject) > 0){
+			t = "Subject: Re: ";
+			if(strlen(m->subject) >= 3)
+				if(tolower(m->subject[0])=='r' && tolower(m->subject[1])=='e' && m->subject[2]==':')
+					t = "Subject: ";
+			Bprint(r->w->body, "%s%s\n", t, m->subject);
+		}
+		if(!quotereply){
+			Bprint(r->w->body, "Include: %sraw\n", dir);
+			free(dir);
+		}
+	}
+	Bprint(r->w->body, "\n");
+	if(m == nil)
+		Bprint(r->w->body, "\n");
+	else if(quotereply){
+		quote(m, r->w->body, dir, quotetext);
+		free(dir);
+	}
+	winclosebody(r->w);
+	if(m==nil && (to==nil || to[0]=='\0'))
+		winselect(r->w, "0", 0);
+	else
+		winselect(r->w, "$", 0);
+	winclean(r->w);
+	windormant(r->w);
+}
+
+void
+delreply(Message *m)
+{
+	if(m->next == nil)
+		replies.tail = m->prev;
+	else
+		m->next->prev = m->prev;
+	if(m->prev == nil)
+		replies.head = m->next;
+	else
+		m->prev->next = m->next;
+	mesgfreeparts(m);
+	free(m);
+}
+
+/* copy argv to stack and free the incoming strings, so we don't leak argument vectors */
+void
+buildargv(char **inargv, char *argv[NARGS+1], char args[NARGCHAR])
+{
+	int i, n;
+	char *s, *a;
+
+	s = args;
+	for(i=0; i<NARGS; i++){
+		a = inargv[i];
+		if(a == nil)
+			break;
+		n = strlen(a)+1;
+		if((s-args)+n >= NARGCHAR)	/* too many characters */
+			break;
+		argv[i] = s;
+		memmove(s, a, n);
+		s += n;
+		free(a);
+	}
+	argv[i] = nil;
+}
+
+void
+execproc(void *v)
+{
+	struct Exec *e;
+	int p[2], q[2];
+	char *prog;
+	char *argv[NARGS+1], args[NARGCHAR];
+
+	e = v;
+	p[0] = e->p[0];
+	p[1] = e->p[1];
+	q[0] = e->q[0];
+	q[1] = e->q[1];
+	prog = e->prog;	/* known not to be malloc'ed */
+	rfork(RFFDG);
+	sendul(e->sync, 1);
+	buildargv(e->argv, argv, args);
+	free(e->argv);
+	chanfree(e->sync);
+	free(e);
+	dup(p[0], 0);
+	close(p[0]);
+	close(p[1]);
+	if(q[0]){
+		dup(q[1], 1);
+		close(q[0]);
+		close(q[1]);
+	}
+	procexec(nil, prog, argv);
+//fprint(2, "exec: %s", e->prog);
+//{int i;
+//for(i=0; argv[i]; i++) print(" '%s'", argv[i]);
+//print("\n");
+//}
+//argv[0] = "cat";
+//argv[1] = nil;
+//procexec(nil, "/bin/cat", argv);
+	fprint(2, "Mail: can't exec %s: %r\n", prog);
+	threadexits("can't exec");
+}
+
+enum{
+	ATTACH,
+	BCC,
+	CC,
+	FROM,
+	INCLUDE,
+	TO,
+};
+
+char *headers[] = {
+	"attach:",
+	"bcc:",
+	"cc:",
+	"from:",
+	"include:",
+	"to:",
+	nil,
+};
+
+int
+whichheader(char *h)
+{
+	int i;
+
+	for(i=0; headers[i]!=nil; i++)
+		if(cistrcmp(h, headers[i]) == 0)
+			return i;
+	return -1;
+}
+
+char *tolist[200];
+char	*cclist[200];
+char	*bcclist[200];
+int ncc, nbcc, nto;
+char	*attlist[200];
+char	included[200];
+
+int
+addressed(char *name)
+{
+	int i;
+
+	for(i=0; i<nto; i++)
+		if(strcmp(name, tolist[i]) == 0)
+			return 1;
+	for(i=0; i<ncc; i++)
+		if(strcmp(name, cclist[i]) == 0)
+			return 1;
+	for(i=0; i<nbcc; i++)
+		if(strcmp(name, bcclist[i]) == 0)
+			return 1;
+	return 0;
+}
+
+char*
+skipbl(char *s, char *e)
+{
+	while(s < e){
+		if(*s!=' ' && *s!='\t' && *s!=',')
+			break;
+		s++;
+	}
+	return s;
+}
+
+char*
+findbl(char *s, char *e)
+{
+	while(s < e){
+		if(*s==' ' || *s=='\t' || *s==',')
+			break;
+		s++;
+	}
+	return s;
+}
+
+/*
+ * comma-separate possibly blank-separated strings in line; e points before newline
+ */
+void
+commas(char *s, char *e)
+{
+	char *t;
+
+	/* may have initial blanks */
+	s = skipbl(s, e);
+	while(s < e){
+		s = findbl(s, e);
+		if(s == e)
+			break;
+		t = skipbl(s, e);
+		if(t == e)	/* no more words */
+			break;
+		/* patch comma */
+		*s++ = ',';
+		while(s < t)
+			*s++ = ' ';
+	}
+}
+
+int
+print2(int fd, int ofd, char *fmt, ...)
+{
+	int m, n;
+	char *s;
+	va_list arg;
+
+	va_start(arg, fmt);
+	s = vsmprint(fmt, arg);
+	va_end(arg);
+	if(s == nil)
+		return -1;
+	m = strlen(s);
+	n = write(fd, s, m);
+	if(ofd > 0)
+		write(ofd, s, m);
+	return n;
+}
+
+int
+write2(int fd, int ofd, char *buf, int n, int nofrom)
+{
+	char *from, *p;
+	int m = 0;
+
+	if(fd >= 0)
+		m = write(fd, buf, n);
+
+	if(ofd <= 0)
+		return m;
+
+	if(nofrom == 0)
+		return write(ofd, buf, n);
+
+	/* need to escape leading From lines to avoid corrupting 'outgoing' mailbox */
+	for(p=buf; *p; p+=m){
+		from = cistrstr(p, "from");
+		if(from == nil)
+			m = n;
+		else
+			m = from - p;
+		if(m > 0)
+			write(ofd, p, m);
+		if(from){
+			/* escape with space if From is at start of line */
+			if(p==buf || from[-1]=='\n')
+				write(ofd, " ", 1);
+			write(ofd, from, 4);
+			m += 4;
+		}
+		n -= m;
+	}
+	return p - buf;
+}
+
+void
+mesgsend(Message *m)
+{
+	char *s, *body, *to;
+	int i, j, h, n, natt, p[2];
+	struct Exec *e;
+	Channel *sync;
+	int first, nfld, delit, ofd;
+	char *copy, *fld[100], *now;
+
+	body = winreadbody(m->w, &n);
+	/* assemble to: list from first line, to: line, and cc: line */
+	nto = 0;
+	natt = 0;
+	ncc = 0;
+	nbcc = 0;
+	first = 1;
+	to = body;
+	for(;;){
+		for(s=to; *s!='\n'; s++)
+			if(*s == '\0'){
+				free(body);
+				return;
+			}
+		if(s++ == to)	/* blank line */
+			break;
+		/* make copy of line to tokenize */
+		copy = emalloc(s-to);
+		memmove(copy, to, s-to);
+		copy[s-to-1] = '\0';
+		nfld = tokenizec(copy, fld, nelem(fld), ", \t");
+		if(nfld == 0){
+			free(copy);
+			break;
+		}
+		n -= s-to;
+		switch(h = whichheader(fld[0])){
+		case TO:
+		case FROM:
+			delit = 1;
+			commas(to+strlen(fld[0]), s-1);
+			for(i=1; i<nfld && nto<nelem(tolist); i++)
+				if(!addressed(fld[i]))
+					tolist[nto++] = estrdup(fld[i]);
+			break;
+		case BCC:
+			delit = 1;
+			commas(to+strlen(fld[0]), s-1);
+			for(i=1; i<nfld && nbcc<nelem(bcclist); i++)
+				if(!addressed(fld[i]))
+					bcclist[nbcc++] = estrdup(fld[i]);
+			break;
+		case CC:
+			delit = 1;
+			commas(to+strlen(fld[0]), s-1);
+			for(i=1; i<nfld && ncc<nelem(cclist); i++)
+				if(!addressed(fld[i]))
+					cclist[ncc++] = estrdup(fld[i]);
+			break;
+		case ATTACH:
+		case INCLUDE:
+			delit = 1;
+			for(i=1; i<nfld && natt<nelem(attlist); i++){
+				attlist[natt] = estrdup(fld[i]);
+				included[natt++] = (h == INCLUDE);
+			}
+			break;
+		default:
+			if(first){
+				delit = 1;
+				for(i=0; i<nfld && nto<nelem(tolist); i++)
+					tolist[nto++] = estrdup(fld[i]);
+			}else	/* ignore it */
+				delit = 0;
+			break;
+		}
+		if(delit){
+			/* delete line from body */
+			memmove(to, s, n+1);
+		}else
+			to = s;
+		free(copy);
+		first = 0;
+	}
+
+	ofd = open(outgoing, OWRITE|OCEXEC);	/* no error check necessary */
+	if(ofd > 0){
+		/* From dhog Fri Aug 24 22:13:00 EDT 2001 */
+		now = ctime(time(0));
+		fprint(ofd, "From %s %s", user, now);
+		fprint(ofd, "From: %s\n", user);
+		fprint(ofd, "Date: %s", now);
+		for(i=0; i<natt; i++)
+			if(included[i])
+				fprint(ofd, "Include: %s\n", attlist[i]);
+			else
+				fprint(ofd, "Attach: %s\n", attlist[i]);
+		/* needed because mail is by default Latin-1 */
+		fprint(ofd, "Content-Type: text/plain; charset=\"UTF-8\"\n");
+		fprint(ofd, "Content-Transfer-Encoding: 8bit\n");
+	}
+
+	e = emalloc(sizeof(struct Exec));
+	if(pipe(p) < 0)
+		error("can't create pipe: %r");
+	e->p[0] = p[0];
+	e->p[1] = p[1];
+	e->prog = "/bin/upas/marshal";
+	e->argv = emalloc((1+1+2+4*natt+1)*sizeof(char*));
+	e->argv[0] = estrdup("marshal");
+	e->argv[1] = estrdup("-8");
+	j = 2;
+	if(m->replyname){
+		e->argv[j++] = estrdup("-R");
+		e->argv[j++] = estrstrdup(mbox.name, m->replyname);
+	}
+	for(i=0; i<natt; i++){
+		if(included[i])
+			e->argv[j++] = estrdup("-A");
+		else
+			e->argv[j++] = estrdup("-a");
+		e->argv[j++] = estrdup(attlist[i]);
+	}
+	sync = chancreate(sizeof(int), 0);
+	e->sync = sync;
+	proccreate(execproc, e, EXECSTACK);
+	recvul(sync);
+	close(p[0]);
+
+	/* using marshal -8, so generate rfc822 headers */
+	if(nto > 0){
+		print2(p[1], ofd, "To: ");
+		for(i=0; i<nto-1; i++)
+			print2(p[1], ofd, "%s, ", tolist[i]);
+		print2(p[1], ofd, "%s\n", tolist[i]);
+	}
+	if(ncc > 0){
+		print2(p[1], ofd, "CC: ");
+		for(i=0; i<ncc-1; i++)
+			print2(p[1], ofd, "%s, ", cclist[i]);
+		print2(p[1], ofd, "%s\n", cclist[i]);
+	}
+	if(nbcc > 0){
+		print2(p[1], ofd, "BCC: ");
+		for(i=0; i<nbcc-1; i++)
+			print2(p[1], ofd, "%s, ", bcclist[i]);
+		print2(p[1], ofd, "%s\n", bcclist[i]);
+	}
+
+	i = strlen(body);
+	if(i > 0)
+		write2(p[1], ofd, body, i, 1);
+
+	/* guarantee a blank line, to ensure attachments are separated from body */
+	if(i==0 || body[i-1]!='\n')
+		write2(p[1], ofd, "\n\n", 2, 0);
+	else if(i>1 && body[i-2]!='\n')
+		write2(p[1], ofd, "\n", 1, 0);
+
+	/* these look like pseudo-attachments in the "outgoing" box */
+	if(ofd>0 && natt>0){
+		for(i=0; i<natt; i++)
+			if(included[i])
+				fprint(ofd, "=====> Include: %s\n", attlist[i]);
+			else
+				fprint(ofd, "=====> Attach: %s\n", attlist[i]);
+	}
+	if(ofd > 0)
+		write(ofd, "\n", 1);
+
+	for(i=0; i<natt; i++)
+		free(attlist[i]);
+	close(ofd);
+	close(p[1]);
+	free(body);
+
+	if(m->replyname != nil)
+		mesgmenumark(mbox.w, m->replyname, "\t[replied]");
+	if(m->name[0] == '/')
+		s = estrdup(m->name);
+	else
+		s = estrstrdup(mbox.name, m->name);
+	s = egrow(s, "-R", nil);
+	winname(m->w, s);
+	free(s);
+	winclean(m->w);
+	/* mark message unopened because it's no longer the original message */
+	m->opened = 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/util.c
@@ -1,0 +1,105 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include "dat.h"
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("can't malloc: %r");
+	memset(p, 0, n);
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void*
+erealloc(void *p, uint n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		error("can't realloc: %r");
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s)+1);
+	strcpy(t, s);
+	return t;
+}
+
+char*
+estrstrdup(char *s, char *t)
+{
+	char *u;
+
+	u = emalloc(strlen(s)+strlen(t)+1);
+	strcpy(u, s);
+	strcat(u, t);
+	return u;
+}
+
+char*
+eappend(char *s, char *sep, char *t)
+{
+	char *u;
+
+	if(t == nil)
+		u = estrstrdup(s, sep);
+	else{
+		u = emalloc(strlen(s)+strlen(sep)+strlen(t)+1);
+		strcpy(u, s);
+		strcat(u, sep);
+		strcat(u, t);
+	}
+	free(s);
+	return u;
+}
+
+char*
+egrow(char *s, char *sep, char *t)
+{
+	s = eappend(s, sep, t);
+	free(t);
+	return s;
+}
+
+void
+error(char *fmt, ...)
+{
+	Fmt f;
+	char buf[64];
+	va_list arg;
+
+	fmtfdinit(&f, 2, buf, sizeof buf);
+	fmtprint(&f, "Mail: ");
+	va_start(arg, fmt);
+	fmtvprint(&f, fmt, arg);
+	va_end(arg);
+	fmtprint(&f, "\n");
+	fmtfdflush(&f);
+	threadexitsall(fmt);
+}
+
+void
+ctlprint(int fd, char *fmt, ...)
+{
+	int n;
+	va_list arg;
+
+	va_start(arg, fmt);
+	n = vfprint(fd, fmt, arg);
+	va_end(arg);
+	if(n <= 0)
+		error("control file write error: %r");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/win.c
@@ -1,0 +1,341 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include "dat.h"
+
+Window*
+newwindow(void)
+{
+	char buf[12];
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	ctlprint(w->ctl, "noscroll\n");
+	w->id = atoi(buf);
+	w->event = winopenfile(w, "event");
+	w->addr = -1;	/* will be opened when needed */
+	w->body = nil;
+	w->data = -1;
+	w->cevent = chancreate(sizeof(Event*), 0);
+	return w;
+}
+
+void
+winsetdump(Window *w, char *dir, char *cmd)
+{
+	if(dir != nil)
+		ctlprint(w->ctl, "dumpdir %s\n", dir);
+	if(cmd != nil)
+		ctlprint(w->ctl, "dump %s\n", cmd);
+}
+
+void
+wineventproc(void *v)
+{
+	Window *w;
+	int i;
+
+	w = v;
+	for(i=0; ; i++){
+		if(i >= NEVENT)
+			i = 0;
+		wingetevent(w, &w->e[i]);
+		sendp(w->cevent, &w->e[i]);
+	}
+}
+
+static int
+winopenfile1(Window *w, char *f, int m)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/wsys/%d/%s", w->id, f);
+	fd = open(buf, m|OCEXEC);
+	if(fd < 0)
+		error("can't open window file %s: %r", f);
+	return fd;
+}
+
+int
+winopenfile(Window *w, char *f)
+{
+	return winopenfile1(w, f, ORDWR);
+}
+
+void
+wintagwrite(Window *w, char *s, int n)
+{
+	int fd;
+
+	fd = winopenfile(w, "tag");
+	if(write(fd, s, n) != n)
+		error("tag write: %r");
+	close(fd);
+}
+
+void
+winname(Window *w, char *s)
+{
+	ctlprint(w->ctl, "name %s\n", s);
+}
+
+void
+winopenbody(Window *w, int mode)
+{
+	char buf[256];
+
+	sprint(buf, "/mnt/wsys/%d/body", w->id);
+	w->body = Bopen(buf, mode|OCEXEC);
+	if(w->body == nil)
+		error("can't open window body file: %r");
+}
+
+void
+winclosebody(Window *w)
+{
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+}
+
+void
+winwritebody(Window *w, char *s, int n)
+{
+	if(w->body == nil)
+		winopenbody(w, OWRITE);
+	if(Bwrite(w->body, s, n) != n)
+		error("write error to window: %r");
+}
+
+int
+wingetec(Window *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0){
+			/* probably because window has exited, and only called by wineventproc, so just shut down */
+			threadexits(nil);
+		}
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+wingeten(Window *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=wingetec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+int
+wingeter(Window *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = wingetec(w);
+	buf[0] = r;
+	n = 1;
+	if(r >= Runeself) {
+		while(!fullrune(buf, n))
+			buf[n++] = wingetec(w);
+		chartorune(&r, buf);
+	} 
+	*nb = n;
+	return r;
+}
+
+void
+wingetevent(Window *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = wingetec(w);
+	e->c2 = wingetec(w);
+	e->q0 = wingeten(w);
+	e->q1 = wingeten(w);
+	e->flag = wingeten(w);
+	e->nr = wingeten(w);
+	if(e->nr > EVENTSIZE)
+		error("event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = wingeter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(wingetec(w) != '\n')
+		error("event syntax error");
+}
+
+void
+winwriteevent(Window *w, Event *e)
+{
+	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+void
+winread(Window *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	m = q0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			error("error writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = utfnlen(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+}
+
+void
+windormant(Window *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+
+int
+windel(Window *w, int sure)
+{
+	if(sure)
+		write(w->ctl, "delete\n", 7);
+	else if(write(w->ctl, "del\n", 4) != 4)
+		return 0;
+	/* event proc will die due to read error from event file */
+	windormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return 1;
+}
+
+void
+winclean(Window *w)
+{
+	if(w->body)
+		Bflush(w->body);
+	ctlprint(w->ctl, "clean\n");
+}
+
+int
+winsetaddr(Window *w, char *addr, int errok)
+{
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		if(!errok)
+			error("error writing addr(%s): %r", addr);
+		return 0;
+	}
+	return 1;
+}
+
+int
+winselect(Window *w, char *addr, int errok)
+{
+	if(winsetaddr(w, addr, errok)){
+		ctlprint(w->ctl, "dot=addr\n");
+		return 1;
+	}
+	return 0;
+}
+
+char*
+winreadbody(Window *w, int *np)	/* can't use readfile because acme doesn't report the length */
+{
+	char *s;
+	int m, na, n;
+
+	if(w->body != nil)
+		winclosebody(w);
+	winopenbody(w, OREAD);
+	s = nil;
+	na = 0;
+	n = 0;
+	for(;;){
+		if(na < n+512){
+			na += 1024;
+			s = realloc(s, na+1);
+		}
+		m = Bread(w->body, s+n, na-n);
+		if(m <= 0)
+			break;
+		n += m;
+	}
+	s[n] = 0;
+	winclosebody(w);
+	*np = n;
+	return s;
+}
+
+char*
+winselection(Window *w)
+{
+	int fd, m, n;
+	char *buf;
+	char tmp[256];
+
+	fd = winopenfile1(w, "rdsel", OREAD);
+	if(fd < 0)
+		error("can't open rdsel: %r");
+	n = 0;
+	buf = nil;
+	for(;;){
+		m = read(fd, tmp, sizeof tmp);
+		if(m <= 0)
+			break;
+		buf = erealloc(buf, n+m+1);
+		memmove(buf+n, tmp, m);
+		n += m;
+		buf[n] = '\0';
+	}
+	close(fd);
+	return buf;
+}
--- a/sys/src/cmd/upas/alias/aliasmail.c
+++ b/sys/src/cmd/upas/alias/aliasmail.c
@@ -5,28 +5,30 @@
  *  local ones.
  */
 
-/* predeclared */
-static String	*getdbfiles(void);
-static int	translate(char*, char**, String*, String*);
-static int	lookup(String**, String*, String*);
-static int	compare(String*, char*);
-static char*	mklower(char*);
+static	String	*getdbfiles(void);
+static	int	translate(char*, char**, String*, String*);
+static	int	lookup(String**, String*, String*);
+static	char	*mklower(char*);
 
-static int debug;
-static int from;
-static char *namefiles = "namefiles";
-#define DEBUG if(debug)
+static	int	debug;
+static	int	from;
+static	char	*namefiles = "namefiles";
 
-/* loop through the names to be translated */
+#define dprint(...) if(debug)fprint(2, __VA_ARGS__); else {}
+
 void
+usage(void)
+{
+	fprint(2, "usage: aliasmail [-df] [-n namefile] [names ...]\n");
+	exits("usage");
+}
+
+void
 main(int argc, char *argv[])
 {
-	String *s;
-	String *alias;		/* the alias for the name */
-	char **names;		/* names of this system */
-	String *files;		/* list of files to search */
+	char *alias, **names, *p;		/* names of this system */
 	int i, rv;
-	char *p;
+	String *s, *salias, *files;
 
 	ARGBEGIN {
 	case 'd':
@@ -36,50 +38,46 @@
 		from = 1;
 		break;
 	case 'n':
-		namefiles = ARGF();
+		namefiles = EARGF(usage());
 		break;
+	default:
+		usage();
 	} ARGEND
-	if (chdir(UPASLIB) < 0) {
-		perror("translate(chdir):");
-		exit(1);
-	}
+	if (chdir(UPASLIB) < 0)
+		sysfatal("chdir: %r");
 
-	/* get environmental info */
 	names = sysnames_read();
 	files = getdbfiles();
-	alias = s_new();
+	salias = s_new();
 
 	/* loop through the names to be translated (from standard input) */
 	for(i=0; i<argc; i++) {
 		s = unescapespecial(s_copy(mklower(argv[i])));
 		if(strchr(s_to_c(s), '!') == 0)
-			rv = translate(s_to_c(s), names, files, alias);
+			rv = translate(s_to_c(s), names, files, salias);
 		else
 			rv = -1;
+		alias = s_to_c(salias);
 		if(from){
-			if (rv >= 0 && *s_to_c(alias) != '\0'){
-				p = strchr(s_to_c(alias), '\n');
-				if(p)
+			if (rv >= 0 && *alias != '\0'){
+				if(p = strchr(alias, '\n'))
 					*p = 0;
-				p = strchr(s_to_c(alias), '!');
-				if(p) {
+				if(p = strchr(alias, '!')) {
 					*p = 0;
-					print("%s", s_to_c(alias));
+					print("%s", alias);
 				} else {
-					p = strchr(s_to_c(alias), '@');
-					if(p)
+					if(p = strchr(alias, '@'))
 						print("%s", p+1);
 					else
-						print("%s", s_to_c(alias));
+						print("%s", alias);
 				}
 			}
 		} else {
-			if (rv < 0 || *s_to_c(alias) == '\0')
+			if (rv < 0 || *alias == '\0')
 				print("local!%s\n", s_to_c(s));
-			else {
+			else
 				/* this must be a write, not a print */
-				write(1, s_to_c(alias), strlen(s_to_c(alias)));
-			}
+				write(1, alias, strlen(alias));
 		}
 		s_free(s);
 	}
@@ -90,9 +88,9 @@
 static String *
 getdbfiles(void)
 {
-	Sinstack *sp;
-	String *files = s_new();
 	char *nf;
+	Sinstack *sp;
+	String *files;
 
 	if(from)
 		nf = "fromfiles";
@@ -100,6 +98,7 @@
 		nf = namefiles;
 
 	/* system wide aliases */
+	files = s_new();
 	if ((sp = s_allocinstack(nf)) != 0){
 		while(s_rdinstack(sp, files))
 			s_append(files, " ");
@@ -106,31 +105,27 @@
 		s_freeinstack(sp);
 	}
 
-
-	DEBUG print("files are %s\n", s_to_c(files));
-
+	dprint("files are %s\n", s_to_c(files));
 	return files;
 }
 
 /* loop through the translation files */
 static int
-translate(char *name,		/* name to translate */
-	char **namev,		/* names of this system */
-	String *files,		/* names of system alias files */
-	String *alias)		/* where to put the alias */
+translate(char *name, char **namev,	String *files, String *alias)
 {
-	String *file = s_new();
-	String **fullnamev;
 	int n, rv;
+	String *file, **fullnamev;
 
 	rv = -1;
+	file = s_new();
 
-	DEBUG print("translate(%s, %s, %s)\n", name,
+	dprint("translate(%s, %s, %s)\n", name,
 		s_to_c(files), s_to_c(alias));
 
 	/* create the full name to avoid loops (system!name) */
 	for(n = 0; namev[n]; n++)
 		;
+
 	fullnamev = (String**)malloc(sizeof(String*)*(n+2));
 	n = 0;
 	fullnamev[n++] = s_copy(name);
@@ -144,12 +139,11 @@
 
 	/* look at system-wide names */
 	s_restart(files);
-	while (s_parse(files, s_restart(file)) != 0) {
+	while (s_parse(files, s_restart(file)) != 0)
 		if (lookup(fullnamev, file, alias)==0) {
 			rv = 0;
 			goto out;
 		}
-	}
 
 out:
 	for(n = 0; fullnamev[n]; n++)
@@ -183,22 +177,23 @@
 /*  Loop through the entries in a translation file looking for a match.
  *  Return 0 if found, -1 otherwise.
  */
+#define compare(a, b) cistrcmp(s_to_c(a), b)
+
 static int
-lookup(
-	String **namev,
-	String *file,
-	String *alias)	/* returned String */
+lookup(String **namev, String *file, String *alias)
 {
-	String *line = s_new();
-	String *token = s_new();
-	String *bangtoken;
-	int i, rv = -1;
-	char *name =  s_to_c(namev[0]);
+	char *name;
+	int i, rv;
+	String *line, *token, *bangtoken;
 	Sinstack *sp;
 
-	DEBUG print("lookup(%s, %s, %s, %s)\n", s_to_c(namev[0]), s_to_c(namev[1]),
+	dprint("lookup(%s, %s, %s, %s)\n", s_to_c(namev[0]), s_to_c(namev[1]),
 		s_to_c(file), s_to_c(alias));
 
+	rv = -1;
+	name = s_to_c(namev[0]);
+	line = s_new();
+	token = s_new();
 	s_reset(alias);
 	if ((sp = s_allocinstack(s_to_c(file))) == 0)
 		return -1;
@@ -205,7 +200,7 @@
 
 	/* look for a match */
 	while (s_rdinstack(sp, s_restart(line))!=0) {
-		DEBUG print("line is %s\n", s_to_c(line));
+		dprint("line is %s\n", s_to_c(line));
 		s_restart(token);
 		if (s_parse(s_restart(line), token)==0)
 			continue;
@@ -247,35 +242,13 @@
 	return rv;
 }
 
-#define lower(c) ((c)>='A' && (c)<='Z' ? (c)-('A'-'a'):(c))
-
-/* compare two Strings (case insensitive) */
-static int
-compare(String *s1,
-	char *p2)
-{
-	char *p1 = s_to_c(s1);
-	int rv;
-
-	DEBUG print("comparing %s to %s\n", p1, p2);
-	while((rv = lower(*p1) - lower(*p2)) == 0) {
-		if (*p1 == '\0')
-			break;
-		p1++;
-		p2++;
-	}
-	return rv;
-}
-
 static char*
 mklower(char *name)
 {
-	char *p;
-	char c;
+	char c, *p;
 
-	for(p = name; *p; p++){
-		c = *p;
-		*p = lower(c);
-	}
+	for(p = name; c = *p; p++)
+		if(c >= 'A' && c <= 'Z')
+			*p = c + 0x20;
 	return name;
 }
--- a/sys/src/cmd/upas/alias/mkfile
+++ b/sys/src/cmd/upas/alias/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=aliasmail
 
@@ -9,8 +10,6 @@
 HFILES=../common/common.h\
 	 ../common/sys.h\
 
-
-BIN=/$objtype/bin/upas
 
 UPDATE=\
 	mkfile\
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/isspam.rc
@@ -1,0 +1,2 @@
+#!/bin/rc
+exec /mail/lib/isspam.rc $*
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/mkfile
@@ -1,0 +1,34 @@
+</$objtype/mkfile
+<../mkupas
+
+RCFILES=isspam\
+	msgcat\
+	spam\
+	tfmt\
+	unspam\
+
+all:Q:
+	;
+
+installall:Q:	install
+	;
+
+install:V: ${RCFILES:%=$BIN/%}
+
+safeinstall:V: install
+
+safeinstallall:V: install
+
+clean:Q:
+	;
+nuke:V:
+	rm $BIN/^($RCFILES)
+
+UPDATE=$RCFILES
+
+update:V:
+	update $UPDATEFLAGS $UPDATE
+
+$BIN/%: %.rc
+	cp $stem.rc $BIN/$stem
+
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/mkfile.rc
@@ -1,0 +1,38 @@
+
+RCFILES=mail.rc\
+
+all:Q:
+	;
+
+installall:Q:	install
+	;
+
+install:V:
+	cp mail.rc /rc/bin/mail
+
+safeinstall:V:
+	cp mail.rc /rc/bin/mail
+
+safeinstallall:V:
+	cp mail.rc /rc/bin/mail
+
+clean:Q:
+	;
+nuke:V:
+	rm /rc/bin/mail
+
+UPDATE=\
+	gone.fishing\
+	gone.msg\
+	mail.rc\
+	mail.sh\
+	makefile\
+	mkfile\
+	namefiles\
+	omail.rc\
+	qmail\
+	remotemail\
+	rewrite\
+
+update:V:
+	update $UPDATEFLAGS $UPDATE
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/msgcat.rc
@@ -1,0 +1,11 @@
+#!/bin/rc
+
+f=$*
+if(~ $#f 0)
+	f=/mail/fs/mbox/[0-9]*
+f=`{echo $f|sed s:/mail/fs/mbox/::g}
+
+{
+	for(i in $f)
+		echo $i p
+} | upas/nedmail >[2=]
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/spam.rc
@@ -1,0 +1,2 @@
+#!/bin/rc
+exec /mail/lib/spam.rc $*
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/tfmt.rc
@@ -1,0 +1,25 @@
+#!/bin/rc
+# anti-topposting defense
+
+# sed '/^[ 	]*>[ 	]*>[ 	]*>/q'
+
+awk '
+{
+	if(l[i] ~ /^[ 	]*>[ 	]*>[ 	]*>/)
+		q = 1
+	if(q == 0)
+		l[i = NR] = $0;
+}
+END{
+	for(; i > 1; i--)
+		if(l[i] !~ /^([ 	]*>)*[ 	]*$/)
+			break;
+	for(; i > 1; i--)
+		if(l[i] !~ /^[ 	]*>[ 	]*>/)
+			break;
+	for(; i > 1; i--)
+		if(l[i] !~ /^([ 	]*>)*[ 	]*$/)
+			break;
+	for(j = 1; j <= i; j++)
+		print l[j]
+}' |dd -conv block >[2=]
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/unspam.rc
@@ -1,0 +1,2 @@
+#!/bin/rc
+exec /mail/lib/unspam.rc $*
--- a/sys/src/cmd/upas/common/appendfiletombox.c
+++ /dev/null
@@ -1,164 +1,0 @@
-#include "common.h"
-
-enum {
-	Buffersize = 64*1024,
-};
-
-typedef struct Inbuf Inbuf;
-struct Inbuf
-{
-	char buf[Buffersize];
-	char *wp;
-	char *rp;
-	int eof;
-	int in;
-	int out;
-	int last;
-	ulong bytes;
-};
-
-static Inbuf*
-allocinbuf(int in, int out)
-{
-	Inbuf *b;
-
-	b = mallocz(sizeof(Inbuf), 1);
-	if(b == nil)
-		sysfatal("reading mailbox: %r");
-	b->rp = b->wp = b->buf;
-	b->in = in;
-	b->out = out;
-	return b;
-}
-
-/* should only be called at start of file or when b->rp[-1] == '\n' */
-static int
-fill(Inbuf *b, int addspace)
-{
-	int i, n;
-
-	if(b->eof && b->wp - b->rp == 0)
-		return 0;
-
-	n = b->rp - b->buf;
-	if(n > 0){
-		i = write(b->out, b->buf, n);
-		if(i != n)
-			return -1;
-		b->last = b->buf[n-1];
-		b->bytes += n;
-	}
-	if(addspace){
-		if(write(b->out, " ", 1) != 1)
-			return -1;
-		b->last = ' ';
-		b->bytes++;
-	}
-
-	n = b->wp - b->rp;
-	memmove(b->buf, b->rp, n);
-	b->rp = b->buf;
-	b->wp = b->rp + n;
-
-	i = read(b->in, b->buf+n, sizeof(b->buf)-n);
-	if(i < 0)
-		return -1;
-	b->wp += i;
-
-	return b->wp - b->rp;
-}
-
-enum { Fromlen = sizeof "From " - 1, };
-
-/* code to escape ' '*From' ' at the beginning of a line */
-int
-appendfiletombox(int in, int out)
-{
-	int addspace, n, sol;
-	char *p;
-	Inbuf *b;
-
-	seek(out, 0, 2);
-
-	b = allocinbuf(in, out);
-	addspace = 0;
-	sol = 1;
-
-	for(;;){
-		if(b->wp - b->rp < Fromlen){
-			/*
-			 * not enough unread bytes in buffer to match "From ",
-			 * so get some more.  We must only inject a space at
-			 * the start of a line (one that begins with "From ").
-			 */
-			if (b->rp == b->buf || b->rp[-1] == '\n') {
-				n = fill(b, addspace);
-				addspace = 0;
-			} else
-				n = fill(b, 0);
-			if(n < 0)
-				goto error;
-			if(n == 0)
-				break;
-			if(n < Fromlen){	/* still can't match? */
-				b->rp = b->wp;
-				continue;
-			}
-		}
-
-		/* state machine looking for ' '*From' ' */
-		if(!sol){
-			p = memchr(b->rp, '\n', b->wp - b->rp);
-			if(p == nil)
-				b->rp = b->wp;
-			else{
-				b->rp = p+1;
-				sol = 1;
-			}
-			continue;
-		} else {
-			if(*b->rp == ' ' || strncmp(b->rp, "From ", Fromlen) != 0){
-				b->rp++;
-				continue;
-			}
-			addspace = 1;
-			sol = 0;
-		}
-	}
-
-	/* mailbox entries always terminate with two newlines */
-	n = b->last == '\n' ? 1 : 2;
-	if(write(out, "\n\n", n) != n)
-		goto error;
-	n += b->bytes;
-	free(b);
-	return n;
-error:
-	free(b);
-	return -1;
-}
-
-int
-appendfiletofile(int in, int out)
-{
-	int n;
-	Inbuf *b;
-
-	seek(out, 0, 2);
-
-	b = allocinbuf(in, out);
-	for(;;){
-		n = fill(b, 0);
-		if(n < 0)
-			goto error;
-		if(n == 0)
-			break;
-		b->rp = b->wp;
-	}
-	n = b->bytes;
-	free(b);
-	return n;
-error:
-	free(b);
-	return -1;
-}
--- a/sys/src/cmd/upas/common/aux.c
+++ b/sys/src/cmd/upas/common/aux.c
@@ -1,42 +1,5 @@
 #include "common.h"
 
-/* expand a path relative to some `.' */
-extern String *
-abspath(char *path, char *dot, String *to)
-{
-	if (*path == '/') {
-		to = s_append(to, path);
-	} else {
-		to = s_append(to, dot);
-		to = s_append(to, "/");
-		to = s_append(to, path);
-	}
-	return to;
-}
-
-/* return a pointer to the base component of a pathname */
-extern char *
-basename(char *path)
-{
-	char *cp;
-
-	cp = strrchr(path, '/');
-	return cp==0 ? path : cp+1;
-}
-
-/* append a sub-expression match onto a String */
-extern void
-append_match(Resub *subexp, String *sp, int se)
-{
-	char *cp, *ep;
-
-	cp = subexp[se].sp;
-	ep = subexp[se].ep;
-	for (; cp < ep; cp++)
-		s_putc(sp, *cp);
-	s_terminate(sp);
-}
-
 /*
  *  check for shell characters in a String
  */
@@ -95,7 +58,7 @@
 	return ns;
 }
 
-int
+uint
 hex2uint(char x)
 {
 	if(x >= '0' && x <= '9')
@@ -113,10 +76,9 @@
 extern String*
 unescapespecial(String *s)
 {
-	int c;
-	String *ns;
 	char *sp;
-	uint n;
+	uint c, n;
+	String *ns;
 
 	if(strstr(s_to_c(s), escape) == 0)
 		return s;
@@ -126,7 +88,7 @@
 	for(sp = s_to_c(s); *sp; sp++){
 		if(strncmp(sp, escape, n) == 0){
 			c = (hex2uint(sp[n])<<4) | hex2uint(sp[n+1]);
-			if(c < 0)
+			if(c & 0x80)
 				s_putc(ns, *sp);
 			else {
 				s_putc(ns, c);
@@ -144,6 +106,5 @@
 int
 returnable(char *path)
 {
-
 	return strcmp(path, "/dev/null") != 0;
 }
--- a/sys/src/cmd/upas/common/become.c
+++ b/sys/src/cmd/upas/common/become.c
@@ -6,11 +6,10 @@
  *  become powerless user
  */
 int
-become(char **cmd, char *who)
+become(char **, char *who)
 {
 	int fd;
 
-	USED(cmd);
 	if(strcmp(who, "none") == 0) {
 		fd = open("#c/user", OWRITE);
 		if(fd < 0 || write(fd, "none", strlen("none")) < 0) {
--- a/sys/src/cmd/upas/common/common.h
+++ b/sys/src/cmd/upas/common/common.h
@@ -1,59 +1,61 @@
+enum
+{
+	Elemlen	= 56,
+	Pathlen	= 256,
+};
+
 #include "sys.h"
+#include <String.h>
 
-/* format of REMOTE FROM lines */
-extern char *REMFROMRE;
-extern int REMSENDERMATCH;
-extern int REMDATEMATCH;
-extern int REMSYSMATCH;
+enum{
+	Fields	= 18,
 
-/* format of mailbox FROM lines */
-#define IS_HEADER(p) ((p)[0]=='F'&&(p)[1]=='r'&&(p)[2]=='o'&&(p)[3]=='m'&&(p)[4]==' ')
-#define IS_TRAILER(p) ((p)[0]=='m'&&(p)[1]=='o'&&(p)[2]=='r'&&(p)[3]=='F'&&(p)[4]=='\n')
-extern char *FROMRE;
-extern int SENDERMATCH;
-extern int DATEMATCH;
-
-enum
-{
-	Elemlen= 28,
-	Errlen=	ERRMAX,
-	Pathlen= 256,
+	/* flags */
+	Fanswered	= 1<<0, /* a */
+	Fdeleted		= 1<<1, /* D */
+	Fdraft		= 1<<2, /* d */
+	Fflagged		= 1<<3, /* f */
+	Frecent		= 1<<4, /* r	we are the first fs to see this */
+	Fseen		= 1<<5, /* s */
+	Fstored		= 1<<6, /* S */
+	Nflags		= 7,
 };
-enum { Atnoteunknown, Atnoterecog };
 
 /*
- *  routines in mail.c
+ * flag.c
  */
-extern int	print_header(Biobuf*, char*, char*);
-extern int	print_remote_header(Biobuf*, char*, char*, char*);
-extern int	parse_header(char*, String*, String*);
+char	*flagbuf(char*, int);
+int	buftoflags(char*);
+char	*txflags(char*, uchar*);
 
 /*
  *  routines in aux.c
  */
-extern String	*abspath(char*, char*, String*);
-extern String	*mboxpath(char*, char*, String*, int);
-extern char	*basename(char*);
-extern int	delivery_status(String*);
-extern void	append_match(Resub*, String*, int);
-extern int	shellchars(char*);
-extern String*	escapespecial(String*);
-extern String*	unescapespecial(String*);
-extern int	returnable(char*);
+char	*mboxpathbuf(char*, int, char*, char*);
+char	*basename(char*);
+int	shellchars(char*);
+String	*escapespecial(String*);
+String	*unescapespecial(String*);
+int	returnable(char*);
 
-/* in copymessage */
-extern int	appendfiletombox(int, int);
-extern int	appendfiletofile(int, int);
+/* folder.c */
+Biobuf	*openfolder(char*, long);
+int	closefolder(Biobuf*);
+int	appendfolder(Biobuf*, char*, long*, int);
+int	fappendfolder(char*, long, char *, int);
+int	fappendfile(char*, char*, int);
+char*	foldername(char*, char*, char*);
+char*	ffoldername(char*, char*, char*);
 
-/* mailbox types */
-#define MF_NORMAL 0
-#define MF_PIPE 1
-#define MF_FORWARD 2
-#define MF_NOMBOX 3
-#define MF_NOTMBOX 4
+/* fmt.c */
+void	mailfmtinstall(void);	/* 'U' = 2047fmt */
+#pragma varargck	type	"U"	char*
 
+/* totm.c */
+int	fromtotm(char*, Tm*);
+
 /* a pipe between parent and child*/
-typedef struct {
+typedef struct{
 	Biobuf	bb;
 	Biobuf	*fp;	/* parent process end*/
 	int	fd;	/* child process end*/
@@ -60,7 +62,7 @@
 } stream;
 
 /* a child process*/
-typedef struct process{
+typedef struct{
 	stream	*std[3];	/* standard fd's*/
 	int	pid;		/* process identifier*/
 	int	status;		/* exit status*/
@@ -67,14 +69,11 @@
 	Waitmsg	*waitmsg;
 } process;
 
-extern stream	*instream(void);
-extern stream	*outstream(void);
-extern void	stream_free(stream*);
-extern process	*noshell_proc_start(char**, stream*, stream*, stream*, int, char*);
-extern process	*proc_start(char*, stream*, stream*, stream*, int, char*);
-extern int	proc_wait(process*);
-extern int	proc_free(process*);
-extern int	proc_kill(process*);
-
-/* tell compiler we're using a value so it won't complain */
-#define USE(x)	if(x)
+stream	*instream(void);
+stream	*outstream(void);
+void	stream_free(stream*);
+process	*noshell_proc_start(char**, stream*, stream*, stream*, int, char*);
+process	*proc_start(char*, stream*, stream*, stream*, int, char*);
+int	proc_wait(process*);
+int	proc_free(process*);
+//int	proc_kill(process*);
--- a/sys/src/cmd/upas/common/config.c
+++ b/sys/src/cmd/upas/common/config.c
@@ -1,11 +1,9 @@
 #include "common.h"
 
-char *MAILROOT =	"/mail";
-char *UPASLOG =		"/sys/log";
-char *UPASLIB = 	"/mail/lib";
-char *UPASBIN=		"/bin/upas";
-char *UPASTMP = 	"/mail/tmp";
-char *SHELL = 		"/bin/rc";
-char *POST =		"/sys/lib/post/dispatch";
-
-int MBOXMODE = 0662;
+char *MAILROOT = "/mail";
+char *SPOOL	= "/mail";
+char *UPASLOG	= "/sys/log";
+char *UPASLIB	= "/mail/lib";
+char *UPASBIN	= "/bin/upas";
+char *UPASTMP	= "/mail/tmp";
+char *SHELL	= "/bin/rc";
--- /dev/null
+++ b/sys/src/cmd/upas/common/flags.c
@@ -1,0 +1,73 @@
+#include "common.h"
+
+static uchar flagtab[] = {
+	'a',	Fanswered,
+	'D',	Fdeleted,
+	'd',	Fdraft,
+	'f',	Fflagged,
+	'r',	Frecent,
+	's',	Fseen,
+	'S',	Fstored,
+};
+
+char*
+flagbuf(char *buf, int flags)
+{
+	char *p, c;
+	int i;
+
+	p = buf;
+	for(i = 0; i < nelem(flagtab); i += 2){
+		c = '-';
+		if(flags & flagtab[i+1])
+			c = flagtab[i];
+		*p++ = c;
+	}
+	*p = 0;
+	return buf;
+}
+
+int
+buftoflags(char *p)
+{
+	uchar flags;
+	int i;
+
+	flags = 0;
+	for(i = 0; i < nelem(flagtab); i += 2)
+		if(p[i>>1] == flagtab[i])
+			flags |= flagtab[i + 1];
+	return flags;
+}
+
+char*
+txflags(char *p, uchar *flags)
+{
+	uchar neg, f, c, i;
+
+	for(;;){
+		neg = 0;
+	again:
+		if((c = *p++) == '-'){
+			neg = 1;
+			goto again;
+		}else if(c == '+'){
+			neg = 0;
+			goto again;	
+		}
+		if(c == 0)
+			return nil;
+		for(i = 0;; i += 2){
+			if(i == nelem(flagtab))
+				return "bad flag";
+			if(c == flagtab[i]){
+				f = flagtab[i+1];
+				break;
+			}
+		}
+		if(neg)
+			*flags &= ~f;
+		else
+			*flags |= f;
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/common/fmt.c
@@ -1,0 +1,35 @@
+#include "common.h"
+
+int
+rfc2047fmt(Fmt *fmt)
+{
+	char *s, *p;
+
+	s = va_arg(fmt->args, char*);
+	if(s == nil)
+		return fmtstrcpy(fmt, "");
+	for(p=s; *p; p++)
+		if((uchar)*p >= 0x80)
+			goto hard;
+	return fmtstrcpy(fmt, s);
+
+hard:
+	fmtprint(fmt, "=?utf-8?q?");
+	for(p = s; *p; p++){
+		if(*p == ' ')
+			fmtrune(fmt, '_');
+		else if(*p == '_' || *p == '\t' || *p == '=' || *p == '?' ||
+		    (uchar)*p >= 0x80)
+			fmtprint(fmt, "=%.2uX", (uchar)*p);
+		else
+			fmtrune(fmt, (uchar)*p);
+	}
+	fmtprint(fmt, "?=");
+	return 0;
+}
+
+void
+mailfmtinstall(void)
+{
+	fmtinstall('U', rfc2047fmt);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/common/folder.c
@@ -1,0 +1,361 @@
+#include "common.h"
+
+enum{
+	Mbox	= 1,
+	Mdir,
+};
+
+typedef struct Folder Folder;
+struct Folder{
+	int	open;
+	int	ofd;
+	int	type;
+	Biobuf	*out;
+	Mlock	*l;
+};
+static Folder ftab[5];
+
+static Folder*
+getfolder(Biobuf *out)
+{
+	int i;
+	Folder *f;
+
+	for(i = 0; i < nelem(ftab); i++){
+		f = ftab+i;
+		if(f->open == 0){
+			f->open = 1;
+			f->ofd = -1;
+			f->type = 0;
+			return f;
+		}
+		if(f->out == out)
+			return f;
+	}
+	sysfatal("folder.c:ftab too small");
+	return 0;
+}
+
+static int
+putfolder(Folder *f)
+{
+	int r;
+
+	r = 0;
+	if(f->l)
+		sysunlock(f->l);
+	if(f->out)
+		r |= Bterm(f->out);
+	if(f->ofd > 0){
+		close(f->ofd);
+		free(f->out);
+	}
+	memset(f, 0, sizeof *f);
+	return r;
+}
+
+static Biobuf*
+mboxopen(char *s)
+{
+	Folder *f;
+
+	f = getfolder(0);
+	f->l = syslock(s);		/* traditional botch: ignore failure */
+	if((f->ofd = open(s, OWRITE)) == -1)
+	if((f->ofd = create(s, OWRITE|OEXCL, DMAPPEND|0600)) == -1){
+		putfolder(f);
+		return nil;
+	}
+	seek(f->ofd, 0, 2);
+	f->out = malloc(sizeof *f->out);
+	Binit(f->out, f->ofd, OWRITE);
+	f->type = Mbox;
+	return f->out;
+}
+
+/*
+ * sync with send/cat_mail.c:/^mdir
+ */
+static Biobuf*
+mdiropen(char *s, long t)
+{
+	char buf[64];
+	long i;
+	Folder *f;
+
+	f = getfolder(0);
+	for(i = 0; i < 100; i++){
+		snprint(buf, sizeof buf, "%s/%lud.%.2ld", s, t, i);
+		if((f->ofd = create(buf, OWRITE|OEXCL, DMAPPEND|0660)) != -1)
+			goto found;
+	}
+	putfolder(f);
+	return nil;
+found:
+	werrstr("");
+	f->out = malloc(sizeof *f->out);
+	Binit(f->out, f->ofd, OWRITE);
+	f->type = Mdir;
+	return f->out;
+}
+
+Biobuf*
+openfolder(char *s, long t)
+{
+	int isdir;
+	Dir *d;
+
+	if(d = dirstat(s)){
+		isdir = d->mode&DMDIR;
+		free(d);
+	}else{
+		isdir = create(s, OREAD, DMDIR|0777);
+		if(isdir == -1)
+			return nil;
+		close(isdir);
+		isdir = 1;
+	}
+	if(isdir)
+		return mdiropen(s, t);
+	else
+		return mboxopen(s);
+}
+
+int
+renamefolder(Biobuf *b, long t)
+{
+	char buf[32];
+	int i;
+	Dir d;
+	Folder *f;
+
+	f = getfolder(b);
+	if(f->type != Mdir)
+		return 0;
+	for(i = 0; i < 100; i++){
+		nulldir(&d);
+		snprint(buf, sizeof buf, "%lud.%.2d", t, i);
+		d.name = buf;
+		if(dirfwstat(Bfildes(b), &d) > 0)
+			return 0;
+	}
+	return -1;
+}
+
+int
+closefolder(Biobuf *b)
+{
+	return putfolder(getfolder(b));
+}
+
+/*
+ * escape "From " at the beginning of a line;
+ * translate \r\n to \n for imap
+ */
+static int
+mboxesc(Biobuf *in, Biobuf *out, int type)
+{
+	char *s;
+	int n;
+
+	for(; s = Brdstr(in, '\n', 0); free(s)){
+		if(!strncmp(s, "From ", 5))
+			Bputc(out, ' ');
+		n = strlen(s);
+		if(n > 1 && s[n-2] == '\r'){
+			s[n-2] = '\n';
+			n--;
+		}
+		if(Bwrite(out, s, n) == Beof){
+			free(s);
+			return -1;
+		}
+		if(s[n-1] != '\n')
+			Bputc(out, '\n');
+	}
+	if(type == Mbox)
+		Bputc(out, '\n');
+	if(Bflush(out) == Beof)
+		return -1;
+	return 0;
+}
+
+int
+appendfolder(Biobuf *b, char *addr, long *t, int fd)
+{
+	char *s;
+	int r;
+	Biobuf bin;
+	Folder *f;
+	Tm tm;
+
+	f = getfolder(b);
+	Bseek(f->out, 0, 2);
+	Binit(&bin, fd, OREAD);
+	s = Brdstr(&bin, '\n', 0);
+	if(!s || strncmp(s, "From ", 5))
+		Bprint(f->out, "From %s %.28s\n", addr, ctime(*t));
+	else if(fromtotm(s, &tm) >= 0)
+		*t = tm2sec(&tm);
+	if(s)
+		Bwrite(f->out, s, strlen(s));
+	free(s);
+	r = mboxesc(&bin, f->out, f->type);
+	return r | Bterm(&bin);
+}
+
+int
+fappendfolder(char *addr, long t, char *s, int fd)
+{
+	long t0, r;
+	Biobuf *b;
+
+	b = openfolder(s, t);
+	if(b == nil)
+		return -1;
+	t0 = t;
+	r = appendfolder(b, addr, &t, fd);
+	if(t != t0)
+		renamefolder(b, t);
+	r |= closefolder(b);
+	return r;
+}
+
+/*
+ * BOTCH sync with ../imap4d/mbox.c:/^okmbox
+ */
+
+static char *specialfile[] =
+{
+	"L.mbox",
+	"forward",
+	"headers",
+	"imap.subscribed",
+	"names",
+	"pipefrom",
+	"pipeto",
+};
+
+static int
+special(char *s)
+{
+	char *p;
+	int i;
+
+	p = strrchr(s, '/');
+	if(p == nil)
+		p = s;
+	else
+		p++;
+	for(i = 0; i < nelem(specialfile); i++)
+		if(strcmp(p, specialfile[i]) == 0)
+			return 1;
+	return 0;
+}
+
+static char*
+mkmbpath(char *s, int n, char *user, char *mb, char *path)
+{
+	char *p, *e, *r, buf[Pathlen];
+
+	if(!mb)
+		return mboxpathbuf(s, n, user, path);
+	e = buf+sizeof buf;
+	p = seprint(buf, e, "%s", mb);
+	if(r = strrchr(buf, '/'))
+		p = r;
+	seprint(p, e, "/%s", path);
+	return mboxpathbuf(s, n, user, buf);
+}
+
+
+/*
+ * fancy processing for ned:
+ * we default to storing in $mail/f then just in $mail.
+ */
+char*
+ffoldername(char *mb, char *user, char *rcvr)
+{
+	char *p;
+	int c, n;
+	Dir *d;
+	static char buf[Pathlen];
+
+	d = dirstat(mkmbpath(buf, sizeof buf, user, mb, "f/"));
+	n = strlen(buf);
+	if(!d ||  d->qid.type != QTDIR)
+		buf[n -= 2] = 0;
+	free(d);
+
+	if(p = strrchr(rcvr, '!'))
+		rcvr = p+1;
+	while(n < sizeof buf-1 && (c = *rcvr++)){
+		if(c== '@')
+			break;
+		if(c == '/')
+			c = '_';
+		buf[n++] = c;
+	}
+	buf[n] = 0;
+
+	if(special(buf)){
+		fprint(2, "!won't overwrite %s\n", buf);
+		return nil;
+	}
+	return buf;
+}
+
+char*
+foldername(char *mb, char *user, char *path)
+{
+	static char buf[Pathlen];
+
+	mkmbpath(buf, sizeof buf, user, mb, path);
+	if(special(buf)){
+		fprint(2, "!won't overwrite %s\n", buf);
+		return nil;
+	}
+	return buf;
+}
+
+static int
+append(Biobuf *in, Biobuf *out)
+{
+	char *buf;
+	int n, m;
+
+	buf = malloc(8192);
+	for(;;){
+		m = 0;
+		n = Bread(in, buf, 8192);
+		if(n <= 0)
+			break;
+		m = Bwrite(out, buf, n);
+		if(m != n)
+			break;
+	}
+	if(m != n)
+		n = -1;
+	else
+		n = 1;
+	free(buf);
+	return n;
+}
+
+/* symmetry for nedmail; misnamed */
+int
+fappendfile(char*, char *target, int in)
+{
+	int fd, r;
+	Biobuf bin, out;
+
+	if((fd = create(target, ORDWR|OEXCL, 0666)) == -1)
+		return -1;
+	Binit(&out, fd, OWRITE);
+	Binit(&bin, in, OREAD);
+	r = append(&bin, &out);
+	Bterm(&bin);
+	Bterm(&out);
+	close(fd);
+	return r;
+}
--- a/sys/src/cmd/upas/common/libsys.c
+++ b/sys/src/cmd/upas/common/libsys.c
@@ -3,25 +3,15 @@
 #include <ndb.h>
 
 /*
- *  number of predefined fd's
- */
-int nsysfile=3;
-
-static char err[Errlen];
-
-/*
  *  return the date
  */
-extern char *
+char*
 thedate(void)
 {
 	static char now[64];
-	char *cp;
 
 	strcpy(now, ctime(time(0)));
-	cp = strchr(now, '\n');
-	if(cp)
-		*cp = 0;
+	now[28] = 0;
 	return now;
 }
 
@@ -28,42 +18,27 @@
 /*
  *  return the user id of the current user
  */
-extern char *
+char *
 getlog(void)
 {
-	static char user[64];
-	int fd;
-	int n;
-
-	fd = open("/dev/user", 0);
-	if(fd < 0)
-		return nil;
-	if((n=read(fd, user, sizeof(user)-1)) <= 0)
-		return nil;
-	close(fd);
-	user[n] = 0;
-	return user;
+	return getuser();
 }
 
 /*
  *  return the lock name (we use one lock per directory)
  */
-static String *
-lockname(char *path)
+static void
+lockname(Mlock *l, char *path)
 {
-	String *lp;
-	char *cp;
+	char *e, *q;
 
-	/*
-	 *  get the name of the lock file
-	 */
-	lp = s_new();
-	cp = strrchr(path, '/');
-	if(cp)
-		s_nappend(lp, path, cp - path + 1);
-	s_append(lp, "L.mbox");
-
-	return lp;
+	seprint(l->name, e = l->name+sizeof l->name, "%s", path);
+	q = strrchr(l->name, '/');
+	if(q == nil)
+		q = l->name;
+	else
+		q++;
+	seprint(q, e, "%s", "L.mbox");
 }
 
 int
@@ -92,58 +67,43 @@
 openlockfile(Mlock *l)
 {
 	int fd;
-	Dir *d;
-	Dir nd;
+	Dir *d, nd;
 	char *p;
 
-	fd = open(s_to_c(l->name), OREAD);
-	if(fd >= 0){
-		l->fd = fd;
+	l->fd = open(l->name, OREAD);
+	if(l->fd >= 0)
 		return 0;
+	if(d = dirstat(l->name)){
+		free(d);
+		return 1;	/* try again later */
 	}
-
-	d = dirstat(s_to_c(l->name));
-	if(d == nil){
-		/* file doesn't exist */
-		/* try creating it */
-		fd = create(s_to_c(l->name), OREAD, DMEXCL|0666);
-		if(fd >= 0){
-			nulldir(&nd);
-			nd.mode = DMEXCL|0666;
-			if(dirfwstat(fd, &nd) < 0){
-				/* if we can't chmod, don't bother */
-				/* live without the lock but log it */
-				syslog(0, "mail", "lock error: %s: %r", s_to_c(l->name));
-				remove(s_to_c(l->name));
-			}
-			l->fd = fd;
-			return 0;
+	l->fd = create(l->name, OREAD, DMEXCL|0666);
+	if(l->fd >= 0){
+		nulldir(&nd);
+		nd.mode = DMEXCL|0666;
+		if(dirfwstat(l->fd, &nd) < 0){
+			/* if we can't chmod, don't bother */
+			/* live without the lock but log it */
+			close(l->fd);
+			l->fd = -1;
+			syslog(0, "mail", "lock error: %s: %r", l->name);
+			remove(l->name);
 		}
-
-		/* couldn't create */
-		/* do we have write access to the directory? */
-		p = strrchr(s_to_c(l->name), '/');
-		if(p != 0){
-			*p = 0;
-			fd = access(s_to_c(l->name), 2);
-			*p = '/';
-			if(fd < 0){
-				/* live without the lock but log it */
-				syslog(0, "mail", "lock error: %s: %r", s_to_c(l->name));
-				return 0;
-			}
-		} else {
-			fd = access(".", 2);
-			if(fd < 0){
-				/* live without the lock but log it */
-				syslog(0, "mail", "lock error: %s: %r", s_to_c(l->name));
-				return 0;
-			}
-		}
-	} else
-		free(d);
-
-	return 1; /* try again later */
+		return 0;
+	}
+	/* couldn't create; let's see what we can whine about */
+	p = strrchr(l->name, '/');
+	if(p != 0){
+		*p = 0;
+		fd = access(l->name, 2);
+		*p = '/';
+	}else
+		fd = access(".", 2);
+	if(fd < 0)
+		/* live without the lock but log it */
+		syslog(0, "mail", "lock error: %s: %r", l->name);
+	close(fd);
+	return 0;
 }
 
 #define LSECS 5*60
@@ -152,7 +112,7 @@
  *  Set a lock for a particular file.  The lock is a file in the same directory
  *  and has L. prepended to the name of the last element of the file name.
  */
-extern Mlock *
+Mlock*
 syslock(char *path)
 {
 	Mlock *l;
@@ -162,12 +122,11 @@
 	if(l == 0)
 		return nil;
 
-	l->name = lockname(path);
-
+	lockname(l, path);
 	/*
 	 *  wait LSECS seconds for it to unlock
 	 */
-	for(tries = 0; tries < LSECS*2; tries++){
+	for(tries = 0; tries < LSECS*2; tries++)
 		switch(openlockfile(l)){
 		case 0:
 			return l;
@@ -174,13 +133,7 @@
 		case 1:
 			sleep(500);
 			break;
-		default:
-			goto noway;
 		}
-	}
-
-noway:
-	s_free(l->name);
 	free(l);
 	return nil;
 }
@@ -188,20 +141,19 @@
 /*
  *  like lock except don't wait
  */
-extern Mlock *
+Mlock *
 trylock(char *path)
 {
-	Mlock *l;
 	char buf[1];
 	int fd;
+	Mlock *l;
 
-	l = malloc(sizeof(Mlock));
+	l = mallocz(sizeof(Mlock), 1);
 	if(l == 0)
 		return 0;
 
-	l->name = lockname(path);
+	lockname(l, path);
 	if(openlockfile(l) != 0){
-		s_free(l->name);
 		free(l);
 		return 0;
 	}
@@ -217,12 +169,12 @@
 			if(pread(fd, buf, 1, 0) < 0)
 				break;
 		}
-		_exits(0);
+		_exits(nil);
 	}
 	return l;
 }
 
-extern void
+void
 syslockrefresh(Mlock *l)
 {
 	char buf[1];
@@ -230,16 +182,12 @@
 	pread(l->fd, buf, 1, 0);
 }
 
-extern void
+void
 sysunlock(Mlock *l)
 {
 	if(l == 0)
 		return;
-	if(l->name){
-		s_free(l->name);
-	}
-	if(l->fd >= 0)
-		close(l->fd);
+	close(l->fd);
 	if(l->pid > 0)
 		postnote(PNPROC, l->pid, "time to die");
 	free(l);
@@ -254,15 +202,10 @@
  *	w	- writable
  *	A	- append only (doesn't exist in Bio)
  */
-extern Biobuf *
+Biobuf*
 sysopen(char *path, char *mode, ulong perm)
 {
-	int sysperm;
-	int sysmode;
-	int fd;
-	int docreate;
-	int append;
-	int truncate;
+	int sysperm, sysmode, fd, docreate, append, truncate;
 	Dir *d, nd;
 	Biobuf *bp;
 
@@ -274,7 +217,7 @@
 	docreate = 0;
 	append = 0;
 	truncate = 0;
- 	for(; mode && *mode; mode++)
+ 	for(; *mode; mode++)
 		switch(*mode){
 		case 'A':
 			sysmode = OWRITE;
@@ -372,15 +315,6 @@
 }
 
 /*
- *  create a file
- */
-int
-syscreate(char *file, int mode, ulong perm)
-{
-	return create(file, mode, perm);
-}
-
-/*
  *  make a directory
  */
 int
@@ -395,75 +329,44 @@
 }
 
 /*
- *  change the group of a file
- */
-int
-syschgrp(char *file, char *group)
-{
-	Dir nd;
-
-	if(group == 0)
-		return -1;
-	nulldir(&nd);
-	nd.gid = group;
-	return dirwstat(file, &nd);
-}
-
-extern int
-sysdirreadall(int fd, Dir **d)
-{
-	return dirreadall(fd, d);
-}
-
-/*
  *  read in the system name
  */
-extern char *
+char *
 sysname_read(void)
 {
 	static char name[128];
-	char *cp;
+	char *s, *c;
 
-	cp = getenv("site");
-	if(cp == 0 || *cp == 0)
-		cp = alt_sysname_read();
-	if(cp == 0 || *cp == 0)
-		cp = "kremvax";
-	strecpy(name, name+sizeof name, cp);
+	c = s = getenv("site");
+	if(!c)
+		c = alt_sysname_read();
+	if(!c)
+		c = "kremvax";
+	strecpy(name, name+sizeof name, c);
+	free(s);
 	return name;
 }
-extern char *
+
+char *
 alt_sysname_read(void)
 {
-	static char name[128];
-	int n, fd;
-
-	fd = open("/dev/sysname", OREAD);
-	if(fd < 0)
-		return 0;
-	n = read(fd, name, sizeof(name)-1);
-	close(fd);
-	if(n <= 0)
-		return 0;
-	name[n] = 0;
-	return name;
+	return sysname();
 }
 
 /*
  *  get all names
  */
-extern char**
+char**
 sysnames_read(void)
 {
-	static char **namev;
-	Ndbtuple *t, *nt;
 	int n;
-	char *cp;
+	Ndbtuple *t, *nt;
+	static char **namev;
 
 	if(namev)
 		return namev;
 
-	free(csgetvalue(0, "sys", alt_sysname_read(), "dom", &t));
+	free(csgetvalue(0, "sys", sysname(), "dom", &t));
 
 	n = 0;
 	for(nt = t; nt; nt = nt->entry)
@@ -471,13 +374,10 @@
 			n++;
 
 	namev = (char**)malloc(sizeof(char *)*(n+3));
-
 	if(namev){
-		n = 0;
-		namev[n++] = strdup(sysname_read());
-		cp = alt_sysname_read();
-		if(cp)
-			namev[n++] = strdup(cp);
+		namev[0] = strdup(sysname_read());
+		namev[1] = strdup(alt_sysname_read());
+		n = 2;
 		for(nt = t; nt; nt = nt->entry)
 			if(strcmp(nt->attr, "dom") == 0)
 				namev[n++] = strdup(nt->val);
@@ -492,69 +392,21 @@
 /*
  *  read in the domain name
  */
-extern char *
+char*
 domainname_read(void)
 {
-	char **namev;
+	char **p;
 
-	for(namev = sysnames_read(); *namev; namev++)
-		if(strchr(*namev, '.'))
-			return *namev;
+	for(p = sysnames_read(); *p; p++)
+		if(strchr(*p, '.'))
+			return *p;
 	return 0;
 }
 
 /*
- *  return true if the last error message meant file
- *  did not exist.
- */
-extern int
-e_nonexistent(void)
-{
-	rerrstr(err, sizeof(err));
-	return strcmp(err, "file does not exist") == 0;
-}
-
-/*
- *  return true if the last error message meant file
- *  was locked.
- */
-extern int
-e_locked(void)
-{
-	rerrstr(err, sizeof(err));
-	return strcmp(err, "open/create -- file is locked") == 0;
-}
-
-/*
- *  return the length of a file
- */
-extern long
-sysfilelen(Biobuf *fp)
-{
-	Dir *d;
-	long rv;
-
-	d = dirfstat(Bfildes(fp));
-	if(d == nil)
-		return -1;
-	rv = d->length;
-	free(d);
-	return rv;
-}
-
-/*
- *  remove a file
- */
-extern int
-sysremove(char *path)
-{
-	return remove(path);
-}
-
-/*
  *  rename a file, fails unless both are in the same directory
  */
-extern int
+int
 sysrename(char *old, char *new)
 {
 	Dir d;
@@ -579,81 +431,27 @@
 	return dirwstat(old, &d);
 }
 
-/*
- *  see if a file exists
- */
-extern int
+int
 sysexist(char *file)
 {
-	Dir	*d;
-
-	d = dirstat(file);
-	if(d == nil)
-		return 0;
-	free(d);
-	return 1;
+	return access(file, AEXIST) == 0;
 }
 
-/*
- *  return nonzero if file is a directory
- */
-extern int
-sysisdir(char *file)
-{
-	Dir	*d;
-	int	rv;
+static char yankeepig[] = "die: yankee pig dog";
 
-	d = dirstat(file);
-	if(d == nil)
-		return 0;
-	rv = d->mode & DMDIR;
-	free(d);
-	return rv;
-}
-
-/*
- * kill a process or process group
- */
-
-static int
-stomp(int pid, char *file)
-{
-	char name[64];
-	int fd;
-
-	snprint(name, sizeof(name), "/proc/%d/%s", pid, file);
-	fd = open(name, 1);
-	if(fd < 0)
-		return -1;
-	if(write(fd, "die: yankee pig dog\n", sizeof("die: yankee pig dog\n") - 1) <= 0){
-		close(fd);
-		return -1;
-	}
-	close(fd);
-	return 0;
-	
-}
-
-/*
- *  kill a process
- */
-extern int
+int
 syskill(int pid)
 {
-	return stomp(pid, "note");
-	
+	return postnote(PNPROC, pid, yankeepig);
 }
 
-/*
- *  kill a process group
- */
-extern int
+int
 syskillpg(int pid)
 {
-	return stomp(pid, "notepg");
+	return postnote(PNGROUP, pid, yankeepig);
 }
 
-extern int
+int
 sysdetach(void)
 {
 	if(rfork(RFENVG|RFNAMEG|RFNOTEG) < 0) {
@@ -668,11 +466,10 @@
  */
 static int *closedflag;
 static int
-catchpipe(void *a, char *msg)
+catchpipe(void *, char *msg)
 {
 	static char *foo = "sys: write on closed pipe";
 
-	USED(a);
 	if(strncmp(msg, foo, strlen(foo)) == 0){
 		if(closedflag)
 			*closedflag = 1;
@@ -692,30 +489,21 @@
 	atnotify(catchpipe, 0);
 }
 
-void
-exit(int i)
-{
-	char buf[32];
-
-	if(i == 0)
-		exits(0);
-	snprint(buf, sizeof(buf), "%d", i);
-	exits(buf);
-}
-
-static int
+int
 islikeatty(int fd)
 {
 	char buf[64];
+	int l;
 
 	if(fd2path(fd, buf, sizeof buf) != 0)
 		return 0;
 
 	/* might be /mnt/term/dev/cons */
-	return strlen(buf) >= 9 && strcmp(buf+strlen(buf)-9, "/dev/cons") == 0;
+	l = strlen(buf);
+	return l >= 9 && strcmp(buf+l-9, "/dev/cons") == 0;
 }
 
-extern int
+int
 holdon(void)
 {
 	int fd;
@@ -729,13 +517,13 @@
 	return fd;
 }
 
-extern int
+int
 sysopentty(void)
 {
 	return open("/dev/cons", ORDWR);
 }
 
-extern void
+void
 holdoff(int fd)
 {
 	write(fd, "holdoff", 7);
@@ -742,7 +530,7 @@
 	close(fd);
 }
 
-extern int
+int
 sysfiles(void)
 {
 	return 128;
@@ -754,142 +542,66 @@
  *  if the path starts with / or ./, don't change it
  *
  */
-extern String *
-mboxpath(char *path, char *user, String *to, int dot)
+char*
+mboxpathbuf(char *to, int n, char *user, char *path)
 {
-	if (dot || *path=='/' || strncmp(path, "./", 2) == 0
-			      || strncmp(path, "../", 3) == 0) {
-		to = s_append(to, path);
-	} else {
-		to = s_append(to, MAILROOT);
-		to = s_append(to, "/box/");
-		to = s_append(to, user);
-		to = s_append(to, "/");
-		to = s_append(to, path);
-	}
+	if(*path == '/' || !strncmp(path, "./", 2) || !strncmp(path, "../", 3))
+		snprint(to, n, "%s", path);
+	else
+		snprint(to, n, "%s/box/%s/%s", MAILROOT, user, path);
 	return to;
 }
 
-extern String *
-mboxname(char *user, String *to)
+/*
+ * warning: we're not quoting bad characters.  we're not encoding
+ * non-ascii characters.  basically this function sucks.  don't use.
+ */
+char*
+username0(Biobuf *b, char *from)
 {
-	return mboxpath("mbox", user, to, 0);
-}
-
-extern String *
-deadletter(String *to)		/* pass in sender??? */
-{
-	char *cp;
-
-	cp = getlog();
-	if(cp == 0)
-		return 0;
-	return mboxpath("dead.letter", cp, to, 0);
-}
-
-char *
-homedir(char *user)
-{
-	USED(user);
-	return getenv("home");
-}
-
-String *
-readlock(String *file)
-{
-	char *cp;
-
-	cp = getlog();
-	if(cp == 0)
-		return 0;
-	return mboxpath("reading", cp, file, 0);
-}
-
-String *
-username(String *from)
-{
+	char *p, *f[6];
 	int n;
-	Biobuf *bp;
-	char *p, *q;
-	String *s;
+	static char buf[32];
 
-	bp = Bopen("/adm/keys.who", OREAD);
-	if(bp == 0)
-		bp = Bopen("/adm/netkeys.who", OREAD);
-	if(bp == 0)
-		return 0;
-
-	s = 0;
-	n = strlen(s_to_c(from));
-	for(;;) {
-		p = Brdline(bp, '\n');
+	n = strlen(from);
+	buf[0] = 0;
+	for(;; free(p)) {
+		p = Brdstr(b, '\n', 1);
 		if(p == 0)
 			break;
-		p[Blinelen(bp)-1] = 0;
-		if(strncmp(p, s_to_c(from), n))
+		if(strncmp(p, from, n)  || p[n] != '|')
 			continue;
-		p += n;
-		if(*p != ' ' && *p != '\t')	/* must be full match */
+		if(getfields(p, f, nelem(f), 0, "|") < 3)
 			continue;
-		while(*p && (*p == ' ' || *p == '\t'))
-				p++;
-		if(*p == 0)
-			continue;
-		for(q = p; *q; q++)
-			if(('0' <= *q && *q <= '9') || *q == '<')
-				break;
-		while(q > p && q[-1] != ' ' && q[-1] != '\t')
-			q--;
-		while(q > p && (q[-1] == ' ' || q[-1] == '\t'))
-			q--;
-		*q = 0;
-		s = s_new();
-		s_append(s, "\"");
-		s_append(s, p);
-		s_append(s, "\"");
-		break;
+		snprint(buf, sizeof buf, "\"%s\"", f[2]);
+		/* no break; last match wins */
 	}
-	Bterm(bp);
-	return s;
+	return buf[0]? buf: 0;
 }
 
-char *
-remoteaddr(int fd, char *dir)
+char*
+username(char *from)
 {
-	char buf[128], *p;
-	int n;
+	char *s;
+	Biobuf *b;
 
-	if(dir == 0){
-		if(fd2path(fd, buf, sizeof(buf)) != 0)
-			return "";
-
-		/* parse something of the form /net/tcp/nnnn/data */
-		p = strrchr(buf, '/');
-		if(p == 0)
-			return "";
-		strncpy(p+1, "remote", sizeof(buf)-(p-buf)-2);
-	} else
-		snprint(buf, sizeof buf, "%s/remote", dir);
-	buf[sizeof(buf)-1] = 0;
-
-	fd = open(buf, OREAD);
-	if(fd < 0)
-		return "";
-	n = read(fd, buf, sizeof(buf)-1);
-	close(fd);
-	if(n > 0){
-		buf[n] = 0;
-		p = strchr(buf, '!');
-		if(p)
-			*p = 0;
-		return strdup(buf);
+	s = 0;
+	if(b = Bopen("/adm/keys.who", OREAD)){
+		s = username0(b, from);
+		Bterm(b);
 	}
-	return "";
+	if(s == 0 && (b = Bopen("/adm/netkeys.who", OREAD))){
+		s = username0(b, from);
+		Bterm(b);
+	}
+	return s;
 }
 
-//  create a file and 
-//	1) ensure the modes we asked for
-//	2) make gid == uid
+/*
+ * create a file and 
+ *	1) ensure the modes we asked for
+ *	2) make gid == uid
+ */
 static int
 docreate(char *file, int perm)
 {
@@ -897,7 +609,7 @@
 	Dir ndir;
 	Dir *d;
 
-	//  create the mbox
+	/*  create the mbox */
 	fd = create(file, OREAD, perm);
 	if(fd < 0){
 		fprint(2, "couldn't create %s\n", file);
@@ -917,55 +629,77 @@
 	return 0;
 }
 
-//  create a mailbox
-int
-creatembox(char *user, char *folder)
+static int
+createfolder0(char *user, char *folder, char *ftype)
 {
-	char *p;
-	String *mailfile;
-	char buf[512];
-	Mlock *ml;
+	char *p, *s, buf[Pathlen];
+	int isdir, mode;
+	Dir *d;
 
-	mailfile = s_new();
-	if(folder == 0)
-		mboxname(user, mailfile);
-	else {
-		snprint(buf, sizeof(buf), "%s/mbox", folder);
-		mboxpath(buf, user, mailfile, 0);
-	}
-
-	// don't destroy existing mailbox
-	if(access(s_to_c(mailfile), 0) == 0){
-		fprint(2, "mailbox already exists\n");
+	assert(folder != 0);
+	mboxpathbuf(buf, sizeof buf, user, folder);
+	if(access(buf, 0) == 0){
+		fprint(2, "%s already exists\n", ftype);
 		return -1;
 	}
-	fprint(2, "creating new mbox: %s\n", s_to_c(mailfile));
+	fprint(2, "creating new %s: %s\n", ftype, buf);
 
-	//  make sure preceding levels exist
-	for(p = s_to_c(mailfile); p; p++) {
-		if(*p == '/')	/* skip leading or consecutive slashes */
+	/*
+	 * if we can deliver to this mbox, it needs
+	 * to be read/execable all the way down
+	 */
+	mode = 0711;
+	if(!strncmp(buf, "/mail/box/", 10))
+	if((s = strrchr(buf, '/')) && !strcmp(s+1, "mbox"))
+		mode = 0755;
+	for(p = buf; p; p++) {
+		if(*p == '/')
 			continue;
 		p = strchr(p, '/');
 		if(p == 0)
 			break;
 		*p = 0;
-		if(access(s_to_c(mailfile), 0) != 0){
-			if(docreate(s_to_c(mailfile), DMDIR|0711) < 0)
-				return -1;
-		}
+		if(access(buf, 0) != 0)
+		if(docreate(buf, DMDIR|mode) < 0)
+			return -1;
 		*p = '/';
 	}
+	/* must match folder.c:/^openfolder */
+	isdir = create(buf, OREAD, DMDIR|0777);
 
-	//  create the mbox
-	if(docreate(s_to_c(mailfile), 0622|DMAPPEND|DMEXCL) < 0)
-		return -1;
-
 	/*
-	 *  create the lock file if it doesn't exist
+	 *  make sure everyone can write here if it's a mailbox
+	 * rather than a folder
 	 */
-	ml = trylock(s_to_c(mailfile));
-	if(ml != nil)
-		sysunlock(ml);
+	if(mode == 0755)
+	if(isdir >= 0 && (d = dirfstat(isdir))){
+		d->mode |= 0773;
+		dirfwstat(isdir, d);
+		free(d);
+	}
 
+	if(isdir == -1){
+		fprint(2, "can't create %s: %s\n", ftype, buf);
+		return -1;
+	}
+	close(isdir);
 	return 0;
+}
+
+int
+createfolder(char *user, char *folder)
+{
+	return createfolder0(user, folder, "folder");
+}
+
+int
+creatembox(char *user, char *mbox)
+{
+	char buf[Pathlen];
+
+	if(mbox == 0)
+		snprint(buf, sizeof buf, "mbox");
+	else
+		snprint(buf, sizeof buf, "%s/mbox", mbox);
+	return createfolder0(user, buf, "mbox");
 }
--- a/sys/src/cmd/upas/common/mail.c
+++ /dev/null
@@ -1,57 +1,0 @@
-#include "common.h"
-
-/* format of REMOTE FROM lines */
-char *REMFROMRE =
-	"^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)[ \t]+remote[ \t]+from[ \t]+(.*)\n$";
-int REMSENDERMATCH = 1;
-int REMDATEMATCH = 4;
-int REMSYSMATCH = 5;
-
-/* format of LOCAL FROM lines */
-char *FROMRE =
-	"^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)\n$";
-int SENDERMATCH = 1;
-int DATEMATCH = 4;
-
-/* output a unix style local header */
-int
-print_header(Biobuf *fp, char *sender, char *date)
-{
-	return Bprint(fp, "From %s %s\n", sender, date);
-}
-
-/* output a unix style remote header */
-int
-print_remote_header(Biobuf *fp, char *sender, char *date, char *system)
-{
-	return Bprint(fp, "From %s %s remote from %s\n", sender, date, system);
-}
-
-/* parse a mailbox style header */
-int
-parse_header(char *line, String *sender, String *date)
-{
-	if (!IS_HEADER(line))
-		return -1;
-	line += sizeof("From ") - 1;
-	s_restart(sender);
-	while(*line==' '||*line=='\t')
-		line++;
-	if(*line == '"'){
-		s_putc(sender, *line++);
-		while(*line && *line != '"')
-			s_putc(sender, *line++);
-		s_putc(sender, *line++);
-	} else {
-		while(*line && *line != ' ' && *line != '\t')
-			s_putc(sender, *line++);
-	}
-	s_terminate(sender);
-	s_restart(date);
-	while(*line==' '||*line=='\t')
-		line++;
-	while(*line)
-		s_putc(date, *line++);
-	s_terminate(date);
-	return 0;
-}
--- a/sys/src/cmd/upas/common/makefile
+++ /dev/null
@@ -1,18 +1,0 @@
-CFLAGS=${UNIX} -g -I. -I../libc -I../common -I/usr/include ${SCFLAGS}
-OBJS=mail.o aux.o string.o ${SYSOBJ}
-AR=ar
-.c.o: ; ${CC} -c ${CFLAGS} $*.c
-
-common.a: ${OBJS}
-	${AR} cr common.a ${OBJS}
-	-ranlib common.a
-
-aux.o:		aux.h string.h mail.h
-string.o:	string.h mail.h
-mail.o:		mail.h
-syslog.o:	sys.h
-mail.h:		sys.h
-
-clean:
-	-rm -f *.[oO] core a.out *.a *.sL common.a
-
--- a/sys/src/cmd/upas/common/mkfile
+++ b/sys/src/cmd/upas/common/mkfile
@@ -1,13 +1,17 @@
 </$objtype/mkfile
+<../mkupas
 
 LIB=libcommon.a$O
-OFILES=aux.$O\
+OFILES=\
+	aux.$O\
 	become.$O\
-	mail.$O\
-	process.$O\
-	libsys.$O\
 	config.$O\
-	appendfiletombox.$O\
+	folder.$O\
+	flags.$O\
+	fmt.$O\
+	libsys.$O\
+	process.$O\
+	totm.$O\
 
 HFILES=common.h\
 	sys.h\
@@ -18,3 +22,7 @@
 	$HFILES\
 
 </sys/src/cmd/mklib
+
+nuke:V:
+	mk clean
+	rm -f libcommon.a[$OS]
--- a/sys/src/cmd/upas/common/process.c
+++ b/sys/src/cmd/upas/common/process.c
@@ -96,8 +96,7 @@
 		if(who)
 			become(av, who);
 		exec(av[0], av);
-		perror("proc_start");
-		exits("proc_start");
+		sysfatal("proc_start");
 	default:
 		for (i=0; i<3; i++)
 			if (pp->std[i] != 0) {
@@ -126,12 +125,12 @@
 proc_wait(process *pp)
 {
 	Waitmsg *status;
-	char err[Errlen];
+	char err[ERRMAX];
 
 	for(;;){
 		status = wait();
 		if(status == nil){
-			rerrstr(err, sizeof(err));
+			errstr(err, sizeof(err));
 			if(strstr(err, "interrupt") == 0)
 				break;
 		}
@@ -161,13 +160,13 @@
 	if (pp->pid >= 0)
 		proc_wait(pp);
 	free(pp->waitmsg);
-	free((char *)pp);
+	free(pp);
 	return 0;
 }
 
 /* kill a process */
-extern int
-proc_kill(process *pp)
-{
-	return syskill(pp->pid);
-}
+//extern int
+//proc_kill(process *pp)
+//{
+//	return syskill(pp->pid);
+//}
--- a/sys/src/cmd/upas/common/sys.h
+++ b/sys/src/cmd/upas/common/sys.h
@@ -1,12 +1,6 @@
-/*
- * System dependent header files for research
- */
-
 #include <u.h>
 #include <libc.h>
-#include <regexp.h>
 #include <bio.h>
-#include "String.h"
 
 /*
  *  for the lock routines in libsys.c
@@ -13,9 +7,9 @@
  */
 typedef struct Mlock	Mlock;
 struct Mlock {
-	int fd;
-	int pid;
-	String *name;
+	int	fd;
+	int	pid;
+	char	name[Pathlen];
 };
 
 /*
@@ -22,64 +16,48 @@
  *  from config.c
  */
 extern char *MAILROOT;	/* root of mail system */
+extern char *SPOOL;	/* spool directory; for spam ctl */
 extern char *UPASLOG;	/* log directory */
 extern char *UPASLIB;	/* upas library directory */
 extern char *UPASBIN;	/* upas binary directory */
 extern char *UPASTMP;	/* temporary directory */
 extern char *SHELL;	/* path name of shell */
-extern char *POST;	/* path name of post server addresses */
-extern int MBOXMODE;	/* default mailbox protection mode */
 
+enum {
+	Mboxmode	= 0622,
+};
+
 /*
  *  files in libsys.c
  */
-extern char	*sysname_read(void);
-extern char	*alt_sysname_read(void);
-extern char	*domainname_read(void);
-extern char	**sysnames_read(void);
-extern char	*getlog(void);
-extern char	*thedate(void);
-extern Biobuf	*sysopen(char*, char*, ulong);
-extern int	sysopentty(void);
-extern int	sysclose(Biobuf*);
-extern int	sysmkdir(char*, ulong);
-extern int	syschgrp(char*, char*);
-extern Mlock	*syslock(char *);
-extern void	sysunlock(Mlock *);
-extern void	syslockrefresh(Mlock *);
-extern int	e_nonexistent(void);
-extern int	e_locked(void);
-extern long	sysfilelen(Biobuf*);
-extern int	sysremove(char*);
-extern int	sysrename(char*, char*);
-extern int	sysexist(char*);
-extern int	sysisdir(char*);
-extern int	syskill(int);
-extern int	syskillpg(int);
-extern int	syscreate(char*, int, ulong);
-extern Mlock	*trylock(char *);
-extern void	exit(int);
-extern void	pipesig(int*);
-extern void	pipesigoff(void);
-extern int	holdon(void);
-extern void	holdoff(int);
-extern int	syscreatelocked(char*, int, int);
-extern int	sysopenlocked(char*, int);
-extern int	sysunlockfile(int);
-extern int	sysfiles(void);
-extern int 	become(char**, char*);
-extern int	sysdetach(void);
-extern int	sysdirreadall(int, Dir**);
-extern String	*username(String*);
-extern char*	remoteaddr(int, char*);
-extern int	creatembox(char*, char*);
-
-extern String	*readlock(String*);
-extern char	*homedir(char*);
-extern String	*mboxname(char*, String*);
-extern String	*deadletter(String*);
-
-/*
- *  maximum size for a file path
- */
-#define MAXPATHLEN 128
+char	*sysname_read(void);
+char	*alt_sysname_read(void);
+char	*domainname_read(void);
+char	**sysnames_read(void);
+char	*getlog(void);
+char	*thedate(void);
+Biobuf	*sysopen(char*, char*, ulong);
+int	sysopentty(void);
+int	sysclose(Biobuf*);
+int	sysmkdir(char*, ulong);
+Mlock	*syslock(char *);
+void	sysunlock(Mlock *);
+void	syslockrefresh(Mlock *);
+int	sysrename(char*, char*);
+int	sysexist(char*);
+int	syskill(int);
+int	syskillpg(int);
+Mlock	*trylock(char *);
+void	pipesig(int*);
+void	pipesigoff(void);
+int	holdon(void);
+void	holdoff(int);
+int	syscreatelocked(char*, int, int);
+int	sysopenlocked(char*, int);
+int	sysunlockfile(int);
+int	sysfiles(void);
+int 	become(char**, char*);
+int	sysdetach(void);
+char	*username(char*);
+int	creatembox(char*, char*);
+int	createfolder(char*, char*);
--- /dev/null
+++ b/sys/src/cmd/upas/common/totm.c
@@ -1,0 +1,36 @@
+#include <common.h>
+
+static char mtab[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
+
+int
+ctimetotm(char *s, Tm *tm)
+{
+	char buf[32];
+
+	if(strlen(s) < 28)
+		return -1;
+	snprint(buf, sizeof buf, "%s", s);
+	memset(tm, 0, sizeof *tm);
+	buf[7] = 0;
+	tm->mon = (strstr(mtab, buf+4) - mtab)/3;
+	tm->mday = atoi(buf+8);
+	tm->hour = atoi(buf+11);
+	tm->min = atoi(buf+14);
+	tm->sec = atoi(buf+17);
+	tm->zone[0] = buf[20];
+	tm->zone[1] = buf[21];
+	tm->zone[2] = buf[22];
+	tm->year = atoi(buf+24) - 1900;
+	return 0;
+}
+
+int
+fromtotm(char *s, Tm *tm)
+{
+	char buf[256], *f[3];
+
+	snprint(buf, sizeof buf, "%s", s);
+	if(getfields(buf, f, nelem(f), 0, " ") != 3)
+		return -1;
+	return ctimetotm(f[2], tm);
+}
--- a/sys/src/cmd/upas/filterkit/deliver.c
+++ b/sys/src/cmd/upas/filterkit/deliver.c
@@ -7,7 +7,7 @@
 void
 usage(void)
 {
-	fprint(2, "usage: %s recipient fromfile mbox\n", argv0);
+	fprint(2, "usage: deliver recipient fromaddr-file mbox\n");
 	exits("usage");
 }
 
@@ -14,57 +14,25 @@
 void
 main(int argc, char **argv)
 {
-	int bytes, fd, i;
-	char now[30];
-	char *deliveredto;
+	char *to, *s;
+	int r;
+	long l;
 	Addr *a;
-	Mlock *l;
 
 	ARGBEGIN{
 	}ARGEND;
-
 	if(argc != 3)
 		usage();
-
-	deliveredto = strrchr(argv[0], '!');
-	if(deliveredto == nil)
-		deliveredto = argv[0];
+	if(to = strrchr(argv[0], '!'))
+		to++;
 	else
-		deliveredto++;
+		to = argv[0];
 	a = readaddrs(argv[1], nil);
 	if(a == nil)
 		sysfatal("missing from address");
-
-	l = syslock(argv[2]);
-
-	/* append to mbox */
-	i = 0;
-retry:
-	fd = open(argv[2], OWRITE);
-	if(fd < 0){
-		rerrstr(now, sizeof(now));
-		if(strstr(now, "exclusive lock") && i++ < 20){
-			sleep(500);	/* wait for lock to go away */
-			goto retry;
-		}
-		sysfatal("opening mailbox: %r");
-	}
-	seek(fd, 0, 2);
-	strncpy(now, ctime(time(0)), sizeof(now));
-	now[28] = 0;
-	if(fprint(fd, "From %s %s\n", a->val, now) < 0)
-		sysfatal("writing mailbox: %r");
-
-	/* copy message handles escapes and any needed new lines */
-	bytes = appendfiletombox(0, fd);
-	if(bytes < 0)
-		sysfatal("writing mailbox: %r");
-
-	close(fd);
-	sysunlock(l);
-
-	/* log it */
-	syslog(0, "mail", "delivered %s From %s %s (%s) %d", deliveredto,
-		a->val, now, argv[0], bytes);
-	exits(0);
+	s = ctime(l = time(0));
+	werrstr("");
+	r = fappendfolder(a->val, l, argv[2], 0);
+	syslog(0, "mail", "delivered %s From %s %.28s (%s) %d %r", to, a->val, s, argv[0], r);
+	exits("");
 }
--- a/sys/src/cmd/upas/filterkit/list.c
+++ b/sys/src/cmd/upas/filterkit/list.c
@@ -2,8 +2,8 @@
 #include <libc.h>
 #include <regexp.h>
 #include <libsec.h>
-#include <String.h>
 #include <bio.h>
+#include <String.h>
 #include "dat.h"
 
 int debug;
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/mbappend.c
@@ -1,0 +1,63 @@
+/*
+ * deliver to one's own folder with locking & logging
+ */
+#include "dat.h"
+#include "common.h"
+
+void
+append(int fd, char *mb, char *from, long t)
+{
+	char *folder, *s;
+	int r;
+
+	s = ctime(t);
+	folder = foldername(from, getuser(), mb);
+	r = fappendfolder(0, t, folder, fd);
+	if(r == 0)
+		werrstr("");
+	syslog(0, "mail", "mbappend %s %.28s (%s) %r", mb, s, folder);
+	if(r)
+		exits("fail");
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: mbappend [-t time] [-f from] mbox [file ...]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *mb, *from;
+	int fd;
+	long t;
+
+	from = nil;
+	t = time(0);
+	ARGBEGIN{
+	case 't':
+		t = strtoul(EARGF(usage()), 0, 0);
+		break;
+	case 'f':
+		from = EARGF(usage());
+		break;
+	default:
+		usage();
+	}ARGEND;
+	if(*argv == 0)
+		usage();
+	werrstr("");
+	mb = *argv++;
+	if(*argv == 0)
+		append(0, mb, from, t);
+	else for(; *argv; argv++){
+		fd = open(*argv, OREAD);
+		if(fd < 0)
+			sysfatal("open: %r");
+		append(fd, mb, from, t);
+		close(fd);
+	}
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/mbcreate.c
@@ -1,0 +1,32 @@
+#include "dat.h"
+#include "common.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: mbcreate [-f] ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int r;
+	int (*f)(char*, char*);
+
+	f = creatembox;
+	ARGBEGIN{
+	case 'f':
+		f = createfolder;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	r = 0;
+	for(; *argv; argv++)
+		r |= f(getuser(), *argv);
+	if(r)
+		exits("errors");
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/mbremove.c
@@ -1,0 +1,243 @@
+/*
+ * why did i write this and not use upas/fs?
+ */
+#include "dat.h"
+#include "common.h"
+
+int	qflag;
+int	pflag;
+int	rflag;
+int	tflag;
+int	vflag;
+
+/* must be [0-9]+(\..*)? */
+static int
+dirskip(Dir *a, uvlong *uv)
+{
+	char *p;
+
+	if(a->length == 0)
+		return 1;
+	*uv = strtoul(a->name, &p, 0);
+	if(*uv < 1000000 || *p != '.')
+		return 1;
+	*uv = *uv<<8 | strtoul(p+1, &p, 10);
+	if(*p)
+		return 1;
+	return 0;
+}
+
+static int
+ismbox(char *path)
+{
+	char buf[512];
+	int fd, r;
+
+	fd = open(path, OREAD);
+	if(fd == -1)
+		return 0;
+	r = 1;
+	if(read(fd, buf, sizeof buf) < 28+5)
+		r = 0;
+	else if(strncmp(buf, "From ", 5))
+		r = 0;
+	close(fd);
+	return r;
+}
+
+int
+isindex(Dir *d)
+{
+	char *p;
+
+	p = strrchr(d->name, '.');
+	if(!p)
+		return -1;
+	if(strcmp(p, ".idx") || strcmp(p, ".imp"))
+		return 1;
+	return 0;
+}
+
+int
+idiotcheck(char *path, Dir *d, int getindex)
+{
+	uvlong v;
+
+	if(d->mode & DMDIR)
+		return 0;
+	if(!strncmp(d->name, "L.", 2))
+		return 0;
+	if(getindex && isindex(d))
+		return 0;
+	if(!dirskip(d, &v) || ismbox(path))
+		return 0;
+	return -1;
+}
+
+int
+vremove(char *buf)
+{
+	if(vflag)
+		fprint(2, "rm %s\n", buf);
+	if(!pflag)
+		return remove(buf);
+	return 0;
+}
+
+int
+rm(char *dir, int level)
+{
+	char buf[Pathlen];
+	int i, n, r, fd, isdir;
+	Dir *d;
+
+	d = dirstat(dir);
+	isdir = d->mode & DMDIR;
+	free(d);
+	if(!isdir)
+		return 0;
+	fd = open(dir, OREAD);
+	if(fd == -1)
+		return -1;
+	n = dirreadall(fd, &d);
+	close(fd);
+	r = 0;
+	for(i = 0; i < n; i++){
+		snprint(buf, sizeof buf, "%s/%s", dir, d[i].name);
+		if(rflag)
+			r |= rm(buf, level+1);
+		if(idiotcheck(buf, d+i, level+rflag) == -1)
+			continue;
+		if(vremove(buf) != 0)
+			r = -1;
+	}
+	free(d);
+	return r;
+}
+
+void
+nukeidx(char *buf)
+{
+	char buf2[Pathlen];
+
+	snprint(buf2, sizeof buf2, "%s.idx", buf);
+	vremove(buf2);
+	snprint(buf2, sizeof buf2, "%s.imp", buf);
+	vremove(buf2);
+}
+
+void
+truncidx(char *buf)
+{
+	char buf2[Pathlen];
+
+	snprint(buf2, sizeof buf2, "%s.idx", buf);
+	vremove(buf2);
+//	snprint(buf2, sizeof buf2, "%s.imp", buf);
+//	vremove(buf2);
+}
+
+static int
+removefolder0(char *user, char *folder, char *ftype)
+{
+	char *msg, buf[Pathlen];
+	int r, isdir;
+	Dir *d;
+
+	assert(folder != 0);
+	mboxpathbuf(buf, sizeof buf, user, folder);
+	if((d = dirstat(buf)) == 0){
+		fprint(2, "%s: %s doesn't exist\n", buf, ftype);
+		return 0;
+	}
+	isdir = d->mode & DMDIR;
+	free(d);
+	msg = "deleting";
+	if(tflag)
+		msg = "truncating";
+	fprint(2, "%s %s: %s\n", msg, ftype, buf);
+
+	/* must match folder.c:/^openfolder */
+	r = rm(buf, 0);
+	if(!tflag)
+		r = vremove(buf);
+	else if(!isdir)
+		r = open(buf, OWRITE|OTRUNC);
+
+	if(tflag)
+		truncidx(buf);
+	else
+		nukeidx(buf);
+
+	if(r == -1){
+		fprint(2, "%s: can't %s %s\n", buf, msg, ftype);
+		return -1;
+	}
+	close(r);
+	return 0;
+}
+
+int
+removefolder(char *user, char *folder)
+{
+	return removefolder0(user, folder, "folder");
+}
+
+int
+removembox(char *user, char *mbox)
+{
+	char buf[Pathlen];
+
+	if(mbox == 0)
+		snprint(buf, sizeof buf, "mbox");
+	else
+		snprint(buf, sizeof buf, "%s/mbox", mbox);
+	return removefolder0(user, buf, "mbox");
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: mbremove [-fpqrtv] ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int r;
+	int (*f)(char*, char*);
+
+	f = removembox;
+	ARGBEGIN{
+	case 'f':
+		f = removefolder;
+		break;
+	case 'p':
+		pflag++;
+		break;
+	case 'q':
+		qflag++;
+		close(2);
+		open("/dev/null", OWRITE);
+		break;
+	case 'r':
+		rflag++;
+		break;
+	case 't':
+		tflag++;
+		break;
+	case 'v':
+		vflag++;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	r = 0;
+	for(; *argv; argv++)
+		r |= f(getuser(), *argv);
+	if(r)
+		exits("errors");
+	exits("");
+}
--- a/sys/src/cmd/upas/filterkit/mkfile
+++ b/sys/src/cmd/upas/filterkit/mkfile
@@ -1,13 +1,14 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=\
-	token\
-	list\
 	deliver\
+	list\
+	mbappend\
+	token\
 
 LIB=../common/libcommon.a$O\
 
-BIN=/$objtype/bin/upas
 OFILES=readaddrs.$O
 UPDATE=\
 	mkfile\
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/testemail
@@ -1,0 +1,4 @@
+From: erik quanstrom <[email protected]>
+Subject: 1 testing
+
+testing
--- a/sys/src/cmd/upas/filterkit/token.c
+++ b/sys/src/cmd/upas/filterkit/token.c
@@ -1,60 +1,50 @@
 #include <u.h>
 #include <libc.h>
 #include <libsec.h>
-#include <String.h>
 #include "dat.h"
 
 void
 usage(void)
 {
-	fprint(2, "usage: %s key [token [file]]\n", argv0);
+	fprint(2, "usage: token key [token]\n");
 	exits("usage");
 }
 
-static String*
-mktoken(char *key, long thetime)
+static char*
+mktoken(char *key, long t)
 {
-	char *now;
+	char *now, token[64];
 	uchar digest[SHA1dlen];
-	char token[64];
-	String *s;
-	
-	now = ctime(thetime);
+
+	now = ctime(t);
 	memset(now+11, ':', 8);
 	hmac_sha1((uchar*)now, strlen(now), (uchar*)key, strlen(key), digest, nil);
 	enc64(token, sizeof token, digest, sizeof digest);
-	s = s_new();
-	s_nappend(s, token, 5);
-	return s;
+	return smprint("%.5s", token);
 }
 
 static char*
 check_token(char *key, char *file)
 {
-	String *s;
+	char *s, buf[1024];
+	int i, fd, m;
 	long now;
-	int i;
-	char buf[1024];
-	int fd;
 
 	fd = open(file, OREAD);
 	if(fd < 0)
 		return "no match";
-	i = read(fd, buf, sizeof(buf)-1);
+	i = read(fd, buf, sizeof buf-1);
 	close(fd);
 	if(i < 0)
 		return "no match";
 	buf[i] = 0;
-	
 	now = time(0);
-
 	for(i = 0; i < 14; i++){
 		s = mktoken(key, now-24*60*60*i);
-		if(strstr(buf, s_to_c(s)) != nil){
-			s_free(s);
+		m = s != nil && strstr(buf, s) != nil;
+		free(s);
+		if(m)
 			return nil;
-		}
-		s_free(s);
 	}
 	return "no match";
 }
@@ -62,10 +52,7 @@
 static char*
 create_token(char *key)
 {
-	String *s;
-
-	s = mktoken(key, time(0));
-	print("%s", s_to_c(s));
+	print("%s", mktoken(key, time(0)));
 	return nil;
 }
 
@@ -78,10 +65,8 @@
 	switch(argc){
 	case 2:
 		exits(check_token(argv[0], argv[1]));
-		break;
 	case 1:
 		exits(create_token(argv[0]));
-		break;
 	default:
 		usage();
 	}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/bos.c
@@ -1,0 +1,40 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+/*
+ * assume:
+ * - the stack segment can't be resized
+ * - stacks may not be segattached (by name Stack, anyway)
+ * - no thread library
+ */
+uintptr
+absbos(void)
+{
+	char buf[64], *f[10], *s, *r;
+	int n;
+	uintptr p, q;
+	Biobuf *b;
+
+	p = 0xd0000000;	/* guess pc kernel */
+	snprint(buf, sizeof buf, "/proc/%ud/segment", getpid());
+	b = Bopen(buf, OREAD);
+	if(b == nil)
+		return p;
+	for(; s = Brdstr(b, '\n', 1); free(s)){
+		if((n = tokenize(s, f, nelem(f))) < 3)
+			continue;
+		if(strcmp(f[0], "Stack") != 0)
+			continue;
+		/*
+		 * addressing from end because segment
+		 * flags could become discontiguous  if
+		 * additional flags are added
+		 */
+		q = strtoull(f[n - 3], &r, 16);
+		if(*r == 0 && (char*)q > end)
+			p = q;
+	}
+	Bterm(b);
+	return p;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/cache.c
@@ -1,0 +1,552 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+int
+findcache(Mcache *c, Message *m)
+{
+	int i;
+
+	for(i = 0; i < c->ntab; i++)
+		if(c->ctab[i] == m)
+			return i;
+	return -1;
+}
+
+static void
+prcache(Mcache *c, char *prefix)
+{
+	int j;
+	Message *m;
+
+	if(!debug)
+		return;
+	for(j = 0; j < c->ntab; j++){
+		m = c->ctab[j];
+		dprint("%s%d/%s\t%p\t%d\t%ld\n", prefix, j, m->name, m, m->refs, m->csize);
+	}
+}
+
+/* debugging only */
+static void
+dupchk(Mcache *c)
+{
+	int i, j;
+
+	if(!debug)
+		return;
+	for(i = 0; i < c->ntab; i++)
+		for(j = i + 1; j < c->ntab; j++)
+			if(c->ctab[i] == c->ctab[j])
+				goto lose;
+	return;
+lose:
+	for(j = 0; j < c->ntab; j++)
+		dprint("%d\t%p	%d\t%ld\n", j, c->ctab[j], c->ctab[j]->refs, c->ctab[j]->size);
+	abort();
+}
+	
+int
+addcache(Mcache *c, Message *m)
+{
+	int i;
+
+	if((i = findcache(c, m)) < 0){
+		if(c->ntab + 1 == nelem(c->ctab))
+			abort();
+		i = c->ntab++;
+	}else{
+		/* rotate */
+		if(i == c->ntab - 1)
+			return i;		/* silly shortcut to prevent excessive printage. */
+		dprint("addcache rotate %d %d\n", i, c->ntab);
+		prcache(c, "");
+		memmove(c->ctab + i, c->ctab + i + 1, (c->ntab - i - 1)*sizeof c->ctab[0]);
+		i = c->ntab - 1;
+c->ctab[i] = m;
+dupchk(c);
+	}
+	dprint("addcache %d %d 	%p\n", i, c->ntab, m);
+	c->ctab[i] = m;
+	return i;
+}
+
+static void
+notecache(Mailbox *mb, Message *m, long sz)
+{
+	assert(Topmsg(mb, m));
+	assert(sz >= 0 && sz < Maxmsg);
+	m->csize += sz;
+	mb->cached += sz;
+	addcache(mb, m);
+}
+
+static long
+cachefree0(Mailbox *mb, Message *m, int force)
+{
+	long sz, i;
+	Message *s;
+
+	if(!force && !mb->fetch)
+		return 0;
+	for(s = m->part; s; s = s->next)
+		cachefree(mb, s, force);
+	dprint("cachefree: %D	%p,	%p\n", m->fileid, m, m->start);
+	if(m->mallocd){
+		free(m->start);
+		m->mallocd = 0;
+	}
+	if(m->ballocd){
+		free(m->body);
+		m->ballocd = 0;
+	}
+	if(m->hallocd){
+		free(m->header);
+		m->hallocd = 0;
+	}
+	for(i = 0; i < nelem(m->references); i++){
+		free(m->references[i]);
+		m->references[i] = 0;
+	}
+	sz = m->csize;
+	m->csize = 0;
+	m->start = 0;
+	m->end = 0;
+	m->header = 0;
+	m->hend = 0;
+	m->hlen = -1;
+	m->body = 0;
+	m->bend = 0;
+	m->mheader = 0;
+	m->mhend = 0;
+	if(mb->decache)
+		mb->decache(mb, m);
+	m->decoded = 0;
+	m->converted = 0;
+	m->badchars = 0;
+	m->cstate &= ~(Cheader|Cbody);
+	if(Topmsg(mb, m))
+		mb->cached -= sz;
+	return sz;
+}
+
+long
+cachefree(Mailbox *mb, Message *m, int force)
+{
+	long sz, i;
+
+	sz = cachefree0(mb, m, force);
+	for(i = 0; i < mb->ntab; i++)
+		if(m == mb->ctab[i]){
+			mb->ntab--;
+			memmove(mb->ctab + i, mb->ctab + i + 1, sizeof m*mb->ntab - i);
+			dupchk(mb);
+			break;
+		}
+	return sz;
+}
+
+enum{
+	Maxntab	= nelem(mbl->ctab) - 10,
+};
+
+vlong
+sumcache(Mcache *c)
+{
+	int i;
+	vlong sz;
+
+	sz = 0;
+	for(i = 0; i < c->ntab; i++)
+		sz += c->ctab[i]->csize;
+	return sz;
+}
+
+int
+scancache(Mcache *c)
+{
+	int i;
+
+	for(i = 0; i < c->ntab; i++)
+		if(c->ctab[i]->csize > Maxmsg)
+			return -1;
+	return 0;
+}
+
+/* debugging only */
+static void
+chkcsize(Mailbox *mb, vlong sz, vlong sz0)
+{
+	int j;
+	Mcache *c;
+	Message *m;
+
+	if(sumcache(mb) == mb->cached)
+	if(scancache(mb) == 0)
+		return;
+	eprint("sz0 %lld sz %lld sum %lld sumca %lld\n", sz0, sz, sumcache(mb), mb->cached);
+	eprint("%lld\n", sumcache(mb));
+	c = mb;
+	for(j = 0; j < c->ntab; j++){
+		m = c->ctab[j];
+		eprint("%d	%p	%d	%ld	%ld\n", j, m, m->refs, m->csize, m->size);
+	}
+	abort();
+}
+
+/*
+ * strategy: start with i = 0. while cache exceeds limits,
+ * find j so that all the [i:j] elements have refs == 0.
+ * uncache all the [i:j], reduce ntab by i-j.  the tail
+ * [j+1:ntab] is shifted to [i:ntab], and finally i = i+1.
+ * we may safely skip the new i, since the condition
+ * that stopped our scan there still holds.
+ */
+void
+putcache(Mailbox *mb, Message *m)
+{
+	int i, j, k;
+	vlong sz, sz0;
+	Message **p;
+
+	p = mb->ctab;
+	sz0 = mb->cached;
+	dupchk(mb);
+	for(i = 0;; i++){
+		sz = mb->cached;
+		for(j = i;; j++){
+			if(j >= mb->ntab ||
+			sz < cachetarg && mb->ntab - (j - i) < Maxntab){
+				if(j != i)
+					break;
+chkcsize(mb, sz, sz0);
+				return;
+			}
+			if(p[j]->refs > 0)
+				break;
+			sz -= p[j]->csize;
+		}
+		if(sz == mb->cached){
+			if(i >= mb->ntab)
+				break;
+			continue;
+		}
+		for(k = i; k < j; k++)
+			cachefree0(mb, p[k], 0);
+		mb->ntab -= j - i;
+		memmove(p + i, p + j, (mb->ntab - i)*sizeof *p);
+	}
+chkcsize(mb, sz, sz0);
+	k = 0;
+	for(i = 0; i < mb->ntab; i++)
+		k += p[i]->refs > 0;
+	if((mb->ntab > 1 || k != mb->ntab) && Topmsg(mb, m))
+		eprint("cache overflow: %D %llud bytes; %d entries\n", 
+			m? m->fileid: 1ll, mb->cached, mb->ntab);
+	if(k == mb->ntab)
+		return;
+	debug = 1; prcache(mb, "");
+	abort();
+}
+
+static int
+squeeze(Message *m, uvlong o, long l, int c)
+{
+	char *p, *q, *e;
+	int n;
+
+	q = memchr(m->start + o, c, l);
+	if(q == nil)
+		return 0;
+	n = 0;
+	e = m->start + o + l;
+	for(p = q; q < e; q++){
+		if(*q == c){
+			n++;
+			continue;
+		}
+		*p++ = *q;
+	}
+	return n;
+}
+
+void
+msgrealloc(Message *m, ulong l)
+{
+	long l0, h0, m0, me, b0;
+
+	l0 = m->end - m->start;
+	m->mallocd = 1;
+	h0 = m->hend - m->start;
+	m0 = m->mheader - m->start;
+	me = m->mhend - m->start;
+	b0 = m->body - m->start;
+	assert(h0 >= 0 && m0 >= 0 && me >= 0 && b0 >= 0);
+	m->start = erealloc(m->start, l + 1);
+	m->rbody = m->start + b0;
+	m->rbend = m->end = m->start + l0;
+	if(!m->hallocd){
+		m->header = m->start;
+		m->hend = m->start + h0;
+	}
+	if(!m->ballocd){
+		m->body = m->start + b0;
+		m->bend = m->start + l0;
+	}
+	m->mheader = m->start + m0;
+	m->mhend = m->start + me;
+}
+
+/*
+ * the way we squeeze out bad characters is exceptionally sneaky.
+ */
+static int
+fetch(Mailbox *mb, Message *m, uvlong o, ulong l)
+{
+	int expand;
+	long l0, n, sz0;
+
+top:
+	l0 = m->end - m->start;
+	assert(l0 >= 0);
+	dprint("fetch %lud sz %lud o %llud l %lud badchars %d\n", l0, m->size, o, l, m->badchars);
+	assert(m->badchars < Maxmsg/10);
+	if(l0 == m->size || o > m->size)
+		return 0;
+	expand = 0;
+	if(o + l > m->size)
+		l = m->size - o;
+	if(o + l == m->size)
+		l += m->ibadchars - m->badchars;
+	if(o + l > l0){
+		expand = 1;
+		msgrealloc(m, o + m->badchars + l);
+	}
+	assert(l0 <= o);
+	sz0 = m->size;
+	if(mb->fetch(mb, m, o + m->badchars, l) == -1){
+		logmsg(m, "can't fetch %D %llud %lud", m->fileid, o, l);
+		m->deleted = Dead;
+		return -1;
+	}
+	if(m->size - sz0)
+		l += m->size - sz0;	/* awful botch for gmail */
+	if(expand){
+		/* grumble.  poor planning. */
+		if(m->badchars > 0)
+			memmove(m->start + o, m->start + o + m->badchars, l);
+		n = squeeze(m, o, l, 0);
+		n += squeeze(m, o, l - n, '\r');
+		if(n > 0){
+			if(m->ibadchars == 0)
+				dprint("   %ld more badchars\n", n);
+			l -= n;
+			m->badchars += n;
+			msgrealloc(m, o + l);
+		}
+		notecache(mb, m, l);
+		m->bend = m->rbend = m->end = m->start + o + l;
+		if(n)
+		if(o + l + n == m->size && m->cstate&Cidx){
+			dprint("   redux %llud %ld\n", o + l, n);
+			o += l;
+			l = n;
+			goto top;
+		}
+	}else
+		eprint("unhandled case in fetch\n");
+	*m->end = 0;
+	return 0;
+}
+
+void
+cachehash(Mailbox *mb, Message *m)
+{
+//	fprint(2, "cachehash %P\n", mpair(mb, m));
+	if(m->whole == m->whole->whole)
+		henter(PATH(mb->id, Qmbox), m->name,
+			(Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
+	else
+		henter(PATH(m->whole->id, Qdir), m->name,
+			(Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
+	henter(PATH(m->id, Qdir), "xxx",
+		(Qid){PATH(m->id, Qmax), 0, QTFILE}, m, mb);	/* sleezy speedup */
+}
+
+void
+newcachehash(Mailbox *mb, Message *m, int doplumb)
+{
+	if(doplumb)
+		mailplumb(mb, m, 0);
+	else
+		if(insurecache(mb, m) == 0)
+			msgdecref(mb, m);
+	/* avoid cachehash on error? */
+	cachehash(mb, m);
+}
+
+static char *itab[] = {
+	"idx",
+	"stale",
+	"header",
+	"body"
+};
+
+char*
+cstate(Message *m)
+{
+	char *p, *e;
+	int i, s;
+	static char buf[64];
+
+	s = m->cstate;
+	p = e = buf;
+	e += sizeof buf;
+	for(i = 0; i < 8; i++)
+		if(s & 1<<i)
+		if(i < nelem(itab))
+			p = seprint(p, e, "%s ", itab[i]);
+	if(p > buf)
+		p--;
+	p[0] = 0;
+	return buf;
+}
+
+
+static int
+middlecache(Mailbox *mb, Message *m)
+{
+	int y;
+
+	y = 0;
+	while(!Topmsg(mb, m)){
+		m = m->whole;
+		if((m->cstate & Cbody) == 0)
+			y = 1;
+	}
+	if(y == 0)
+		return 0;
+	dprint("middlecache %d [%D] %lud %lud\n", m->id, m->fileid, m->end - m->start, m->size);
+	return cachebody(mb, m);
+}
+
+int
+cacheheaders(Mailbox *mb, Message *m)
+{
+	char *p, *e;
+	int r;
+	ulong o;
+
+	if(!mb->fetch || m->cstate&Cheader)
+		return 0;
+	if(!Topmsg(mb, m))
+		return middlecache(mb, m);
+	dprint("cacheheaders %d %D\n", m->id, m->fileid);
+	if(m->size < 10000)
+		r = fetch(mb, m, 0, m->size);
+	else for(r = 0; (o = m->end - m->start) < m->size; ){
+		if((r = fetch(mb, m, o, 4096)) < 0)
+			break;
+		p = m->start + o;
+		if(o)
+			p--;
+		for(e = m->end - 2; p < e; p++){
+			p = memchr(p, '\n', e - p);
+			if(p == nil)
+				break;
+			if(p[1] == '\n' || (p[1] == '\r' && p[2] == '\n'))
+				goto found;
+		}
+	}
+	if(r < 0)
+		return -1;
+found:
+	parseheaders(mb, m, mb->addfrom, 0);
+	return 0;
+}
+
+void
+digestmessage(Mailbox *mb, Message *m)
+{
+	assert(m->digest == 0);
+	m->digest = emalloc(SHA1dlen);
+	sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
+	if(mtreeisdup(mb, m)){
+		logmsg(m, "dup detected");
+		m->deleted = Dup;	/* no dups allowed */
+	}else
+		mtreeadd(mb, m);
+	dprint("%d %#A\n", m->id, m->digest);
+}
+
+int
+cachebody(Mailbox *mb, Message *m)
+{
+	ulong o;
+
+	while(!Topmsg(mb, m))
+		m = m->whole;
+	if(!mb->fetch || m->cstate&Cbody)
+		return 0;
+	o = m->end - m->start;
+	dprint("cachebody %d [%D] %lud %lud %s\n", m->id, m->fileid, o, m->size, cstate(m));
+	if(o < m->size)
+	if(fetch(mb, m, o, m->size - o) < 0)
+		return -1;
+	if((m->cstate&Cidx) == 0){
+		assert(m->ibadchars == 0);
+		if(m->badchars > 0)
+			dprint("reducing size %ld %ld\n", m->size, m->size - m->badchars);
+		m->size -= m->badchars;		/* sneaky */
+		m->ibadchars = m->badchars;
+	}
+	if(m->digest == 0)
+		digestmessage(mb, m);
+	if(m->lines == 0)
+		m->lines = countlines(m);
+	parse(mb, m, mb->addfrom, 0);
+	dprint("  →%s\n", cstate(m));
+	return 0;
+}
+
+int
+cacheidx(Mailbox *mb, Message *m)
+{
+	if(m->cstate & Cidx)
+		return 0;
+	if(cachebody(mb, m) == -1)
+		return -1;
+	m->cstate |= Cidxstale|Cidx;
+	return 0;
+}
+
+static int
+countparts(Message *m)
+{
+	Message *p;
+
+	if(m->nparts == 0)
+		for(p = m->part; p; p = p->next){
+			countparts(p);
+			m->nparts++;
+		}
+	return m->nparts;
+}
+
+int
+insurecache(Mailbox *mb, Message *m)
+{
+	if(m->deleted || !m->inmbox)
+		return -1;
+	msgincref(m);
+	cacheidx(mb, m);
+	if((m->cstate & Cidx) == 0){
+		logmsg(m, "%s: can't cache: %s: %r", mb->path, m->name);
+		msgdecref(mb, m);
+		return -1;
+	}
+	if(m->digest == 0)
+		sysfatal("digest?");
+	countparts(m);
+	return 0;
+}
binary files /dev/null b/sys/src/cmd/upas/fs/chkidx differ
--- /dev/null
+++ b/sys/src/cmd/upas/fs/chkidx.c
@@ -1,0 +1,416 @@
+#include "common.h"
+#include <auth.h>
+#include <libsec.h>
+#include <bin.h>
+#include "dat.h"
+
+#define idprint(...)	if(1) fprint(2, __VA_ARGS__); else {}
+enum{
+	Maxver	= 10,
+};
+static char *magictab[Maxver] = {
+[4]	"idx magic v4\n",
+[7]	"idx magic v7\n",
+};
+static int fieldstab[Maxver] = {
+[4]	19,
+[7]	21,
+};
+
+static	char	*magic;
+static	int	Idxfields;
+static	int	lineno;
+static	int	idxver;
+
+int
+newid(void)
+{
+	static int id;
+
+	return ++id;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+
+	p = mallocz(n, 1);
+	if(!p)
+		sysfatal("malloc %lud: %r", n);
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+	
+static int
+Afmt(Fmt *f)
+{
+	char buf[SHA1dlen*2 + 1];
+	uchar *u, i;
+
+	u = va_arg(f->args, uchar*);
+	if(u == 0 && f->flags & FmtSharp)
+		return fmtstrcpy(f, "-");
+	if(u == 0)
+		return fmtstrcpy(f, "<nildigest>");
+	for(i = 0; i < SHA1dlen; i++)
+		sprint(buf + 2*i, "%2.2ux", u[i]);
+	return fmtstrcpy(f, buf);
+}
+
+static int
+Dfmt(Fmt *f)
+{
+	char buf[32];
+	int seq;
+	uvlong v;
+
+	v = va_arg(f->args, uvlong);
+	seq = v & 0xff;
+	if(seq > 99)
+		seq = 99;
+	snprint(buf, sizeof buf, "%llud.%.2d", v>>8, seq);
+	return fmtstrcpy(f, buf);
+}
+
+static Mailbox*
+shellmailbox(char *path)
+{
+	Mailbox *mb;
+
+	mb = malloc(sizeof *mb);
+	if(mb == 0)
+		sysfatal("malloc");
+	memset(mb, 0, sizeof *mb);
+	snprint(mb->path, sizeof mb->path, "%s", path);
+	mb->id = newid();
+	mb->root = newmessage(nil);
+	mb->mtree = mkavltree(mtreecmp);
+	return mb;
+}
+
+void
+shellmailboxfree(Mailbox*)
+{
+}
+
+Message*
+newmessage(Message *parent)
+{
+	static int id;
+	Message *m;
+
+//	msgallocd++;
+
+	m = mallocz(sizeof *m, 1);
+	if(m == 0)
+		sysfatal("malloc");
+	m->disposition = Dnone;
+//	m->type = newrefs("text/plain");
+//	m->charset = newrefs("iso-8859-1");
+	m->cstate = Cidxstale;
+	m->flags = Frecent;
+	m->id = newid();
+	if(parent)
+		snprint(m->name, sizeof m->name, "%d", ++(parent->subname));
+	if(parent == nil)
+		parent = m;
+	m->whole = parent;
+	m->hlen = -1;
+	return m;
+}
+
+void
+unnewmessage(Mailbox *mb, Message *parent, Message *m)
+{
+	assert(parent->subname > 0);
+//	delmessage(mb, m);
+	USED(mb, m);
+	parent->subname -= 1;
+}
+
+
+static int
+validmessage(Mailbox *mb, Message *m, int level)
+{
+	if(level){
+		if(m->digest != 0)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+		if(m->fileid != 0)
+			goto lose;
+	}else{
+		if(m->digest == 0)
+			goto lose;
+		if(m->size == 0)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+			goto lose;
+		if(mtreefind(mb, m->digest))
+			goto lose;
+	}
+	return 1;
+lose:
+	fprint(2, "invalid cache[%d] %#A size %ld %D\n", level, m->digest, m->size, m->fileid);
+	return 0;
+}
+
+static char*
+∫(char *x)
+{
+	if(x && *x)
+		return x;
+	return nil;
+}
+
+static char*
+brdstr(Biobuf *b, int c, int eat)
+{
+	char *s;
+
+	s = Brdstr(b, c, eat);
+	if(s)
+		lineno++;
+	return s;
+}
+
+static int
+nibble(int c)
+{
+	if(c >= '0' && c <= '9')
+		return c - '0';
+	if(c < 0x20)
+		c += 0x20;
+	if(c >= 'a' && c <= 'f')
+		return c - 'a'+10;
+	return 0xff;
+}
+
+static uchar*
+hackdigest(char *s)
+{
+	uchar t[SHA1dlen];
+	int i;
+
+	if(strcmp(s, "-") == 0)
+		return 0;
+	if(strlen(s) != 2*SHA1dlen){
+		fprint(2, "bad digest %s\n", s);
+		return 0;
+	}
+	for(i = 0; i < SHA1dlen; i++)
+		t[i] = nibble(s[2*i])<<4 | nibble(s[2*i + 1]);
+	memmove(s, t, SHA1dlen);
+	return (uchar*)s;
+}
+
+static Message*
+findmessage(Mailbox *, Message *parent, int n)
+{
+	Message *m;
+
+	for(m = parent->part; m; m = m->next)
+		if(!m->digest && n-- == 0)
+			return m;
+	return 0;
+}
+
+static uvlong
+rdfileid(char *s, int level)
+{
+	char *p;
+	uvlong uv;
+
+	uv = strtoul(s, &p, 0);
+	if((level == 0 && uv < 1000000) || *p != '.')
+		return 0;
+	return uv<<8 | strtoul(p + 1, 0, 10);
+}
+
+static int
+rdidx(Biobuf *b, Mailbox *mb, Message *parent, int npart, int level)
+{
+	char *f[50 + 1], *s;
+	uchar *digest;
+	int n, nparts, good, bad, redux;
+	Message *m, **ll, *l;
+
+	bad = good = redux = 0;
+	ll = &parent->part;
+	nparts = npart;
+	for(; npart != 0 && (s = brdstr(b, '\n', 1)); npart--){
+//if(lineno>18&&lineno<25)idprint("%d: %d [%s]\n", lineno, level, s);
+		n = tokenize(s, f, nelem(f));
+		if(n != Idxfields){
+			print("%d: bad line\n", lineno);
+			bad++;
+			free(s);
+			continue;
+		}
+		digest = hackdigest(f[0]);
+		if(level == 0){
+			if(digest == 0)
+				idprint("%d: no digest\n", lineno);
+			m = mtreefind(mb, digest);
+		}else{
+			m = findmessage(mb, parent, nparts - npart);
+			if(m == 0){
+			//	idprint("can't find message\n");
+			}
+		}
+		if(m){
+			/*
+			 * read in mutable information.
+			 * currently this is only flags
+			 */
+			idprint("%d seen before %d... %.2ux", level, m->id, m->cstate);
+			redux++;
+			m->flags |= strtoul(f[1], 0, 16);
+			m->cstate &= ~Cidxstale;
+			m->cstate |= Cidx;
+			idprint("→%.2ux\n", m->cstate);
+			free(s);
+
+			if(m->nparts)
+				rdidx(b, mb, m, m->nparts, level + 1);
+			ll = &m->next;
+			continue;
+		}
+		m = newmessage(parent);
+//if(lineno>18&&lineno<25)idprint("%d: %d %d %A\n", lineno, level, m->id, digest);
+//		idprint("%d new %d %#A \n", level, m->id, digest);
+		m->digest = digest;
+		m->flags = strtoul(f[1], 0, 16);
+		m->fileid = rdfileid(f[2], level);
+		m->lines = atoi(f[3]);
+		m->ffrom = ∫(f[4]);
+		m->from = ∫(f[5]);
+		m->to = ∫(f[6]);
+		m->cc = ∫(f[7]);
+		m->bcc = ∫(f[8]);
+		m->replyto = ∫(f[9]);
+		m->messageid = ∫(f[10]);
+		m->subject = ∫(f[11]);
+		m->sender = ∫(f[12]);
+		m->inreplyto = ∫(f[13]);
+//		m->type = newrefs(f[14]);
+		m->disposition = atoi(f[15]);
+		m->size = strtoul(f[16], 0, 0);
+		m->rawbsize = strtoul(f[17], 0, 0);
+		switch(idxver){
+		case 4:
+			m->nparts = strtoul(f[18], 0, 0);
+		case 7:
+			m->ibadchars = strtoul(f[18], 0, 0);
+			m->idxaux = ∫(f[19]);
+			m->nparts = strtoul(f[20], 0, 0);
+		}
+		m->cstate &= ~Cidxstale;
+		m->cstate |= Cidx;
+		m->str = s;
+//		free(s);
+		if(!validmessage(mb, m, level)){
+			/*
+			 *  if this was an okay message, and somebody
+			 * wrote garbage to the index file, we lose the msg.
+			 */
+			print("%d: !validmessage\n", lineno);
+			bad++;
+			unnewmessage(mb, parent, m);
+			continue;
+		}
+		if(level == 0)
+			m->inmbox = 1;
+//		cachehash(mb, m);		/* hokey */
+		l = *ll;
+		*ll = m;
+		ll = &m->next;
+		*ll = l;
+		good++;
+
+		if(m->nparts){
+//			fprint(2, "%d: %d parts [%s]\n", lineno, m->nparts, f[18]);
+			rdidx(b, mb, m, m->nparts, level + 1);
+		}
+	}
+	if(level == 0)
+		print("idx: %d %d %d\n", good, bad, redux);
+	return 0;
+}
+
+static int
+verscmp(Biobuf *b)
+{
+	char *s;
+	int i;
+
+	if((s = brdstr(b, '\n', 0)) == 0)
+		return -1;
+	for(i = 0; i < Maxver; i++)
+		if(magictab[i])
+		if(strcmp(s, magictab[i]) == 0)
+			break;
+	free(s);
+	if(i == Maxver)
+		return -1;
+	idxver = i;
+	magic = magictab[i];
+	Idxfields = fieldstab[i];
+	fprint(2, "version %d\n", i);
+	return 0;
+}
+
+int
+mbvers(Biobuf *b)
+{
+	char *s;
+
+	if(s = brdstr(b, '\n', 1)){
+		free(s);
+		return 0;
+	}
+	return -1;
+}
+
+int
+ckidxfile(Mailbox *mb)
+{
+	char buf[Pathlen + 4];
+	int r;
+	Biobuf *b;
+
+	snprint(buf, sizeof buf, "%s", mb->path);
+	b = Bopen(buf, OREAD);
+	if(b == nil)
+		return -1;
+	if(verscmp(b) == -1)
+		return -1;
+	if(idxver >= 7)
+		mbvers(b);
+	r = rdidx(b, mb, mb->root, -1, 0);
+	Bterm(b);
+	return r;
+}
+
+static char *bargv[] = {"/fd/0", 0};
+
+void
+main(int argc, char **argv)
+{
+	Mailbox *mb;
+
+	fmtinstall('A', Afmt);
+	fmtinstall('D', Dfmt);
+	ARGBEGIN{
+	}ARGEND
+	if(*argv == 0)
+		argv = bargv;
+	for(; *argv; argv++){
+		mb = shellmailbox(*argv);
+		lineno = 0;
+		if(ckidxfile(mb) == -1)
+			fprint(2, "%s: %r\n", *argv);
+		shellmailboxfree(mb);
+	}
+	exits("");
+}
--- a/sys/src/cmd/upas/fs/dat.h
+++ b/sys/src/cmd/upas/fs/dat.h
@@ -1,191 +1,313 @@
+#include <avl.h>
+
+enum {
+	/* cache states */
+	Cidx		= 1<<0,
+	Cidxstale	= 1<<1,
+	Cheader 	= 1<<2,
+	Cbody		= 1<<3,
+
+	/* encodings */
+	Enone=	0,
+	Ebase64,
+	Equoted,
+
+	/* dispositions */
+	Dnone=	0,
+	Dinline,
+	Dfile,
+	Dignore,
+
+	/* mb create flags */
+	DMcreate	=  0x02000000,
+
+	/* rm flags */
+	Rrecur		= 1<<0,
+	Rtrunc		= 1<<1,
+
+	/* m->deleted flags */
+	Deleted		= 1<<0,
+	Dup		= 1<<1,
+	Dead		= 1<<2,
+	Disappear	= 1<<3,
+	Dmark		= 1<<4,	/* temporary mark for idx scan */
+
+	/* mime flags */
+	Mtrunc		= 1<<0,	/* message had no boundary */
+
+	Maxmsg		= 75*1024*1024,	/* maxmessage size; debugging */
+	Maxcache	= 512*1024,	/* target cache size; set low for debugging */
+	Nctab		= 15,		/* max # of cached messages >10 */
+	Nref		= 10,
+};
+
+typedef struct Idx Idx;
+struct Idx {
+	char	*str;			/* as read from idx file */
+	uchar	*digest;
+	uchar	flags;
+	uvlong	fileid;
+	ulong	lines;
+	ulong	size;
+	ulong	rawbsize;			/* nasty imap4d */
+	ulong	ibadchars;
+
+	char	*ffrom;
+	char	*from;
+	char	*to;
+	char	*cc;
+	char	*bcc;
+	char	*replyto;
+	char	*messageid;
+	char	*subject;
+	char	*sender;
+	char	*inreplyto;
+	char	*idxaux;			/* mailbox specific */
+
+	int	type;			/* very few types: refstring */
+	int	disposition;		/* very few types: refstring */
+	int	nparts;
+};
+
 typedef struct Message Message;
-struct Message
-{
+struct Message {
 	int	id;
 	int	refs;
 	int	subname;
-	char	name[Elemlen];
+	char	name[12];
 
-	// pointers into message
-	char	*start;		// start of message
-	char	*end;		// end of message
-	char	*header;	// start of header
-	char	*hend;		// end of header
-	int	hlen;		// length of header minus ignored fields
-	char	*mheader;	// start of mime header
-	char	*mhend;		// end of mime header
-	char	*body;		// start of body
-	char	*bend;		// end of body
-	char	*rbody;		// raw (unprocessed) body
-	char	*rbend;		// end of raw (unprocessed) body
-	char	*lim;
-	char	deleted;
-	char	inmbox;
-	char	mallocd;	// message is malloc'd
-	char	ballocd;	// body is malloc'd
-	char	hallocd;	// header is malloce'd
+	/* top-level indexed information */
+	Idx;
 
-	// mail info
-	String	*unixheader;
-	String	*unixfrom;
-	String	*unixdate;
-	String	*from822;
-	String	*sender822;
-	String	*to822;
-	String	*bcc822;
-	String	*cc822;
-	String	*replyto822;
-	String	*date822;
-	String	*inreplyto822;
-	String	*subject822;
-	String	*messageid822;
-	String	*addrs;
-	String	*mimeversion;
-	String	*sdigest;
+	/* caching help */
+	uchar	cstate;
+	ulong	infolen;
+	ulong	csize;
 
-	// mime info
-	String	*boundary;
-	String	*type;
-	int	encoding;
-	int	disposition;
-	String	*charset;
-	String	*filename;
-	int	converted;
-	int	decoded;
-	char	lines[10];	// number of lines in rawbody
+	/*
+	 * a plethoria of pointers into message
+	 * and some status.  not valid unless cached
+	 */
+	char	*start;		/* start of message */
+	char	*end;		/* end of message */
+	char	*header;		/* start of header */
+	char	*hend;		/* end of header */
+	int	hlen;		/* length of header minus ignored fields */
+	char	*mheader;	/* start of mime header */
+	char	*mhend;		/* end of mime header */
+	char	*body;		/* start of body */
+	char	*bend;		/* end of body */
+	char	*rbody;		/* raw (unprocessed) body */
+	char	*rbend;		/* end of raw (unprocessed) body */
+	char	mallocd;		/* message is malloc'd */
+	char	ballocd;		/* body is malloc'd */
+	char	hallocd;		/* header is malloc'd */
+	int	badchars;	/* running count of bad chars. */
 
-	Message	*next;		// same level
-	Message	*part;		// down a level
-	Message	*whole;		// up a level
+	char	deleted;
+	char	inmbox;
 
-	uchar	digest[SHA1dlen];
+	/* mail info */
+	char	*unixheader;
+	char	*unixfrom;
+	char	*date822;
+	char	*references[Nref];
 
-	vlong	imapuid;	// used by imap4
+	/* mime info */
+	char	*boundary;
+	int	charset;
+	char	*filename;
+	char	encoding;
+	char	converted;
+	char	decoded;
+	char	mimeflag;
 
-	char		uidl[80];	// used by pop3
-	int		mesgno;
+	Message	*next;
+	Message	*part;
+	Message	*whole;
+
+	union{
+		char	*lim;	/* used by plan9; not compatable with cache */
+		vlong	imapuid;	/* used by imap4 */
+		void	*aux;
+	};
 };
 
-enum
-{
-	// encodings
-	Enone=	0,
-	Ebase64,
-	Equoted,
+typedef struct {
+	Avl;
+	Message *m;
+} Mtree;
 
-	// disposition possibilities
-	Dnone=	0,
-	Dinline,
-	Dfile,
-	Dignore,
-
-	PAD64=	'=',
+typedef struct Mcache Mcache;
+struct Mcache {
+	uvlong	cached;
+	int	ntab;
+	Message	*ctab[Nctab];
 };
 
 typedef struct Mailbox Mailbox;
-struct Mailbox
-{
+struct Mailbox {
 	QLock;
+	long	idxsem;		/* abort on concurrent index access */
+	long	syncsem;		/* abort on concurrent syncs */
 	int	refs;
 	Mailbox	*next;
 	int	id;
-	int	dolock;		// lock when syncing?
-	int	std;
+	int	flags;
+	char	rmflags;
+	char	dolock;		/* lock when syncing? */
+	char	addfrom;
 	char	name[Elemlen];
 	char	path[Pathlen];
 	Dir	*d;
 	Message	*root;
-	int	vers;		// goes up each time mailbox is read
+	Avltree	*mtree;
+	ulong	vers;		/* goes up each time mailbox is changed */
 
-	ulong waketime;
-	char	*(*sync)(Mailbox*, int);
+	/* cache tracking */
+	Mcache;
+
+	/* index tracking */
+	Qid	qid;
+
+	ulong	waketime;
 	void	(*close)(Mailbox*);
-	char	*(*fetch)(Mailbox*, Message*);
+	void	(*decache)(Mailbox*, Message*);
+	int	(*fetch)(Mailbox*, Message*, uvlong, ulong);
+	void	(*delete)(Mailbox*, Message*);
 	char	*(*ctl)(Mailbox*, int, char**);
-	void	*aux;		// private to Mailbox implementation
+	char	*(*remove)(Mailbox *, int);
+	char	*(*rename)(Mailbox*, char*, int);
+	char	*(*sync)(Mailbox*, int, int*);
+	void	(*modflags)(Mailbox*, Message*, int);
+	void	(*idxwrite)(Biobuf*, Mailbox*);
+	int	(*idxread)(char*, Mailbox*);
+	void	(*idxinvalid)(Mailbox*);
+	void	*aux;		/* private to Mailbox implementation */
 };
 
+/* print argument tango; can't varargck 2 types.  should fix compiler */
+typedef struct Mpair Mpair;
+struct Mpair {
+	Mailbox	*mb;
+	Message	*m;
+};
+Mpair mpair(Mailbox*, Message*);
+
 typedef char *Mailboxinit(Mailbox*, char*);
 
-extern Message	*root;
-extern Mailboxinit	plan9mbox;
-extern Mailboxinit	pop3mbox;
-extern Mailboxinit	imap4mbox;
-extern Mailboxinit	planbmbox;
-extern Mailboxinit	planbvmbox;
+Mailboxinit	plan9mbox;
+Mailboxinit	planbmbox;
+Mailboxinit	pop3mbox;
+Mailboxinit	imap4mbox;
+Mailboxinit	mdirmbox;
 
+void		genericidxwrite(Biobuf*, Mailbox*);
+int		genericidxread(char*, Mailbox*);
+void		genericidxinvalid(Mailbox*);
+
+void		cachehash(Mailbox*, Message*);
+void		newcachehash(Mailbox*, Message*, int);
+int		cacheheaders(Mailbox*, Message*);		/* "getcache" */
+int		cachebody(Mailbox*, Message*);
+int		cacheidx(Mailbox*, Message*);
+int		insurecache(Mailbox*, Message*);
+
+/**/
+void		putcache(Mailbox*, Message*);		/* asymmetricial */
+long		cachefree(Mailbox*, Message*, int);
+
+Message*	gettopmsg(Mailbox*, Message*);
 char*		syncmbox(Mailbox*, int);
-char*		geterrstr(void);
 void*		emalloc(ulong);
 void*		erealloc(void*, ulong);
 Message*	newmessage(Message*);
+void		unnewmessage(Mailbox*, Message*, Message*);
 void		delmessage(Mailbox*, Message*);
-void		delmessages(int, char**);
+char*		delmessages(int, char**);
+char		*flagmessages(int, char**);
+void		digestmessage(Mailbox*, Message*);
+
+uintptr		absbos(void);
+void		eprint(char*, ...);
+void		iprint(char *, ...);
 int		newid(void);
 void		mailplumb(Mailbox*, Message*, int);
-char*		newmbox(char*, char*, int);
+char*		newmbox(char*, char*, int, Mailbox**);
 void		freembox(char*);
-void		logmsg(char*, Message*);
+char*		removembox(char*, int);
+void		syncallmboxes(void);
+void		logmsg(Message*, char*, ...);
 void		msgincref(Message*);
 void		msgdecref(Mailbox*, Message*);
 void		mboxincref(Mailbox*);
 void		mboxdecref(Mailbox*);
+char		*mboxrename(char*, char*, int);
 void		convert(Message*);
 void		decode(Message*);
-int		cistrncmp(char*, char*, int);
-int		cistrcmp(char*, char*);
 int		decquoted(char*, char*, char*, int);
 int		xtoutf(char*, char**, char*, char*);
-void		countlines(Message*);
-int		headerlen(Message*);
-void		parse(Message*, int, Mailbox*, int);
-void		parseheaders(Message*, int, Mailbox*, int);
+ulong		countlines(Message*);
+void		parse(Mailbox*, Message*, int, int);
+void		parseheaders(Mailbox*, Message*, int, int);
 void		parsebody(Message*, Mailbox*);
-void		parseunix(Message*);
-String*	date822tounix(char*);
+char*		date822tounix(Message*, char*);
 int		fidmboxrefs(Mailbox*);
 int		hashmboxrefs(Mailbox*);
+void		checkmboxrefs(void);
+int		strtotm(char*, Tm*);
+char*		lowercase(char*);
 
-extern int	debug;
-extern int	fflag;
-extern int	logging;
-extern char	user[Elemlen];
-extern char	stdmbox[Pathlen];
-extern QLock	mbllock;
-extern Mailbox	*mbl;
-extern char	*mntpt;
-extern int	biffing;
-extern int	plumbing;
-extern char*	Enotme;
+char*		sputc(char*, char*, int);
+char*		seappend(char*, char*, char*, int);
 
-enum
-{
-	/* mail subobjects */
-	Qbody,
+int		hdrlen(char*, char*);
+char*		rfc2047(char*, char*, char*, int, int);
+
+char*		localremove(Mailbox*, int);
+char*		localrename(Mailbox*, char*, int);
+void		rmidx(char*, int);
+int		vremove(char*);
+int		rename(char *, char*, int);
+
+int		mtreecmp(Avl*, Avl*);
+int		mtreeisdup(Mailbox *, Message *);
+Message*	mtreefind(Mailbox*, uchar*);
+void		mtreeadd(Mailbox*, Message*);
+void		mtreedelete(Mailbox*, Message*);
+
+enum {
+	/* mail sub-objects; must be sorted */
 	Qbcc,
+	Qbody,
 	Qcc,
 	Qdate,
 	Qdigest,
 	Qdisposition,
+	Qffrom,
+	Qfileid,
 	Qfilename,
+	Qflags,
 	Qfrom,
 	Qheader,
+	Qinfo,
 	Qinreplyto,
 	Qlines,
-	Qmimeheader,
 	Qmessageid,
+	Qmimeheader,
 	Qraw,
 	Qrawbody,
 	Qrawheader,
 	Qrawunix,
+	Qreferences,
 	Qreplyto,
 	Qsender,
+	Qsize,
 	Qsubject,
 	Qto,
 	Qtype,
-	Qunixheader,
-	Qinfo,
 	Qunixdate,
+	Qunixheader,
 	Qmax,
 
 	/* other files */
@@ -196,12 +318,10 @@
 	Qmboxctl,
 };
 
-#define PATH(id, f)	((((id)&0xfffff)<<10) | (f))
+#define PATH(id, f)	((((id) & 0xfffff)<<10) | (f))
 #define FILE(p)		((p) & 0x3ff)
 
-char *dirtab[];
-
-// hash table to aid in name lookup, all files have an entry
+/* hash table to aid in name lookup, all files have an entry */
 typedef struct Hash Hash;
 struct Hash {
 	Hash	*next;
@@ -216,5 +336,50 @@
 void	henter(ulong, char*, Qid, Message*, Mailbox*);
 void	hfree(ulong, char*);
 
-ulong msgallocd, msgfreed;
+typedef struct {
+	char	*s;
+	int	l;
+	ulong	ref;
+} Refs;
+
+int	newrefs(char*);
+void	delrefs(int);
+void	refsinit(void);
+int	prrefs(Biobuf*);
+int	rdrefs(Biobuf*);
+
+void	idxfree(Idx*);
+int	rdidxfile(Mailbox*, int);
+int	wridxfile(Mailbox*);
+
+char	*modflags(Mailbox*, Message*, char*);
+int	getmtokens(char *, char**, int, int);
+
+extern char	Enotme[];
+extern char	*mntpt;
+extern char	user[Elemlen];
+extern char 	*dirtab[];
+extern int	Sflag;
+extern int	iflag;
+extern int	biffing;
+extern ulong	cachetarg;
+extern int	debug;
+extern int	lflag;
+extern int	plumbing;
+extern ulong	msgallocd;
+extern ulong	msgfreed;
+extern Mailbox	*mbl;
+extern Message	*root;
+extern QLock	mbllock;
+extern Refs	*rtab;
+
+#define	dprint(...)	if(debug) fprint(2, __VA_ARGS__); else {}
+#define	Topmsg(mb, m)	(m->whole == mb->root)
+#pragma	varargck	type	"A"	uchar*
+#pragma	varargck	type	"D"	uvlong
+#pragma	varargck	type	"P"	Mpair
+#pragma	varargck	type	"Δ"	uvlong
+#pragma	varargck	argpos	eprint	1
+#pragma	varargck	argpos	iprint	1
+#pragma	varargck	argpos	logmsg	2
 
binary files /dev/null b/sys/src/cmd/upas/fs/extra/fd2path differ
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/fd2path.c
@@ -1,0 +1,32 @@
+#include <u.h>
+#include <libc.h>
+
+void
+usage(void)
+{
+	fprint(2, "usage: fd2path path ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char buf[1024];
+	int fd;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+
+	if(argc == 0){
+		if(fd2path(0, buf, sizeof buf) != -1)
+			fprint(2, "%s\n", buf);
+	}else for(; *argv; argv++){
+		fd = open(*argv, OREAD);
+		if(fd != -1 && fd2path(fd, buf, sizeof buf) != -1)
+			fprint(2, "%s\n", buf);
+		close(fd);
+	}
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/idxtst.c
@@ -1,0 +1,58 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+static char *etab[] = {
+	"not found",
+	"does not exist",
+	"file is locked",
+	"exclusive lock",
+};
+
+static int
+bad(int idx)
+{
+	char buf[ERRMAX];
+	int i;
+
+	rerrstr(buf, sizeof buf);
+	for(i = idx; i < nelem(etab); i++)
+		if(strstr(buf, etab[i]))
+			return 0;
+	return 1;
+}
+
+static int
+exopen(char *s)
+{
+	int i, fd;
+
+	for(i = 0; i < 30; i++){
+		if((fd = open(s, OWRITE|OTRUNC)) >= 0 || bad(0))
+			return fd;
+		if((fd = create(s, OWRITE|OEXCL, DMEXCL|0600)) >= 0  || bad(2))
+			return fd;
+		sleep(1000);
+	}
+	werrstr("lock timeout");
+	return -1;
+}
+
+void
+main(void)
+{
+	int fd;
+	Biobuf *b;
+
+	fd = exopen("testingex");
+	if(fd == -1)
+		sysfatal("exopen: %r");
+	b = Bopen("testingex", OREAD);
+	if(b){
+		free(b);
+		fprint(2, "print both opened at once\n");
+	}else
+		fprint(2, "bopen: %r\n");
+	close(fd);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/infotst.c
@@ -1,0 +1,89 @@
+/*
+ * simulate the read patterns of external programs for testing
+ * info file. "infotest 511 512" simulates what ned does today.
+ *
+ * here's how the new info scheme was verified:
+ *
+	ramfs
+	s=/sys/src/cmd/upas
+	unmount /mail/fs
+	$s/fs/8.out -p
+	for(f in /mail/fs/mbox/*/info){
+		for(i in `{seq 1 1026})
+			$s/fs/infotst $i `{echo $i + 1 | hoc} > /tmp/$i < $f
+		for(i in /tmp/*)
+			cmp $i /tmp/1
+		rm /tmp/*
+	}
+
+	# now test for differences with old scheme under
+	# ideal reading conditions
+	for(f in /mail/fs/mbox/*/info){
+		i = `{echo $f | sed 's:/mail/fs/mbox/([^/]+)/info:\1:g'}
+		$s/fs/infotst 2048 > /tmp/$i < $f
+	}
+	unmount /mail/fs
+	upas/fs -p
+	for(f in /mail/fs/mbox/*/info){
+		i = `{echo $f | sed 's:/mail/fs/mbox/([^/]+)/info:\1:g'}
+		$s/fs/infotst 2048 > /tmp/$i.o < $f
+	}
+	for(i in /tmp/*.o)
+		cmp $i `{echo $i | sed 's:\.o$::g'}
+	rm /tmp/*
+ */
+#include <u.h>
+#include <libc.h>
+
+enum{
+	Ntab	= 100,
+};
+
+int	tab[Ntab];
+int	ntab;
+int	largest;
+
+void
+usage(void)
+{
+	fprint(2, "usage: infotest n1 n2 ... nm\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *buf;
+	int i, n;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+	if(argc == 0)
+		usage();
+	for(; *argv; argv++){
+		if(ntab == nelem(tab))
+			break;
+		i = atoi(*argv);
+		if(i > largest)
+			largest = i;
+		tab[ntab++] = i;
+	}
+	buf = malloc(largest);
+	if(!buf)
+		sysfatal("malloc: %r");
+	for(i = 0;; ){
+		switch(n = read(0, buf, tab[i])){
+		case -1:
+			sysfatal("read: %r");
+		case 0:
+			exits("");
+		default:
+			write(1, buf, n);
+			break;
+		}
+		if(i < ntab-1)
+			i++;
+	}
+}
binary files /dev/null b/sys/src/cmd/upas/fs/extra/paw differ
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/paw.c
@@ -1,0 +1,23 @@
+#include<u.h>
+#include<libc.h>
+#include<bio.h>
+
+void
+main(void)
+{
+	char *f[10], *s;
+	vlong sum;
+	Biobuf b;
+
+	sum = 0;
+	Binit(&b, 0, OREAD);
+
+	while(s = Brdstr(&b, '\n', 1)){
+		if(getfields(s, f, nelem(f), 1, " ") > 2)
+			sum += strtoul(f[2], 0, 0);
+		free(s);
+	}
+	Bterm(&b);
+	print("%lld\n", sum);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/prflags.c
@@ -1,0 +1,37 @@
+#include "common.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: prflags\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *f[Fields+1], buf[20], *s;
+	int n;
+	Biobuf b, o;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+	if(argc)
+		usage();
+	Binit(&b, 0, OREAD);
+	Binit(&o, 1, OWRITE);
+
+	for(; s = Brdstr(&b, '\n', 1); free(s)){
+		n = gettokens(s, f, nelem(f), " ");
+		if(n != Fields)
+			continue;
+		if(!strcmp(f[0], "-"))
+			continue;
+		Bprint(&o, "%s\n", flagbuf(buf, strtoul(f[1], 0, 16)));
+	}
+	Bterm(&b);
+	Bterm(&o);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/strtotmtst.c
@@ -1,0 +1,17 @@
+#include "strtotm.c"
+
+void
+main(int argc, char **argv)
+{
+	Tm tm;
+
+	ARGBEGIN{
+	}ARGEND
+
+	for(; *argv; argv++)
+		if(strtotm(*argv, &tm) >= 0)
+			print("%s", asctime(&tm));
+		else
+			print("bad\n");
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/tokens.c
@@ -1,0 +1,62 @@
+#include <u.h>
+#include <libc.h>
+
+/* unfortunately, tokenize insists on collapsing multiple seperators */
+static char qsep[] = " \t\r\n";
+
+static char*
+qtoken(char *s, char *sep)
+{
+	int quoting;
+	char *t;
+
+	quoting = 0;
+	t = s;	/* s is output string, t is input string */
+	while(*t!='\0' && (quoting || utfrune(sep, *t)==nil)){
+		if(*t != '\''){
+			*s++ = *t++;
+			continue;
+		}
+		/* *t is a quote */
+		if(!quoting){
+			quoting = 1;
+			t++;
+			continue;
+		}
+		/* quoting and we're on a quote */
+		if(t[1] != '\''){
+			/* end of quoted section; absorb closing quote */
+			t++;
+			quoting = 0;
+			continue;
+		}
+		/* doubled quote; fold one quote into two */
+		t++;
+		*s++ = *t++;
+	}
+	if(*s != '\0'){
+		*s = '\0';
+		if(t == s)
+			t++;
+	}
+	return t;
+}
+
+int
+getmtokens(char *s, char **args, int maxargs, int multiflag)
+{
+	int i;
+
+	for(i = 0; i < maxargs; i++){
+		if(multiflag)
+			while(*s && utfrune(qsep, *s))
+				s++;
+		else if(*s && utfrune(qsep, *s))
+			s++;
+		if(*s == 0)
+			break;
+		args[i] = s;
+		s = qtoken(s, qsep);
+	}
+	return i;
+}
--- a/sys/src/cmd/upas/fs/fs.c
+++ b/sys/src/cmd/upas/fs/fs.c
@@ -1,17 +1,10 @@
 #include "common.h"
-#include <auth.h>
 #include <fcall.h>
 #include <libsec.h>
-#include <ctype.h>
+#include <pool.h>
 #include "dat.h"
 
-enum
-{
-	OPERM	= 0x3,		// mask of all permission types in open mode
-};
-
 typedef struct Fid Fid;
-
 struct Fid
 {
 	Qid	qid;
@@ -21,31 +14,13 @@
 	Fid	*next;
 	Mailbox	*mb;
 	Message	*m;
-	Message *mtop;		// top level message
+	Message *mtop;		/* top level message */
 
-	//finger pointers to speed up reads of large directories
-	long	foff;	// offset/DIRLEN of finger
-	Message	*fptr;	// pointer to message at off
-	int	fvers;	// mailbox version when finger was saved
+	long	foff;		/* offset/DIRLEN of finger */
+	Message	*fptr;		/* pointer to message at off */
+	int	fvers;		/* mailbox version when finger was saved */
 };
 
-ulong	path;		// incremented for each new file
-Fid	*fids;
-int	mfd[2];
-char	user[Elemlen];
-int	messagesize = 4*1024+IOHDRSZ;
-uchar	mdata[8*1024+IOHDRSZ];
-uchar	mbuf[8*1024+IOHDRSZ];
-Fcall	thdr;
-Fcall	rhdr;
-int	fflg;
-char	*mntpt;
-int	biffing;
-int	plumbing = 1;
-
-QLock	mbllock;
-Mailbox	*mbl;
-
 Fid		*newfid(int);
 void		error(char*);
 void		io(void);
@@ -54,9 +29,6 @@
 void		usage(void);
 void		reader(void);
 int		readheader(Message*, char*, int, int);
-int		cistrncmp(char*, char*, int);
-int		tokenconvert(String*, char*, int);
-String*		stringconvert(String*, char*, int);
 void		post(char*, char*, int);
 
 char	*rflush(Fid*), *rauth(Fid*),
@@ -93,30 +65,36 @@
 char	Excl[] = 	"exclusive use file already open";
 char	Ename[] = 	"illegal name";
 char	Ebadctl[] =	"unknown control message";
+char	Ebadargs[] =	"invalid arguments";
+char 	Enotme[] =	"path not served by this file server";
 
-char *dirtab[] =
-{
+char *dirtab[] = {
 [Qdir]		".",
-[Qbody]		"body",
 [Qbcc]		"bcc",
+[Qbody]		"body",
 [Qcc]		"cc",
 [Qdate]		"date",
 [Qdigest]	"digest",
 [Qdisposition]	"disposition",
+[Qffrom]		"ffrom",
+[Qfileid]		"fileid",
 [Qfilename]	"filename",
+[Qflags]		"flags",
 [Qfrom]		"from",
 [Qheader]	"header",
 [Qinfo]		"info",
 [Qinreplyto]	"inreplyto",
-[Qlines]	"lines",
-[Qmimeheader]	"mimeheader",
+[Qlines]		"lines",
 [Qmessageid]	"messageid",
+[Qmimeheader]	"mimeheader",
 [Qraw]		"raw",
-[Qrawunix]	"rawunix",
 [Qrawbody]	"rawbody",
 [Qrawheader]	"rawheader",
+[Qrawunix]	"rawunix",
+[Qreferences]	"references",
 [Qreplyto]	"replyto",
 [Qsender]	"sender",
+[Qsize]		"size",
 [Qsubject]	"subject",
 [Qto]		"to",
 [Qtype]		"type",
@@ -128,19 +106,193 @@
 
 enum
 {
-	Hsize=	1277,
+	Hsize=	1999,
 };
 
-Hash	*htab[Hsize];
 
+char	*mntpt;
+char	user[Elemlen];
+int	Dflag;
+int	Sflag;
+int	iflag;
+int	lflag;
+int	biffing;
 int	debug;
-int	fflag;
-int	logging;
+int	plumbing = 1;
+ulong	cachetarg = Maxcache;
+Mailbox	*mbl;
+QLock	mbllock;
 
+static	int	messagesize = 8*1024 + IOHDRSZ;
+static	int	mfd[2];
+static	char	hbuf[32*1024];
+static	uchar	mbuf[16*1024 + IOHDRSZ];
+static	uchar	mdata[16*1024 + IOHDRSZ];
+static	ulong	path;		/* incremented for each new file */
+static	Hash	*htab[Hsize];
+static	Fcall	rhdr;
+static	Fcall	thdr;
+static	Fid	*fids;
+static	uintptr	bos = 0xd0000000;		/* pc kernel specific */
+
+#define onstack(x)	((uintptr)(x) >= bos)
+#define intext(x)		((char*)(x) <= end)
+#define validgptr(x)	assert(!onstack(x) && !intext(x))
 void
+sanemsg(Message *m)
+{
+	if(bos == 0)
+		bos = absbos();
+	assert(m->refs < 100);
+	validgptr(m->whole);
+	if(debug)
+		poolcheck(mainmem);
+	validgptr(m);
+	assert(m->next != m);
+	if(m->end < m->start)
+		abort();
+	if(m->ballocd && (m->start <= m->body && m->end >= m->body))
+		abort();
+	if(m->end - m->start > Maxmsg)
+		abort();
+	if(m->size > Maxmsg)
+		abort();
+	if(m->fileid != 0 && m->fileid <= 1000000ull<<8)
+		abort();
+}
+
+void
+sanembmsg(Mailbox *mb, Message *m)
+{
+	sanemsg(m);
+	if(Topmsg(mb, m)){
+		if(m->start > end && m->size == 0)
+			abort();
+		if(m->fileid <= 1000000ull<<8)
+			abort();
+	}
+}
+
+static void
+sanefid(Fid *f)
+{
+	if(f->m == 0)
+		return;
+	if(f->mtop){
+		sanemsg(f->mtop);
+		assert(f->mtop->refs > 0);
+	}
+	sanemsg(f->m);
+	if(f->m)
+	if(Topmsg(f->mb, f->m))
+		assert(f->m->refs > 0);
+}
+
+void
+sanefids(void)
+{
+	Fid *f;
+
+	for(f = fids; f; f = f->next)
+		if(f->busy)
+			sanefid(f);
+}
+
+static int
+Afmt(Fmt *f)
+{
+	char buf[SHA1dlen*2 + 1];
+	uchar *u, i;
+
+	u = va_arg(f->args, uchar*);
+	if(u == 0 && f->flags & FmtSharp)
+		return fmtstrcpy(f, "-");
+	if(u == 0)
+		return fmtstrcpy(f, "<nildigest>");
+	for(i = 0; i < SHA1dlen; i++)
+		sprint(buf + 2*i, "%2.2ux", u[i]);
+	return fmtstrcpy(f, buf);
+}
+
+static int
+Δfmt(Fmt *f)
+{
+	char buf[32];
+	uvlong v;
+
+	v = va_arg(f->args, uvlong);
+	if(f->flags & FmtSharp)
+		if((v>>8) == 0)
+			return fmtstrcpy(f, "");
+	strcpy(buf, ctime(v>>8));
+	buf[28] = 0;
+	return fmtstrcpy(f, buf);
+}
+
+static int
+Dfmt(Fmt *f)
+{
+	char buf[32];
+	int seq;
+	uvlong v;
+
+	v = va_arg(f->args, uvlong);
+	seq = v & 0xff;
+	if(seq > 99)
+		seq = 99;
+	snprint(buf, sizeof buf, "%llud.%.2d", v>>8, seq);
+	return fmtstrcpy(f, buf);
+}
+
+Mpair
+mpair(Mailbox *mb, Message *m)
+{
+	Mpair mp;
+
+	mp.mb = mb;
+	mp.m = m;
+	return mp;
+}
+
+static int
+Pfmt(Fmt *f)
+{
+	char buf[128], *p, *e;
+	int i, dots;
+	Mailbox *mb;
+	Message *t[32], *m;
+	Mpair mp;
+
+	mp = va_arg(f->args, Mpair);
+	mb = mp.mb;
+	m = mp.m;
+	if(m == nil || mb == nil)
+		return fmtstrcpy(f, "<P nil>");
+	i = 0;
+	for(; !Topmsg(mb, m); m = m->whole){
+		t[i++] = m;
+		if(i == nelem(t)-1)
+			break;
+	}
+	t[i++] = m;
+	dots = 0;
+	if(i == nelem(t))
+		dots = 1;
+	e = buf + sizeof buf;
+	p = buf;
+	if(dots)
+		p = seprint(p, e, ".../");
+	while(--i >= 1)
+		p = seprint(p, e, "%s/", t[i]->name);
+	if(i == 0)
+		seprint(p, e, "%s", t[0]->name);
+	return fmtstrcpy(f, buf);
+}
+
+void
 usage(void)
 {
-	fprint(2, "usage: upas/fs [-bdlnps] [-f mboxfile] [-m mountpoint]\n");
+	fprint(2, "usage: upas/fs [-DSbdlmnps] [-c cachetarg] [-f mboxfile] [-m mountpoint]\n");
 	exits("usage");
 }
 
@@ -147,42 +299,97 @@
 void
 notifyf(void *, char *s)
 {
-	if(strstr(s, "alarm") || strstr(s, "interrupt"))
+	if(strncmp(s, "interrupt", 9) == 0)
 		noted(NCONT);
+	if(strncmp(s, "die: yankee pig dog", 19) != 0)
+		/* don't want to call syslog from notify handler */
+		fprint(2, "upas/fs: user: %s; note: %s\n", getuser(), s);
 	noted(NDFLT);
 }
 
 void
+setname(char **v)
+{
+	char buf[128], buf2[32], *p, *e;
+	int fd, i;
+
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s", v[0]);
+	for(i = 0; v[++i]; )
+		p = seprint(p, e, " %s", v[i]);
+	snprint(buf2, sizeof buf2, "#p/%d/args", getpid());
+	if((fd = open(buf2, OWRITE)) >= 0){
+		write(fd, buf, p - buf);
+		close(fd);
+	}
+}
+
+ulong
+ntoi(char *s)
+{
+	ulong n;
+
+	n = strtoul(s, &s, 0);
+	for(;;)
+	switch(*s++){
+	default:
+		usage();
+	case 'g':
+		n *= 1024;
+	case 'm':
+		n *= 1024;
+	case 'k':
+		n *= 1024;
+		break;
+	case 0:
+		return n;
+	}
+}
+
+void
 main(int argc, char *argv[])
 {
-	int p[2], std, nodflt;
-	char maildir[128];
-	char mbox[128];
+	char maildir[Pathlen], mbox[Pathlen], srvfile[64], **v;
 	char *mboxfile, *err;
-	char srvfile[64];
-	int srvpost;
+	int p[2], nodflt, srvpost;
 
 	rfork(RFNOTEG);
-	mntpt = nil;
-	fflag = 0;
 	mboxfile = nil;
-	std = 0;
 	nodflt = 0;
 	srvpost = 0;
+	v = argv;
 
 	ARGBEGIN{
+	case 'D':
+		Dflag = 1;
+		break;
+	case 'S':
+		Sflag = 1;
+		break;
 	case 'b':
 		biffing = 1;
 		break;
+	case 'c':
+		cachetarg = ntoi(EARGF(usage()));
+		break;
+	case 'd':
+		debug = 1;
+		mainmem->flags |= POOL_PARANOIA;
+		break;
 	case 'f':
-		fflag = 1;
 		mboxfile = EARGF(usage());
 		break;
+	case 'i':
+		iflag++;
+		break;
+	case 'l':
+		lflag = 1;
+		break;
 	case 'm':
 		mntpt = EARGF(usage());
 		break;
-	case 'd':
-		debug = 1;
+	case 'n':
+		nodflt = 1;
 		break;
 	case 'p':
 		plumbing = 0;
@@ -190,12 +397,6 @@
 	case 's':
 		srvpost = 1;
 		break;
-	case 'l':
-		logging = 1;
-		break;
-	case 'n':
-		nodflt = 1;
-		break;
 	default:
 		usage();
 	}ARGEND
@@ -202,6 +403,13 @@
 
 	if(argc)
 		usage();
+	fmtinstall('A', Afmt);
+	fmtinstall('D', Dfmt);
+	fmtinstall(L'Δ', Δfmt);
+	fmtinstall('F', fcallfmt);
+	fmtinstall('H', encodefmt);		/* forces tls stuff */
+	fmtinstall('P', Pfmt);
+	quotefmtinstall();
 	if(pipe(p) < 0)
 		error("pipe failed");
 	mfd[0] = p[0];
@@ -214,19 +422,13 @@
 		mntpt = maildir;
 	}
 	if(mboxfile == nil && !nodflt){
-		snprint(mbox, sizeof(mbox), "/mail/box/%s/mbox", user);
+		snprint(mbox, sizeof mbox, "/mail/box/%s/mbox", user);
 		mboxfile = mbox;
-		std = 1;
 	}
 
-	if(debug)
-		fmtinstall('F', fcallfmt);
-
-	if(mboxfile != nil){
-		err = newmbox(mboxfile, "mbox", std);
-		if(err != nil)
+	if(mboxfile != nil)
+		if(err = newmbox(mboxfile, "mbox", 0, 0))
 			sysfatal("opening %s: %s", mboxfile, err);
-	}
 
 	switch(rfork(RFFDG|RFPROC|RFNAMEG|RFNOTEG|RFREND)){
 	case -1:
@@ -235,46 +437,67 @@
 		henter(PATH(0, Qtop), dirtab[Qctl],
 			(Qid){PATH(0, Qctl), 0, QTFILE}, nil, nil);
 		close(p[1]);
+		setname(v);
 		io();
-		postnote(PNGROUP, getpid(), "die yankee pig dog");
+		syncallmboxes();
+		syskillpg(getpid());
 		break;
 	default:
 		close(p[0]);	/* don't deadlock if child fails */
 		if(srvpost){
-			sprint(srvfile, "/srv/upasfs.%s", user);
+			snprint(srvfile, sizeof srvfile, "/srv/upasfs.%s", user);
 			post(srvfile, "upasfs", p[1]);
-		} else {
+		}else
 			if(mount(p[1], -1, mntpt, MREPL, "") < 0)
 				error("mount failed");
-		}
 	}
-	exits(0);
+	exits("");
 }
 
+char*
+sputc(char *p, char *e, int c)
+{
+	if(p < e - 1)
+		*p++ = c;
+	return p;
+}
+
+char*
+seappend(char *s, char *e, char *a, int n)
+{
+	int l;
+
+	l = e - s - 1;
+	if(l < n)
+		n = l;
+	memcpy(s, a, n);
+	s += n;
+	*s = 0;
+	return s;
+}
+
 static int
-fileinfo(Message *m, int t, char **pp)
+fileinfo(Mailbox *mb, Message *m, int t, char **pp)
 {
-	char *p;
-	int len;
+	char *s, *e, *p;
+	int len, i;
+	static char buf[64 + 512];
 
-	p = "";
-	len = 0;
+	cacheidx(mb, m);
+	sanembmsg(mb, m);
+	p = nil;
+	len = -1;
 	switch(t){
 	case Qbody:
+		cachebody(mb, m);
 		p = m->body;
-		len = m->bend - m->body;
+		len = m->bend - p;
 		break;
 	case Qbcc:
-		if(m->bcc822){
-			p = s_to_c(m->bcc822);
-			len = strlen(p);
-		}
+		p = m->bcc;
 		break;
 	case Qcc:
-		if(m->cc822){
-			p = s_to_c(m->cc822);
-			len = strlen(p);
-		}
+		p = m->cc;
 		break;
 	case Qdisposition:
 		switch(m->disposition){
@@ -285,142 +508,131 @@
 			p = "file";
 			break;
 		}
-		len = strlen(p);
 		break;
 	case Qdate:
-		if(m->date822){
-			p = s_to_c(m->date822);
-			len = strlen(p);
-		} else if(m->unixdate != nil){
-			p = s_to_c(m->unixdate);
-			len = strlen(p);
+		p = m->date822;
+		if(!p){
+			p = buf;
+			len = snprint(buf, sizeof buf, "%#Δ", m->fileid);
 		}
 		break;
 	case Qfilename:
-		if(m->filename){
-			p = s_to_c(m->filename);
-			len = strlen(p);
-		}
+		p = m->filename;
 		break;
+	case Qflags:
+		p = flagbuf(buf, m->flags);
+		break;
 	case Qinreplyto:
-		if(m->inreplyto822){
-			p = s_to_c(m->inreplyto822);
-			len = strlen(p);
-		}
+		p = m->inreplyto;
 		break;
 	case Qmessageid:
-		if(m->messageid822){
-			p = s_to_c(m->messageid822);
-			len = strlen(p);
-		}
+		p = m->messageid;
 		break;
 	case Qfrom:
-		if(m->from822){
-			p = s_to_c(m->from822);
-			len = strlen(p);
-		} else if(m->unixfrom != nil){
-			p = s_to_c(m->unixfrom);
-			len = strlen(p);
-		}
+		if(m->from)
+			p = m->from;
+		else
+			p = m->unixfrom;
 		break;
-	case Qheader:
-		p = m->header;
-		len = headerlen(m);
+	case Qffrom:
+		if(m->ffrom)
+			p = m->ffrom;
 		break;
 	case Qlines:
-		p = m->lines;
-		if(*p == 0)
-			countlines(m);
-		len = strlen(m->lines);
+		len = snprint(buf, sizeof buf, "%lud", m->lines);
+		p = buf;
 		break;
 	case Qraw:
+		cachebody(mb, m);
 		p = m->start;
-		if(strncmp(m->start, "From ", 5) == 0){
-			p = strchr(p, '\n');
-			if(p == nil)
-				p = m->start;
-			else
-				p++;
-		}
-		len = m->end - p;
+		if(strncmp(m->start, "From ", 5) == 0)
+		if(e = strchr(p, '\n'))
+			p = e + 1;
+		len = m->rbend - p;
 		break;
 	case Qrawunix:
+		cachebody(mb, m);
 		p = m->start;
 		len = m->end - p;
 		break;
 	case Qrawbody:
+		cachebody(mb, m);
 		p = m->rbody;
 		len = m->rbend - p;
 		break;
 	case Qrawheader:
+		cacheheaders(mb, m);
 		p = m->header;
 		len = m->hend - p;
 		break;
 	case Qmimeheader:
+		cacheheaders(mb, m);
 		p = m->mheader;
 		len = m->mhend - p;
 		break;
-	case Qreplyto:
-		p = nil;
-		if(m->replyto822 != nil){
-			p = s_to_c(m->replyto822);
-			len = strlen(p);
-		} else if(m->from822 != nil){
-			p = s_to_c(m->from822);
-			len = strlen(p);
-		} else if(m->sender822 != nil){
-			p = s_to_c(m->sender822);
-			len = strlen(p);
-		} else if(m->unixfrom != nil){
-			p = s_to_c(m->unixfrom);
-			len = strlen(p);
+	case Qreferences:
+		cacheheaders(mb, m);
+		e = buf + sizeof buf;
+		s = buf;
+		for(i = 0; i < nelem(m->references); i++){
+			if(m->references[i] == 0)
+				break;
+			s = seprint(s, e, "%s\n", m->references[i]);
 		}
+		*s = 0;
+		p = buf;
+		len = s - buf;
 		break;
+	case Qreplyto:
+		if(m->replyto != nil)
+			p = m->replyto;
+		else if(m->from != nil)
+			p = m->from;
+		else if(m->sender != nil)
+			p = m->sender;
+		else if(m->unixfrom != nil)
+			p = m->unixfrom;
+		break;
 	case Qsender:
-		if(m->sender822){
-			p = s_to_c(m->sender822);
-			len = strlen(p);
-		}
+		p = m->sender;
 		break;
 	case Qsubject:
-		p = nil;
-		if(m->subject822){
-			p = s_to_c(m->subject822);
-			len = strlen(p);
-		}
+		p = m->subject;
 		break;
+	case Qsize:
+		len = snprint(buf, sizeof buf, "%lud", m->size);
+		p = buf;
+		break;
 	case Qto:
-		if(m->to822){
-			p = s_to_c(m->to822);
-			len = strlen(p);
-		}
+		p = m->to;
 		break;
 	case Qtype:
-		if(m->type){
-			p = s_to_c(m->type);
-			len = strlen(p);
-		}
+		p = rtab[m->type].s;
+		len = rtab[m->type].l;
 		break;
 	case Qunixdate:
-		if(m->unixdate){
-			p = s_to_c(m->unixdate);
-			len = strlen(p);
-		}
+		p = buf;
+		len = snprint(buf, sizeof buf, "%#Δ", m->fileid);
 		break;
+	case Qfileid:
+		p = buf;
+		len = snprint(buf, sizeof buf, "%D", m->fileid);
+		break;
 	case Qunixheader:
-		if(m->unixheader){
-			p = s_to_c(m->unixheader);
-			len = s_len(m->unixheader);
-		}
+		cacheheaders(mb, m);
+		p = m->unixheader;
 		break;
 	case Qdigest:
-		if(m->sdigest){
-			p = s_to_c(m->sdigest);
-			len = strlen(p);
-		}
+		p = buf;
+		len = snprint(buf, sizeof buf, "%A", m->digest);
 		break;
 	}
+	if(p == nil)
+		p = "";
+	if(len == -1)
+		len = strlen(p);
 	*pp = p;
+	putcache(mb, m);
 	return len;
 }
 
@@ -441,47 +653,53 @@
 	Qsender,
 	Qmessageid,
 	Qlines,
-	-1,
+	Qsize,
+	Qflags,
+	Qfileid,
+	Qffrom,
 };
 
-static int
-readinfo(Message *m, char *buf, long off, int count)
+int
+readinfo(Mailbox *mb, Message *m, char *buf, long off, int count)
 {
-	char *p;
-	int len, i, n;
-	String *s;
+	char *s, *p, *e;
+	int i, n;
+	long off0;
 
-	s = s_new();
-	len = 0;
-	for(i = 0; len < count && infofields[i] >= 0; i++){
-		n = fileinfo(m, infofields[i], &p);
-		s = stringconvert(s, p, n);
-		s_append(s, "\n");
-		p = s_to_c(s);
-		n = strlen(p);
-		if(off > 0){
-			if(off >= n){
-				off -= n;
-				continue;
-			}
-			p += off;
+	if(m->infolen > 0 && off >= m->infolen)
+		return 0;
+	off0 = off;
+	s = buf;
+	e = s + count;
+	for(i = 0; s < e; i++){
+		if(i == nelem(infofields)){
+			m->infolen = s - buf + off0;
+			break;
+		}
+		n = fileinfo(mb, m, infofields[i], &p);
+		if(off > n){
+			off -= n + 1;
+			continue;
+		}
+		if(off){
 			n -= off;
+			p += off;
 			off = 0;
 		}
-		if(n > count - len)
-			n = count - len;
-		if(buf)
-			memmove(buf+len, p, n);
-		len += n;
+		if(s + n > e)
+			n = e - s;
+		memcpy(s, p, n);
+		s += n;
+		if(s < e)
+			*s++ = '\n';
 	}
-	s_free(s);
-	return len;
+	return s - buf;
 }
 
 static void
 mkstat(Dir *d, Mailbox *mb, Message *m, int t)
 {
-	char *p;
+	char *p, *e;
 
 	d->uid = user;
 	d->gid = user;
@@ -491,13 +709,13 @@
 	d->qid.type = QTFILE;
 	d->type = 0;
 	d->dev = 0;
-	if(mb != nil && mb->d != nil){
-		d->atime = mb->d->atime;
-		d->mtime = mb->d->mtime;
-	} else {
+	if(m && m->fileid > 1000000ull)
+		d->atime = m->fileid >> 8;
+	else if(mb && mb->d)
+		d->atime = mb->d->mtime;
+	else
 		d->atime = time(0);
-		d->mtime = d->atime;
-	}
+	d->mtime = d->atime;
 
 	switch(t){
 	case Qtop:
@@ -530,6 +748,12 @@
 		d->length = 0;
 		d->qid.path = PATH(0, Qctl);
 		break;
+	case Qheader:
+		d->name = dirtab[t];
+		cacheheaders(mb, m);
+		d->length = readheader(m, hbuf, 0, sizeof hbuf);
+		putcache(mb, m);
+		break;
 	case Qmboxctl:
 		d->name = dirtab[t];
 		d->mode = 0222;
@@ -539,12 +763,37 @@
 		break;
 	case Qinfo:
 		d->name = dirtab[t];
-		d->length = readinfo(m, nil, 0, 1<<30);
+		d->length = readinfo(mb, m, hbuf, 0, sizeof hbuf);
 		d->qid.path = PATH(m->id, t);
 		break;
+	case Qraw:
+		cacheheaders(mb, m);
+		p = m->start;
+		if(strncmp(m->start, "From ", 5) == 0)
+		if(e = strchr(p, '\n'))
+			p = e + 1;
+		d->name = dirtab[t];
+		d->length = m->size - (p - m->start);
+		putcache(mb, m);
+		break;
+	case Qrawbody:
+		d->name = dirtab[t];
+		d->length = m->rawbsize;
+		break;
+	case Qrawunix:
+		d->name = dirtab[t];
+		d->length = m->size;
+		if(mb->addfrom && Topmsg(mb, m)){
+			cacheheaders(mb, m);
+			d->length += strlen(m->unixheader);
+			putcache(mb, m);
+		}
+		break;
+	case Qflags:
+		d->mode = 0666;
 	default:
 		d->name = dirtab[t];
-		d->length = fileinfo(m, t, &p);
+		d->length = fileinfo(mb, m, t, &p);
 		d->qid.path = PATH(m->id, t);
 		break;
 	}
@@ -577,9 +826,8 @@
 }
 
 char*
-rflush(Fid *f)
+rflush(Fid*)
 {
-	USED(f);
 	return 0;
 }
 
@@ -608,30 +856,43 @@
 		return nil;
 	nf->busy = 1;
 	nf->open = 0;
-	nf->m = f->m;
-	nf->mtop = f->mtop;
-	nf->mb = f->mb;
-	if(f->mb != nil)
-		mboxincref(f->mb);
-	if(f->mtop != nil){
-		qlock(f->mb);
-		msgincref(f->mtop);
-		qunlock(f->mb);
+	if(nf->mb = f->mb)
+		mboxincref(nf->mb);
+	if(nf->m = f->m)
+		msgincref(gettopmsg(nf->mb, nf->m));
+	if(nf->mtop = f->mtop){
+		qlock(nf->mb);
+		msgincref(nf->mtop);
+		qunlock(nf->mb);
 	}
 	nf->qid = f->qid;
+sanefid(nf);
+sanefid(f);
 	return nf;
 }
 
+/* slow?  binary search? */
+static int
+dindex(char *name)
+{
+	int i;
+
+	for(i = 0; i < Qmax; i++)
+		if(dirtab[i] != nil)
+		if(strcmp(dirtab[i], name) == 0)
+			return i;
+	return -1;
+}
+
 char*
 dowalk(Fid *f, char *name)
 {
-	int t;
-	Mailbox *omb, *mb;
 	char *rv, *p;
+	int t, t1;
+	Mailbox *omb, *mb;
 	Hash *h;
 
 	t = FILE(f->qid.path);
-
 	rv = Enotexist;
 
 	omb = f->mb;
@@ -640,12 +901,26 @@
 	else
 		qlock(&mbllock);
 
-	// this must catch everything except . and ..
+	/* this must catch everything except . and .. */
 retry:
-	h = hlook(f->qid.path, name);
+sanefid(f);
+	t1 = FILE(f->qid.path);
+	if((t1 == Qmbox || t1 == Qdir) && *name >= 'a' && *name <= 'z'){
+		h = hlook(f->qid.path, "xxx");		/* sleezy speedup */
+		t1 = dindex(name);
+		if(t1 == -1)
+			h = nil;
+	}else
+		h = hlook(f->qid.path, name);
 	if(h != nil){
+		if(f->m)
+			msgdecref(f->mb, gettopmsg(f->mb, f->m));
+		if(f->mb && f->mb != h->mb)
+			mboxdecref(f->mb);
 		f->mb = h->mb;
 		f->m = h->m;
+		if(f->m)
+			msgincref(gettopmsg(f->mb, f->m));
 		switch(t){
 		case Qtop:
 			if(f->mb != nil)
@@ -659,8 +934,11 @@
 			break;
 		}
 		f->qid = h->qid;
+		if(t1 < Qmax)
+			f->qid.path = PATH(f->m->id, t1);	/* sleezy speedup */
+sanefid(f);
 		rv = nil;
-	} else if((p = strchr(name, '.')) != nil && *name != '.'){
+	}else if((p = strchr(name, '.')) != nil && *name != '.'){
 		*p = 0;
 		goto retry;
 	}
@@ -697,13 +975,15 @@
 			break;
 		case Qdir:
 			qlock(f->mb);
-			if(f->m->whole == f->mb->root){
+			if(Topmsg(f->mb, f->m)){
 				f->qid.path = PATH(f->mb->id, Qmbox);
 				f->qid.type = QTDIR;
 				f->qid.vers = f->mb->d->qid.vers;
 				msgdecref(f->mb, f->mtop);
+				msgdecref(f->mb, f->m);
 				f->m = f->mtop = nil;
 			} else {
+				/* refs don't change; still the same message */
 				f->m = f->m->whole;
 				f->qid.path = PATH(f->m->id, Qdir);
 				f->qid.type = QTDIR;
@@ -723,6 +1003,7 @@
 	char *rv;
 	int i;
 
+sanefid(f);
 	if(f->open)
 		return Eisopen;
 
@@ -754,14 +1035,14 @@
 	}
 	rhdr.nwqid = i;
 
-	/* we only error out if no walk  */
+	/* we only error out if no walk */
 	if(i > 0)
 		rv = nil;
-
+sanefid(f);
 	return rv;
 }
 
-char *
+char*
 ropen(Fid *f)
 {
 	int file;
@@ -768,18 +1049,17 @@
 
 	if(f->open)
 		return Eisopen;
-
 	file = FILE(f->qid.path);
 	if(thdr.mode != OREAD)
-		if(file != Qctl && file != Qmboxctl)
+		if(file != Qctl && file != Qmboxctl && file != Qflags)
 			return Eperm;
 
-	// make sure we've decoded
+	/* make sure we've decoded */
 	if(file == Qbody){
-		if(f->m->decoded == 0)
-			decode(f->m);
-		if(f->m->converted == 0)
-			convert(f->m);
+		cachebody(f->mb, f->m);
+		decode(f->m);
+		convert(f->m);
+		putcache(f->mb, f->m);
 	}
 
 	rhdr.iounit = 0;
@@ -788,7 +1068,7 @@
 	return 0;
 }
 
-char *
+char*
 rcreate(Fid*)
 {
 	return Eperm;
@@ -816,7 +1096,7 @@
 		
 	for(mb = mbl; mb != nil; mb = mb->next){
 		mkstat(&d, mb, nil, Qmbox);
-		m = convD2M(&d, &buf[n], blen-n);
+		m = convD2M(&d, &buf[n], blen - n);
 		if(off <= pos){
 			if(m <= BIT16SZ || m > cnt)
 				break;
@@ -851,7 +1131,7 @@
 			off -= m;
 	}
 
-	// to avoid n**2 reads of the directory, use a saved finger pointer
+	/* to avoid n**2 reads of the directory, use a saved finger pointer */
 	if(f->mb->vers == f->fvers && off >= f->foff && f->fptr != nil){
 		msg = f->fptr;
 		pos = f->foff;
@@ -858,15 +1138,15 @@
 	} else {
 		msg = f->mb->root->part;
 		pos = 0;
-	} 
+	}
 
 	for(; cnt > 0 && msg != nil; msg = msg->next){
-		// act like deleted files aren't there
+		/* act like deleted files aren't there */
 		if(msg->deleted)
 			continue;
 
 		mkstat(&d, f->mb, msg, Qdir);
-		m = convD2M(&d, &buf[n], blen-n);
+		m = convD2M(&d, &buf[n], blen - n);
 		if(off <= pos){
 			if(m <= BIT16SZ || m > cnt)
 				break;
@@ -876,7 +1156,7 @@
 		pos += m;
 	}
 
-	// save a finger pointer for next read of the mbox directory
+	/* save a finger pointer for next read of the mbox directory */
 	f->foff = pos;
 	f->fptr = msg;
 	f->fvers = f->mb->vers;
@@ -896,7 +1176,7 @@
 	pos = 0;
 	for(i = 0; i < Qmax; i++){
 		mkstat(&d, f->mb, f->m, i);
-		m = convD2M(&d, &buf[n], blen-n);
+		m = convD2M(&d, &buf[n], blen - n);
 		if(off <= pos){
 			if(m <= BIT16SZ || m > cnt)
 				return n;
@@ -907,7 +1187,7 @@
 	}
 	for(msg = f->m->part; msg != nil; msg = msg->next){
 		mkstat(&d, f->mb, msg, Qdir);
-		m = convD2M(&d, &buf[n], blen-n);
+		m = convD2M(&d, &buf[n], blen - n);
 		if(off <= pos){
 			if(m <= BIT16SZ || m > cnt)
 				break;
@@ -920,60 +1200,110 @@
 	return n;
 }
 
+static int
+mboxctlread(Mailbox *mb, char **p)
+{
+	static char buf[128];
+
+	*p = buf;
+	return snprint(*p, sizeof buf, "%s\n%ld\n", mb->path, mb->vers);
+}
+
 char*
 rread(Fid *f)
 {
-	long off;
-	int t, i, n, cnt;
 	char *p;
+	int t, i, n, cnt;
+	long off;
 
 	rhdr.count = 0;
 	off = thdr.offset;
 	cnt = thdr.count;
-
 	if(cnt > messagesize - IOHDRSZ)
 		cnt = messagesize - IOHDRSZ;
-
 	rhdr.data = (char*)mbuf;
 
+sanefid(f);
 	t = FILE(f->qid.path);
 	if(f->qid.type & QTDIR){
-		if(t == Qtop) {
+		if(t == Qtop){
 			qlock(&mbllock);
 			n = readtopdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
 			qunlock(&mbllock);
-		} else if(t == Qmbox) {
+		}else if(t == Qmbox) {
 			qlock(f->mb);
 			if(off == 0)
 				syncmbox(f->mb, 1);
 			n = readmboxdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
 			qunlock(f->mb);
-		} else if(t == Qmboxctl) {
+		}else if(t == Qmboxctl)
 			n = 0;
-		} else {
+		else
 			n = readmsgdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
-		}
-
 		rhdr.count = n;
 		return nil;
 	}
 
-	if(FILE(f->qid.path) == Qheader){
+	switch(t){
+	case Qctl:
+		rhdr.count = 0;
+		break;
+	case Qmboxctl:
+		i = mboxctlread(f->mb, &p);
+		goto output;
+		break;
+	case Qheader:
+		cacheheaders(f->mb, f->m);
 		rhdr.count = readheader(f->m, (char*)mbuf, off, cnt);
-		return nil;
+		putcache(f->mb, f->m);
+		break;
+	case Qinfo:
+		if(cnt > sizeof mbuf)
+			cnt = sizeof mbuf;
+		rhdr.count = readinfo(f->mb, f->m, (char*)mbuf, off, cnt);
+		break;
+	case Qrawunix:
+		if(f->mb->addfrom && Topmsg(f->mb, f->m)){
+			cacheheaders(f->mb, f->m);
+			p = f->m->unixheader;
+			if(off < strlen(p)){
+				rhdr.count = strlen(p + off);
+				memmove(mbuf, p + off, rhdr.count);
+				break;
+			}
+			off -= strlen(p);
+			putcache(f->mb, f->m);
+		}
+	default:
+		i = fileinfo(f->mb, f->m, t, &p);
+	output:
+		if(off < i){
+			if(off + cnt > i)
+				cnt = i - off;
+			if(cnt > sizeof mbuf)
+				cnt = sizeof mbuf;
+			memmove(mbuf, p + off, cnt);
+			rhdr.count = cnt;
+		}
+		break;
 	}
+	return nil;
+}
 
-	if(FILE(f->qid.path) == Qinfo){
-		rhdr.count = readinfo(f->m, (char*)mbuf, off, cnt);
-		return nil;
-	}
+char*
+modflags(Mailbox *mb, Message *m, char *p)
+{
+	char *err;
+	uchar f;
 
-	i = fileinfo(f->m, FILE(f->qid.path), &p);
-	if(off < i){
-		if((off + cnt) > i)
-			cnt = i - off;
-		memmove(mbuf, p + off, cnt);
-		rhdr.count = cnt;
+	f = m->flags;
+	if(err = txflags(p, &f))
+		return err;
+	if(f != m->flags){
+		if(mb->modflags != nil)
+			mb->modflags(mb, m, f);
+		m->flags = f;
+		m->cstate |= Cidxstale;
 	}
 	return nil;
 }
@@ -981,87 +1311,141 @@
 char*
 rwrite(Fid *f)
 {
-	char *err;
-	char *token[1024];
-	int t, n;
-	String *file;
+	char *argvbuf[1024], **argv, file[Pathlen], *err, *v0;
+	int i, t, argc, flags;
+	Message *m;
 
 	t = FILE(f->qid.path);
 	rhdr.count = thdr.count;
+	sanefid(f);
+	if(thdr.count == 0)
+		return Ebadctl;
+	if(thdr.data[thdr.count - 1] == '\n')
+		thdr.data[thdr.count - 1] = 0;
+	else
+		thdr.data[thdr.count] = 0;
+	argv = argvbuf;
 	switch(t){
 	case Qctl:
-		if(thdr.count == 0)
+		memset(argvbuf, 0, sizeof argvbuf);
+		argc = tokenize(thdr.data, argv, nelem(argvbuf) - 1);
+		if(argc == 0)
 			return Ebadctl;
-		if(thdr.data[thdr.count-1] == '\n')
-			thdr.data[thdr.count-1] = 0;
-		else
-			thdr.data[thdr.count] = 0;
-		n = tokenize(thdr.data, token, nelem(token));
-		if(n == 0)
-			return Ebadctl;
-		if(strcmp(token[0], "open") == 0){
-			file = s_new();
-			switch(n){
-			case 1:
-				err = Ebadctl;
-				break;
-			case 2:
-				mboxpath(token[1], getlog(), file, 0);
-				err = newmbox(s_to_c(file), nil, 0);
-				break;
-			default:
-				mboxpath(token[1], getlog(), file, 0);
-				if(strchr(token[2], '/') != nil)
-					err = "/ not allowed in mailbox name";
-				else
-					err = newmbox(s_to_c(file), token[2], 0);
-				break;
-			}
-			s_free(file);
-			return err;
+		if(strcmp(argv[0], "open") == 0 || strcmp(argv[0], "create") == 0){
+			if(argc == 1 || argc > 3)
+				return Ebadargs;
+			mboxpathbuf(file, sizeof file, getlog(), argv[1]);
+			if(argc == 3){
+				if(strchr(argv[2], '/') != nil)
+					return "/ not allowed in mailbox name";
+			}else
+				argv[2] = nil;
+			flags = 0;
+			if(strcmp(argv[0], "create") == 0)
+				flags |= DMcreate;
+			return newmbox(file, argv[2], flags, 0);
 		}
-		if(strcmp(token[0], "close") == 0){
-			if(n < 2)
+		if(strcmp(argv[0], "close") == 0){
+			if(argc < 2)
 				return nil;
-			freembox(token[1]);
+			for(i = 1; i < argc; i++)
+				freembox(argv[i]);
 			return nil;
 		}
-		if(strcmp(token[0], "delete") == 0){
-			if(n < 3)
+		if(strcmp(argv[0], "delete") == 0){
+			if(argc < 3)
 				return nil;
-			delmessages(n-1, &token[1]);
+			delmessages(argc - 1, argv + 1);
 			return nil;
 		}
+		if(strcmp(argv[0], "flag") == 0){
+			if(argc < 3)
+				return nil;
+			return flagmessages(argc - 1, argv + 1);
+		}
+		if(strcmp(argv[0], "remove") == 0){
+			v0 = argv0;
+			flags = 0;
+			ARGBEGIN{
+			default:
+				argv0 = v0;
+				return Ebadargs;
+			case 'r':
+				flags |= Rrecur;
+				break;
+			case 't':
+				flags |= Rtrunc;
+				break;
+			}ARGEND
+			argv0 = v0;
+			if(argc == 0)
+				return Ebadargs;
+			for(; *argv; argv++){
+				mboxpathbuf(file, sizeof file, getlog(), *argv);
+				if(err = newmbox(file, nil, 0, 0))
+					return err;
+//				if(!mb->remove)
+//					return "remove not implemented";
+				if(err = removembox(file, flags))
+					return err;
+			}
+			return 0;
+		}
+		if(strcmp(argv[0], "rename") == 0){
+			v0 = argv0;
+			flags = 0;
+			ARGBEGIN{
+			case 't':
+				flags |= Rtrunc;
+				break;
+			}ARGEND
+			argv0 = v0;
+			if(argc != 2)
+				return Ebadargs;
+			return mboxrename(argv[0], argv[1], flags);
+		}
 		return Ebadctl;
 	case Qmboxctl:
 		if(f->mb && f->mb->ctl){
-			if(thdr.count == 0)
+			argc = tokenize(thdr.data, argv, nelem(argvbuf));
+			if(argc == 0)
 				return Ebadctl;
-			if(thdr.data[thdr.count-1] == '\n')
-				thdr.data[thdr.count-1] = 0;
-			else
-				thdr.data[thdr.count] = 0;
-			n = tokenize(thdr.data, token, nelem(token));
-			if(n == 0)
-				return Ebadctl;
-			return (*f->mb->ctl)(f->mb, n, token);
+			return f->mb->ctl(f->mb, argc, argv);
 		}
+		break;
+	case Qflags:
+		/*
+		 * modifying flags on subparts is a little strange.
+		 */
+		if(!f->mb || !f->m)
+			break;
+		m = gettopmsg(f->mb, f->m);
+		err = modflags(f->mb, m, thdr.data);
+//		premature optimization?  flags not written immediately.
+//		if(err == nil && f->m->cstate&Cidxstale)
+//			wridxfile(f->mb);		/* syncmbox(f->mb, 1); */
+		return err;
 	}
 	return Eperm;
 }
 
-char *
+char*
 rclunk(Fid *f)
 {
 	Mailbox *mb;
 
-	f->busy = 0;
+sanefid(f);
+	f->busy = 1;
+	/* coherence(); */
+	f->fid = -1;
 	f->open = 0;
-	if(f->mtop != nil){
+	if(f->mtop){
 		qlock(f->mb);
 		msgdecref(f->mb, f->mtop);
 		qunlock(f->mb);
 	}
+	if(f->m)
+		msgdecref(f->mb, gettopmsg(f->mb, f->m));
 	f->m = f->mtop = nil;
 	mb = f->mb;
 	if(mb != nil){
@@ -1071,7 +1455,7 @@
 		mboxdecref(mb);
 		qunlock(&mbllock);
 	}
-	f->fid = -1;
+	f->busy = 0;
 	return 0;
 }
 
@@ -1078,10 +1462,11 @@
 char *
 rremove(Fid *f)
 {
+sanefid(f);
 	if(f->m != nil){
 		if(f->m->deleted == 0)
 			mailplumb(f->mb, f->m, 1);
-		f->m->deleted = 1;
+		f->m->deleted = Deleted;
 	}
 	return rclunk(f);
 }
@@ -1091,6 +1476,7 @@
 {
 	Dir d;
 
+sanefid(f);
 	if(FILE(f->qid.path) == Qmbox){
 		qlock(f->mb);
 		syncmbox(f->mb, 1);
@@ -1102,13 +1488,13 @@
 	return 0;
 }
 
-char *
+char*
 rwstat(Fid*)
 {
 	return Eperm;
 }
 
-Fid *
+Fid*
 newfid(int fid)
 {
 	Fid *f, *ff;
@@ -1152,7 +1538,7 @@
 	int n;
 
 	/* start a process to watch the mailboxes*/
-	if(plumbing){
+	if(plumbing || biffing)
 		switch(rfork(RFPROC|RFMEM)){
 		case -1:
 			/* oh well */
@@ -1159,19 +1545,30 @@
 			break;
 		case 0:
 			reader();
-			exits(nil);
+			exits("");
 		default:
 			break;
 		}
-	}
 
-	while((n = read9pmsg(mfd[0], mdata, messagesize)) != 0){
+	for(;;){
+		/*
+		 * reading from a pipe or a network device
+		 * will give an error after a few eof reads
+		 * however, we cannot tell the difference
+		 * between a zero-length read and an interrupt
+		 * on the processes writing to us,
+		 * so we wait for the error
+		 */
+		checkmboxrefs();
+		n = read9pmsg(mfd[0], mdata, messagesize);
+		if(n == 0)
+			continue;
 		if(n < 0)
-			error("mount read");
-		if(convM2S(mdata, n, &thdr) != n)
- 			error("convM2S format error");
+			return;
+		if(convM2S(mdata, n, &thdr) == 0)
+			continue;
 
-		if(debug)
+		if(Dflag)
 			fprint(2, "%s:<-%F\n", argv0, &thdr);
 
 		rhdr.data = (char*)mdata + messagesize;
@@ -1178,7 +1575,7 @@
 		if(!fcalls[thdr.type])
 			err = "bad fcall type";
 		else
-			err = (*fcalls[thdr.type])(newfid(thdr.fid));
+			err = fcalls[thdr.type](newfid(thdr.fid));
 		if(err){
 			rhdr.type = Rerror;
 			rhdr.ename = err;
@@ -1187,8 +1584,8 @@
 			rhdr.fid = thdr.fid;
 		}
 		rhdr.tag = thdr.tag;
-		if(debug)
-			fprint(2, "%s:->%F\n", argv0, &rhdr);/**/
+		if(Dflag)
+			fprint(2, "%s:->%F\n", argv0, &rhdr);
 		n = convS2M(&rhdr, mdata, messagesize);
 		if(write(mfd[1], mdata, n) != n)
 			error("mount write");
@@ -1195,6 +1592,8 @@
 	}
 }
 
+static char *readerargv[] = {"upas/fs", "plumbing", 0};
+
 void
 reader(void)
 {
@@ -1202,6 +1601,7 @@
 	Dir *d;
 	Mailbox *mb;
 
+	setname(readerargv);
 	sleep(15*1000);
 	for(;;){
 		t = time(0);
@@ -1208,7 +1608,7 @@
 		qlock(&mbllock);
 		for(mb = mbl; mb != nil; mb = mb->next){
 			assert(mb->refs > 0);
-			if(mb->waketime != 0 && t > mb->waketime){
+			if(mb->waketime != 0 && t >= mb->waketime){
 				qlock(mb);
 				mb->waketime = 0;
 				break;
@@ -1256,8 +1656,8 @@
 void
 error(char *s)
 {
-	postnote(PNGROUP, getpid(), "die yankee pig dog");
-	fprint(2, "%s: %s: %r\n", argv0, s);
+	syskillpg(getpid());
+	eprint("upas/fs: fatal error: %s: %r\n", s);
 	exits(s);
 }
 
@@ -1266,13 +1666,13 @@
 struct Ignorance
 {
 	Ignorance *next;
-	char	*str;		/* string */
-	int	partial;	/* true if not exact match */
+	char	*str;
+	int	len;
 };
 Ignorance *ignorance;
 
 /*
- *  read the file of headers to ignore 
+ *  read the file of headers to ignore
  */
 void
 readignore(void)
@@ -1288,15 +1688,13 @@
 	if(b == 0)
 		return;
 	while(p = Brdline(b, '\n')){
-		p[Blinelen(b)-1] = 0;
+		p[Blinelen(b) - 1] = 0;
 		while(*p && (*p == ' ' || *p == '\t'))
 			p++;
 		if(*p == '#')
 			continue;
-		i = malloc(sizeof(Ignorance));
-		if(i == 0)
-			break;
-		i->partial = strlen(p);
+		i = emalloc(sizeof *i);
+		i->len = strlen(p);
 		i->str = strdup(p);
 		if(i->str == 0){
 			free(i);
@@ -1315,159 +1713,37 @@
 
 	readignore();
 	for(i = ignorance; i != nil; i = i->next)
-		if(cistrncmp(i->str, p, i->partial) == 0)
+		if(cistrncmp(i->str, p, i->len) == 0)
 			return 1;
 	return 0;
 }
 
 int
-hdrlen(char *p, char *e)
-{
-	char *ep;
-
-	ep = p;
-	do {
-		ep = strchr(ep, '\n');
-		if(ep == nil){
-			ep = e;
-			break;
-		}
-		ep++;
-		if(ep >= e){
-			ep = e;
-			break;
-		}
-	} while(*ep == ' ' || *ep == '\t');
-	return ep - p;
-}
-
-// rfc2047 non-ascii: =?charset?q?encoded-text?=
-int
-rfc2047convert(String *s, char *token, int len)
-{
-	char charset[100], decoded[1024], *e, *x;
-	int l;
-
-	if(len == 0)
-		return -1;
-
-	e = token+len-2;
-	token += 2;
-
-	x = memchr(token, '?', e-token);
-	if(x == nil || (l=x-token) >= sizeof charset)
-		return -1;
-	memmove(charset, token, l);
-	charset[l] = 0;
-
-	token = x+1;
-
-	// bail if it doesn't fit 
-	if(e-token > sizeof(decoded)-1)
-		return -1;
-
-	// bail if we don't understand the encoding
-	if(cistrncmp(token, "b?", 2) == 0){
-		token += 2;
-		len = dec64((uchar*)decoded, sizeof(decoded), token, e-token);
-		decoded[len] = 0;
-	} else if(cistrncmp(token, "q?", 2) == 0){
-		token += 2;
-		len = decquoted(decoded, token, e, 1);
-		if(len > 0 && decoded[len-1] == '\n')
-			len--;
-		decoded[len] = 0;
-	} else
-		return -1;
-
-	if(xtoutf(charset, &x, decoded, decoded+len) <= 0)
-		s_append(s, decoded);
-	else {
-		s_append(s, x);
-		free(x);
-	}
-	return 0;
-}
-
-char*
-rfc2047start(char *start, char *end)
-{
-	int quests;
-
-	if(*--end != '=')
-		return nil;
-	if(*--end != '?')
-		return nil;
-
-	quests = 0;
-	for(end--; end >= start; end--){
-		switch(*end){
-		case '=':
-			if(quests == 3 && *(end+1) == '?')
-				return end;
-			break;
-		case '?':
-			++quests;
-			break;
-		case ' ':
-		case '\t':
-		case '\n':
-		case '\r':
-			/* can't have white space in a token */
-			return nil;
-		}
-	}
-	return nil;
-}
-
-// convert a header line
-String*
-stringconvert(String *s, char *uneaten, int len)
-{
-	char *token, *p, *e;
-
-	s = s_reset(s);
-	p = uneaten;
-	for(e = p+len; p < e; ){
-		while(*p++ == '=' && (token = rfc2047start(uneaten, p))){
-			s_nappend(s, uneaten, token-uneaten);
-			if(rfc2047convert(s, token, p - token) < 0)
-				s_nappend(s, token, p - token);
-			uneaten = p;
-			for(; p<e && isspace(*p);)
-				p++;
-			if(p+2 < e && p[0] == '=' && p[1] == '?')
-				uneaten = p;	// paste
-		}
-	}
-	if(p > uneaten)
-		s_nappend(s, uneaten, p-uneaten);
-	return s;
-}
-
-int
 readheader(Message *m, char *buf, int off, int cnt)
 {
-	char *p, *e;
-	int n, ns;
-	char *to = buf;
-	String *s;
+	char *s, *end, *se, *p, *e, *to;
+	int n, ns, salloc;
 
+	to = buf;
 	p = m->header;
 	e = m->hend;
-	s = nil;
+	s = emalloc(salloc = 2048);
+	end = s + salloc;
 
-	// copy in good headers
+	/* copy in good headers */
 	while(cnt > 0 && p < e){
 		n = hdrlen(p, e);
+		assert(n > 0);
 		if(ignore(p)){
 			p += n;
 			continue;
 		}
-
-		// rfc2047 processing
-		s = stringconvert(s, p, n);
-		ns = s_len(s);
+		if(n + 1 > salloc){
+			s = erealloc(s, salloc = n + 1);
+			end = s + salloc;
+		}
+		se = rfc2047(s, end, p, n, 0);
+		ns = se - s;
 		if(off > 0){
 			if(ns <= off){
 				off -= ns;
@@ -1478,34 +1754,16 @@
 		}
 		if(ns > cnt)
 			ns = cnt;
-		memmove(to, s_to_c(s)+off, ns);
+		memmove(to, s + off, ns);
 		to += ns;
 		p += n;
 		cnt -= ns;
 		off = 0;
 	}
-
-	s_free(s);
+	free(s);
 	return to - buf;
 }
 
-int
-headerlen(Message *m)
-{
-	char buf[1024];
-	int i, n;
-
-	if(m->hlen >= 0)
-		return m->hlen;
-	for(n = 0; ; n += i){
-		i = readheader(m, buf, n, sizeof(buf));
-		if(i <= 0)
-			break;
-	}
-	m->hlen = n;
-	return n;
-}
-
 QLock hashlock;
 
 uint
@@ -1545,6 +1803,7 @@
 	int h;
 	Hash *hp, **l;
 
+//if(m)sanemsg(m);
 	qlock(&hashlock);
 	h = hash(ppath, name);
 	for(l = &htab[h]; *l != nil; l = &(*l)->next){
@@ -1595,25 +1854,43 @@
 	int refs = 0;
 
 	qlock(&hashlock);
-	for(h = 0; h < Hsize; h++){
+	for(h = 0; h < Hsize; h++)
 		for(hp = htab[h]; hp != nil; hp = hp->next)
 			if(hp->mb == mb)
 				refs++;
-	}
 	qunlock(&hashlock);
 	return refs;
 }
 
 void
+checkmboxrefs(void)
+{
+	int refs;
+	Mailbox *mb;
+
+//	qlock(&mbllock);
+	for(mb = mbl; mb; mb = mb->next){
+		qlock(mb);
+		refs = fidmboxrefs(mb) + 1;
+		if(refs != mb->refs){
+			eprint("%s:%s ref mismatch got %d expected %d\n", mb->name, mb->path, refs, mb->refs);
+			abort();
+		}
+		qunlock(mb);
+	}
+//	qunlock(&mbllock);
+}
+
+void
 post(char *name, char *envname, int srvfd)
 {
-	int fd;
 	char buf[32];
+	int fd;
 
 	fd = create(name, OWRITE, 0600);
 	if(fd < 0)
 		error("post failed");
-	sprint(buf, "%d",srvfd);
+	snprint(buf, sizeof buf, "%d", srvfd);
 	if(write(fd, buf, strlen(buf)) != strlen(buf))
 		error("srv write");
 	close(fd);
--- /dev/null
+++ b/sys/src/cmd/upas/fs/header.c
@@ -1,0 +1,176 @@
+#include "common.h"
+#include <ctype.h>
+#include <libsec.h>
+#include "dat.h"
+
+int
+hdrlen(char *p, char *e)
+{
+	char *ep;
+
+	ep = p;
+	do {
+		ep = strchr(ep, '\n');
+		if(ep == nil){
+			ep = e;
+			break;
+		}
+		if(ep == p)
+			break;
+		if(ep - p == 1 && ep[-1] == '\r')
+			break;
+		ep++;
+		if(ep >= e){
+			ep = e;
+			break;
+		}
+	} while(*ep == ' ' || *ep == '\t');
+	return ep - p;
+}
+
+/* rfc2047 non-ascii: =?charset?q?encoded-text?= */
+static int
+tok(char **sp, char *se, char *token, int len)
+{
+	char charset[100], *s, *e, *x;
+	int l;
+
+	if(len == 0)
+		return -1;
+	s = *sp;
+	e = token + len - 2;
+	token += 2;
+
+	x = memchr(token, '?', e - token);
+	if(x == nil || (l = x - token) >= sizeof charset)
+		return -1;
+	memmove(charset, token, l);
+	charset[l] = 0;
+
+	/* bail if it doesn't fit */
+	token = x + 1;
+	if(e - token > se - s - 1)
+		return -1;
+
+	if(cistrncmp(token, "b?", 2) == 0){
+		token += 2;
+		len = dec64((uchar*)s, se - s - 1, token, e - token);
+		if(len == -1)
+			return -1;
+		s[len] = 0;
+	}else if(cistrncmp(token, "q?", 2) == 0){
+		token += 2;
+		len = decquoted(s, token, e, 1);
+		if(len > 0 && s[len - 1] == '\n')
+			len--;
+		s[len] = 0;
+	}else
+		return -1;
+
+	if(xtoutf(charset, &x, s, s + len) <= 0)
+		s += len;
+	else {
+		s = seprint(s, se, "%s", x);
+		free(x);
+	}
+	*sp = s;
+	return 0;
+}
+
+char*
+tokbegin(char *start, char *end)
+{
+	int quests;
+
+	if(*--end != '=')
+		return nil;
+	if(*--end != '?')
+		return nil;
+
+	quests = 0;
+	for(end--; end >= start; end--){
+		switch(*end){
+		case '=':
+			if(quests == 3 && *(end + 1) == '?')
+				return end;
+			break;
+		case '?':
+			++quests;
+			break;
+		case ' ':
+		case '\t':
+		case '\n':
+		case '\r':
+			/* can't have white space in a token */
+			return nil;
+		}
+	}
+	return nil;
+}
+
+static char*
+seappend822f(char *s, char *e, char *a, int n)
+{
+	int skip, c;
+
+	skip = 0;
+	for(; n--; a++){
+		c = *a;
+		if(skip && isspace(c))
+			continue;
+		if(c == '\n'){
+			c = ' ';
+			skip = 1;
+		}else{
+			if(c < 0x20)
+				continue;
+			skip = 0;
+		}
+		s = sputc(s, e, c);
+	}
+	return s;
+}
+
+static char*
+seappend822(char *s, char *e, char *a, int n)
+{
+	int c;
+
+	for(; n--; a++){
+		c = *a;
+		if(c < 0x20 && c != '\n' && c != '\t')
+			continue;
+		s = sputc(s, e, c);
+	}
+	return s;
+}
+
+/* convert a header line */
+char*
+rfc2047(char *s, char *se, char *uneaten, int len, int fold)
+{
+	char *sp, *token, *p, *e;
+	char *(*f)(char*, char*, char*, int);
+
+	f = seappend822;
+	if(fold)
+		f = seappend822f;
+	sp = s;
+	p = uneaten;
+	for(e = p + len; p < e; ){
+		while(*p++ == '=' && (token = tokbegin(uneaten, p))){
+			sp = f(sp, se, uneaten, token - uneaten);
+			if(tok(&sp, se, token, p - token) < 0)
+				sp = f(sp, se, token, p - token);
+			uneaten = p;
+			for(; p < e && isspace(*p);)
+				p++;
+			if(p + 2 < e && p[0] == '=' && p[1] == '?')
+				uneaten = p;	/* paste */
+		}
+	}
+	if(p > uneaten)
+		sp = f(sp, se, uneaten, e - uneaten);
+	*sp = 0;
+	return sp;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/idx.c
@@ -1,0 +1,535 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+#define idprint(...)	if(iflag > 1) fprint(2, __VA_ARGS__); else {}
+#define iprint(...)		if(iflag) fprint(2, __VA_ARGS__); else {}
+
+static char *magic		= "idx magic v7\n";
+static char *mbmagic	= "genericv1";
+enum {
+	Idxfields		= 21,
+
+	Idxto		= 30000,		/* index timeout in ms */
+	Idxstep		= 300,		/* sleep between tries */
+};
+
+void
+idxfree(Idx *i)
+{
+	if(i->str)
+		free(i->str);
+	else{
+		free(i->digest);
+		free(i->ffrom);
+		free(i->from);
+		free(i->to);
+		free(i->cc);
+		free(i->bcc);
+		free(i->replyto);
+		free(i->messageid);
+		free(i->subject);
+		free(i->sender);
+		free(i->inreplyto);
+		free(i->idxaux);
+	}
+	memset(i, 0, sizeof *i);
+}
+
+static char*
+∂(char *x)
+{
+	if(x)
+		return x;
+	return "";
+}
+
+static int
+pridxmsg(Biobuf *b, Idx *x)
+{
+	Bprint(b, "%#A %ux %D %lud ", x->digest, x->flags&~Frecent, x->fileid, x->lines);
+	Bprint(b, "%q %q %q %q %q ", ∂(x->ffrom), ∂(x->from), ∂(x->to), ∂(x->cc), ∂(x->bcc));
+	Bprint(b, "%q %q %q %q %q ", ∂(x->replyto), ∂(x->messageid), ∂(x->subject), ∂(x->sender), ∂(x->inreplyto));
+	Bprint(b, "%s %d %lud %lud ", rtab[x->type].s, x->disposition, x->size, x->rawbsize);
+	Bprint(b, "%lud %q %d\n", x->ibadchars, ∂(x->idxaux), x->nparts);
+	return 0;
+}
+
+static int
+pridx0(Biobuf *b, Mailbox *mb, Message *m, int l)
+{
+	for(; m; m = m->next){
+		if(l == 0)
+		if(insurecache(mb, m) == -1)
+			continue;
+		if(pridxmsg(b, m))
+			return -1;
+		if(m->part)
+			pridx0(b, mb, m->part, l + 1);
+		m->cstate &= ~Cidxstale;
+		m->cstate |= Cidx;
+		if(l == 0)
+			msgdecref(mb, m);
+	}
+	return 0;
+}
+
+void
+genericidxwrite(Biobuf *b, Mailbox*)
+{
+	Bprint(b, "%s\n", mbmagic);
+}
+
+static int
+pridx(Biobuf *b, Mailbox *mb)
+{
+	int i;
+
+	Bprint(b, magic);
+	mb->idxwrite(b, mb);
+//	prrefs(b);
+	i = pridx0(b, mb, mb->root->part, 0);
+	return i;
+}
+
+static char *eopen[] = {
+	"not found",
+	"does not exist",
+	"file is locked",
+	"file locked",
+	"exclusive lock",
+	0,
+};
+
+static char *ecreate[] = {
+	"already exists",
+	"file is locked",
+	"file locked",
+	"exclusive lock",
+	0,
+};
+
+static int
+bad(char **t)
+{
+	char buf[ERRMAX];
+	int i;
+
+	rerrstr(buf, sizeof buf);
+	for(i = 0; t[i]; i++)
+		if(strstr(buf, t[i]))
+			return 0;
+	return 1;
+}
+
+static int
+forceexcl(int fd)
+{
+	int r;
+	Dir *d;
+
+	d = dirfstat(fd);
+	if(d == nil)
+		return 0;			/* ignore: assume file removed */
+	if(d->mode & DMEXCL){
+		free(d);
+		return 0;
+	}
+	d->mode |= DMEXCL;
+	d->qid.type |= QTEXCL;
+	r = dirfwstat(fd, d);
+	free(d);
+	if(r == -1)
+		return 0;			/* ignore unwritable (e.g dump) */
+	close(fd);
+	return -1;
+}
+
+static int
+exopen(char *s)
+{
+	int i, fd;
+
+	for(i = 0; i < Idxto/Idxstep; i++){
+		if((fd = open(s, OWRITE|OTRUNC)) >= 0 || bad(eopen)){
+			if(fd != -1 && forceexcl(fd) == -1)
+				continue;
+			return fd;
+		}
+		if((fd = create(s, OWRITE|OEXCL, DMTMP|DMEXCL|0600)) >= 0  || bad(ecreate))
+			return fd;
+		sleep(Idxstep);
+	}
+	werrstr("lock timeout");
+	return -1;
+}
+
+static Message*
+findmessage(Mailbox *, Message *parent, int n)
+{
+	Message *m;
+
+	for(m = parent->part; m; m = m->next)
+		if(!m->digest && n-- == 0)
+			return m;
+	return 0;
+}
+
+static int
+validmessage(Mailbox *mb, Message *m, int level)
+{
+	if(level){
+		if(m->digest != 0)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+		if(m->fileid != 0)
+			goto lose;
+	}else{
+		if(m->digest == 0)
+			goto lose;
+		if(m->size == 0)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+			goto lose;
+		if(mtreefind(mb, m->digest))
+			goto lose;
+	}
+	return 1;
+lose:
+	eprint("invalid cache[%d] %#A size %ld %D\n", level, m->digest, m->size, m->fileid);
+	return 0;
+}
+
+/*
+ * n.b.: we don't insure this is the index version we last read.
+ *
+ * we may overwrite changes.  dualing deletes should sync eventually.
+ * mboxsync should complain about missing messages but
+ * mutable information (which is not in the email itself)
+ * may be lost.
+ */
+int
+wridxfile(Mailbox *mb)
+{
+	char buf[Pathlen + 4];
+	int r, fd;
+	Biobuf b;
+	Dir *d;
+
+	assert(semacquire(&mb->idxsem, 0) != -1);
+	snprint(buf, sizeof buf, "%s.idx", mb->path);
+	iprint("wridxfile %s\n", buf);
+	if((fd = exopen(buf)) == -1){
+		rerrstr(buf, sizeof buf);
+		if(strcmp(buf, "no creates") != 0)
+		if(strstr(buf, "file system read only") == 0)
+			eprint("wridxfile: %r\n");
+		semrelease(&mb->idxsem, 1);
+		return -1;
+	}
+	seek(fd, 0, 0);
+	Binit(&b, fd, OWRITE);
+	r = pridx(&b, mb);
+	Bterm(&b);
+	d = dirfstat(fd);
+	if(d == 0)
+		sysfatal("dirfstat: %r");
+	mb->qid = d->qid;
+	free(d);
+	close(fd);
+	semrelease(&mb->idxsem, 1);
+	return r;
+}
+
+static int
+nibble(int c)
+{
+	if(c >= '0' && c <= '9')
+		return c - '0';
+	if(c < 0x20)
+		c += 0x20;
+	if(c >= 'a' && c <= 'f')
+		return c - 'a'+10;
+	return 0xff;
+}
+
+static uchar*
+hackdigest(char *s)
+{
+	uchar t[SHA1dlen];
+	int i;
+
+	if(strcmp(s, "-") == 0)
+		return 0;
+	if(strlen(s) != 2*SHA1dlen){
+		eprint("bad digest %s\n", s);
+		return 0;
+	}
+	for(i = 0; i < SHA1dlen; i++)
+		t[i] = nibble(s[2*i])<<4 | nibble(s[2*i + 1]);
+	memmove(s, t, SHA1dlen);
+	return (uchar*)s;
+}
+
+static uvlong
+rdfileid(char *s, int level)
+{
+	char *p;
+	uvlong uv;
+
+	uv = strtoul(s, &p, 0);
+	if((level == 0 && uv < 1000000) || *p != '.')
+		return 0;
+	return uv<<8 | strtoul(p + 1, 0, 10);
+}
+
+static char*
+∫(char *x)
+{
+	if(x && *x)
+		return x;
+	return nil;
+}
+
+/*
+ * strategy:  use top-level avl tree to merge index with
+ * our ideas about the mailbox.  new or old messages
+ * with corrupt index entries are marked Dead.  they
+ * will be cleared out of the mailbox and are kept out
+ * of the index.  when messages are marked Dead, a
+ * reread of the mailbox is forced.
+ *
+ * side note.  if we get a new message while we are
+ * running it is added to the list in order but m->id
+ * looks out-of-order.  this is because m->id must
+ * increase monotonicly.  a new instance of the fs
+ * will result in a different ordering.
+ */
+
+static int
+rdidx(Biobuf *b, Mailbox *mb, Message *parent, int npart, int level, int doplumb)
+{
+	char *f[Idxfields + 1], *s;
+	uchar *digest;
+	int n, flags, nparts, good, bad, redux;
+	Message *m, **ll, *l;
+
+	bad = good = redux = 0;
+	ll = &parent->part;
+	nparts = npart;
+	for(; npart != 0 && (s = Brdstr(b, '\n', 1)); npart--){
+		m = 0;
+		digest = 0;
+		n = tokenize(s, f, nelem(f));
+		if(n != Idxfields){
+dead:
+			eprint("bad index %#A %d %d n=%d\n", digest, level, npart, n);
+			bad++;
+			free(s);
+			if(level)
+				return -1;
+			if(m)
+				m->deleted = Dead;
+			continue;
+		}
+		digest = hackdigest(f[0]);
+		if(digest == 0 ^ level != 0)
+			goto dead;
+		if(level == 0)
+			m = mtreefind(mb, digest);
+		else
+			m = findmessage(mb, parent, nparts - npart);
+		if(m){
+			/*
+			 * read in mutable information.
+			 * currently this is only flags
+			 */
+			redux++;
+			if(level == 0)
+				m->deleted &= ~Dmark;
+			if(m->nparts)
+			if(rdidx(b, mb, m, m->nparts, level + 1, 0) == -1)
+				goto dead;
+			ll = &m->next;
+			idprint("%d seen before %d... %.2ux", level, m->id, m->cstate);
+			flags = m->flags;
+			m->flags |= strtoul(f[1], 0, 16);
+			if(flags != m->flags)
+				m->cstate |= Cidxstale;
+			m->cstate |= Cidx;
+			idprint("→%.2ux\n", m->cstate);
+			free(s);
+			// s = 0;
+			continue;
+		}
+		m = newmessage(parent);
+		idprint("%d new %d %#A\n", level, m->id, digest);
+		m->digest = digest;
+		m->flags = strtoul(f[1], 0, 16);
+		m->fileid = rdfileid(f[2], level);
+		m->lines = atoi(f[3]);
+		m->ffrom = ∫(f[4]);
+		m->from = ∫(f[5]);
+		m->to = ∫(f[6]);
+		m->cc = ∫(f[7]);
+		m->bcc = ∫(f[8]);
+		m->replyto = ∫(f[9]);
+		m->messageid = ∫(f[10]);
+		m->subject = ∫(f[11]);
+		m->sender = ∫(f[12]);
+		m->inreplyto = ∫(f[13]);
+		m->type = newrefs(f[14]);
+		m->disposition = atoi(f[15]);
+		m->size = strtoul(f[16], 0, 0);
+		m->rawbsize = strtoul(f[17], 0, 0);
+		m->ibadchars = strtoul(f[18], 0, 0);
+		m->idxaux = ∫(f[19]);
+		m->nparts = strtoul(f[20], 0, 0);
+		m->cstate &= ~Cidxstale;
+		m->cstate |= Cidx;
+		m->str = s;
+		s = 0;
+		if(!validmessage(mb, m, level))
+			goto dead;
+		if(level == 0){
+			mtreeadd(mb, m);
+			m->inmbox = 1;
+		}
+		cachehash(mb, m);		/* hokey */
+		l = *ll;
+		*ll = m;
+		ll = &m->next;
+		*ll = l;
+		good++;
+
+		if(m->nparts)
+		if(rdidx(b, mb, m, m->nparts, level + 1, 0) == -1)
+			goto dead;
+		if(doplumb && level == 0)
+			mailplumb(mb, m, 0);
+	}
+	if(level == 0 && bad + redux > 0)
+		iprint("idx: %d %d %d\n", good, bad, redux);
+	if(bad)
+		return -1;
+	return 0;
+}
+
+/* bug: should check time. */
+static int
+qidcmp(int fd, Qid *q)
+{
+	int r;
+	Dir *d;
+	Qid q0;
+
+	d = dirfstat(fd);
+	if(!d)
+		sysfatal("dirfstat: %r");
+	r = 1;
+	if(d->qid.path == q->path)
+	if(d->qid.vers == q->vers)
+		r = 0;
+	q0 = *q;
+	*q = d->qid;
+	free(d);
+	if(q0.path != 0 && r)
+		iprint("qidcmp ... index changed [%ld .. %ld]\n", q0.vers, q->vers);
+	return r;
+}
+
+static int
+verscmp(Biobuf *b, Mailbox *mb)
+{
+	char *s;
+	int n;
+
+	n = -1;
+	if(s = Brdstr(b, '\n', 0))
+		n = strcmp(s, magic);
+	free(s);
+	if(n)
+		return -1;
+	n = -1;
+	if(s = Brdstr(b, '\n', 0))
+		n = mb->idxread(s, mb);
+	free(s);
+	return n;
+}
+
+int
+genericidxread(char *s, Mailbox*)
+{
+	return strcmp(s, mbmagic);
+}
+
+void
+genericidxinvalid(Mailbox *mb)
+{
+	if(mb->d)
+		memset(&mb->d->qid, 0, sizeof mb->d->qid);
+	mb->waketime = time(0);
+}
+
+void
+mark(Mailbox *mb)
+{
+	Message *m;
+
+	for(m = mb->root->part; m != nil; m = m->next)
+		m->deleted |= Dmark;
+}
+
+int
+unmark(Mailbox *mb)
+{
+	int i;
+	Message *m;
+
+	i = 0;
+	for(m = mb->root->part; m != nil; m = m->next)
+		if(m->deleted & Dmark){
+			i++;
+			m->deleted &= ~Dmark;	/* let mailbox scan figure this out.  BOTCH?? */
+		}
+	return i;
+}
+
+int
+rdidxfile0(Mailbox *mb, int doplumb)
+{
+	char buf[Pathlen + 4];
+	int r, v;
+	Biobuf *b;
+
+	snprint(buf, sizeof buf, "%s.idx", mb->path);
+	b = Bopen(buf, OREAD);
+	if(b == nil)
+		return -2;
+	if(qidcmp(Bfildes(b), &mb->qid) == 0)
+		r = 0;
+	else if(verscmp(b, mb) == -1)
+		r = -1;
+	else{
+		mark(mb);
+		r = rdidx(b, mb, mb->root, -1, 0, doplumb);
+		v = unmark(mb);
+		if(r == 0 && v > 0)
+			r = -1;
+	}
+	Bterm(b);
+	return r;
+}
+
+int
+rdidxfile(Mailbox *mb, int doplumb)
+{
+	int r;
+
+	assert(semacquire(&mb->idxsem, 0) > 0);
+	r = rdidxfile0(mb, doplumb);
+	if(r == -1 && mb->idxinvalid)
+		mb->idxinvalid(mb);
+	semrelease(&mb->idxsem, 1);
+	return r;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/imap.c
@@ -1,0 +1,1271 @@
+/*
+ * todo:
+ * 1.	sync with imap server's flags
+ * 2.	better algorithm for avoiding downloading message list.
+ * 3.	get sender — eating envelope is lots of work!
+ */
+#include "common.h"
+#include <libsec.h>
+#include <auth.h>
+#include "dat.h"
+
+#define	idprint(i, ...)	if(i->flags & Fdebug) fprint(2, __VA_ARGS__); else {}
+#pragma varargck argpos	imap4cmd	2
+#pragma varargck	type	"Z"		char*
+#pragma varargck	type	"U"		uvlong
+#pragma varargck	type	"U"		vlong
+
+static char	confused[]	= "confused about fetch response";
+static char	qsep[]		= " \t\r\n";
+static char	Eimap4ctl[]	= "bad imap4 control message";
+
+enum{
+	/* cap */
+	Cnolog	= 1<<0,
+	Ccram	= 1<<1,
+	Cntlm	= 1<<2,
+
+	/* flags */
+	Fssl	= 1<<0,
+	Fdebug	= 1<<1,
+	Fgmail	= 1<<2,
+};
+
+typedef struct {
+	uvlong	uid;
+	ulong	sizes;
+	ulong	dates;
+} Fetchi;
+
+typedef struct Imap Imap;
+struct Imap {
+	long	lastread;
+
+	char	*mbox;
+	/* free this to free the strings below */
+	char	*freep;
+	char	*host;
+	char	*user;
+
+	int	refreshtime;
+	uchar	cap;
+	uchar	flags;
+
+	ulong	tag;
+	ulong	validity;
+	int	nmsg;
+	int	size;
+
+	Fetchi	*f;
+	int	nuid;
+	int	muid;
+
+	Thumbprint *thumb;
+
+	/* open network connection */
+	Biobuf	bin;
+	Biobuf	bout;
+	int	binit;
+	int	fd;
+};
+
+enum
+{
+	Qok = 0,
+	Qquote,
+	Qbackslash,
+};
+
+static int
+needtoquote(Rune r)
+{
+	if(r >= Runeself)
+		return Qquote;
+	if(r <= ' ')
+		return Qquote;
+	if(r == '\\' || r == '"')
+		return Qbackslash;
+	return Qok;
+}
+
+static int
+Zfmt(Fmt *f)
+{
+	char *s, *t;
+	int w, quotes;
+	Rune r;
+
+	s = va_arg(f->args, char*);
+	if(s == 0 || *s == 0)
+		return fmtstrcpy(f, "\"\"");
+
+	quotes = 0;
+	for(t = s; *t; t += w){
+		w = chartorune(&r, t);
+		quotes |= needtoquote(r);
+	}
+	if(quotes == 0)
+		return fmtstrcpy(f, s);
+
+	fmtrune(f, '"');
+	for(t = s; *t; t += w){
+		w = chartorune(&r, t);
+		if(needtoquote(r) == Qbackslash)
+			fmtrune(f, '\\');
+		fmtrune(f, r);
+	}
+	return fmtrune(f, '"');
+}
+
+static int
+Ufmt(Fmt *f)
+{
+	char buf[20*2 + 2];
+	ulong a, b;
+	uvlong u;
+
+	u = va_arg(f->args, uvlong);
+	if(u == 1)
+		return fmtstrcpy(f, "nil");
+	if(u == 0)
+		return fmtstrcpy(f, "-");
+	a = u>>32;
+	b = u;
+	snprint(buf, sizeof buf, "%lud:%lud", a, b);
+	return fmtstrcpy(f, buf);
+}
+
+static void
+imap4cmd(Imap *imap, char *fmt, ...)
+{
+	char buf[256], *p;
+	va_list va;
+
+	va_start(va, fmt);
+	p = buf + sprint(buf, "9x%lud ", imap->tag);
+	vseprint(p, buf + sizeof buf, fmt, va);
+	va_end(va);
+
+	p = buf + strlen(buf);
+	if(p > buf + sizeof buf - 3)
+		sysfatal("imap4 command too long");
+	idprint(imap, "-> %s\n", buf);
+	strcpy(p, "\r\n");
+	Bwrite(&imap->bout, buf, strlen(buf));
+	Bflush(&imap->bout);
+}
+
+enum {
+	Ok,
+	No,
+	Bad,
+	Bye,
+	Exists,
+	Status,
+	Fetch,
+	Cap,
+	Auth,
+
+	Unknown,
+};
+
+static char *verblist[] = {
+[Ok]	"ok",
+[No]	"no",
+[Bad]	"bad",
+[Bye]	"bye",
+[Exists]	"exists",
+[Status]	"status",
+[Fetch]	"fetch",
+[Cap]	"capability",
+[Auth]	"authenticate",
+};
+
+static int
+verbcode(char *verb)
+{
+	int i;
+	char *q;
+
+	if(q = strchr(verb, ' '))
+		*q = '\0';
+	for(i = 0; i < nelem(verblist) - 1; i++)
+		if(strcmp(verblist[i], verb) == 0)
+			break;
+	if(q)
+		*q = ' ';
+	return i;
+}
+
+static vlong
+mkuid(Imap *i, char *id)
+{
+	vlong v;
+
+	v = (vlong)i->validity<<32;
+	return v | strtoul(id, 0, 10);
+}
+
+static vlong
+xnum(char *s, int a, int b)
+{
+	vlong v;
+
+	if(*s != a)
+		return -1;
+	v = strtoull(s + 1, &s, 10);
+	if(*s != b)
+		return -1;
+	return v;
+}
+
+static struct{
+	char	*flag;
+	int	e;
+} ftab[] = {
+	"Answered",	Fanswered,
+	"\\Deleted",	Fdeleted,
+	"\\Draft",		Fdraft,
+	"\\Flagged",	Fflagged,
+	"\\Recent",	Frecent,
+	"\\Seen",		Fseen,
+	"\\Stored",	Fstored,
+};
+
+static void
+parseflags(Message *m, char *s)
+{
+	char *f[10];
+	int i, j, j0, n;
+
+	n = tokenize(s, f, nelem(f));
+	qsort(f, n, sizeof *f, (int (*)(void*,void*))strcmp);
+	j = 0;
+	for(i = 0; i < n; i++)
+		for(j0 = j;; j++){
+			if(j == nelem(ftab)){
+				j = j0;		/* restart search */
+				break;
+			}
+			if(strcmp(f[i], ftab[j].flag) == 0){
+				m->flags |= ftab[j].e;
+				break;
+			}
+		}
+}
+
+/* "17-Jul-1996 02:44:25 -0700" */
+long
+internaltounix(char *s)
+{
+	Tm tm;
+	if(strlen(s) < 20 || s[2] != '-' || s[6] != '-')
+		return -1;
+	s[2] = ' ';
+	s[6] = ' ';
+	if(strtotm(s, &tm) == -1)
+		return -1;
+	return tm2sec(&tm);
+}
+	
+static char*
+qtoken(char *s, char *sep)
+{
+	int quoting;
+	char *t;
+
+	quoting = 0;
+	t = s;	/* s is output string, t is input string */
+	while(*t!='\0' && (quoting || utfrune(sep, *t)==nil)){
+		if(*t != '"' && *t  != '(' && *t != ')'){
+			*s++ = *t++;
+			continue;
+		}
+		/* *t is a quote */
+		if(!quoting || *t == '('){
+			quoting++;
+			t++;
+			continue;
+		}
+		/* quoting and we're on a quote */
+		if(t[1] != '"'){
+			/* end of quoted section; absorb closing quote */
+			t++;
+			if(quoting > 0)
+				quoting--;
+			continue;
+		}
+		/* doubled quote; fold one quote into two */
+		t++;
+		*s++ = *t++;
+	}
+	if(*s != '\0'){
+		*s = '\0';
+		if(t == s)
+			t++;
+	}
+	return t;
+}
+
+int
+imaptokenize(char *s, char **args, int maxargs)
+{
+	int nargs;
+
+	for(nargs=0; nargs < maxargs; nargs++){
+		while(*s!='\0' && utfrune(qsep, *s)!=nil)
+			s++;
+		if(*s == '\0')
+			break;
+		args[nargs] = s;
+		s = qtoken(s, qsep);
+	}
+
+	return nargs;
+}
+
+static char*
+fetchrsp(Imap *imap, char *p, Mailbox *, Message *m)
+{
+	char *f[15], *s, *q;
+	int i, n, a;
+	ulong o, l;
+	uvlong v;
+	static char error[256];
+	extern void msgrealloc(Message*, ulong);
+
+redux:
+	n = imaptokenize(p, f, nelem(f));
+	if(n%2)
+		return confused;
+	for(i = 0; i < n; i += 2){
+		if(strcmp(f[i], "internaldate") == 0){
+			l = internaltounix(f[i + 1]);
+			if(l < 418319360)
+				abort();
+			if(imap->nuid < imap->muid)
+				imap->f[imap->nuid].dates = l;
+		}else if(strcmp(f[i], "rfc822.size") == 0){
+			l = strtoul(f[i + 1], 0, 0);
+			if(m)
+				m->size = l;
+			else if(imap->nuid < imap->muid)
+				imap->f[imap->nuid].sizes = l;
+		}else if(strcmp(f[i], "uid") == 0){
+			v = mkuid(imap, f[1]);
+			if(m)
+				m->imapuid = v;
+			if(imap->nuid < imap->muid)
+				imap->f[imap->nuid].uid = v;
+		}else if(strcmp(f[i], "flags") == 0)
+			parseflags(m, f[i + 1]);
+		else if(strncmp(f[i], "body[]", 6) == 0){
+			s = f[i]+6;
+			o = 0;
+			if(*s == '<')
+				o = xnum(s, '<', '>');
+			if(o == -1)
+				return confused;
+			l = xnum(f[i + 1], '{', '}');
+			a = o + l - m->ibadchars - m->size;
+			if(a > 0){
+				assert(imap->flags & Fgmail);
+				m->size = o + l;
+				msgrealloc(m, m->size);
+				m->size -= m->ibadchars;
+			}
+			if(Bread(&imap->bin, m->start + o, l) != l){
+				snprint(error, sizeof error, "read: %r");
+				return error;
+			}
+			if(Bgetc(&imap->bin) == ')'){
+				while(Bgetc(&imap->bin) != '\n')
+					;
+				return 0;
+			}
+			/* evil */
+			if(!(p = Brdline(&imap->bin, '\n')))
+				return 0;
+			q = p + Blinelen(&imap->bin);
+			while(q > p && (q[-1] == '\n' || q[-1] == '\r'))
+				q--;
+			*q = 0;
+			lowercase(p);
+			idprint(imap, "<- %s\n", p);
+
+			goto redux;
+		}else
+			return confused;
+	}
+	return 0;
+}
+
+void
+parsecap(Imap *imap, char *s)
+{
+	char *t[32], *p;
+	int n, i;
+
+	s = strdup(s);
+	n = getfields(s, t, nelem(t), 0, " ");
+	for(i = 0; i < n; i++){
+		if(strncmp(t[i], "auth=", 5) == 0){
+			p = t[i] + 5;
+			if(strcmp(p, "cram-md5") == 0)
+				imap->cap |= Ccram;
+			if(strcmp(p, "ntlm") == 0)
+				imap->cap |= Cntlm;
+		}else if(strcmp(t[i], "logindisabled") == 0)
+			imap->cap |= Cnolog;
+	}
+	free(s);
+}
+
+/*
+ *  get imap4 response line.  there might be various
+ *  data or other informational lines mixed in.
+ */
+static char*
+imap4resp0(Imap *imap, Mailbox *mb, Message *m)
+{
+	char *e, *line, *p, *ep, *op, *q, *verb;
+	int n, unexp;
+	static char error[256];
+
+	unexp = 0;
+	while(p = Brdline(&imap->bin, '\n')){
+		ep = p + Blinelen(&imap->bin);
+		while(ep > p && (ep[-1] == '\n' || ep[-1] == '\r'))
+			*--ep = '\0';
+		idprint(imap, "<- %s\n", p);
+		if(unexp && p[0] != '9' && p[1] != 'x')
+		if(strtoul(p + 2, &p, 10) != imap->tag)
+			continue;
+		if(p[0] != '+')
+			lowercase(p);		/* botch */
+
+		switch(p[0]){
+		case '+':				/* cram challenge */
+			if(ep - p > 2)
+				return p + 2;
+			break;
+		case '*':
+			if(p[1] != ' ')
+				continue;
+			p += 2;
+			line = p;
+			n = strtol(p, &p, 10);
+			if(*p == ' ')
+				p++;
+			verb = p;
+	
+			if(p = strchr(verb, ' '))
+				p++;
+			else
+				p = verb + strlen(verb);
+
+			switch(verbcode(verb)){
+			case Bye:
+				/* early disconnect */
+				snprint(error, sizeof error, "%s", p);
+				return error;
+			case Ok:
+			case No:
+			case Bad:
+				/* human readable text at p; */
+				break;
+			case Exists:
+				imap->nmsg = n;
+				break;
+			case Cap:
+				parsecap(imap, p);
+				break;
+			case Status:
+				/* * status inbox (messages 2 uidvalidity 960164964) */
+				if(q = strstr(p, "messages"))
+					imap->nmsg = strtoul(q + 8, 0, 10);
+				if(q = strstr(p, "uidvalidity"))
+					imap->validity = strtoul(q + 11, 0, 10);
+				break;
+			case Fetch:
+				if(*p == '('){
+					p++;
+					if(ep[-1] == ')')
+						*--ep = 0;
+				}
+				if(e = fetchrsp(imap, p, mb, m))
+					eprint("imap: fetchrsp: %s\n", e);
+				imap->nuid++;
+				break;
+			case Auth:
+				break;
+			}
+			if(imap->tag == 0)
+				return line;
+			break;
+		case '9':		/* response to our message */
+			op = p;
+			if(p[1] == 'x' && strtoul(p + 2, &p, 10) == imap->tag){
+				while(*p == ' ')
+					p++;
+				imap->tag++;
+				return p;
+			}
+			eprint("imap: expected %lud; got %s\n", imap->tag, op);
+			break;
+		default:
+			if(imap->flags&Fdebug || *p){
+				eprint("imap: unexpected line: %s\n", p);
+				unexp = 1;
+			}
+		}
+	}
+	snprint(error, sizeof error, "i/o error: %r\n");
+	return error;
+}
+
+static char*
+imap4resp(Imap *i)
+{
+	return imap4resp0(i, 0, 0);
+}
+
+static int
+isokay(char *resp)
+{
+	return cistrncmp(resp, "OK", 2) == 0;
+}
+
+static char*
+findflag(int idx)
+{
+	int i;
+
+	for(i = 0; i < nelem(ftab); i++)
+		if(ftab[i].e == 1<<idx)
+			return ftab[i].flag;
+	return nil;
+}
+
+static void
+imap4modflags(Mailbox *mb, Message *m, int flags)
+{
+	char buf[128], *p, *e, *fs;
+	int i, f;
+	Imap *imap;
+
+	imap = mb->aux;
+	e = buf + sizeof buf;
+	p = buf;
+	f = flags & ~Frecent;
+	for(i = 0; i < Nflags; i++)
+		if(f & 1<<i && (fs = findflag(i)))
+			p = seprint(p, e, "%s ", fs);
+	if(p > buf){
+		p[-1] = 0;
+		imap4cmd(imap, "uid store %lud flags (%s)", (ulong)m->imapuid, buf);
+		imap4resp(imap);
+	}
+}
+
+static char*
+imap4cram(Imap *imap)
+{
+	char *s, *p, ch[128], usr[64], rbuf[128], ubuf[128], ebuf[192];
+	int i, n, l;
+
+	fmtinstall('[', encodefmt);
+
+	imap4cmd(imap, "authenticate cram-md5");
+	p = imap4resp(imap);
+	if(p == nil)
+		return "no challenge";
+	l = dec64((uchar*)ch, sizeof ch, p, strlen(p));
+	if(l == -1)
+		return "bad base64";
+	ch[l] = 0;
+	idprint(imap, "challenge [%s]\n", ch);
+
+	if(imap->user == nil)
+		imap->user = getlog();
+	n = auth_respond(ch, l, usr, sizeof usr, rbuf, sizeof rbuf, auth_getkey,
+		"proto=cram role=client server=%q user=%s", imap->host, imap->user);
+	if(n == -1)
+		return "cannot find IMAP password";
+	for(i = 0; i < n; i++)
+		if(rbuf[i] >= 'A' && rbuf[i] <= 'Z')
+			rbuf[i] += 'a' - 'A';
+	l = snprint(ubuf, sizeof ubuf, "%s %.*s", usr, n, rbuf);
+	idprint(imap, "raw cram [%s]\n", ubuf);
+	snprint(ebuf, sizeof ebuf, "%.*[", l, ubuf);
+
+	imap->tag = 1;
+	idprint(imap, "-> %s\n", ebuf);
+	Bprint(&imap->bout, "%s\r\n", ebuf);
+	Bflush(&imap->bout);
+
+	if(!isokay(s = imap4resp(imap)))
+		return s;
+	return nil;
+}
+
+/*
+ *  authenticate to IMAP4 server using NTLM (untested)
+ * 
+ *  http://davenport.sourceforge.net/ntlm.html#ntlmImapAuthentication
+ *  http://msdn.microsoft.com/en-us/library/cc236621%28PROT.13%29.aspx
+ */
+static uchar*
+psecb(uchar *p, uint o, int n)
+{
+	p[0] = n;
+	p[1] = n>>8;
+	p[2] = n;
+	p[3] = n>>8;
+	p[4] = o;
+	p[5] = o>>8;
+	p[6] = o>>16;
+	p[7] = o>>24;
+	return p+8;
+}
+
+static uchar*
+psecq(uchar *q, char *s, int n)
+{
+	memcpy(q, s, n);
+	return q+n;
+}
+
+static char*
+imap4ntlm(Imap *imap)
+{
+	char *s, ruser[64], enc[256];
+	uchar buf[128], *p, *ep, *q, *eq, *chal;
+	int n;
+	MSchapreply mcr;
+
+	imap4cmd(imap, "authenticate ntlm");
+	imap4resp(imap);
+
+	/* simple NtLmNegotiate blob with NTLM+OEM flags */
+	imap4cmd(imap, "TlRMTVNTUAABAAAAAgIAAA==");
+	s = imap4resp(imap);
+	n = dec64(buf, sizeof buf, s, strlen(s));
+	if(n < 32 || memcmp(buf, "NTLMSSP", 8) != 0)
+		return "bad NtLmChallenge";
+	chal = buf+24;
+
+	if(auth_respond(chal, 8, ruser, sizeof ruser,
+			&mcr, sizeof mcr, auth_getkey,
+			"proto=mschap role=client service=imap server=%q user?",
+			imap->host) < 0)
+		return "auth_respond failed";
+
+	/* prepare NtLmAuthenticate blob */
+
+	memset(buf, sizeof buf, 0);
+	p = buf;
+	ep = p + 8 + 6*8 + 2*4;
+	q = ep;
+	eq = buf + sizeof buf;
+
+
+	memcpy(p, "NTLMSSP", 8);	/* magic */
+	p += 8;
+
+	*p++ = 3;
+	*p++ = 0;
+	*p++ = 0;
+	*p++ = 0;
+
+	p = psecb(p, q-buf, 24);		/* LMresp */
+	q = psecq(q, mcr.LMresp, 24);
+
+	p = psecb(p, q-buf, 24);		/* NTresp */
+	q = psecq(q, mcr.NTresp, 24);
+
+	p = psecb(p, q-buf, 0);		/* realm */
+
+	n = strlen(ruser);
+	p = psecb(p, q-buf, n);		/* user name */
+	q = psecq(q, ruser, n);
+
+	p = psecb(p, q-buf, 0);		/* workstation name */
+	p = psecb(p, q-buf, 0);		/* session key */
+
+	*p++ = 0x02;			/* flags: oem(2)|ntlm(0x200) */
+	*p++ = 0x02;
+	*p++ = 0;
+	*p++ = 0;
+
+	if(p > ep || q > eq)
+		return "error creating NtLmAuthenticate";
+	enc64(enc, sizeof enc, buf, q-buf);
+
+	imap4cmd(imap, enc);
+	if(!isokay(s = imap4resp(imap)))
+		return s;
+	return nil;
+}
+
+static char*
+imap4passwd(Imap *imap)
+{
+	char *s;
+	UserPasswd *up;
+
+	if(imap->user != nil)
+		up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q user=%q", imap->host, imap->user);
+	else
+		up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q", imap->host);
+	if(up == nil)
+		return "cannot find IMAP password";
+
+	imap->tag = 1;
+	imap4cmd(imap, "login %Z %Z", up->user, up->passwd);
+	free(up);
+	if(!isokay(s = imap4resp(imap)))
+		return s;
+	return nil;
+}
+
+static char*
+imap4login(Imap *imap)
+{
+	char *e;
+
+	if(imap->cap & Ccram)
+		e = imap4cram(imap);
+	else if(imap->cap & Cntlm)
+		e = imap4ntlm(imap);
+	else
+		e = imap4passwd(imap);
+	if(e)
+		return e;
+	imap4cmd(imap, "select %Z", imap->mbox);
+	if(!isokay(e = imap4resp(imap)))
+		return e;
+	return nil;
+}
+
+static char*
+imaperrstr(char *host, char *port)
+{
+	char err[ERRMAX];
+	static char buf[256];
+
+	err[0] = 0;
+	errstr(err, sizeof err);
+	snprint(buf, sizeof buf, "%s/%s:%s", host, port, err);
+	return buf;
+}
+
+static int
+starttls(Imap *imap, TLSconn *tls)
+{
+	char buf[Pathlen];
+	uchar digest[SHA1dlen];
+	int sfd, fd;
+
+	memset(tls, 0, sizeof *tls);
+	sfd = tlsClient(imap->fd, tls);
+	if(sfd < 0){
+		werrstr("tlsClient: %r");
+		return -1;
+	}
+	if(tls->cert == nil || tls->certlen <= 0){
+		close(sfd);
+		werrstr("server did not provide TLS certificate");
+		return -1;
+	}
+	sha1(tls->cert, tls->certlen, digest, nil);
+	if(!imap->thumb || !okThumbprint(digest, imap->thumb)){
+		close(sfd);
+		werrstr("server certificate %.*H not recognized",
+			SHA1dlen, digest);
+		return -1;
+	}
+	close(imap->fd);
+	imap->fd = sfd;
+
+	if(imap->flags & Fdebug){
+		snprint(buf, sizeof buf, "%s/ctl", tls->dir);
+		fd = open(buf, OWRITE);
+		fprint(fd, "debug");
+		close(fd);
+	}
+
+	return 1;
+}
+
+static void
+imap4disconnect(Imap *imap)
+{
+	if(imap->binit){
+		Bterm(&imap->bin);
+		Bterm(&imap->bout);
+		imap->binit = 0;
+	}
+	close(imap->fd);
+	imap->fd = -1;
+}
+
+char*
+capabilties(Imap *imap)
+{
+	char * err;
+
+	imap4cmd(imap, "capability");
+	imap4resp(imap);
+	err = imap4resp(imap);
+	if(isokay(err))
+		err = 0;
+	return err;
+}
+
+static char*
+imap4dial(Imap *imap)
+{
+	char *err, *port;
+	TLSconn conn;
+
+	if(imap->fd >= 0){
+		imap4cmd(imap, "noop");
+		if(isokay(imap4resp(imap)))
+			return nil;
+		imap4disconnect(imap);
+	}
+	if(imap->flags & Fssl)
+		port = "imaps";
+	else
+		port = "imap";
+	if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
+		return imaperrstr(imap->host, port);
+	if(imap->flags & Fssl && starttls(imap, &conn) == -1){
+		err = imaperrstr(imap->host, port);
+		free(conn.cert);
+		imap4disconnect(imap);
+		return err;
+	}
+	assert(imap->binit == 0);
+	Binit(&imap->bin, imap->fd, OREAD);
+	Binit(&imap->bout, imap->fd, OWRITE);
+	imap->binit = 1;
+
+	imap->tag = 0;
+	err = imap4resp(imap);
+	if(!isokay(err))
+		return "error in initial IMAP handshake";
+
+	if((err = capabilties(imap)) || (err = imap4login(imap))){
+		eprint("imap: err is %s\n", err);
+		imap4disconnect(imap);
+		return err;
+	}
+	return nil;
+}
+
+static void
+imap4hangup(Imap *imap)
+{
+	imap4cmd(imap, "logout");
+	imap4resp(imap);
+	imap4disconnect(imap);
+}
+
+/* gmail lies about message sizes */
+static ulong
+gmaildiscount(Message *m, uvlong o, ulong l)
+{
+	if((m->cstate&Cidx) == 0)
+	if(o + l == m->size)
+		return l + 100 + (o + l)/5;
+	return l;
+}
+
+static int
+imap4fetch(Mailbox *mb, Message *m, uvlong o, ulong l)
+{
+	Imap *imap;
+
+	imap = mb->aux;
+	if(imap->flags & Fgmail)
+		l = gmaildiscount(m, o, l);
+	idprint(imap, "uid fetch %lud (body.peek[]<%llud.%lud>)\n", (ulong)m->imapuid, o, l);
+	imap4cmd(imap, "uid fetch %lud (body.peek[]<%llud.%lud>)", (ulong)m->imapuid, o, l);
+	if(!isokay(imap4resp0(imap, mb, m))){
+		eprint("imap: imap fetch failed\n");
+		return -1;
+	}
+	return 0;
+}
+
+static uvlong
+datesec(Imap *imap, int i)
+{
+	int j;
+	uvlong v;
+	Fetchi *f;
+
+	f = imap->f;
+	v = (uvlong)f[i].dates << 8;
+
+	/* shifty; these sequences should be stable. */
+	for(j = i; j-- > 0; )
+		if(f[i].dates != f[j].dates)
+			break;
+	v |= i - (j + 1);
+	return v;
+}
+
+static void
+markdel(Mailbox *mb, Message *m, int doplumb)
+{
+	if(doplumb)
+		mailplumb(mb, m, 1);
+	m->inmbox = 0;
+	m->deleted = Disappear;
+}
+
+static int
+vcmp(vlong a, vlong b)
+{
+	a -= b;
+	if(a > 0)
+		return 1;
+	if(a < 0)
+		return -1;
+	return 0;
+}
+
+static int
+fetchicmp(Fetchi *f1, Fetchi *f2)
+{
+	return vcmp(f1->uid, f2->uid);
+}
+
+static int
+setsize(Mailbox *, Message *m, Fetchi *f)
+{
+	if(f->sizes >= Maxmsg)
+		return -1;
+//	if(!gmailmbox(mb))
+	return m->size = f->sizes;
+}
+
+static char*
+imap4read(Imap *imap, Mailbox *mb, int doplumb, int *new)
+{
+	char *s;
+	int i, n, c, nnew, ndel;
+	Fetchi *f;
+	Message *m, **ll;
+
+	*new = 0;
+	if(time(0) - imap->lastread < 10)
+		return nil;
+	imap->lastread = time(0);
+	imap4cmd(imap, "status %Z (messages uidvalidity)", imap->mbox);
+	if(!isokay(s = imap4resp(imap)))
+		return s;
+
+	imap->nuid = 0;
+	imap->muid = imap->nmsg;
+	imap->f = erealloc(imap->f, imap->nmsg*sizeof imap->f[0]);
+	f = imap->f;
+	n = imap->nmsg;
+
+	if(imap->nmsg > 0){
+		imap4cmd(imap, "uid fetch 1:* (uid rfc822.size internaldate)");
+		if(!isokay(s = imap4resp(imap)))
+			return s;
+	}
+
+	qsort(f, n, sizeof f[0], (int(*)(void*, void*))fetchicmp);
+	nnew = ndel = 0;
+	ll = &mb->root->part;
+	for(i = 0; *ll || i < n; ){
+		c = -1;
+		if(i >= n)
+			c = 1;
+		else if(*ll){
+			if((*ll)->imapuid == 0)
+				(*ll)->imapuid = strtoull((*ll)->idxaux, 0, 0);
+			c = vcmp(f[i].uid, (*ll)->imapuid);
+		}
+		idprint(imap, "consider %U and %U -> %d\n", i<n? f[i].uid: 0, *ll? (*ll)->imapuid: 1, c);
+		if(c < 0){
+			/* new message */
+			idprint(imap, "new: %U (%U)\n", f[i].uid, *ll? (*ll)->imapuid: 0);
+			m = newmessage(mb->root);
+			m->inmbox = 1;
+			m->idxaux = smprint("%llud", f[i].uid);
+			m->imapuid = f[i].uid;
+			m->fileid = datesec(imap, i);
+			if(setsize(mb, m, f + i) < 0 || m->size >= Maxmsg){
+				/* message disappeared?  unchain */
+				idprint(imap, "deleted → %r (%U)\n", m->imapuid);
+				logmsg(m, "disappeared");
+				if(doplumb)
+					mailplumb(mb, m, 1); /* redundant */
+				unnewmessage(mb, mb->root, m);
+				/* we're out of sync; here's were to signal that */
+				break;
+			}
+			nnew++;
+			logmsg(m, "new %s", m->idxaux);
+			m->next = *ll;
+			*ll = m;
+			ll = &m->next;
+			i++;
+			newcachehash(mb, m, doplumb);
+			putcache(mb, m);
+		}else if(c > 0){
+			/* deleted message; */
+			idprint(imap, "deleted: %U (%U)\n", i<n? f[i].uid: 0, *ll? (*ll)->imapuid: 0);
+			ndel++;
+			logmsg(*ll, "deleted");
+			markdel(mb, *ll, doplumb);
+			ll = &(*ll)->next;
+		}else{
+			//logmsg(*ll, "duplicate %s", d[i].name);
+			i++;
+			ll = &(*ll)->next;
+		}
+	}
+
+	*new = nnew;
+	return nil;
+}
+
+static void
+imap4delete(Mailbox *mb, Message *m)
+{
+	Imap *imap;
+
+	imap = mb->aux;
+	if((ulong)(m->imapuid>>32) == imap->validity){
+		imap4cmd(imap, "uid store %lud +flags (\\Deleted)", (ulong)m->imapuid);
+		imap4resp(imap);
+		imap4cmd(imap, "expunge");
+		imap4resp(imap);
+//		if(!isokay(imap4resp(imap))
+//			return -1;
+	}
+	m->inmbox = 0;
+}
+
+static char*
+imap4sync(Mailbox *mb, int doplumb, int *new)
+{
+	char *err;
+	Imap *imap;
+
+	imap = mb->aux;
+	if(err = imap4dial(imap))
+		goto out;
+	if((err = imap4read(imap, mb, doplumb, new)) == nil)
+		mb->d->atime = mb->d->mtime = time(0);
+out:
+	mb->waketime = time(0) + imap->refreshtime;
+	return err;
+}
+
+static char*
+imap4ctl(Mailbox *mb, int argc, char **argv)
+{
+	char *a, *b;
+	Imap *imap;
+
+	imap = mb->aux;
+	if(argc < 1)
+		return Eimap4ctl;
+
+	if(argc == 1 && strcmp(argv[0], "debug") == 0){
+		imap->flags ^= Fdebug;
+		return nil;
+	}
+	if(strcmp(argv[0], "thumbprint") == 0){
+		if(imap->thumb){
+			freeThumbprints(imap->thumb);
+			imap->thumb = 0;
+		}
+		a = "/sys/lib/tls/mail";
+		b = "/sys/lib/tls/mail.exclude";
+		switch(argc){
+		default:
+			return Eimap4ctl;
+		case 4:
+			b = argv[2];
+		case 3:
+			a = argv[1];
+		case 2:
+			break;
+		}
+		imap->thumb = initThumbprints(a, b);
+		return nil;
+	}
+	if(argc == 2 && strcmp(argv[0], "uid") == 0){
+		uvlong l;
+		Message *m;
+
+		for(m = mb->root->part; m; m = m->next)
+			if(strcmp(argv[1], m->name) == 0){
+				l = strtoull(m->idxaux, 0, 0);
+				fprint(2, "uid %s %lud %lud %lud %lud\n", m->name, (ulong)(l>>32), (ulong)l,
+					(ulong)(m->imapuid>>32), (ulong)m->imapuid);
+			}
+		return nil;
+	}
+	if(strcmp(argv[0], "refresh") == 0)
+		switch(argc){
+		case 1:
+			imap->refreshtime = 60;
+			return nil;
+		case 2:
+			imap->refreshtime = atoi(argv[1]);
+			return nil;
+		}
+
+	return Eimap4ctl;
+}
+
+static void
+imap4close(Mailbox *mb)
+{
+	Imap *imap;
+
+	imap = mb->aux;
+	imap4disconnect(imap);
+	free(imap->f);
+	free(imap);
+}
+
+static char*
+mkmbox(Imap *imap, char *p, char *e)
+{
+	p = seprint(p, e, "%s/box/%s/imap.%s", MAILROOT, getlog(), imap->host);
+	if(imap->user && strcmp(imap->user, getlog()))
+		p = seprint(p, e, ".%s", imap->user);
+	if(cistrcmp(imap->mbox, "inbox"))
+		p = seprint(p, e, ".%s", imap->mbox);
+	return p;
+}
+
+static char*
+findmbox(char *p)
+{
+	char *f[10], path[Pathlen];
+	int nf;
+
+	snprint(path, sizeof path, "%s", p);
+	nf = getfields(path, f, 5, 0, "/");
+	if(nf < 3)
+		return nil;
+	return f[nf - 1];
+}
+
+static char*
+imap4rename(Mailbox *mb, char *p2, int)
+{
+	char *r, *new;
+	Imap *imap;
+
+	imap = mb->aux;
+	new = findmbox(p2);
+	idprint(imap, "rename %s %s\n", imap->mbox, new);
+	imap4cmd(imap, "rename %s %s", imap->mbox, new);
+	r = imap4resp(imap);
+	if(!isokay(r))
+		return r;
+	free(imap->mbox);
+	imap->mbox = smprint("%s", new);
+	mkmbox(imap, mb->path, mb->path + sizeof mb->path);
+	return 0;
+}
+
+/*
+ * incomplete; when we say remove we want to get subfolders, too.
+ * so we need to to a list, and recursivly nuke folders.
+ */
+static char*
+imap4remove(Mailbox *mb, int flags)
+{
+	char *r;
+	Imap *imap;
+
+	imap = mb->aux;
+	idprint(imap, "remove %s\n", imap->mbox);
+	imap4cmd(imap, "delete %s", imap->mbox);
+	r = imap4resp(imap);
+	if(!isokay(r))
+		return r;
+	if(flags & Rtrunc){
+		imap4cmd(imap, "create %s", imap->mbox);
+		r = imap4resp(imap);
+		if(!isokay(r))
+			return r;
+	}
+	return 0;
+}
+
+char*
+imap4mbox(Mailbox *mb, char *path)
+{
+	char *f[10];
+	uchar flags;
+	int nf;
+	Imap *imap;
+
+	fmtinstall('Z', Zfmt);
+	fmtinstall('U', Ufmt);
+	if(strncmp(path, "/imap/", 6) == 0)
+		flags = 0;
+	else if(strncmp(path, "/imaps/", 7) == 0)
+		flags = Fssl;
+	else
+		return Enotme;
+
+	path = strdup(path);
+	if(path == nil)
+		return "out of memory";
+
+	nf = getfields(path, f, 5, 0, "/");
+	if(nf < 3){
+		free(path);
+		return "bad imap path syntax /imap[s]/system[/user[/mailbox]]";
+	}
+
+	imap = emalloc(sizeof *imap);
+	imap->fd = -1;
+	imap->freep = path;
+	imap->flags = flags;
+	imap->host = f[2];
+	if(strstr(imap->host, "gmail.com"))
+		imap->flags |= Fgmail;
+	imap->refreshtime = 60;
+	if(nf < 4)
+		imap->user = nil;
+	else
+		imap->user = f[3];
+	if(nf < 5)
+		imap->mbox = strdup("inbox");
+	else
+		imap->mbox = strdup(f[4]);
+	mkmbox(imap, mb->path, mb->path + sizeof mb->path);
+	if(imap->flags & Fssl)
+		imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
+
+	mb->aux = imap;
+	mb->sync = imap4sync;
+	mb->close = imap4close;
+	mb->ctl = imap4ctl;
+	mb->fetch = imap4fetch;
+	mb->delete = imap4delete;
+	mb->rename = imap4rename;
+//	mb->remove = imap4remove;
+	mb->modflags = imap4modflags;
+	mb->d = emalloc(sizeof *mb->d);
+	mb->addfrom = 1;
+	return nil;
+}
--- a/sys/src/cmd/upas/fs/imap4.c
+++ /dev/null
@@ -1,878 +1,0 @@
-#include "common.h"
-#include <ctype.h>
-#include <plumb.h>
-#include <libsec.h>
-#include <auth.h>
-#include "dat.h"
-
-#pragma varargck argpos imap4cmd 2
-#pragma varargck	type	"Z"	char*
-
-int	doublequote(Fmt*);
-
-// if pipeline == 1 and upas/fs is used with dovecot,
-// 9Xn OK responses sometimes come much later after FETCH responses, i.e.
-// <- * 1 FETCH ...
-// <- * 2 FETCH ...
-// <- * 3 FETCH ...
-// <- 9X5 OK Fetch completed.
-// <- 9X6 OK Fetch completed.
-// download 40: did not get message body
-// <- 9X7 OK Fetch completed.
-// causing multiple messages to turn into one in imap4.c:/^imap4resp.
-int	pipeline = 0;
-
-static char Eio[] = "i/o error";
-
-typedef struct Imap Imap;
-struct Imap {
-	char *freep;	// free this to free the strings below
-
-	char *host;
-	char *user;
-	char *mbox;
-
-	int mustssl;
-	int refreshtime;
-	int debug;
-
-	ulong tag;
-	ulong validity;
-	int nmsg;
-	int size;
-	char *base;
-	char *data;
-
-	vlong *uid;
-	int nuid;
-	int muid;
-
-	Thumbprint *thumb;
-
-	// open network connection
-	Biobuf bin;
-	Biobuf bout;
-	int fd;
-};
-
-static char*
-removecr(char *s)
-{
-	char *r, *w;
-
-	for(r=w=s; *r; r++)
-		if(*r != '\r')
-			*w++ = *r;
-	*w = '\0';
-	return s;
-}
-
-//
-// send imap4 command
-//
-static void
-imap4cmd(Imap *imap, char *fmt, ...)
-{
-	char buf[128], *p;
-	va_list va;
-
-	va_start(va, fmt);
-	p = buf+sprint(buf, "9X%lud ", imap->tag);
-	vseprint(p, buf+sizeof(buf), fmt, va);
-	va_end(va);
-
-	p = buf+strlen(buf);
-	if(p > (buf+sizeof(buf)-3))
-		sysfatal("imap4 command too long");
-
-	if(imap->debug)
-		fprint(2, "-> %s\n", buf);
-	strcpy(p, "\r\n");
-	Bwrite(&imap->bout, buf, strlen(buf));
-	Bflush(&imap->bout);
-}
-
-enum {
-	OK,
-	NO,
-	BAD,
-	BYE,
-	EXISTS,
-	STATUS,
-	FETCH,
-	UNKNOWN,
-};
-
-static char *verblist[] = {
-[OK]		"OK",
-[NO]		"NO",
-[BAD]	"BAD",
-[BYE]	"BYE",
-[EXISTS]	"EXISTS",
-[STATUS]	"STATUS",
-[FETCH]	"FETCH",
-};
-
-static int
-verbcode(char *verb)
-{
-	int i;
-	char *q;
-
-	if(q = strchr(verb, ' '))
-		*q = '\0';
-
-	for(i=0; i<nelem(verblist); i++)
-		if(verblist[i] && strcmp(verblist[i], verb)==0){
-			if(q)
-				*q = ' ';
-			return i;
-		}
-	if(q)
-		*q = ' ';
-	return UNKNOWN;
-}
-
-static void
-strupr(char *s)
-{
-	for(; *s; s++)
-		if('a' <= *s && *s <= 'z')
-			*s += 'A'-'a';
-}
-
-static void
-imapgrow(Imap *imap, int n)
-{
-	int i;
-
-	if(imap->data == nil){
-		imap->base = emalloc(n+1);	
-		imap->data = imap->base;
-		imap->size = n+1;
-	}
-	if(n >= imap->size){
-		// friggin microsoft - reallocate
-		i = imap->data - imap->base;
-		imap->base = erealloc(imap->base, i+n+1);
-		imap->data = imap->base + i;
-		imap->size = n+1;
-	}
-}
-
-
-//
-// get imap4 response line.  there might be various 
-// data or other informational lines mixed in.
-//
-static char*
-imap4resp(Imap *imap)
-{
-	char *line, *p, *ep, *op, *q, *r, *en, *verb;
-	int i, n;
-	static char error[256];
-
-	while(p = Brdline(&imap->bin, '\n')){
-		ep = p+Blinelen(&imap->bin);
-		while(ep > p && (ep[-1]=='\n' || ep[-1]=='\r'))
-			*--ep = '\0';
-		
-		if(imap->debug)
-			fprint(2, "<- %s\n", p);
-		strupr(p);
-
-		switch(p[0]){
-		case '+':
-			if(imap->tag == 0)
-				fprint(2, "unexpected: %s\n", p);
-			break;
-
-		// ``unsolicited'' information; everything happens here.
-		case '*':
-			if(p[1]!=' ')
-				continue;
-			p += 2;
-			line = p;
-			n = strtol(p, &p, 10);
-			if(*p==' ')
-				p++;
-			verb = p;
-			
-			if(p = strchr(verb, ' '))
-				p++;
-			else
-				p = verb+strlen(verb);
-
-			switch(verbcode(verb)){
-			case OK:
-			case NO:
-			case BAD:
-				// human readable text at p;
-				break;
-			case BYE:
-				// early disconnect
-				// human readable text at p;
-				break;
-
-			// * 32 EXISTS
-			case EXISTS:
-				imap->nmsg = n;
-				break;
-
-			// * STATUS Inbox (MESSAGES 2 UIDVALIDITY 960164964)
-			case STATUS:
-				if(q = strstr(p, "MESSAGES"))
-					imap->nmsg = atoi(q+8);
-				if(q = strstr(p, "UIDVALIDITY"))
-					imap->validity = strtoul(q+11, 0, 10);
-				break;
-
-			case FETCH:
-				// * 1 FETCH (uid 8889 RFC822.SIZE 3031 body[] {3031}
-				// <3031 bytes of data>
- 				// )
-				if(strstr(p, "RFC822.SIZE") && strstr(p, "BODY[]")){
-					if((q = strchr(p, '{')) 
-					&& (n=strtol(q+1, &en, 0), *en=='}')){
-						if(imap->data == nil || n >= imap->size)
-							imapgrow(imap, n);
-						if((i = Bread(&imap->bin, imap->data, n)) != n){
-							snprint(error, sizeof error,
-								"short read %d != %d: %r\n",
-								i, n);
-							return error;
-						}
-						if(imap->debug)
-							fprint(2, "<- read %d bytes\n", n);
-						imap->data[n] = '\0';
-						if(imap->debug)
-							fprint(2, "<- %s\n", imap->data);
-						imap->data += n;
-						imap->size -= n;
-						p = Brdline(&imap->bin, '\n');
-						if(imap->debug)
-							fprint(2, "<- ignoring %.*s\n",
-								Blinelen(&imap->bin), p);
-					}else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
-						*r = '\0';
-						q++;
-						n = r-q;
-						if(imap->data == nil || n >= imap->size)
-							imapgrow(imap, n);
-						memmove(imap->data, q, n);
-						imap->data[n] = '\0';
-						imap->data += n;
-						imap->size -= n;
-					}else
-						return "confused about FETCH response";
-					break;
-				}
-
-				// * 1 FETCH (UID 1 RFC822.SIZE 511)
-				if(q=strstr(p, "RFC822.SIZE")){
-					imap->size = atoi(q+11);
-					break;
-				}
-
-				// * 1 FETCH (UID 1 RFC822.HEADER {496}
-				// <496 bytes of data>
- 				// )
-				// * 1 FETCH (UID 1 RFC822.HEADER "data")
-				if(strstr(p, "RFC822.HEADER") || strstr(p, "RFC822.TEXT")){
-					if((q = strchr(p, '{')) 
-					&& (n=strtol(q+1, &en, 0), *en=='}')){
-						if(imap->data == nil || n >= imap->size)
-							imapgrow(imap, n);
-						if((i = Bread(&imap->bin, imap->data, n)) != n){
-							snprint(error, sizeof error,
-								"short read %d != %d: %r\n",
-								i, n);
-							return error;
-						}
-						if(imap->debug)
-							fprint(2, "<- read %d bytes\n", n);
-						imap->data[n] = '\0';
-						if(imap->debug)
-							fprint(2, "<- %s\n", imap->data);
-						imap->data += n;
-						imap->size -= n;
-						p = Brdline(&imap->bin, '\n');
-						if(imap->debug)
-							fprint(2, "<- ignoring %.*s\n",
-								Blinelen(&imap->bin), p);
-					}else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
-						*r = '\0';
-						q++;
-						n = r-q;
-						if(imap->data == nil || n >= imap->size)
-							imapgrow(imap, n);
-						memmove(imap->data, q, n);
-						imap->data[n] = '\0';
-						imap->data += n;
-						imap->size -= n;
-					}else
-						return "confused about FETCH response";
-					break;
-				}
-
-				// * 1 FETCH (UID 1)
-				// * 2 FETCH (UID 6)
-				if(q = strstr(p, "UID")){
-					if(imap->nuid < imap->muid)
-						imap->uid[imap->nuid++] = ((vlong)imap->validity<<32)|strtoul(q+3, nil, 10);
-					break;
-				}
-			}
-
-			if(imap->tag == 0)
-				return line;
-			break;
-
-		case '9':		// response to our message
-			op = p;
-			if(p[1]=='X' && strtoul(p+2, &p, 10)==imap->tag){
-				while(*p==' ')
-					p++;
-				imap->tag++;
-				return p;
-			}
-			fprint(2, "expected %lud; got %s\n", imap->tag, op);
-			break;
-
-		default:
-			if(imap->debug || *p)
-				fprint(2, "unexpected line: %s\n", p);
-		}
-	}
-	snprint(error, sizeof error, "i/o error: %r\n");
-	return error;
-}
-
-static int
-isokay(char *resp)
-{
-	return strncmp(resp, "OK", 2)==0;
-}
-
-//
-// log in to IMAP4 server, select mailbox, no SSL at the moment
-//
-static char*
-imap4login(Imap *imap)
-{
-	char *s;
-	UserPasswd *up;
-
-	imap->tag = 0;
-	s = imap4resp(imap);
-	if(!isokay(s))
-		return "error in initial IMAP handshake";
-
-	if(imap->user != nil)
-		up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q user=%q", imap->host, imap->user);
-	else
-		up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q", imap->host);
-	if(up == nil)
-		return "cannot find IMAP password";
-
-	imap->tag = 1;
-	imap4cmd(imap, "LOGIN %Z %Z", up->user, up->passwd);
-	free(up);
-	if(!isokay(s = imap4resp(imap)))
-		return s;
-
-	imap4cmd(imap, "SELECT %Z", imap->mbox);
-	if(!isokay(s = imap4resp(imap)))
-		return s;
-
-	return nil;
-}
-
-static char*
-imaperrstr(char *host, char *port)
-{
-	/*
-	 * make mess big enough to hold a TLS certificate fingerprint
-	 * plus quite a bit of slop.
-	 */
-	static char mess[3 * Errlen];
-	char err[Errlen];
-
-	err[0] = '\0';
-	errstr(err, sizeof(err));
-	snprint(mess, sizeof(mess), "%s/%s:%s", host, port, err);
-	return mess;
-}
-
-static int
-starttls(Imap *imap)
-{
-	int sfd;
-	uchar digest[SHA1dlen];
-	TLSconn conn;
-
-	memset(&conn, 0, sizeof(conn));
-	sfd = tlsClient(imap->fd, &conn);
-	if(sfd < 0) {
-		werrstr("tlsClient: %r");
-		return -1;
-	}
-	imap->fd = sfd;
-	free(conn.sessionID);
-	if(conn.cert==nil || conn.certlen <= 0) {
-		werrstr("server did not provide TLS certificate");
-		return -1;
-	}
-	sha1(conn.cert, conn.certlen, digest, nil);
-	free(conn.cert);
-	if(!imap->thumb || !okThumbprint(digest, imap->thumb)){
-		fmtinstall('H', encodefmt);
-		werrstr("server certificate %.*H not recognized",
-			SHA1dlen, digest);
-		return -1;
-	}
-	return sfd;
-}
-
-//
-// dial and handshake with the imap server
-//
-static char*
-imap4dial(Imap *imap)
-{
-	char *err, *port;
-
-	if(imap->fd >= 0){
-		imap4cmd(imap, "noop");
-		if(isokay(imap4resp(imap)))
-			return nil;
-		close(imap->fd);
-		imap->fd = -1;
-	}
-
-	if(imap->mustssl)
-		port = "imaps";
-	else
-		port = "imap";
-
-	if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
-		return imaperrstr(imap->host, port);
-
-	if(imap->mustssl){
-		if(starttls(imap) < 0){
-			err = imaperrstr(imap->host, port);
-			goto Out;
-		}
-	}
-	Binit(&imap->bin, imap->fd, OREAD);
-	Binit(&imap->bout, imap->fd, OWRITE);
-	err = imap4login(imap);
-Out:
-	if(err != nil){
-		if(imap->fd >= 0){
-			close(imap->fd);
-			imap->fd = -1;
-		}
-	}
-	return err;
-}
-
-//
-// close connection
-//
-static void
-imap4hangup(Imap *imap)
-{
-	if(imap->fd < 0)
-		return;
-	imap4cmd(imap, "LOGOUT");
-	imap4resp(imap);
-	close(imap->fd);
-	imap->fd = -1;
-}
-
-//
-// download a single message
-//
-static char*
-imap4fetch(Mailbox *mb, Message *m)
-{
-	int i;
-	char *p, *s, sdigest[2*SHA1dlen+1];
-	Imap *imap;
-
-	imap = mb->aux;
-
-	imap->size = 0;
-
-	if(!isokay(s = imap4resp(imap)))
-		return s;
-
-	p = imap->base;
-	if(p == nil)
-		return "did not get message body";
-
-	removecr(p);
-	free(m->start);
-	m->start = p;
-	m->end = p+strlen(p);
-	m->bend = m->rbend = m->end;
-	m->header = m->start;
-
-	imap->base = nil;
-	imap->data = nil;
-
-	parse(m, 0, mb, 1);
-
-	// digest headers
-	sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
-	for(i = 0; i < SHA1dlen; i++)
-		sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
-	m->sdigest = s_copy(sdigest);
-
-	return nil;
-}
-
-//
-// check for new messages on imap4 server
-// download new messages, mark deleted messages
-//
-static char*
-imap4read(Imap *imap, Mailbox *mb, int doplumb)
-{
-	char *s;
-	int i, ignore, nnew, t;
-	Message *m, *next, **l;
-
-	imap4cmd(imap, "STATUS %Z (MESSAGES UIDVALIDITY)", imap->mbox);
-	if(!isokay(s = imap4resp(imap)))
-		return s;
-
-	imap->nuid = 0;
-	imap->uid = erealloc(imap->uid, imap->nmsg*sizeof(imap->uid[0]));
-	imap->muid = imap->nmsg;
-
-	if(imap->nmsg > 0){
-		imap4cmd(imap, "UID FETCH 1:* UID");
-		if(!isokay(s = imap4resp(imap)))
-			return s;
-	}
-
-	l = &mb->root->part;
-	for(i=0; i<imap->nuid; i++){
-		ignore = 0;
-		while(*l != nil){
-			if((*l)->imapuid == imap->uid[i]){
-				ignore = 1;
-				l = &(*l)->next;
-				break;
-			}else{
-				// old mail, we don't have it anymore
-				if(doplumb)
-					mailplumb(mb, *l, 1);
-				(*l)->inmbox = 0;
-				(*l)->deleted = 1;
-				l = &(*l)->next;
-			}
-		}
-		if(ignore)
-			continue;
-
-		// new message
-		m = newmessage(mb->root);
-		m->mallocd = 1;
-		m->inmbox = 1;
-		m->imapuid = imap->uid[i];
-
-		// add to chain, will download soon
-		*l = m;
-		l = &m->next;
-	}
-
-	// whatever is left at the end of the chain is gone
-	while(*l != nil){
-		if(doplumb)
-			mailplumb(mb, *l, 1);
-		(*l)->inmbox = 0;
-		(*l)->deleted = 1;
-		l = &(*l)->next;
-	}
-
-	// download new messages
-	t = imap->tag;
-	if(pipeline)
-	switch(rfork(RFPROC|RFMEM)){
-	case -1:
-		sysfatal("rfork: %r");
-	default:
-		break;
-	case 0:
-		for(m = mb->root->part; m != nil; m = m->next){
-			if(m->start != nil)
-				continue;
-			if(imap->debug)
-				fprint(2, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
-					t, (ulong)m->imapuid);
-			Bprint(&imap->bout, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
-				t++, (ulong)m->imapuid);
-		}
-		Bflush(&imap->bout);
-		_exits(nil);
-	}
-
-	nnew = 0;
-	for(m=mb->root->part; m!=nil; m=next){
-		next = m->next;
-		if(m->start != nil)
-			continue;
-
-		if(!pipeline){
-			Bprint(&imap->bout, "9X%lud UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
-				(ulong)imap->tag, (ulong)m->imapuid);
-			Bflush(&imap->bout);
-		}
-
-		if(s = imap4fetch(mb, m)){
-			// message disappeared?  unchain
-			fprint(2, "download %lud: %s\n", (ulong)m->imapuid, s);
-			delmessage(mb, m);
-			mb->root->subname--;
-			continue;
-		}
-		nnew++;
-		if(doplumb)
-			mailplumb(mb, m, 0);
-	}
-	if(pipeline)
-		waitpid();
-
-	if(nnew || mb->vers == 0){
-		mb->vers++;
-		henter(PATH(0, Qtop), mb->name,
-			(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
-	}
-	return nil;
-}
-
-//
-// sync mailbox
-//
-static void
-imap4purge(Imap *imap, Mailbox *mb)
-{
-	int ndel;
-	Message *m, *next;
-
-	ndel = 0;
-	for(m=mb->root->part; m!=nil; m=next){
-		next = m->next;
-		if(m->deleted && m->refs==0){
-			if(m->inmbox && (ulong)(m->imapuid>>32)==imap->validity){
-				imap4cmd(imap, "UID STORE %lud +FLAGS (\\Deleted)", (ulong)m->imapuid);
-				if(isokay(imap4resp(imap))){
-					ndel++;
-					delmessage(mb, m);
-				}
-			}else
-				delmessage(mb, m);
-		}
-	}
-
-	if(ndel){
-		imap4cmd(imap, "EXPUNGE");
-		imap4resp(imap);
-	}
-}
-
-//
-// connect to imap4 server, sync mailbox
-//
-static char*
-imap4sync(Mailbox *mb, int doplumb)
-{
-	char *err;
-	Imap *imap;
-
-	imap = mb->aux;
-
-	if(err = imap4dial(imap)){
-		mb->waketime = time(0) + imap->refreshtime;
-		return err;
-	}
-
-	if((err = imap4read(imap, mb, doplumb)) == nil){
-		imap4purge(imap, mb);
-		mb->d->atime = mb->d->mtime = time(0);
-	}
-	/*
-	 * don't hang up; leave connection open for next time.
-	 */
-	// imap4hangup(imap);
-	mb->waketime = time(0) + imap->refreshtime;
-	return err;
-}
-
-static char Eimap4ctl[] = "bad imap4 control message";
-
-static char*
-imap4ctl(Mailbox *mb, int argc, char **argv)
-{
-	int n;
-	Imap *imap;
-
-	imap = mb->aux;
-	if(argc < 1)
-		return Eimap4ctl;
-
-	if(argc==1 && strcmp(argv[0], "debug")==0){
-		imap->debug = 1;
-		return nil;
-	}
-
-	if(argc==1 && strcmp(argv[0], "nodebug")==0){
-		imap->debug = 0;
-		return nil;
-	}
-
-	if(argc==1 && strcmp(argv[0], "thumbprint")==0){
-		if(imap->thumb)
-			freeThumbprints(imap->thumb);
-		imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
-	}
-	if(strcmp(argv[0], "refresh")==0){
-		if(argc==1){
-			imap->refreshtime = 60;
-			return nil;
-		}
-		if(argc==2){
-			n = atoi(argv[1]);
-			if(n < 15)
-				return Eimap4ctl;
-			imap->refreshtime = n;
-			return nil;
-		}
-	}
-
-	return Eimap4ctl;
-}
-
-//
-// free extra memory associated with mb
-//
-static void
-imap4close(Mailbox *mb)
-{
-	Imap *imap;
-
-	imap = mb->aux;
-	free(imap->freep);
-	free(imap->base);
-	free(imap->uid);
-	if(imap->fd >= 0)
-		close(imap->fd);
-	free(imap);
-}
-
-//
-// open mailboxes of the form /imap/host/user
-//
-char*
-imap4mbox(Mailbox *mb, char *path)
-{
-	char *f[10];
-	int mustssl, nf;
-	Imap *imap;
-
-	quotefmtinstall();
-	fmtinstall('Z', doublequote);
-	if(strncmp(path, "/imap/", 6) != 0 && strncmp(path, "/imaps/", 7) != 0)
-		return Enotme;
-	mustssl = (strncmp(path, "/imaps/", 7) == 0);
-
-	path = strdup(path);
-	if(path == nil)
-		return "out of memory";
-
-	nf = getfields(path, f, 5, 0, "/");
-	if(nf < 3){
-		free(path);
-		return "bad imap path syntax /imap[s]/system[/user[/mailbox]]";
-	}
-
-	imap = emalloc(sizeof(*imap));
-	imap->fd = -1;
-	imap->debug = debug;
-	imap->freep = path;
-	imap->mustssl = mustssl;
-	imap->host = f[2];
-	if(nf < 4)
-		imap->user = nil;
-	else
-		imap->user = f[3];
-	if(nf < 5)
-		imap->mbox = "Inbox";
-	else
-		imap->mbox = f[4];
-	imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
-
-	mb->aux = imap;
-	mb->sync = imap4sync;
-	mb->close = imap4close;
-	mb->ctl = imap4ctl;
-	mb->d = emalloc(sizeof(*mb->d));
-	//mb->fetch = imap4fetch;
-
-	return nil;
-}
-
-//
-// Formatter for %"
-// Use double quotes to protect white space, frogs, \ and "
-//
-enum
-{
-	Qok = 0,
-	Qquote,
-	Qbackslash,
-};
-
-static int
-needtoquote(Rune r)
-{
-	if(r >= Runeself)
-		return Qquote;
-	if(r <= ' ')
-		return Qquote;
-	if(r=='\\' || r=='"')
-		return Qbackslash;
-	return Qok;
-}
-
-int
-doublequote(Fmt *f)
-{
-	char *s, *t;
-	int w, quotes;
-	Rune r;
-
-	s = va_arg(f->args, char*);
-	if(s == nil || *s == '\0')
-		return fmtstrcpy(f, "\"\"");
-
-	quotes = 0;
-	for(t=s; *t; t+=w){
-		w = chartorune(&r, t);
-		quotes |= needtoquote(r);
-	}
-	if(quotes == 0)
-		return fmtstrcpy(f, s);
-
-	fmtrune(f, '"');
-	for(t=s; *t; t+=w){
-		w = chartorune(&r, t);
-		if(needtoquote(r) == Qbackslash)
-			fmtrune(f, '\\');
-		fmtrune(f, r);
-	}
-	return fmtrune(f, '"');
-}
--- a/sys/src/cmd/upas/fs/mbox.c
+++ b/sys/src/cmd/upas/fs/mbox.c
@@ -5,104 +5,188 @@
 #include "dat.h"
 
 typedef struct Header Header;
-
 struct Header {
-	char *type;
-	void (*f)(Message*, Header*, char*);
-	int len;
+	char	*type;
+	uintptr	offset;
+	char	*(*f)(Message*, Header*, char*, char*);
+	int	len;
+	int	str;
 };
 
 /* headers */
-static	void	ctype(Message*, Header*, char*);
-static	void	cencoding(Message*, Header*, char*);
-static	void	cdisposition(Message*, Header*, char*);
-static	void	date822(Message*, Header*, char*);
-static	void	from822(Message*, Header*, char*);
-static	void	to822(Message*, Header*, char*);
-static	void	sender822(Message*, Header*, char*);
-static	void	replyto822(Message*, Header*, char*);
-static	void	subject822(Message*, Header*, char*);
-static	void	inreplyto822(Message*, Header*, char*);
-static	void	cc822(Message*, Header*, char*);
-static	void	bcc822(Message*, Header*, char*);
-static	void	messageid822(Message*, Header*, char*);
-static	void	mimeversion(Message*, Header*, char*);
-static	void	nullsqueeze(Message*);
+static	char	*ctype(Message*, Header*, char*, char*);
+static	char	*cencoding(Message*, Header*, char*, char*);
+static	char	*cdisposition(Message*, Header*, char*, char*);
+static	char	*from822(Message*, Header*, char*, char*);
+static	char	*replace822(Message*, Header*, char*, char*);
+static	char	*concat822(Message*, Header*, char*, char*);
+static	char	*copy822(Message*, Header*, char*, char*);
+static	char	*ref822(Message*, Header*, char*, char*);
+
 enum
 {
-	Mhead=	11,	/* offset of first mime header */
+	Mhead	= 11,	/* offset of first mime header */
 };
 
-Header head[] =
+#define O(x)	offsetof(Message, x)
+static Header head[] =
 {
-	{ "date:", date822, },
-	{ "from:", from822, },
-	{ "to:", to822, },
-	{ "sender:", sender822, },
-	{ "reply-to:", replyto822, },
-	{ "subject:", subject822, },
-	{ "cc:", cc822, },
-	{ "bcc:", bcc822, },
-	{ "in-reply-to:", inreplyto822, },
-	{ "mime-version:", mimeversion, },
-	{ "message-id:", messageid822, },
+	"date:",		O(date822),	copy822,		0,	0,
+	"from:", 		O(from),		from822,	0,	1,
+	"to:", 		O(to),		concat822,	0,	1,
+	"sender:",	O(sender),	replace822,	0,	1,
+	"reply-to:",	O(replyto),	replace822,	0,	1,
+	"subject:",	O(subject),	copy822,		0,	1,
+	"cc:", 		O(cc),		concat822,	0,	1,
+	"bcc:",		O(bcc),		concat822,	0,	1,
+	"in-reply-to:",	O(inreplyto),	replace822,	0,	1,
+	"message-id:",	O(messageid),	replace822,	0,	1,
+	"references:",	~0,		ref822,		0,	0,
 
-[Mhead]	{ "content-type:", ctype, },
-	{ "content-transfer-encoding:", cencoding, },
-	{ "content-disposition:", cdisposition, },
-	{ 0, },
+[Mhead]	"content-type:", 	~0,		ctype,		0, 	0,
+	"content-transfer-encoding:", ~0,	cencoding,	0,	0,
+	"content-disposition:", ~0,		cdisposition,	0,	0,
 };
 
-static	void	fatal(char *fmt, ...);
-static	void	initquoted(void);
-static	void	startheader(Message*);
-static	void	startbody(Message*);
-static	char*	skipwhite(char*);
-static	char*	skiptosemi(char*);
-static	char*	getstring(char*, String*, int);
-static	void	setfilename(Message*, char*);
-static	char*	lowercase(char*);
-static	int	is8bit(Message*);
-static	int	headerline(char**, String*);
-static	void	initheaders(void);
-static void	parseattachments(Message*, Mailbox*);
-
-int		debug;
-
-char *Enotme = "path not served by this file server";
-
-enum
-{
-	Chunksize = 1024,
-};
-
-Mailboxinit *boxinit[] = {
+static Mailboxinit *boxinit[] = {
 	imap4mbox,
 	pop3mbox,
-	planbmbox,
-	planbvmbox,
+	mdirmbox,
+//	planbmbox,
 	plan9mbox,
 };
 
+/*
+ * do we want to plumb flag changes?
+ */
 char*
 syncmbox(Mailbox *mb, int doplumb)
 {
-	return (*mb->sync)(mb, doplumb);
+	char *s;
+	int n, d, y, a;
+	Message *m, *next;
+
+	if(semacquire(&mb->syncsem, 0) <= 0)
+		return nil;
+	a = mb->root->subname;
+	if(rdidxfile(mb, doplumb) == -2)
+		wridxfile(mb);
+	if(s = mb->sync(mb, doplumb, &n)){
+		semrelease(&mb->syncsem, 1);
+		return s;
+	}
+	d = 0;
+	y = 0;
+	for(m = mb->root->part; m; m = next){
+		next = m->next;
+		if(m->cstate & Cidxstale)
+			y++;
+		if(m->deleted == 0 || m->refs > 0)
+			continue;
+		if(mb->delete && m->inmbox && m->deleted & Deleted)
+			mb->delete(mb, m);
+		if(!m->inmbox){
+			delmessage(mb, m);
+			d++;
+		}
+	}
+	a = mb->root->subname - a;
+	assert(a >= 0);
+	if(n + d + y + a){
+		iprint("deleted: %d; new %d; stale %d\n", d, n, y);
+		logmsg(nil, "deleted: %d; new %d; stale %d", d, n, y);
+		wridxfile(mb);
+	}
+	if(n + d + y + a){
+		mb->vers++;
+		henter(PATH(0, Qtop), mb->name,
+			(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+	}
+	semrelease(&mb->syncsem, 1);
+	return nil;
 }
 
-/* create a new mailbox */
+/*
+ * not entirely clear where the locking should take place, if
+ * it is required.
+ */
 char*
-newmbox(char *path, char *name, int std)
+mboxrename(char *a, char *b, int flags)
 {
-	Mailbox *mb, **l;
+	char f0[Pathlen + 4], f1[Pathlen + 4], *err, *p0, *p1;
+	Mailbox *mb;
+
+	snprint(f0, sizeof f0, "%s", a);
+	snprint(f1, sizeof f1, "%s", b);
+	err = newmbox(f0, nil, 0, &mb);
+	dprint("mboxrename %s %s -> %s\n", f0, f1, err);
+	if(!err && !mb->rename)
+		err = "rename not supported";
+	if(err)
+		goto done;
+	err = mb->rename(mb, f1, flags);
+	if(err)
+		goto done;
+	if(flags & Rtrunc)
+		/* we're comitted, so forget bailing */
+		err = newmbox(f0, nil, DMcreate, 0);
+	p0 = f0 + strlen(f0);
+	p1 = f1 + strlen(f1);
+
+	strcat(f0, ".idx");
+	strcat(f1, ".idx");
+	rename(f0, f1, 0);
+
+	*p0 = *p1 = 0;
+	strcat(f0, ".imp");
+	strcat(f1, ".imp");
+	rename(f0, f1, 0);
+
+	snprint(mb->path, sizeof mb->path, "%s", b);
+	hfree(PATH(0, Qtop), mb->name);
+	p0 = strrchr(mb->path, '/') + 1;
+	if(p0 == (char*)1)
+		p0 = mb->path;
+	snprint(mb->name, sizeof mb->name, "%s", p0);
+	henter(PATH(0, Qtop), mb->name,
+		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+done:
+	if(!mb)
+		return err;
+	qunlock(mb);
+//	if(err)
+//		mboxdecref(mb);
+	return err;
+}
+
+static void
+initheaders(void)
+{
+	int i;
+	static int already;
+
+	if(already)
+		return;
+	already = 1;
+	for(i = 0; i < nelem(head); i++)
+		head[i].len = strlen(head[i].type);
+}
+
+char*
+newmbox(char *path, char *name, int flags, Mailbox **r)
+{
 	char *p, *rv;
 	int i;
+	Mailbox *mb, **l;
 
 	initheaders();
-
-	mb = emalloc(sizeof(*mb));
-	strncpy(mb->path, path, sizeof(mb->path)-1);
-	if(name == nil){
+	mb = emalloc(sizeof *mb);
+	mb->idxsem = 1;
+	mb->syncsem = 1;
+	mb->flags = flags;
+	strncpy(mb->path, path, sizeof mb->path - 1);
+	p = name;
+	if(p == nil){
 		p = strrchr(path, '/');
 		if(p == nil)
 			p = path;
@@ -112,30 +196,25 @@
 			free(mb);
 			return "bad mbox name";
 		}
-		strncpy(mb->name, p, sizeof(mb->name)-1);
-	} else {
-		strncpy(mb->name, name, sizeof(mb->name)-1);
 	}
+	strncpy(mb->name, p, sizeof mb->name - 1);
+	mb->idxread = genericidxread;
+	mb->idxwrite = genericidxwrite;
+	mb->idxinvalid = genericidxinvalid;
 
-	rv = nil;
-	// check for a mailbox type
-	for(i=0; i<nelem(boxinit); i++)
-		if((rv = (*boxinit[i])(mb, path)) != Enotme)
+	/* check for a mailbox type */
+	rv = Enotme;	/* can't happen; shut compiler up */
+	for(i = 0; i < nelem(boxinit); i++)
+		if((rv = boxinit[i](mb, path)) != Enotme)
 			break;
-	if(i == nelem(boxinit)){
-		free(mb);
-		return "bad path";
-	}
-
-	// on error, give up
 	if(rv){
 		free(mb);
 		return rv;
 	}
 
-	// make sure name isn't taken
+	/* make sure name isn't taken */
 	qlock(&mbllock);
-	for(l = &mbl; *l != nil; l = &(*l)->next){
+	for(l = &mbl; *l != nil; l = &(*l)->next)
 		if(strcmp((*l)->name, mb->name) == 0){
 			if(strcmp(path, (*l)->path) == 0)
 				rv = nil;
@@ -142,36 +221,39 @@
 			else
 				rv = "mbox name in use";
 			if(mb->close)
-				(*mb->close)(mb);
+				mb->close(mb);
 			free(mb);
 			qunlock(&mbllock);
 			return rv;
 		}
-	}
 
-	// always try locking
+	/* always try locking */
 	mb->dolock = 1;
-
 	mb->refs = 1;
 	mb->next = nil;
 	mb->id = newid();
 	mb->root = newmessage(nil);
-	mb->std = std;
+	mb->mtree = avlcreate(mtreecmp);
+
 	*l = mb;
 	qunlock(&mbllock);
 
 	qlock(mb);
-	if(mb->ctl){
+	henter(PATH(0, Qtop), mb->name,
+		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+	if(mb->ctl)
 		henter(PATH(mb->id, Qmbox), "ctl",
 			(Qid){PATH(mb->id, Qmboxctl), 0, QTFILE}, nil, mb);
-	}
 	rv = syncmbox(mb, 0);
-	qunlock(mb);
+	if(r)
+		*r = mb;
+	else
+		qunlock(mb);
 
 	return rv;
 }
 
-// close the named mailbox
+/* close the named mailbox */
 void
 freembox(char *name)
 {
@@ -178,7 +260,7 @@
 	Mailbox **l, *mb;
 
 	qlock(&mbllock);
-	for(l=&mbl; *l != nil; l=&(*l)->next){
+	for(l=&mbl; *l != nil; l=&(*l)->next)
 		if(strcmp(name, (*l)->name) == 0){
 			mb = *l;
 			*l = mb->next;
@@ -185,328 +267,468 @@
 			mboxdecref(mb);
 			break;
 		}
-	}
 	hfree(PATH(0, Qtop), name);
 	qunlock(&mbllock);
 }
 
-static void
-initheaders(void)
+void
+syncallmboxes(void)
 {
-	Header *h;
-	static int already;
+	char *err;
+	Mailbox *m;
 
-	if(already)
-		return;
-	already = 1;
+	qlock(&mbllock);
+	for(m = mbl; m != nil; m = m->next)
+		if(err = syncmbox(m, 1))
+			eprint("syncmbox: %s\n", err);
+	qunlock(&mbllock);
+}
 
-	for(h = head; h->type != nil; h++)
-		h->len = strlen(h->type);
+
+char*
+removembox(char *name, int flags)
+{
+	int found;
+	Mailbox **l, *mb;
+
+	found = 0;
+	qlock(&mbllock);
+	for(l=&mbl; *l != nil; l=&(*l)->next)
+		if(strcmp(name, (*l)->path) == 0){
+			mb = *l;
+			*l = mb->next;
+			mb->flags |= ORCLOSE;
+			mb->rmflags = flags;
+			mboxdecref(mb);
+			found = 1;
+			break;
+		}
+	hfree(PATH(0, Qtop), name);
+	qunlock(&mbllock);
+
+	if(found == 0)
+		return "maibox not found";
+	return 0;
 }
 
 /*
- *  parse a Unix style header
+ *  look for the date in the first Received: line.
+ *  it's likely to be the right time zone (it's
+ *  the local system) and in a convenient format.
  */
-void
-parseunix(Message *m)
+static int
+rxtotm(Message *m, Tm *tm)
 {
-	char *p;
-	String *h;
+	char *p, *q;
+	int r;
 
-	h = s_new();
-	for(p = m->start + 5; *p && *p != '\r' && *p != '\n'; p++)
-		s_putc(h, *p);
-	s_terminate(h);
-	s_restart(h);
+	if(cistrncmp(m->header, "received:", 9))
+		return -1;
+	q = strchr(m->header, ';');
+	if(!q)
+		return -1;
+	p = q;
+	while((p = strchr(p, '\n')) != nil){
+		if(p[1] != ' ' && p[1] != '\t' && p[1] != '\n')
+			break;
+		p++;
+	}
+	if(!p)
+		return -1;
+	*p = '\0';
+	r = strtotm(q + 1, tm);
+	*p = '\n';
+	return r;
+}
 
-	m->unixfrom = s_parse(h, s_reset(m->unixfrom));
-	m->unixdate = s_append(s_reset(m->unixdate), h->ptr);
+Message*
+gettopmsg(Mailbox *mb, Message *m)
+{
+	while(!Topmsg(mb, m))
+		m = m->whole;
+	return m;
+}
 
-	s_free(h);
+void
+datesec(Mailbox *mb, Message *m)
+{
+	char *s;
+	vlong v;
+	Tm tm;
+
+	if(m->fileid > 1000000ull<<8)
+		return;
+	if(m->unixfrom && strtotm(m->unixfrom, &tm) >= 0)
+		v = tm2sec(&tm);
+	else if(m->date822 && strtotm(m->date822, &tm) >= 0)
+		v = tm2sec(&tm);
+	else if(rxtotm(m, &tm) >= 0)
+		v = tm2sec(&tm);
+	else{
+		s = rtab[m->type].s;
+		logmsg(gettopmsg(mb, m), "%s:%s: datasec %s %s\n", mb->path,
+			m->whole? m->whole->name: "?",
+			m->name, s);
+		if(Topmsg(mb, m) || strcmp(s, "message/rfc822") == 0)
+			abort();
+		v = 0;
+	}
+	m->fileid = v<<8;
 }
 
 /*
  *  parse a message
  */
-void
-parseheaders(Message *m, int justmime, Mailbox *mb, int addfrom)
+extern void sanemsg(Message*);
+extern void sanembmsg(Mailbox*, Message*);
+
+static Message*
+haschild(Message *m, int i)
 {
-	String *hl;
-	Header *h;
-	char *p, *q;
+	for(m = m->part; m && i; i--)
+		m = m->next;
+	if(m)
+		m->mimeflag = 0;
+	return m;
+}
+
+static void
+parseattachments(Message *m, Mailbox *mb)
+{
+	char *p, *x;
 	int i;
+	Message *nm, **l;
 
-	if(m->whole == m->whole->whole){
-		henter(PATH(mb->id, Qmbox), m->name,
-			(Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
-	} else {
-		henter(PATH(m->whole->id, Qdir), m->name,
-			(Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
-	}
-	for(i = 0; i < Qmax; i++)
-		henter(PATH(m->id, Qdir), dirtab[i],
-			(Qid){PATH(m->id, i), 0, QTFILE}, m, mb);
+	/* if there's a boundary, recurse... */
+sanemsg(m);
+//	dprint("parseattachments %p %ld\n", m->start, m->end - m->start);
+	if(m->boundary != nil){
+		p = m->body;
+		nm = nil;
+		l = &m->part;
+		for(i = 0;;){
+sanemsg(m);
+			x = strstr(p, m->boundary);
+			/* two sequential boundaries; ignore nil message */
+			if(nm && x == p){
+				p = strchr(x, '\n');
+				if(p == nil){
+					nm->rbend = nm->bend = nm->end = x;
+					sanemsg(nm);
+					break;
+				}
+				p = p + 1;
+				continue;
+			}
+			/* no boundary, we're done */
+			if(x == nil){
+				if(nm != nil){
+					nm->rbend = nm->bend = nm->end = m->bend;
+sanemsg(nm);
+					if(nm->end == nm->start)
+						nm->mimeflag |= Mtrunc;
+				}
+				break;
+			}
+			/* boundary must be at the start of a line */
+			if(x != m->body && x[-1] != '\n'){
+				p = x + 1;
+				continue;
+			}
 
-	// parse mime headers
-	p = m->header;
-	hl = s_new();
-	while(headerline(&p, hl)){
-		if(justmime)
-			h = &head[Mhead];
-		else
-			h = head;
-		for(; h->type; h++){
-			if(cistrncmp(s_to_c(hl), h->type, h->len) == 0){
-				(*h->f)(m, h, s_to_c(hl));
+			if(nm != nil)
+{
+				nm->rbend = nm->bend = nm->end = x;
+sanemsg(nm);}
+			x += strlen(m->boundary);
+
+			/* is this the last part? ignore anything after it */
+			if(strncmp(x, "--", 2) == 0)
 				break;
+			p = strchr(x, '\n');
+			if(p == nil)
+				break;
+			if((nm = haschild(m, i++)) == nil){
+				nm = newmessage(m);
+				*l = nm;
+				l = &nm->next;
 			}
+			nm->start = ++p;
+			assert(nm->ballocd == 0);
+			nm->mheader = nm->header = nm->body = nm->rbody = nm->start;
 		}
-		s_reset(hl);
+		for(nm = m->part; nm != nil; nm = nm->next){
+			parse(mb, nm, 0, 1);
+			cachehash(mb, nm);		/* botchy place for this */
+		}
+		return;
 	}
-	s_free(hl);
 
-	// the blank line isn't really part of the body or header
+	/* if we've got an rfc822 message, recurse... */
+	if(strcmp(rtab[m->type].s, "message/rfc822") == 0){
+		if((nm = haschild(m, 0)) == nil){
+			nm = newmessage(m);
+			m->part = nm;
+		}
+		assert(nm->ballocd == 0);
+		nm->start = nm->header = nm->body = nm->rbody = m->body;
+		nm->end = nm->bend = nm->rbend = m->bend;
+		if(nm->end == nm->start)
+			nm->mimeflag |= Mtrunc;
+		parse(mb, nm, 0, 0);
+		cachehash(mb, nm);			/* botchy place for this */
+	}
+}
+
+void
+parseheaders(Mailbox *mb, Message *m, int addfrom, int justmime)
+{
+	char *p, *e, *o, *t, *s;
+	int i, i0, n;
+	uintptr a;
+
+/*sanembmsg(mb, m);	/* fails with pop but i want this debugging for now */
+
+	/* parse mime headers */
+	p = m->header;
+	i0 = 0;
+	if(justmime)
+		i0 = Mhead;
+	s = emalloc(2048);
+	e = s + 2048 - 1;
+	while((n = hdrlen(p, m->end)) != 0){
+		if(n > e - s){
+			s = erealloc(s, n);
+			e = s + n - 1;
+		}
+		rfc2047(s, e, p, n, 1);
+		p += n;
+
+		for(i = i0; i < nelem(head); i++)
+			if(!cistrncmp(s, head[i].type, head[i].len)){
+				a = head[i].offset;
+				if(a != ~0){
+					if(o = *(char**)((char*)m + a))
+						continue;
+					t = head[i].f(m, head + i, o, s);
+					*(char**)((char*)m + a) = t;
+				}else
+					head[i].f(m, head + i, 0, s);
+				break;
+			}
+	}
+	free(s);
+/*sanembmsg(mb, m);	/* fails with pop but i want this debugging for now */
+	/* the blank line isn't really part of the body or header */
 	if(justmime){
 		m->mhend = p;
 		m->hend = m->header;
-	} else {
+	} else{
 		m->hend = p;
+		m->mhend = m->header;
 	}
+	/*
+	 * not all attachments have mime headers themselves.
+	 */
+	if(!m->mheader)
+		m->mhend = 0;
 	if(*p == '\n')
 		p++;
 	m->rbody = m->body = p;
 
-	// if type is text, get any nulls out of the body.  This is
-	// for the two seans and imap clients that get confused.
-	if(strncmp(s_to_c(m->type), "text/", 5) == 0)
-		nullsqueeze(m);
+	if(!justmime)
+		datesec(mb, m);
 
-	//
-	// cobble together Unix-style from line
-	// for local mailbox messages, we end up recreating the
-	// original header.
-	// for pop3 messages, the best we can do is 
-	// use the From: information and the RFC822 date.
-	//
-	if(m->unixdate == nil || strcmp(s_to_c(m->unixdate), "???") == 0
-	|| strcmp(s_to_c(m->unixdate), "Thu Jan 1 00:00:00 GMT 1970") == 0){
-		if(m->unixdate){
-			s_free(m->unixdate);
-			m->unixdate = nil;
-		}
-		// look for the date in the first Received: line.
-		// it's likely to be the right time zone (it's
-	 	// the local system) and in a convenient format.
-		if(cistrncmp(m->header, "received:", 9)==0){
-			if((q = strchr(m->header, ';')) != nil){
-				p = q;
-				while((p = strchr(p, '\n')) != nil){
-					if(p[1] != ' ' && p[1] != '\t' && p[1] != '\n')
-						break;
-					p++;
-				}
-				if(p){
-					*p = '\0';
-					m->unixdate = date822tounix(q+1);
-					*p = '\n';
-				}
-			}
-		}
-
-		// fall back on the rfc822 date	
-		if(m->unixdate==nil && m->date822)
-			m->unixdate = date822tounix(s_to_c(m->date822));
-	}
-
-	if(m->unixheader != nil)
-		s_free(m->unixheader);
-
-	// only fake header for top-level messages for pop3 and imap4
-	// clients (those protocols don't include the unix header).
-	// adding the unix header all the time screws up mime-attached
-	// rfc822 messages.
-	if(!addfrom && !m->unixfrom){
+	/*
+	 *  only fake header for top-level messages for pop3 and imap4
+	 *  clients (those protocols don't include the unix header).
+	 *  adding the unix header all the time screws up mime-attached
+	 *  rfc822 messages.
+	 */
+/*sanembmsg(mb, m);	/* fails with pop but i want this debugging for now */
+	if(!addfrom && !m->unixfrom)
 		m->unixheader = nil;
-		return;
+	else if(m->unixheader == nil){
+		if(m->unixfrom && strcmp(m->unixfrom, "???") != 0)
+			p = m->unixfrom;
+		else if(m->from)
+			p = m->from;
+		else
+			p = "???";
+		m->unixheader = smprint("From %s %Δ\n", p, m->fileid);
 	}
-
-	m->unixheader = s_copy("From ");
-	if(m->unixfrom && strcmp(s_to_c(m->unixfrom), "???") != 0)
-		s_append(m->unixheader, s_to_c(m->unixfrom));
-	else if(m->from822)
-		s_append(m->unixheader, s_to_c(m->from822));
-	else
-		s_append(m->unixheader, "???");
-
-	s_append(m->unixheader, " ");
-	if(m->unixdate)
-		s_append(m->unixheader, s_to_c(m->unixdate));
-	else
-		s_append(m->unixheader, "Thu Jan  1 00:00:00 GMT 1970");
-
-	s_append(m->unixheader, "\n");
+	m->cstate |= Cheader;
+sanembmsg(mb, m);
 }
 
-String*
-promote(String **sp)
+char*
+promote(char *s)
 {
-	String *s;
-
-	if(*sp != nil)
-		s = s_clone(*sp);
-	else
-		s = nil;
-	return s;
+	return s? strdup(s): nil;
 }
 
 void
 parsebody(Message *m, Mailbox *mb)
 {
+	char *s;
+	int l;
 	Message *nm;
 
-	// recurse
-	if(strncmp(s_to_c(m->type), "multipart/", 10) == 0){
+	/* recurse */
+	s = rtab[m->type].s;
+	l = rtab[m->type].l;
+	if(l >= 10 && strncmp(s, "multipart/", 10) == 0)
 		parseattachments(m, mb);
-	} else if(strcmp(s_to_c(m->type), "message/rfc822") == 0){
+	else if(l == 14 && strcmp(s, "message/rfc822") == 0){
 		decode(m);
 		parseattachments(m, mb);
 		nm = m->part;
 
-		// promote headers
-		if(m->replyto822 == nil && m->from822 == nil && m->sender822 == nil){
-			m->from822 = promote(&nm->from822);
-			m->to822 = promote(&nm->to822);
-			m->date822 = promote(&nm->date822);
-			m->sender822 = promote(&nm->sender822);
-			m->replyto822 = promote(&nm->replyto822);
-			m->subject822 = promote(&nm->subject822);
-			m->unixdate = promote(&nm->unixdate);
+		/* promote headers */
+		if(m->replyto == nil && m->from == nil && m->sender == nil){
+			m->from = promote(nm->from);
+			m->to = promote(nm->to);
+			m->date822 = promote(nm->date822);
+			m->sender = promote(nm->sender);
+			m->replyto = promote(nm->replyto);
+			m->subject = promote(nm->subject);
 		}
-	}
+	}else if(strncmp(rtab[m->type].s, "text/", 5) == 0)
+		sanemsg(m);
+	m->rawbsize = m->rbend - m->rbody;
+	m->cstate |= Cbody;
 }
 
 void
-parse(Message *m, int justmime, Mailbox *mb, int addfrom)
+parse(Mailbox *mb, Message *m, int addfrom, int justmime)
 {
-	parseheaders(m, justmime, mb, addfrom);
+	sanemsg(m);
+	assert(m->end - m->start > 0 || m->mimeflag&Mtrunc && m->end - m->start == 0);
+	if((m->cstate & Cheader) == 0)
+		parseheaders(mb, m, addfrom, justmime);
 	parsebody(m, mb);
+	sanemsg(m);
 }
 
-static void
-parseattachments(Message *m, Mailbox *mb)
+static char*
+skipwhite(char *p)
 {
-	Message *nm, **l;
-	char *p, *x;
+	while(isascii(*p) && isspace(*p))
+		p++;
+	return p;
+}
 
-	// if there's a boundary, recurse...
-	if(m->boundary != nil){
-		p = m->body;
-		nm = nil;
-		l = &m->part;
-		for(;;){
-			x = strstr(p, s_to_c(m->boundary));
+static char*
+skiptosemi(char *p)
+{
+	while(*p && *p != ';')
+		p++;
+	while(*p == ';' || (isascii(*p) && isspace(*p)))
+		p++;
+	return p;
+}
 
-			/* no boundary, we're done */
-			if(x == nil){
-				if(nm != nil)
-					nm->rbend = nm->bend = nm->end = m->bend;
-				break;
-			}
+static char*
+getstring(char *p, char *s, char *e, int dolower)
+{
+	int c;
 
-			/* boundary must be at the start of a line */
-			if(x != m->body && *(x-1) != '\n'){
-				p = x+1;
+	p = skipwhite(p);
+	if(*p == '"'){
+		for(p++; (c = *p) != '"'; p++){
+			if(c == '\\')
+				c = *++p;
+			/*
+			 * 821 says <x> after \ can be anything at all.
+			 * we just don't care.
+			 */
+			if(c == 0)
+				break;
+			if(c < ' ')
 				continue;
-			}
-
-			if(nm != nil)
-				nm->rbend = nm->bend = nm->end = x;
-			x += strlen(s_to_c(m->boundary));
-
-			/* is this the last part? ignore anything after it */
-			if(strncmp(x, "--", 2) == 0)
+			if(dolower && c >= 'A' && c <= 'Z')
+				c += 0x20;
+			s = sputc(s, e, c);
+		}
+		if(*p == '"')
+			p++;
+	}else{
+		for(; (c = *p) && !isspace(c) && c != ';'; p++){
+			if(c == '\\')
+				c = *++p;
+			/*
+			 * 821 says <x> after \ can be anything at all.
+			 * we just don't care.
+			 */
+			if(c == 0)
 				break;
-
-			p = strchr(x, '\n');
-			if(p == nil)
-				break;
-			nm = newmessage(m);
-			nm->start = nm->header = nm->body = nm->rbody = ++p;
-			nm->mheader = nm->header;
-			*l = nm;
-			l = &nm->next;
+			if(c < ' ')
+				continue;
+			if(dolower && c >= 'A' && c <= 'Z')
+				c += 0x20;
+			s = sputc(s, e, c);
 		}
-		for(nm = m->part; nm != nil; nm = nm->next)
-			parse(nm, 1, mb, 0);
-		return;
 	}
+	*s = 0;
+	return p;
+}
 
-	// if we've got an rfc822 message, recurse...
-	if(strcmp(s_to_c(m->type), "message/rfc822") == 0){
-		nm = newmessage(m);
-		m->part = nm;
-		nm->start = nm->header = nm->body = nm->rbody = m->body;
-		nm->end = nm->bend = nm->rbend = m->bend;
-		parse(nm, 0, mb, 0);
-	}
+static void
+setfilename(Message *m, char *p)
+{
+	char buf[Pathlen];
+
+	free(m->filename);
+	getstring(p, buf, buf + sizeof buf - 1, 0);
+	m->filename = smprint("%s", buf);
+	for(p = m->filename; *p; p++)
+		if(*p == ' ' || *p == '\t' || *p == ';')
+			*p = '_';
 }
 
-/*
- *  pick up a header line
- */
-static int
-headerline(char **pp, String *hl)
+static char*
+rtrim(char *p)
 {
-	char *p, *x;
+	char *e;
 
-	s_reset(hl);
-	p = *pp;
-	x = strpbrk(p, ":\n");
-	if(x == nil || *x == '\n')
-		return 0;
-	for(;;){
-		x = strchr(p, '\n');
-		if(x == nil)
-			x = p + strlen(p);
-		s_nappend(hl, p, x-p);
-		p = x;
-		if(*p != '\n' || *++p != ' ' && *p != '\t')
-			break;
-		while(*p == ' ' || *p == '\t')
-			p++;
-		s_putc(hl, ' ');
-	}
-	*pp = p;
-	return 1;
+	if(p == 0)
+		return p;
+	e = p + strlen(p) - 1;
+	while(e > p && isascii(*e) && isspace(*e))
+		*e-- = 0;
+	return p;
 }
 
-/* returns nil iff there are no addressees */
-static String*
-addr822(char *p)
+static char*
+addr822(char *p, char **ac)
 {
-	String *s, *list;
-	int incomment, addrdone, inanticomment, quoted;
-	int n;
-	int c;
+	int n, c, space, incomment, addrdone, inanticomment, quoted;
+	char s[128+1], *ps, *e, *x, *list;
 
-	list = s_new();
-	s = s_new();
-	quoted = incomment = addrdone = inanticomment = 0;
+	list = 0;
+	s[0] = 0;
+	ps = s;
+	e = s + sizeof s;
+	space = quoted = incomment = addrdone = inanticomment = 0;
 	n = 0;
-	for(; *p; p++){
-		c = *p;
-
-		// whitespace is ignored
+	for(; c = *p; p++){
+		if(!inanticomment && !quoted && !space && ps != s && c == ' '){
+			ps = sputc(ps, e, c);
+			space = 1;
+			continue;
+		}
+		space = 0;
 		if(!quoted && isspace(c) || c == '\r')
 			continue;
-
-		// strings are always treated as atoms
+		/* strings are always treated as atoms */
 		if(!quoted && c == '"'){
-			if(!addrdone && !incomment)
-				s_putc(s, c);
-			for(p++; *p; p++){
+			if(!addrdone && !incomment && !ac)
+				ps = sputc(ps, e, c);
+			for(p++; c = *p; p++){
+				if(ac && c == '"')
+					break;
 				if(!addrdone && !incomment)
-					s_putc(s, *p);
+					ps = sputc(ps, e, c);
 				if(!quoted && *p == '"')
 					break;
 				if(*p == '\\')
@@ -514,13 +736,13 @@
 				else
 					quoted = 0;
 			}
-			if(*p == 0)
+			if(c == 0)
 				break;
 			quoted = 0;
 			continue;
 		}
 
-		// ignore everything in an expicit comment
+		/* ignore everything in an expicit comment */
 		if(!quoted && c == '('){
 			incomment = 1;
 			continue;
@@ -532,10 +754,21 @@
 			continue;
 		}
 
-		// anticomments makes everything outside of them comments
+		/* anticomments makes everything outside of them comments */
 		if(!quoted && c == '<' && !inanticomment){
+			if(ac){
+				*ps-- = 0;
+				if(ps > s && *ps == ' ')
+					*ps = 0;
+				if(*ac){
+					*ac = smprint("%s, %s", x=*ac, s);
+					free(x);
+				}else
+					*ac = smprint("%s", s);
+			}
+
 			inanticomment = 1;
-			s = s_reset(s);
+			ps = s;
 			continue;
 		}
 		if(!quoted && c == '>' && inanticomment){
@@ -544,21 +777,23 @@
 			continue;
 		}
 
-		// commas separate addresses
+		/* commas separate addresses */
 		if(!quoted && c == ',' && !inanticomment){
-			s_terminate(s);
+			*ps = 0;
 			addrdone = 0;
-			if(n++ != 0)
-				s_append(list, " ");
-			s_append(list, s_to_c(s));
-			s = s_reset(s);
+			if(n++ != 0){
+				list = smprint("%s %s", x=list, s);
+				free(x);
+			}else
+				list = smprint("%s", s);
+			ps = s;
 			continue;
 		}
 
-		// what's left is part of the address
-		s_putc(s, c);
+		/* what's left is part of the address */
+		ps = sputc(ps, e, c);
 
-		// quoted characters are recognized only as characters
+		/* quoted characters are recognized only as characters */
 		if(c == '\\')
 			quoted = 1;
 		else
@@ -566,19 +801,15 @@
 
 	}
 
-	if(*s_to_c(s) != 0){
-		s_terminate(s);
-		if(n++ != 0)
-			s_append(list, " ");
-		s_append(list, s_to_c(s));
+	if(ps > s){
+		*ps = 0;
+		if(n != 0){
+			list = smprint("%s %s", x=list, s);
+			free(x);
+		}else
+			list = smprint("%s", s);
 	}
-	s_free(s);
-
-	if(n == 0){		/* no addressees given, just the keyword */
-		s_free(list);
-		return nil;
-	}
-	return list;
+	return rtrim(list);
 }
 
 /*
@@ -586,140 +817,97 @@
  * concatenating their values.
  */
 
-static void
-to822(Message *m, Header *h, char *p)
+static char*
+concat822(Message*, Header *h, char *o, char *p)
 {
-	String *s;
+	char *s, *n;
 
 	p += strlen(h->type);
-	s = addr822(p);
-	if (m->to822 == nil)
-		m->to822 = s;
-	else if (s != nil) {
-		s_append(m->to822, " ");
-		s_append(m->to822, s_to_c(s));
-		s_free(s);
-	}
+	s = addr822(p, 0);
+	if(o){
+		n = smprint("%s %s", o, s);
+		free(s);
+	}else
+		n = s;
+	return n;
 }
 
-static void
-cc822(Message *m, Header *h, char *p)
+static char*
+from822(Message *m, Header *h, char*, char *p)
 {
-	String *s;
-
-	p += strlen(h->type);
-	s = addr822(p);
-	if (m->cc822 == nil)
-		m->cc822 = s;
-	else if (s != nil) {
-		s_append(m->cc822, " ");
-		s_append(m->cc822, s_to_c(s));
-		s_free(s);
-	}
+	if(m->ffrom)
+		free(m->ffrom);
+	m->from = 0;
+	return addr822(p + h->len, &m->ffrom);
 }
 
-static void
-bcc822(Message *m, Header *h, char *p)
+static char*
+replace822(Message *, Header *h, char*, char *p)
 {
-	String *s;
-
-	p += strlen(h->type);
-	s = addr822(p);
-	if (m->bcc822 == nil)
-		m->bcc822 = s;
-	else if (s != nil) {
-		s_append(m->bcc822, " ");
-		s_append(m->bcc822, s_to_c(s));
-		s_free(s);
-	}
+	return addr822(p + h->len, 0);
 }
 
-static void
-from822(Message *m, Header *h, char *p)
+static char*
+copy822(Message*, Header *h, char*, char *p)
 {
-	p += strlen(h->type);
-	s_free(m->from822);
-	m->from822 = addr822(p);
+	return rtrim(strdup(skipwhite(p + h->len)));
 }
 
-static void
-sender822(Message *m, Header *h, char *p)
+/*
+ * firefox, e.g. doesn't keep references unique
+ */
+static int
+uniqarray(char **a, int n, int allocd)
 {
-	p += strlen(h->type);
-	s_free(m->sender822);
-	m->sender822 = addr822(p);
-}
+	int i, j;
 
-static void
-replyto822(Message *m, Header *h, char *p)
-{
-	p += strlen(h->type);
-	s_free(m->replyto822);
-	m->replyto822 = addr822(p);
+	for(i = 0; i < n; i++)
+		for(j = i + 1; j < n; j++)
+			if(strcmp(a[i], a[j]) == 0){
+				if(allocd)
+					free(a[j]);
+				memmove(a + j, a + j + 1, sizeof *a*(n - (j + 1)));
+				a[--n] = 0;
+			}
+	return n;
 }
 
-static void
-mimeversion(Message *m, Header *h, char *p)
+static char*
+ref822(Message *m, Header *h, char*, char *p)
 {
-	p += strlen(h->type);
-	s_free(m->mimeversion);
-	m->mimeversion = addr822(p);
-}
+	char **a, *s, *f[Nref + 1];
+	int i, j, k, n;
 
-static void
-killtrailingwhite(char *p)
-{
-	char *e;
-
-	e = p + strlen(p) - 1;
-	while(e > p && isspace(*e))
-		*e-- = 0;
+	s = strdup(skipwhite(p + h->len));
+	n = getfields(s, f, nelem(f), 1, "<> \n\t\r,");
+	if(n > Nref)
+		n = Nref;
+	n = uniqarray(f, n, 0);
+	a = m->references;
+	for(i = 0; i < Nref; i++)
+		if(a[i] == 0)
+			break;
+	/*
+	 * if there are too many references, drop from the beginning
+	 * of the list.
+	 */
+	j = i + n - Nref;
+	if(j > 0){
+		if(j > Nref)
+			j = Nref;
+		for(k = 0; k < j; k++)
+			free(a[k]);
+		memmove(a, a + j, sizeof a[0]*(Nref - j));
+		memset(a + j, 0, Nref - j);
+		i -= j;
+	}
+	for(j = 0; j < n;)
+		a[i++] = strdup(f[j++]);
+	free(s);
+	uniqarray(a, i, 1);
+	return (char*)~0;
 }
 
-static void
-date822(Message *m, Header *h, char *p)
-{
-	p += strlen(h->type);
-	p = skipwhite(p);
-	s_free(m->date822);
-	m->date822 = s_copy(p);
-	p = s_to_c(m->date822);
-	killtrailingwhite(p);
-}
-
-static void
-subject822(Message *m, Header *h, char *p)
-{
-	p += strlen(h->type);
-	p = skipwhite(p);
-	s_free(m->subject822);
-	m->subject822 = s_copy(p);
-	p = s_to_c(m->subject822);
-	killtrailingwhite(p);
-}
-
-static void
-inreplyto822(Message *m, Header *h, char *p)
-{
-	p += strlen(h->type);
-	p = skipwhite(p);
-	s_free(m->inreplyto822);
-	m->inreplyto822 = s_copy(p);
-	p = s_to_c(m->inreplyto822);
-	killtrailingwhite(p);
-}
-
-static void
-messageid822(Message *m, Header *h, char *p)
-{
-	p += strlen(h->type);
-	p = skipwhite(p);
-	s_free(m->messageid822);
-	m->messageid822 = s_copy(p);
-	p = s_to_c(m->messageid822);
-	killtrailingwhite(p);
-}
-
 static int
 isattribute(char **pp, char *attr)
 {
@@ -741,24 +929,20 @@
 	return 1;
 }
 
-static void
-ctype(Message *m, Header *h, char *p)
+static char*
+ctype(Message *m, Header *h, char*, char *p)
 {
-	String *s;
+	char buf[128], *e;
 
-	p += h->len;
-	p = skipwhite(p);
+	e = buf + sizeof buf - 1;
+	p = getstring(skipwhite(p + h->len), buf, e, 1);
+	m->type = newrefs(buf);
 
-	p = getstring(p, m->type, 1);
-	
-	while(*p){
+	for(; *p; p = skiptosemi(p))
 		if(isattribute(&p, "boundary")){
-			s = s_new();
-			p = getstring(p, s, 0);
-			m->boundary = s_reset(m->boundary);
-			s_append(m->boundary, "--");
-			s_append(m->boundary, s_to_c(s));
-			s_free(s);
+			p = getstring(p, buf, e, 0);
+			free(m->boundary);
+			m->boundary = smprint("--%s", buf);
 		} else if(cistrncmp(p, "multipart", 9) == 0){
 			/*
 			 *  the first unbounded part of a multipart message,
@@ -768,44 +952,41 @@
 			if(m->filename == nil)
 				setfilename(m, p);
 		} else if(isattribute(&p, "charset")){
-			p = getstring(p, s_reset(m->charset), 0);
+			p = getstring(p, buf, e, 0);
+			lowercase(buf);
+			m->charset = newrefs(buf);
 		}
-		
-		p = skiptosemi(p);
-	}
+	return (char*)~0;
 }
 
-static void
-cencoding(Message *m, Header *h, char *p)
+static char*
+cencoding(Message *m, Header *h, char*, char *p)
 {
-	p += h->len;
-	p = skipwhite(p);
+	p = skipwhite(p + h->len);
 	if(cistrncmp(p, "base64", 6) == 0)
 		m->encoding = Ebase64;
 	else if(cistrncmp(p, "quoted-printable", 16) == 0)
 		m->encoding = Equoted;
+	return (char*)~0;
 }
 
-static void
-cdisposition(Message *m, Header *h, char *p)
+static char*
+cdisposition(Message *m, Header *h, char*, char *p)
 {
-	p += h->len;
-	p = skipwhite(p);
-	while(*p){
-		if(cistrncmp(p, "inline", 6) == 0){
+	for(p = skipwhite(p + h->len); *p; p = skiptosemi(p))
+		if(cistrncmp(p, "inline", 6) == 0)
 			m->disposition = Dinline;
-		} else if(cistrncmp(p, "attachment", 10) == 0){
+		else if(cistrncmp(p, "attachment", 10) == 0)
 			m->disposition = Dfile;
-		} else if(cistrncmp(p, "filename=", 9) == 0){
+		else if(cistrncmp(p, "filename=", 9) == 0){
 			p += 9;
 			setfilename(m, p);
 		}
-		p = skiptosemi(p);
-	}
-
+	return (char*)~0;
 }
 
-ulong msgallocd, msgfreed;
+ulong	msgallocd;
+ulong	msgfreed;
 
 Message*
 newmessage(Message *parent)
@@ -815,14 +996,16 @@
 
 	msgallocd++;
 
-	m = emalloc(sizeof(*m));
-	memset(m, 0, sizeof(*m));
+	m = emalloc(sizeof *m);
+	dprint("newmessage %ld	%p	%p\n", msgallocd, parent, m);
 	m->disposition = Dnone;
-	m->type = s_copy("text/plain");
-	m->charset = s_copy("iso-8859-1");
+	m->type = newrefs("text/plain");
+	m->charset = newrefs("iso-8859-1");
+	m->cstate = Cidxstale;
+	m->flags = Frecent;
 	m->id = newid();
 	if(parent)
-		sprint(m->name, "%d", ++(parent->subname));
+		snprint(m->name, sizeof m->name, "%d", ++(parent->subname));
 	if(parent == nil)
 		parent = m;
 	m->whole = parent;
@@ -830,74 +1013,65 @@
 	return m;
 }
 
-// delete a message from a mailbox
+/* delete a message from a mailbox */
 void
 delmessage(Mailbox *mb, Message *m)
 {
 	Message **l;
-	int i;
 
 	mb->vers++;
 	msgfreed++;
 
 	if(m->whole != m){
-		// unchain from parent
+		/* unchain from parent */
 		for(l = &m->whole->part; *l && *l != m; l = &(*l)->next)
 			;
 		if(*l != nil)
 			*l = m->next;
 
-		// clear out of name lookup hash table
+		/* clear out of name lookup hash table */
 		if(m->whole->whole == m->whole)
 			hfree(PATH(mb->id, Qmbox), m->name);
 		else
 			hfree(PATH(m->whole->id, Qdir), m->name);
-		for(i = 0; i < Qmax; i++)
-			hfree(PATH(m->id, Qdir), dirtab[i]);
+		hfree(PATH(m->id, Qdir), "xxx");		/* sleezy speedup */
 	}
-
-	/* recurse through sub-parts */
+	if(Topmsg(mb, m)){
+		if(m != mb->root)
+			mtreedelete(mb, m);
+		cachefree(mb, m, 1);
+	}
+	delrefs(m->type);
+	delrefs(m->charset);
+	idxfree(m);
 	while(m->part)
 		delmessage(mb, m->part);
+	free(m->unixfrom);
+	free(m->unixheader);
+	free(m->date822);
+	free(m->inreplyto);
+	free(m->boundary);
+	free(m->filename);
 
-	/* free memory */
-	if(m->mallocd)
-		free(m->start);
-	if(m->hallocd)
-		free(m->header);
-	if(m->ballocd)
-		free(m->body);
-	s_free(m->unixfrom);
-	s_free(m->unixdate);
-	s_free(m->unixheader);
-	s_free(m->from822);
-	s_free(m->sender822);
-	s_free(m->to822);
-	s_free(m->bcc822);
-	s_free(m->cc822);
-	s_free(m->replyto822);
-	s_free(m->date822);
-	s_free(m->inreplyto822);
-	s_free(m->subject822);
-	s_free(m->messageid822);
-	s_free(m->addrs);
-	s_free(m->mimeversion);
-	s_free(m->sdigest);
-	s_free(m->boundary);
-	s_free(m->type);
-	s_free(m->charset);
-	s_free(m->filename);
-
 	free(m);
 }
 
-// mark messages (identified by path) for deletion
 void
+unnewmessage(Mailbox *mb, Message *parent, Message *m)
+{
+	assert(parent->subname > 0);
+	m->deleted = Dup;
+	delmessage(mb, m);
+	parent->subname -= 1;
+}
+
+/* mark messages (identified by path) for deletion */
+char*
 delmessages(int ac, char **av)
 {
+	int i, needwrite;
 	Mailbox *mb;
 	Message *m;
-	int i, needwrite;
 
 	qlock(&mbllock);
 	for(mb = mbl; mb != nil; mb = mb->next)
@@ -907,26 +1081,61 @@
 		}
 	qunlock(&mbllock);
 	if(mb == nil)
-		return;
+		return "no such mailbox";
 
 	needwrite = 0;
-	for(i = 1; i < ac; i++){
+	for(i = 1; i < ac; i++)
 		for(m = mb->root->part; m != nil; m = m->next)
 			if(strcmp(m->name, av[i]) == 0){
 				if(!m->deleted){
 					mailplumb(mb, m, 1);
 					needwrite = 1;
-					m->deleted = 1;
-					logmsg("deleting", m);
+					m->deleted = Deleted;
+					logmsg(m, "deleting");
 				}
 				break;
 			}
-	}
 	if(needwrite)
 		syncmbox(mb, 1);
 	qunlock(mb);
+	return 0;
 }
 
+char*
+flagmessages(int argc, char **argv)
+{
+	char *err, *rerr;
+	int i, needwrite;
+	Mailbox *mb;
+	Message *m;
+
+	if(argc%2)
+		return "bad flags";
+	qlock(&mbllock);
+	for(mb = mbl; mb; mb = mb->next)
+		if(strcmp(*argv, mb->name) == 0){
+			qlock(mb);
+			break;
+		}
+	qunlock(&mbllock);
+	if(mb == nil)
+		return "no such mailbox";
+	needwrite = 0;
+	rerr = 0;
+	for(i = 1; i < argc; i += 2)
+		for(m = mb->root->part; m; m = m->next)
+			if(strcmp(m->name, argv[i]) == 0){
+				if(err = modflags(mb, m, argv[i + 1]))
+					rerr = err;
+				else
+					needwrite = 1;
+			}
+	if(needwrite)
+		syncmbox(mb, 1);
+	qunlock(mb);
+	return rerr;
+}
+
 /*
  *  the following are called with the mailbox qlocked
  */
@@ -935,12 +1144,18 @@
 {
 	m->refs++;
 }
+
 void
 msgdecref(Mailbox *mb, Message *m)
 {
+	assert(m->refs > 0);
 	m->refs--;
-	if(m->refs == 0 && m->deleted)
-		syncmbox(mb, 1);
+	if(m->refs == 0){
+		if(m->deleted)
+			syncmbox(mb, 1);
+		else
+			putcache(mb, m);
+	}
 }
 
 /*
@@ -952,6 +1167,20 @@
 	assert(mb->refs > 0);
 	mb->refs++;
 }
+
+static void
+mbrmidx(char *path, int flags)
+{
+	char buf[Pathlen];
+
+	snprint(buf, sizeof buf, "%s.idx", path);
+	vremove(buf);
+	if((flags & Rtrunc) == 0){
+		snprint(buf, sizeof buf, "%s.imp", path);
+		vremove(buf);
+	}
+}
+
 void
 mboxdecref(Mailbox *mb)
 {
@@ -959,98 +1188,41 @@
 	qlock(mb);
 	mb->refs--;
 	if(mb->refs == 0){
+		syncmbox(mb, 1);
 		delmessage(mb, mb->root);
 		if(mb->ctl)
 			hfree(PATH(mb->id, Qmbox), "ctl");
 		if(mb->close)
-			(*mb->close)(mb);
+			mb->close(mb);
+		if(mb->flags & ORCLOSE && mb->remove)
+		if(mb->remove(mb, mb->rmflags))
+			rmidx(mb->path, mb->rmflags);
+		free(mb->mtree);
+		free(mb->d);
 		free(mb);
 	} else
 		qunlock(mb);
 }
 
+/* just space over \r.  sleezy but necessary for ms email. */
 int
-cistrncmp(char *a, char *b, int n)
+deccr(char *x, int len)
 {
-	while(n-- > 0){
-		if(tolower(*a++) != tolower(*b++))
-			return -1;
-	}
-	return 0;
-}
+	char *e;
 
-int
-cistrcmp(char *a, char *b)
-{
+	e = x + len;
 	for(;;){
-		if(tolower(*a) != tolower(*b++))
-			return -1;
-		if(*a++ == 0)
+		x = memchr(x, '\r', e - x);
+		if(x == nil)
 			break;
+		*x = ' ';
 	}
-	return 0;
+	return len;
 }
 
-static char*
-skipwhite(char *p)
-{
-	while(isspace(*p))
-		p++;
-	return p;
-}
-
-static char*
-skiptosemi(char *p)
-{
-	while(*p && *p != ';')
-		p++;
-	while(*p == ';' || isspace(*p))
-		p++;
-	return p;
-}
-
-static char*
-getstring(char *p, String *s, int dolower)
-{
-	s = s_reset(s);
-	p = skipwhite(p);
-	if(*p == '"'){
-		p++;
-		for(;*p && *p != '"'; p++)
-			if(dolower)
-				s_putc(s, tolower(*p));
-			else
-				s_putc(s, *p);
-		if(*p == '"')
-			p++;
-		s_terminate(s);
-
-		return p;
-	}
-
-	for(; *p && !isspace(*p) && *p != ';'; p++)
-		if(dolower)
-			s_putc(s, tolower(*p));
-		else
-			s_putc(s, *p);
-	s_terminate(s);
-
-	return p;
-}
-
-static void
-setfilename(Message *m, char *p)
-{
-	m->filename = s_reset(m->filename);
-	getstring(p, m->filename, 0);
-	for(p = s_to_c(m->filename); *p; p++)
-		if(*p == ' ' || *p == '\t' || *p == ';')
-			*p = '_';
-}
-
-//
-// undecode message body
-//
+/*
+ *  undecode message body
+ */
 void
 decode(Message *m)
 {
@@ -1062,9 +1234,15 @@
 	switch(m->encoding){
 	case Ebase64:
 		len = m->bend - m->body;
-		i = (len*3)/4+1;	// room for max chars + null
+		i = (len*3)/4 + 1;	/* room for max chars + null */
 		x = emalloc(i);
 		len = dec64((uchar*)x, i, m->body, len);
+		if(len == -1){
+			free(x);
+			break;
+		}
+		if(strncmp(rtab[m->type].s, "text/", 5) == 0)
+			len = deccr(x, len);
 		if(m->ballocd)
 			free(m->body);
 		m->body = x;
@@ -1073,7 +1251,7 @@
 		break;
 	case Equoted:
 		len = m->bend - m->body;
-		x = emalloc(len+2);	// room for null and possible extra nl
+		x = emalloc(len + 2);	/* room for null and possible extra nl */
 		len = decquoted(x, m->body, m->bend, 0);
 		if(m->ballocd)
 			free(m->body);
@@ -1087,7 +1265,7 @@
 	m->decoded = 1;
 }
 
-// convert latin1 to utf
+/* convert x to utf8 */
 void
 convert(Message *m)
 {
@@ -1094,15 +1272,13 @@
 	int len;
 	char *x;
 
-	// don't convert if we're not a leaf, not text, or already converted
+	/* don't convert if we're not a leaf, not text, or already converted */
 	if(m->converted)
 		return;
-	if(m->part != nil)
+	m->converted = 1;
+	if(m->part != nil || cistrncmp(rtab[m->type].s, "text", 4) != 0)
 		return;
-	if(cistrncmp(s_to_c(m->type), "text", 4) != 0)
-		return;
-
-	len = xtoutf(s_to_c(m->charset), &x, m->body, m->bend);
+	len = xtoutf(rtab[m->charset].s, &x, m->body, m->bend);
 	if(len > 0){
 		if(m->ballocd)
 			free(m->body);
@@ -1110,7 +1286,6 @@
 		m->bend = x + len;
 		m->ballocd = 1;
 	}
-	m->converted = 1;
 }
 
 static int
@@ -1119,18 +1294,20 @@
 	if(x >= '0' && x <= '9')
 		return x - '0';
 	if(x >= 'A' && x <= 'F')
-		return (x - 'A') + 10;
+		return x - 'A' + 10;
 	if(x >= 'a' && x <= 'f')
-		return (x - 'a') + 10;
+		return x - 'a' + 10;
 	return -1;
 }
 
-// underscores are translated in 2047 headers (uscores=1) 
-// but not in the body (uscores=0)
+/*
+ *  underscores are translated in 2047 headers (uscores=1)
+ *  but not in the body (uscores=0)
+ */
 static char*
 decquotedline(char *out, char *in, char *e, int uscores)
 {
-	int c, c2, soft;
+	int c, soft;
 
 	/* dump trailing white space */
 	while(e >= in && (*e == ' ' || *e == '\t' || *e == '\r' || *e == '\n'))
@@ -1155,11 +1332,11 @@
 			*out++ = c;
 			break;
 		case '=':
-			c  = hex2int(*in++);
-			c2 = hex2int(*in++);
-			if (c != -1 && c2 != -1)
-				*out++ = c<<4 | c2;
-			else {
+			c = hex2int(*in++)<<4;
+			c |= hex2int(*in++);
+			if(c != -1)
+				*out++ = c;
+			else{
 				*out++ = '=';
 				in -= 2;
 			}
@@ -1184,10 +1361,10 @@
 		in = nl + 1;
 	}
 	if(in < e)
-		p = decquotedline(p, in, e-1, uscores);
+		p = decquotedline(p, in, e - 1, uscores);
 
-	// make sure we end with a new line
-	if(*(p-1) != '\n'){
+	/* make sure we end with a new line */
+	if(*(p - 1) != '\n'){
 		*p++ = '\n';
 		*p = 0;
 	}
@@ -1195,7 +1372,7 @@
 	return p - out;
 }
 
-static char*
+char*
 lowercase(char *p)
 {
 	char *op;
@@ -1207,7 +1384,7 @@
 	return op;
 }
 
-// translate latin1 directly since it fits neatly in utf
+/* translate latin1 directly since it fits neatly in utf */
 static int
 latin1toutf(char **out, char *in, char *e)
 {
@@ -1222,13 +1399,13 @@
 	if(n == 0)
 		return 0;
 
-	n += e-in;
-	*out = p = malloc(UTFmax*n+1);
+	n += e - in;
+	*out = p = malloc(n + 1);
 	if(p == nil)
 		return 0;
 
 	for(; in < e; in++){
-		r = (*in) & 0xff;
+		r = (uchar)*in;
 		p += runetochar(p, &r);
 	}
 	*p = 0;
@@ -1235,25 +1412,22 @@
 	return p - *out;
 }
 
-// translate any thing using the tcs program
+/* translate any thing using the tcs program */
 int
 xtoutf(char *charset, char **out, char *in, char *e)
 {
-	char *av[4];
-	int totcs[2];
-	int fromtcs[2];
-	int n, len, sofar;
-	char *p;
+	char *av[4], *p;
+	int totcs[2], fromtcs[2], n, len, sofar;
 
-	// might not need to convert
+	/* might not need to convert */
 	if(cistrcmp(charset, "us-ascii") == 0 || cistrcmp(charset, "utf-8") == 0)
 		return 0;
 	if(cistrcmp(charset, "iso-8859-1") == 0)
 		return latin1toutf(out, in, e);
 
-	len = e-in+1;
+	len = e - in + 1;
 	sofar = 0;
-	*out = p = malloc(len+1);
+	*out = p = malloc(len + 1);
 	if(p == nil)
 		return 0;
 
@@ -1279,7 +1453,7 @@
 		close(fromtcs[1]); close(totcs[0]);
 		dup(open("/dev/null", OWRITE), 2);
 		exec("/bin/tcs", av);
-		_exits(0);
+		_exits("");
 	default:
 		close(fromtcs[1]); close(totcs[0]);
 		switch(rfork(RFPROC|RFFDG|RFNOWAIT)){
@@ -1289,17 +1463,17 @@
 		case 0:
 			close(fromtcs[0]);
 			while(in < e){
-				n = write(totcs[1], in, e-in);
+				n = write(totcs[1], in, e - in);
 				if(n <= 0)
 					break;
 				in += n;
 			}
 			close(totcs[1]);
-			_exits(0);
+			_exits("");
 		default:
 			close(totcs[1]);
 			for(;;){
-				n = read(fromtcs[0], &p[sofar], len-sofar);
+				n = read(fromtcs[0], &p[sofar], len - sofar);
 				if(n <= 0)
 					break;
 				sofar += n;
@@ -1306,7 +1480,7 @@
 				p[sofar] = 0;
 				if(sofar == len){
 					len += 1024;
-					p = realloc(p, len+1);
+					p = realloc(p, len + 1);
 					if(p == nil)
 						goto error;
 					*out = p;
@@ -1333,10 +1507,8 @@
 	void *p;
 
 	p = mallocz(n, 1);
-	if(!p){
-		fprint(2, "%s: out of memory alloc %lud\n", argv0, n);
-		exits("out of memory");
-	}
+	if(!p)
+		sysfatal("malloc %lud: %r", n);
 	setmalloctag(p, getcallerpc(&n));
 	return p;
 }
@@ -1347,53 +1519,57 @@
 	if(n == 0)
 		n = 1;
 	p = realloc(p, n);
-	if(!p){
-		fprint(2, "%s: out of memory realloc %lud\n", argv0, n);
-		exits("out of memory");
-	}
+	if(!p)
+		sysfatal("realloc %lud: %r", n);
 	setrealloctag(p, getcallerpc(&p));
 	return p;
 }
 
+int
+myplumbsend(int fd, Plumbmsg *m)
+{
+	char *buf;
+	int n;
+
+	buf = plumbpack(m, &n);
+	if(buf == nil)
+		return -1;
+	n = write(fd, buf, n);
+	free(buf);
+	return n;
+}
+
 void
 mailplumb(Mailbox *mb, Message *m, int delete)
 {
+	char buf[256], dbuf[SHA1dlen*2 + 1], len[10], date[30], *from, *subject;
+	int ai, cache;
 	Plumbmsg p;
 	Plumbattr a[7];
-	char buf[256];
-	int ai;
-	char lenstr[10], *from, *subject, *date;
 	static int fd = -1;
 
-	if(m->subject822 == nil)
+	cache = insurecache(mb, m) == 0;	/* living dangerously if deleted */
+	subject = m->subject;
+	if(subject == nil)
 		subject = "";
-	else
-		subject = s_to_c(m->subject822);
 
-	if(m->from822 != nil)
-		from = s_to_c(m->from822);
+	if(m->from != nil)
+		from = m->from;
 	else if(m->unixfrom != nil)
-		from = s_to_c(m->unixfrom);
+		from = m->unixfrom;
 	else
 		from = "";
 
-	if(m->unixdate != nil)
-		date = s_to_c(m->unixdate);
-	else
-		date = "";
-
-	sprint(lenstr, "%zd", m->end-m->start);
-
+	sprint(len, "%lud", m->size);
 	if(biffing && !delete)
-		print("[ %s / %s / %s ]\n", from, subject, lenstr);
-
+		fprint(2, "[ %s / %s / %s ]\n", from, subject, len);
 	if(!plumbing)
-		return;
+		goto out;
 
 	if(fd < 0)
 		fd = plumbopen("send", OWRITE);
 	if(fd < 0)
-		return;
+		goto out;
 
 	p.src = "mailfs";
 	p.dst = "seemail";
@@ -1409,102 +1585,123 @@
 	a[ai-1].next = &a[ai];
 
 	a[++ai].name = "length";
-	a[ai].value = lenstr;
+	a[ai].value = len;
 	a[ai-1].next = &a[ai];
 
 	a[++ai].name = "mailtype";
-	a[ai].value = delete?"delete":"new";
+	a[ai].value = delete? "delete": "new";
 	a[ai-1].next = &a[ai];
 
+	snprint(date, sizeof date, "%Δ", m->fileid);
 	a[++ai].name = "date";
 	a[ai].value = date;
 	a[ai-1].next = &a[ai];
 
-	if(m->sdigest){
+	if(m->digest){
+		snprint(dbuf, sizeof dbuf, "%A", m->digest);
 		a[++ai].name = "digest";
-		a[ai].value = s_to_c(m->sdigest);
+		a[ai].value = dbuf;
 		a[ai-1].next = &a[ai];
 	}
-
 	a[ai].next = nil;
-
 	p.attr = a;
-	snprint(buf, sizeof(buf), "%s/%s/%s",
+	snprint(buf, sizeof buf, "%s/%s/%s",
 		mntpt, mb->name, m->name);
 	p.ndata = strlen(buf);
 	p.data = buf;
 
-	plumbsend(fd, &p);
+	myplumbsend(fd, &p);
+out:
+	if(cache)
+		msgdecref(mb, m);
 }
 
-//
-// count the number of lines in the body (for imap4)
-//
-void
+/*
+ *  count the number of lines in the body (for imap4)
+ */
+ulong
 countlines(Message *m)
 {
-	int i;
 	char *p;
+	ulong i;
 
 	i = 0;
-	for(p = strchr(m->rbody, '\n'); p != nil && p < m->rbend; p = strchr(p+1, '\n'))
+	for(p = strchr(m->rbody, '\n'); p != nil && p < m->rbend; p = strchr(p + 1, '\n'))
 		i++;
-	sprint(m->lines, "%d", i);
+	return i;
 }
 
-char *LOG = "fs";
+static char *logf = "fs";
 
 void
-logmsg(char *s, Message *m)
+logmsg(Message *m, char *fmt, ...)
 {
-	int pid;
+	char buf[256], *p, *e;
+	va_list args;
 
-	if(!logging)
+	if(!lflag)
 		return;
-	pid = getpid();
-	if(m == nil)
-		syslog(0, LOG, "%s.%d: %s", user, pid, s);
-	else
-		syslog(0, LOG, "%s.%d: %s msg from %s digest %s",
-			user, pid, s,
-			m->from822 ? s_to_c(m->from822) : "?",
-			s_to_c(m->sdigest));
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s.%d: ", user, getpid());
+	if(m)
+		p = seprint(p, e, "from %s digest %A ",
+			m->from, m->digest);
+	va_start(args, fmt);
+	vseprint(p, e, fmt, args);
+	va_end(args);
+
+	if(Sflag)
+		fprint(2, "%s\n", buf);
+	syslog(Sflag, logf, "%s", buf);
 }
 
-/*
- *  squeeze nulls out of the body
- */
-static void
-nullsqueeze(Message *m)
+void
+iprint(char *fmt, ...)
 {
-	char *p, *q;
+	char buf[256], *p, *e;
+	va_list args;
 
-	q = memchr(m->body, 0, m->end-m->body);
-	if(q == nil)
+	if(!iflag)
 		return;
-
-	for(p = m->body; q < m->end; q++){
-		if(*q == 0)
-			continue;
-		*p++ = *q;
-	}
-	m->bend = m->rbend = m->end = p;
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s.%d: ", user, getpid());
+	va_start(args, fmt);
+	vseprint(p, e, fmt, args);
+	vfprint(2, fmt, args);
+	va_end(args);
+	syslog(Sflag, logf, "%s", buf);
 }
 
+void
+eprint(char *fmt, ...)
+{
+	char buf[256], buf2[256], *p, *e;
+	va_list args;
 
-//
-// convert an RFC822 date into a Unix style date
-// for when the Unix From line isn't there (e.g. POP3).
-// enough client programs depend on having a Unix date
-// that it's easiest to write this conversion code once, right here.
-//
-// people don't follow RFC822 particularly closely,
-// so we use strtotm, which is a bunch of heuristics.
-//
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s.%d: ", user, getpid());
+	va_start(args, fmt);
+	vseprint(p, e, fmt, args);
+	e = buf2 + sizeof buf2;
+	p = seprint(buf2, e, "upas/fs: ");
+	vseprint(p, e, fmt, args);
+	va_end(args);
+	syslog(Sflag, logf, "%s", buf);
+	fprint(2, "%s", buf2);
+}
 
-extern int strtotm(char*, Tm*);
-String*
-date822tounix(char *s)
+/*
+ *  convert an RFC822 date into a Unix style date
+ *  for when the Unix From line isn't there (e.g. POP3).
+ *  enough client programs depend on having a Unix date
+ *  that it's easiest to write this conversion code once, right here.
+ *
+ *  people don't follow RFC822 particularly closely,
+ *  so we use strtotm, which is a bunch of heuristics.
+ */
+
+char*
+date822tounix(Message *, char *s)
 {
 	char *p, *q;
 	Tm tm;
@@ -1515,6 +1712,5 @@
 	p = asctime(&tm);
 	if(q = strchr(p, '\n'))
 		*q = '\0';
-	return s_copy(p);
+	return strdup(p);
 }
-
--- /dev/null
+++ b/sys/src/cmd/upas/fs/mdir.c
@@ -1,0 +1,354 @@
+#include "common.h"
+#include "dat.h"
+
+typedef struct {
+	int	debug;
+} Mdir;
+
+#define	mdprint(mdir, ...)	if(mdir->debug) fprint(2, __VA_ARGS__)
+
+static int
+slurp(char *f, char *b, uvlong o, long l)
+{
+	int fd, r;
+
+	if((fd = open(f, OREAD)) == -1)
+		return -1;
+
+	seek(fd, o, 0);
+	r = readn(fd, b, l) != l;
+	close(fd);
+	return r? -1: 0;
+}
+
+static void
+parseunix(Message *m)
+{
+	char *s, *p;
+	int l;
+
+	l = m->header - m->start;
+	m->unixheader = smprint("%.*s", l, m->start);
+	s = m->start + 5;
+	if((p = strchr(s, ' ')) == nil)
+		abort();
+	*p = 0;
+	m->unixfrom = strdup(s);
+	*p = ' ';
+}
+
+static int
+mdirfetch(Mailbox *mb, Message *m, uvlong o, ulong l)
+{
+	char buf[Pathlen], *x;
+	Mdir *mdir;
+
+	mdir = mb->aux;
+	mdprint(mdir, "mdirfetch(%D) ...", m->fileid);
+
+	snprint(buf, sizeof buf, "%s/%D", mb->path, m->fileid);
+	if(slurp(buf, m->start + o, o, l) == -1){
+		logmsg(m, "fetch failed: %r");
+		mdprint(mdir, "%r\n");
+		return -1;
+	}
+	if(m->header == nil)
+		m->header = m->start;
+	if(m->header == m->start)
+	if(o + l >= 36)
+	if(strncmp(m->start, "From ", 5) == 0)
+	if(x = strchr(m->start, '\n')){
+		m->header = x + 1;
+		if(m->unixfrom == nil)
+			parseunix(m);
+	}
+	m->mheader = m->mhend = m->header;
+	mdprint(mdir, "fetched [%llud, %llud]\n", o, o + l);
+	return 0;
+}
+
+static int
+setsize(Mailbox *mb, Message *m)
+{
+	char buf[Pathlen];
+	Dir *d;
+
+	snprint(buf, sizeof buf, "%s/%D", mb->path, m->fileid);
+	if((d = dirstat(buf)) == nil)
+		return -1;
+	m->size = d->length;
+	free(d);
+	return 0;
+}
+
+/* must be [0-9]+(\..*)? */
+int
+dirskip(Dir *a, uvlong *uv)
+{
+	char *p;
+
+	if(a->length == 0)
+		return 1;
+	*uv = strtoul(a->name, &p, 0);
+	if(*uv < 1000000 || *p != '.')
+		return 1;
+	*uv = *uv<<8 | strtoul(p + 1, &p, 10);
+	if(*p)
+		return 1;
+	return 0;
+}
+
+static int
+vcmp(vlong a, vlong b)
+{
+	a -= b;
+	if(a > 0)
+		return 1;
+	if(a < 0)
+		return -1;
+	return 0;
+}
+
+static int
+dircmp(Dir *a, Dir *b)
+{
+	uvlong x, y;
+
+	dirskip(a, &x);
+	dirskip(b, &y);
+	return vcmp(x, y);
+}
+
+static char*
+mdirread(Mdir* mdir, Mailbox* mb, int doplumb, int *new)
+{
+	int i, nnew, ndel, fd, n, c;
+	uvlong uv;
+	Dir *d;
+	Message *m, **ll;
+	static char err[ERRMAX];
+
+	mdprint(mdir, "mdirread()\n");
+	if((fd = open(mb->path, OREAD)) == -1){
+		errstr(err, sizeof err);
+		return err;
+	}
+	if((d = dirfstat(fd)) == nil){
+		errstr(err, sizeof err);
+		close(fd);
+		return err;
+	}
+	*new = nnew = 0;
+	if(mb->d){
+		if(d->qid.path == mb->d->qid.path)
+		if(d->qid.vers == mb->d->qid.vers){
+			mdprint(mdir, "\tqids match\n");
+			close(fd);
+			free(d);
+			goto finished;
+		}
+		free(mb->d);
+	}
+	logmsg(nil, "reading %s (mdir)", mb->path);
+	mb->d = d;
+
+	n = dirreadall(fd, &d);
+	close(fd);
+	if(n == -1){
+		errstr(err, sizeof err);
+		return err;
+	}
+
+	qsort(d, n, sizeof *d, (int(*)(void*, void*))dircmp);
+	ndel = 0;
+	ll = &mb->root->part;
+	for(i = 0; *ll || i < n; ){
+		if(i < n && dirskip(d + i, &uv)){
+			i++;
+			continue;
+		}
+		c = -1;
+		if(i >= n)
+			c = 1;
+		else if(*ll)
+			c = vcmp(uv, (*ll)->fileid);
+		mdprint(mdir, "consider %s and %D -> %d\n", i<n? d[i].name: 0, *ll? (*ll)->fileid: 1ull, c);
+		if(c < 0){
+			/* new message */
+			mdprint(mdir, "new: %s (%D)\n", d[i].name, *ll? (*ll)->fileid: 0);
+			m = newmessage(mb->root);
+			m->fileid = uv;
+			if(setsize(mb, m) < 0 || m->size >= Maxmsg){
+				/* message disappeared?  unchain */
+				mdprint(mdir, "deleted → %r (%D)\n", m->fileid);
+				logmsg(m, "disappeared");
+				if(doplumb)
+					mailplumb(mb, m, 1); /* redundant */
+				unnewmessage(mb, mb->root, m);
+				/* we're out of sync; note this by dropping cached qid */
+				mb->d->qid.path = 0;
+				break;
+			}
+			m->inmbox = 1;
+			nnew++;
+			m->next = *ll;
+			*ll = m;
+			ll = &m->next;
+			logmsg(m, "new %s", d[i].name);
+			i++;
+			newcachehash(mb, m, doplumb);
+			putcache(mb, m);
+		}else if(c > 0){
+			/* deleted message; */
+			mdprint(mdir, "deleted: %s (%D)\n", i<n? d[i].name: 0, *ll? (*ll)->fileid: 0);
+			ndel++;
+			logmsg(*ll, "deleted (refs=%d)", *ll? (*ll)->refs: -42);
+			if(doplumb)
+				mailplumb(mb, *ll, 1);
+			(*ll)->inmbox = 0;
+			(*ll)->deleted = Disappear;
+			ll = &(*ll)->next;
+		}else{
+			//logmsg(*ll, "duplicate %s", d[i].name);
+			i++;
+			ll = &(*ll)->next;
+		}
+	}
+
+	free(d);
+	logmsg(nil, "mbox read: %d new %d deleted", nnew, ndel);
+finished:
+	*new = nnew;
+	return nil;
+}
+
+static void
+mdirdelete(Mailbox *mb, Message *m)
+{
+	char mpath[Pathlen];
+	Mdir* mdir;
+
+	mdir = mb->aux;
+	snprint(mpath, sizeof mpath, "%s/%D", mb->path, m->fileid);
+	mdprint(mdir, "remove: %s\n", mpath);
+	/* may have been removed by other fs.  just log the error. */
+	if(remove(mpath) == -1)
+		logmsg(m, "remove: %s: %r", mpath);
+	m->inmbox = 0;
+}
+
+static char*
+mdirsync(Mailbox* mb, int doplumb, int *new)
+{
+	Mdir *mdir;
+
+	mdir = mb->aux;
+	mdprint(mdir, "mdirsync()\n");
+	return mdirread(mdir, mb, doplumb, new);
+}
+
+static char*
+mdirctl(Mailbox *mb, int c, char **v)
+{
+	Mdir *mdir;
+
+	mdir = mb->aux;
+	if(c == 1 && strcmp(*v, "debug") == 0)
+		mdir->debug = 1;
+	else if(c == 1 && strcmp(*v, "nodebug") == 0)
+		mdir->debug = 0;
+	else
+		return "bad mdir control message";
+	return nil;
+}
+
+static void
+mdirclose(Mailbox *mb)
+{
+	free(mb->aux);
+}
+
+static int
+qidcmp(Qid *a, Qid *b)
+{
+	if(a->path != b->path)
+		return 1;
+	return a->vers - b->vers;
+}
+
+/*
+ * .idx strategy. we save the qid.path and .vers
+ * of the mdir directory and the date to the index.
+ * we accept the work done by the other upas/fs if
+ * the index is based on the same (or a newer)
+ * qid.  in any event, we recheck the directory after
+ * the directory is four hours old.
+ */
+static int
+idxr(char *s, Mailbox *mb)
+{
+	char *f[5];
+	long t, δt, n;
+	Dir d;
+
+	n = tokenize(s, f, nelem(f));
+	if(n != 4 || strcmp(f[0], "mdirv1") != 0)
+		return -1;
+	t = strtoul(f[1], 0, 0);
+	δt = time(0) - t;
+	if(δt < 0 || δt > 4*3600)
+		return 0;
+	memset(&d, 0, sizeof d);
+	d.qid.path = strtoull(f[2], 0, 0);
+	d.qid.vers = strtoull(f[3], 0, 0);
+	if(mb->d && qidcmp(&mb->d->qid, &d.qid) >= 0)
+		return 0;
+	if(mb->d == 0)
+		mb->d = emalloc(sizeof d);
+	mb->d->qid = d.qid;
+	mb->d->mtime = t;
+	return 0;
+}
+
+static void
+idxw(Biobuf *b, Mailbox *mb)
+{
+	Qid q;
+
+	memset(&q, 0, sizeof q);
+	if(mb->d)
+		q = mb->d->qid;
+	Bprint(b, "mdirv1 %lud %llud %lud\n", time(0), q.path, q.vers);
+}
+
+char*
+mdirmbox(Mailbox *mb, char *path)
+{
+	int m;
+	Dir *d;
+	Mdir *mdir;
+
+	d = dirstat(path);
+	if(!d && mb->flags & DMcreate){
+		createfolder(getuser(), path);
+		d = dirstat(path);
+	}
+	m = d && (d->mode & DMDIR);
+	free(d);
+	if(!m)
+		return Enotme;
+	snprint(mb->path, sizeof mb->path, "%s", path);
+	mdir = emalloc(sizeof *mdir);
+	mdir->debug = 0;
+	mb->aux = mdir;
+	mb->sync = mdirsync;
+	mb->close = mdirclose;
+	mb->fetch = mdirfetch;
+	mb->delete = mdirdelete;
+	mb->remove = localremove;
+	mb->rename = localrename;
+	mb->idxread = idxr;
+	mb->idxwrite = idxw;
+	mb->ctl = mdirctl;
+	return nil;
+}
--- a/sys/src/cmd/upas/fs/mkfile
+++ b/sys/src/cmd/upas/fs/mkfile
@@ -1,14 +1,24 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=	fs\
 
 OFILES=\
+	bos.$O\
+	cache.$O\
 	fs.$O\
-	imap4.$O\
+	header.$O\
+	idx.$O\
+	imap.$O\
 	mbox.$O\
+	mdir.$O\
+	mtree.$O\
 	plan9.$O\
 	planb.$O\
 	pop3.$O\
+	ref.$O\
+	remove.$O\
+	rename.$O\
 	strtotm.$O\
 
 LIB=../common/libcommon.a$O\
@@ -16,8 +26,6 @@
 HFILES= ../common/common.h\
 	dat.h
 
-BIN=/$objtype/bin/upas
-
 UPDATE=\
 	mkfile\
 	$HFILES\
@@ -25,4 +33,10 @@
 	${OFILES:%.$O=%.c}\
 
 </sys/src/cmd/mkone
-CFLAGS=$CFLAGS -I/sys/include -I../common
+CFLAGS=$CFLAGS  -I../common
+
+acid:V:
+	$CC -a $CFLAGS fs.c>a$O
+
+chkidx: mtree.$O chkidx.$O
+	$LD $LDFLAGS -o $target $prereq
--- /dev/null
+++ b/sys/src/cmd/upas/fs/mtree.c
@@ -1,0 +1,80 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+int
+mtreecmp(Avl *va, Avl *vb)
+{
+	Mtree *a, *b;
+
+	a = (Mtree*)va;
+	b = (Mtree*)vb;
+	return memcmp(a->m->digest, b->m->digest, SHA1dlen);
+}
+
+int
+mtreeisdup(Mailbox *mb, Message *m)
+{
+	Mtree t;
+
+	assert(Topmsg(mb, m) && m->digest);
+	if(!m->digest)
+		return 0;
+	memset(&t, 0, sizeof t);
+	t.m = m;
+	if(avllookup(mb->mtree, &t))
+		return 1;
+	return 0;
+}
+
+Message*
+mtreefind(Mailbox *mb, uchar *digest)
+{
+	Message m0;
+	Mtree t, *p;
+
+	m0.digest = digest;
+	memset(&t, 0, sizeof t);
+	t.m = &m0;
+	if(p = (Mtree*)avllookup(mb->mtree, &t))
+		return p->m;
+	return nil;
+}
+
+void
+mtreeadd(Mailbox *mb, Message *m)
+{
+	Avl *old;
+	Mtree *p;
+
+	assert(Topmsg(mb, m) && m->digest);
+	p = emalloc(sizeof *p);
+	p->m = m;
+	old = avlinsert(mb->mtree, p);
+	assert(old == 0);
+}
+
+void
+mtreedelete(Mailbox *mb, Message *m)
+{
+	Mtree t, *p;
+
+	assert(Topmsg(mb, m));
+	memset(&t, 0, sizeof t);
+	t.m = m;
+	if(m->deleted & ~Deleted){
+		if(m->digest == nil)
+			return;
+		p = (Mtree*)avllookup(mb->mtree, &t);
+		if(p == nil || p->m != m)
+			return;
+		p = (Mtree*)avldelete(mb->mtree, &t);
+		free(p);
+		return;
+	}
+	assert(m->digest);
+	p = (Mtree*)avldelete(mb->mtree, &t);
+	if(p == nil)
+		_assert("mtree delete fails");
+	free(p);
+}
--- a/sys/src/cmd/upas/fs/plan9.c
+++ b/sys/src/cmd/upas/fs/plan9.c
@@ -1,144 +1,181 @@
 #include "common.h"
-#include <ctype.h>
-#include <plumb.h>
 #include <libsec.h>
 #include "dat.h"
 
-enum {
-	Buffersize = 64*1024,
-};
+typedef struct {
+	Biobuf	*in;
+	char	*shift;
+} Inbuf;
 
-typedef struct Inbuf Inbuf;
-struct Inbuf
+/*
+ *  parse a Unix style header
+ */
+static int
+memtotm(char *p, int n, Tm *t)
 {
-	int	fd;
-	uchar	*lim;
-	uchar	*rptr;
-	uchar	*wptr;
-	uchar	data[Buffersize+7];
-};
+	char buf[128];
 
+	if(n > sizeof buf - 1)
+		n = sizeof buf -1;
+	memcpy(buf, p, n);
+	buf[n] = 0;
+	return strtotm(buf, t);
+}
+
+static int
+chkunix0(char *s, int n)
+{
+	char *p;
+	Tm tm;
+
+	if(n > 256)
+		return -1;
+	if((p = memchr(s, ' ', n)) == nil)
+		return -1;
+	if(memtotm(p, n - (p - s), &tm) < 0)
+		return -1;
+	if(tm2sec(&tm) < 1000000)
+		return -1;
+	return 0;
+}
+
+static int
+chkunix(char *s, int n)
+{
+	int r;
+
+	r = chkunix0(s, n);
+	if(r == -1)
+		eprint("plan9: warning naked from [%.*s]\n", n, s);
+	return r;
+}
+
+static char*
+parseunix(Message *m)
+{
+	char *s, *p, *q;
+	int l;
+	Tm tm;
+
+	l = m->header - m->start;
+	m->unixheader = smprint("%.*s", l, m->start);
+	s = m->start + 5;
+	if((p = strchr(s, ' ')) == nil)
+		return s;
+	*p = 0;
+	m->unixfrom = strdup(s);
+	*p++ = ' ';
+	if(q = strchr(p, '\n'))
+		*q = 0;
+	if(strtotm(p, &tm) < 0)
+		return p;
+	if(q)
+		*q = '\n';
+	m->fileid = (uvlong)tm2sec(&tm) << 8;
+	return 0;
+}
+
 static void
-addtomessage(Message *m, uchar *p, int n, int done)
+addtomessage(Message *m, char *p, int n)
 {
 	int i, len;
 
-	// add to message (+1 in malloc is for a trailing NUL)
+	if(n == 0)
+		return;
+	/* add to message (+1 in malloc is for a trailing NUL) */
 	if(m->lim - m->end < n){
 		if(m->start != nil){
-			i = m->end-m->start;
-			if(done)
-				len = i + n;
-			else
-				len = (4*(i+n))/3;
+			i = m->end - m->start;
+			len = (4*(i + n))/3;
 			m->start = erealloc(m->start, len + 1);
 			m->end = m->start + i;
 		} else {
-			if(done)
-				len = n;
-			else
-				len = 2*n;
+			len = 2*n;
 			m->start = emalloc(len + 1);
 			m->end = m->start;
 		}
 		m->lim = m->start + len;
-		*m->lim = '\0';
+		*m->lim = 0;
 	}
 
 	memmove(m->end, p, n);
 	m->end += n;
-	*m->end = '\0';
+	*m->end = 0;
 }
 
-//
-//  read in a single message
-//
+/*
+ *   read in a single message
+ */
 static int
-readmessage(Message *m, Inbuf *inb)
+okmsg(Mailbox *mb, Message *m, Inbuf *b)
 {
-	int i, n, done;
-	uchar *p, *np;
-	char sdigest[SHA1dlen*2+1];
-	char tmp[64];
+	char e[ERRMAX], buf[128];
 
-	for(done = 0; !done;){
-		n = inb->wptr - inb->rptr;
-		if(n < 6){
-			if(n)
-				memmove(inb->data, inb->rptr, n);
-			inb->rptr = inb->data;
-			inb->wptr = inb->rptr + n;
-			i = read(inb->fd, inb->wptr, Buffersize);
-			if(i < 0){
-				if(fd2path(inb->fd, tmp, sizeof tmp) < 0)
-					strcpy(tmp, "unknown mailbox");
-				fprint(2, "error reading '%s': %r\n", tmp);
-				return -1;
-			}
-			if(i == 0){
-				if(n != 0)
-					addtomessage(m, inb->rptr, n, 1);
-				if(m->end == m->start)
-					return -1;
-				break;
-			}
-			inb->wptr += i;
-		}
-
-		// look for end of message
-		for(p = inb->rptr; p < inb->wptr; p = np+1){
-			// first part of search for '\nFrom '
-			np = memchr(p, '\n', inb->wptr - p);
-			if(np == nil){
-				p = inb->wptr;
-				break;
-			}
-
-			/*
-			 *  if we've found a \n but there's
-			 *  not enough room for '\nFrom ', don't do
-			 *  the comparison till we've read in more.
-			 */
-			if(inb->wptr - np < 6){
-				p = np;
-				break;
-			}
-
-			if(strncmp((char*)np, "\nFrom ", 6) == 0){
-				done = 1;
-				p = np+1;
-				break;
-			}
-		}
-
-		// add to message (+ 1 in malloc is for a trailing null)
-		n = p - inb->rptr;
-		addtomessage(m, inb->rptr, n, done);
-		inb->rptr += n;
+	rerrstr(e, sizeof e);
+	if(strlen(e)){
+		if(fd2path(Bfildes(b->in), buf, sizeof buf) < 0)
+			strcpy(buf, "unknown mailbox");
+		eprint("plan9: error reading %s: %r\n", buf);
+		return -1;
 	}
-
-	// if it doesn't start with a 'From ', this ain't a mailbox
-	if(strncmp(m->start, "From ", 5) != 0)
+	if(m->end == m->start)
 		return -1;
-
-	// dump trailing newline, make sure there's a trailing null
-	// (helps in body searches)
-	if(*(m->end-1) == '\n')
+	if(m->end[-1] == '\n')
 		m->end--;
 	*m->end = 0;
+	m->size = m->end - m->start;
+	if(m->size >= Maxmsg)
+		return -1;
 	m->bend = m->rbend = m->end;
+	if(m->digest == 0)
+		digestmessage(mb, m);
+	return 0;
+}
 
-	// digest message
-	sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
-	for(i = 0; i < SHA1dlen; i++)
-		sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
-	m->sdigest = s_copy(sdigest);
+static char*
+inbread(Inbuf *b)
+{
+	if(b->shift)
+		return b->shift;
+	return b->shift = Brdline(b->in, '\n');
+}
 
-	return 0;
+void
+inbconsume(Inbuf *b)
+{
+	b->shift = 0;
 }
 
+/*
+ * bug: very long line with From at the buffer break.
+ */
+static int
+readmessage(Mailbox *mb, Message *m, Inbuf *b)
+{
+	char *s, *n;
+	long l, state;
 
-// throw out deleted messages.  return number of freshly deleted messages
+	werrstr("");
+	state = 0;
+	for(;;){
+		s = inbread(b);
+		if(s == 0)
+			break;
+		n = s + (l = Blinelen(b->in)) - 1;
+		if(l >= 28 + 7 && n[0] == '\n')
+		if(strncmp(s, "From ", 5) == 0)
+		if(!chkunix(s + 5, l - 5))
+		if(++state == 2)
+			break;
+		if(state == 0)
+			return -1;
+		addtomessage(m, s, l);
+		inbconsume(b);
+	}
+	return okmsg(mb, m, b);
+}
+
+/* throw out deleted messages.  return number of freshly deleted messages */
 int
 purgedeleted(Mailbox *mb)
 {
@@ -145,7 +182,7 @@
 	Message *m, *next;
 	int newdels;
 
-	// forget about what's no longer in the mailbox
+	/* forget about what's no longer in the mailbox */
 	newdels = 0;
 	for(m = mb->root->part; m != nil; m = next){
 		next = m->next;
@@ -158,19 +195,37 @@
 	return newdels;
 }
 
-//
-//  read in the mailbox and parse into messages.
-//
+static void
+mergemsg(Message *m, Message *x)
+{
+	assert(m->start == 0);
+	m->mallocd = 1;
+	m->inmbox = 1;
+	m->lim = x->lim;
+	m->start = x->start;
+	m->end = x->end;
+	m->bend = x->bend;
+	m->rbend = x->rbend;
+	x->lim = 0;
+	x->start = 0;
+	x->end = 0;
+	x->bend = 0;
+	x->rbend = 0;
+}
+
+/*
+ *   read in the mailbox and parse into messages.
+ */
 static char*
-_readmbox(Mailbox *mb, int doplumb, Mlock *lk)
+readmbox(Mailbox *mb, int doplumb, int *new, Mlock *lk)
 {
-	int fd, n;
-	String *tmp;
+	char *p, *x, buf[Pathlen];
+	int nnew;
+	Biobuf *in;
 	Dir *d;
-	static char err[Errlen];
+	Inbuf b;
 	Message *m, **l;
-	Inbuf *inb;
-	char *x;
+	static char err[ERRMAX];
 
 	l = &mb->root->part;
 
@@ -177,25 +232,14 @@
 	/*
 	 *  open the mailbox.  If it doesn't exist, try the temporary one.
 	 */
-	n = 0;
 retry:
-	fd = open(mb->path, OREAD);
-	if(fd < 0){
-		rerrstr(err, sizeof(err));
-		if(strstr(err, "locked") != nil
-		|| strstr(err, "exclusive lock") != nil)
-			if(n++ < 20){
-				sleep(500);	/* wait for lock to go away */
+	in = Bopen(mb->path, OREAD);
+	if(in == nil){
+		errstr(err, sizeof(err));
+		if(strstr(err, "exist") != 0){
+			snprint(buf, sizeof buf, "%s.tmp", mb->path);
+			if(sysrename(buf, mb->path) == 0)
 				goto retry;
-			}
-		if(strstr(err, "exist") != nil){
-			tmp = s_copy(mb->path);
-			s_append(tmp, ".tmp");
-			if(sysrename(s_to_c(tmp), mb->path) == 0){
-				s_free(tmp);
-				goto retry;
-			}
-			s_free(tmp);
 		}
 		return err;
 	}
@@ -204,15 +248,16 @@
 	 *  a new qid.path means reread the mailbox, while
 	 *  a new qid.vers means read any new messages
 	 */
-	d = dirfstat(fd);
+	d = dirfstat(Bfildes(in));
 	if(d == nil){
-		close(fd);
-		errstr(err, sizeof(err));
+		Bterm(in);
+		errstr(err, sizeof err);
 		return err;
 	}
 	if(mb->d != nil){
 		if(d->qid.path == mb->d->qid.path && d->qid.vers == mb->d->qid.vers){
-			close(fd);
+			*new = 0;
+			Bterm(in);
 			free(d);
 			return nil;
 		}
@@ -219,22 +264,19 @@
 		if(d->qid.path == mb->d->qid.path){
 			while(*l != nil)
 				l = &(*l)->next;
-			seek(fd, mb->d->length, 0);
+			Bseek(in, mb->d->length, 0);
 		}
 		free(mb->d);
 	}
 	mb->d = d;
-	mb->vers++;
-	henter(PATH(0, Qtop), mb->name,
-		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
 
-	inb = emalloc(sizeof(Inbuf));
-	inb->rptr = inb->wptr = inb->data;
-	inb->fd = fd;
+	memset(&b, 0, sizeof b);
+	b.in = in;
+	b.shift = 0;
 
-	//  read new messages
-	snprint(err, sizeof err, "reading '%s'", mb->path);
-	logmsg(err, nil);
+	/*  read new messages */
+	logmsg(nil, "reading %s", mb->path);
+	nnew = 0;
 	for(;;){
 		if(lk != nil)
 			syslockrefresh(lk);
@@ -241,98 +283,99 @@
 		m = newmessage(mb->root);
 		m->mallocd = 1;
 		m->inmbox = 1;
-		if(readmessage(m, inb) < 0){
-			delmessage(mb, m);
-			mb->root->subname--;
+		if(readmessage(mb, m, &b) < 0){
+			unnewmessage(mb, mb->root, m);
 			break;
 		}
-
-		// merge mailbox versions
+		/* merge mailbox versions */
 		while(*l != nil){
 			if(memcmp((*l)->digest, m->digest, SHA1dlen) == 0){
-				// matches mail we already read, discard
-				logmsg("duplicate", *l);
-				delmessage(mb, m);
-				mb->root->subname--;
-				m = nil;
-				l = &(*l)->next;
+				if((*l)->start == nil){
+					logmsg(*l, "read indexed");
+					mergemsg(*l, m);
+					unnewmessage(mb, mb->root, m);
+					m = *l;
+				}else{
+					logmsg(*l, "duplicate");
+					m->inmbox = 1;		/* remove it */
+					unnewmessage(mb, mb->root, m);
+					m = nil;
+					l = &(*l)->next;
+				}
 				break;
 			} else {
-				// old mail no longer in box, mark deleted
-				logmsg("disappeared", *l);
+				/* old mail no longer in box, mark deleted */
+				logmsg(*l, "disappeared");
 				if(doplumb)
 					mailplumb(mb, *l, 1);
 				(*l)->inmbox = 0;
-				(*l)->deleted = 1;
+				(*l)->deleted = Disappear;
 				l = &(*l)->next;
 			}
 		}
 		if(m == nil)
 			continue;
-
-		x = strchr(m->start, '\n');
-		if(x == nil)
-			m->header = m->end;
-		else
+		m->header = m->end;
+		if(x = strchr(m->start, '\n'))
 			m->header = x + 1;
+		if(p = parseunix(m))
+			sysfatal("%s:%lld naked From in body? [%s]", mb->path, seek(Bfildes(in), 0, 1), p);
 		m->mheader = m->mhend = m->header;
-		parseunix(m);
-		parse(m, 0, mb, 0);
-		logmsg("new", m);
-
+		parse(mb, m, 0, 0);
+		if(m != *l && m->deleted != Dup){
+			logmsg(m, "new");
+			newcachehash(mb, m, doplumb);
+			putcache(mb, m);
+			nnew++;
+		}
 		/* chain in */
 		*l = m;
 		l = &m->next;
-		if(doplumb)
-			mailplumb(mb, m, 0);
-
 	}
-	logmsg("mbox read", nil);
+	logmsg(nil, "mbox read");
 
-	// whatever is left has been removed from the mbox, mark deleted
+	/* whatever is left has been removed from the mbox, mark deleted */
 	while(*l != nil){
 		if(doplumb)
 			mailplumb(mb, *l, 1);
 		(*l)->inmbox = 0;
-		(*l)->deleted = 1;
+		(*l)->deleted = Deleted;
 		l = &(*l)->next;
 	}
 
-	close(fd);
-	free(inb);
+	Bterm(in);
+	*new = nnew;
 	return nil;
 }
 
 static void
-_writembox(Mailbox *mb, Mlock *lk)
+writembox(Mailbox *mb, Mlock *lk)
 {
-	Dir *d;
-	Message *m;
-	String *tmp;
+	char buf[Pathlen];
 	int mode, errs;
 	Biobuf *b;
+	Dir *d;
+	Message *m;
 
-	tmp = s_copy(mb->path);
-	s_append(tmp, ".tmp");
+	snprint(buf, sizeof buf, "%s.tmp", mb->path);
 
 	/*
 	 * preserve old files permissions, if possible
 	 */
-	d = dirstat(mb->path);
-	if(d != nil){
-		mode = d->mode&0777;
+	mode = Mboxmode;
+	if(d = dirstat(mb->path)){
+		mode = d->mode & 0777;
 		free(d);
-	} else
-		mode = MBOXMODE;
+	}
 
-	sysremove(s_to_c(tmp));
-	b = sysopen(s_to_c(tmp), "alc", mode);
+	remove(buf);
+	b = sysopen(buf, "alc", mode);
 	if(b == 0){
-		fprint(2, "can't write temporary mailbox %s: %r\n", s_to_c(tmp));
+		eprint("plan9: can't write temporary mailbox %s: %r\n", buf);
 		return;
 	}
 
-	logmsg("writing new mbox", nil);
+	logmsg(nil, "writing new mbox");
 	errs = 0;
 	for(m = mb->root->part; m != nil; m = m->next){
 		if(lk != nil)
@@ -339,28 +382,26 @@
 			syslockrefresh(lk);
 		if(m->deleted)
 			continue;
-		logmsg("writing", m);
+		logmsg(m, "writing");
 		if(Bwrite(b, m->start, m->end - m->start) < 0)
 			errs = 1;
 		if(Bwrite(b, "\n", 1) < 0)
 			errs = 1;
 	}
-	logmsg("wrote new mbox", nil);
+	logmsg(nil, "wrote new mbox");
 
 	if(sysclose(b) < 0)
 		errs = 1;
 
 	if(errs){
-		fprint(2, "error writing temporary mail file\n");
-		s_free(tmp);
+		eprint("plan9: error writing temporary mail file\n");
 		return;
 	}
 
-	sysremove(mb->path);
-	if(sysrename(s_to_c(tmp), mb->path) < 0)
-		fprint(2, "%s: can't rename %s to %s: %r\n", argv0,
-			s_to_c(tmp), mb->path);
-	s_free(tmp);
+	remove(mb->path);
+	if(sysrename(buf, mb->path) < 0)
+		eprint("plan9: can't rename %s to %s: %r\n",
+			buf, mb->path);
 	if(mb->d != nil)
 		free(mb->d);
 	mb->d = dirstat(mb->path);
@@ -367,10 +408,10 @@
 }
 
 char*
-plan9syncmbox(Mailbox *mb, int doplumb)
+plan9syncmbox(Mailbox *mb, int doplumb, int *new)
 {
-	Mlock *lk;
 	char *rv;
+	Mlock *lk;
 
 	lk = nil;
 	if(mb->dolock){
@@ -379,9 +420,9 @@
 			return "can't lock mailbox";
 	}
 
-	rv = _readmbox(mb, doplumb, lk);		/* interpolate */
+	rv = readmbox(mb, doplumb, new, lk);		/* interpolate */
 	if(purgedeleted(mb) > 0)
-		_writembox(mb, lk);
+		writembox(mb, lk);
 
 	if(lk != nil)
 		sysunlock(lk);
@@ -389,26 +430,30 @@
 	return rv;
 }
 
-//
-//  look to see if we can open this mail box
-//
+void
+plan9decache(Mailbox*, Message *m)
+{
+	m->lim = 0;
+}
+
+/*
+ *   look to see if we can open this mail box
+ */
 char*
 plan9mbox(Mailbox *mb, char *path)
 {
-	static char err[Errlen];
-	String *tmp;
+	char buf[Pathlen];
+	static char err[Pathlen];
 
 	if(access(path, AEXIST) < 0){
-		errstr(err, sizeof(err));
-		tmp = s_copy(path);
-		s_append(tmp, ".tmp");
-		if(access(s_to_c(tmp), AEXIST) < 0){
-			s_free(tmp);
+		errstr(err, sizeof err);
+		snprint(buf, sizeof buf, "%s.tmp", path);
+		if(access(buf, AEXIST) < 0)
 			return err;
-		}
-		s_free(tmp);
 	}
-
 	mb->sync = plan9syncmbox;
+	mb->remove = localremove;
+	mb->rename = localrename;
+	mb->decache = plan9decache;
 	return nil;
 }
--- a/sys/src/cmd/upas/fs/planb.c
+++ b/sys/src/cmd/upas/fs/planb.c
@@ -15,12 +15,37 @@
 #include <libsec.h>
 #include "dat.h"
 
+static char*
+parseunix(Message *m)
+{
+	char *s, *p, *q;
+	int l;
+	Tm tm;
+
+	l = m->header - m->start;
+	m->unixheader = smprint("%.*s", l, m->start);
+	s = m->start + 5;
+	if((p = strchr(s, ' ')) == nil)
+		return s;
+	*p = 0;
+	m->unixfrom = strdup(s);
+	*p++ = ' ';
+	if(q = strchr(p, '\n'))
+		*q = 0;
+	if(strtotm(p, &tm) < 0)
+		return p;
+	if(q)
+		*q = '\n';
+	m->fileid = (uvlong)tm2sec(&tm) << 8;
+	return 0;
+}
+
 static int
 readmessage(Message *m, char *msg)
 {
-	int fd, i, n;
+	int fd, n;
 	char *buf, *name, *p;
-	char hdr[128], sdigest[SHA1dlen*2+1];
+	char hdr[128];
 	Dir *d;
 
 	buf = nil;
@@ -29,8 +54,10 @@
 	if(name == nil)
 		return -1;
 	if(m->filename != nil)
-		s_free(m->filename);
-	m->filename = s_copy(name);
+		free(m->filename);
+	m->filename = strdup(name);
+	if(m->filename == nil)
+		sysfatal("malloc: %r");
 	fd = open(name, OREAD);
 	if(fd < 0)
 		goto Fail;
@@ -75,10 +102,7 @@
 		m->end--;
 	*m->end = 0;
 	m->bend = m->rbend = m->end;
-	sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
-	for(i = 0; i < SHA1dlen; i++)
-		sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
-	m->sdigest = s_copy(sdigest);
+
 	return 0;
 Fail:
 	if(fd >= 0)
@@ -98,7 +122,7 @@
 	char *dir, *p, *nname;
 	Dir d;
 
-	dir = strdup(s_to_c(m->filename));
+	dir = strdup(m->filename);
 	nname = nil;
 	if(dir == nil)
 		return;
@@ -162,21 +186,20 @@
 }
 
 static int
-readpbmessage(Mailbox *mb, char *msg, int doplumb)
+readpbmessage(Mailbox *mb, char *msg, int doplumb, int *nnew)
 {
 	Message *m, **l;
-	char *x;
+	char *x, *p;
 
 	m = newmessage(mb->root);
 	m->mallocd = 1;
 	m->inmbox = 1;
 	if(readmessage(m, msg) < 0){
-		delmessage(mb, m);
-		mb->root->subname--;
+		unnewmessage(mb, mb->root, m);
 		return -1;
 	}
 	for(l = &mb->root->part; *l != nil; l = &(*l)->next)
-		if(strcmp(s_to_c((*l)->filename), s_to_c(m->filename)) == 0 &&
+		if(strcmp((*l)->filename, m->filename) == 0 &&
 		    *l != m){
 			if((*l)->deleted < 0)
 				(*l)->deleted = 0;
@@ -184,15 +207,19 @@
 			mb->root->subname--;
 			return -1;
 		}
-	x = strchr(m->start, '\n');
-	if(x == nil)
-		m->header = m->end;
-	else
+	m->header = m->end;
+	if(x = strchr(m->start, '\n'))
 		m->header = x + 1;
+	if(p = parseunix(m))
+		sysfatal("%s:%s naked From in body? [%s]", mb->path, (*l)->filename, p);
 	m->mheader = m->mhend = m->header;
-	parseunix(m);
-	parse(m, 0, mb, 0);
-	logmsg("new", m);
+	parse(mb, m, 0, 0);
+	if(m != *l && m->deleted != Dup){
+		logmsg(m, "new");
+		newcachehash(mb, m, doplumb);
+		putcache(mb, m);
+		nnew[0]++;
+	}
 
 	/* chain in */
 	*l = m;
@@ -215,17 +242,18 @@
 	return strcmp(an, bn);
 }
 
-static void
-readpbmbox(Mailbox *mb, int doplumb)
+static char*
+readpbmbox(Mailbox *mb, int doplumb, int *new)
 {
-	int fd, i, j, nd, nmd;
 	char *month, *msg;
+	int fd, i, j, nd, nmd;
 	Dir *d, *md;
+	static char err[ERRMAX];
 
 	fd = open(mb->path, OREAD);
 	if(fd < 0){
-		fprint(2, "%s: %s: %r\n", argv0, mb->path);
-		return;
+		errstr(err, sizeof err);
+		return err;
 	}
 	nd = dirreadall(fd, &d);
 	close(fd);
@@ -249,7 +277,7 @@
 			for(j = 0; j < nmd; j++)
 				if(mustshow(md[j].name)){
 					msg = smprint("%s/%s", month, md[j].name);
-					readpbmessage(mb, msg, doplumb);
+					readpbmessage(mb, msg, doplumb, new);
 					free(msg);
 				}
 		}
@@ -259,45 +287,45 @@
 		md = nil;
 	}
 	free(d);
+	return nil;
 }
 
-static void
-readpbvmbox(Mailbox *mb, int doplumb)
+static char*
+readpbvmbox(Mailbox *mb, int doplumb, int *new)
 {
+	char *data, *ln, *p, *nln, *msg;
 	int fd, nr;
 	long sz;
-	char *data, *ln, *p, *nln, *msg;
 	Dir *d;
+	static char err[ERRMAX];
 
 	fd = open(mb->path, OREAD);
 	if(fd < 0){
-		fprint(2, "%s: %s: %r\n", argv0, mb->path);
-		return;
+		errstr(err, sizeof err);
+		return err;
 	}
 	d = dirfstat(fd);
 	if(d == nil){
-		fprint(2, "%s: %s: %r\n", argv0, mb->path);
-		close(fd);
-		return;
+		errstr(err, sizeof err);
+		return err;
 	}
 	sz = d->length;
 	free(d);
 	if(sz > 2 * 1024 * 1024){
 		sz = 2 * 1024 * 1024;
-		fprint(2, "%s: %s: bug: folder too big\n", argv0, mb->path);
+		fprint(2, "upas/fs: %s: bug: folder too big\n", mb->path);
 	}
 	data = malloc(sz+1);
 	if(data == nil){
-		close(fd);
-		fprint(2, "%s: no memory\n", argv0);
-		return;
+		errstr(err, sizeof err);
+		return err;
 	}
 	nr = readn(fd, data, sz);
 	close(fd);
 	if(nr < 0){
-		fprint(2, "%s: %s: %r\n", argv0, mb->path);
+		errstr(err, sizeof err);
 		free(data);
-		return;
+		return err;
 	}
 	data[nr] = 0;
 
@@ -318,22 +346,24 @@
 			*p = 0;
 		msg = smprint("/mail/box/%s/msgs/%s", user, ln);
 		if(msg == nil){
-			fprint(2, "%s: no memory\n", argv0);
+			fprint(2, "upas/fs: malloc: %r\n");
 			continue;
 		}
-		readpbmessage(mb, msg, doplumb);
+		readpbmessage(mb, msg, doplumb, new);
 		free(msg);
 	}
 	free(data);
+	return nil;
 }
 
 static char*
-readmbox(Mailbox *mb, int doplumb, int virt)
+readmbox(Mailbox *mb, int doplumb, int virt, int *new)
 {
+	char *mberr;
 	int fd;
 	Dir *d;
 	Message *m;
-	static char err[Errlen];
+	static char err[128];
 
 	if(debug)
 		fprint(2, "read mbox %s\n", mb->path);
@@ -364,15 +394,15 @@
 	henter(PATH(0, Qtop), mb->name,
 		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
 	snprint(err, sizeof err, "reading '%s'", mb->path);
-	logmsg(err, nil);
+	logmsg(nil, err, nil);
 
 	for(m = mb->root->part; m != nil; m = m->next)
 		if(m->deleted == 0)
 			m->deleted = -1;
 	if(virt == 0)
-		readpbmbox(mb, doplumb);
+		mberr = readpbmbox(mb, doplumb, new);
 	else
-		readpbvmbox(mb, doplumb);
+		mberr = readpbvmbox(mb, doplumb, new);
 
 	/*
 	 * messages removed from the mbox; flag them to go.
@@ -379,30 +409,30 @@
 	 */
 	for(m = mb->root->part; m != nil; m = m->next)
 		if(m->deleted < 0 && doplumb){
-			m->inmbox = 0;
-			m->deleted = 1;
-			mailplumb(mb, m, 1);
+			delmessage(mb, m);
+			if(doplumb)
+				mailplumb(mb, m, 1);
 		}
-	logmsg("mbox read", nil);
-	return nil;
+	logmsg(nil, "mbox read");
+	return mberr;
 }
 
 static char*
-mbsync(Mailbox *mb, int doplumb)
+mbsync(Mailbox *mb, int doplumb, int *new)
 {
 	char *rv;
 
-	rv = readmbox(mb, doplumb, 0);
+	rv = readmbox(mb, doplumb, 0, new);
 	purgembox(mb, 0);
 	return rv;
 }
 
 static char*
-mbvsync(Mailbox *mb, int doplumb)
+mbvsync(Mailbox *mb, int doplumb, int *new)
 {
 	char *rv;
 
-	rv = readmbox(mb, doplumb, 1);
+	rv = readmbox(mb, doplumb, 1, new);
 	purgembox(mb, 1);
 	return rv;
 }
--- a/sys/src/cmd/upas/fs/pop3.c
+++ b/sys/src/cmd/upas/fs/pop3.c
@@ -1,6 +1,4 @@
 #include "common.h"
-#include <ctype.h>
-#include <plumb.h>
 #include <libsec.h>
 #include <auth.h>
 #include "dat.h"
@@ -7,37 +5,49 @@
 
 #pragma varargck type "M" uchar*
 #pragma varargck argpos pop3cmd 2
+#define pdprint(p, ...)	if((p)->debug) fprint(2, __VA_ARGS__); else{}
 
+typedef struct Popm Popm;
+struct Popm{
+	int	mesgno;
+};
+
 typedef struct Pop Pop;
 struct Pop {
-	char *freep;	// free this to free the strings below
+	char	*freep;		/* free this to free the strings below */
+	char	*host;
+	char	*user;
+	char	*port;
 
-	char *host;
-	char *user;
-	char *port;
+	int	ppop;
+	int	refreshtime;
+	int	debug;
+	int	pipeline;
+	int	encrypted;
+	int	needtls;
+	int	notls;
+	int	needssl;
 
-	int ppop;
-	int refreshtime;
-	int debug;
-	int pipeline;
-	int encrypted;
-	int needtls;
-	int notls;
-	int needssl;
-
-	// open network connection
-	Biobuf bin;
-	Biobuf bout;
-	int fd;
-	char *lastline;	// from Brdstr
-
+	Biobuf	bin;		/* open network connection */
+	Biobuf	bout;
+	int	fd;
+	char	*lastline;		/* from Brdstr */
 	Thumbprint *thumb;
 };
 
-char*
+static int
+mesgno(Message *m)
+{
+	Popm *a;
+
+	a = m->aux;
+	return a->mesgno;
+}
+
+static char*
 geterrstr(void)
 {
-	static char err[Errlen];
+	static char err[64];
 
 	err[0] = '\0';
 	errstr(err, sizeof(err));
@@ -44,11 +54,11 @@
 	return err;
 }
 
-//
-// get pop3 response line , without worrying
-// about multiline responses; the clients
-// will deal with that.
-//
+/*
+ *  get pop3 response line , without worrying
+ *  about multiline responses; the clients
+ *  will deal with that.
+ */
 static int
 isokay(char *s)
 {
@@ -62,15 +72,13 @@
 	va_list va;
 
 	va_start(va, fmt);
-	vseprint(buf, buf+sizeof(buf), fmt, va);
+	vseprint(buf, buf + sizeof buf, fmt, va);
 	va_end(va);
 
-	p = buf+strlen(buf);
-	if(p > (buf+sizeof(buf)-3))
+	p = buf + strlen(buf);
+	if(p > buf + sizeof buf - 3)
 		sysfatal("pop3 command too long");
-
-	if(pop->debug)
-		fprint(2, "<- %s\n", buf);
+	pdprint(pop, "<- %s\n", buf);
 	strcpy(p, "\r\n");
 	Bwrite(&pop->bout, buf, strlen(buf));
 	Bflush(&pop->bout);
@@ -83,20 +91,19 @@
 	char *p;
 
 	alarm(60*1000);
-	s = Brdstr(&pop->bin, '\n', 0);
-	alarm(0);
-	if(s == nil){
+	if((s = Brdstr(&pop->bin, '\n', 0)) == nil){
 		close(pop->fd);
 		pop->fd = -1;
+		alarm(0);
 		return "unexpected eof";
 	}
+	alarm(0);
 
-	p = s+strlen(s)-1;
+	p = s + strlen(s) - 1;
 	while(p >= s && (*p == '\r' || *p == '\n'))
 		*p-- = '\0';
 
-	if(pop->debug)
-		fprint(2, "-> %s\n", s);
+	pdprint(pop, "-> %s\n", s);
 	free(pop->lastline);
 	pop->lastline = s;
 	return s;
@@ -119,40 +126,35 @@
 	int fd;
 	uchar digest[SHA1dlen];
 	TLSconn conn;
-	char *err;
 
-	err = nil;
 	memset(&conn, 0, sizeof conn);
 	// conn.trace = pop3log;
 	fd = tlsClient(pop->fd, &conn);
-	if(fd < 0){
-		err = "tls error";
-		goto out;
-	}
-	pop->fd = fd;
-	Binit(&pop->bin, pop->fd, OREAD);
-	Binit(&pop->bout, pop->fd, OWRITE);
+	if(fd < 0)
+		return "tls error";
 	if(conn.cert==nil || conn.certlen <= 0){
-		err = "server did not provide TLS certificate";
-		goto out;
+		close(fd);
+		return "server did not provide TLS certificate";
 	}
 	sha1(conn.cert, conn.certlen, digest, nil);
 	if(!pop->thumb || !okThumbprint(digest, pop->thumb)){
-		fmtinstall('H', encodefmt);
-		fprint(2, "upas/fs pop3: server certificate %.*H not recognized\n", SHA1dlen, digest);
-		err = "bad server certificate";
-		goto out;
+		close(fd);
+		free(conn.cert);
+		eprint("pop3: server certificate %.*H not recognized\n", SHA1dlen, digest);
+		return "bad server certificate";
 	}
-	pop->encrypted = 1;
-out:
-	free(conn.sessionID);
 	free(conn.cert);
-	return err;
+	close(pop->fd);
+	pop->fd = fd;
+	pop->encrypted = 1;
+	Binit(&pop->bin, pop->fd, OREAD);
+	Binit(&pop->bout, pop->fd, OWRITE);
+	return nil;
 }
 
-//
-// get capability list, possibly start tls
-//
+/*
+ *  get capability list, possibly start tls
+ */
 static char*
 pop3capa(Pop *pop)
 {
@@ -186,9 +188,9 @@
 	return nil;
 }
 
-//
-// log in using APOP if possible, password if allowed by user
-//
+/*
+ *  log in using APOP if possible, password if allowed by user
+ */
 static char*
 pop3login(Pop *pop)
 {
@@ -207,10 +209,10 @@
 	else
 		ubuf[0] = '\0';
 
-	// look for apop banner
-	if(pop->ppop==0 && (p = strchr(s, '<')) && (q = strchr(p+1, '>'))) {
+	/* look for apop banner */
+	if(pop->ppop == 0 && (p = strchr(s, '<')) && (q = strchr(p + 1, '>'))) {
 		*++q = '\0';
-		if((n=auth_respond(p, q-p, user, sizeof user, buf, sizeof buf, auth_getkey, "proto=apop role=client server=%q%s",
+		if((n=auth_respond(p, q - p, user, sizeof user, buf, sizeof buf, auth_getkey, "proto=apop role=client server=%q%s",
 			pop->host, ubuf)) < 0)
 			return "factotum failed";
 		if(user[0]=='\0')
@@ -253,65 +255,57 @@
 	}
 }
 
-//
-// dial and handshake with pop server
-//
+/*
+ *  dial and handshake with pop server
+ */
 static char*
 pop3dial(Pop *pop)
 {
 	char *err;
 
-	if(pop->fd >= 0){
-		close(pop->fd);
-		pop->fd = -1;
-	}
 	if((pop->fd = dial(netmkaddr(pop->host, "net", pop->needssl ? "pop3s" : "pop3"), 0, 0, 0)) < 0)
 		return geterrstr();
 
 	if(pop->needssl){
 		if((err = pop3pushtls(pop)) != nil)
-			goto Out;
+			return err;
 	}else{
 		Binit(&pop->bin, pop->fd, OREAD);
 		Binit(&pop->bout, pop->fd, OWRITE);
 	}
-	err = pop3login(pop);
-Out:
-	if(err != nil){
-		if(pop->fd >= 0){
-			close(pop->fd);
-			pop->fd = -1;
-		}
+
+	if(err = pop3login(pop)) {
+		close(pop->fd);
+		return err;
 	}
-	return err;
+
+	return nil;
 }
 
-//
-// close connection
-//
+/*
+ *  close connection
+ */
 static void
 pop3hangup(Pop *pop)
 {
-	if(pop->fd < 0)
-		return;
 	pop3cmd(pop, "QUIT");
 	pop3resp(pop);
 	close(pop->fd);
-	pop->fd = -1;
 }
 
-//
-// download a single message
-//
+/*
+ *  download a single message
+ */
 static char*
-pop3download(Pop *pop, Message *m)
+pop3download(Mailbox *mb, Pop *pop, Message *m)
 {
 	char *s, *f[3], *wp, *ep;
-	char sdigest[SHA1dlen*2+1];
-	int i, l, sz;
+	int l, sz, pos, n;
+	Popm *a;
 
+	a = m->aux;
 	if(!pop->pipeline)
-		pop3cmd(pop, "LIST %d", m->mesgno);
+		pop3cmd(pop, "LIST %d", a->mesgno);
 	if(!isokay(s = pop3resp(pop)))
 		return s;
 
@@ -318,18 +312,18 @@
 	if(tokenize(s, f, 3) != 3)
 		return "syntax error in LIST response";
 
-	if(atoi(f[1]) != m->mesgno)
+	if(atoi(f[1]) != a->mesgno)
 		return "out of sync with pop3 server";
 
-	sz = atoi(f[2])+200;	/* 200 because the plan9 pop3 server lies */
+	sz = atoi(f[2]) + 200;	/* 200 because the plan9 pop3 server lies */
 	if(sz == 0)
 		return "invalid size in LIST response";
 
-	m->start = wp = emalloc(sz+1);
-	ep = wp+sz;
+	m->start = wp = emalloc(sz + 1);
+	ep = wp + sz;
 
 	if(!pop->pipeline)
-		pop3cmd(pop, "RETR %d", m->mesgno);
+		pop3cmd(pop, "RETR %d", a->mesgno);
 	if(!isokay(s = pop3resp(pop))) {
 		m->start = nil;
 		free(wp);
@@ -347,7 +341,7 @@
 		if(strcmp(s, ".") == 0)
 			break;
 
-		l = strlen(s)+1;
+		l = strlen(s) + 1;
 		if(s[0] == '.') {
 			s++;
 			l--;
@@ -356,14 +350,17 @@
 		 * grow by 10%/200bytes - some servers
 		 *  lie about message sizes
 		 */
-		if(wp+l > ep) {
-			int pos = wp - m->start;
-			sz += ((sz / 10) < 200)? 200: sz/10;
-			m->start = erealloc(m->start, sz+1);
-			wp = m->start+pos;
-			ep = m->start+sz;
+		if(wp + l > ep) {
+			pos = wp - m->start;
+			n = sz/10;
+			if(n < 200)
+				n = 200;
+			sz += n;
+			m->start = erealloc(m->start, sz + 1);
+			wp = m->start + pos;
+			ep = m->start + sz;
 		}
-		memmove(wp, s, l-1);
+		memmove(wp, s, l - 1);
 		wp[l-1] = '\n';
 		wp += l;
 	}
@@ -373,40 +370,41 @@
 
 	m->end = wp;
 
-	// make sure there's a trailing null
-	// (helps in body searches)
+	/*
+	 *  make sure there's a trailing null
+	 *  (helps in body searches)
+	 */
 	*m->end = 0;
 	m->bend = m->rbend = m->end;
 	m->header = m->start;
+	m->size = m->end - m->start;
+	if(m->digest == nil)
+		digestmessage(mb, m);
 
-	// digest message
-	sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
-	for(i = 0; i < SHA1dlen; i++)
-		sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
-	m->sdigest = s_copy(sdigest);
-
 	return nil;
 }
 
-//
-// check for new messages on pop server
-// UIDL is not required by RFC 1939, but 
-// netscape requires it, so almost every server supports it.
-// we'll use it to make our lives easier.
-//
+/*
+ *  check for new messages on pop server
+ *  UIDL is not required by RFC 1939, but
+ *  netscape requires it, so almost every server supports it.
+ *  we'll use it to make our lives easier.
+ */
 static char*
-pop3read(Pop *pop, Mailbox *mb, int doplumb)
+pop3read(Pop *pop, Mailbox *mb, int doplumb, int *new)
 {
 	char *s, *p, *uidl, *f[2];
-	int mesgno, ignore, nnew;
+	int mno, ignore, nnew;
 	Message *m, *next, **l;
+	Popm *a;
 
-	// Some POP servers disallow UIDL if the maildrop is empty.
+	*new = 0;
+	/* Some POP servers disallow UIDL if the maildrop is empty. */
 	pop3cmd(pop, "STAT");
 	if(!isokay(s = pop3resp(pop)))
 		return s;
 
-	// fetch message listing; note messages to grab
+	/* fetch message listing; note messages to grab */
 	l = &mb->root->part;
 	if(strncmp(s, "+OK 0 ", 6) != 0) {
 		pop3cmd(pop, "UIDL");
@@ -421,25 +419,32 @@
 			if(tokenize(p, f, 2) != 2)
 				continue;
 
-			mesgno = atoi(f[0]);
+			mno = atoi(f[0]);
 			uidl = f[1];
-			if(strlen(uidl) > 75)	// RFC 1939 says 70 characters max
+			if(strlen(uidl) > 75)	/* RFC 1939 says 70 characters max */
 				continue;
 
 			ignore = 0;
 			while(*l != nil) {
-				if(strcmp((*l)->uidl, uidl) == 0) {
-					// matches mail we already have, note mesgno for deletion
-					(*l)->mesgno = mesgno;
+				a = (*l)->aux;
+				if(strcmp((*l)->idxaux, uidl) == 0){
+					if(a == 0){
+						m = *l;
+						m->mallocd = 1;
+						m->inmbox = 1;
+						m->aux = a = emalloc(sizeof *a);
+					}
+					/* matches mail we already have, note mesgno for deletion */
+					a->mesgno = mno;
 					ignore = 1;
 					l = &(*l)->next;
 					break;
-				} else {
-					// old mail no longer in box mark deleted
+				}else{
+					/* old mail no longer in box mark deleted */
 					if(doplumb)
 						mailplumb(mb, *l, 1);
 					(*l)->inmbox = 0;
-					(*l)->deleted = 1;
+					(*l)->deleted = Deleted;
 					l = &(*l)->next;
 				}
 			}
@@ -449,30 +454,31 @@
 			m = newmessage(mb->root);
 			m->mallocd = 1;
 			m->inmbox = 1;
-			m->mesgno = mesgno;
-			strcpy(m->uidl, uidl);
+			m->idxaux = strdup(uidl);
+			m->aux = a = emalloc(sizeof *a);
+			a->mesgno = mno;
 
-			// chain in; will fill in message later
+			/* chain in; will fill in message later */
 			*l = m;
 			l = &m->next;
 		}
 	}
 
-	// whatever is left has been removed from the mbox, mark as deleted
+	/* whatever is left has been removed from the mbox, mark as deleted */
 	while(*l != nil) {
 		if(doplumb)
 			mailplumb(mb, *l, 1);
 		(*l)->inmbox = 0;
-		(*l)->deleted = 1;
+		(*l)->deleted = Disappear;
 		l = &(*l)->next;
 	}
 
-	// download new messages
+	/* download new messages */
 	nnew = 0;
 	if(pop->pipeline){
 		switch(rfork(RFPROC|RFMEM)){
 		case -1:
-			fprint(2, "rfork: %r\n");
+			eprint("pop3: rfork: %r\n");
 			pop->pipeline = 0;
 
 		default:
@@ -480,12 +486,12 @@
 
 		case 0:
 			for(m = mb->root->part; m != nil; m = m->next){
-				if(m->start != nil)
+				if(m->start != nil || m->deleted)
 					continue;
-				Bprint(&pop->bout, "LIST %d\r\nRETR %d\r\n", m->mesgno, m->mesgno);
+				Bprint(&pop->bout, "LIST %d\r\nRETR %d\r\n", mesgno(m), mesgno(m));
 			}
 			Bflush(&pop->bout);
-			_exits(nil);
+			_exits("");
 		}
 	}
 
@@ -492,37 +498,29 @@
 	for(m = mb->root->part; m != nil; m = next) {
 		next = m->next;
 
-		if(m->start != nil)
+		if(m->start != nil || m->deleted)
 			continue;
-
-		if(s = pop3download(pop, m)) {
-			// message disappeared? unchain
-			fprint(2, "download %d: %s\n", m->mesgno, s);
+		if(s = pop3download(mb, pop, m)) {
+			/* message disappeared? unchain */
+			eprint("pop3: download %d: %s\n", mesgno(m), s);
 			delmessage(mb, m);
 			mb->root->subname--;
 			continue;
 		}
 		nnew++;
-		parse(m, 0, mb, 1);
-
-		if(doplumb)
-			mailplumb(mb, m, 0);
+		parse(mb, m, 1, 0);
+		newcachehash(mb, m, doplumb);
+		putcache(mb, m);
 	}
 	if(pop->pipeline)
 		waitpid();
-
-	if(nnew || mb->vers == 0) {
-		mb->vers++;
-		henter(PATH(0, Qtop), mb->name,
-			(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
-	}
-
+	*new = nnew;
 	return nil;	
 }
 
-//
-// delete marked messages
-//
+/*
+ *  delete marked messages
+ */
 static void
 pop3purge(Pop *pop, Mailbox *mb)
 {
@@ -531,7 +529,7 @@
 	if(pop->pipeline){
 		switch(rfork(RFPROC|RFMEM)){
 		case -1:
-			fprint(2, "rfork: %r\n");
+			eprint("pop3: rfork: %r\n");
 			pop->pipeline = 0;
 
 		default:
@@ -542,11 +540,11 @@
 				next = m->next;
 				if(m->deleted && m->refs == 0){
 					if(m->inmbox)
-						Bprint(&pop->bout, "DELE %d\r\n", m->mesgno);
+						Bprint(&pop->bout, "DELE %d\r\n", mesgno(m));
 				}
 			}
 			Bflush(&pop->bout);
-			_exits(nil);
+			_exits("");
 		}
 	}
 	for(m = mb->root->part; m != nil; m = next) {
@@ -554,7 +552,7 @@
 		if(m->deleted && m->refs == 0) {
 			if(m->inmbox) {
 				if(!pop->pipeline)
-					pop3cmd(pop, "DELE %d", m->mesgno);
+					pop3cmd(pop, "DELE %d", mesgno(m));
 				if(isokay(pop3resp(pop)))
 					delmessage(mb, m);
 			} else
@@ -564,19 +562,21 @@
 }
 
 
-// connect to pop3 server, sync mailbox
+/* connect to pop3 server, sync mailbox */
 static char*
-pop3sync(Mailbox *mb, int doplumb)
+pop3sync(Mailbox *mb, int doplumb, int *new)
 {
 	char *err;
 	Pop *pop;
 
 	pop = mb->aux;
+
 	if(err = pop3dial(pop)) {
 		mb->waketime = time(0) + pop->refreshtime;
 		return err;
 	}
-	if((err = pop3read(pop, mb, doplumb)) == nil){
+
+	if((err = pop3read(pop, mb, doplumb, new)) == nil){
 		pop3purge(pop, mb);
 		mb->d->atime = mb->d->mtime = time(0);
 	}
@@ -629,7 +629,7 @@
 	return Epop3ctl;
 }
 
-// free extra memory associated with mb
+/* free extra memory associated with mb */
 static void
 pop3close(Mailbox *mb)
 {
@@ -640,9 +640,18 @@
 	free(pop);
 }
 
-//
-// open mailboxes of the form /pop/host/user or /apop/host/user
-//
+static char*
+mkmbox(Pop *pop, char *p, char *e)
+{
+	p = seprint(p, e, "%s/box/%s/pop.%s", MAILROOT, getlog(), pop->host);
+	if(pop->user && strcmp(pop->user, getlog()))
+		p = seprint(p, e, ".%s", pop->user);
+	return p;
+}
+
+/*
+ *  open mailboxes of the form /pop/host/user or /apop/host/user
+ */
 char*
 pop3mbox(Mailbox *mb, char *path)
 {
@@ -650,7 +659,6 @@
 	int nf, apop, ppop, popssl, apopssl, apoptls, popnotls, apopnotls, poptls;
 	Pop *pop;
 
-	quotefmtinstall();
 	popssl = strncmp(path, "/pops/", 6) == 0;
 	apopssl = strncmp(path, "/apops/", 7) == 0;
 	poptls = strncmp(path, "/poptls/", 8) == 0;
@@ -673,8 +681,7 @@
 		return "bad pop3 path syntax /[a]pop[tls|ssl]/system[/user]";
 	}
 
-	pop = emalloc(sizeof(*pop));
-	pop->fd = -1;
+	pop = emalloc(sizeof *pop);
 	pop->freep = path;
 	pop->host = f[2];
 	if(nf < 4)
@@ -688,12 +695,13 @@
 	pop->notls = popnotls || apopnotls;
 	pop->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
 
+	mkmbox(pop, mb->path, mb->path + sizeof mb->path);
 	mb->aux = pop;
 	mb->sync = pop3sync;
 	mb->close = pop3close;
 	mb->ctl = pop3ctl;
-	mb->d = emalloc(sizeof(*mb->d));
-
+	mb->d = emalloc(sizeof *mb->d);
+	mb->addfrom = 1;
 	return nil;
 }
 
--- a/sys/src/cmd/upas/fs/readdir.c
+++ /dev/null
@@ -1,15 +1,0 @@
-#include <u.h>
-#include <libc.h>
-
-void
-main(void)
-{
-	Dir d;
-	int fd, n;
-
-	fd = open("/mail/fs", OREAD);
-	while((n = dirread(fd, &d, sizeof(d))) > 0){
-		print("%s\n", d.name);
-	}
-	print("n = %d\n", n);
-}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/ref.c
@@ -1,0 +1,100 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+/* all the data that's fit to cache */
+
+typedef struct{
+	char	*s;
+	int	l;
+	ulong	ref;
+}Refs;
+
+Refs	*rtab;
+int	nrtab;
+int	nralloc;
+
+int
+newrefs(char *s)
+{
+	int l, i;
+	Refs *r;
+
+	l = strlen(s);
+	for(i = 0; i < nrtab; i++){
+		r = rtab + i;
+		if(r->ref == 0)
+			goto enter;
+		if(l == r->l && strcmp(r->s, s) == 0){
+			r->ref++;
+			return i;
+		}
+	}
+	if(nrtab == nralloc)
+		rtab = erealloc(rtab, sizeof *rtab*(nralloc += 50));
+	nrtab = i + 1;
+enter:
+	r = rtab + i;
+	r->s = strdup(s);
+	r->l = l;
+	r->ref = 1;
+	return i;
+}
+
+void
+delrefs(int i)
+{
+	Refs *r;
+
+	r = rtab + i;
+	if(--r->ref > 0)
+		return;
+	free(r->s);
+	memset(r, 0, sizeof *r);
+}
+
+void
+refsinit(void)
+{
+	newrefs("");
+}
+
+static char *sep = "--------\n";
+
+int
+prrefs(Biobuf *b)
+{
+	int i, n;
+
+	n = 0;
+	for(i = 1; i < nrtab; i++){
+		if(rtab[i].ref == 0)
+			continue;
+		Bprint(b, "%s ", rtab[i].s);
+		if(n++%8 == 7)
+			Bprint(b, "\n");
+	}
+	if(n%8 != 7)
+		Bprint(b, "\n");
+	Bprint(b, sep);
+	return 0;
+}
+
+int
+rdrefs(Biobuf *b)
+{
+	char *f[10], *s;
+	int i, n;
+
+	while(s = Brdstr(b, '\n', 1)){
+		if(strcmp(s, sep) == 0){
+			free(s);
+			return 0;
+		}
+		n = tokenize(s, f, nelem(f));
+		for(i = 0; i < n; i++)
+			newrefs(f[i]);
+		free(s);
+	}
+	return -1;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/remove.c
@@ -1,0 +1,141 @@
+#include "common.h"
+#include "dat.h"
+
+#define deprint(...)	/* eprint(__VA_ARGS__) */
+
+extern int dirskip(Dir*, uvlong*);
+
+static int
+ismbox(char *path)
+{
+	char buf[512];
+	int fd, r;
+
+	fd = open(path, OREAD);
+	if(fd == -1)
+		return 0;
+	r = 1;
+	if(read(fd, buf, sizeof buf) < 28 + 5)
+		r = 0;
+	else if(strncmp(buf, "From ", 5))
+		r = 0;
+	close(fd);
+	return r;
+}
+
+static int
+isindex(Dir *d)
+{
+	char *p;
+
+	p = strrchr(d->name, '.');
+	if(!p)
+		return -1;
+	if(strcmp(p, ".idx") || strcmp(p, ".imp"))
+		return 1;
+	return 0;
+}
+
+static int
+idiotcheck(char *path, Dir *d, int getindex)
+{
+	uvlong v;
+
+	if(d->mode & DMDIR)
+		return 0;
+	if(strncmp(d->name, "L.", 2) == 0)
+		return 0;
+	if(getindex && isindex(d))
+		return 0;
+	if(!dirskip(d, &v) || ismbox(path))
+		return 0;
+	return -1;
+}
+
+int
+vremove(char *buf)
+{
+	deprint("rm %s\n", buf);
+	return remove(buf);
+}
+
+static int
+rm(char *dir, int flags, int level)
+{
+	char buf[Pathlen];
+	int i, n, r, fd, isdir, rflag;
+	Dir *d;
+
+	d = dirstat(dir);
+	isdir = d->mode & DMDIR;
+	free(d);
+	if(!isdir)
+		return 0;
+	fd = open(dir, OREAD);
+	if(fd == -1)
+		return -1;
+	n = dirreadall(fd, &d);
+	close(fd);
+	r = 0;
+	rflag = flags & Rrecur;
+	for(i = 0; i < n; i++){
+		snprint(buf, sizeof buf, "%s/%s", dir, d[i].name);
+		if(rflag)
+			r |= rm(buf, flags, level + 1);
+		if(idiotcheck(buf, d + i, level + rflag) == -1)
+			continue;
+		if(vremove(buf) != 0)
+			r = -1;
+	}
+	free(d);
+	return r;
+}
+
+void
+rmidx(char *buf, int flags)
+{
+	char buf2[Pathlen];
+
+	snprint(buf2, sizeof buf2, "%s.idx", buf);
+	vremove(buf2);
+	if((flags & Rtrunc) == 0){
+		snprint(buf2, sizeof buf2, "%s.imp", buf);
+		vremove(buf2);
+	}
+}
+
+char*
+localremove(Mailbox *mb, int flags)
+{
+	char *msg, *path;
+	int r, isdir;
+	Dir *d;
+	static char err[2*Pathlen];
+
+	path = mb->path;
+	if((d = dirstat(path)) == 0){
+		snprint(err, sizeof err, "%s: doesn't exist\n", path);
+		return 0;
+	}
+	isdir = d->mode & DMDIR;
+	free(d);
+	msg = "deleting";
+	if(flags & Rtrunc)
+		msg = "truncating";
+	deprint("%s: %s\n", msg, path);
+
+	/* must match folder.c:/^openfolder */
+	r = rm(path, flags, 0);
+	if((flags & Rtrunc) == 0)
+		r = vremove(path);
+	else if(!isdir)
+		close(r = open(path, OWRITE|OTRUNC));
+
+	rmidx(path, flags);
+
+	if(r == -1){
+		snprint(err, sizeof err, "%s: can't %s\n", path, msg);
+		return err;
+	}
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/rename.c
@@ -1,0 +1,234 @@
+#include "common.h"
+#include "dat.h"
+
+#define deprint(...)	/* eprint(__VA_ARGS__) */
+
+static int
+delivery(char *s)
+{
+	if(strncmp(s, "/mail/fs/", 9) == 0)
+	if((s = strrchr(s, '/')) && strcmp(s + 1, "mbox") == 0)
+		return 1;
+	return 0;
+}
+
+static int
+isdir(char *s)
+{
+	int isdir;
+	Dir *d;
+
+	d = dirstat(s);
+	isdir = d && d->mode & DMDIR;
+	free(d);
+	return isdir;
+}
+
+static int
+docreate(char *file, int perm)
+{
+	int fd;
+	Dir ndir;
+	Dir *d;
+
+	fd = create(file, OREAD, perm);
+	if(fd < 0)
+		return -1;
+	d = dirfstat(fd);
+	if(d == nil)
+		return -1;
+	nulldir(&ndir);
+	ndir.mode = perm;
+	ndir.gid = d->uid;
+	dirfwstat(fd, &ndir);
+	close(fd);
+	return 0;
+}
+
+static int
+rollup(char *s)
+{
+	char *p;
+	int mode;
+
+	if(access(s, 0) == 0)
+		return -1;
+
+	/*
+	 * if we can deliver to this mbox, it needs
+	 * to be read/execable all the way down
+	 */
+	mode = 0711;
+	if(delivery(s))
+		mode = 0755;
+
+	for(p = s; p; p++) {
+		if(*p == '/')
+			continue;
+		p = strchr(p, '/');
+		if(p == 0)
+			break;
+		*p = 0;
+		if(access(s, 0) != 0)
+		if(docreate(s, DMDIR|mode) < 0)
+			return -1;
+		*p = '/';
+	}
+	return 0;
+}
+
+static int
+copyfile(char *a, char *b, int flags)
+{
+	char *s;
+	int fd, fd1, mode, i, m, n, r;
+	Dir *d;
+
+	mode = 0600;
+	if(delivery(b))
+		mode = 0622;
+	fd = open(a, OREAD);
+	fd1 = create(b, OWRITE|OEXCL, DMEXCL|mode);
+	if(fd == -1 || fd1 == -1){
+		close(fd);
+		close(fd1);
+		return -1;
+	}
+	s = malloc(64*1024);
+	i = m = 0;
+	while((n = read(fd, s, sizeof s)) > 0)
+		for(i = 0; i != n; i += m)
+			if((m = write(fd1, s + i, n - i)) == -1)
+				goto lose;
+lose:
+	free(s);
+	close(fd);
+	close(fd1);
+	if(i != m || n != 0)
+		return -1;
+
+	if((flags & Rtrunc) == 0)
+		return vremove(a);
+
+	fd = open(a, ORDWR);
+	if(fd == -1)
+		return -1;
+	r = -1;
+	if(d = dirfstat(fd)){
+		d->length = 0;
+		r = dirfwstat(fd, d);
+		free(d);
+	}
+	return r;
+}
+
+static int
+copydir(char *a, char *b, int flags)
+{
+	char *p, buf[Pathlen], ns[Pathlen], owd[Pathlen];
+	int fd, fd1, len, i, n, r;
+	Dir *d;
+
+	fd = open(a, OREAD);
+	fd1 = create(b, OWRITE|OEXCL, DMEXCL|0777);
+	close(fd1);
+	if(fd == -1 || fd1 == -1){
+		close(fd);
+		return -1;
+	}
+
+	/* fixup mode */
+	if(delivery(b))
+	if(d = dirfstat(fd)){
+		d->mode |= 0777;
+		dirfwstat(fd, d);
+		free(d);
+	}
+
+	getwd(owd, sizeof owd);
+	if(chdir(a) == -1)
+		return -1;
+
+	p = seprint(buf, buf + sizeof buf, "%s/", b);
+	len = buf + sizeof buf - p;
+	n = dirreadall(fd, &d);
+	r = 0;
+	for(i = 0; i < n; i++){
+		snprint(p, len, "%s", d[i].name);
+		if(d->mode & DMDIR){
+			snprint(ns, sizeof ns, "%s/%s", a, d[i].name);
+			r |= copydir(ns, buf, 0);
+			chdir(a);
+		}else
+			r |= copyfile(d[i].name, buf, 0);
+		if(r)
+			break;
+	}
+	free(d);
+
+	if((flags & Rtrunc) == 0)
+		r |= vremove(a);
+
+	chdir(owd);
+	return r;
+}
+
+int
+rename(char *a, char *b, int flags)
+{
+	char *e0, *e1;
+	int fd, r;
+	Dir *d;
+
+	e0 = strrchr(a, '/');
+	e1 = strrchr(b, '/');
+	if(!e0 || !e1 || !e1[1])
+		return -1;
+
+	if(e0 - a == e1 - b)
+	if(strncmp(a, b, e0 - a) == 0)
+	if(!delivery(a) || isdir(a)){
+		fd = open(a, OREAD);
+		if(!(d = dirfstat(fd))){
+			close(fd);
+			return -1;
+		}
+		d->name = e1 + 1;
+		r = dirfwstat(fd, d);
+		deprint("rename %s %s -> %d\n", a, b, r);
+		if(r != -1 && flags & Rtrunc)
+			close(create(a, OWRITE, d->mode));
+		free(d);
+		close(fd);
+		return r;
+	}
+
+	if(rollup(b) == -1)
+		return -1;
+	if(isdir(a))
+		return copydir(a, b, flags);
+	return copyfile(a, b, flags);
+}
+
+char*
+localrename(Mailbox *mb, char *p2, int flags)
+{
+	char *path, *msg;
+	int r;
+	static char err[2*Pathlen];
+
+	path = mb->path;
+	msg = "rename";
+	if(flags & Rtrunc)
+		msg = "move";
+	deprint("localrename %s: %s %s\n", msg, path, p2);
+
+	r = rename(path, p2, flags);
+	if(r == -1){
+		snprint(err, sizeof err, "%s: can't %s\n", path, msg);
+		deprint("localrename %s\n", err);
+		return err;
+	}
+	close(r);
+	return 0;
+}
--- a/sys/src/cmd/upas/fs/rfc2047-test
+++ /dev/null
@@ -1,28 +1,0 @@
-From [email protected] Tue Mar 28 21:58:10 CST 2006
-From: =?US-ASCII?Q?Keith_Moore?= <[email protected]>
-To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <[email protected]>
-CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>
-Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=
-    =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=
-
-From [email protected] Tue Mar 28 21:58:10 CST 2006
-From: =?ISO-8859-1?Q?Olle_J=E4rnefors?= <[email protected]>
-To: [email protected], [email protected]
-Subject: Time for ISO 10646?
-
-From [email protected] Tue Mar 28 21:58:10 CST 2006
-To: Dave Crocker <[email protected]>
-Cc: [email protected], [email protected]
-From: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <[email protected]>
-Subject: Re: RFC-HDR care and feeding
-
-From [email protected] Tue Mar 28 21:58:10 CST 2006
-From: Nathaniel Borenstein <[email protected]>
-	(=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)
-To: Greg Vaudreuil <[email protected]>, Ned Freed
-      <[email protected]>, Keith Moore <[email protected]>
-Subject: Test of new header generator
-MIME-Version: 1.0
-Content-type: text/plain; charset=ISO-8859-1
-
-
--- /dev/null
+++ b/sys/src/cmd/upas/fs/seg.c
@@ -1,0 +1,164 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+/*
+ * unnatural acts with virtual memory
+ */
+
+typedef struct{
+	int	ref;
+	char	*va;
+	long	sz;
+}S;
+
+static S		s[15];		/* 386 only gives 4 */
+static int		nstab = nelem(s);
+static long	ssem = 1;
+//static ulong	thresh = 10*1024*1024;
+static ulong	thresh = 1024;
+
+void*
+segmalloc(ulong sz)
+{
+	int i, j;
+	void *va;
+
+	if(sz < thresh)
+		return emalloc(sz);
+	semacquire(&ssem, 1);
+	for(i = 0; i < nstab; i++)
+		if(s[i].ref == 0)
+			goto found;
+notfound:
+	/* errstr not informative; assume we hit seg limit */
+	for(j = nstab - 1; j >= i; j--)
+		if(s[j].ref)
+			break;
+	nstab = j;
+	semrelease(&ssem, 1);
+	return emalloc(sz);
+found:
+	/*
+	 * the system doesn't leave any room for expansion
+	 */
+	va = segattach(SG_CEXEC, "memory", 0, sz + sz/10 + 4096);
+	if(va == 0)
+		goto notfound;
+	s[i].ref++;
+	s[i].va = va;
+	s[i].sz = sz;
+	semrelease(&ssem, 1);
+	memset(va, 0, sz);
+	return va;
+}
+
+void
+segmfree(void *va)
+{
+	char *a;
+	int i;
+
+	a = va;
+	for(i = 0; i < nstab; i++)
+		if(s[i].va == a)
+			goto found;
+	free(va);
+	return;
+found:
+	semacquire(&ssem, 1);
+	s[i].ref--;
+	s[i].va = 0;
+	s[i].sz = 0;
+	semrelease(&ssem, 1);
+}
+
+void*
+segreallocfixup(int i, ulong sz)
+{
+	char buf[ERRMAX];
+	void *va, *ova;
+
+	rerrstr(buf, sizeof buf);
+	if(strstr(buf, "segments overlap") == 0)
+		sysfatal("segibrk: %r");
+	va = segattach(SG_CEXEC, "memory", 0, sz);
+	if(va == 0)
+		sysfatal("segattach: %r");
+	ova = s[i].va;
+fprint(2, "fix memcpy(%p, %p, %lud)\n", va, ova, s[i].sz);
+	memcpy(va, ova, s[i].sz);
+	s[i].va = va;
+	s[i].sz = sz;
+	segdetach(ova);
+	return va;
+}
+
+void*
+segrealloc(void *va, ulong sz)
+{
+	char *a;
+	int i;
+	ulong sz0;
+
+fprint(2, "segrealloc %p %lud\n", va, sz);
+	if(va == 0)
+		return segmalloc(sz);
+	a = va;
+	for(i = 0; i < nstab; i++)
+		if(s[i].va == a)
+			goto found;
+	if(sz >= thresh)
+	if(a = segmalloc(sz)){
+		sz0 = msize(va);
+		memcpy(a, va, sz0);
+fprint(2, "memset(%p, 0, %lud)\n", a + sz0, sz - sz0);
+		memset(a + sz0, 0, sz - sz0);
+		return a;
+	}
+	return realloc(va, sz);
+found:
+	sz0 = s[i].sz;
+fprint(2, "segbrk(%p, %p)\n", s[i].va, s[i].va + sz);
+	va = segbrk(s[i].va, s[i].va + sz);
+	if(va == (void*)-1 || va < end)
+		return segreallocfixup(i, sz);
+	a = va;
+	if(sz > sz0)
+{
+fprint(2, "memset(%p, 0, %lud)\n", a + sz0, sz - sz0);
+		memset(a + sz0, 0, sz - sz0);
+}
+	s[i].va = va;
+	s[i].sz = sz;
+	return va;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+fprint(2, "emalloc %lud\n", n);
+	p = mallocz(n, 1);
+	if(!p)
+		sysfatal("malloc %lud: %r", n);
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void
+main(void)
+{
+	char *p;
+	int i;
+	ulong sz;
+
+	p = 0;
+	for(i = 0; i < 6; i++){
+		sz = i*512;
+		p = segrealloc(p, sz);
+		memset(p, 0, sz);
+	}
+	segmfree(p);
+	exits("");
+}
--- a/sys/src/cmd/upas/fs/strtotm.c
+++ b/sys/src/cmd/upas/fs/strtotm.c
@@ -1,11 +1,10 @@
 #include <u.h>
 #include <libc.h>
-#include <ctype.h>
 
 static char*
 skiptext(char *q)
 {
-	while(*q!='\0' && *q!=' ' && *q!='\t' && *q!='\r' && *q!='\n')
+	while(*q != '\0' && *q != ' ' && *q != '\t' && *q != '\r' && *q != '\n')
 		q++;
 	return q;
 }
@@ -13,7 +12,7 @@
 static char*
 skipwhite(char *q)
 {
-	while(*q==' ' || *q=='\t' || *q=='\r' || *q=='\n')
+	while(*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n')
 		q++;
 	return q;
 }
@@ -20,29 +19,12 @@
 
 static char* months[] = {
 	"jan", "feb", "mar", "apr",
-	"may", "jun", "jul", "aug", 
+	"may", "jun", "jul", "aug",
 	"sep", "oct", "nov", "dec"
 };
 
-static int
-strcmplwr(char *a, char *b, int n)
-{
-	char *eb;
-
-	eb = b+n;
-	while(*a && *b && b<eb){
-		if(tolower(*a) != tolower(*b))
-			return 1;
-		a++;
-		b++;
-	}
-	if(b==eb)
-		return 0;
-	return *a != *b;
-}
-
 int
-strtotm(char *p, Tm *tmp)
+strtotm(char *p, Tm *t)
 {
 	char *q, *r;
 	int j;
@@ -56,15 +38,15 @@
 	tm.min = -1;
 	tm.year = -1;
 	tm.mday = -1;
-	for(p=skipwhite(p); *p; p=skipwhite(q)){
+	for(p = skipwhite(p); *p; p = skipwhite(q)){
 		q = skiptext(p);
 
 		/* look for time in hh:mm[:ss] */
-		if(r = memchr(p, ':', q-p)){
+		if(r = memchr(p, ':', q - p)){
 			tm.hour = strtol(p, 0, 10);
-			tm.min = strtol(r+1, 0, 10);
-			if(r = memchr(r+1, ':', q-(r+1)))
-				tm.sec = strtol(r+1, 0, 10);
+			tm.min = strtol(r + 1, 0, 10);
+			if(r = memchr(r + 1, ':', q - (r + 1)))
+				tm.sec = strtol(r + 1, 0, 10);
 			else
 				tm.sec = 0;
 			continue;
@@ -71,43 +53,45 @@
 		}
 
 		/* look for month */
-		for(j=0; j<12; j++)
-			if(strcmplwr(p, months[j], 3)==0){
+		for(j = 0; j < 12; j++)
+			if(cistrncmp(p, months[j], 3) == 0){
 				tm.mon = j;
 				break;
 			}
-
-		if(j!=12)
+		if(j != 12)
 			continue;
 
 		/* look for time zone [A-Z][A-Z]T */
-		if(q-p==3 && 'A' <= p[0] && p[0] <= 'Z' 
-		&& 'A' <= p[1] && p[1] <= 'Z' && p[2] == 'T'){
-			strecpy(tm.zone, tm.zone+4, p);
+		if(q - p == 3)
+		if(p[0] >= 'A' && p[0] <= 'Z')
+		if(p[1] >= 'A' && p[1] <= 'Z')
+		if(p[2] == 'T'){
+			strecpy(tm.zone, tm.zone + 4, p);
 			continue;
 		}
 
-		if(p[0]=='+'||p[0]=='-')
-		if(q-p==5 && strspn(p+1, "0123456789") == 4){
-			delta = (((p[1]-'0')*10+p[2]-'0')*60+(p[3]-'0')*10+p[4]-'0')*60;
+		if(p[0] == '+'||p[0] == '-')
+		if(q - p == 5 && strspn(p + 1, "0123456789") == 4){
+			delta = (((p[1] - '0')*10 + p[2] - '0')*60 + (p[3] - '0')*10 + p[4] - '0')*60;
 			if(p[0] == '-')
 				delta = -delta;
 			continue;
 		}
-		if(strspn(p, "0123456789") == q-p){
+		if(strspn(p, "0123456789") == q - p){
 			j = strtol(p, nil, 10);
-			if(1 <= j && j <= 31)
+			if(j >= 1 && j <= 31)
 				tm.mday = j;
 			if(j >= 1900)
-				tm.year = j-1900;
+				tm.year = j - 1900;
+			continue;
 		}
+		//eprint("strtotm: garbage %.*s\n", q - p, p);
 	}
-
-	if(tm.mon<0 || tm.year<0
-	|| tm.hour<0 || tm.min<0
-	|| tm.mday<0)
+	if(tm.mon < 0 || tm.year < 0
+	|| tm.hour < 0 || tm.min < 0
+	|| tm.mday < 0)
 		return -1;
 
-	*tmp = *localtime(tm2sec(&tm)-delta);
+	*t = *localtime(tm2sec(&tm) - delta);
 	return 0;
 }
--- a/sys/src/cmd/upas/fs/tester.c
+++ /dev/null
@@ -1,81 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <ctype.h>
-#include <String.h>
-#include "message.h"
-
-Message *root;
-
-void
-prindent(int i)
-{
-	for(; i > 0; i--)
-		print(" ");
-}
-
-void
-prstring(int indent, char *tag, String *s)
-{
-	if(s == nil)
-		return;
-	prindent(indent+1);
-	print("%s %s\n", tag, s_to_c(s));
-}
-
-void
-info(int indent, int mno, Message *m)
-{
-	int i;
-	Message *nm;
-
-	prindent(indent);
-	print("%d%c %d ", mno, m->allocated?'*':' ', m->end - m->start);
-	if(m->unixfrom != nil)
-		print("uf %s ", s_to_c(m->unixfrom));
-	if(m->unixdate != nil)
-		print("ud %s ", s_to_c(m->unixdate));
-	print("\n");
-	prstring(indent, "from:", m->from822);
-	prstring(indent, "sender:", m->sender822);
-	prstring(indent, "to:", m->to822);
-	prstring(indent, "cc:", m->cc822);
-	prstring(indent, "reply-to:", m->replyto822);
-	prstring(indent, "subject:", m->subject822);
-	prstring(indent, "date:", m->date822);
-	prstring(indent, "filename:", m->filename);
-	prstring(indent, "type:", m->type);
-	prstring(indent, "charset:", m->charset);
-
-	i = 1;
-	for(nm = m->part; nm != nil; nm = nm->next){
-		info(indent+1, i++, nm);
-	}
-}
-	
-
-void
-main(int argc, char **argv)
-{
-	char *err;
-	char *mboxfile;
-
-	ARGBEGIN{
-	}ARGEND;
-
-	if(argc > 0)
-		mboxfile = argv[0];
-	else
-		mboxfile = "./mbox";
-
-	root = newmessage(nil);
-
-	err = readmbox(mboxfile, &root->part);
-	if(err != nil){
-		fprint(2, "boom: %s\n", err);
-		exits(0);
-	}
-
-	info(0, 1, root);
-
-	exits(0);
-}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/auth.c
@@ -1,0 +1,280 @@
+#include "imap4d.h"
+#include <libsec.h>
+
+static char Ebadch[]	= "can't get challenge";
+static char Ecantstart[]	= "can't initialize mail system: %r";
+static char Ecancel[]	= "client cancelled authentication";
+static char Ebadau[]	= "login failed";
+
+/*
+ * hack to allow smtp forwarding.
+ * hide the peer IP address under a rock in the ratifier FS.
+ */
+void
+enableforwarding(void)
+{
+	char buf[64], peer[64], *p;
+	int fd;
+	ulong now;
+	static ulong last;
+
+	if(remote == nil)
+		return;
+
+	now = time(0);
+	if(now < last + 5*60)
+		return;
+	last = now;
+
+	fd = open("/srv/ratify", ORDWR);
+	if(fd < 0)
+		return;
+	if(!mount(fd, -1, "/mail/ratify", MBEFORE, "")){
+		close(fd);
+		return;
+	}
+	close(fd);
+
+	strncpy(peer, remote, sizeof peer);
+	peer[sizeof peer - 1] = 0;
+	p = strchr(peer, '!');
+	if(p != nil)
+		*p = 0;
+
+	snprint(buf, sizeof buf, "/mail/ratify/trusted/%s#32", peer);
+
+	/*
+	 * if the address is already there and the user owns it,
+	 * remove it and recreate it to give him a new time quanta.
+	 */
+	if(access(buf, 0) >= 0 && remove(buf) < 0)
+		return;
+
+	fd = create(buf, OREAD, 0666);
+	if(fd >= 0)
+		close(fd);
+}
+
+void
+setupuser(AuthInfo *ai)
+{
+	int pid;
+	Waitmsg *w;
+
+	if(ai){
+		strecpy(username, username + sizeof username, ai->cuid);
+
+		if(auth_chuid(ai, nil) == -1)
+			bye("user auth failed: %r");
+		auth_freeAI(ai);
+	}else
+		strecpy(username, username + sizeof username, getuser());
+
+	if(strcmp(username, "none") == 0 || newns(username, 0) == -1)
+		bye("user login failed: %r");
+	if(binupas){
+		if(bind(binupas, "/bin/upas", MREPL) > 0)
+			ilog("bound %s on /bin/upas", binupas);
+		else
+			bye("bind %s failed: %r", binupas);
+	}
+
+	/*
+	 * hack to allow access to outgoing smtp forwarding
+	 */
+	enableforwarding();
+
+	snprint(mboxdir, Pathlen, "/mail/box/%s", username);
+	if(mychdir(mboxdir) < 0)
+		bye("can't open user's mailbox");
+
+	switch(pid = fork()){
+	case -1:
+		bye(Ecantstart);
+		break;
+	case 0:
+if(!strstr(argv0, "8.out"))
+		execl("/bin/upas/fs", "upas/fs", "-np", nil);
+else{
+ilog("using /sys/src/cmd/upas/fs/8.out");
+execl("/sys/src/cmd/upas/fs/8.out", "upas/fs", "-np", nil);
+}
+		_exits(0);
+		break;
+	default:
+		break;
+	}
+	if((w = wait()) == nil || w->pid != pid || w->msg[0] != 0)
+		bye(Ecantstart);
+	free(w);
+}
+
+static char*
+authread(int *len)
+{
+	char *t;
+	int n;
+
+	t = Brdline(&bin, '\n');
+	n = Blinelen(&bin);
+	if(n < 2)
+		return nil;
+	n--;
+	if(t[n-1] == '\r')
+		n--;
+	t[n] = 0;
+	if(n == 0 || strcmp(t, "*") == 0)
+		return nil;
+	*len = n;
+	return t;
+}
+
+static char*
+authresp(void)
+{
+	char *s, *t;
+	int n;
+
+	t = authread(&n);
+	if(t == nil)
+		return nil;
+	s = binalloc(&parsebin, n + 1, 1);
+	n = dec64((uchar*)s, n, t, n);
+	s[n] = 0;
+	return s;
+}
+
+/*
+ * rfc 2195 cram-md5 authentication
+ */
+char*
+cramauth(void)
+{
+	char *s, *t;
+	int n;
+	AuthInfo *ai;
+	Chalstate *cs;
+
+	if((cs = auth_challenge("proto=cram role=server")) == nil)
+		return Ebadch;
+
+	n = cs->nchal;
+	s = binalloc(&parsebin, n * 2, 0);
+	n = enc64(s, n * 2, (uchar*)cs->chal, n);
+	Bprint(&bout, "+ ");
+	Bwrite(&bout, s, n);
+	Bprint(&bout, "\r\n");
+	if(Bflush(&bout) < 0)
+		writeerr();
+
+	s = authresp();
+	if(s == nil)
+		return Ecancel;
+
+	t = strchr(s, ' ');
+	if(t == nil)
+		return Ebadch;
+	*t++ = 0;
+	strncpy(username, s, Userlen);
+	username[Userlen - 1] = 0;
+
+	cs->user = username;
+	cs->resp = t;
+	cs->nresp = strlen(t);
+	if((ai = auth_response(cs)) == nil)
+		return Ebadau;
+	auth_freechal(cs);
+	setupuser(ai);
+	return nil;
+}
+
+char*
+crauth(char *u, char *p)
+{
+	char response[64];
+	AuthInfo *ai;
+	static char nchall[64];
+	static Chalstate *ch;
+
+again:
+	if(ch == nil){
+		if(!(ch = auth_challenge("proto=p9cr role=server user=%q", u)))
+			return Ebadch;
+		snprint(nchall, 64, " encrypt challenge: %s", ch->chal);
+		return nchall;
+	} else {
+		strncpy(response, p, 64);
+		ch->resp = response;
+		ch->nresp = strlen(response);
+		ai = auth_response(ch);
+		auth_freechal(ch);
+		ch = nil;
+		if(ai == nil)
+			goto again;
+		setupuser(ai);
+		return nil;
+	}
+}
+
+char*
+passauth(char *u, char *secret)
+{
+	char response[2*MD5dlen + 1];
+	uchar digest[MD5dlen];
+	int i;
+	AuthInfo *ai;
+	Chalstate *cs;
+
+	if((cs = auth_challenge("proto=cram role=server")) == nil)
+		return Ebadch;
+	hmac_md5((uchar*)cs->chal, strlen(cs->chal),
+		(uchar*)secret, strlen(secret), digest, nil);
+	for(i = 0; i < MD5dlen; i++)
+		snprint(response + 2*i, sizeof response - 2*i, "%2.2ux", digest[i]);
+	cs->user = u;
+	cs->resp = response;
+	cs->nresp = strlen(response);
+	ai = auth_response(cs);
+	if(ai == nil)
+		return Ebadau;
+	auth_freechal(cs);
+	setupuser(ai);
+	return nil;
+}
+
+static int
+niltokenize(char *buf, int n, char **f, int nelemf)
+{
+	int i, nf;
+
+	f[0] = buf;
+	nf = 1;
+	for(i = 0; i < n - 1; i++)
+		if(buf[i] == 0){
+			f[nf++] = buf + i + 1;
+			if(nf == nelemf)
+				break;
+		}
+	return nf;
+}
+
+char*
+plainauth(char *ch)
+{
+	char buf[256*3 + 2], *f[4];
+	int n, nf;
+
+	if(ch == nil){
+		Bprint(&bout, "+ \r\n");
+		if(Bflush(&bout) < 0)
+			writeerr();
+		ch = authread(&n);
+	}
+	if(ch == nil || strlen(ch) == 0)
+		return Ecancel;
+	n  = dec64((uchar*)buf, sizeof buf, ch, strlen(ch));
+	nf = niltokenize(buf, n, f, nelem(f));
+	if(nf != 3)
+		return Ebadau;
+	return passauth(f[1], f[2]);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/capability
@@ -1,0 +1,37 @@
+status
+u	acl			[rfc2086][rfc4314]
+u	annotate-experiment-1	[rfc5257]
+u	binary			[rfc3516]
+	catenate			[rfc4469]
+	children			[rfc3348]
+u	compress=deflate		[rfc4978]
+	condstore		[rfc4551]
+	context=search		[rfc5267]
+	context=sort		[rfc5267]
+u	convert			[rfc-ietf-lemonade-convert-20.txt]
+	enable			[rfc5161]
+*	esearch			[rfc4731]
+	esort			[rfc5267]
+u	i18nlevel=1		[rfc5255]
+u	i18nlevel=2		[rfc5255]
+u	id			[rfc2971]
+y	idle			[rfc2177]
+u	language			[rfc5255]
+	literal+			[rfc2088]
+	login-referrals		[rfc2221]
+y	logindisabled		[rfc2595][rfc3501]
+	mailbox-referrals		[rfc2193]
+	multiappend		[rfc3502]
+y	namespace		[rfc2342]
+	qresync			[rfc5162]
+y	quota			[rfc2087]
+u	rights=			[rfc4314]			
+	sasl-ir			[rfc4959]
+*	searchres			[rfc5182]
+*	sort			[rfc5256]
+	starttls			[rfc2595][rfc3501]
+n	thread			[rfc5256]
+y	uidplus			[rfc4315]	
+n	unselect			[rfc3691]			
+u	urlauth			[rfc4467]
+	within			[rfc5032]
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/copy.c
@@ -1,0 +1,248 @@
+#include "imap4d.h"
+#include <libsec.h>
+
+int
+copycheck(Box*, Msg *m, int, void *)
+{
+	int fd;
+
+	if(m->expunged)
+		return 0;
+	fd = msgfile(m, "rawunix");
+	if(fd < 0){
+		msgdead(m);
+		return 0;
+	}
+	close(fd);
+	return 1;
+}
+
+static int
+opendeliver(int *pip, char *folder, char *from, long t)
+{
+	char *av[7], buf[32];
+	int i, pid, fd[2];
+
+	if(pipe(fd) != 0)
+		sysfatal("pipe: %r");
+	pid = fork();
+	switch(pid){
+	case -1:
+		return -1;
+	case 0:
+		av[0] = "mbappend";
+		av[1] = folder;
+		i = 2;
+		if(from){
+			av[i++] = "-f";
+			av[i++] = from;
+		}
+		if(t != 0){
+			snprint(buf, sizeof buf, "%ld", t);
+			av[i++] = "-t";
+			av[i++] = buf;
+		}
+		av[i] = 0;
+		close(0);
+		dup(fd[1], 0);
+		if(fd[1] != 0)
+			close(fd[1]);
+		close(fd[0]);
+		exec("/bin/upas/mbappend", av);
+		ilog("exec: %r");
+		_exits("b0rked");
+		return -1;
+	default:
+		*pip = fd[0];
+		close(fd[1]);
+		return pid;
+	}
+}
+
+static int
+closedeliver(int pid, int fd)
+{
+	int nz, wpid;
+	Waitmsg *w;
+
+	close(fd);
+	while(w = wait()){
+		nz = !w->msg || !w->msg[0];
+		wpid = w->pid;
+		free(w);
+		if(wpid == pid)
+			return nz? 0: -1;
+	}
+	return -1;
+}
+
+/*
+ * we're going to all this trouble of fiddling the .imp file for
+ * the target mailbox because we wish to save the flags.  we
+ * should be using upas/fs's flags instead.
+ *
+ * note.  appendmb for mbox fmt wants to lock the directory.  
+ * since the locking is intentionally broken, we could get by
+ * with aquiring the lock before we fire up appendmb and
+ * trust that he doesn't worry if he does acquire the lock.
+ * instead, we'll just do locking around the .imp file.
+ */
+static int
+savemsg(char *dst, int flags, char *head, int nhead, Biobuf *b, long n, Uidplus *u)
+{
+	char *digest, buf[Bufsize + 1], digbuf[Ndigest + 1], folder[Pathlen];
+	uchar shadig[SHA1dlen];
+	int i, fd, pid, nr, ok;
+	DigestState *dstate;
+	Mblock *ml;
+
+	snprint(folder, sizeof folder, "%s/%s", mboxdir, dst);
+	pid = opendeliver(&fd, folder, 0, 0);
+	if(pid == -1)
+		return 0;
+	ok = 1;
+	dstate = sha1(nil, 0, nil, nil);
+	if(nhead){
+		sha1((uchar*)head, nhead, nil, dstate);
+		if(write(fd, head, nhead) != nhead){
+			ok = 0;
+			goto loose;
+		}
+	}
+	while(n > 0){
+		nr = n;
+		if(nr > Bufsize)
+			nr = Bufsize;
+		nr = Bread(b, buf, nr);
+		if(nr <= 0){
+			ok = 0;
+			break;
+		}
+		n -= nr;
+		sha1((uchar*)buf, nr, nil, dstate);
+		if(write(fd, buf, nr) != nr){
+			ok = 0;
+			break;
+		}
+	}
+loose:
+	closedeliver(pid, fd);
+	sha1(nil, 0, shadig, dstate);
+	if(ok){
+		digest = digbuf;
+		for(i = 0; i < SHA1dlen; i++)
+			sprint(digest + 2*i, "%2.2ux", shadig[i]);
+		ml = mblock();
+		if(ml == nil)
+			return 0;
+		ok = appendimp(dst, digest, flags, u) == 0;
+		mbunlock(ml);
+	}
+	return ok;
+}
+
+static int
+copysave(Box*, Msg *m, int, void *vs, Uidplus *u)
+{
+	int ok, fd;
+	vlong length;
+	Biobuf b;
+	Dir *d;
+
+	if(m->expunged)
+		return 0;
+	if((fd = msgfile(m, "rawunix")) == -1){
+		msgdead(m);
+		return 0;
+	}
+	if((d = dirfstat(fd)) == nil){
+		close(fd);
+		return 0;
+	}
+	length = d->length;
+	free(d);
+
+	Binit(&b, fd, OREAD);
+	ok = savemsg(vs, m->flags, 0, 0, &b, length, u);
+	Bterm(&b);
+	close(fd);
+	return ok;
+}
+
+int
+copysaveu(Box *box, Msg *m, int i, void *vs)
+{
+	int ok;
+	Uidplus *u;
+
+	u = binalloc(&parsebin, sizeof *u, 1);
+	ok = copysave(box, m, i, vs, u);
+	*uidtl = u;
+	uidtl = &u->next;
+	return ok;
+}
+
+
+/*
+ * first spool the input into a temorary file,
+ * and massage the input in the process.
+ * then save to real box.
+ */
+/*
+ * copy from bin to bout,
+ * map "\r\n" to "\n" and
+ * return the number of bytes in the mapped file.
+ *
+ * exactly n bytes must be read from the input,
+ * unless an input error occurs.
+ */
+static long
+spool(Biobuf *bout, Biobuf *bin, long n)
+{
+	int c;
+
+	while(n > 0){
+		c = Bgetc(bin);
+		n--;
+		if(c == '\r' && n-- > 0){
+			c = Bgetc(bin);
+			if(c != '\n')
+				Bputc(bout, '\r');
+		}
+		if(c < 0)
+			return -1;
+		if(Bputc(bout, c) < 0)
+			return -1;
+	}
+	if(Bflush(bout) < 0)
+		return -1;
+	return Boffset(bout);
+}
+
+int
+appendsave(char *mbox, int flags, char *head, Biobuf *b, long n, Uidplus *u)
+{
+	int fd, ok;
+	Biobuf btmp;
+
+	fd = imaptmp();
+	if(fd < 0)
+		return 0;
+	Bprint(&bout, "+ Ready for literal data\r\n");
+	if(Bflush(&bout) < 0)
+		writeerr();
+	Binit(&btmp, fd, OWRITE);
+	n = spool(&btmp, b, n);
+	Bterm(&btmp);
+	if(n < 0){
+		close(fd);
+		return 0;
+	}
+
+	seek(fd, 0, 0);
+	Binit(&btmp, fd, OREAD);
+	ok = savemsg(mbox, flags, head, strlen(head), &btmp, n, u);
+	Bterm(&btmp);
+	close(fd);
+	return ok;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/csquery.c
@@ -1,0 +1,40 @@
+#include <u.h>
+#include <libc.h>
+
+/*
+ *  query the connection server
+ */
+char*
+csquery(char *attr, char *val, char *rattr)
+{
+	char token[64 + 4], buf[256], *p, *sp;
+	int fd, n;
+
+	if(val == nil || val[0] == 0)
+		return nil;
+	fd = open("/net/cs", ORDWR);
+	if(fd < 0)
+		return nil;
+	fprint(fd, "!%s=%s", attr, val);
+	seek(fd, 0, 0);
+	snprint(token, sizeof token, "%s=", rattr);
+	for(;;){
+		n = read(fd, buf, sizeof buf - 1);
+		if(n <= 0)
+			break;
+		buf[n] = 0;
+		p = strstr(buf, token);
+		if(p != nil && (p == buf || p[-1] == 0)){
+			close(fd);
+			sp = strchr(p, ' ');
+			if(sp)
+				*sp = 0;
+			p = strchr(p, '=');
+			if(p == nil)
+				return nil;
+			return strdup(p + 1);
+		}
+	}
+	close(fd);
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/date.c
@@ -1,0 +1,263 @@
+#include "imap4d.h"
+
+static char *wdayname[] = {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+static char *monname[] = {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+/*
+ * zone	: [A-Za-z][A-Za-z][A-Za-z]	some time zone names
+ *	| [A-IK-Z]			military time; rfc1123 says the rfc822 spec is wrong.
+ *	| "UT"				universal time
+ *	| [+-][0-9][0-9][0-9][0-9]
+ * zones is the rfc-822 list of time zone names
+ */
+static Namedint zones[] =
+{
+	{"A",	-1 * 3600},
+	{"B",	-2 * 3600},
+	{"C",	-3 * 3600},
+	{"CDT", -5 * 3600},
+	{"CST", -6 * 3600},
+	{"D",	-4 * 3600},
+	{"E",	-5 * 3600},
+	{"EDT", -4 * 3600},
+	{"EST", -5 * 3600},
+	{"F",	-6 * 3600},
+	{"G",	-7 * 3600},
+	{"GMT", 0},
+	{"H",	-8 * 3600},
+	{"I",	-9 * 3600},
+	{"K",	-10 * 3600},
+	{"L",	-11 * 3600},
+	{"M",	-12 * 3600},
+	{"MDT", -6 * 3600},
+	{"MST", -7 * 3600},
+	{"N",	+1 * 3600},
+	{"O",	+2 * 3600},
+	{"P",	+3 * 3600},
+	{"PDT", -7 * 3600},
+	{"PST", -8 * 3600},
+	{"Q",	+4 * 3600},
+	{"R",	+5 * 3600},
+	{"S",	+6 * 3600},
+	{"T",	+7 * 3600},
+	{"U",	+8 * 3600},
+	{"UT",	0},
+	{"V",	+9 * 3600},
+	{"W",	+10 * 3600},
+	{"X",	+11 * 3600},
+	{"Y",	+12 * 3600},
+	{"Z",	0},
+};
+
+static void
+zone2tm(Tm *tm, char *s)
+{
+	int i;
+	Tm aux, *atm;
+
+	if(*s == '+' || *s == '-'){
+		i = strtol(s, &s, 10);
+		tm->tzoff = (i/100)*3600 + i%100;
+		strncpy(tm->zone, "", 4);
+		return;
+	}
+
+	/*
+	 * look it up in the standard rfc822 table
+	 */
+	strncpy(tm->zone, s, 3);
+	tm->zone[3] = 0;
+	tm->tzoff = 0;
+	for(i = 0; i < nelem(zones); i++){
+		if(cistrcmp(zones[i].name, s) == 0){
+			tm->tzoff = zones[i].v;
+			return;
+		}
+	}
+
+	/*
+	 * one last try: look it up in the current local timezone
+	 * probe a couple of times to get daylight/standard time change.
+	 */
+	aux = *tm;
+	memset(aux.zone, 0, 4);
+	aux.hour--;
+	for(i = 0; i < 2; i++){
+		atm = localtime(tm2sec(&aux));
+		if(cistrcmp(tm->zone, atm->zone) == 0){
+			tm->tzoff = atm->tzoff;
+			return;
+		}
+		aux.hour++;
+	}
+
+	strncpy(tm->zone, "GMT", 4);
+	tm->tzoff = 0;
+}
+
+/*
+ * hh[:mm[:ss]]
+ */
+static void
+time2tm(Tm *tm, char *s)
+{
+	tm->hour = strtoul(s, &s, 10);
+	if(*s++ != ':')
+		return;
+	tm->min = strtoul(s, &s, 10);
+	if(*s++ != ':')
+		return;
+	tm->sec = strtoul(s, &s, 10);
+}
+
+static int
+dateindex(char *d, char **tab, int n)
+{
+	int i;
+
+	for(i = 0; i < n; i++)
+		if(cistrcmp(d, tab[i]) == 0)
+			return i;
+	return -1;
+}
+
+int
+imap4date(Tm *tm, char *date)
+{
+	char *flds[4];
+
+	if(getfields(date, flds, 3, 0, "-") != 3)
+		return 0;
+
+	tm->mday = strtol(flds[0], nil, 10);
+	tm->mon = dateindex(flds[1], monname, 12);
+	tm->year = strtol(flds[2], nil, 10) - 1900;
+	return 1;
+}
+
+/*
+ * parse imap4 dates
+ */
+ulong
+imap4datetime(char *date)
+{
+	char *flds[4], *sflds[4];
+	ulong t;
+	Tm tm;
+
+	if(getfields(date, flds, 4, 0, " ") != 3)
+		return ~0;
+
+	if(!imap4date(&tm, flds[0]))
+		return ~0;
+
+	if(getfields(flds[1], sflds, 3, 0, ":") != 3)
+		return ~0;
+
+	tm.hour = strtol(sflds[0], nil, 10);
+	tm.min = strtol(sflds[1], nil, 10);
+	tm.sec = strtol(sflds[2], nil, 10);
+
+	strcpy(tm.zone, "GMT");
+	tm.yday = 0;
+	t = tm2sec(&tm);
+	zone2tm(&tm, flds[2]);
+	t -= tm.tzoff;
+	return t;
+}
+
+/*
+ * parse dates of formats
+ * [Wkd[,]] DD Mon YYYY HH:MM:SS zone
+ * [Wkd] Mon ( D|DD) HH:MM:SS zone YYYY
+ * plus anything similar
+ * return nil for a failure
+ */
+Tm*
+date2tm(Tm *tm, char *date)
+{
+	char *flds[7], *s, dstr[64];
+	int n;
+	Tm gmt, *atm;
+
+	/*
+	 * default date is Thu Jan  1 00:00:00 GMT 1970
+	 */
+	tm->wday = 4;
+	tm->mday = 1;
+	tm->mon = 1;
+	tm->hour = 0;
+	tm->min = 0;
+	tm->sec = 0;
+	tm->year = 70;
+	strcpy(tm->zone, "GMT");
+	tm->tzoff = 0;
+
+	strncpy(dstr, date, sizeof dstr);
+	dstr[sizeof dstr - 1] = 0;
+	n = tokenize(dstr, flds, 7);
+	if(n != 6 && n != 5)
+		return nil;
+
+	if(n == 5){
+		for(n = 5; n >= 1; n--)
+			flds[n] = flds[n - 1];
+		n = 5;
+	}else{
+		/*
+		 * Wday[,]
+		 */
+		s = strchr(flds[0], ',');
+		if(s != nil)
+			*s = 0;
+		tm->wday = dateindex(flds[0], wdayname, 7);
+		if(tm->wday < 0)
+			return nil;
+	}
+
+	/*
+	 * check for the two major formats:
+	 * Month first or day first
+	 */
+	tm->mon = dateindex(flds[1], monname, 12);
+	if(tm->mon >= 0){
+		tm->mday = strtoul(flds[2], nil, 10);
+		time2tm(tm, flds[3]);
+		zone2tm(tm, flds[4]);
+		tm->year = strtoul(flds[5], nil, 10);
+		if(strlen(flds[5]) > 2)
+			tm->year -= 1900;
+	}else{
+		tm->mday = strtoul(flds[1], nil, 10);
+		tm->mon = dateindex(flds[2], monname, 12);
+		tm->year = strtoul(flds[3], nil, 10);
+		if(strlen(flds[3]) > 2)
+			tm->year -= 1900;
+		time2tm(tm, flds[4]);
+		zone2tm(tm, flds[5]);
+	}
+
+	if(n == 5){
+		gmt = *tm;
+		strncpy(gmt.zone, "", 4);
+		gmt.tzoff = 0;
+		atm = gmtime(tm2sec(&gmt));
+		tm->wday = atm->wday;
+	}else{
+		/*
+		 * Wday[,]
+		 */
+		s = strchr(flds[0], ',');
+		if(s != nil)
+			*s = 0;
+		tm->wday = dateindex(flds[0], wdayname, 7);
+		if(tm->wday < 0)
+			return nil;
+	}
+	return tm;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/debug.c
@@ -1,0 +1,30 @@
+#include "imap4d.h"
+
+char	logfile[28]	= "imap4";
+
+void
+debuglog(char *fmt, ...)
+{
+	char buf[1024];
+	va_list arg;
+
+	if(debug == 0)
+		return;
+	va_start(arg, fmt);
+	vseprint(buf, buf + sizeof buf, fmt, arg);
+	va_end(arg);
+	syslog(0, logfile, "[%s:%d] %s", username, getpid(), buf);
+}
+
+void
+ilog(char *fmt, ...)
+{
+	char buf[1024];
+	va_list arg;
+
+	va_start(arg, fmt);
+	vseprint(buf, buf + sizeof buf, fmt, arg);
+	va_end(arg);
+	syslog(0, logfile, "[%s:%d] %s", username, getpid(), buf);
+
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fetch.c
@@ -1,0 +1,559 @@
+#include "imap4d.h"
+
+char *fetchpartnames[FPmax] =
+{
+	"",
+	"HEADER",
+	"HEADER.FIELDS",
+	"HEADER.FIELDS.NOT",
+	"MIME",
+	"TEXT",
+};
+
+/*
+ * implicitly set the \seen flag.  done in a separate pass
+ * so the .imp file doesn't need to be open while the
+ * messages are sent to the client.
+ */
+int
+fetchseen(Box *box, Msg *m, int uids, void *vf)
+{
+	Fetch *f;
+
+	if(m->expunged)
+		return uids;
+	for(f = vf; f != nil; f = f->next){
+		switch(f->op){
+		case Frfc822:
+		case Frfc822text:
+		case Fbodysect:
+			msgseen(box, m);
+			return 1;
+		}
+	}
+	return 1;
+}
+
+/*
+ * fetch messages
+ *
+ * imap4 body[] requests get translated to upas/fs files as follows
+ *	body[id.header] == id/rawheader file + extra \r\n
+ *	body[id.text] == id/rawbody
+ *	body[id.mime] == id/mimeheader + extra \r\n
+ *	body[id] === body[id.header] + body[id.text]
+*/
+int
+fetchmsg(Box *, Msg *m, int uids, void *vf)
+{
+	char *sep;
+	Fetch *f;
+	Tm tm;
+
+	if(m->expunged)
+		return uids;
+	for(f = vf; f != nil; f = f->next)
+		switch(f->op){
+		case Fflags:
+			break;
+		case Fuid:
+			break;
+		case Finternaldate:
+		case Fenvelope:
+		case Frfc822:
+		case Frfc822head:
+		case Frfc822text:
+		case Frfc822size:
+		case Fbodysect:
+		case Fbodypeek:
+		case Fbody:
+		case Fbodystruct:
+			if(!msgstruct(m, 1)){
+				msgdead(m);
+				return uids;
+			}
+			break;
+		default:
+			bye("bad implementation of fetch");
+			return 0;
+		}
+	if(m->expunged)
+		return uids;
+	if(vf == 0)
+		return 1;
+
+	/*
+	 * note: it is allowed to send back the responses one at a time
+	 * rather than all together.  this is exploited to send flags elsewhere.
+	 */
+	Bprint(&bout, "* %ud FETCH (", m->seq);
+	sep = "";
+	if(uids){
+		Bprint(&bout, "UID %ud", m->uid);
+		sep = " ";
+	}
+	for(f = vf; f != nil; f = f->next){
+		switch(f->op){
+		default:
+			bye("bad implementation of fetch");
+			break;
+		case Fflags:
+			Bprint(&bout, "%sFLAGS (", sep);
+			writeflags(&bout, m, 1);
+			Bprint(&bout, ")");
+			break;
+		case Fuid:
+			if(uids)
+				continue;
+			Bprint(&bout, "%sUID %ud", sep, m->uid);
+			break;
+		case Fenvelope:
+			Bprint(&bout, "%sENVELOPE ", sep);
+			fetchenvelope(m);
+			break;
+		case Finternaldate:
+			Bprint(&bout, "%sINTERNALDATE %#D", sep, date2tm(&tm, m->unixdate));
+			break;
+		case Fbody:
+			Bprint(&bout, "%sBODY ", sep);
+			fetchbodystruct(m, &m->head, 0);
+			break;
+		case Fbodystruct:
+			Bprint(&bout, "%sBODYSTRUCTURE ", sep);
+			fetchbodystruct(m, &m->head, 1);
+			break;
+		case Frfc822size:
+			Bprint(&bout, "%sRFC822.SIZE %ud", sep, msgsize(m));
+			break;
+		case Frfc822:
+			f->part = FPall;
+			Bprint(&bout, "%sRFC822", sep);
+			fetchbody(m, f);
+			break;
+		case Frfc822head:
+			f->part = FPhead;
+			Bprint(&bout, "%sRFC822.HEADER", sep);
+			fetchbody(m, f);
+			break;
+		case Frfc822text:
+			f->part = FPtext;
+			Bprint(&bout, "%sRFC822.TEXT", sep);
+			fetchbody(m, f);
+			break;
+		case Fbodysect:
+		case Fbodypeek:
+			Bprint(&bout, "%sBODY", sep);
+			fetchbody(fetchsect(m, f), f);
+			break;
+		}
+		sep = " ";
+	}
+	Bprint(&bout, ")\r\n");
+
+	return 1;
+}
+
+/*
+ * print out section, part, headers;
+ * find and return message section
+ */
+Msg *
+fetchsect(Msg *m, Fetch *f)
+{
+	Bputc(&bout, '[');
+	Bnlist(&bout, f->sect, ".");
+	if(f->part != FPall){
+		if(f->sect != nil)
+			Bputc(&bout, '.');
+		Bprint(&bout, "%s", fetchpartnames[f->part]);
+		if(f->hdrs != nil){
+			Bprint(&bout, " (");
+			Bslist(&bout, f->hdrs, " ");
+			Bputc(&bout, ')');
+		}
+	}
+	Bprint(&bout, "]");
+	return findmsgsect(m, f->sect);
+}
+
+/*
+ * actually return the body pieces
+ */
+void
+fetchbody(Msg *m, Fetch *f)
+{
+	char *s, *t, *e, buf[Bufsize + 2];
+	uint n, start, stop, pos;
+	int fd, nn;
+	Pair p;
+
+	if(m == nil){
+		fetchbodystr(f, "", 0);
+		return;
+	}
+	switch(f->part){
+	case FPheadfields:
+	case FPheadfieldsnot:
+		n = m->head.size + 3;
+		s = emalloc(n);
+		n = selectfields(s, n, m->head.buf, f->hdrs, f->part == FPheadfields);
+		fetchbodystr(f, s, n);
+		free(s);
+		return;
+	case FPhead:
+//ilog("head.size %d", m->head.size);
+		fetchbodystr(f, m->head.buf, m->head.size);
+		return;
+	case FPmime:
+		fetchbodystr(f, m->mime.buf, m->mime.size);
+		return;
+	case FPall:
+		fd = msgfile(m, "rawbody");
+		if(fd < 0){
+			msgdead(m);
+			fetchbodystr(f, "", 0);
+			return;
+		}
+		p = fetchbodypart(f, msgsize(m));
+		start = p.start;
+//ilog("head.size %d", m->head.size);
+		if(start < m->head.size){
+			stop = p.stop;
+			if(stop > m->head.size)
+				stop = m->head.size;
+//ilog("fetch header %ld.%ld (%ld)", start, stop, m->head.size);
+			Bwrite(&bout, m->head.buf + start, stop - start);
+			start = 0;
+			stop = p.stop;
+			if(stop <= m->head.size){
+				close(fd);
+				return;
+			}
+		}else
+			start -= m->head.size;
+		stop = p.stop - m->head.size;
+		break;
+	case FPtext:
+		fd = msgfile(m, "rawbody");
+		if(fd < 0){
+			msgdead(m);
+			fetchbodystr(f, "", 0);
+			return;
+		}
+		p = fetchbodypart(f, m->size);
+		start = p.start;
+		stop = p.stop;
+		break;
+	default:
+		fetchbodystr(f, "", 0);
+		return;
+	}
+
+	/*
+	 * read in each block, convert \n without \r to \r\n.
+	 * this means partial fetch requires fetching everything
+	 * through stop, since we don't know how many \r's will be added
+	 */
+	buf[0] = ' ';
+	for(pos = 0; pos < stop; ){
+		n = Bufsize;
+		if(n > stop - pos)
+			n = stop - pos;
+		n = read(fd, &buf[1], n);
+//ilog("read %ld at %d stop %ld\n", n, pos, stop);
+		if(n <= 0){
+ilog("must fill %ld bytes\n", stop - pos);
+fprint(2, "must fill %d bytes\n", stop - pos);
+			fetchbodyfill(stop - pos);
+			break;
+		}
+		e = &buf[n + 1];
+		*e = 0;
+		for(s = &buf[1]; s < e && pos < stop; s = t + 1){
+			t = memchr(s, '\n', e - s);
+			if(t == nil)
+				t = e;
+			n = t - s;
+			if(pos < start){
+				if(pos + n <= start){
+					s = t;
+					pos += n;
+				}else{
+					s += start - pos;
+					pos = start;
+				}
+				n = t - s;
+			}
+			nn = n;
+			if(pos + nn > stop)
+				nn = stop - pos;
+			if(Bwrite(&bout, s, nn) != nn)
+				writeerr();
+//ilog("w %ld at %ld->%ld stop %ld\n", nn, pos, pos + nn, stop);
+			pos += n;
+			if(*t == '\n'){
+				if(t[-1] != '\r'){
+					if(pos >= start && pos < stop)
+						Bputc(&bout, '\r');
+					pos++;
+				}
+				if(pos >= start && pos < stop)
+					Bputc(&bout, '\n');
+				pos++;
+			}
+		}
+		buf[0] = e[-1];
+	}
+	close(fd);
+}
+
+/*
+ * resolve the actual bounds of any partial fetch,
+ * and print out the bounds & size of string returned
+ */
+Pair
+fetchbodypart(Fetch *f, uint size)
+{
+	uint start, stop;
+	Pair p;
+
+	start = 0;
+	stop = size;
+	if(f->partial){
+		start = f->start;
+		if(start > size)
+			start = size;
+		stop = start + f->size;
+		if(stop > size)
+			stop = size;
+		Bprint(&bout, "<%ud>", start);
+	}
+	Bprint(&bout, " {%ud}\r\n", stop - start);
+	p.start = start;
+	p.stop = stop;
+	return p;
+}
+
+/*
+ * something went wrong fetching data
+ * produce fill bytes for what we've committed to produce
+ */
+void
+fetchbodyfill(uint n)
+{
+	while(n-- > 0)
+		if(Bputc(&bout, ' ') < 0)
+			writeerr();
+}
+
+/*
+ * return a simple string
+ */
+void
+fetchbodystr(Fetch *f, char *buf, uint size)
+{
+	Pair p;
+
+	p = fetchbodypart(f, size);
+	Bwrite(&bout, buf + p.start, p.stop - p.start);
+}
+
+char*
+printnlist(Nlist *sect)
+{
+	static char buf[100];
+	char *p;
+
+	for(p = buf; sect; sect = sect->next){
+		p += sprint(p, "%ud", sect->n);
+		if(sect->next)
+			*p++ = '.';
+	}
+	*p = 0;
+	return buf;
+}
+
+/*
+ * find the numbered sub-part of the message
+ */
+Msg*
+findmsgsect(Msg *m, Nlist *sect)
+{
+	uint id;
+
+	for(; sect != nil; sect = sect->next){
+		id = sect->n;
+		for(m = m->kids; m != nil; m = m->next)
+			if(m->id == id)
+				break;
+		if(m == nil)
+			return nil;
+	}
+	return m;
+}
+
+void
+fetchenvelope(Msg *m)
+{
+	Tm tm;
+
+	Bprint(&bout, "(%#D %Z ", date2tm(&tm, m->info[Idate]), m->info[Isubject]);
+	Bimapaddr(&bout, m->from);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->sender);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->replyto);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->to);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->cc);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->bcc);
+	Bprint(&bout, " %Z %Z)", m->info[Iinreplyto], m->info[Imessageid]);
+}
+
+static int
+Bmime(Biobuf *b, Mimehdr *mh)
+{
+	char *sep;
+
+	if(mh == nil)
+		return Bprint(b, "NIL");
+	sep = "(";
+	for(; mh != nil; mh = mh->next){
+		Bprint(b, "%s%Z %Z", sep, mh->s, mh->t);
+		sep = " ";
+	}
+	Bputc(b, ')');
+	return 0;
+}
+
+static void
+fetchext(Biobuf *b, Header *h)
+{
+	Bputc(b, ' ');
+	if(h->disposition != nil){
+		Bprint(b, "(%Z ", h->disposition->s);
+		Bmime(b, h->disposition->next);
+		Bputc(b, ')');
+	}else
+		Bprint(b, "NIL");
+	Bputc(b, ' ');
+	if(h->language != nil){
+		if(h->language->next != nil)
+			Bmime(b, h->language->next);
+		else
+			Bprint(&bout, "%Z", h->language->s);
+	}else
+		Bprint(b, "NIL");
+}
+
+void
+fetchbodystruct(Msg *m, Header *h, int extensions)
+{
+	uint len;
+	Msg *k;
+
+	if(msgismulti(h)){
+		Bputc(&bout, '(');
+		for(k = m->kids; k != nil; k = k->next)
+			fetchbodystruct(k, &k->mime, extensions);
+		if(m->kids)
+			Bputc(&bout, ' ');
+		Bprint(&bout, "%Z", h->type->t);
+		if(extensions){
+			Bputc(&bout, ' ');
+			Bmime(&bout, h->type->next);
+			fetchext(&bout, h);
+		}
+
+		Bputc(&bout, ')');
+		return;
+	}
+
+	Bputc(&bout, '(');
+	if(h->type != nil){
+		Bprint(&bout, "%Z %Z ", h->type->s, h->type->t);
+		Bmime(&bout, h->type->next);
+	}else
+		Bprint(&bout, "\"text\" \"plain\" NIL");
+
+	Bputc(&bout, ' ');
+	if(h->id != nil)
+		Bprint(&bout, "%Z", h->id->s);
+	else
+		Bprint(&bout, "NIL");
+
+	Bputc(&bout, ' ');
+	if(h->description != nil)
+		Bprint(&bout, "%Z", h->description->s);
+	else
+		Bprint(&bout, "NIL");
+
+	Bputc(&bout, ' ');
+	if(h->encoding != nil)
+		Bprint(&bout, "%Z", h->encoding->s);
+	else
+		Bprint(&bout, "NIL");
+
+	/*
+	 * this is so strange: return lengths for a body[text] response,
+	 * except in the case of a multipart message, when return lengths for a body[] response
+	 */
+	len = m->size;
+	if(h == &m->mime)
+		len += m->head.size;
+	Bprint(&bout, " %ud", len);
+
+	len = m->lines;
+	if(h == &m->mime)
+		len += m->head.lines;
+
+	if(h->type == nil || cistrcmp(h->type->s, "text") == 0)
+		Bprint(&bout, " %ud", len);
+	else if(msgis822(h)){
+		Bputc(&bout, ' ');
+		k = m;
+		if(h != &m->mime)
+			k = m->kids;
+		if(k == nil)
+			Bprint(&bout, "(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) (\"text\" \"plain\" NIL NIL NIL NIL 0 0) 0");
+		else{
+			fetchenvelope(k);
+			Bputc(&bout, ' ');
+			fetchbodystruct(k, &k->head, extensions);
+			Bprint(&bout, " %ud", len);
+		}
+	}
+
+	if(extensions){
+		Bprint(&bout, " NIL");	/* md5 */
+		fetchext(&bout, h);
+	}
+	Bputc(&bout, ')');
+}
+
+/*
+ * print a list of addresses;
+ * each address is printed as '(' personalname atdomainlist mboxname hostname ')'
+ * the atdomainlist is always NIL
+ */
+int
+Bimapaddr(Biobuf *b, Maddr *a)
+{
+	char *host, *sep;
+
+	if(a == nil)
+		return Bprint(b, "NIL");
+	Bputc(b, '(');
+	sep = "";
+	for(; a != nil; a = a->next){
+		/*
+		 * can't send NIL as hostname, since that is code for a group
+		 */
+		host = a->host? a->host: "";
+		Bprint(b, "%s(%Z NIL %Z %Z)", sep, a->personal, a->box, host);
+		sep = " ";
+	}
+	return Bputc(b, ')');
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fns.h
@@ -1,0 +1,138 @@
+/*
+ * sorted by Edit 4,/^$/|sort -bd +1
+ */
+int	Bimapaddr(Biobuf*, Maddr*);
+int	Bimapmimeparams(Biobuf*, Mimehdr*);
+int	Bnlist(Biobuf*, Nlist*, char*);
+int	Bslist(Biobuf*, Slist*, char*);
+int	Dfmt(Fmt*);
+int	δfmt(Fmt*);
+int	Ffmt(Fmt*);
+int	Xfmt(Fmt*);
+int	Zfmt(Fmt*);
+int	appendsave(char*, int , char*, Biobuf*, long, Uidplus*);
+void	bye(char*, ...);
+int	cdcreate(char*, char*, int, ulong);
+Dir	*cddirstat(char*, char*);
+int	cddirwstat(char*, char*, Dir*);
+int	cdexists(char*, char*);
+int	cdopen(char*, char*, int);
+int	cdremove(char*, char*);
+Mblock	*checkbox(Box*, int );
+void	closebox(Box*, int opened);
+void	closeimp(Box*, Mblock*);
+int	copycheck(Box*, Msg*, int uids, void*);
+int	copysaveu(Box*, Msg*, int uids, void*);
+char	*cramauth(void);
+char	*crauth(char*, char*);
+int	creatembox(char*);
+Tm	*date2tm(Tm*, char*);
+void	debuglog(char*, ...);
+char	*decfs(char*, int, char*);
+char	*decmutf7(char*, int, char*);
+int	deletemsg(Box *, Msgset*);
+void	*emalloc(ulong);
+int	emptyimp(char*);
+void	enableforwarding(void);
+char	*encfs(char*, int, char*);
+char	*encmutf7(char*, int nout, char*);
+void	*erealloc(void*, ulong);
+char	*estrdup(char*);
+int	expungemsgs(Box*, int);
+void	*ezmalloc(ulong);
+void	fetchbody(Msg*, Fetch*);
+void	fetchbodyfill(uint);
+Pair	fetchbodypart(Fetch*, uint);
+void	fetchbodystr(Fetch*, char*, uint);
+void	fetchbodystruct(Msg*, Header*, int);
+void	fetchenvelope(Msg*);
+int	fetchmsg(Box*, Msg *, int, void*);
+Msg	*fetchsect(Msg*, Fetch*);
+int	fetchseen(Box*, Msg*, int, void*);
+void	fetchstructext(Header*);
+Msg	*findmsgsect(Msg*, Nlist*);
+int	formsgs(Box*, Msgset*, uint, int, int (*)(Box*, Msg*, int, void*), void*);
+int	fqid(int, Qid*);
+void	freemsg(Box*, Msg*);
+vlong	getquota(void); 
+void	ilog(char*, ...);
+int	imap4date(Tm*, char*);
+ulong	imap4datetime(char*);
+int	imaptmp(void);
+char	*impname(char*);
+int	inmsgset(Msgset*, uint);
+int	isdotdot(char*);
+int	isprefix(char*, char*);
+int	issuffix(char*, char*);
+int	listboxes(char*, char*, char*);
+char	*loginauth(char*, char*);
+int	lsubboxes(char*, char*, char*);
+char	*maddrstr(Maddr*);
+uint	mapflag(char*);
+uint	mapint(Namedint*, char*);
+int	mblocked(void);
+void	mblockrefresh(Mblock*);
+Mblock	*mblock(void);
+char	*mboxname(char*);
+void	mbunlock(Mblock*);
+Fetch	*mkfetch(int, Fetch*);
+Slist	*mkslist(char*, Slist*);
+Store	*mkstore(int, int, int);
+int	movebox(char*, char*);
+void	msgdead(Msg*);
+int	msgfile(Msg*, char*);
+int	msginfo(Msg*);
+int	msgis822(Header*);
+int	msgismulti(Header*);
+int	msgseen(Box*, Msg*);
+uint	msgsize(Msg*);
+int	msgstruct(Msg*, int top);
+char	*mutf7str(char*);
+int	mychdir(char*);
+int	okmbox(char*);
+Box	*openbox(char*, char*, int);
+int	openlocked(char*, char*, int);
+void	parseerr(char*);
+int	parseimp(Biobuf*, Box*);
+char	*passauth(char*, char*);
+char	*plainauth(char*);
+char	*readfile(int);
+int	removembox(char*);
+int	renamebox(char*, char*, int);
+void	resetcurdir(void);
+Fetch	*revfetch(Fetch*);
+Slist	*revslist(Slist*);
+int	searchmsg(Msg*, Search*, int);
+int	searchld(Search*);
+long	selectfields(char*, long n, char*, Slist*, int);
+void	sendflags(Box*, int uids);
+void	setflags(Box*, Msg*, int f);
+void	setname(char*, ...);
+void	setupuser(AuthInfo*);
+int	storemsg(Box*, Msg*, int, void*);
+char	*strmutf7(char*);
+void	strrev(char*, char*);
+int	subscribe(char*, int);
+int	wrimp(Biobuf*, Box*);
+int	appendimp(char*, char*, int, Uidplus*);
+void	writeerr(void);
+void	writeflags(Biobuf*, Msg*, int);
+
+void	fstreeadd(Box*, Msg*);
+void	fstreedelete(Box*, Msg*);
+Msg	*fstreefind(Box*, int);
+int	fstreecmp(Avl*, Avl*);
+
+#pragma varargck argpos	bye		1
+#pragma varargck argpos	debuglog	1
+#pragma varargck argpos	imap4cmd	2
+#pragma varargck	type	"F"		char*
+#pragma varargck	type	"D"		Tm*
+#pragma varargck	type	"δ"		Tm*
+#pragma varargck	type	"X"		char*
+#pragma varargck	type	"Y"		char*
+#pragma varargck	type	"Z"		char*
+
+#define	MK(t)		((t*)emalloc(sizeof(t)))
+#define	MKZ(t)		((t*)ezmalloc(sizeof(t)))
+#define STRLEN(cs)	(sizeof(cs)-1)
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/folder.c
@@ -1,0 +1,186 @@
+#include "imap4d.h"
+
+static	Mblock	mblck = {
+.fd = -1
+};
+
+static char curdir[Pathlen];
+
+void
+resetcurdir(void)
+{
+	curdir[0] = 0;
+}
+
+int
+mychdir(char *dir)
+{
+	if(strcmp(dir, curdir) == 0)
+		return 0;
+	if(dir[0] != '/' || strlen(dir) > Pathlen)
+		return -1;
+	strcpy(curdir, dir);
+	if(chdir(dir) < 0){
+		werrstr("mychdir failed: %r");
+		return -1;
+	}
+	return 0;
+}
+
+int
+cdcreate(char *dir, char *file, int mode, ulong perm)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return create(file, mode, perm);
+}
+
+Dir*
+cddirstat(char *dir, char *file)
+{
+	if(mychdir(dir) < 0)
+		return nil;
+	return dirstat(file);
+}
+
+int
+cdexists(char *dir, char *file)
+{
+	Dir *d;
+
+	d = cddirstat(dir, file);
+	if(d == nil)
+		return 0;
+	free(d);
+	return 1;
+}
+
+int
+cddirwstat(char *dir, char *file, Dir *d)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return dirwstat(file, d);
+}
+
+int
+cdopen(char *dir, char *file, int mode)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return open(file, mode);
+}
+
+int
+cdremove(char *dir, char *file)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return remove(file);
+}
+
+/*
+ * open the one true mail lock file
+ */
+Mblock*
+mblock(void)
+{
+	if(mblck.fd >= 0)
+		bye("mail lock deadlock");
+	mblck.fd = openlocked(mboxdir, "L.mbox", OREAD);
+	if(mblck.fd >= 0)
+		return &mblck;
+	ilog("mblock: %r");
+	return nil;
+}
+
+void
+mbunlock(Mblock *ml)
+{
+	if(ml != &mblck)
+		bye("bad mail unlock");
+	if(ml->fd < 0)
+		bye("mail unlock when not locked");
+	close(ml->fd);
+	ml->fd = -1;
+}
+
+void
+mblockrefresh(Mblock *ml)
+{
+	char buf[1];
+
+	seek(ml->fd, 0, 0);
+	read(ml->fd, buf, 1);
+}
+
+int
+mblocked(void)
+{
+	return mblck.fd >= 0;
+}
+
+char*
+impname(char *name)
+{
+	char *s, buf[Pathlen];
+	int n;
+
+	encfs(buf, sizeof buf, name);
+	n = strlen(buf) + STRLEN(".imp") + 1;
+	s = binalloc(&parsebin, n, 0);
+	if(s == nil)
+		return nil;
+	snprint(s, n, "%s.imp", name);
+	return s;
+}
+
+/*
+ * massage the mailbox name into something valid
+ * eliminates all .', and ..',s, redundatant and trailing /'s.
+ */
+char *
+mboxname(char *s)
+{
+	char *ss, *p;
+
+	ss = mutf7str(s);
+	if(ss == nil)
+		return nil;
+	cleanname(ss);
+	if(!okmbox(ss))
+		return nil;
+	p = binalloc(&parsebin, Pathlen, 0);
+	return encfs(p, Pathlen, ss);
+}
+
+char*
+strmutf7(char *s)
+{
+	char *m;
+	int n;
+
+	n = strlen(s) * Mutf7max + 1;
+	m = binalloc(&parsebin, n, 0);
+	if(m == nil)
+		return nil;
+	return encmutf7(m, n, s);
+}
+
+char*
+mutf7str(char *s)
+{
+	char *m;
+	int n;
+
+	/*
+	 * n = strlen(s) * UTFmax / (2.67) + 1
+	 * UTFmax / 2.67 == 3 / (8/3) == 9 / 8
+	 */
+	n = strlen(s);
+	n = (n * 9 + 7) / 8 + 1;
+	m = binalloc(&parsebin, n, 0);
+	if(m == nil)
+		return nil;
+	return decmutf7(m, n, s);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fsenc.c
@@ -1,0 +1,98 @@
+/*
+ * more regrettable, goofy processing
+ */
+#include "imap4d.h"
+
+char tab[0x7f] = {
+['\t']	'0',
+[' ']	'#',
+['#']	'1',
+};
+
+char itab[0x7f] = {
+['0']	'\t',
+['#']	' ',
+['1']	'#',
+};
+
+char*
+encfs(char *buf, int n, char *s)
+{
+	char *p, c;
+
+	if(!s){
+		*buf = 0;
+		return 0;
+	}
+	if(!cistrcmp(s, "inbox"))
+		s = "mbox";
+	for(p = buf; n > 0 && (c = *s++); n--){
+		if(tab[c & 0x7f]){
+			if(n < 1)
+				break;
+			if((c = tab[c]) == 0)
+				break;
+			*p++ = '#';
+		}
+		*p++ = c;
+	}
+	*p = 0;
+	return buf;
+}
+
+char*
+decfs(char *buf, int n, char *s)
+{
+	char *p, c;
+
+	if(!s){
+		*buf = 0;
+		return 0;
+	}
+	if(!cistrcmp(s, "mbox"))
+		s = "INBOX";
+	for(p = buf; n > 0 && (c = *s++); n--){
+		if(c == '#'){
+			c = *s++;
+			if((c = itab[c]) == 0)
+				break;
+		}
+		*p++ = c;
+	}
+	*p = 0;
+	return buf;
+}
+
+/*
+void
+usage(void)
+{
+	fprint(2, "usage: encfs [-d] ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char buf[1024];
+	int dflag;
+	char *(*f)(char*, int, char*);
+
+	dflag = 0;
+	ARGBEGIN{
+	case 'd':
+		dflag ^= 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+	f = encfs;
+	if(dflag)
+		f = decfs;
+	while(*argv){
+		f(buf, sizeof buf, *argv++);
+		print("%s\n", buf);
+	}
+	exits("");
+}
+*/
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fstree.c
@@ -1,0 +1,58 @@
+#include "imap4d.h"
+
+int
+fstreecmp(Avl *va, Avl *vb)
+{
+	int i;
+	Fstree *a, *b;
+
+	a = (Fstree*)va;
+	b = (Fstree*)vb;
+	i = a->m->id - b->m->id;
+	if(i > 0)
+		i = 1;
+	if(i < 0)
+		i = -1;
+	return i;
+}
+
+Msg*
+fstreefind(Box *mb, int id)
+{
+	Msg m0;
+	Fstree t, *p;
+
+	memset(&t, 0, sizeof t);
+	m0.id = id;
+	t.m = &m0;
+	if(p = (Fstree*)avllookup(mb->fstree, &t))
+		return p->m;
+	return nil;
+}
+
+void
+fstreeadd(Box *mb, Msg *m)
+{
+	Avl *old;
+	Fstree *p;
+
+	assert(m->id > 0);
+	p = ezmalloc(sizeof *p);
+	p->m = m;
+	old = avlinsert(mb->fstree, p);
+	assert(old == 0);
+}
+
+void
+fstreedelete(Box *mb, Msg *m)
+{
+	Fstree t, *p;
+
+	memset(&t, 0, sizeof t);
+	t.m = m;
+	assert(m->id > 0);
+	p = (Fstree*)avldelete(mb->fstree, &t);
+	if(p == nil)
+		_assert("fstree delete fails");
+	free(p);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/imap4d.c
@@ -1,0 +1,2292 @@
+#include "imap4d.h"
+
+/*
+ * these should be in libraries
+ */
+char	*csquery(char *attr, char *val, char *rattr);
+
+/*
+ * implemented:
+ * /lib/rfc/rfc3501	imap4rev1
+ * /lib/rfc/rfc2683	implementation advice
+ * /lib/rfc/rfc2342	namespace capability
+ * /lib/rfc/rfc2222	security protocols
+ * /lib/rfc/rfc1731	security protocols
+ * /lib/rfc/rfc2177	idle capability
+ * /lib/rfc/rfc2195	cram-md5 authentication
+ * /lib/rfc/rfc4315	uidplus capability
+ *
+ * not implemented, priority:
+ * /lib/rfc/rfc5256	sort and thread
+ *	requires missing support from upas/fs.
+ *
+ * not implemented, low priority:
+ * /lib/rfc/rfc2088	literal+ capability
+ * /lib/rfc/rfc2221	login-referrals
+ * /lib/rfc/rfc2193	mailbox-referrals
+ * /lib/rfc/rfc1760	s/key authentication
+ *
+ */
+
+typedef struct	Parsecmd	Parsecmd;
+struct Parsecmd
+{
+	char	*name;
+	void	(*f)(char*, char*);
+};
+
+static	void	appendcmd(char*, char*);
+static	void	authenticatecmd(char*, char*);
+static	void	capabilitycmd(char*, char*);
+static	void	closecmd(char*, char*);
+static	void	copycmd(char*, char*);
+static	void	createcmd(char*, char*);
+static	void	deletecmd(char*, char*);
+static	void	expungecmd(char*, char*);
+static	void	fetchcmd(char*, char*);
+static	void	getquotacmd(char*, char*);
+static	void	getquotarootcmd(char*, char*);
+static	void	idlecmd(char*, char*);
+static	void	listcmd(char*, char*);
+static	void	logincmd(char*, char*);
+static	void	logoutcmd(char*, char*);
+static	void	namespacecmd(char*, char*);
+static	void	noopcmd(char*, char*);
+static	void	renamecmd(char*, char*);
+static	void	searchcmd(char*, char*);
+static	void	selectcmd(char*, char*);
+static	void	setquotacmd(char*, char*);
+static	void	statuscmd(char*, char*);
+static	void	storecmd(char*, char*);
+static	void	subscribecmd(char*, char*);
+static	void	uidcmd(char*, char*);
+static	void	unsubscribecmd(char*, char*);
+static	void	xdebugcmd(char*, char*);
+static	void	copyucmd(char*, char*, int);
+static	void	fetchucmd(char*, char*, int);
+static	void	searchucmd(char*, char*, int);
+static	void	storeucmd(char*, char*, int);
+
+static	void	imap4(int);
+static	void	status(int expungeable, int uids);
+static	void	cleaner(void);
+static	void	check(void);
+static	int	catcher(void*, char*);
+
+static	Search	*searchkey(int first);
+static	Search	*searchkeys(int first, Search *tail);
+static	char	*astring(void);
+static	char	*atomstring(char *disallowed, char *initial);
+static	char	*atom(void);
+static	void	clearcmd(void);
+static	char	*command(void);
+static	void	crnl(void);
+static	Fetch	*fetchatt(char *s, Fetch *f);
+static	Fetch	*fetchwhat(void);
+static	int	flaglist(void);
+static	int	flags(void);
+static	int	getc(void);
+static	char	*listmbox(void);
+static	char	*literal(void);
+static	uint	litlen(void);
+static	Msgset	*msgset(int);
+static	void	mustbe(int c);
+static	uint	number(int nonzero);
+static	int	peekc(void);
+static	char	*quoted(void);
+static	void	secttext(Fetch *, int);
+static	uint	seqno(void);
+static	Store	*storewhat(void);
+static	char	*tag(void);
+static	uint	uidno(void);
+static	void	ungetc(void);
+
+static	Parsecmd	Snonauthed[] =
+{
+	{"capability",		capabilitycmd},
+	{"logout",		logoutcmd},
+	{"noop",		noopcmd},
+	{"x-exit",		logoutcmd},
+
+	{"authenticate",	authenticatecmd},
+	{"login",		logincmd},
+
+	nil
+};
+
+static	Parsecmd	Sauthed[] =
+{
+	{"capability",		capabilitycmd},
+	{"logout",		logoutcmd},
+	{"noop",		noopcmd},
+	{"x-exit",		logoutcmd},
+	{"xdebug",		xdebugcmd},
+
+	{"append",		appendcmd},
+	{"create",		createcmd},
+	{"delete",		deletecmd},
+	{"examine",		selectcmd},
+	{"select",		selectcmd},
+	{"idle",		idlecmd},
+	{"list",		listcmd},
+	{"lsub",		listcmd},
+	{"namespace",		namespacecmd},
+	{"rename",		renamecmd},
+	{"setquota",		setquotacmd},
+	{"getquota",		getquotacmd},
+	{"getquotaroot",		getquotarootcmd},
+	{"status",		statuscmd},
+	{"subscribe",		subscribecmd},
+	{"unsubscribe",		unsubscribecmd},
+
+	nil
+};
+
+static	Parsecmd	Sselected[] =
+{
+	{"capability",		capabilitycmd},
+	{"xdebug",		xdebugcmd},
+	{"logout",		logoutcmd},
+	{"x-exit",		logoutcmd},
+	{"noop",		noopcmd},
+
+	{"append",		appendcmd},
+	{"create",		createcmd},
+	{"delete",		deletecmd},
+	{"examine",		selectcmd},
+	{"select",		selectcmd},
+	{"idle",		idlecmd},
+	{"list",		listcmd},
+	{"lsub",		listcmd},
+	{"namespace",		namespacecmd},
+	{"rename",		renamecmd},
+	{"status",		statuscmd},
+	{"subscribe",		subscribecmd},
+	{"unsubscribe",		unsubscribecmd},
+
+	{"check",		noopcmd},
+	{"close",		closecmd},
+	{"copy",		copycmd},
+	{"expunge",		expungecmd},
+	{"fetch",		fetchcmd},
+	{"search",		searchcmd},
+	{"store",		storecmd},
+	{"uid",			uidcmd},
+
+	nil
+};
+
+static	char		*atomstop = "(){%*\"\\";
+static	Parsecmd	*imapstate;
+static	jmp_buf		parsejmp;
+static	char		*parsemsg;
+static	int		allowpass;
+static	int		allowcr;
+static	int		exiting;
+static	QLock		imaplock;
+static	int		idlepid = -1;
+
+Biobuf	bout;
+Biobuf	bin;
+char	username[Userlen];
+char	mboxdir[Pathlen];
+char	*servername;
+char	*site;
+char	*remote;
+char	*binupas;
+Box	*selected;
+Bin	*parsebin;
+int	debug;
+Uidplus	*uidlist;
+Uidplus	**uidtl;
+
+void
+usage(void)
+{
+	fprint(2, "usage: upas/imap4d [-acpv] [-l logfile] [-b binupas] [-d site] [-r remotehost] [-s servername]\n");
+	bye("usage");
+}
+
+void
+main(int argc, char *argv[])
+{
+	int preauth;
+
+	Binit(&bin, dup(0, -1), OREAD);
+	close(0);
+	Binit(&bout, 1, OWRITE);
+	quotefmtinstall();
+	fmtinstall('F', Ffmt);
+	fmtinstall('D', Dfmt);	/* rfc822; # imap date %Z */
+	fmtinstall(L'δ', Dfmt);	/* rfc822; # imap date %s */
+	fmtinstall('X', Xfmt);
+	fmtinstall('Y', Zfmt);
+	fmtinstall('Z', Zfmt);
+
+	preauth = 0;
+	allowpass = 0;
+	allowcr = 0;
+	ARGBEGIN{
+	case 'a':
+		preauth = 1;
+		break;
+	case 'b':
+		binupas = EARGF(usage());
+		break;
+	case 'c':
+		allowcr = 1;
+		break;
+	case 'd':
+		site = EARGF(usage());
+		break;
+	case 'l':
+		snprint(logfile, sizeof logfile, "%s", EARGF(usage()));
+		break;
+	case 'p':
+		allowpass = 1;
+		break;
+	case 'r':
+		remote = EARGF(usage());
+		break;
+	case 's':
+		servername = EARGF(usage());
+		break;
+	case 'v':
+		debug ^= 1;
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND
+
+	if(allowpass && allowcr){
+		fprint(2, "imap4d: -c and -p are mutually exclusive\n");
+		usage();
+	}
+
+	if(preauth)
+		setupuser(nil);
+
+	if(servername == nil){
+		servername = csquery("sys", sysname(), "dom");
+		if(servername == nil)
+			servername = sysname();
+		if(servername == nil){
+			fprint(2, "ip/imap4d can't find server name: %r\n");
+			bye("can't find system name");
+		}
+	}
+	if(site == nil)
+		site = getenv("site");
+	if(site == nil){
+		site = strchr(servername, '.');
+		if(site)
+			site++;
+		else
+			site = servername;
+	}
+
+	rfork(RFNOTEG|RFREND);
+
+	atnotify(catcher, 1);
+	qlock(&imaplock);
+	atexit(cleaner);
+	imap4(preauth);
+}
+
+static void
+imap4(int preauth)
+{
+	char *volatile tg;
+	char *volatile cmd;
+	Parsecmd *st;
+
+	if(preauth){
+		Bprint(&bout, "* preauth %s IMAP4rev1 server ready user %s authenticated\r\n", servername, username);
+		imapstate = Sauthed;
+	}else{
+		Bprint(&bout, "* OK %s IMAP4rev1 server ready\r\n", servername);
+		imapstate = Snonauthed;
+	}
+	if(Bflush(&bout) < 0)
+		writeerr();
+
+	tg = nil;
+	cmd = nil;
+	if(setjmp(parsejmp)){
+		if(tg == nil)
+			Bprint(&bout, "* bad empty command line: %s\r\n", parsemsg);
+		else if(cmd == nil)
+			Bprint(&bout, "%s BAD no command: %s\r\n", tg, parsemsg);
+		else
+			Bprint(&bout, "%s BAD %s %s\r\n", tg, cmd, parsemsg);
+		clearcmd();
+		if(Bflush(&bout) < 0)
+			writeerr();
+		binfree(&parsebin);
+	}
+	for(;;){
+		if(mblocked())
+			bye("internal error: mailbox lock held");
+		tg = nil;
+		cmd = nil;
+		tg = tag();
+		mustbe(' ');
+		cmd = atom();
+
+		/*
+		 * note: outlook express is broken: it requires echoing the
+		 * command as part of matching response
+		 */
+		for(st = imapstate; st->name != nil; st++)
+			if(cistrcmp(cmd, st->name) == 0){
+				st->f(tg, cmd);
+				break;
+			}
+		if(st->name == nil){
+			clearcmd();
+			Bprint(&bout, "%s BAD %s illegal command\r\n", tg, cmd);
+		}
+
+		if(Bflush(&bout) < 0)
+			writeerr();
+		binfree(&parsebin);
+	}
+}
+
+void
+bye(char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	Bprint(&bout, "* bye ");
+	Bvprint(&bout, fmt, arg);
+	Bprint(&bout, "\r\n");
+	Bflush(&bout);
+	exits(0);
+}
+
+void
+parseerr(char *msg)
+{
+	debuglog("parse error: %s", msg);
+	parsemsg = msg;
+	longjmp(parsejmp, 1);
+}
+
+/*
+ * an error occured while writing to the client
+ */
+void
+writeerr(void)
+{
+	cleaner();
+	_exits("connection closed");
+}
+
+static int
+catcher(void *, char *msg)
+{
+	if(strstr(msg, "closed pipe") != nil)
+		return 1;
+	return 0;
+}
+
+/*
+ * wipes out the idlecmd backgroung process if it is around.
+ * this can only be called if the current proc has qlocked imaplock.
+ * it must be the last piece of imap4d code executed.
+ */
+static void
+cleaner(void)
+{
+	int i;
+
+	debuglog("cleaner");
+	if(idlepid < 0)
+		return;
+	exiting = 1;
+	close(0);
+	close(1);
+	close(2);
+	close(bin.fid);
+	bin.fid = -1;
+	/*
+	 * the other proc is either stuck in a read, a sleep,
+	 * or is trying to lock imap4lock.
+	 * get him out of it so he can exit cleanly
+	 */
+	qunlock(&imaplock);
+	for(i = 0; i < 4; i++)
+		postnote(PNGROUP, getpid(), "die");
+}
+
+/*
+ * send any pending status updates to the client
+ * careful: shouldn't exit, because called by idle polling proc
+ *
+ * can't always send pending info
+ * in particular, can't send expunge info
+ * in response to a fetch, store, or search command.
+ * 
+ * rfc2060 5.2:	server must send mailbox size updates
+ * rfc2060 5.2:	server may send flag updates
+ * rfc2060 5.5:	servers prohibited from sending expunge while fetch, store, search in progress
+ * rfc2060 7:	in selected state, server checks mailbox for new messages as part of every command
+ * 		sends untagged EXISTS and RECENT respsonses reflecting new size of the mailbox
+ * 		should also send appropriate untagged FETCH and EXPUNGE messages if another agent
+ * 		changes the state of any message flags or expunges any messages
+ * rfc2060 7.4.1	expunge server response must not be sent when no command is in progress,
+ * 		nor while responding to a fetch, stort, or search command (uid versions are ok)
+ * 		command only "in progress" after entirely parsed.
+ *
+ * strategy for third party deletion of messages or of a mailbox
+ *
+ * deletion of a selected mailbox => act like all message are expunged
+ *	not strictly allowed by rfc2180, but close to method 3.2.
+ *
+ * renaming same as deletion
+ *
+ * copy
+ *	reject iff a deleted message is in the request
+ *
+ * search, store, fetch operations on expunged messages
+ *	ignore the expunged messages
+ *	return tagged no if referenced
+ */
+static void
+status(int expungeable, int uids)
+{
+	int tell;
+
+	if(!selected)
+		return;
+	tell = 0;
+	if(expungeable)
+		tell = expungemsgs(selected, 1);
+	if(selected->sendflags)
+		sendflags(selected, uids);
+	if(tell || selected->toldmax != selected->max){
+		Bprint(&bout, "* %ud EXISTS\r\n", selected->max);
+		selected->toldmax = selected->max;
+	}
+	if(tell || selected->toldrecent != selected->recent){
+		Bprint(&bout, "* %ud RECENT\r\n", selected->recent);
+		selected->toldrecent = selected->recent;
+	}
+	if(tell)
+		closeimp(selected, checkbox(selected, 1));
+}
+
+/*
+ * careful: can't exit, because called by idle polling proc
+ */
+static void
+check(void)
+{
+	if(!selected)
+		return;
+	checkbox(selected, 0);
+	status(1, 0);
+}
+
+static void
+appendcmd(char *tg, char *cmd)
+{
+	char *mbox, head[128];
+	uint t, n, now;
+	int flags, ok;
+	Uidplus u;
+
+	mustbe(' ');
+	mbox = astring();
+	mustbe(' ');
+	flags = 0;
+	if(peekc() == '('){
+		flags = flaglist();
+		mustbe(' ');
+	}
+	now = time(nil);
+	if(peekc() == '"'){
+		t = imap4datetime(quoted());
+		if(t == ~0)
+			parseerr("illegal date format");
+		mustbe(' ');
+		if(t > now)
+			t = now;
+	}else
+		t = now;
+	n = litlen();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		check();
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+	/* bug.  this is upas/fs's job */
+	if(!cdexists(mboxdir, mbox)){
+		check();
+		Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd);
+		return;
+	}
+
+	snprint(head, sizeof head, "From %s %s", username, ctime(t));
+	ok = appendsave(mbox, flags, head, &bin, n, &u);
+	crnl();
+	check();
+	if(ok)
+		Bprint(&bout, "%s OK [APPENDUID %ud %ud] %s completed\r\n",
+			tg, u.uidvalidity, u.uid, cmd);
+	else
+		Bprint(&bout, "%s NO %s message save failed\r\n", tg, cmd);
+}
+
+static void
+authenticatecmd(char *tg, char *cmd)
+{
+	char *s, *t;
+
+	mustbe(' ');
+	s = atom();
+	if(cistrcmp(s, "cram-md5") == 0){
+		crnl();
+		t = cramauth();
+		if(t == nil){
+			Bprint(&bout, "%s OK %s\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}else
+			Bprint(&bout, "%s NO %s failed %s\r\n", tg, cmd, t);
+	}else if(cistrcmp(s, "plain") == 0){
+		s = nil;
+		if(peekc() == ' '){
+			mustbe(' ');
+			s = astring();
+		}
+		crnl();
+		if(!allowpass)
+			Bprint(&bout, "%s NO %s plaintext passwords disallowed\r\n", tg, cmd);
+		else if(t = plainauth(s))
+			Bprint(&bout, "%s NO %s failed %s\r\n", tg, cmd, t);
+		else{
+			Bprint(&bout, "%s OK %s\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}
+	}else
+		Bprint(&bout, "%s NO %s unsupported authentication protocol\r\n", tg, cmd);
+}
+
+static void
+capabilitycmd(char *tg, char *cmd)
+{
+	crnl();
+	check();
+	Bprint(&bout, "* CAPABILITY IMAP4REV1 IDLE NAMESPACE QUOTA XDEBUG");
+	Bprint(&bout, " UIDPLUS");
+	if(allowpass || allowcr)
+		Bprint(&bout, " AUTH=CRAM-MD5 AUTH=PLAIN");
+	else
+		Bprint(&bout, " LOGINDISABLED AUTH=CRAM-MD5");
+	Bprint(&bout, "\r\n%s OK %s\r\n", tg, cmd);
+}
+
+static void
+closecmd(char *tg, char *cmd)
+{
+	crnl();
+	imapstate = Sauthed;
+	closebox(selected, 1);
+	selected = nil;
+	Bprint(&bout, "%s OK %s mailbox closed, now in authenticated state\r\n", tg, cmd);
+}
+
+/*
+ * note: message id's are before any pending expunges
+ */
+static void
+copycmd(char *tg, char *cmd)
+{
+	copyucmd(tg, cmd, 0);
+}
+
+static char *uidpsep;
+static int
+printuid(Box*, Msg *m, int, void*)
+{
+	Bprint(&bout, "%s%ud", uidpsep, m->uid);
+	uidpsep = ",";
+	return 1;
+}
+
+static void
+copyucmd(char *tg, char *cmd, int uids)
+{
+	char *uid, *mbox;
+	int ok;
+	uint max;
+	Msgset *ms;
+	Uidplus *u;
+
+	mustbe(' ');
+	ms = msgset(uids);
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+
+	uid = "";
+	if(uids)
+		uid = "UID ";
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		status(1, uids);
+		Bprint(&bout, "%s NO %s%s bad mailbox\r\n", tg, uid, cmd);
+		return;
+	}
+	if(!cdexists(mboxdir, mbox)){
+		check();
+		Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd);
+		return;
+	}
+
+	uidlist = 0;
+	uidtl = &uidlist;
+
+	max = selected->max;
+	checkbox(selected, 0);
+	ok = formsgs(selected, ms, max, uids, copycheck, nil);
+	if(ok)
+		ok = formsgs(selected, ms, max, uids, copysaveu, mbox);
+	status(1, uids);
+	if(ok && uidlist){
+		u = uidlist;
+		Bprint(&bout, "%s OK [COPYUID %ud", tg, u->uidvalidity);
+		uidpsep = " ";
+		formsgs(selected, ms, max, uids, printuid, mbox);
+		Bprint(&bout, " %ud", u->uid);
+		for(u = u->next; u; u = u->next)
+			Bprint(&bout, ",%ud", u->uid);
+		Bprint(&bout, "] %s%s completed\r\n", uid, cmd);
+	}else if(ok)
+		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+	else
+		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
+}
+
+static void
+createcmd(char *tg, char *cmd)
+{
+	char *mbox;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+	if(cistrcmp(mbox, "mbox") == 0){
+		Bprint(&bout, "%s NO %s cannot remotely create INBOX\r\n", tg, cmd);
+		return;
+	}
+	if(creatembox(mbox) == -1)
+		Bprint(&bout, "%s NO %s cannot create mailbox %#Y\r\n", tg, cmd, mbox);
+	else
+		Bprint(&bout, "%s OK %#Y %s completed\r\n", tg, mbox, cmd);
+}
+
+static void
+xdebugcmd(char *tg, char *)
+{
+	char *s, *t;
+
+	mustbe(' ');
+	s = astring();
+	t = 0;
+	if(!cistrcmp(s, "file")){
+		mustbe(' ');
+		t = astring();
+	}
+	crnl();
+	check();
+	if(!cistrcmp(s, "on") || !cistrcmp(s, "1")){
+		Bprint(&bout, "%s OK debug on\r\n", tg);
+		debug = 1;
+	}else if(!cistrcmp(s, "file")){
+		if(!strstr(t, ".."))
+			snprint(logfile, sizeof logfile, "%s", t);
+		Bprint(&bout, "%s OK debug file %#Z\r\n", tg, logfile);
+	}else{
+		Bprint(&bout, "%s OK debug off\r\n", tg);
+		debug = 0;
+	}
+}
+
+static void
+deletecmd(char *tg, char *cmd)
+{
+	char *mbox;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+
+	/*
+	 * i don't know if this is a hack or not.  a delete of the
+	 * currently-selected box seems fishy.  the standard doesn't
+	 * specify any behavior.
+	 */
+	if(selected && strcmp(selected->name, mbox) == 0){
+		ilog("delete: client bug? close of selected mbox %s", selected->fs);
+		imapstate = Sauthed;
+		closebox(selected, 1);
+		selected = nil;
+		setname("[none]");
+	}
+
+	if(!cistrcmp(mbox, "mbox") || !removembox(mbox) == -1)
+		Bprint(&bout, "%s NO %s cannot delete mailbox %#Y\r\n", tg, cmd, mbox);
+	else
+		Bprint(&bout, "%s OK %#Y %s completed\r\n", tg, mbox, cmd);
+}
+
+static void
+expungeucmd(char *tg, char *cmd, int uids)
+{
+	int ok;
+	Msgset *ms;
+
+	ms = 0;
+	if(uids){
+		mustbe(' ');
+		ms = msgset(uids);
+	}
+	crnl();
+	ok = deletemsg(selected, ms);
+	check();
+	if(ok)
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s NO %s some messages not expunged\r\n", tg, cmd);
+}
+
+static void
+expungecmd(char *tg, char *cmd)
+{
+	expungeucmd(tg, cmd, 0);
+}
+
+static void
+fetchcmd(char *tg, char *cmd)
+{
+	fetchucmd(tg, cmd, 0);
+}
+
+static void
+fetchucmd(char *tg, char *cmd, int uids)
+{
+	char *uid;
+	int ok;
+	uint max;
+	Fetch *f;
+	Msgset *ms;
+	Mblock *ml;
+
+	mustbe(' ');
+	ms = msgset(uids);
+	mustbe(' ');
+	f = fetchwhat();
+	crnl();
+	uid = "";
+	if(uids)
+		uid = "uid ";
+	max = selected->max;
+	ml = checkbox(selected, 1);
+	if(ml != nil)
+		formsgs(selected, ms, max, uids, fetchseen, f);
+	closeimp(selected, ml);
+	ok = ml != nil && formsgs(selected, ms, max, uids, fetchmsg, f);
+	status(uids, uids);
+	if(ok)
+		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+	else{
+		if(ml == nil)
+			ilog("nil maillock\n");
+		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
+	}
+}
+
+static void
+idlecmd(char *tg, char *cmd)
+{
+	int c, pid;
+
+	crnl();
+	Bprint(&bout, "+ idling, waiting for done\r\n");
+	if(Bflush(&bout) < 0)
+		writeerr();
+
+	if(idlepid < 0){
+		pid = rfork(RFPROC|RFMEM|RFNOWAIT);
+		if(pid == 0){
+			setname("imap idle");
+			for(;;){
+				qlock(&imaplock);
+				if(exiting)
+					break;
+
+				/*
+				 * parent may have changed curdir, but it doesn't change our .
+				 */
+				resetcurdir();
+
+				check();
+				if(Bflush(&bout) < 0)
+					writeerr();
+				qunlock(&imaplock);
+				sleep(15*1000);
+				enableforwarding();
+			}
+			_exits(0);
+		}
+		idlepid = pid;
+	}
+
+	qunlock(&imaplock);
+
+	/*
+	 * clear out the next line, which is supposed to contain (case-insensitive)
+	 * done\n
+	 * this is special code since it has to dance with the idle polling proc
+	 * and handle exiting correctly.
+	 */
+	for(;;){
+		c = getc();
+		if(c < 0){
+			qlock(&imaplock);
+			if(!exiting)
+				cleaner();
+			_exits(0);
+		}
+		if(c == '\n')
+			break;
+	}
+
+	qlock(&imaplock);
+	if(exiting)
+		_exits(0);
+
+	/*
+	 * child may have changed curdir, but it doesn't change our .
+	 */
+	resetcurdir();
+	check();
+	Bprint(&bout, "%s OK %s terminated\r\n", tg, cmd);
+}
+
+static void
+listcmd(char *tg, char *cmd)
+{
+	char *s, *t, *ref, *mbox;
+
+	mustbe(' ');
+	s = astring();
+	mustbe(' ');
+	t = listmbox();
+	crnl();
+	check();
+	ref = mutf7str(s);
+	mbox = mutf7str(t);
+	if(ref == nil || mbox == nil){
+		Bprint(&bout, "%s BAD %s modified utf-7\r\n", tg, cmd);
+		return;
+	}
+
+	/*
+	 * special request for hierarchy delimiter and root name
+	 * root name appears to be name up to and including any delimiter,
+	 * or the empty string, if there is no delimiter.
+	 *
+	 * this must change if the # namespace convention is supported.
+	 */
+	if(*mbox == '\0'){
+		s = strchr(ref, '/');
+		if(s == nil)
+			ref = "";
+		else
+			s[1] = '\0';
+		Bprint(&bout, "* %s (\\Noselect) \"/\" \"%s\"\r\n", cmd, ref);
+		Bprint(&bout, "%s OK %s\r\n", tg, cmd);
+		return;
+	}
+
+	/*
+	 * hairy exception: these take non-fsencoded strings.  BUG?
+	 */
+	if(cistrcmp(cmd, "lsub") == 0)
+		lsubboxes(cmd, ref, mbox);
+	else
+		listboxes(cmd, ref, mbox);
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+logincmd(char *tg, char *cmd)
+{
+	char *r, *s, *t;
+
+	mustbe(' ');
+	s = astring();	/* uid */
+	mustbe(' ');
+	t = astring();	/* password */
+	crnl();
+	if(allowcr){
+		if(r = crauth(s, t)){
+			Bprint(&bout, "* NO [ALERT] %s\r\n", r);
+			Bprint(&bout, "%s NO %s succeeded\r\n", tg, cmd);
+		}else{
+			Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}
+		return;
+	}else if(allowpass){
+		if(r = passauth(s, t))
+			Bprint(&bout, "%s NO %s failed check [%s]\r\n", tg, cmd, r);
+		else{
+			Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}
+		return;
+	}
+	Bprint(&bout, "%s NO %s plaintext passwords disallowed\r\n", tg, cmd);
+}
+
+/*
+ * logout or x-exit, which doesn't expunge the mailbox
+ */
+static void
+logoutcmd(char *tg, char *cmd)
+{
+	crnl();
+
+	if(cmd[0] != 'x' && selected){
+		closebox(selected, 1);
+		selected = nil;
+	}
+	Bprint(&bout, "* bye\r\n");
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	exits(0);
+}
+
+static void
+namespacecmd(char *tg, char *cmd)
+{
+	crnl();
+	check();
+
+	/*
+	 * personal, other users, shared namespaces
+	 * send back nil or descriptions of (prefix heirarchy-delim) for each case
+	 */
+	Bprint(&bout, "* NAMESPACE ((\"\" \"/\")) nil nil\r\n");
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+noopcmd(char *tg, char *cmd)
+{
+	crnl();
+	check();
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	enableforwarding();
+}
+
+static void
+getquota0(char *tg, char *cmd, char *r)
+{
+extern vlong getquota(void);
+	vlong v;
+
+	if(r[0]){
+		Bprint(&bout, "%s NO %s no such quota root\r\n", tg, cmd);
+		return;
+	}
+	v = getquota();
+	if(v == -1){
+		Bprint(&bout, "%s NO %s bad [%r]\r\n", tg, cmd);
+		return;
+	}
+	Bprint(&bout, "* %s "" (storage %llud %d)\r\n", cmd, v/1024, 256*1024);
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+getquotacmd(char *tg, char *cmd)
+{
+	char *r;
+
+	mustbe(' ');
+	r = astring();
+	crnl();
+	check();
+	getquota0(tg, cmd, r);
+}
+
+static void
+getquotarootcmd(char *tg, char *cmd)
+{
+	char *r;
+
+	mustbe(' ');
+	r = astring();
+	crnl();
+	check();
+
+	Bprint(&bout, "* %s %s \"\"\r\n", cmd, r);
+	getquota0(tg, cmd, "");
+}
+
+static void
+setquotacmd(char *tg, char *cmd)
+{
+	mustbe(' ');
+	astring();
+	mustbe(' ');
+	mustbe('(');
+	for(;;){
+		astring();
+		mustbe(' ');
+		number(0);
+		if(peekc() == ')')
+			break;
+	}
+	getc();
+	crnl();
+	check();
+	Bprint(&bout, "%s NO %s error: can't set that data\r\n", tg, cmd);
+}
+
+/*
+ * this is only a partial implementation
+ * should copy files to other directories,
+ * and copy & truncate inbox
+ */
+static void
+renamecmd(char *tg, char *cmd)
+{
+	char *from, *to;
+
+	mustbe(' ');
+	from = astring();
+	mustbe(' ');
+	to = astring();
+	crnl();
+	check();
+
+	to = mboxname(to);
+	if(to == nil || cistrcmp(to, "mbox") == 0){
+		Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd);
+		return;
+	}
+	if(access(to, AEXIST) >= 0){
+		Bprint(&bout, "%s NO %s mailbox already exists\r\n", tg, cmd);
+		return;
+	}
+	from = mboxname(from);
+	if(from == nil){
+		Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd);
+		return;
+	}
+	if(renamebox(from, to, strcmp(from, "mbox")))
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s NO %s failed\r\n", tg, cmd);
+}
+
+static void
+searchcmd(char *tg, char *cmd)
+{
+	searchucmd(tg, cmd, 0);
+}
+
+/*
+ * mail.app has a vicious habit of appending a message to
+ * a folder and then immediately searching for it by message-id.
+ * for a 10,000 message sent folder, this can be quite painful.
+ *
+ * evil strategy.  for message-id searches, check the last
+ * message in the mailbox!  if that fails, use the normal algorithm.
+ */
+static Msg*
+mailappsucks(Search *s)
+{
+	Msg *m;
+
+	if(s->key == SKuid)
+		s = s->next;
+	if(s && s->next == nil)
+	if(s->key == SKheader && cistrcmp(s->hdr, "message-id") == 0){
+		for(m = selected->msgs; m && m->next; m = m->next)
+			;
+		if(m != nil)
+		if(m->matched = searchmsg(m, s, 0))
+			return m;
+	}
+	return 0;
+}
+
+static void
+searchucmd(char *tg, char *cmd, int uids)
+{
+	char *uid;
+	uint id, ld;
+	Msg *m;
+	Search rock;
+
+	mustbe(' ');
+	rock.next = nil;
+	searchkeys(1, &rock);
+	crnl();
+	uid = "";
+	if(uids)
+		uid = "UID ";		/* android needs caps */
+	if(rock.next != nil && rock.next->key == SKcharset){
+		if(cistrcmp(rock.next->s, "utf-8") != 0
+		&& cistrcmp(rock.next->s, "us-ascii") != 0){
+			Bprint(&bout, "%s NO [BADCHARSET] (\"US-ASCII\" \"UTF-8\") %s%s failed\r\n", tg, uid, cmd);
+			checkbox(selected, 0);
+			status(uids, uids);
+			return;
+		}
+		rock.next = rock.next->next;
+	}
+	Bprint(&bout, "* search");
+	if(m = mailappsucks(rock.next))
+			goto cheat;
+	ld = searchld(rock.next);
+	for(m = selected->msgs; m != nil; m = m->next)
+		m->matched = searchmsg(m, rock.next, ld);
+	for(m = selected->msgs; m != nil; m = m->next){
+cheat:
+		if(m->matched){
+			if(uids)
+				id = m->uid;
+			else
+				id = m->seq;
+			Bprint(&bout, " %ud", id);
+		}
+	}
+	Bprint(&bout, "\r\n");
+	checkbox(selected, 0);
+	status(uids, uids);
+	Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+}
+
+static void
+selectcmd(char *tg, char *cmd)
+{
+	char *s, *m0, *mbox, buf[Pathlen];
+	Msg *m;
+
+	mustbe(' ');
+	m0 = astring();
+	crnl();
+
+	if(selected){
+		imapstate = Sauthed;
+		closebox(selected, 1);
+		selected = nil;
+		setname("[none]");
+	}
+	debuglog("select %s", m0);
+
+	mbox = mboxname(m0);
+	if(mbox == nil){
+		debuglog("select %s [%s] -> no bad", mbox, m0);
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+
+	selected = openbox(mbox, "imap", cistrcmp(cmd, "select") == 0);
+	if(selected == nil){
+		Bprint(&bout, "%s NO %s can't open mailbox %#Y: %r\r\n", tg, cmd, mbox);
+		return;
+	}
+
+	setname("%s", decfs(buf, sizeof buf, selected->name));
+	imapstate = Sselected;
+
+	Bprint(&bout, "* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n");
+	Bprint(&bout, "* %ud EXISTS\r\n", selected->max);
+	selected->toldmax = selected->max;
+	Bprint(&bout, "* %ud RECENT\r\n", selected->recent);
+	selected->toldrecent = selected->recent;
+	for(m = selected->msgs; m != nil; m = m->next){
+		if(!m->expunged && (m->flags & Fseen) != Fseen){
+			Bprint(&bout, "* OK [UNSEEN %ud]\r\n", m->seq);
+			break;
+		}
+	}
+	Bprint(&bout, "* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Draft \\Deleted)]\r\n");
+	Bprint(&bout, "* OK [UIDNEXT %ud]\r\n", selected->uidnext);
+	Bprint(&bout, "* OK [UIDVALIDITY %ud]\r\n", selected->uidvalidity);
+	s = "READ-ONLY";
+	if(selected->writable)
+		s = "READ-WRITE";
+	Bprint(&bout, "%s OK [%s] %s %#Y completed\r\n", tg, s, cmd, mbox);
+}
+
+static Namedint	statusitems[] =
+{
+	{"MESSAGES",	Smessages},
+	{"RECENT",	Srecent},
+	{"UIDNEXT",	Suidnext},
+	{"UIDVALIDITY",	Suidvalidity},
+	{"UNSEEN",	Sunseen},
+	{nil,		0}
+};
+
+static void
+statuscmd(char *tg, char *cmd)
+{
+	char *s, *mbox;
+	int si, i, opened;
+	uint v;
+	Box *box;
+	Msg *m;
+
+	mustbe(' ');
+	mbox = astring();
+	mustbe(' ');
+	mustbe('(');
+	si = 0;
+	for(;;){
+		s = atom();
+		i = mapint(statusitems, s);
+		if(i == 0)
+			parseerr("illegal status item");
+		si |= i;
+		if(peekc() == ')')
+			break;
+		mustbe(' ');
+	}
+	mustbe(')');
+	crnl();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		check();
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+
+	opened = 0;
+	if(selected && !strcmp(mbox, selected->name))
+		box = selected;
+	else{
+		box = openbox(mbox, "status", 1);
+		if(box == nil){
+			check();
+			Bprint(&bout, "%s NO [TRYCREATE] %s can't open mailbox %#Y: %r\r\n", tg, cmd, mbox);
+			return;
+		}
+		opened = 1;
+	}
+
+	Bprint(&bout, "* STATUS %#Y (", mbox);
+	s = "";
+	for(i = 0; statusitems[i].name != nil; i++)
+		if(si & statusitems[i].v){
+			v = 0;
+			switch(statusitems[i].v){
+			case Smessages:
+				v = box->max;
+				break;
+			case Srecent:
+				v = box->recent;
+				break;
+			case Suidnext:
+				v = box->uidnext;
+				break;
+			case Suidvalidity:
+				v = box->uidvalidity;
+				break;
+			case Sunseen:
+				v = 0;
+				for(m = box->msgs; m != nil; m = m->next)
+					if((m->flags & Fseen) != Fseen)
+						v++;
+				break;
+			default:
+				Bprint(&bout, ")");
+				bye("internal error: status item not implemented");
+				break;
+			}
+			Bprint(&bout, "%s%s %ud", s, statusitems[i].name, v);
+			s = " ";
+		}
+	Bprint(&bout, ")\r\n");
+	if(opened)
+		closebox(box, 1);
+
+	check();
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+storecmd(char *tg, char *cmd)
+{
+	storeucmd(tg, cmd, 0);
+}
+
+static void
+storeucmd(char *tg, char *cmd, int uids)
+{
+	char *uid;
+	int ok;
+	uint max;
+	Mblock *ml;
+	Msgset *ms;
+	Store *st;
+
+	mustbe(' ');
+	ms = msgset(uids);
+	mustbe(' ');
+	st = storewhat();
+	crnl();
+	uid = "";
+	if(uids)
+		uid = "uid ";
+	max = selected->max;
+	ml = checkbox(selected, 1);
+	ok = ml != nil && formsgs(selected, ms, max, uids, storemsg, st);
+	closeimp(selected, ml);
+	status(uids, uids);
+	if(ok)
+		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+	else
+		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
+}
+
+/*
+ * minimal implementation of subscribe
+ * all folders are automatically subscribed,
+ * and can't be unsubscribed
+ */
+static void
+subscribecmd(char *tg, char *cmd)
+{
+	char *mbox;
+	int ok;
+	Box *box;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+	mbox = mboxname(mbox);
+	ok = 0;
+	if(mbox != nil && (box = openbox(mbox, "subscribe", 0))){
+		ok = subscribe(mbox, 's');
+		closebox(box, 1);
+	}
+	if(!ok)
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+uidcmd(char *tg, char *cmd)
+{
+	char *sub;
+
+	mustbe(' ');
+	sub = atom();
+	if(cistrcmp(sub, "copy") == 0)
+		copyucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "fetch") == 0)
+		fetchucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "search") == 0)
+		searchucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "store") == 0)
+		storeucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "expunge") == 0)
+		expungeucmd(tg, sub, 1);
+	else{
+		clearcmd();
+		Bprint(&bout, "%s BAD %s illegal uid command %s\r\n", tg, cmd, sub);
+	}
+}
+
+static void
+unsubscribecmd(char *tg, char *cmd)
+{
+	char *mbox;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+	mbox = mboxname(mbox);
+	if(mbox == nil || !subscribe(mbox, 'u'))
+		Bprint(&bout, "%s NO %s can't unsubscribe\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static char *gbuf;
+static void
+badsyn(void)
+{
+	debuglog("syntax error [%s]", gbuf);
+	parseerr("bad syntax");
+}
+
+static void
+clearcmd(void)
+{
+	int c;
+
+	for(;;){
+		c = getc();
+		if(c < 0)
+			bye("end of input");
+		if(c == '\n')
+			return;
+	}
+}
+
+static void
+crnl(void)
+{
+	int c;
+
+	c = getc();
+	if(c == '\n')
+		return;
+	if(c != '\r' || getc() != '\n')
+		badsyn();
+}
+
+static void
+mustbe(int c)
+{
+	int x;
+
+	if((x = getc()) != c){
+		ungetc();
+		ilog("must be '%c' got %c", c, x);
+		badsyn();
+	}
+}
+
+/*
+ * flaglist	: '(' ')' | '(' flags ')'
+ */
+static int
+flaglist(void)
+{
+	int f;
+
+	mustbe('(');
+	f = 0;
+	if(peekc() != ')')
+		f = flags();
+
+	mustbe(')');
+	return f;
+}
+
+/*
+ * flags	: flag | flags ' ' flag
+ * flag		: '\' atom | atom
+ */
+static int
+flags(void)
+{
+	char *s;
+	int ff, flags, c;
+
+	flags = 0;
+	for(;;){
+		c = peekc();
+		if(c == '\\'){
+			mustbe('\\');
+			s = atomstring(atomstop, "\\");
+		}else if(strchr(atomstop, c) != nil)
+			s = atom();
+		else
+			break;
+		ff = mapflag(s);
+		if(ff == 0)
+			parseerr("flag not supported");
+		flags |= ff;
+		if(peekc() != ' ')
+			break;
+		mustbe(' ');
+	}
+	if(flags == 0)
+		parseerr("no flags given");
+	return flags;
+}
+
+/*
+ * storewhat	: osign 'FLAGS' ' ' storeflags
+ *		| osign 'FLAGS.SILENT' ' ' storeflags
+ * osign	:
+ *		| '+' | '-'
+ * storeflags	: flaglist | flags
+ */
+static Store*
+storewhat(void)
+{
+	char *s;
+	int c, f, w;
+
+	c = peekc();
+	if(c == '+' || c == '-')
+		mustbe(c);
+	else
+		c = 0;
+	s = atom();
+	w = 0;
+	if(cistrcmp(s, "flags") == 0)
+		w = Stflags;
+	else if(cistrcmp(s, "flags.silent") == 0)
+		w = Stflagssilent;
+	else
+		parseerr("illegal store attribute");
+	mustbe(' ');
+	if(peekc() == '(')
+		f = flaglist();
+	else
+		f = flags();
+	return mkstore(c, w, f);
+}
+
+/*
+ * fetchwhat	: "ALL" | "FULL" | "FAST" | fetchatt | '(' fetchatts ')'
+ * fetchatts	: fetchatt | fetchatts ' ' fetchatt
+ */
+static char *fetchatom	= "(){}%*\"\\[]";
+static Fetch*
+fetchwhat(void)
+{
+	char *s;
+	Fetch *f;
+
+	if(peekc() == '('){
+		getc();
+		f = nil;
+		for(;;){
+			s = atomstring(fetchatom, "");
+			f = fetchatt(s, f);
+			if(peekc() == ')')
+				break;
+			mustbe(' ');
+		}
+		getc();
+		return revfetch(f);
+	}
+
+	s = atomstring(fetchatom, "");
+	if(cistrcmp(s, "all") == 0)
+		f = mkfetch(Fflags, mkfetch(Finternaldate, mkfetch(Frfc822size, mkfetch(Fenvelope, nil))));
+	else if(cistrcmp(s, "fast") == 0)
+		f = mkfetch(Fflags, mkfetch(Finternaldate, mkfetch(Frfc822size, nil)));
+	else if(cistrcmp(s, "full") == 0)
+		f = mkfetch(Fflags, mkfetch(Finternaldate, mkfetch(Frfc822size, mkfetch(Fenvelope, mkfetch(Fbody, nil)))));
+	else
+		f = fetchatt(s, nil);
+	return f;
+}
+
+/*
+ * fetchatt	: "ENVELOPE" | "FLAGS" | "INTERNALDATE"
+ *		| "RFC822" | "RFC822.HEADER" | "RFC822.SIZE" | "RFC822.TEXT"
+ *		| "BODYSTRUCTURE"
+ *		| "UID"
+ *		| "BODY"
+ *		| "BODY" bodysubs
+ *		| "BODY.PEEK" bodysubs
+ * bodysubs	: sect
+ *		| sect '<' number '.' nz-number '>'
+ * sect		: '[' sectspec ']'
+ * sectspec	: sectmsgtext
+ *		| sectpart
+ *		| sectpart '.' secttext
+ * sectpart	: nz-number
+ *		| sectpart '.' nz-number
+ */
+Nlist*
+mknlist(void)
+{
+	Nlist *nl;
+
+	nl = binalloc(&parsebin, sizeof *nl, 1);
+	if(nl == nil)
+		parseerr("out of memory");
+	nl->n = number(1);
+	return nl;
+}
+
+static Fetch*
+fetchatt(char *s, Fetch *f)
+{
+	int c;
+	Nlist *n;
+
+	if(cistrcmp(s, "envelope") == 0)
+		return mkfetch(Fenvelope, f);
+	if(cistrcmp(s, "flags") == 0)
+		return mkfetch(Fflags, f);
+	if(cistrcmp(s, "internaldate") == 0)
+		return mkfetch(Finternaldate, f);
+	if(cistrcmp(s, "RFC822") == 0)
+		return mkfetch(Frfc822, f);
+	if(cistrcmp(s, "RFC822.header") == 0)
+		return mkfetch(Frfc822head, f);
+	if(cistrcmp(s, "RFC822.size") == 0)
+		return mkfetch(Frfc822size, f);
+	if(cistrcmp(s, "RFC822.text") == 0)
+		return mkfetch(Frfc822text, f);
+	if(cistrcmp(s, "bodystructure") == 0)
+		return mkfetch(Fbodystruct, f);
+	if(cistrcmp(s, "uid") == 0)
+		return mkfetch(Fuid, f);
+
+	if(cistrcmp(s, "body") == 0){
+		if(peekc() != '[')
+			return mkfetch(Fbody, f);
+		f = mkfetch(Fbodysect, f);
+	}else if(cistrcmp(s, "body.peek") == 0)
+		f = mkfetch(Fbodypeek, f);
+	else
+		parseerr("illegal fetch attribute");
+
+	mustbe('[');
+	c = peekc();
+	if(c >= '1' && c <= '9'){
+		n = f->sect = mknlist();
+		while(peekc() == '.'){
+			getc();
+			c = peekc();
+			if(c < '1' || c > '9')
+				break;
+			n->next = mknlist();
+			n = n->next;
+		}
+	}
+	if(peekc() != ']')
+		secttext(f, f->sect != nil);
+	mustbe(']');
+
+	if(peekc() != '<')
+		return f;
+
+	f->partial = 1;
+	mustbe('<');
+	f->start = number(0);
+	mustbe('.');
+	f->size = number(1);
+	mustbe('>');
+	return f;
+}
+
+/*
+ * secttext	: sectmsgtext | "MIME"
+ * sectmsgtext	: "HEADER"
+ *		| "TEXT"
+ *		| "HEADER.FIELDS" ' ' hdrlist
+ *		| "HEADER.FIELDS.NOT" ' ' hdrlist
+ * hdrlist	: '(' hdrs ')'
+ * hdrs:	: astring
+ *		| hdrs ' ' astring
+ */
+static void
+secttext(Fetch *f, int mimeok)
+{
+	char *s;
+	Slist *h;
+
+	s = atomstring(fetchatom, "");
+	if(cistrcmp(s, "header") == 0){
+		f->part = FPhead;
+		return;
+	}
+	if(cistrcmp(s, "text") == 0){
+		f->part = FPtext;
+		return;
+	}
+	if(mimeok && cistrcmp(s, "mime") == 0){
+		f->part = FPmime;
+		return;
+	}
+	if(cistrcmp(s, "header.fields") == 0)
+		f->part = FPheadfields;
+	else if(cistrcmp(s, "header.fields.not") == 0)
+		f->part = FPheadfieldsnot;
+	else
+		parseerr("illegal fetch section text");
+	mustbe(' ');
+	mustbe('(');
+	h = nil;
+	for(;;){
+		h = mkslist(astring(), h);
+		if(peekc() == ')')
+			break;
+		mustbe(' ');
+	}
+	mustbe(')');
+	f->hdrs = revslist(h);
+}
+
+/*
+ * searchwhat	: "CHARSET" ' ' astring searchkeys | searchkeys
+ * searchkeys	: searchkey | searchkeys ' ' searchkey
+ * searchkey	: "ALL" | "ANSWERED" | "DELETED" | "FLAGGED" | "NEW" | "OLD" | "RECENT"
+ *		| "SEEN" | "UNANSWERED" | "UNDELETED" | "UNFLAGGED" | "DRAFT" | "UNDRAFT"
+ *		| astrkey ' ' astring
+ *		| datekey ' ' date
+ *		| "KEYWORD" ' ' flag | "UNKEYWORD" flag
+ *		| "LARGER" ' ' number | "SMALLER" ' ' number
+ * 		| "HEADER" astring ' ' astring
+ *		| set | "UID" ' ' set
+ *		| "NOT" ' ' searchkey
+ *		| "OR" ' ' searchkey ' ' searchkey
+ *		| '(' searchkeys ')'
+ * astrkey	: "BCC" | "BODY" | "CC" | "FROM" | "SUBJECT" | "TEXT" | "TO"
+ * datekey	: "BEFORE" | "ON" | "SINCE" | "SENTBEFORE" | "SENTON" | "SENTSINCE"
+ */
+static Namedint searchmap[] =
+{
+	{"ALL",		SKall},
+	{"ANSWERED",	SKanswered},
+	{"DELETED",	SKdeleted},
+	{"FLAGGED",	SKflagged},
+	{"NEW",		SKnew},
+	{"OLD",		SKold},
+	{"RECENT",	SKrecent},
+	{"SEEN",	SKseen},
+	{"UNANSWERED",	SKunanswered},
+	{"UNDELETED",	SKundeleted},
+	{"UNFLAGGED",	SKunflagged},
+	{"DRAFT",	SKdraft},
+	{"UNDRAFT",	SKundraft},
+	{"UNSEEN",	SKunseen},
+	{nil,		0}
+};
+
+static Namedint searchmapstr[] =
+{
+	{"CHARSET",	SKcharset},
+	{"BCC",		SKbcc},
+	{"BODY",	SKbody},
+	{"CC",		SKcc},
+	{"FROM",	SKfrom},
+	{"SUBJECT",	SKsubject},
+	{"TEXT",	SKtext},
+	{"TO",		SKto},
+	{nil,		0}
+};
+
+static Namedint searchmapdate[] =
+{
+	{"BEFORE",	SKbefore},
+	{"ON",		SKon},
+	{"SINCE",	SKsince},
+	{"SENTBEFORE",	SKsentbefore},
+	{"SENTON",	SKsenton},
+	{"SENTSINCE",	SKsentsince},
+	{nil,		0}
+};
+
+static Namedint searchmapflag[] =
+{
+	{"KEYWORD",	SKkeyword},
+	{"UNKEYWORD",	SKunkeyword},
+	{nil,		0}
+};
+
+static Namedint searchmapnum[] =
+{
+	{"SMALLER",	SKsmaller},
+	{"LARGER",	SKlarger},
+	{nil,		0}
+};
+
+static Search*
+searchkeys(int first, Search *tail)
+{
+	Search *s;
+
+	for(;;){
+		if(peekc() == '('){
+			getc();
+			tail = searchkeys(0, tail);
+			mustbe(')');
+		}else{
+			s = searchkey(first);
+			tail->next = s;
+			tail = s;
+		}
+		first = 0;
+		if(peekc() != ' ')
+			break;
+		getc();
+	}
+	return tail;
+}
+
+static Search*
+searchkey(int first)
+{
+	char *a;
+	int i, c;
+	Search *sr, rock;
+	Tm tm;
+
+	sr = binalloc(&parsebin, sizeof *sr, 1);
+	if(sr == nil)
+		parseerr("out of memory");
+
+	c = peekc();
+	if(c >= '0' && c <= '9'){
+		sr->key = SKset;
+		sr->set = msgset(0);
+		return sr;
+	}
+
+	a = atom();
+	if(i = mapint(searchmap, a))
+		sr->key = i;
+	else if(i = mapint(searchmapstr, a)){
+		if(!first && i == SKcharset)
+			parseerr("illegal search key");
+		sr->key = i;
+		mustbe(' ');
+		sr->s = astring();
+	}else if(i = mapint(searchmapdate, a)){
+		sr->key = i;
+		mustbe(' ');
+		c = peekc();
+		if(c == '"')
+			getc();
+		a = atom();
+		if(a == nil || !imap4date(&tm, a))
+			parseerr("bad date format");
+		sr->year = tm.year;
+		sr->mon = tm.mon;
+		sr->mday = tm.mday;
+		if(c == '"')
+			mustbe('"');
+	}else if(i = mapint(searchmapflag, a)){
+		sr->key = i;
+		mustbe(' ');
+		c = peekc();
+		if(c == '\\'){
+			mustbe('\\');
+			a = atomstring(atomstop, "\\");
+		}else
+			a = atom();
+		i = mapflag(a);
+		if(i == 0)
+			parseerr("flag not supported");
+		sr->num = i;
+	}else if(i = mapint(searchmapnum, a)){
+		sr->key = i;
+		mustbe(' ');
+		sr->num = number(0);
+	}else if(cistrcmp(a, "HEADER") == 0){
+		sr->key = SKheader;
+		mustbe(' ');
+		sr->hdr = astring();
+		mustbe(' ');
+		sr->s = astring();
+	}else if(cistrcmp(a, "UID") == 0){
+		sr->key = SKuid;
+		mustbe(' ');
+		sr->set = msgset(0);
+	}else if(cistrcmp(a, "NOT") == 0){
+		sr->key = SKnot;
+		mustbe(' ');
+		rock.next = nil;
+		searchkeys(0, &rock);
+		sr->left = rock.next;
+	}else if(cistrcmp(a, "OR") == 0){
+		sr->key = SKor;
+		mustbe(' ');
+		rock.next = nil;
+		searchkeys(0, &rock);
+		sr->left = rock.next;
+		mustbe(' ');
+		rock.next = nil;
+		searchkeys(0, &rock);
+		sr->right = rock.next;
+	}else
+		parseerr("illegal search key");
+	return sr;
+}
+
+/*
+ * set	: seqno
+ *	| seqno ':' seqno
+ *	| set ',' set
+ * seqno: nz-number
+ *	| '*'
+ *
+ */
+static Msgset*
+msgset(int uids)
+{
+	uint from, to;
+	Msgset head, *last, *ms;
+
+	last = &head;
+	head.next = nil;
+	for(;;){
+		from = uids ? uidno() : seqno();
+		to = from;
+		if(peekc() == ':'){
+			getc();
+			to = uids? uidno(): seqno();
+		}
+		ms = binalloc(&parsebin, sizeof *ms, 0);
+		if(ms == nil)
+			parseerr("out of memory");
+		if(to < from){
+			ms->from = to;
+			ms->to = from;
+		}else{
+			ms->from = from;
+			ms->to = to;
+		}
+		ms->next = nil;
+		last->next = ms;
+		last = ms;
+		if(peekc() != ',')
+			break;
+		getc();
+	}
+	return head.next;
+}
+
+static uint
+seqno(void)
+{
+	if(peekc() == '*'){
+		getc();
+		return ~0UL;
+	}
+	return number(1);
+}
+
+static uint
+uidno(void)
+{
+	if(peekc() == '*'){
+		getc();
+		return ~0UL;
+	}
+	return number(0);
+}
+
+/*
+ * 7 bit, non-ctl chars, no (){%*"\
+ * NIL is special case for nstring or parenlist
+ */
+static char *
+atom(void)
+{
+	return atomstring(atomstop, "");
+}
+
+/*
+ * like an atom, but no +
+ */
+static char *
+tag(void)
+{
+	return atomstring("+(){%*\"\\", "");
+}
+
+/*
+ * string or atom allowing %*
+ */
+static char *
+listmbox(void)
+{
+	int c;
+
+	c = peekc();
+	if(c == '{')
+		return literal();
+	if(c == '"')
+		return quoted();
+	return atomstring("(){\"\\", "");
+}
+
+/*
+ * string or atom
+ */
+static char *
+astring(void)
+{
+	int c;
+
+	c = peekc();
+	if(c == '{')
+		return literal();
+	if(c == '"')
+		return quoted();
+	return atom();
+}
+
+/*
+ * 7 bit, non-ctl chars, none from exception list
+ */
+static char *
+atomstring(char *disallowed, char *initial)
+{
+	char *s;
+	int c, ns, as;
+
+	ns = strlen(initial);
+	s = binalloc(&parsebin, ns + Stralloc, 0);
+	if(s == nil)
+		parseerr("out of memory");
+	strcpy(s, initial);
+	as = ns + Stralloc;
+	for(;;){
+		c = getc();
+		if(c <= ' ' || c >= 0x7f || strchr(disallowed, c) != nil){
+			ungetc();
+			break;
+		}
+		s[ns++] = c;
+		if(ns >= as){
+			s = bingrow(&parsebin, s, as, as + Stralloc, 0);
+			if(s == nil)
+				parseerr("out of memory");
+			as += Stralloc;
+		}
+	}
+	if(ns == 0)
+		badsyn();
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * quoted: '"' chars* '"'
+ * chars:	1-128 except \r and \n
+ */
+static char *
+quoted(void)
+{
+	char *s;
+	int c, ns, as;
+
+	mustbe('"');
+	s = binalloc(&parsebin, Stralloc, 0);
+	if(s == nil)
+		parseerr("out of memory");
+	as = Stralloc;
+	ns = 0;
+	for(;;){
+		c = getc();
+		if(c == '"')
+			break;
+		if(c < 1 || c > 0x7f || c == '\r' || c == '\n')
+			badsyn();
+		if(c == '\\'){
+			c = getc();
+			if(c != '\\' && c != '"')
+				badsyn();
+		}
+		s[ns++] = c;
+		if(ns >= as){
+			s = bingrow(&parsebin, s, as, as + Stralloc, 0);
+			if(s == nil)
+				parseerr("out of memory");
+			as += Stralloc;
+		}
+	}
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * litlen: {number}\r\n
+ */
+static uint
+litlen(void)
+{
+	uint v;
+
+	mustbe('{');
+	v = number(0);
+	mustbe('}');
+	crnl();
+	return v;
+}
+
+/*
+ * literal: litlen data<0:litlen>
+ */
+static char*
+literal(void)
+{
+	char *s;
+	uint v;
+
+	v = litlen();
+	s = binalloc(&parsebin, v + 1, 0);
+	if(s == nil)
+		parseerr("out of memory");
+	Bprint(&bout, "+ Ready for literal data\r\n");
+	if(Bflush(&bout) < 0)
+		writeerr();
+	if(v != 0 && Bread(&bin, s, v) != v)
+		badsyn();
+	s[v] = '\0';
+	return s;
+}
+
+/*
+ * digits; number is 32 bits
+ */
+enum{
+	Max = 0xffffffff/10,
+};
+
+static uint
+number(int nonzero)
+{
+	uint n, nn;
+	int c, first, ovfl;
+
+	n = 0;
+	first = 1;
+	ovfl = 0;
+	for(;;){
+		c = getc();
+		if(c < '0' || c > '9'){
+			ungetc();
+			if(first)
+				badsyn();
+			break;
+		}
+		c -= '0';
+		first = 0;
+		if(n > Max)
+			ovfl = 1;
+		nn = n*10 + c;
+		if(nn < n)
+			ovfl = 1;
+		n = nn;
+	}
+	if(nonzero && n == 0)
+		badsyn();
+	if(ovfl)
+		parseerr("number out of range\r\n");
+	return n;
+}
+
+static void
+logit(char *o)
+{
+	char *s, *p, *q;
+
+	if(!debug)
+		return;
+	s = strdup(o);
+	p = strchr(s, ' ');
+	if(!p)
+		goto emit;
+	q = strchr(++p, ' ');
+	if(!q)
+		goto emit;
+	if(!cistrncmp(p, "login", 5)){
+		q = strchr(++q, ' ');
+		if(!q)
+			goto emit;
+		for(q = q + 1; *q != ' ' && *q; q++)
+			*q = '*';
+	}
+emit:
+	for(p = s + strlen(s) - 1; p >= s && (/**p == '\r' ||*/ *p == '\n'); )
+		*p-- = 0;
+	ilog("%s", s);
+	free(s);
+}
+
+static char *gbuf;
+static char *gbufp = "";
+
+static int
+getc(void)
+{
+	if(*gbufp == 0){
+		free(gbuf);
+		werrstr("");
+		gbufp = gbuf = Brdstr(&bin, '\n', 0);
+		if(gbuf == 0){
+			ilog("empty line [%d]: %r", bin.fid);
+			gbufp = "";
+			return -1;
+		}
+		logit(gbuf);
+	}
+	return *gbufp++;
+}
+
+static void
+ungetc(void)
+{
+	if(gbufp > gbuf)
+		gbufp--;
+}
+
+static int
+peekc(void)
+{
+	return *gbufp;
+}
+
+#ifdef normal
+
+static int
+getc(void)
+{
+	return Bgetc(&bin);
+}
+
+static void
+ungetc(void)
+{
+	Bungetc(&bin);
+}
+
+static int
+peekc(void)
+{
+	int c;
+
+	c = Bgetc(&bin);
+	Bungetc(&bin);
+	return c;
+}
+#endif
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/imap4d.h
@@ -1,0 +1,395 @@
+/*
+ * mailbox and message representations
+ *
+ * these structures are allocated with emalloc and must be explicitly freed
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <auth.h>
+#include <avl.h>
+#include <bin.h>
+
+typedef struct Box	Box;
+typedef struct Header	Header;
+typedef struct Maddr	Maddr;
+typedef struct Mblock	Mblock;
+typedef struct Mimehdr	Mimehdr;
+typedef struct Msg	Msg;
+typedef struct Namedint	Namedint;
+typedef struct Pair	Pair;
+typedef struct Uidplus	Uidplus;
+
+enum
+{
+	Stralloc		= 32,		/* characters allocated at a time */
+	Bufsize		= 8*1024,	/* size of transfer block */
+	Ndigest		= 40,		/* length of digest string */
+	Nuid		= 10,		/* length of .imp uid string */
+	Nflags		= 8,		/* length of .imp flag string */
+	Locksecs		= 5 * 60,	/* seconds to wait for acquiring a locked file */
+	Pathlen		= 256,		/* max. length of upas/fs mbox name */
+	Filelen		= 32,		/* max. length of a file in a upas/fs mbox */
+	Userlen		= 64,		/* max. length of user's name */
+
+	Mutf7max	= 6,		/* max bytes for a mutf7 character: &bbbb- */
+
+	/*
+	 * message flags
+	 */
+	Fseen		= 1 << 0,
+	Fanswered	= 1 << 1,
+	Fflagged	= 1 << 2,
+	Fdeleted	= 1 << 3,
+	Fdraft		= 1 << 4,
+	Frecent		= 1 << 5,
+};
+
+typedef struct Fstree Fstree;
+struct Fstree {
+	Avl;
+	Msg *m;
+};
+
+struct Box
+{
+	char	*name;		/* path name of mailbox */
+	char	*fs;		/* fs name of mailbox */
+	char	*fsdir;		/* /mail/fs/box->fs */
+	char	*imp;		/* path name of .imp file */
+	uchar	writable;	/* can write back messages? */
+	uchar	dirtyimp;	/* .imp file needs to be written? */
+	uchar	sendflags;	/* need flags update */
+	Qid	qid;		/* qid of fs mailbox */
+	Qid	impqid;		/* qid of .imp when last synched */
+	long	mtime;		/* file mtime when last read */
+	uint	max;		/* maximum msgs->seq, same as number of messages */
+	uint	toldmax;	/* last value sent to client */
+	uint	recent;		/* number of recently received messaged */
+	uint	toldrecent;	/* last value sent to client */
+	uint	uidnext;	/* next uid value assigned to a message */
+	uint	uidvalidity;	/* uid of mailbox */
+	Msg	*msgs;		/* msgs in uid order */
+	Avltree	*fstree;		/* msgs in upas/fs order */
+};
+
+/*
+ * fields of Msg->info
+ */
+enum
+{
+	/*
+	 * read from upasfs
+	 */
+	Ifrom,
+	Ito,
+	Icc,
+	Ireplyto,
+	Iunixdate,
+	Isubject,
+	Itype,
+	Idisposition,
+	Ifilename,
+	Idigest,
+	Ibcc,
+	Iinreplyto,
+	Idate,
+	Isender,
+	Imessageid,
+	Ilines,		/* number of lines of raw body */
+	Isize,
+//	Iflags,
+//	Idatesec
+
+	Imax
+};
+
+struct Header
+{
+	char	*buf;		/* header, including terminating \r\n */
+	uint	size;		/* strlen(buf) */
+	uint	lines;		/* number of \n characters in buf */
+
+	/*
+	 * pre-parsed mime headers
+	 */
+	Mimehdr	*type;		/* content-type */
+	Mimehdr	*id;		/* content-id */
+	Mimehdr	*description;	/* content-description */
+	Mimehdr	*encoding;	/* content-transfer-encoding */
+//	Mimehdr	*md5;		/* content-md5 */
+	Mimehdr	*disposition;	/* content-disposition */
+	Mimehdr	*language;	/* content-language */
+};
+
+struct Msg
+{
+	Msg	*next;
+	Msg	*kids;
+	Msg	*parent;
+	char	*fsdir;		/* box->fsdir of enclosing message */
+	Header	head;		/* message header */
+	Header	mime;		/* mime header from enclosing multipart spec */
+	int	flags;
+	uchar	sendflags;	/* flags value needs to be sent to client */
+	uchar	expunged;	/* message actually expunged, but not yet reported to client */
+	uchar	matched;	/* search succeeded? */
+	uint	uid;		/* imap unique identifier */
+	uint	seq;		/* position in box; 1 is oldest */
+	uint	id;		/* number of message directory in upas/fs */
+	char	*fs;		/* name of message directory */
+	char	*efs;		/* pointer after / in fs; enough space for file name */
+
+	uint	size;		/* size of fs/rawbody, in bytes, with \r added before \n */
+	uint	lines;		/* number of lines in rawbody */
+
+	char	*ibuf;
+	char	*info[Imax];	/* all info about message */
+
+	char	*unixdate;
+	Maddr	*unixfrom;
+
+	Maddr	*to;		/* parsed out address lines */
+	Maddr	*from;
+	Maddr	*replyto;
+	Maddr	*sender;
+	Maddr	*cc;
+	Maddr	*bcc;
+};
+
+/*
+ * pre-parsed header lines
+ */
+struct Maddr
+{
+	char	*personal;
+	char	*box;
+	char	*host;
+	Maddr	*next;
+};
+
+struct Mimehdr
+{
+	char	*s;
+	char	*t;
+	Mimehdr	*next;
+};
+
+/*
+ * mapping of integer & names
+ */
+struct Namedint
+{
+	char	*name;
+	int	v;
+};
+
+/*
+ * lock for all mail file operations
+ */
+struct Mblock
+{
+	int	fd;
+};
+
+/*
+ * parse nodes for imap4rev1 protocol
+ *
+ * important: all of these items are allocated
+ * in one can, so they can be tossed out at the same time.
+ * this allows leakless parse error recovery by simply tossing the can away.
+ * however, it means these structures cannot be mixed with the mailbox structures
+ */
+
+typedef struct Fetch	Fetch;
+typedef struct Nlist	Nlist;
+typedef struct Slist	Slist;
+typedef struct Msgset	Msgset;
+typedef struct Store	Store;
+typedef struct Search	Search;
+
+/*
+ * parse tree for fetch command
+ */
+enum
+{
+	Fenvelope,
+	Fflags,
+	Finternaldate,
+	Frfc822,
+	Frfc822head,
+	Frfc822size,
+	Frfc822text,
+	Fbodystruct,
+	Fuid,
+	Fbody,			/* BODY */
+	Fbodysect,		/* BODY [...] */
+	Fbodypeek,
+
+	Fmax
+};
+
+enum
+{
+	FPall,
+	FPhead,
+	FPheadfields,
+	FPheadfieldsnot,
+	FPmime,
+	FPtext,
+
+	FPmax
+};
+
+struct Fetch
+{
+	uchar	op;		/* F.* operator */
+	uchar	part;		/* FP.* subpart for body[] & body.peek[]*/
+	uchar	partial;	/* partial fetch? */
+	long	start;		/* partial fetch amounts */
+	long	size;
+	Nlist	*sect;
+	Slist	*hdrs;
+	Fetch	*next;
+};
+
+/*
+ * status items
+ */
+enum{
+	Smessages	= 1 << 0,
+	Srecent		= 1 << 1,
+	Suidnext		= 1 << 2,
+	Suidvalidity	= 1 << 3,
+	Sunseen		= 1 << 4,
+};
+
+/*
+ * parse tree for store command
+ */
+enum
+{
+	Stflags,
+	Stflagssilent,
+
+	Stmax
+};
+
+struct Store
+{
+	uchar	sign;
+	uchar	op;
+	int	flags;
+};
+
+/*
+ * parse tree for search command
+ */
+enum
+{
+	SKnone,
+
+	SKcharset,
+
+	SKall,
+	SKanswered,
+	SKbcc,
+	SKbefore,
+	SKbody,
+	SKcc,
+	SKdeleted,
+	SKdraft,
+	SKflagged,
+	SKfrom,
+	SKheader,
+	SKkeyword,
+	SKlarger,
+	SKnew,
+	SKnot,
+	SKold,
+	SKon,
+	SKor,
+	SKrecent,
+	SKseen,
+	SKsentbefore,
+	SKsenton,
+	SKsentsince,
+	SKset,
+	SKsince,
+	SKsmaller,
+	SKsubject,
+	SKtext,
+	SKto,
+	SKuid,
+	SKunanswered,
+	SKundeleted,
+	SKundraft,
+	SKunflagged,
+	SKunkeyword,
+	SKunseen,
+
+	SKmax
+};
+
+struct Search
+{
+	int	key;
+	char	*s;
+	char	*hdr;
+	uint	num;
+	int	year;
+	int	mon;
+	int	mday;
+	Msgset	*set;
+	Search	*left;
+	Search	*right;
+	Search	*next;
+};
+
+struct Nlist
+{
+	uint	n;
+	Nlist	*next;
+};
+
+struct Slist
+{
+	char	*s;
+	Slist	*next;
+};
+
+struct Msgset
+{
+	uint	from;
+	uint	to;
+	Msgset	*next;
+};
+
+struct Pair
+{
+	uint	start;
+	uint	stop;
+};
+
+struct Uidplus
+{
+	uint	uid;
+	uint	uidvalidity;
+	Uidplus	*next;
+};
+
+extern	Bin	*parsebin;
+extern	Biobuf	bout;
+extern	Biobuf	bin;
+extern	char	username[Userlen];
+extern	char	mboxdir[Pathlen];
+extern	char	*fetchpartnames[FPmax];
+extern	char	*binupas;
+extern	char	*site;
+extern	char	*remote;
+extern	int	debug;
+extern	char	logfile[28];
+extern	Uidplus	*uidlist;
+extern	Uidplus	**uidtl;
+
+#include "fns.h"
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/imp.c
@@ -1,0 +1,315 @@
+#include "imap4d.h"
+
+static	char	magic[]	= "imap internal mailbox description\n";
+
+/* another appearance of this nasty hack. */
+typedef struct{
+	Avl;
+	Msg	*m;
+}Mtree;
+
+static	Avltree	*mtree;
+static	Bin	*mbin;
+
+static int
+mtreecmp(Avl *va, Avl *vb)
+{
+	Mtree *a, *b;
+
+	a = (Mtree*)va;
+	b = (Mtree*)vb;
+	return strcmp(a->m->info[Idigest], b->m->info[Idigest]);
+}
+
+static Namedint	flagcmap[Nflags] =
+{
+	{"s",	Fseen},
+	{"a",	Fanswered},
+	{"f",	Fflagged},
+	{"D",	Fdeleted},
+	{"d",	Fdraft},
+	{"r",	Frecent},
+};
+
+static int
+parseflags(char *flags)
+{
+	int i, f;
+
+	f = 0;
+	for(i = 0; i < Nflags; i++){
+		if(flags[i] == '-')
+			continue;
+		if(flags[i] != flagcmap[i].name[0])
+			return 0;
+		f |= flagcmap[i].v;
+	}
+	return f;
+}
+
+static int
+impflags(Box *box, Msg *m, char *flags)
+{
+	int f;
+
+	f = parseflags(flags);
+	/*
+	 * recent flags are set until the first time message's box is selected or examined.
+	 * it may be stored in the file as a side effect of a status or subscribe command;
+	 * if so, clear it out.
+	 */
+	if((f & Frecent) && strcmp(box->fs, "imap") == 0)
+		box->dirtyimp = 1;
+	f |= m->flags & Frecent;
+
+	/*
+	 * all old messages with changed flags should be reported to the client
+	 */
+	if(m->uid && m->flags != f){
+		box->sendflags = 1;
+		m->sendflags = 1;
+	}
+	m->flags = f;
+	return 1;
+}
+
+/*
+ * considerations:
+ * . messages can be deleted by another agent
+ * . we might still have a Msg for an expunged message,
+ *	because we haven't told the client yet.
+ * . we can have a Msg without a .imp entry.
+ * . flag information is added at the end of the .imp by copy & append
+ */
+
+static int
+rdimp(Biobuf *b, Box *box)
+{
+	char *s, *f[4];
+	uint u;
+	Msg *m, m0;
+	Mtree t, *p;
+
+	memset(&m0, 0, sizeof m0);
+	for(; s = Brdline(b, '\n'); ){
+		s[Blinelen(b) - 1] = 0;
+		if(tokenize(s, f, nelem(f)) != 3)
+			return -1;
+		u = strtoul(f[1], 0, 10);
+
+		memset(&t, 0, sizeof t);
+		m0.info[Idigest] = f[0];
+		t.m = &m0;
+		p = (Mtree*)avllookup(mtree, &t);
+		if(p){
+			m = p->m;
+			if(m->uid && m->uid != u){
+				ilog("dup? %ud %ud %s", u, m->uid, f[0]);
+				continue;
+			}
+			if(m->uid >= box->uidnext){
+				ilog("uid %ud >= %ud\n", m->uid, box->uidnext);
+				box->uidnext = m->uid;
+			}
+			if(m->uid == 0)
+				m->flags = 0;
+			if(impflags(box, m, f[2]) == -1)
+				return -1;
+			m->uid = u;
+		}else{
+			/*
+			 * message has been deleted.
+			 */
+//			ilog("flags, uid dropped on floor [%s, %ud]", m0.info[Idigest], u);
+		}
+	}
+	return 0;
+}
+
+enum{
+	Rmagic,
+	Rrdstr,
+	Rtok,
+	Rvalidity,
+	Ruidnext,
+};
+
+static char *rtab[] = {
+	"magic",
+	"rdstr",
+	"tok",
+	"val",
+	"uidnext"
+};
+
+char*
+sreason(int r)
+{
+	if(r >= 0 && r <= nelem(rtab))
+		return rtab[r];
+	return "*GOK*";
+}
+
+static int
+verscmp(Biobuf *b, Box *box, int *reason)
+{
+	char *s, *f[3];
+	int n;
+	uint u, v;
+
+	n = -1;
+	*reason = Rmagic;
+	if(s = Brdstr(b, '\n', 0))
+		n = strcmp(s, magic);
+	free(s);
+	if(n == -1)
+		return -1;
+	n = -1;
+	v = box->uidvalidity;
+	if((s = Brdstr(b, '\n', 1)) && ++*reason)
+	if(tokenize(s, f, nelem(f)) == 2 && ++*reason)
+	if((u = strtoul(f[0], 0, 10)) == v || v == 0 && ++*reason)
+	if((v = strtoul(f[1], 0, 10)) >= box->uidnext && ++*reason){
+		box->uidvalidity = u;
+		box->uidnext = v;
+		n = 0;
+	}
+	free(s);
+	return n;
+}
+
+int
+parseimp(Biobuf *b, Box *box)
+{
+	int r, reason;
+	Msg *m;
+	Mtree *p;
+
+	if(verscmp(b, box, &reason) == -1)
+		return -1;
+	mtree = avlcreate(mtreecmp);
+	r = 0;
+	for(m = box->msgs; m; m = m->next)
+		r++;
+	p = binalloc(&mbin, r*sizeof *p, 1);
+	if(p == nil)
+		bye("no memory");
+	for(m = box->msgs; m; m = m->next){
+		p->m = m;
+		avlinsert(mtree, p);
+		p++;
+	}
+	r = rdimp(b, box);
+	binfree(&mbin);
+	free(mtree);
+	return r;
+}
+
+static void
+wrimpflags(char *buf, int flags, int killrecent)
+{
+	int i;
+
+	if(killrecent)
+		flags &= ~Frecent;
+	memset(buf, '-', Nflags);
+	for(i = 0; i < Nflags; i++)
+		if(flags & flagcmap[i].v)
+			buf[i] = flagcmap[i].name[0];
+	buf[i] = 0;
+}
+
+int
+wrimp(Biobuf *b, Box *box)
+{
+	char buf[16];
+	int i;
+	Msg *m;
+
+	box->dirtyimp = 0;
+	Bprint(b, "%s", magic);
+	Bprint(b, "%.*ud %.*ud\n", Nuid, box->uidvalidity, Nuid, box->uidnext);
+	i = strcmp(box->fs, "imap") == 0;
+	for(m = box->msgs; m != nil; m = m->next){
+		if(m->expunged)
+			continue;
+		wrimpflags(buf, m->flags, i);
+		Bprint(b, "%.*s %.*ud %s\n", Ndigest, m->info[Idigest], Nuid, m->uid, buf);
+	}
+	return 0;
+}
+
+static uint
+scanferdup(Biobuf *b, char *digest, int *flags, vlong *pos)
+{
+	char *s, *f[4];
+	uint uid;
+
+	uid = 0;
+	for(; s = Brdline(b, '\n'); ){
+		s[Blinelen(b) - 1] = 0;
+		if(tokenize(s, f, nelem(f)) != 3)
+			return ~0;
+		if(strcmp(f[0], digest) == 0){
+			uid = strtoul(f[1], 0, 10);
+//			fprint(2, "digest %s matches uid %ud\n", f[0], uid);
+			*flags |= parseflags(f[2]);
+			break;
+		}
+		*pos += Blinelen(b);
+	}
+	return uid;
+}
+
+int
+appendimp(char *bname, char *digest, int flags, Uidplus *u)
+{
+	char buf[16], *iname;
+	int fd, reason;
+	uint dup;
+	vlong pos;
+	Biobuf b;
+	Box box;
+
+	dup = 0;
+	pos = 0;
+	memset(&box, 0, sizeof box);
+	iname = impname(bname);
+	fd = cdopen(mboxdir, iname, ORDWR);
+	if(fd == -1){
+		fd = cdcreate(mboxdir, iname, OWRITE, 0664);
+		if(fd == -1)
+			return -1;
+		box.uidvalidity = time(0);
+		box.uidnext = 1;
+	}else{
+		dup = ~0;
+		Binit(&b, fd, OREAD);
+		if(verscmp(&b, &box, &reason) == -1)
+			ilog("bad verscmp %s", sreason(reason));
+		else{
+			pos = Bseek(&b, 0, 1);
+			dup = scanferdup(&b, digest, &flags, &pos);
+		}
+		Bterm(&b);
+	}
+	if(dup == ~0){
+		close(fd);
+		return -1;
+	}
+	Binit(&b, fd, OWRITE);
+	if(dup == 0){
+		Bseek(&b, 0, 0);
+		Bprint(&b, "%s", magic);
+		Bprint(&b, "%.*ud %.*ud\n", Nuid, box.uidvalidity, Nuid, box.uidnext + 1);
+		Bseek(&b, 0, 2);
+	}else
+ 		Bseek(&b, pos, 0);
+	wrimpflags(buf, flags, 0);
+	Bprint(&b, "%.*s %.*ud %s\n", Ndigest, digest, Nuid, dup? dup: box.uidnext, buf);
+	Bterm(&b);
+	close(fd);
+	u->uidvalidity = box.uidvalidity;
+	u->uid = box.uidnext;
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/list.c
@@ -1,0 +1,425 @@
+#include "imap4d.h"
+
+enum{
+	Mfolder	= 0,
+	Mbox,
+	Mdir,
+};
+
+	char	subscribed[] = "imap.subscribed";
+static	int	ldebug;
+
+#define	dprint(...)	if(ldebug)fprint(2, __VA_ARGS__); else {}
+
+static int	lmatch(char*, char*, char*);
+
+static int
+mopen(char *box, int mode)
+{
+	char buf[Pathlen];
+
+	if(!strcmp(box, "..") || strstr(box, "/.."))
+		return -1;
+	return cdopen(mboxdir, encfs(buf, sizeof buf, box), mode);
+}
+
+static Dir*
+mdirstat(char *box)
+{
+	char buf[Pathlen];
+
+	return cddirstat(mboxdir, encfs(buf, sizeof buf, box));
+}
+
+static long
+mtime(char *box)
+{
+	long mtime;
+	Dir *d;
+
+	mtime = 0;
+	if(d = mdirstat(box))
+		mtime = d->mtime;
+	free(d);
+	return mtime;
+}
+
+static int
+mokmbox(char *s)
+{
+	char *p;
+
+	if(p = strrchr(s, '/'))
+		s = p + 1;
+	if(!strcmp(s, "mbox"))
+		return 1;
+	return okmbox(s);
+}
+
+/*
+ * paranoid check to prevent accidents
+ */
+/*
+ * BOTCH: we're taking it upon ourselves to
+ * identify mailboxes.  this is a bad idea.
+ * keep in sync with ../fs/mdir.c
+ */
+static int
+dirskip(Dir *a, uvlong *uv)
+{
+	char *p;
+
+	if(a->length == 0)
+		return 1;
+	*uv = strtoul(a->name, &p, 0);
+	if(*uv < 1000000 || *p != '.')
+		return 1;
+	*uv = *uv<<8 | strtoul(p+1, &p, 10);
+	if(*p)
+		return 1;
+	return 0;
+}
+
+static int
+chkmbox(char *path, int mode)
+{
+	char buf[32];
+	int i, r, n, fd, type;
+	uvlong uv;
+	Dir *d;
+
+	type = Mbox;
+	if(mode & DMDIR)
+		type = Mdir;
+	fd = mopen(path, OREAD);
+	if(fd == -1)
+		return -1;
+	r = -1;
+	if(type == Mdir && (n = dirread(fd, &d)) > 0){
+		r = Mfolder;
+		for(i = 0; i < n; i++)
+			if(!dirskip(d + i, &uv)){
+				r = Mdir;
+				break;
+			}
+		free(d);
+	}else if(type == Mdir)
+		r = Mdir;
+	else if(type == Mbox){
+		if(pread(fd, buf, sizeof buf, 0) == sizeof buf)
+		if(!strncmp(buf, "From ", 5))
+			r = Mbox;
+	}
+	close(fd);
+	return r;
+}
+
+static int
+chkmboxpath(char *f)
+{
+	int r;
+	Dir *d;
+
+	r = -1;
+	if(d = mdirstat(f))
+		r = chkmbox(f, d->mode);
+	free(d);
+	return r;
+}
+
+static char*
+appendwd(char *nwd, int n, char *wd, char *a)
+{
+	if(wd[0] && a[0] != '/')
+		snprint(nwd, n, "%s/%s", wd, a);
+	else
+		snprint(nwd, n, "%s", a);
+	return nwd;
+}
+
+static int
+output(char *cmd, char *wd, Dir *d, int term)
+{
+	char path[Pathlen], dec[Pathlen], *s, *flags;
+
+	appendwd(path, sizeof path, wd, d->name);
+	dprint("Xoutput %s %s %d\n", wd, d->name, term);
+	switch(chkmbox(path, d->mode)){
+	default:
+		return 0;
+	case Mfolder:
+		flags = "(\\Noselect)";
+		break;
+	case Mdir:
+	case Mbox:
+		s = impname(path);
+		if(s != nil && mtime(s) < d->mtime)
+			flags = "(\\Noinferiors \\Marked)";
+		else
+			flags = "(\\Noinferiors)";
+		break;
+	}
+
+	if(!term)
+		return 1;
+
+	if(s = strmutf7(decfs(dec, sizeof dec, path)))
+		Bprint(&bout, "* %s %s \"/\" %#Z\r\n", cmd, flags, s);
+	return 1;
+}
+
+static int
+rematch(char *cmd, char *wd, char *pat, Dir *d)
+{
+	char nwd[Pathlen];
+
+	appendwd(nwd, sizeof nwd, wd, d->name);
+	if(d->mode & DMDIR)
+	if(chkmbox(nwd, d->mode) == Mfolder)
+	if(lmatch(cmd, pat, nwd))
+		return 1;
+	return 0;
+}
+
+static int
+match(char *cmd, char *wd, char *pat, Dir *d, int i)
+{
+	char *p, *p1;
+	int m, n;
+	Rune r, r1;
+
+	m = 0;
+	for(p = pat; ; p = p1){
+		n = chartorune(&r, p);
+		p1 = p + n;
+		dprint("r = %C [%.2ux]\n", r, r);
+		switch(r){
+		case '*':
+		case '%':
+			for(r1 = 1; r1;){
+				if(match(cmd, wd, p1, d, i))
+				if(output(cmd, wd, d, 0)){
+					m++;
+					break;
+				}
+				i += chartorune(&r1, d->name + i);
+			}
+			if(r == '*' && rematch(cmd, wd, p, d))
+				return 1;
+			if(m > 0)
+				return 1;
+			break;
+		case '/':
+			return rematch(cmd, wd, p1, d);
+		default:
+			chartorune(&r1, d->name + i);
+			if(r1 != r)
+				return 0;
+			if(r == 0)
+				return output(cmd, wd, d, 1);
+			dprint("  r %C ~ %C [%.2ux]\n", r, r1, r1);
+			i += n;
+			break;
+		}
+	}
+}
+
+static int
+lmatch(char *cmd, char *pat, char *wd)
+{
+	char dec[Pathlen];
+	int fd, n, m, i;
+	Dir *d;
+
+	if((fd = mopen(wd[0]? wd: ".", OREAD)) == -1)
+		return -1;
+	if(wd[0])
+		dprint("wd %s\n", wd);
+	m = 0;
+	for(;;){
+		n = dirread(fd, &d);
+		if(n <= 0)
+			break;
+		for(i = 0; i < n; i++)
+			if(mokmbox(d[i].name)){
+				d[i].name = decfs(dec, sizeof dec, d[i].name);
+				m += match(cmd, wd, pat, d + i, 0);
+			}
+		free(d);
+	}
+	close(fd);
+	return m;
+}
+
+int
+listboxes(char *cmd, char *ref, char *pat)
+{
+	char buf[Pathlen];
+
+	pat = appendwd(buf, sizeof buf, ref, pat);
+	return lmatch(cmd, pat, "") > 0;
+}
+
+static int
+opensubscribed(void)
+{
+	int fd;
+
+	fd = cdopen(mboxdir, subscribed, ORDWR);
+	if(fd >= 0)
+		return fd;
+	fd = cdcreate(mboxdir, subscribed, ORDWR, 0664);
+	if(fd < 0)
+		return -1;
+	fprint(fd, "#imap4 subscription list\nINBOX\n");
+	seek(fd, 0, 0);
+	return fd;
+}
+
+/*
+ * resistance to hand-edits
+ */
+static char*
+trim(char *s, int l)
+{
+	int c;
+
+	for(;; l--){
+		if(l == 0)
+			return 0;
+		c = s[l - 1];
+		if(c != '\t' && c != ' ')
+			break;
+	}
+	for(s[l] = 0; c = *s; s++)
+		if(c != '\t' && c != ' ')
+			break;
+	if(c == 0 || c == '#')
+		return 0;
+	return s;
+}
+
+static int
+poutput(char *cmd, char *f, int term)
+{
+	char *p, *wd;
+	int r;
+	Dir *d;
+
+	if(!mokmbox(f) || !(d = mdirstat(f)))
+		return 0;
+	wd = "";
+	if(p = strrchr(f, '/')){
+		*p = 0;
+		wd = f;
+	}
+	r = output(cmd, wd, d, term);
+	if(p)
+		*p = '/';
+	free(d);
+	return r;
+}
+
+static int
+pmatch(char *cmd, char *pat, char *f, int i)
+{
+	char *p, *p1;
+	int m, n;
+	Rune r, r1;
+
+	dprint("pmatch pat[%s] f[%s]\n", pat, f + i);
+	m = 0;
+	for(p = pat; ; p = p1){
+		n = chartorune(&r, p);
+		p1 = p + n;
+		switch(r){
+		case '*':
+		case '%':
+			for(r1 = 1; r1;){
+				if(pmatch(cmd, p1, f, i))
+				if(poutput(cmd, f, 0)){
+					m++;
+					break;
+				}
+				i += chartorune(&r1, f + i);
+				if(r == '%' && r1 == '/')
+					break;
+			}
+			if(m > 0)
+				return 1;
+			break;
+		default:
+			chartorune(&r1, f + i);
+			if(r1 != r)
+				return 0;
+			if(r == 0)
+				return poutput(cmd, f, 1);
+			i += n;
+			break;
+		}
+	}
+}
+
+int
+lsubboxes(char *cmd, char *ref, char *pat)
+{
+	char *s, buf[Pathlen];
+	int r, fd;
+	Biobuf b;
+	Mblock *l;
+
+	pat = appendwd(buf, sizeof buf, ref, pat);
+	if((l = mblock()) == nil)
+		return 0;
+	fd = opensubscribed();
+	r = 0;
+	Binit(&b, fd, OREAD);
+	while(s = Brdline(&b, '\n'))
+		if(s = trim(s, Blinelen(&b) - 1))
+			r += pmatch(cmd, pat, s, 0);
+	Bterm(&b);
+	close(fd);
+	mbunlock(l);
+	return r;
+}
+
+int
+subscribe(char *mbox, int how)
+{
+	char *s, *in, *ein;
+	int fd, tfd, ok, l;
+	Mblock *mb;
+
+	if(cistrcmp(mbox, "inbox") == 0)
+		mbox = "INBOX";
+	if((mb = mblock()) == nil)
+		return 0;
+	fd = opensubscribed();
+	if(fd < 0 || (in = readfile(fd)) == nil){
+		close(fd);
+		mbunlock(mb);
+		return 0;
+	}
+	l = strlen(mbox);
+	s = strstr(in, mbox);
+	while(s != nil && (s != in && s[-1] != '\n' || s[l] != '\n'))
+		s = strstr(s + 1, mbox);
+	ok = 0;
+	if(how == 's' && s == nil){
+		if(chkmboxpath(mbox) > 0)
+		if(fprint(fd, "%s\n", mbox) > 0)
+			ok = 1;
+	}else if(how == 'u' && s != nil){
+		ein = strchr(s, 0);
+		memmove(s, &s[l+1], ein - &s[l+1]);
+		ein -= l + 1;
+		tfd = cdopen(mboxdir, subscribed, OWRITE|OTRUNC);
+		if(tfd >= 0 && pwrite(fd, in, ein - in, 0) == ein - in)
+			ok = 1;
+		close(tfd);
+	}else
+		ok = 1;
+	close(fd);
+	mbunlock(mb);
+	return ok;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/mbox.c
@@ -1,0 +1,630 @@
+#include "imap4d.h"
+
+static	int	fsctl		= -1;
+static	char	Ecanttalk[]	= "can't talk to mail server";
+
+static void
+fsinit(void)
+{
+	if(fsctl != -1)
+		return;
+	fsctl = open("/mail/fs/ctl", ORDWR);
+	if(fsctl == -1)
+		bye(Ecanttalk);
+}
+
+static void
+boxflags(Box *box)
+{
+	Msg *m;
+
+	box->recent = 0;
+	for(m = box->msgs; m != nil; m = m->next){
+		if(m->uid == 0){
+	//		fprint(2, "unassigned uid %s\n", m->info[Idigest]);
+			box->dirtyimp = 1;
+			m->uid = box->uidnext++;
+		}
+		if(m->flags & Frecent)
+			box->recent++;
+	}
+}
+
+/*
+ * try to match permissions with mbox
+ */
+static int
+createimp(Box *box, Qid *qid)
+{
+	int fd;
+	long mode;
+	Dir *d;
+
+	fd = cdcreate(mboxdir, box->imp, OREAD, 0664);
+	if(fd < 0)
+		return -1;
+	d = cddirstat(mboxdir, box->name);
+	if(d != nil){
+		mode = d->mode & 0777;
+		nulldir(d);
+		d->mode = mode;
+		dirfwstat(fd, d);
+		free(d);
+	}
+	if(fqid(fd, qid) < 0){
+		close(fd);
+		return -1;
+	}
+
+	return fd;
+}
+
+/*
+ * read in the .imp file, or make one if it doesn't exist.
+ * make sure all flags and uids are consistent.
+ * return the mailbox lock.
+ */
+static Mblock*
+openimp(Box *box, int new)
+{
+	char buf[ERRMAX];
+	int fd;
+	Biobuf b;
+	Mblock *ml;
+	Qid qid;
+
+	ml = mblock();
+	if(ml == nil)
+		return nil;
+	fd = cdopen(mboxdir, box->imp, OREAD);
+	if(fd < 0 || fqid(fd, &qid) < 0){
+		if(fd < 0){
+			errstr(buf, sizeof buf);
+			if(cistrstr(buf, "does not exist") == nil)
+				ilog("imp: %s: %s", box->imp, buf);
+			else
+				debuglog("imp: %s: %s .. creating", box->imp, buf);
+		}else{
+			close(fd);
+			ilog("%s: bogus imp: bad qid: recreating", box->imp);
+		}
+		fd = createimp(box, &qid);
+		if(fd < 0){
+			ilog("createimp fails: %r");
+			mbunlock(ml);
+			return nil;
+		}
+		box->dirtyimp = 1;
+		if(box->uidvalidity == 0){
+			ilog("set uidvalidity %lud [new]\n", box->uidvalidity);
+			box->uidvalidity = box->mtime;
+		}
+		box->impqid = qid;
+		new = 1;
+	}else if(qid.path != box->impqid.path || qid.vers != box->impqid.vers){
+		Binit(&b, fd, OREAD);
+		if(parseimp(&b, box) == -1){
+			ilog("%s: bogus imp: parse failure", box->imp);
+			box->dirtyimp = 1;
+			if(box->uidvalidity == 0){
+				ilog("set uidvalidity %lud [parseerr]\n", box->uidvalidity);
+				box->uidvalidity = box->mtime;
+			}
+		}
+		Bterm(&b);
+		box->impqid = qid;
+		new = 1;
+	}
+	if(new)
+		boxflags(box);
+	close(fd);
+	return ml;
+}
+
+/*
+ * mailbox is unreachable, so mark all messages expunged
+ * clean up .imp files as well.
+ */
+static void
+mboxgone(Box *box)
+{
+	char buf[ERRMAX];
+	Msg *m;
+
+	rerrstr(buf, ERRMAX);
+	if(strstr(buf, "hungup channel"))
+		bye(Ecanttalk);
+//	too smart.
+//	if(cdexists(mboxdir, box->name) < 0)
+//		cdremove(mboxdir, box->imp);
+	for(m = box->msgs; m != nil; m = m->next)
+		m->expunged = 1;
+	ilog("mboxgone");
+	box->writable = 0;
+}
+
+/*
+ * read messages in the mailbox
+ * mark message that no longer exist as expunged
+ * returns -1 for failure, 0 if no new messages, 1 if new messages.
+ */
+enum {
+	Gone	= 2,		/* don't unexpunge messages */
+};
+
+static int
+readbox(Box *box)
+{
+	char buf[ERRMAX];
+	int i, n, fd, new, id;
+	Dir *d;
+	Msg *m, *last;
+
+	fd = cdopen(box->fsdir, ".", OREAD);
+	if(fd == -1){
+goinggoinggone:
+		rerrstr(buf, ERRMAX);
+		ilog("upas/fs stat of %s/%s aka %s failed: %r",
+			username, box->name, box->fsdir);
+		mboxgone(box);
+		return -1;
+	}
+
+	if((d = dirfstat(fd)) == nil){
+		close(fd);
+		goto goinggoinggone;
+	}
+	box->mtime = d->mtime;
+	box->qid = d->qid;
+	last = nil;
+	for(m = box->msgs; m != nil; m = m->next){
+		last = m;
+		m->expunged |= Gone;
+	}
+	new = 0;
+	free(d);
+
+	for(;;){
+		n = dirread(fd, &d);
+		if(n <= 0){
+			close(fd);
+			if(n == -1)
+				goto goinggoinggone;
+			break;
+		}
+		for(i = 0; i < n; i++){
+			if((d[i].qid.type & QTDIR) == 0)
+				continue;
+			id = atoi(d[i].name);
+			if(m = fstreefind(box, id)){
+				m->expunged &= ~Gone;
+				continue;
+			}
+			new = 1;
+			m = MKZ(Msg);
+			m->id = id;
+			m->fsdir = box->fsdir;
+			m->fs = emalloc(2 * (Filelen + 1));
+			m->efs = seprint(m->fs, m->fs + (Filelen + 1), "%ud/", id);
+			m->size = ~0UL;
+			m->lines = ~0UL;
+			m->flags = Frecent;
+			if(!msginfo(m))
+				freemsg(0, m);
+			else{
+				fstreeadd(box, m);
+				if(last == nil)
+					box->msgs = m;
+				else
+					last->next = m;
+				last = m;
+			}
+		}
+		free(d);
+	}
+
+	/* box->max is invalid here */
+	return new;
+}
+
+int
+uidcmp(void *va, void *vb)
+{
+	Msg **a, **b;
+
+	a = va;
+	b = vb;
+	return (*a)->uid - (*b)->uid;
+}
+
+static void
+sequence(Box *box)
+{
+	Msg **a, *m;
+	int n, i;
+
+	n = 0;
+	for(m = box->msgs; m; m = m->next)
+		n++;
+	a = ezmalloc(n * sizeof *a);
+	i = 0;
+	for(m = box->msgs; m; m = m->next)
+		a[i++] = m;
+	qsort(a, n, sizeof *a, uidcmp);
+	for(i = 0; i < n - 1; i++)
+		a[i]->next = a[i + 1];
+	for(i = 0; i < n; i++)
+		if(a[i]->seq && a[i]->seq != i + 1)
+			bye("internal error assigning message numbers");
+		else
+			a[i]->seq = i + 1;
+	box->msgs = nil;
+	if(n > 0){
+		a[n - 1]->next = nil;
+		box->msgs = a[0];
+	}
+	box->max = n;
+	memset(a, 0, n*sizeof *a);
+	free(a);
+}
+
+/*
+ * strategy:
+ * every mailbox file has an associated .imp file
+ * which maps upas/fs message digests to uids & message flags.
+ *
+ * the .imp files are locked by /mail/fs/usename/L.mbox.
+ * whenever the flags can be modified, the lock file
+ * should be opened, thereby locking the uid & flag state.
+ * for example, whenever new uids are assigned to messages,
+ * and whenever flags are changed internally, the lock file
+ * should be open and locked.  this means the file must be
+ * opened during store command, and when changing the \seen
+ * flag for the fetch command.
+ *
+ * if no .imp file exists, a null one must be created before
+ * assigning uids.
+ *
+ * the .imp file has the following format
+ * imp		: "imap internal mailbox description\n"
+ * 			uidvalidity " " uidnext "\n"
+ *			messagelines
+ *
+ * messagelines	:
+ *		| messagelines digest " " uid " " flags "\n"
+ *
+ * uid, uidnext, and uidvalidity are 32 bit decimal numbers
+ * printed right justified in a field Nuid characters long.
+ * the 0 uid implies that no uid has been assigned to the message,
+ * but the flags are valid. note that message lines are in mailbox
+ * order, except possibly for 0 uid messages.
+ *
+ * digest is an ascii hex string Ndigest characters long.
+ *
+ * flags has a character for each of NFlag flag fields.
+ * if the flag is clear, it is represented by a "-".
+ * set flags are represented as a unique single ascii character.
+ * the currently assigned flags are, in order:
+ *	Fseen		s
+ *	Fanswered	a
+ *	Fflagged	f
+ *	Fdeleted	D
+ *	Fdraft		d
+ */
+
+Box*
+openbox(char *name, char *fsname, int writable)
+{
+	char err[ERRMAX];
+	int new;
+	Box *box;
+	Mblock *ml;
+
+	fsinit();
+if(!strcmp(name, "mbox"))ilog("open %F %q", name, fsname);
+	if(fprint(fsctl, "open %F %q", name, fsname) < 0){
+		rerrstr(err, sizeof err);
+		if(strstr(err, "file does not exist") == nil)
+			ilog("fs open %F as %s: %s", name, fsname, err);
+		if(strstr(err, "hungup channel"))
+			bye(Ecanttalk);
+		fprint(fsctl, "close %s", fsname);
+		return nil;
+	}
+
+	/*
+	 * read box to find all messages
+	 * each one has a directory, and is in numerical order
+	 */
+	box = MKZ(Box);
+	box->writable = writable;
+	box->name = smprint("%s", name);
+	box->imp = smprint("%s.imp", name);
+	box->fs = smprint("%s", fsname);
+	box->fsdir = smprint("/mail/fs/%s", fsname);
+	box->uidnext = 1;
+	box->fstree = avlcreate(fstreecmp);
+	new = readbox(box);
+	if(new >= 0 && (ml = openimp(box, new))){
+		closeimp(box, ml);
+		sequence(box);
+		return box;
+	}
+	closebox(box, 0);
+	return nil;
+}
+
+/*
+ * careful: called by idle polling proc
+ */
+Mblock*
+checkbox(Box *box, int imped)
+{
+	int new;
+	Dir *d;
+	Mblock *ml;
+
+	if(box == nil)
+		return nil;
+
+	/*
+	 * if stat fails, mailbox must be gone
+	 */
+	d = cddirstat(box->fsdir, ".");
+	if(d == nil){
+		mboxgone(box);
+		return nil;
+	}
+	new = 0;
+	if(box->qid.path != d->qid.path || box->qid.vers != d->qid.vers
+	|| box->mtime != d->mtime){
+		new = readbox(box);
+		if(new < 0){
+			free(d);
+			return nil;
+		}
+	}
+	free(d);
+	ml = openimp(box, new);
+	if(ml == nil){
+		ilog("openimp fails; box->writable = 0: %r");
+		box->writable = 0;
+	}else if(!imped){
+		closeimp(box, ml);
+		ml = nil;
+	}
+	if(new || box->dirtyimp)
+		sequence(box);
+	return ml;
+}
+
+/*
+ * close the .imp file, after writing out any changes
+ */
+void
+closeimp(Box *box, Mblock *ml)
+{
+	int fd;
+	Biobuf b;
+	Qid qid;
+
+	if(ml == nil)
+		return;
+	if(!box->dirtyimp){
+		mbunlock(ml);
+		return;
+	}
+	fd = cdcreate(mboxdir, box->imp, OWRITE, 0664);
+	if(fd < 0){
+		mbunlock(ml);
+		return;
+	}
+	Binit(&b, fd, OWRITE);
+	box->dirtyimp = 0;
+	wrimp(&b, box);
+	Bterm(&b);
+
+	if(fqid(fd, &qid) == 0)
+		box->impqid = qid;
+	close(fd);
+	mbunlock(ml);
+}
+
+void
+closebox(Box *box, int opened)
+{
+	Msg *m, *next;
+
+	/*
+	 * make sure to leave the mailbox directory so upas/fs can close the mailbox
+	 */
+	mychdir(mboxdir);
+
+	if(box->writable){
+		deletemsg(box, 0);
+		if(expungemsgs(box, 0))
+			closeimp(box, checkbox(box, 1));
+	}
+
+	if(fprint(fsctl, "close %s", box->fs) < 0 && opened)
+		bye(Ecanttalk);
+	for(m = box->msgs; m != nil; m = next){
+		next = m->next;
+		freemsg(box, m);
+	}
+	free(box->name);
+	free(box->fs);
+	free(box->fsdir);
+	free(box->imp);
+	free(box->fstree);
+	free(box);
+}
+
+int
+deletemsg(Box *box, Msgset *ms)
+{
+	char buf[Bufsize], *p, *start;
+	int ok;
+	Msg *m;
+
+	if(!box->writable)
+		return 0;
+
+	/*
+	 * first pass: delete messages; gang the writes together for speed.
+	 */
+	ok = 1;
+	start = seprint(buf, buf + sizeof buf, "delete %s", box->fs);
+	p = start;
+	for(m = box->msgs; m != nil; m = m->next)
+		if(ms == 0 || ms && inmsgset(ms, m->uid))
+		if((m->flags & Fdeleted) && !m->expunged){
+			m->expunged = 1;
+			p = seprint(p, buf + sizeof buf, " %ud", m->id);
+			if(p + 32 >= buf + sizeof buf){
+				if(write(fsctl, buf, p - buf) == -1)
+					bye(Ecanttalk);
+				p = start;
+			}
+		}
+	if(p != start && write(fsctl, buf, p - buf) == -1)
+		bye(Ecanttalk);
+	return ok;
+}
+
+/*
+ * second pass: remove the message structure,
+ * and renumber message sequence numbers.
+ * update messages counts in mailbox.
+ * returns true if anything changed.
+ */
+int
+expungemsgs(Box *box, int send)
+{
+	uint n;
+	Msg *m, *next, *last;
+
+	n = 0;
+	last = nil;
+	for(m = box->msgs; m != nil; m = next){
+		m->seq -= n;
+		next = m->next;
+		if(m->expunged){
+			if(send)
+				Bprint(&bout, "* %ud expunge\r\n", m->seq);
+			if(m->flags & Frecent)
+				box->recent--;
+			n++;
+			if(last == nil)
+				box->msgs = next;
+			else
+				last->next = next;
+			freemsg(box, m);
+		}else
+			last = m;
+	}
+	if(n){
+		box->max -= n;
+		box->dirtyimp = 1;
+	}
+	return n;
+}
+
+static char *stoplist[] =
+{
+	".",
+	"dead.letter",
+	"forward",
+	"headers",
+	"imap.subscribed",
+	"mbox",
+	"names",
+	"pipefrom",
+	"pipeto",
+	0
+};
+
+/*
+ * reject bad mailboxes based on mailbox name
+ */
+int
+okmbox(char *path)
+{
+	char *name;
+	int i, c;
+
+	name = strrchr(path, '/');
+	if(name == nil)
+		name = path;
+	else
+		name++;
+	if(strlen(name) + STRLEN(".imp") >= Pathlen)
+		return 0;
+	for(i = 0; stoplist[i]; i++)
+		if(strcmp(name, stoplist[i]) == 0)
+			return 0;
+	c = name[0];
+	if(c == 0 || c == '-' || c == '/'
+	|| isdotdot(name)
+	|| isprefix("L.", name)
+	|| isprefix("imap-tmp.", name)
+	|| issuffix("-", name)
+	|| issuffix(".00", name)
+	|| issuffix(".imp", name)
+	|| issuffix(".idx", name))
+		return 0;
+
+	return 1;
+}
+
+int
+creatembox(char *mbox)
+{
+	fsinit();
+	if(fprint(fsctl, "create %q", mbox) > 0){
+		fprint(fsctl, "close %s", mbox);
+		return 0;
+	}
+	return -1;
+}
+
+/*
+ * rename mailbox.  truncaes or removes the source.
+ * bug? is the lock required
+ * upas/fs helpfully moves our .imp file.
+ */
+int
+renamebox(char *from, char *to, int doremove)
+{
+	char *p;
+	int r;
+	Mblock *ml;
+
+	fsinit();
+	ml = mblock();
+	if(ml == nil)
+		return 0;
+	if(doremove)
+		r = fprint(fsctl, "rename %F %F", from, to);
+	else
+		r = fprint(fsctl, "rename -t %F %F", from, to);
+	if(r > 0){
+		if(p = strrchr(to, '/'))
+			p++;
+		else
+			p = to;
+		fprint(fsctl, "close %s", p);
+	}
+	mbunlock(ml);
+	return r > 0;
+}
+
+/*
+ * upas/fs likes us; he removes the .imp file
+ */
+int
+removembox(char *path)
+{
+	fsinit();
+	return fprint(fsctl, "remove %s", path) > 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/mkfile
@@ -1,0 +1,36 @@
+</$objtype/mkfile
+<../mkupas
+
+OFILES=\
+	auth.$O\
+	copy.$O\
+	csquery.$O\
+	date.$O\
+	debug.$O\
+	fetch.$O\
+	folder.$O\
+	fsenc.$O\
+	fstree.$O\
+	imp.$O\
+	imap4d.$O\
+	list.$O\
+	mbox.$O\
+	msg.$O\
+	mutf7.$O\
+	nodes.$O\
+	print.$O\
+	quota.$O\
+	search.$O\
+	store.$O\
+	utils.$O\
+
+HFILES=imap4d.h\
+	fns.h\
+
+TARG=imap4d
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/msg.c
@@ -1,0 +1,1507 @@
+#include "imap4d.h"
+
+static	char	*headaddrspec(char*, char*);
+static	Maddr	*headaddresses(void);
+static	Maddr	*headaddress(void);
+static	char	*headatom(char*);
+static	int	headchar(int eat);
+static	char	*headdomain(char*);
+static	Maddr	*headmaddr(Maddr*);
+static	char	*headphrase(char*, char*);
+static	char	*headquoted(int start, int stop);
+static	char	*headskipwhite(int);
+static	void	headskip(void);
+static	char	*headsubdomain(void);
+static	char	*headtext(void);
+static	void	headtoend(void);
+static	char	*headword(void);
+static	void	mimedescription(Header*);
+static	void	mimedisposition(Header*);
+static	void	mimeencoding(Header*);
+static	void	mimeid(Header*);
+static	void	mimelanguage(Header*);
+//static	void	mimemd5(Header*);
+static	void	mimetype(Header*);
+static	int	msgbodysize(Msg*);
+static	int	msgheader(Msg*, Header*, char*);
+
+/*
+ * stop list for header fields
+ */
+static	char	*headfieldstop = ":";
+static	char	*mimetokenstop = "()<>@,;:\\\"/[]?=";
+static	char	*headatomstop = "()<>@,;:\\\".[]";
+static	uchar	*headstr;
+static	uchar	*lastwhite;
+
+long
+selectfields(char *dst, long n, char *hdr, Slist *fields, int matches)
+{
+	char *s;
+	uchar *start;
+	long m, nf;
+	Slist *f;
+
+	headstr = (uchar*)hdr;
+	m = 0;
+	for(;;){
+		start = headstr;
+		s = headatom(headfieldstop);
+		if(s == nil)
+			break;
+		headskip();
+		for(f = fields; f != nil; f = f->next){
+			if(cistrcmp(s, f->s) == !matches){
+				nf = headstr - start;
+				if(m + nf > n)
+					return 0;
+				memmove(&dst[m], start, nf);
+				m += nf;
+			}
+		}
+		free(s);
+	}
+	if(m + 3 > n)
+		return 0;
+	dst[m++] = '\r';
+	dst[m++] = '\n';
+	dst[m] = '\0';
+	return m;
+}
+
+static Mimehdr*
+mkmimehdr(char *s, char *t, Mimehdr *next)
+{
+	Mimehdr *mh;
+
+	mh = MK(Mimehdr);
+	mh->s = s;
+	mh->t = t;
+	mh->next = next;
+	return mh;
+}
+
+static void
+freemimehdr(Mimehdr *mh)
+{
+	Mimehdr *last;
+
+	while(mh != nil){
+		last = mh;
+		mh = mh->next;
+		free(last->s);
+		free(last->t);
+		free(last);
+	}
+}
+
+static void
+freeheader(Header *h)
+{
+	freemimehdr(h->type);
+	freemimehdr(h->id);
+	freemimehdr(h->description);
+	freemimehdr(h->encoding);
+//	freemimehdr(h->md5);
+	freemimehdr(h->disposition);
+	freemimehdr(h->language);
+	free(h->buf);
+}
+
+static void
+freemaddr(Maddr *a)
+{
+	Maddr *p;
+
+	while(a != nil){
+		p = a;
+		a = a->next;
+		free(p->personal);
+		free(p->box);
+		free(p->host);
+		free(p);
+	}
+}
+
+void
+freemsg(Box *box, Msg *m)
+{
+	Msg *k, *last;
+
+	if(box != nil)
+		fstreedelete(box, m);
+	free(m->ibuf);
+	freemaddr(m->to);
+	if(m->replyto != m->from)
+		freemaddr(m->replyto);
+	if(m->sender != m->from)
+		freemaddr(m->sender);
+	if(m->from != m->unixfrom)
+		freemaddr(m->from);
+	freemaddr(m->unixfrom);
+	freemaddr(m->cc);
+	freemaddr(m->bcc);
+	free(m->unixdate);
+	freeheader(&m->head);
+	freeheader(&m->mime);
+	for(k = m->kids; k != nil; ){
+		last = k;
+		k = k->next;
+		freemsg(0, last);
+	}
+	free(m->fs);
+	free(m);
+}
+
+uint
+msgsize(Msg *m)
+{
+	return m->head.size + m->size;
+}
+
+char*
+maddrstr(Maddr *a)
+{
+	char *host, *addr;
+
+	host = a->host;
+	if(host == nil)
+		host = "";
+	if(a->personal != nil)
+		addr = smprint("%s <%s@%s>", a->personal, a->box, host);
+	else
+		addr = smprint("%s@%s", a->box, host);
+	return addr;
+}
+
+int
+msgfile(Msg *m, char *f)
+{
+	if(strlen(f) > Filelen)
+		bye("internal error: msgfile name too long");
+	strcpy(m->efs, f);
+	return cdopen(m->fsdir, m->fs, OREAD);
+}
+
+int
+msgismulti(Header *h)
+{
+	return h->type != nil && cistrcmp("multipart", h->type->s) == 0;
+}
+
+int
+msgis822(Header *h)
+{
+	Mimehdr *t;
+
+	t = h->type;
+	return t != nil && cistrcmp("message", t->s) == 0 && cistrcmp("rfc822", t->t) == 0;
+}
+
+/*
+ * check if a message has been deleted by someone else
+ */
+void
+msgdead(Msg *m)
+{
+	if(m->expunged)
+		return;
+	*m->efs = '\0';
+	if(!cdexists(m->fsdir, m->fs))
+		m->expunged = 1;
+}
+
+static long
+msgreadfile(Msg *m, char *file, char **ss)
+{
+	char *s, buf[Bufsize];
+	int fd;
+	long n, nn;
+	vlong length;
+	Dir *d;
+
+	fd = msgfile(m, file);
+	if(fd < 0){
+		msgdead(m);
+		return -1;
+	}
+
+	n = read(fd, buf, Bufsize);
+	if(n < Bufsize){
+		close(fd);
+		if(n < 0){
+			*ss = nil;
+			return -1;
+		}
+		s = emalloc(n + 1);
+		memmove(s, buf, n);
+		s[n] = '\0';
+		*ss = s;
+		return n;
+	}
+
+	d = dirfstat(fd);
+	if(d == nil){
+		close(fd);
+		return -1;
+	}
+	length = d->length;
+	free(d);
+	nn = length;
+	s = emalloc(nn + 1);
+	memmove(s, buf, n);
+	if(nn > n)
+		nn = readn(fd, s + n, nn - n) + n;
+	close(fd);
+	if(nn != length){
+		free(s);
+		return -1;
+	}
+	s[nn] = '\0';
+	*ss = s;
+	return nn;
+}
+
+/*
+ * parse the address in the unix header
+ * last line of defence, so must return something
+ */
+static Maddr *
+unixfrom(char *s)
+{
+	char *e, *t;
+	Maddr *a;
+
+	if(s == nil)
+		return nil;
+	headstr = (uchar*)s;
+	t = emalloc(strlen(s) + 2);
+	e = headaddrspec(t, nil);
+	if(e == nil)
+		a = nil;
+	else{
+		if(*e != '\0')
+			*e++ = '\0';
+		else
+			e = site;
+		a = MKZ(Maddr);
+		a->box = estrdup(t);
+		a->host = estrdup(e);
+	}
+	free(t);
+	return a;
+}
+
+/*
+ * retrieve information from the unixheader file
+ */
+static int
+msgunix(Msg *m, int top)
+{
+	char *s, *ss;
+	Tm tm;
+
+	if(m->unixdate != nil)
+		return 1;
+	if(!top){
+bogus:
+		m->unixdate = estrdup("");
+		m->unixfrom = unixfrom(nil);
+		return 1;
+	}
+
+	if(msgreadfile(m, "unixheader", &ss) < 0)
+		goto bogus;
+	s = ss;
+	s = strchr(s, ' ');
+	if(s == nil){
+		free(ss);
+		goto bogus;
+	}
+	s++;
+	m->unixfrom = unixfrom(s);
+	s = (char*)headstr;
+	if(date2tm(&tm, s) == nil)
+		s = m->info[Iunixdate];
+	if(s == nil){
+		free(ss);
+		goto bogus;
+	}
+	m->unixdate = estrdup(s);
+	free(ss);
+	return 1;
+}
+
+/*
+ * make sure the message has valid associated info
+ * used for Isubject, Idigest, Iinreplyto, Imessageid.
+ */
+int
+msginfo(Msg *m)
+{
+	char *s;
+	int i;
+
+	if(m->info[0] != nil)
+		return 1;
+	if(msgreadfile(m, "info", &m->ibuf) < 0)
+		return 0;
+	s = m->ibuf;
+	for(i = 0; i < Imax; i++){
+		m->info[i] = s;
+		s = strchr(s, '\n');
+		if(s == nil)
+			return 0;
+		if(s == m->info[i])
+			m->info[i] = 0;
+		*s++ = '\0';
+	}
+//	m->lines = strtoul(m->info[Ilines], 0, 0);
+//	m->size = strtoull(m->info[Isize], 0, 0);
+//	m->size += m->lines;			/* BOTCH: this hack belongs elsewhere */
+	return 1;
+}
+
+/*
+ * make sure the message has valid mime structure
+ * and sub-messages
+ */
+int
+msgstruct(Msg *m, int top)
+{
+	char buf[12];
+	int fd, ns, max;
+	Msg *k, head, *last;
+
+	if(m->kids != nil)
+		return 1;
+	if(m->expunged
+	|| !msginfo(m)
+	|| !msgheader(m, &m->mime, "mimeheader")){
+		msgdead(m);
+		return 0;
+	}
+	/* gack.  we need to get the header from the subpart here. */
+	if(msgis822(&m->mime)){
+		free(m->ibuf);
+		m->info[0] = 0;
+		m->efs = seprint(m->efs, m->efs + 5, "/1/");
+		if(!msginfo(m)){
+			msgdead(m);
+			return 0;
+		}
+	}
+	if(!msgunix(m, top)
+	|| !msgbodysize(m)
+	|| (top || msgis822(&m->mime) || msgismulti(&m->mime)) && !msgheader(m, &m->head, "rawheader")){
+		msgdead(m);
+		return 0;
+	}
+
+	/*
+	 * if a message has no kids, it has a kid which is just the body of the real message
+	 */
+	if(!msgismulti(&m->head) && !msgismulti(&m->mime) && !msgis822(&m->head) && !msgis822(&m->mime)){
+		k = MKZ(Msg);
+		k->id = 1;
+		k->fsdir = m->fsdir;
+		k->parent = m->parent;
+		ns = m->efs - m->fs;
+		k->fs = emalloc(ns + (Filelen + 1));
+		memmove(k->fs, m->fs, ns);
+		k->efs = k->fs + ns;
+		*k->efs = '\0';
+		k->size = m->size;
+		m->kids = k;
+		return 1;
+	}
+
+	/*
+	 * read in all child messages messages
+	 */
+	head.next = nil;
+	last = &head;
+	for(max = 1;; max++){
+		snprint(buf, sizeof buf, "%d", max);
+		fd = msgfile(m, buf);
+		if(fd == -1)
+			break;
+		close(fd);
+		m->efs[0] = 0;		/* BOTCH! */
+
+		k = MKZ(Msg);
+		k->id = max;
+		k->fsdir = m->fsdir;
+		k->parent = m;
+		ns = strlen(m->fs) + 2*(Filelen + 1);
+		k->fs = emalloc(ns);
+		k->efs = seprint(k->fs, k->fs + ns, "%s%d/", m->fs, max);
+		k->size = ~0UL;
+		k->lines = ~0UL;
+		last->next = k;
+		last = k;
+	}
+
+	m->kids = head.next;
+
+	/*
+	 * if kids fail, just whack them
+	 */
+	top = top && (msgis822(&m->head) || msgismulti(&m->head));
+	for(k = m->kids; k != nil; k = k->next)
+		if(!msgstruct(k, top)){
+			debuglog("kid fail %p %s", k, k->fs);
+			for(k = m->kids; k != nil; ){
+				last = k;
+				k = k->next;
+				freemsg(0, last);
+			}
+			m->kids = nil;
+			break;
+		}
+	return 1;
+}
+
+/*
+ *  stolen from upas/marshal; base64 encodes from one fd to another.
+ *
+ *  the size of buf is very important to enc64.  Anything other than
+ *  a multiple of 3 will cause enc64 to output a termination sequence.
+ *  To ensure that a full buf corresponds to a multiple of complete lines,
+ *  we make buf a multiple of 3*18 since that's how many enc64 sticks on
+ *  a single line.  This avoids short lines in the output which is pleasing
+ *  but not necessary.
+ */
+static int
+enc64x18(char *out, int lim, uchar *in, int n)
+{
+	int m, mm, nn;
+
+	nn = 0;
+	for(; n > 0; n -= m){
+		m = 18 * 3;
+		if(m > n)
+			m = n;
+		mm = enc64(out, lim - nn, in, m);
+		in += m;
+		out += mm;
+		*out++ = '\r';
+		*out++ = '\n';
+		nn += mm + 2;
+	}
+	return nn;
+}
+
+/*
+ * read in the message body to count \n without a preceding \r
+ */
+static int
+msgbodysize(Msg *m)
+{
+	char buf[Bufsize + 2], *s, *se;
+	uint length, size, lines, needr;
+	int n, fd, c;
+	Dir *d;
+
+	if(m->lines != ~0UL)
+		return 1;
+	fd = msgfile(m, "rawbody");
+	if(fd < 0)
+		return 0;
+	d = dirfstat(fd);
+	if(d == nil){
+		close(fd);
+		return 0;
+	}
+	length = d->length;
+	free(d);
+
+	size = 0;
+	lines = 0;
+	needr = 0;
+	buf[0] = ' ';
+	for(;;){
+		n = read(fd, &buf[1], Bufsize);
+		if(n <= 0)
+			break;
+		size += n;
+		se = &buf[n + 1];
+		for(s = &buf[1]; s < se; s++){
+			c = *s;
+			if(c == '\0')
+				*s = ' ';
+			if(c != '\n')
+				continue;
+			if(s[-1] != '\r')
+				needr++;
+			lines++;
+		}
+		buf[0] = buf[n];
+	}
+	if(size != length)
+		bye("bad length reading rawbody %d != %d; n %d %s", size, length, n, m->fs);
+	size += needr;
+	m->size = size;
+	m->lines = lines;
+	close(fd);
+	return 1;
+}
+
+/*
+ * prepend hdrname: val to the cached header
+ */
+static void
+msgaddhead(Msg *m, char *hdrname, char *val)
+{
+	char *s;
+	long size, n;
+
+	n = strlen(hdrname) + strlen(val) + 4;
+	size = m->head.size + n;
+	s = emalloc(size + 1);
+	snprint(s, size + 1, "%s: %s\r\n%s", hdrname, val, m->head.buf);
+	free(m->head.buf);
+	m->head.buf = s;
+	m->head.size = size;
+	m->head.lines++;
+}
+
+static void
+msgadddate(Msg *m)
+{
+	char buf[64];
+	Tm tm;
+
+	/* don't bother if we don't have a date */
+	if(m->info[Idate] == 0)
+		return;
+
+	date2tm(&tm, m->info[Idate]);
+	snprint(buf, sizeof buf, "%δ", &tm);
+	msgaddhead(m, "Date", buf);
+}
+
+/*
+ * read in the entire header,
+ * and parse out any existing mime headers
+ */
+static int
+msgheader(Msg *m, Header *h, char *file)
+{
+	char *s, *ss, *t, *te;
+	int dated, c;
+	long ns;
+	uint lines, n, nn;
+
+	if(h->buf != nil)
+		return 1;
+
+	ns = msgreadfile(m, file, &ss);
+	if(ns < 0)
+		return 0;
+	s = ss;
+	n = ns;
+
+	/*
+	 * count lines ending with \n and \r\n
+	 * add an extra line at the end, since upas/fs headers
+	 * don't have a terminating \r\n
+	 */
+	lines = 1;
+	te = s + ns;
+	for(t = s; t < te; t++){
+		c = *t;
+		if(c == '\0')
+			*t = ' ';
+		if(c != '\n')
+			continue;
+		if(t == s || t[-1] != '\r')
+			n++;
+		lines++;
+	}
+	if(t > s && t[-1] != '\n'){
+		if(t[-1] != '\r')
+			n++;
+		n++;
+	}
+	if(n > 0)
+		n += 2;
+	h->buf = emalloc(n + 1);
+	h->size = n;
+	h->lines = lines;
+
+	/*
+	 * make sure all headers end in \r\n
+	 */
+	nn = 0;
+	for(t = s; t < te; t++){
+		c = *t;
+		if(c == '\n'){
+			if(!nn || h->buf[nn - 1] != '\r')
+				h->buf[nn++] = '\r';
+			lines++;
+		}
+		h->buf[nn++] = c;
+	}
+	if(nn && h->buf[nn-1] != '\n'){
+		if(h->buf[nn-1] != '\r')
+			h->buf[nn++] = '\r';
+		h->buf[nn++] = '\n';
+	}
+	if(nn > 0){
+		h->buf[nn++] = '\r';
+		h->buf[nn++] = '\n';
+	}
+	h->buf[nn] = '\0';
+	if(nn != n)
+		bye("misconverted header %d %d", nn, n);
+	free(s);
+
+	/*
+	 * and parse some mime headers
+	 */
+	headstr = (uchar*)h->buf;
+	dated = 0;
+	while(s = headatom(headfieldstop)){
+		if(cistrcmp(s, "content-type") == 0)
+			mimetype(h);
+		else if(cistrcmp(s, "content-transfer-encoding") == 0)
+			mimeencoding(h);
+		else if(cistrcmp(s, "content-id") == 0)
+			mimeid(h);
+		else if(cistrcmp(s, "content-description") == 0)
+			mimedescription(h);
+		else if(cistrcmp(s, "content-disposition") == 0)
+			mimedisposition(h);
+//		else if(cistrcmp(s, "content-md5") == 0)
+//			mimemd5(h);
+		else if(cistrcmp(s, "content-language") == 0)
+			mimelanguage(h);
+		else if(h == &m->head){
+			if(cistrcmp(s, "from") == 0)
+				m->from = headmaddr(m->from);
+			else if(cistrcmp(s, "to") == 0)
+				m->to = headmaddr(m->to);
+			else if(cistrcmp(s, "reply-to") == 0)
+				m->replyto = headmaddr(m->replyto);
+			else if(cistrcmp(s, "sender") == 0)
+				m->sender = headmaddr(m->sender);
+			else if(cistrcmp(s, "cc") == 0)
+				m->cc = headmaddr(m->cc);
+			else if(cistrcmp(s, "bcc") == 0)
+				m->bcc = headmaddr(m->bcc);
+			else if(cistrcmp(s, "date") == 0)
+				dated = 1;
+		}
+		headskip();
+		free(s);
+	}
+
+	if(h == &m->head){
+		if(m->from == nil){
+			m->from = m->unixfrom;
+			if(m->from != nil){
+				s = maddrstr(m->from);
+				msgaddhead(m, "From", s);
+				free(s);
+			}
+		}
+		if(m->sender == nil)
+			m->sender = m->from;
+		if(m->replyto == nil)
+			m->replyto = m->from;
+
+		if(m->info[Idate] == 0)
+			m->info[Idate] = m->unixdate;
+		if(!dated && m->from != nil)
+			msgadddate(m);
+	}
+	return 1;
+}
+
+/*
+ * q is a quoted string.  remove enclosing " and and \ escapes
+ */
+static void
+stripquotes(char *q)
+{
+	char *s;
+	int c;
+
+	if(q == nil)
+		return;
+	s = q++;
+	while(c = *q++){
+		if(c == '\\'){
+			c = *q++;
+			if(!c)
+				return;
+		}
+		*s++ = c;
+	}
+	s[-1] = '\0';
+}
+
+/*
+ * parser for rfc822 & mime header fields
+ */
+
+/*
+ * params	:
+ *		| params ';' token '=' token
+ * 		| params ';' token '=' quoted-str
+ */
+static Mimehdr*
+mimeparams(void)
+{
+	char *s, *t;
+	Mimehdr head, *last;
+
+	head.next = nil;
+	last = &head;
+	for(;;){
+		if(headchar(1) != ';')
+			break;
+		s = headatom(mimetokenstop);
+		if(s == nil || headchar(1) != '='){
+			free(s);
+			break;
+		}
+		if(headchar(0) == '"'){
+			t = headquoted('"', '"');
+			stripquotes(t);
+		}else
+			t = headatom(mimetokenstop);
+		if(t == nil){
+			free(s);
+			break;
+		}
+		last->next = mkmimehdr(s, t, nil);
+		last = last->next;
+	}
+	return head.next;
+}
+
+/*
+ * type		: 'content-type' ':' token '/' token params
+ */
+static void
+mimetype(Header *h)
+{
+	char *s, *t;
+
+	if(headchar(1) != ':')
+		return;
+	s = headatom(mimetokenstop);
+	if(s == nil || headchar(1) != '/'){
+		free(s);
+		return;
+	}
+	t = headatom(mimetokenstop);
+	if(t == nil){
+		free(s);
+		return;
+	}
+	h->type = mkmimehdr(s, t, mimeparams());
+}
+
+/*
+ * encoding	: 'content-transfer-encoding' ':' token
+ */
+static void
+mimeencoding(Header *h)
+{
+	char *s;
+
+	if(headchar(1) != ':')
+		return;
+	s = headatom(mimetokenstop);
+	if(s == nil)
+		return;
+	h->encoding = mkmimehdr(s, nil, nil);
+}
+
+/*
+ * mailaddr	: ':' addresses
+ */
+static Maddr*
+headmaddr(Maddr *old)
+{
+	Maddr *a;
+
+	if(headchar(1) != ':')
+		return old;
+
+	if(headchar(0) == '\n')
+		return old;
+
+	a = headaddresses();
+	if(a == nil)
+		return old;
+
+	freemaddr(old);
+	return a;
+}
+
+/*
+ * addresses	: address | addresses ',' address
+ */
+static Maddr*
+headaddresses(void)
+{
+	Maddr *addr, *tail, *a;
+
+	addr = headaddress();
+	if(addr == nil)
+		return nil;
+	tail = addr;
+	while(headchar(0) == ','){
+		headchar(1);
+		a = headaddress();
+		if(a == nil){
+			freemaddr(addr);
+			return nil;
+		}
+		tail->next = a;
+		tail = a;
+	}
+	return addr;
+}
+
+/*
+ * address	: mailbox | group
+ * group	: phrase ':' mboxes ';' | phrase ':' ';'
+ * mailbox	: addr-spec
+ *		| optphrase '<' addr-spec '>'
+ *		| optphrase '<' route ':' addr-spec '>'
+ * optphrase	: | phrase
+ * route	: '@' domain
+ *		| route ',' '@' domain
+ * personal names are the phrase before '<',
+ * or a comment before or after a simple addr-spec
+ */
+static Maddr*
+headaddress(void)
+{
+	char *s, *e, *w, *personal;
+	uchar *hs;
+	int c;
+	Maddr *addr;
+
+	s = emalloc(strlen((char*)headstr) + 2);
+	e = s;
+	personal = headskipwhite(1);
+	c = headchar(0);
+	if(c == '<')
+		w = nil;
+	else{
+		w = headword();
+		c = headchar(0);
+	}
+	if(c == '.' || c == '@' || c == ',' || c == '\n' || c == '\0'){
+		lastwhite = headstr;
+		e = headaddrspec(s, w);
+		if(personal == nil){
+			hs = headstr;
+			headstr = lastwhite;
+			personal = headskipwhite(1);
+			headstr = hs;
+		}
+	}else{
+		if(c != '<' || w != nil){
+			free(personal);
+			if(!headphrase(e, w)){
+				free(s);
+				return nil;
+			}
+
+			/*
+			 * ignore addresses with groups,
+			 * so the only thing left if <
+			 */
+			c = headchar(1);
+			if(c != '<'){
+				free(s);
+				return nil;
+			}
+			personal = estrdup(s);
+		}else
+			headchar(1);
+
+		/*
+		 * after this point, we need to free personal before returning.
+		 * set e to nil to everything afterwards fails.
+		 *
+		 * ignore routes, they are useless, and heavily discouraged in rfc1123.
+		 * imap4 reports them up to, but not including, the terminating :
+		 */
+		e = s;
+		c = headchar(0);
+		if(c == '@'){
+			for(;;){
+				c = headchar(1);
+				if(c != '@'){
+					e = nil;
+					break;
+				}
+				headdomain(e);
+				c = headchar(1);
+				if(c != ','){
+					e = s;
+					break;
+				}
+			}
+			if(c != ':')
+				e = nil;
+		}
+
+		if(e != nil)
+			e = headaddrspec(s, nil);
+		if(headchar(1) != '>')
+			e = nil;
+	}
+
+	/*
+	 * e points to @host, or nil if an error occured
+	 */
+	if(e == nil){
+		free(personal);
+		addr = nil;
+	}else{
+		if(*e != '\0')
+			*e++ = '\0';
+		else
+			e = site;
+		addr = MKZ(Maddr);
+
+		addr->personal = personal;
+		addr->box = estrdup(s);
+		addr->host = estrdup(e);
+	}
+	free(s);
+	return addr;
+}
+
+/*
+ * phrase	: word
+ *		| phrase word
+ * w is the optional initial word of the phrase
+ * returns the end of the phrase, or nil if a failure occured
+ */
+static char*
+headphrase(char *e, char *w)
+{
+	int c;
+
+	for(;;){
+		if(w == nil){
+			w = headword();
+			if(w == nil)
+				return nil;
+		}
+		if(w[0] == '"')
+			stripquotes(w);
+		strcpy(e, w);
+		free(w);
+		w = nil;
+		e = strchr(e, '\0');
+		c = headchar(0);
+		if(c <= ' ' || strchr(headatomstop, c) != nil && c != '"')
+			break;
+		*e++ = ' ';
+		*e = '\0';
+	}
+	return e;
+}
+
+/*
+ * find the ! in domain!rest, where domain must have at least
+ * one internal '.'
+ */
+static char*
+dombang(char *s)
+{
+	int dot, c;
+
+	dot = 0;
+	for(; c = *s; s++){
+		if(c == '!'){
+			if(!dot || dot == 1 && s[-1] == '.' || s[1] == '\0')
+				return nil;
+			return s;
+		}
+		if(c == '"')
+			break;
+		if(c == '.')
+			dot++;
+	}
+	return nil;
+}
+
+/*
+ * addr-spec	: local-part '@' domain
+ *		| local-part			extension to allow ! and local names
+ * local-part	: word
+ *		| local-part '.' word
+ *
+ * if no '@' is present, rewrite d!e!f!u as @d,@e:u@f,
+ * where d, e, f are valid domain components.
+ * the @d,@e: is ignored, since routes are ignored.
+ * perhaps they should be rewritten as e!f!u@d, but that is inconsistent with upas.
+ *
+ * returns a pointer to '@', the end if none, or nil if there was an error
+ */
+static char*
+headaddrspec(char *e, char *w)
+{
+	char *s, *at, *b, *bang, *dom;
+	int c;
+
+	s = e;
+	for(;;){
+		if(w == nil){
+			w = headword();
+			if(w == nil)
+				return nil;
+		}
+		strcpy(e, w);
+		free(w);
+		w = nil;
+		e = strchr(e, '\0');
+		lastwhite = headstr;
+		c = headchar(0);
+		if(c != '.')
+			break;
+		headchar(1);
+		*e++ = '.';
+		*e = '\0';
+	}
+
+	if(c != '@'){
+		/*
+		 * extenstion: allow name without domain
+		 * check for domain!xxx
+		 */
+		bang = dombang(s);
+		if(bang == nil)
+			return e;
+
+		/*
+		 * if dom1!dom2!xxx, ignore dom1!
+		 */
+		dom = s;
+		for(; b = dombang(bang + 1); bang = b)
+			dom = bang + 1;
+
+		/*
+		 * convert dom!mbox into mbox@dom
+		 */
+		*bang = '@';
+		strrev(dom, bang);
+		strrev(bang + 1, e);
+		strrev(dom, e);
+		bang = &dom[e - bang - 1];
+		if(dom > s){
+			bang -= dom - s;
+			for(e = s; *e = *dom; e++)
+				dom++;
+		}
+
+		/*
+		 * eliminate a trailing '.'
+		 */
+		if(e[-1] == '.')
+			e[-1] = '\0';
+		return bang;
+	}
+	headchar(1);
+
+	at = e;
+	*e++ = '@';
+	*e = '\0';
+	if(!headdomain(e))
+		return nil;
+	return at;
+}
+
+/*
+ * domain	: sub-domain
+ *		| domain '.' sub-domain
+ * returns the end of the domain, or nil if a failure occured
+ */
+static char*
+headdomain(char *e)
+{
+	char *w;
+
+	for(;;){
+		w = headsubdomain();
+		if(w == nil)
+			return nil;
+		strcpy(e, w);
+		free(w);
+		e = strchr(e, '\0');
+		lastwhite = headstr;
+		if(headchar(0) != '.')
+			break;
+		headchar(1);
+		*e++ = '.';
+		*e = '\0';
+	}
+	return e;
+}
+
+/*
+ * id		: 'content-id' ':' msg-id
+ * msg-id	: '<' addr-spec '>'
+ */
+static void
+mimeid(Header *h)
+{
+	char *s, *e, *w;
+
+	if(headchar(1) != ':')
+		return;
+	if(headchar(1) != '<')
+		return;
+
+	s = emalloc(strlen((char*)headstr) + 3);
+	e = s;
+	*e++ = '<';
+	e = headaddrspec(e, nil);
+	if(e == nil || headchar(1) != '>'){
+		free(s);
+		return;
+	}
+	e = strchr(e, '\0');
+	*e++ = '>';
+	e[0] = '\0';
+	w = strdup(s);
+	free(s);
+	h->id = mkmimehdr(w, nil, nil);
+}
+
+/*
+ * description	: 'content-description' ':' *text
+ */
+static void
+mimedescription(Header *h)
+{
+	if(headchar(1) != ':')
+		return;
+	headskipwhite(0);
+	h->description = mkmimehdr(headtext(), nil, nil);
+}
+
+/*
+ * disposition	: 'content-disposition' ':' token params
+ */
+static void
+mimedisposition(Header *h)
+{
+	char *s;
+
+	if(headchar(1) != ':')
+		return;
+	s = headatom(mimetokenstop);
+	if(s == nil)
+		return;
+	h->disposition = mkmimehdr(s, nil, mimeparams());
+}
+
+/*
+ * md5		: 'content-md5' ':' token
+ */
+//static void
+//mimemd5(Header *h)
+//{
+//	char *s;
+//
+//	if(headchar(1) != ':')
+//		return;
+//	s = headatom(mimetokenstop);
+//	if(s == nil)
+//		return;
+//	h->md5 = mkmimehdr(s, nil, nil);
+//}
+
+/*
+ * language	: 'content-language' ':' langs
+ * langs	: token
+ *		| langs commas token
+ * commas	: ','
+ *		| commas ','
+ */
+static void
+mimelanguage(Header *h)
+{
+	char *s;
+	Mimehdr head, *last;
+
+	head.next = nil;
+	last = &head;
+	for(;;){
+		s = headatom(mimetokenstop);
+		if(s == nil)
+			break;
+		last->next = mkmimehdr(s, nil, nil);
+		last = last->next;
+		while(headchar(0) != ',')
+			headchar(1);
+	}
+	h->language = head.next;
+}
+
+/*
+ * token	: 1*<char 33-255, except "()<>@,;:\\\"/[]?=" aka mimetokenstop>
+ * atom		: 1*<chars 33-255, except "()<>@,;:\\\".[]" aka headatomstop>
+ * note this allows 8 bit characters, which occur in utf.
+ */
+static char*
+headatom(char *disallowed)
+{
+	char *s;
+	int c, ns, as;
+
+	headskipwhite(0);
+
+	s = emalloc(Stralloc);
+	as = Stralloc;
+	ns = 0;
+	for(;;){
+		c = *headstr++;
+		if(c <= ' ' || strchr(disallowed, c) != nil){
+			headstr--;
+			break;
+		}
+		s[ns++] = c;
+		if(ns >= as){
+			as += Stralloc;
+			s = erealloc(s, as);
+		}
+	}
+	if(ns == 0){
+		free(s);
+		return 0;
+	}
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * sub-domain	: atom | domain-lit
+ */
+static char *
+headsubdomain(void)
+{
+	if(headchar(0) == '[')
+		return headquoted('[', ']');
+	return headatom(headatomstop);
+}
+
+/*
+ * word	: atom | quoted-str
+ */
+static char *
+headword(void)
+{
+	if(headchar(0) == '"')
+		return headquoted('"', '"');
+	return headatom(headatomstop);
+}
+
+/*
+ * quoted-str	: '"' *(any char but '"\\\r', or '\' any char, or linear-white-space) '"'
+ * domain-lit	: '[' *(any char but '[]\\\r', or '\' any char, or linear-white-space) ']'
+ */
+static char *
+headquoted(int start, int stop)
+{
+	char *s;
+	int c, ns, as;
+
+	if(headchar(1) != start)
+		return nil;
+	s = emalloc(Stralloc);
+	as = Stralloc;
+	ns = 0;
+	s[ns++] = start;
+	for(;;){
+		c = *headstr;
+		if(c == stop){
+			headstr++;
+			break;
+		}
+		if(c == '\0'){
+			free(s);
+			return nil;
+		}
+		if(c == '\r'){
+			headstr++;
+			continue;
+		}
+		if(c == '\n'){
+			headstr++;
+			while(*headstr == ' ' || *headstr == '\t' || *headstr == '\r' || *headstr == '\n')
+				headstr++;
+			c = ' ';
+		}else if(c == '\\'){
+			headstr++;
+			s[ns++] = c;
+			c = *headstr;
+			if(c == '\0'){
+				free(s);
+				return nil;
+			}
+			headstr++;
+		}else
+			headstr++;
+		s[ns++] = c;
+		if(ns + 1 >= as){	/* leave room for \c or "0 */
+			as += Stralloc;
+			s = erealloc(s, as);
+		}
+	}
+	s[ns++] = stop;
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * headtext	: contents of rest of header line
+ */
+static char *
+headtext(void)
+{
+	uchar *v;
+	char *s;
+
+	v = headstr;
+	headtoend();
+	s = emalloc(headstr - v + 1);
+	memmove(s, v, headstr - v);
+	s[headstr - v] = '\0';
+	return s;
+}
+
+/*
+ * white space is ' ' '\t' or nested comments.
+ * skip white space.
+ * if com and a comment is seen,
+ * return it's contents and stop processing white space.
+ */
+static char*
+headskipwhite(int com)
+{
+	char *s;
+	int c, incom, as, ns;
+
+	s = nil;
+	as = Stralloc;
+	ns = 0;
+	if(com)
+		s = emalloc(Stralloc);
+	incom = 0;
+	for(; c = *headstr; headstr++){
+		switch(c){
+		case ' ':
+		case '\t':
+		case '\r':
+			c = ' ';
+			break;
+		case '\n':
+			c = headstr[1];
+			if(c != ' ' && c != '\t')
+				goto done;
+			c = ' ';
+			break;
+		case '\\':
+			if(com && incom)
+				s[ns++] = c;
+			c = headstr[1];
+			if(c == '\0')
+				goto done;
+			headstr++;
+			break;
+		case '(':
+			incom++;
+			if(incom == 1)
+				continue;
+			break;
+		case ')':
+			incom--;
+			if(com && !incom){
+				s[ns] = '\0';
+				return s;
+			}
+			break;
+		default:
+			if(!incom)
+				goto done;
+			break;
+		}
+		if(com && incom && (c != ' ' || ns > 0 && s[ns-1] != ' ')){
+			s[ns++] = c;
+			if(ns + 1 >= as){	/* leave room for \c or 0 */
+				as += Stralloc;
+				s = erealloc(s, as);
+			}
+		}
+	}
+done:
+	free(s);
+	return nil;
+}
+
+/*
+ * return the next non-white character
+ */
+static int
+headchar(int eat)
+{
+	int c;
+
+	headskipwhite(0);
+	c = *headstr;
+	if(eat && c != '\0' && c != '\n')
+		headstr++;
+	return c;
+}
+
+static void
+headtoend(void)
+{
+	uchar *s;
+	int c;
+
+	for(;;){
+		s = headstr;
+		c = *s++;
+		while(c == '\r')
+			c = *s++;
+		if(c == '\n'){
+			c = *s++;
+			if(c != ' ' && c != '\t')
+				return;
+		}
+		if(c == '\0')
+			return;
+		headstr = s;
+	}
+}
+
+static void
+headskip(void)
+{
+	int c;
+
+	while(c = *headstr){
+		headstr++;
+		if(c == '\n'){
+			c = *headstr;
+			if(c == ' ' || c == '\t')
+				continue;
+			return;
+		}
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/mutf7.c
@@ -1,0 +1,174 @@
+#include "imap4d.h"
+
+/* not compatable with characters outside the basic plane */
+
+/*
+ * modified utf-7, as per imap4 spec
+ * like utf-7, but substitues , for / in base 64,
+ * does not allow escaped ascii characters.
+ *
+ * /lib/rfc/rfc2152 is utf-7
+ * /lib/rfc/rfc1642 is obsolete utf-7
+ *
+ * test sequences from rfc1642
+ *	'A≢Α.'		'A&ImIDkQ-.'
+ *	'Hi Mom ☺!"	'Hi Mom &Jjo-!'
+ *	'日本語'		'&ZeVnLIqe-'
+ */
+
+static uchar mt64d[256];
+static char mt64e[64];
+
+static void
+initm64(void)
+{
+	int c, i;
+
+	memset(mt64d, 255, 256);
+	memset(mt64e, '=', 64);
+	i = 0;
+	for(c = 'A'; c <= 'Z'; c++){
+		mt64e[i] = c;
+		mt64d[c] = i++;
+	}
+	for(c = 'a'; c <= 'z'; c++){
+		mt64e[i] = c;
+		mt64d[c] = i++;
+	}
+	for(c = '0'; c <= '9'; c++){
+		mt64e[i] = c;
+		mt64d[c] = i++;
+	}
+	mt64e[i] = '+';
+	mt64d['+'] = i++;
+	mt64e[i] = ',';
+	mt64d[','] = i;
+}
+
+char*
+encmutf7(char *out, int lim, char *in)
+{
+	char *start, *e;
+	int nb;
+	ulong r, b;
+	Rune rr;
+
+	start = out;
+	e = out + lim;
+	if(mt64e[0] == 0)
+		initm64();
+	if(in)
+	for(;;){
+		r = *(uchar*)in;
+
+		if(r < ' ' || r >= Runeself){
+			if(r == 0)
+				break;
+			if(out + 1 >= e)
+				return 0;
+			*out++ = '&';
+			b = 0;
+			nb = 0;
+			for(;;){
+				in += chartorune(&rr, in);
+				r = rr;
+				if(r == 0 || r >= ' ' && r < Runeself)
+					break;
+				b = (b << 16) | r;
+				for(nb += 16; nb >= 6; nb -= 6){
+					if(out + 1 >= e)
+						return 0;
+					*out++ = mt64e[(b >> nb - 6) & 0x3f];
+				}
+			}
+			for(; nb >= 6; nb -= 6){
+				if(out + 1 >= e)
+					return 0;
+				*out++ = mt64e[(b >> nb - 6) & 0x3f];
+			}
+			if(nb){
+				if(out + 1 >= e)
+					return 0;
+				*out++ = mt64e[(b << 6 - nb) & 0x3f];
+			}
+
+			if(out + 1 >= e)
+				return 0;
+			*out++ = '-';
+			if(r == 0)
+				break;
+		}else
+			in++;
+		if(out + 1 >= e)
+			return 0;
+		*out = r;
+		out++;
+		if(r == '&')
+			*out++ = '-';
+	}
+	*out = 0;
+	if(!in || out >= e)
+		return 0;
+	return start;
+}
+
+char*
+decmutf7(char *out, int lim, char *in)
+{
+	char *start, *e;
+	int c, b, nb;
+	Rune rr;
+
+	start = out;
+	e = out + lim;
+	if(mt64e[0] == 0)
+		initm64();
+	if(in)
+	for(;;){
+		c = *in;
+
+		if(c < ' ' || c >= Runeself){
+			if(c == 0)
+				break;
+			return 0;
+		}
+		if(c != '&'){
+			if(out + 1 >= e)
+				return 0;
+			*out++ = c;
+			in++;
+			continue;
+		}
+		in++;
+		if(*in == '-'){
+			if(out + 1 >= e)
+				return 0;
+			*out++ = '&';
+			in++;
+			continue;
+		}
+
+		b = 0;
+		nb = 0;
+		while((c = *in++) != '-'){
+			c = mt64d[c];
+			if(c >= 64)
+				return 0;
+			b = (b << 6) | c;
+			nb += 6;
+			if(nb >= 16){
+				rr = b >> (nb - 16);
+				nb -= 16;
+				if(out + UTFmax + 1 >= e && out + runelen(rr) + 1 >= e)
+					return 0;
+				out += runetochar(out, &rr);
+			}
+		}
+		if(b & ((1 << nb) - 1))
+			return 0;
+	}
+	*out = 0;
+	if(!in || out >= e)
+		return 0;
+	return start;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/nlisttst.c
@@ -1,0 +1,94 @@
+#include "nlist.c"
+
+char	username[] = "quanstro";
+char	mboxdir[] = "/mail/box/quanstro/";
+Biobuf	bout;
+Bin	*parsebin;
+
+void
+bye(char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	Bprint(&bout, "* bye ");
+	Bvprint(&bout, fmt, arg);
+	Bprint(&bout, "\r\n");
+	Bflush(&bout);
+	exits(0);
+}
+
+static char *stoplist[] =
+{
+	".",
+	"dead.letter",
+	"forward",
+	"headers",
+	"imap.subscribed",
+	"mbox",
+	"names",
+	"pipefrom",
+	"pipeto",
+	0
+};
+int
+okmbox(char *path)
+{
+	char *name;
+	int i, c;
+
+	name = strrchr(path, '/');
+	if(name == nil)
+		name = path;
+	else
+		name++;
+	if(strlen(name) + STRLEN(".imp") >= Pathlen)
+		return 0;
+	for(i = 0; stoplist[i]; i++)
+		if(strcmp(name, stoplist[i]) == 0)
+			return 0;
+	c = name[0];
+	if(c == 0 || c == '-' || c == '/'
+	|| isdotdot(name)
+	|| isprefix("L.", name)
+	|| isprefix("imap-tmp.", name)
+	|| issuffix("-", name)
+	|| issuffix(".00", name)
+	|| issuffix(".imp", name)
+	|| issuffix(".idx", name))
+		return 0;
+
+	return 1;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: nlist ref pat\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int lsub;
+
+	lsub = 0;
+	ARGBEGIN{
+	case 'l':
+		lsub = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+	if(argc != 2)
+		usage();
+	Binit(&bout, 1, OWRITE);
+	quotefmtinstall();
+	if(lsub)
+		Bprint(&bout, "lsub→%d\n", lsubboxes("lsub", argv[0], argv[1]));
+	else
+		Bprint(&bout, "→%d\n", listboxes("list", argv[0], argv[1]));
+	Bterm(&bout);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/nodes.c
@@ -1,0 +1,184 @@
+#include "imap4d.h"
+
+int
+inmsgset(Msgset *ms, uint id)
+{
+	for(; ms; ms = ms->next)
+		if(ms->from <= id && ms->to >= id)
+			return 1;
+	return 0;
+}
+
+/*
+ * we can't rely on uids being in order, but short-circuting saves us
+ * very little.  we have a few tens of thousands of messages at most.
+ * also use the msg list as the outer loop to avoid 1:5,3:7 returning
+ * duplicates.  this is allowed, but silly.  and could be a problem for
+ * internal uses that aren't idempotent, like (re)moving messages.
+ */
+static int
+formsgsu(Box *box, Msgset *s, uint max, int (*f)(Box*, Msg*, int, void*), void *rock)
+{
+	int ok;
+	Msg *m;
+	Msgset *ms;
+
+	ok = 1;
+	for(m = box->msgs; m != nil && m->seq <= max; m = m->next)
+		for(ms = s; ms != nil; ms = ms->next)
+			if(m->uid >= ms->from && m->uid <= ms->to){
+				if(!f(box, m, 1, rock))
+					ok = 0;
+				break;
+			}
+	return ok;
+}
+
+int
+formsgsi(Box *box, Msgset *ms, uint max, int (*f)(Box*, Msg*, int, void*), void *rock)
+{
+	int ok, rok;
+	uint id;
+	Msg *m;
+
+	ok = 1;
+	for(; ms != nil; ms = ms->next){
+		id = ms->from;
+		rok = 0;
+		for(m = box->msgs; m != nil && m->seq <= max; m = m->next){
+			if(m->seq > id)
+				break;	/* optimization */
+			if(m->seq == id){
+				if(!f(box, m, 0, rock))
+					ok = 0;
+				if(id >= ms->to){
+					rok = 1;
+					break;	/* optimization */
+				}
+				if(ms->to == ~0UL)
+					rok = 1;
+				id++;
+			}
+		}
+		if(!rok)
+			ok = 0;
+	}
+	return ok;
+}
+
+/*
+ * iterated over all of the items in the message set.
+ * errors are accumulated, but processing continues.
+ * if uids, then ignore non-existent messages.
+ * otherwise, that's an error.  additional note from the
+ * rfc:
+ *
+ * “Servers MAY coalesce overlaps and/or execute the
+ * sequence in any order.”
+ */
+int
+formsgs(Box *box, Msgset *ms, uint max, int uids, int (*f)(Box*, Msg*, int, void*), void *rock)
+{
+	if(uids)
+		return formsgsu(box, ms, max, f, rock);
+	else
+		return formsgsi(box, ms, max, f, rock);
+}
+
+Store*
+mkstore(int sign, int op, int flags)
+{
+	Store *st;
+
+	st = binalloc(&parsebin, sizeof *st, 1);
+	if(st == nil)
+		parseerr("out of memory");
+	st->sign = sign;
+	st->op = op;
+	st->flags = flags;
+	return st;
+}
+
+Fetch *
+mkfetch(int op, Fetch *next)
+{
+	Fetch *f;
+
+	f = binalloc(&parsebin, sizeof *f, 1);
+	if(f == nil)
+		parseerr("out of memory");
+	f->op = op;
+	f->next = next;
+	return f;
+}
+
+Fetch*
+revfetch(Fetch *f)
+{
+	Fetch *last, *next;
+
+	last = nil;
+	for(; f != nil; f = next){
+		next = f->next;
+		f->next = last;
+		last = f;
+	}
+	return last;
+}
+
+Slist*
+mkslist(char *s, Slist *next)
+{
+	Slist *sl;
+
+	sl = binalloc(&parsebin, sizeof *sl, 0);
+	if(sl == nil)
+		parseerr("out of memory");
+	sl->s = s;
+	sl->next = next;
+	return sl;
+}
+
+Slist*
+revslist(Slist *sl)
+{
+	Slist *last, *next;
+
+	last = nil;
+	for(; sl != nil; sl = next){
+		next = sl->next;
+		sl->next = last;
+		last = sl;
+	}
+	return last;
+}
+
+int
+Bnlist(Biobuf *b, Nlist *nl, char *sep)
+{
+	char *s;
+	int n;
+
+	s = "";
+	n = 0;
+	for(; nl != nil; nl = nl->next){
+		n += Bprint(b, "%s%ud", s, nl->n);
+		s = sep;
+	}
+	return n;
+}
+
+int
+Bslist(Biobuf *b, Slist *sl, char *sep)
+{
+	char *s;
+	int n;
+
+	s = "";
+	n = 0;
+	for(; sl != nil; sl = sl->next){
+		n += Bprint(b, "%s%Z", s, sl->s);
+		s = sep;
+	}
+	return n;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/print.c
@@ -1,0 +1,129 @@
+#include "imap4d.h"
+
+int
+Ffmt(Fmt *f)
+{
+	char *s, buf[128], buf2[128];
+
+	s = va_arg(f->args, char*);
+	if(strncmp("/imap", s, 5) && strncmp("/pop", s, 4)){
+		snprint(buf, sizeof buf, "/mail/box/%s/%s", username, s);
+		s = buf;
+	}
+	snprint(buf2, sizeof buf2, "%q", s);
+	return fmtstrcpy(f, buf2);
+}
+
+enum {
+	Qok		= 0,
+	Qquote		= 1<<0,
+	Qbackslash	= 1<<1,
+	Qliteral		= 1<<2,
+};
+
+static int
+needtoquote(Rune r)
+{
+	if(r >= 0x7f || r == '\n' || r == '\r')
+		return Qliteral;
+	if(r <= ' ')
+		return Qquote;
+	if(r == '\\' || r == '"')
+		return Qbackslash;
+	return Qok;
+}
+
+int
+Zfmt(Fmt *f)
+{
+	char *s, *t, buf[Pathlen], buf2[Pathlen];
+	int w, quotes, alt;
+	Rune r;
+
+	s = va_arg(f->args, char*);
+	alt = f->flags & FmtSharp;
+	if(s == 0 && !alt)
+		return fmtstrcpy(f, "NIL");
+	if(s == 0 || *s == 0)
+		return fmtstrcpy(f, "\"\"");
+	switch(f->r){
+	case 'Y':
+		s = decfs(buf, sizeof buf, s);
+		s = encmutf7(buf2, sizeof buf2, s);
+		break;
+	}
+	quotes = 0;
+	for(t = s; *t; t += w){
+		w = chartorune(&r, t);
+		quotes |= needtoquote(r);
+		if(quotes & Qliteral && alt)
+			ilog("[%s] bad at [%s] %.2ux\n", s, t, r);
+	}
+	if(alt){
+		if(!quotes)
+			return fmtstrcpy(f, s);
+		if(quotes & Qliteral)
+			return fmtstrcpy(f, "GOK");
+	}else if(quotes & Qliteral)
+		return fmtprint(f, "{%lud}\r\n%s", strlen(s), s);
+
+	fmtrune(f, '"');
+	for(t = s; *t; t += w){
+		w = chartorune(&r, t);
+		if(needtoquote(r) == Qbackslash)
+			fmtrune(f, '\\');
+		fmtrune(f, r);
+	}
+	return fmtrune(f, '"');
+}
+
+int
+Xfmt(Fmt *f)
+{
+	char *s, buf[Pathlen], buf2[Pathlen];
+
+	s = va_arg(f->args, char*);
+	if(s == 0)
+		return fmtstrcpy(f, "NIL");
+	s = decmutf7(buf2, sizeof buf2, s);
+	cleanname(s);
+	return fmtstrcpy(f, encfs(buf, sizeof buf, s));
+}
+
+static char *day[] = {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
+};
+
+static char *mon[] = {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+int
+Dfmt(Fmt *f)
+{
+	char buf[128], *p, *e, *sgn, *fmt;
+	int off;
+	Tm *tm;
+
+	tm = va_arg(f->args, Tm*);
+	if(tm == nil)
+		tm = localtime(time(0));
+	sgn = "+";
+	if(tm->tzoff < 0)
+		sgn = "";
+	e = buf + sizeof buf;
+	p = buf;
+	off = (tm->tzoff/3600)*100 + (tm->tzoff/60)%60;
+	if((f->flags & FmtSharp) == 0){
+		/* rfc822 style */
+		fmt = "%.2d %s %.4d %.2d:%.2d:%.2d %s%.4d";
+		p = seprint(p, e, "%s, ", day[tm->wday]);
+	}else
+		fmt = "%2d-%s-%.4d %2.2d:%2.2d:%2.2d %s%4.4d";
+	seprint(p, e, fmt,
+		tm->mday, mon[tm->mon], tm->year + 1900, tm->hour, tm->min, tm->sec,
+		sgn, off);
+	if(f->r == L'δ')
+		fmtprint(f, "%s", buf);
+	return fmtprint(f, "%Z", buf);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/quota.c
@@ -1,0 +1,73 @@
+#include "imap4d.h"
+
+static int
+openpipe(int *pip, char *cmd, char *av[])
+{
+	int pid, fd[2];
+
+	if(pipe(fd) != 0)
+		sysfatal("pipe: %r");
+	pid = fork();
+	switch(pid){
+	case -1:
+		return -1;
+	case 0:
+		close(1);
+		dup(fd[1], 1);
+		if(fd[1] != 1)
+			close(fd[1]);
+		if(fd[0] != 0)
+			close(fd[0]);
+		exec(cmd, av);
+		ilog("exec: %r");
+		_exits("b0rked");
+		return -1;
+	default:
+		*pip = fd[0];
+		close(fd[1]);
+		return pid;
+	}
+}
+
+static int
+closepipe(int pid, int fd)
+{
+	int nz, wpid;
+	Waitmsg *w;
+
+	close(fd);
+	while(w = wait()){
+		nz = !w->msg || !w->msg[0];
+		wpid = w->pid;
+		free(w);
+		if(wpid == pid)
+			return nz? 0: -1;
+	}
+	return -1;
+}
+
+static char dupath[Pathlen];
+static char *duav[] = { "du", "-s", dupath, 0};
+
+vlong
+getquota(void)
+{
+	char buf[Pathlen + 128], *f[3];
+	int fd, pid;
+
+	werrstr("");
+	memset(buf, 0, sizeof buf);
+	snprint(dupath, sizeof dupath, "%s", mboxdir);
+	pid = openpipe(&fd, "/bin/du", duav);
+	if(pid == -1)
+		return -1;
+	if(read(fd, buf, sizeof buf) < 4){
+		closepipe(pid, fd);
+		return -1;
+	}
+	if(closepipe(pid, fd) == -1)
+		return -1;
+	if(getfields(buf, f, 2, 1, "\t") != 2)
+		return -1;
+	return strtoull(f[0], 0, 0);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/search.c
@@ -1,0 +1,329 @@
+#include "imap4d.h"
+
+static int
+filesearch(Msg *m, char *file, char *pat)
+{
+	char buf[Bufsize + 1];
+	int n, nbuf, npat, fd, ok;
+
+	npat = strlen(pat);
+	if(npat >= Bufsize/2)
+		return 0;
+
+	fd = msgfile(m, file);
+	if(fd < 0)
+		return 0;
+	ok = 0;
+	nbuf = 0;
+	for(;;){
+		n = read(fd, &buf[nbuf], Bufsize - nbuf);
+		if(n <= 0)
+			break;
+		nbuf += n;
+		buf[nbuf] = '\0';
+		if(cistrstr(buf, pat) != nil){
+			ok = 1;
+			break;
+		}
+		if(nbuf > npat){
+			memmove(buf, &buf[nbuf - npat], npat);
+			nbuf = npat;
+		}
+	}
+	close(fd);
+	return ok;
+}
+
+static int
+headersearch(Msg *m, char *hdr, char *pat)
+{
+	char *s, *t;
+	int ok, n;
+	Slist hdrs;
+
+	n = m->head.size + 3;
+	s = emalloc(n);
+	hdrs.next = nil;
+	hdrs.s = hdr;
+	ok = 0;
+	if(selectfields(s, n, m->head.buf, &hdrs, 1) > 0){
+		t = strchr(s, ':');
+		if(t != nil && cistrstr(t + 1, pat) != nil)
+			ok = 1;
+	}
+	free(s);
+	return ok;
+}
+
+static int
+addrsearch(Maddr *a, char *s)
+{
+	char *ok, *addr;
+
+	for(; a != nil; a = a->next){
+		addr = maddrstr(a);
+		ok = cistrstr(addr, s);
+		free(addr);
+		if(ok != nil)
+			return 1;
+	}
+	return 0;
+}
+
+static int
+datecmp(char *date, Search *s)
+{
+	Tm tm;
+
+	date2tm(&tm, date);
+	if(tm.year < s->year)
+		return -1;
+	if(tm.year > s->year)
+		return 1;
+	if(tm.mon < s->mon)
+		return -1;
+	if(tm.mon > s->mon)
+		return 1;
+	if(tm.mday < s->mday)
+		return -1;
+	if(tm.mday > s->mday)
+		return 1;
+	return 0;
+}
+
+enum{
+	Simp	= 0,
+	Sinfo	= 1<<0,
+	Sbody	= 1<<2,
+};
+
+int
+searchld(Search *s)
+{
+	int r;
+
+	for(r = 0; (r & Sbody) == 0 && s; s = s->next)
+	switch(s->key){
+	case SKall:
+	case SKanswered:
+	case SKdeleted:
+	case SKdraft:
+	case SKflagged:
+	case SKkeyword:
+	case SKnew:
+	case SKold:
+	case SKrecent:
+	case SKseen:
+	case SKunanswered:
+	case SKundeleted:
+	case SKundraft:
+	case SKunflagged:
+	case SKunkeyword:
+	case SKunseen:
+	case SKuid:
+	case SKset:
+		break;
+	case SKlarger:
+	case SKsmaller:
+	case SKbcc:
+	case SKcc:
+	case SKfrom:
+	case SKto:
+	case SKsubject:
+	case SKbefore:
+	case SKon:
+	case SKsince:
+	case SKsentbefore:
+	case SKsenton:
+	case SKsentsince:
+		r = Sinfo;
+		break;
+	case SKheader:
+		if(cistrcmp(s->hdr, "message-id") == 0)
+			r = Sinfo;
+		else
+			r = Sbody;
+		break;
+	case SKbody:
+		break;		/* msgstruct doesn't do us any good */
+	case SKtext:
+	default:
+		r = Sbody;
+		break;
+	case SKnot:
+		r = searchld(s->left);
+		break;
+	case SKor:
+		r = searchld(s->left) | searchld(s->right);
+		break;
+	}
+	return 0;
+}
+
+/* important speed hack for apple mail */
+int
+msgidsearch(char *s, char *hdr)
+{
+	char c;
+	int l, r;
+
+	l = strlen(s);
+	c = 0;
+	if(s[0] == '<' && s[l-1] == '>'){
+		l -= 2;
+		s += 1;
+		c = s[l-1];
+	}
+	r = hdr && strstr(s, hdr) != nil;
+	if(c)
+		s[l-1] = c;
+	return r;
+}
+
+/*
+ * free to exit, parseerr, since called before starting any client reply
+ *
+ * the header and envelope searches should convert mime character set escapes.
+ */
+int
+searchmsg(Msg *m, Search *s, int ld)
+{
+	uint ok, id;
+	Msgset *ms;
+
+	if(m->expunged)
+		return 0;
+	if(ld & Sbody){
+		if(!msgstruct(m, 1))
+			return 0;
+	}else if (ld & Sinfo){
+		if(!msginfo(m))
+			return 0;
+	}
+	for(ok = 1; ok && s != nil; s = s->next){
+		switch(s->key){
+		default:
+			ok = 0;
+			break;
+		case SKnot:
+			ok = !searchmsg(m, s->left, ld);
+			break;
+		case SKor:
+			ok = searchmsg(m, s->left, ld) || searchmsg(m, s->right, ld);
+			break;
+		case SKall:
+			ok = 1;
+			break;
+		case SKanswered:
+			ok = (m->flags & Fanswered) == Fanswered;
+			break;
+		case SKdeleted:
+			ok = (m->flags & Fdeleted) == Fdeleted;
+			break;
+		case SKdraft:
+			ok = (m->flags & Fdraft) == Fdraft;
+			break;
+		case SKflagged:
+			ok = (m->flags & Fflagged) == Fflagged;
+			break;
+		case SKkeyword:
+			ok = (m->flags & s->num) == s->num;
+			break;
+		case SKnew:
+			ok = (m->flags & (Frecent|Fseen)) == Frecent;
+			break;
+		case SKold:
+			ok = (m->flags & Frecent) != Frecent;
+			break;
+		case SKrecent:
+			ok = (m->flags & Frecent) == Frecent;
+			break;
+		case SKseen:
+			ok = (m->flags & Fseen) == Fseen;
+			break;
+		case SKunanswered:
+			ok = (m->flags & Fanswered) != Fanswered;
+			break;
+		case SKundeleted:
+			ok = (m->flags & Fdeleted) != Fdeleted;
+			break;
+		case SKundraft:
+			ok = (m->flags & Fdraft) != Fdraft;
+			break;
+		case SKunflagged:
+			ok = (m->flags & Fflagged) != Fflagged;
+			break;
+		case SKunkeyword:
+			ok = (m->flags & s->num) != s->num;
+			break;
+		case SKunseen:
+			ok = (m->flags & Fseen) != Fseen;
+			break;
+		case SKlarger:
+			ok = msgsize(m) > s->num;
+			break;
+		case SKsmaller:
+			ok = msgsize(m) < s->num;
+			break;
+		case SKbcc:
+			ok = addrsearch(m->bcc, s->s);
+			break;
+		case SKcc:
+			ok = addrsearch(m->cc, s->s);
+			break;
+		case SKfrom:
+			ok = addrsearch(m->from, s->s);
+			break;
+		case SKto:
+			ok = addrsearch(m->to, s->s);
+			break;
+		case SKsubject:
+			ok = cistrstr(m->info[Isubject], s->s) != nil;
+			break;
+		case SKbefore:
+			ok = datecmp(m->info[Iunixdate], s) < 0;
+			break;
+		case SKon:
+			ok = datecmp(m->info[Iunixdate], s) == 0;
+			break;
+		case SKsince:
+			ok = datecmp(m->info[Iunixdate], s) > 0;
+			break;
+		case SKsentbefore:
+			ok = datecmp(m->info[Idate], s) < 0;
+			break;
+		case SKsenton:
+			ok = datecmp(m->info[Idate], s) == 0;
+			break;
+		case SKsentsince:
+			ok = datecmp(m->info[Idate], s) > 0;
+			break;
+		case SKuid:
+			id = m->uid;
+			goto set;
+		case SKset:
+			id = m->seq;
+		set:
+			for(ms = s->set; ms != nil; ms = ms->next)
+				if(id >= ms->from && id <= ms->to)
+					break;
+			ok = ms != nil;
+			break;
+		case SKheader:
+			if(cistrcmp(s->hdr, "message-id") == 0)
+				ok = msgidsearch(s->s, m->info[Imessageid]);
+			else
+				ok = headersearch(m, s->hdr, s->s);
+			break;
+		case SKbody:
+		case SKtext:
+			if(s->key == SKtext && cistrstr(m->head.buf, s->s)){
+				ok = 1;
+				break;
+			}
+			ok = filesearch(m, "body", s->s);
+			break;
+		}
+	}
+	return ok;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/store.c
@@ -1,0 +1,119 @@
+#include "imap4d.h"
+
+static Namedint	flagmap[] =
+{
+	{"\\Seen",	Fseen},
+	{"\\Answered",	Fanswered},
+	{"\\Flagged",	Fflagged},
+	{"\\Deleted",	Fdeleted},
+	{"\\Draft",	Fdraft},
+	{"\\Recent",	Frecent},
+	{nil,		0}
+};
+
+int
+storemsg(Box *box, Msg *m, int uids, void *vst)
+{
+	int f, flags;
+	Store *st;
+
+	if(m->expunged)
+		return uids;
+	st = vst;
+	flags = st->flags;
+
+	f = m->flags;
+	if(st->sign == '+')
+		f |= flags;
+	else if(st->sign == '-')
+		f &= ~flags;
+	else
+		f = flags;
+
+	/*
+	 * not allowed to change the recent flag
+	 */
+	f = (f & ~Frecent) | (m->flags & Frecent);
+	setflags(box, m, f);
+
+	if(st->op != Stflagssilent){
+		m->sendflags = 1;
+		box->sendflags = 1;
+	}
+
+	return 1;
+}
+
+/*
+ * update flags & global flag counts in box
+ */
+void
+setflags(Box *box, Msg *m, int f)
+{
+	if(f == m->flags)
+		return;
+	box->dirtyimp = 1;
+	if((f & Frecent) != (m->flags & Frecent)){
+		if(f & Frecent)
+			box->recent++;
+		else
+			box->recent--;
+	}
+	m->flags = f;
+}
+
+void
+sendflags(Box *box, int uids)
+{
+	Msg *m;
+
+	if(!box->sendflags)
+		return;
+
+	box->sendflags = 0;
+	for(m = box->msgs; m != nil; m = m->next){
+		if(!m->expunged && m->sendflags){
+			Bprint(&bout, "* %ud FETCH (", m->seq);
+			if(uids)
+				Bprint(&bout, "uid %ud ", m->uid);
+			Bprint(&bout, "FLAGS (");
+			writeflags(&bout, m, 1);
+			Bprint(&bout, "))\r\n");
+			m->sendflags = 0;
+		}
+	}
+}
+
+void
+writeflags(Biobuf *b, Msg *m, int recentok)
+{
+	char *sep;
+	int f;
+
+	sep = "";
+	for(f = 0; flagmap[f].name != nil; f++){
+		if((m->flags & flagmap[f].v)
+		&& (flagmap[f].v != Frecent || recentok)){
+			Bprint(b, "%s%s", sep, flagmap[f].name);
+			sep = " ";
+		}
+	}
+}
+
+int
+msgseen(Box *box, Msg *m)
+{
+	if(m->flags & Fseen)
+		return 0;
+	m->flags |= Fseen;
+	box->sendflags = 1;
+	m->sendflags = 1;
+	box->dirtyimp = 1;
+	return 1;
+}
+
+uint
+mapflag(char *name)
+{
+	return mapint(flagmap, name);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/utils.c
@@ -1,0 +1,209 @@
+#include "imap4d.h"
+
+/*
+ * reverse string [s:e) in place
+ */
+void
+strrev(char *s, char *e)
+{
+	int c;
+
+	while(--e > s){
+		c = *s;
+		*s++ = *e;
+		*e = c;
+	}
+}
+
+int
+isdotdot(char *s)
+{
+	return s[0] == '.' && s[1] == '.' && (s[2] == '/' || s[2] == 0);
+}
+
+int
+issuffix(char *suf, char *s)
+{
+	int n;
+
+	n = strlen(s) - strlen(suf);
+	if(n < 0)
+		return 0;
+	return strcmp(s + n, suf) == 0;
+}
+
+int
+isprefix(char *pre, char *s)
+{
+	return strncmp(pre, s, strlen(pre)) == 0;
+}
+
+char*
+readfile(int fd)
+{
+	char *s;
+	long length;
+	Dir *d;
+
+	d = dirfstat(fd);
+	if(d == nil)
+		return nil;
+	length = d->length;
+	free(d);
+	s = binalloc(&parsebin, length + 1, 0);
+	if(s == nil || readn(fd, s, length) != length)
+		return nil;
+	s[length] = 0;
+	return s;
+}
+
+/*
+ * create the imap tmp file.
+ * it just happens that we don't need multiple temporary files.
+ */
+int
+imaptmp(void)
+{
+	char buf[ERRMAX], name[Pathlen];
+	int tries, fd;
+
+	snprint(name, sizeof name, "/mail/box/%s/mbox.tmp.imp", username);
+	for(tries = 0; tries < Locksecs*2; tries++){
+		fd = create(name, ORDWR|ORCLOSE|OCEXEC, DMEXCL|0600);
+		if(fd >= 0)
+			return fd;
+		errstr(buf, sizeof buf);
+		if(cistrstr(buf, "locked") == nil)
+			break;
+		sleep(500);
+	}
+	return -1;
+}
+
+/*
+ * open a file which might be locked.
+ * if it is, spin until available
+ */
+static char *etab[] = {
+	"not found",
+	"does not exist",
+	"file is locked",
+	"exclusive lock",
+	"already exists",
+};
+
+static int
+bad(int idx)
+{
+	char buf[ERRMAX];
+	int i;
+
+	rerrstr(buf, sizeof buf);
+	for(i = idx; i < nelem(etab); i++)
+		if(strstr(buf, etab[i]))
+			return 0;
+	return 1;
+}
+
+int
+openlocked(char *dir, char *file, int mode)
+{
+	int i, fd;
+
+	for(i = 0; i < 30; i++){
+		if((fd = cdopen(dir, file, mode)) >= 0 || bad(0))
+			return fd;
+		if((fd = cdcreate(dir, file, mode|OEXCL, DMEXCL|0600)) >= 0  || bad(2))
+			return fd;
+		sleep(1000);
+	}
+	werrstr("lock timeout");
+	return -1;
+}
+
+int
+fqid(int fd, Qid *qid)
+{
+	Dir *d;
+
+	d = dirfstat(fd);
+	if(d == nil)
+		return -1;
+	*qid = d->qid;
+	free(d);
+	return 0;
+}
+
+uint
+mapint(Namedint *map, char *name)
+{
+	int i;
+
+	for(i = 0; map[i].name != nil; i++)
+		if(cistrcmp(map[i].name, name) == 0)
+			break;
+	return map[i].v;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s) + 1);
+	strcpy(t, s);
+	return t;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		bye("server out of memory");
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void*
+ezmalloc(ulong n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		bye("server out of memory");
+	setmalloctag(p, getcallerpc(&n));
+	memset(p, 0, n);
+	return p;
+}
+
+void*
+erealloc(void *p, ulong n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		bye("server out of memory");
+	setrealloctag(p, getcallerpc(&p));
+	return p;
+}
+
+void
+setname(char *fmt, ...)
+{
+	char buf[128], buf2[32], *p;
+	int fd;
+	va_list arg;
+
+	va_start(arg, fmt);
+	p = vseprint(buf, buf + sizeof buf, fmt, arg);
+	va_end(arg);
+
+	snprint(buf2, sizeof buf2, "#p/%d/args", getpid());
+	if((fd = open(buf2, OWRITE)) >= 0){
+		write(fd, buf, p - buf);
+		close(fd);
+	}
+}
--- a/sys/src/cmd/upas/marshal/marshal.c
+++ b/sys/src/cmd/upas/marshal/marshal.c
@@ -196,7 +196,8 @@
 	Addr *to, *cc, *bcc;
 	Attach *first, **l, *a;
 	Biobuf in, out, *b;
-	String *file, *hdrstring;
+	String *hdrstring;
+	char file[Pathlen];
 
 	noinput = 0;
 	subject = nil;
@@ -343,10 +344,8 @@
 		bwritesfree(&out, &hdrstring);
 
 	/* read user's standard headers */
-	file = s_new();
-	mboxpath("headers", user, file, 0);
-	b = Bopen(s_to_c(file), OREAD);
-	if(b != nil){
+	mboxpathbuf(file, sizeof file, user, "headers");
+	if(b = Bopen(file, OREAD)){
 		if (readheaders(b, &flags, &hdrstring, nil, nil, nil, nil, 0) == Error)
 			fatal("reading");
 		Bterm(b);
@@ -1034,69 +1033,15 @@
 	return 0;
 }
 
-/* open the folder using the recipients account name */
-static int
-openfolder(char *rcvr)
-{
-	int c, fd, scarey;
-	char *p;
-	Dir *d;
-	String *file;
-
-	file = s_new();
-	mboxpath("f", user, file, 0);
-
-	/* if $mail/f exists, store there, otherwise in $mail */
-	d = dirstat(s_to_c(file));
-	if(d == nil || d->qid.type != QTDIR){
-		scarey = 1;
-		file->ptr -= 1;
-	} else {
-		s_putc(file, '/');
-		scarey = 0;
-	}
-	free(d);
-
-	p = strrchr(rcvr, '!');
-	if(p != nil)
-		rcvr = p+1;
-
-	while(*rcvr && *rcvr != '@'){
-		c = *rcvr++;
-		if(c == '/')
-			c = '_';
-		s_putc(file, c);
-	}
-	s_terminate(file);
-
-	if(scarey && special(file)){
-		fprint(2, "%s: won't overwrite %s\n", argv0, s_to_c(file));
-		s_free(file);
-		return -1;
-	}
-
-	fd = open(s_to_c(file), OWRITE);
-	if(fd < 0)
-		fd = create(s_to_c(file), OWRITE, 0660);
-
-	s_free(file);
-	return fd;
-}
-
 /* start up sendmail and return an fd to talk to it with */
 int
 sendmail(Addr *to, Addr *cc, Addr *bcc, int *pid, char *rcvr)
 {
-	int ac, fd;
-	int pfd[2];
-	char **av, **v;
+	int ac, fd, pfd[2];
+	char **v, cmd[Pathlen];
 	Addr *a;
-	String *cmd;
+	Biobuf *b;
 
-	fd = -1;
-	if(rcvr != nil)
-		fd = openfolder(rcvr);
-
 	ac = 0;
 	for(a = to; a != nil; a = a->next)
 		ac++;
@@ -1104,7 +1049,7 @@
 		ac++;
 	for(a = bcc; a != nil; a = a->next)
 		ac++;
-	v = av = emalloc(sizeof(char*)*(ac+20));
+	v = emalloc(sizeof(char*)*(ac+20));
 	ac = 0;
 	v[ac++] = "sendmail";
 	if(xflag)
@@ -1145,13 +1090,17 @@
 				break;
 			case 0:
 				close(pfd[0]);
-				seek(fd, 0, 2);
+				b = 0;
+				/* BOTCH; "From " time gets changed */
+				if(rcvr)
+					b = openfolder(foldername(nil, user, rcvr), time(0));
+				fd = b? Bfildes(b): -1;
 				printunixfrom(fd);
 				tee(0, pfd[1], fd);
 				write(fd, "\n", 1);
+				closefolder(b);
 				exits(0);
 			default:
-				close(fd);
 				close(pfd[1]);
 				dup(pfd[0], 0);
 				break;
@@ -1160,16 +1109,14 @@
 
 		if(replymsg != nil)
 			putenv("replymsg", replymsg);
-
-		cmd = mboxpath("pipefrom", login, s_new(), 0);
-		exec(s_to_c(cmd), av);
-		exec("/bin/myupassend", av);
-		exec("/bin/upas/send", av);
+		mboxpathbuf(cmd, sizeof cmd, login, "pipefrom");
+		exec(cmd, v);
+		exec("/bin/myupassend", v);
+		exec("/bin/upas/send", v);
 		fatal("execing: %r");
 		break;
 	default:
-		if(rcvr != nil)
-			close(fd);
+		free(v);
 		close(pfd[0]);
 		break;
 	}
@@ -1267,20 +1214,20 @@
 Alias*
 readaliases(void)
 {
+	char file[Pathlen];
 	Addr *addr, **al;
 	Alias *a, **l, *first;
 	Sinstack *sp;
-	String *file, *line, *token;
+	String *line, *token;
 	static int already;
 
 	first = nil;
-	file = s_new();
 	line = s_new();
 	token = s_new();
 
 	/* open and get length */
-	mboxpath("names", login, file, 0);
-	sp = s_allocinstack(s_to_c(file));
+	mboxpathbuf(file, Pathlen, login, "names");
+	sp = s_allocinstack(file);
 	if(sp == nil)
 		goto out;
 
@@ -1308,7 +1255,6 @@
 	}
 	s_freeinstack(sp);
 out:
-	s_free(file);
 	s_free(line);
 	s_free(token);
 	return first;
--- a/sys/src/cmd/upas/marshal/mkfile
+++ b/sys/src/cmd/upas/marshal/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=marshal
 
@@ -7,8 +8,6 @@
 HFILES=	../common/common.h\
 
 OFILES= marshal.$O
-
-BIN=/$objtype/bin/upas
 
 UPDATE=\
 	mkfile\
--- a/sys/src/cmd/upas/misc/go.fishing
+++ /dev/null
@@ -1,15 +1,0 @@
-#!/bin/rc
-# go.fishing - set up vacation responder
-rfork e
-
-cd /mail/box/$user
-if (test -e gone.fishing) {
-	echo $0: /mail/box/$user/gone.fishing already exists >[1=2]
-	exit 'already fishing'
-}
-
->gone.addrs
-chmod -a gone.addrs
->gone.addrs
-chmod +arw gone.addrs
->>gone.fishing
--- a/sys/src/cmd/upas/misc/gone.fishing
+++ /dev/null
@@ -1,19 +1,0 @@
-#!/bin/rc
-# gone.fishing local!$user /mail/box/$user/mbox - vacation responder
-#	as pipeto script
-
-# standard library.  saves the message on standard input in $TMP.msg and
-# parses it into /mail/fs/mbox/1.
-. /mail/lib/pipeto.lib $*
-
-{cat $TMP.msg; echo} >>/mail/box/$USER/gone.mail
-
-message=/mail/box/$USER/gone.msg
-if (! test -e $message)
-	message=/mail/lib/gone.msg
-
-MAILTO=`{cat $D/replyto}
-grep '^'$"MAILTO'$' /mail/box/$USER/gone.addrs >/dev/null >[2=1] || {
-	echo $MAILTO >>/mail/box/$USER/gone.addrs
-	mail $MAILTO <$message
-}
--- a/sys/src/cmd/upas/misc/gone.msg
+++ b/sys/src/cmd/upas/misc/gone.msg
@@ -1,5 +1,3 @@
-subject: away from my computer
-
 This is a recorded message.  I am currently out of contact with my
 computer system.  Your message to me has been saved and will be
 read upon my return.  This is the last time you will receive this
--- a/sys/src/cmd/upas/misc/mail
+++ /dev/null
@@ -1,12 +1,0 @@
-#!/bin/rc
-switch($#*){
-case 0
-	exec upas/nedmail
-}
-
-switch($1){
-case -f* -r* -c* -m*
-	exec upas/nedmail $*
-case *
-	exec upas/marshal -8 $*
-}
--- /dev/null
+++ b/sys/src/cmd/upas/misc/mail.rc
@@ -1,0 +1,12 @@
+#!/bin/rc
+switch($#*){
+case 0
+	exec upas/nedmail
+}
+
+switch($1){
+case -f* -r* -c* -m*
+	exec upas/nedmail $*
+case *
+	exec upas/marshal -8 $*
+}
--- a/sys/src/cmd/upas/misc/mkfile
+++ b/sys/src/cmd/upas/misc/mkfile
@@ -1,32 +1,35 @@
-RCFILES=mail\
-	go.fishing\
 
-all:VQ:
+RCFILES=mail.rc\
+
+all:Q:
 	;
 
-installall:VQ:	install
+installall:Q:	install
 	;
 
-install:V: mail go.fishing gone.msg gone.fishing
-	cp mail go.fishing /rc/bin
-	cp gone.msg gone.fishing /mail/lib
+install:V:
+	cp mail.rc /rc/bin/mail
 
-safeinstall:V: install
+safeinstall:V:
+	cp mail.rc /rc/bin/mail
 
-safeinstallall:V: install
+safeinstallall:V:
+	cp mail.rc /rc/bin/mail
 
-clean:VQ:
+clean:Q:
 	;
 nuke:V:
-	;	# rm /rc/bin/^(mail gone.fishing)
+	rm /rc/bin/mail
 
 UPDATE=\
-	go.fishing\
 	gone.fishing\
 	gone.msg\
-	mail\
+	mail.rc\
+	mail.sh\
+	makefile\
 	mkfile\
 	namefiles\
+	omail.rc\
 	qmail\
 	remotemail\
 	rewrite\
--- /dev/null
+++ b/sys/src/cmd/upas/misc/omail.rc
@@ -1,0 +1,14 @@
+#!/bin/rc
+switch($#*){
+case 0
+	exec upas/edmail -m
+}
+
+switch($1){
+case -F* -m* -f* -r* -p* -e* -c* -D*
+	exec upas/edmail -m $*
+case '-#'* -a*
+	exec upas/sendmail $*
+case *
+	exec upas/sendmail $*
+}
--- a/sys/src/cmd/upas/misc/unix/gone.fishing.sh
+++ /dev/null
@@ -1,9 +1,0 @@
-#!/bin/sh
-PATH=/bin:/usr/bin
-message=${1-/usr/lib/upas/gone.msg}
-return=`sed '2,$s/^From[ 	]/>&/'|tee -a $HOME/gone.mail|sed -n '1s/^From[ 	]\([^ 	]*\)[ 	].*$/\1/p'`
-echo '' >>$HOME/gone.mail
-grep "^$return" $HOME/gone.addrs >/dev/null 2>/dev/null || {
-	echo $return >>$HOME/gone.addrs
-	mail $return < $message
-}
--- a/sys/src/cmd/upas/misc/unix/mail.c
+++ /dev/null
@@ -1,51 +1,0 @@
-/*
- * #!/bin/sh
- * case $1 in
- * -n)
- * 	exit 0 ;;
- * -m*|-f*|-r*|-p*|-e*|"")
- * 	exec /usr/lib/upas/edmail $*
- * 	exit $? ;;
- * *)
- * 	exec /usr/lib/upas/send $*
- * 	exit $? ;;
- * esac
- */
-
-
-extern *UPASROOT;
-
-#define	EDMAIL	"edmail"
-#define	SEND	"send"
-
-main (argc, argv)
-	int argc;
-	char **argv;
-{
-	char *progname = SEND;
-	char realprog[500];
-
-	if (argc > 1) {
-		if (argv[1][0] == '-') {
-			switch (argv[1][1]) {
-			case 'n':
-				exit (0);
-
-			case 'm':
-			case 'f':
-			case 'r':
-			case 'p':
-			case 'e':
-			case '\0':
-				progname = EDMAIL;
-			}
-		}
-	} else
-		progname = EDMAIL;
-
-	sprint(realprog, "%s/%s", UPASROOT, progname);
-	execv (realprog, argv);
-	perror (realprog);
-	exit (1);
-}
-
--- a/sys/src/cmd/upas/misc/unix/mail.sh
+++ /dev/null
@@ -1,12 +1,0 @@
-#!/bin/sh
-case $1 in
--n)
-	exec LIBDIR/notify
-	exit $? ;;
--m*|-f*|-r*|-p*|-e*|"")
-	exec LIBDIR/edmail $*
-	exit $? ;;
-*)
-	exec LIBDIR/send $*
-	exit $? ;;
-esac
--- a/sys/src/cmd/upas/misc/unix/makefile
+++ /dev/null
@@ -1,44 +1,0 @@
-LIB=/usr/lib/upas
-CFLAGS=${UNIX} -g -I. -I../libc -I../common -I/usr/include -I/usr/include/sys
-LFLAGS=-g
-HOSTNAME=cat /etc/whoami
-
-.c.o: ; $(CC) -c $(CFLAGS) $*.c
-all: mail
-
-sedfile:
-	echo 's+LIBDIR+$(LIB)+g' >sed.file
-	echo 's+HOSTNAME+$(HOSTNAME)+g' >>sed.file
-
-install: sedfile install.fish install.mail.sh
-
-install.fish:
-	cp gone.msg $(LIB)
-	sed -f sed.file gone.fishing >$(LIB)/gone.fishing
-	-chmod 775 $(LIB)/gone.fishing
-	-chown bin $(LIB)/gone.fishing $(LIB)/gone.msg
-
-install.mail.sh:
-	sed -f sed.file mail.sh >/bin/mail
-	-chown bin /bin/mail
-	-chmod 775 /bin/mail
-
-install.notify: notify
-	cp notify $(LIB)/notify
-	-chmod 775 $(LIB)/notify
-	-chown bin $(LIB)/notify
-
-install.mail: mail
-	cp mail /bin
-	strip /bin/mail
-
-notify: notify.o
-	cc $(LFLAGS) notify.o -o notify
-
-mail: mail.o ../config/config.o
-	cc $(LFLAGS) mail.o ../config/config.o -o mail
-
-clean:
-	-rm -f *.[oOa] core a.out *.sL notify
-	-rm -f sed.file mail
-
--- a/sys/src/cmd/upas/mkfile
+++ b/sys/src/cmd/upas/mkfile
@@ -1,7 +1,27 @@
 </$objtype/mkfile
 
 LIBS=common
-PROGS=smtp alias fs ned misc q send scanmail pop3 ml marshal vf filterkit unesc bayes
+PROGS=\
+	Mail\
+	alias\
+	bayes\
+	binscripts\
+	filterkit\
+	fs\
+	imap4d\
+	marshal\
+	misc\
+	ml\
+	ned\
+	pop3\
+	q\
+	scanmail\
+	send\
+	smtp\
+	spf\
+	unesc\
+	vf\
+
 #libs must be made first
 DIRS=$LIBS $PROGS
 
--- /dev/null
+++ b/sys/src/cmd/upas/mkupas
@@ -1,0 +1,5 @@
+BIN=/$objtype/bin/upas
+ABIN=/acme/bin/$objtype
+
+../common/libcommon.a$O:
+	cd ../common; mk; mk clean
--- a/sys/src/cmd/upas/ml/common.c
+++ b/sys/src/cmd/upas/ml/common.c
@@ -4,10 +4,9 @@
 String*
 getaddr(Node *p)
 {
-	for(; p; p = p->next){
+	for(; p; p = p->next)
 		if(p->s && p->addr)
 			return p->s;
-	}
 	return nil;
 }
 
@@ -42,14 +41,15 @@
 		dirwstat(file, &nd);
 	} else
 		seek(fd, 0, 2);
-	if(rem)
+	if(rem){
+		sendnotification(addr, listname, rem);
 		fprint(fd, "!%s\n", addr);
-	else
+	}else{
 		fprint(fd, "%s\n", addr);
+		if(*addr != '#')
+			sendnotification(addr, listname, rem);
+	}
 	close(fd);
-
-	if(*addr != '#')
-		sendnotification(addr, listname, rem);
 }
 
 void
@@ -75,10 +75,9 @@
 	Addr **l;
 	Addr *a;
 
-	for(l = &al; *l; l = &(*l)->next){
+	for(l = &al; *l; l = &(*l)->next)
 		if(strcmp(addr, (*l)->addr) == 0)
 			return 0;
-	}
 	na++;
 	*l = a = malloc(sizeof(*a)+strlen(addr)+1);
 	if(a == nil)
@@ -113,27 +112,25 @@
 	Bterm(b);
 }
 
-/* start a mailer sending to all the receivers for list `name' */
+static void
+setsender(char *name)
+{
+	char *s;
+
+	s = smprint("%s-bounces", name);
+	putenv("upasname", s);
+	free(s);
+}
+
+/* start a mailer sending to all the receivers */
 int
 startmailer(char *name)
 {
-	int pfd[2];
 	char **av;
-	int ac;
+	int pfd[2], ac;
 	Addr *a;
 
-	/*
-	 * we used to send mail to the list from /dev/null,
-	 * which is equivalent to an smtp return address of <>,
-	 * but such a return address should only be used when
-	 * sending a bounce to a single address.  our smtpd lets
-	 * such mail through, but refuses mail from <> to multiple
-	 * addresses, since that's not allowed and is likely spam.
-	 * thus mailing list mail to another upas system with
-	 * multiple addressees was being rejected.
-	 */
-	putenv("upasname", smprint("%s-owner", name));
-
+	setsender(name);
 	if(pipe(pfd) < 0)
 		sysfatal("creating pipe: %r");
 	switch(fork()){
@@ -171,7 +168,7 @@
 	int pfd[2];
 	Waitmsg *w;
 
-	putenv("upasname", smprint("%s-owner", listname));
+	setsender(listname);
 	if(pipe(pfd) < 0)
 		sysfatal("creating pipe: %r");
 	switch(fork()){
--- a/sys/src/cmd/upas/ml/mkfile
+++ b/sys/src/cmd/upas/ml/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=ml\
 	mlowner\
@@ -7,7 +8,7 @@
 OFILES=\
 	common.$O\
 
-LIB=../common/libcommon.av\
+LIB=../common/libcommon.a$O\
 
 UHFILES= ../common/common.h\
 	../common/sys.h\
@@ -18,8 +19,6 @@
 
 LIB=../common/libcommon.a$O\
 
-BIN=/$objtype/bin/upas
-
 UPDATE=\
 	mkfile\
 	$UHFILES\
@@ -34,7 +33,4 @@
 $O.mlowner: ../smtp/rfc822.tab.$O
 
 ../smtp/y.tab.h ../smtp/rfc822.tab.$O:
-	@{
-		cd ../smtp
-		mk rfc822.tab.$O
-	}
+	cd ../smtp && mk rfc822.tab.$O
--- a/sys/src/cmd/upas/ml/ml.c
+++ b/sys/src/cmd/upas/ml/ml.c
@@ -1,18 +1,91 @@
 #include "common.h"
 #include "dat.h"
 
-Biobuf in;
+Biobuf	in;
+Addr	*al;
+int	na;
+String	*from;
+String	*sender;
 
-Addr *al;
-int na;
-String *from;
-String *sender;
+char*
+trim(char *s)
+{
+	while(*s == ' ' || *s == '\t')
+		s++;
+	return s;
+}
 
-void printmsg(int fd, String *msg, char *replyto, char *listname);
-void appendtoarchive(char* listname, String *firstline, String *msg);
-void printsubject(int fd, Field *f, char *listname);
+/* add the listname to the subject */
+void
+printsubject(int fd, Field *f, char *listname)
+{
+	char *s, *e, *ln;
+	Node *p;
 
+	if(f == nil || f->node == nil){
+		fprint(fd, "Subject: [%s]\n", listname);
+		return;
+	}
+	s = e = f->node->end + 1;
+	for(p = f->node; p; p = p->next)
+		e = p->end;
+	*e = 0;
+	ln = smprint("[%s]", listname);
+	if(ln != nil && strstr(s, ln) == nil)
+		fprint(fd, "Subject: %s %s\n", ln, trim(s));
+	else
+		fprint(fd, "Subject: %s\n", trim(s));
+	free(ln);
+	*e = '\n';
+}
+
+/* send message filtering Reply-to out of messages */
 void
+printmsg(int fd, String *msg, char *replyto, char *listname)
+{
+	Field *f, *subject;
+	Node *p;
+	char *cp, *ocp;
+
+	subject = nil;
+	cp = s_to_c(msg);
+	for(f = firstfield; f; f = f->next){
+		ocp = cp;
+		for(p = f->node; p; p = p->next)
+			cp = p->end+1;
+		switch(f->node->c){
+		case SUBJECT:
+			subject = f;
+		case REPLY_TO:
+		case PRECEDENCE:
+			continue;
+		}
+		write(fd, ocp, cp-ocp);
+	}
+	printsubject(fd, subject, listname);
+	fprint(fd, "Reply-To: %s\nPrecedence: bulk\n", replyto);
+	write(fd, cp, s_len(msg) - (cp - s_to_c(msg)));
+}
+
+/* if the mailbox exists, cat the mail to the end of it */
+void
+appendtoarchive(char* listname, String *firstline, String *msg)
+{
+	char *f;
+	Biobuf *b;
+
+	f = foldername(nil, listname, "mbox");
+	if(access(f, 0) < 0)
+		return;
+	if((b = openfolder(f, time(0))) == nil)
+		return;
+	Bwrite(b, s_to_c(firstline), s_len(firstline));
+	Bwrite(b, s_to_c(msg), s_len(msg));
+	Bwrite(b, "\n", 1);
+	closefolder(b);
+}
+
+void
 usage(void)
 {
 	fprint(2, "usage: %s address-list-file listname\n", argv0);
@@ -22,16 +95,21 @@
 void
 main(int argc, char **argv)
 {
-	String *msg;
-	String *firstline;
-	char *listname, *alfile;
+	char *listname, *alfile, *replytoname;
+	int fd, private;
+	String *msg, *firstline;
 	Waitmsg *w;
-	int fd;
-	char *replytoname = nil;
 
+	private = 0;
+	replytoname = nil;
 	ARGBEGIN{
+	default:
+		usage();
+	case 'p':
+		private = 1;
+		break;
 	case 'r':
-		replytoname = ARGF();
+		replytoname = EARGF(usage());
 		break;
 	}ARGEND;
 
@@ -56,8 +134,10 @@
 	if(s_read_line(&in, firstline) == nil)
 		sysfatal("reading input: %r");
 
-	/* read up to the first 128k of the message.  more is ridiculous. 
-	     Not if word documents are distributed.  Upped it to 2MB (pb) */
+	/*
+	 * read up to the first 128k of the message.  more is ridiculous. 
+	 *   Not if word documents are distributed.  Upped it to 2MB (pb)
+	 */
 	if(s_read(&in, msg, 2*1024*1024) <= 0)
 		sysfatal("reading input: %r");
 
@@ -73,7 +153,8 @@
 		sysfatal("message must contain From: or Sender:");
 	if(strcmp(listname, s_to_c(from)) == 0)
 		sysfatal("can't remail messages from myself");
-	addaddr(s_to_c(from));
+	if(addaddr(s_to_c(from)) != 0 && private)
+		sysfatal("not a list member");
 
 	/* start the mailer up and return a pipe to it */
 	fd = startmailer(listname);
@@ -92,76 +173,4 @@
 	/* if the mailbox exists, cat the mail to the end of it */
 	appendtoarchive(listname, firstline, msg);
 	exits(0);
-}
-
-/* send message filtering Reply-to out of messages */
-void
-printmsg(int fd, String *msg, char *replyto, char *listname)
-{
-	Field *f, *subject;
-	Node *p;
-	char *cp, *ocp;
-
-	subject = nil;
-	cp = s_to_c(msg);
-	for(f = firstfield; f; f = f->next){
-		ocp = cp;
-		for(p = f->node; p; p = p->next)
-			cp = p->end+1;
-		if(f->node->c == REPLY_TO)
-			continue;
-		if(f->node->c == PRECEDENCE)
-			continue;
-		if(f->node->c == SUBJECT){
-			subject = f;
-			continue;
-		}
-		write(fd, ocp, cp-ocp);
-	}
-	printsubject(fd, subject, listname);
-	fprint(fd, "Reply-To: %s\nPrecedence: bulk\n", replyto);
-	write(fd, cp, s_len(msg) - (cp - s_to_c(msg)));
-}
-
-/* if the mailbox exists, cat the mail to the end of it */
-void
-appendtoarchive(char* listname, String *firstline, String *msg)
-{
-	String *mbox;
-	int fd;
-
-	mbox = s_new();
-	mboxpath("mbox", listname, mbox, 0);
-	if(access(s_to_c(mbox), 0) < 0)
-		return;
-	fd = open(s_to_c(mbox), OWRITE);
-	if(fd < 0)
-		return;
-	s_append(msg, "\n");
-	write(fd, s_to_c(firstline), s_len(firstline));
-	write(fd, s_to_c(msg), s_len(msg));
-}
-
-/* add the listname to the subject */
-void
-printsubject(int fd, Field *f, char *listname)
-{
-	char *s, *e;
-	Node *p;
-	char *ln;
-
-	if(f == nil || f->node == nil){
-		fprint(fd, "Subject: [%s]\n", listname);
-		return;
-	}
-	s = e = f->node->end + 1;
-	for(p = f->node; p; p = p->next)
-		e = p->end;
-	*e = 0;
-	ln = smprint("[%s]", listname);
-	if(ln != nil && strstr(s, ln) == nil)
-		fprint(fd, "Subject: %s%s\n", ln, s);
-	else
-		fprint(fd, "Subject:%s\n", s);
-	free(ln);
 }
--- a/sys/src/cmd/upas/ml/mlmgr.c
+++ b/sys/src/cmd/upas/ml/mlmgr.c
@@ -1,12 +1,64 @@
 #include "common.h"
 #include "dat.h"
 
-int cflag;
-int aflag;
-int rflag;
+enum {
+	Bounces,
+	Owner,
+	List,
+	Nboxes,
+};
 
-int createpipeto(char *alfile, char *user, char *listname, int owner);
+char *suffix[Nboxes] = {
+[Bounces]	"-bounces",
+[Owner]		"-owner",
+[List]		"",
+};
 
+int
+createpipeto(char *alfile, char *user, char *listname, char *dom, int which)
+{
+	char buf[Pathlen], rflag[64];
+	int fd;
+	Dir *d;
+
+	mboxpathbuf(buf, sizeof buf, user, "pipeto");
+
+	fprint(2, "creating new pipeto: %s\n", buf);
+	fd = create(buf, OWRITE, 0775);
+	if(fd < 0)
+		return -1;
+	d = dirfstat(fd);
+	if(d == nil){
+		fprint(fd, "Couldn't stat %s: %r\n", buf);
+		return -1;
+	}
+	d->mode |= 0775;
+	if(dirfwstat(fd, d) < 0)
+		fprint(fd, "Couldn't wstat %s: %r\n", buf);
+	free(d);
+
+	if(dom != nil)
+		snprint(rflag, sizeof rflag, "-r%s@%s ", listname, dom);
+	else
+		rflag[0] = 0;
+
+	fprint(fd, "#!/bin/rc\n");
+	switch(which){
+	case Owner:
+		fprint(fd, "/bin/upas/mlowner %s %s\n", alfile, listname);
+		break;
+	case List:
+		fprint(fd, "/bin/upas/ml %s%s %s\n", rflag, alfile, user);
+		break;
+	case Bounces:
+		fprint(fd, "exit ''\n");
+		break;
+	}
+	close(fd);
+
+	return 0;
+}
+
 void
 usage(void)
 {
@@ -18,24 +70,24 @@
 void
 main(int argc, char **argv)
 {
-	char *listname, *addr;
-	String *owner, *alfile;
+	char *listname, *dom, *addr, alfile[Pathlen], buf[64], flag[127];
+	int i;
 
+
 	rfork(RFENVG|RFREND);
 
+	memset(flag, 0, sizeof flag);
 	ARGBEGIN{
 	case 'c':
-		cflag = 1;
-		break;
 	case 'r':
-		rflag = 1;
-		break;
 	case 'a':
-		aflag = 1;
+		flag[ARGC()] = 1;
 		break;
+	default:
+		usage();
 	}ARGEND;
 
-	if(aflag + rflag + cflag > 1){
+	if(flag['a'] + flag['r'] + flag['c'] > 1){
 		fprint(2, "%s: -a, -r, and -c are mutually exclusive\n", argv0);
 		exits("usage");
 	}
@@ -44,67 +96,31 @@
 		usage();
 
 	listname = argv[0];
-	alfile = s_new();
-	mboxpath("address-list", listname, alfile, 0);
+	if((dom = strchr(listname, '@')) != nil)
+		*dom++ = 0;
+	mboxpathbuf(alfile, sizeof alfile, listname, "address-list");
 
-	if(cflag){
-		owner = s_copy(listname);
-		s_append(owner, "-owner");
-		if(creatembox(listname, nil) < 0)
-			sysfatal("creating %s's mbox: %r", listname);
-		if(creatembox(s_to_c(owner), nil) < 0)
-			sysfatal("creating %s's mbox: %r", s_to_c(owner));
-		if(createpipeto(s_to_c(alfile), listname, listname, 0) < 0)
-			sysfatal("creating %s's pipeto: %r", s_to_c(owner));
-		if(createpipeto(s_to_c(alfile), s_to_c(owner), listname, 1) < 0)
-			sysfatal("creating %s's pipeto: %r", s_to_c(owner));
-		writeaddr(s_to_c(alfile), "# mlmgr c flag", 0, listname);
-	} else if(rflag){
+	if(flag['c']){
+		for(i = 0; i < Nboxes; i++){
+			snprint(buf, sizeof buf, "%s%s", listname, suffix[i]);
+			if(creatembox(buf, nil) < 0)
+				sysfatal("creating %s's mbox: %r", buf);
+			if(createpipeto(alfile, buf, listname, dom, i) < 0)
+				sysfatal("creating %s's pipeto: %r", buf);
+		}
+		writeaddr(alfile, "# mlmgr c flag", 0, listname);
+	} else if(flag['r']){
 		if(argc != 2)
 			usage();
 		addr = argv[1];
-		writeaddr(s_to_c(alfile), "# mlmgr r flag", 0, listname);
-		writeaddr(s_to_c(alfile), addr, 1, listname);
-	} else if(aflag){
+		writeaddr(alfile, "# mlmgr r flag", 0, listname);
+		writeaddr(alfile, addr, 1, listname);
+	} else if(flag['a']){
 		if(argc != 2)
 			usage();
 		addr = argv[1];
-		writeaddr(s_to_c(alfile), "# mlmgr a flag", 0, listname);
-		writeaddr(s_to_c(alfile), addr, 0, listname);
-	} else
-		usage();
-	exits(0);
-}
-
-int
-createpipeto(char *alfile, char *user, char *listname, int owner)
-{
-	String *f;
-	int fd;
-	Dir *d;
-
-	f = s_new();
-	mboxpath("pipeto", user, f, 0);
-	fprint(2, "creating new pipeto: %s\n", s_to_c(f));
-	fd = create(s_to_c(f), OWRITE, 0775);
-	if(fd < 0)
-		return -1;
-	d = dirfstat(fd);
-	if(d == nil){
-		fprint(fd, "Couldn't stat %s: %r\n", s_to_c(f));
-		return -1;
+		writeaddr(alfile, "# mlmgr a flag", 0, listname);
+		writeaddr(alfile, addr, 0, listname);
 	}
-	d->mode |= 0775;
-	if(dirfwstat(fd, d) < 0)
-		fprint(fd, "Couldn't wstat %s: %r\n", s_to_c(f));
-	free(d);
-
-	fprint(fd, "#!/bin/rc\n");
-	if(owner)
-		fprint(fd, "/bin/upas/mlowner %s %s\n", alfile, listname);
-	else
-		fprint(fd, "/bin/upas/ml %s %s\n", alfile, user);
-	close(fd);
-
-	return 0;
+	exits(0);
 }
--- a/sys/src/cmd/upas/ml/mlowner.c
+++ b/sys/src/cmd/upas/ml/mlowner.c
@@ -22,6 +22,8 @@
 	char *listname;
 
 	ARGBEGIN{
+	default:
+		usage();
 	}ARGEND;
 
 	rfork(RFENVG|RFREND);
--- a/sys/src/cmd/upas/ned/mkfile
+++ b/sys/src/cmd/upas/ned/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=nedmail
 
@@ -7,8 +8,6 @@
 HFILES=	../common/common.h\
 
 OFILES=nedmail.$O
-
-BIN=/$objtype/bin/upas
 
 UPDATE=\
 	mkfile\
--- a/sys/src/cmd/upas/ned/nedmail.c
+++ b/sys/src/cmd/upas/ned/nedmail.c
@@ -1,22 +1,21 @@
 #include "common.h"
 #include <ctype.h>
 #include <plumb.h>
+#include <regexp.h>
 
-typedef struct Message Message;
-typedef struct Ctype Ctype;
 typedef struct Cmd Cmd;
+typedef struct Ctype Ctype;
+typedef struct Dirstats Dirstats;
+typedef struct Message Message;
+typedef Message* (Mfn)(Cmd*,Message*);
 
-char	root[Pathlen];
-char	mbname[Elemlen];
-int	rootlen;
-int	didopen;
-char	*user;
-char	wd[2048];
-String	*mbpath;
-int	natural;
-int	doflush;
+enum{
+	/* nflags */
+	Nmissing	= 1<<0,
+	Nnoflags	= 1<<1,
 
-int interrupted;
+	Narg	= 32,
+};
 
 struct Message {
 	Message	*next;
@@ -24,11 +23,11 @@
 	Message	*cmd;
 	Message	*child;
 	Message	*parent;
-	String	*path;
+	char	*path;
 	int	id;
 	int	len;
-	int	fileno;	// number of directory
-	String	*info;
+	int	fileno;	/* number of directory */
+	char	*info;
 	char	*from;
 	char	*to;
 	char	*cc;
@@ -38,28 +37,34 @@
 	char	*type;
 	char	*disposition;
 	char	*filename;
-	char	deleted;
-	char	stored;
+	uchar	flags;
+	uchar	nflags;
 };
+#pragma varargck	type	"D"	Message*
 
-Message top;
+enum{
+	Display	= 1<<0,
+	Rechk	= 1<<1,	/* always use file to check content type */
+};
 
 struct Ctype {
 	char	*type;
 	char 	*ext;
-	int	display;
+	uchar	flag;
 	char	*plumbdest;
 	Ctype	*next;
 };
 
+/* first element is the default return value */
 Ctype ctype[] = {
-	{ "text/plain",			"txt",	1,	0	},
-	{ "text/html",			"htm",	1,	0	},
-	{ "text/html",			"html",	1,	0	},
-	{ "text/tab-separated-values",	"tsv",	1,	0	},
-	{ "text/richtext",		"rtx",	1,	0	},
-	{ "text/rtf",			"rtf",	1,	0	},
-	{ "text",			"txt",	1,	0	},
+	{ "application/octet-stream", 	"bin", 	Rechk, 	0,	0,	},
+	{ "text/plain",			"txt",	Display,	0	},
+	{ "text/html",			"htm",	Display,	0	},
+	{ "text/html",			"html",	Display,	0	},
+	{ "text/tab-separated-values",	"tsv",	Display,	0	},
+	{ "text/richtext",			"rtx",	Display,	0	},
+	{ "text/rtf",			"rtf",	Display,	0	},
+	{ "text",				"txt",	Display,	0	},
 	{ "message/rfc822",		"msg",	0,	0	},
 	{ "image/bmp",			"bmp",	0,	"image"	},
 	{ "image/jpg",			"jpg",	0,	"image"	},
@@ -66,123 +71,247 @@
 	{ "image/jpeg",			"jpg",	0,	"image"	},
 	{ "image/gif",			"gif",	0,	"image"	},
 	{ "image/png",			"png",	0,	"image"	},
+	{ "image/x-png",			"png",	0,	"image"	},
+	{ "image/tiff",			"tif",	0,	"image"	},
 	{ "application/pdf",		"pdf",	0,	"postscript"	},
-	{ "application/postscript",	"ps",	0,	"postscript"	},
-	{ "application/",		0,	0,	0	},
+	{ "application/postscript",		"ps",	0,	"postscript"	},
+	{ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",		"docx",	0,	"docx"	},
+	{ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",		"xlsx",	0,	"xlsx"	},
+	{ "application/",			0,	0,	0	},
 	{ "image/",			0,	0,	0	},
 	{ "multipart/",			"mul",	0,	0	},
 
 };
 
-Message*	acmd(Cmd*, Message*);
-Message*	bcmd(Cmd*, Message*);
-Message*	dcmd(Cmd*, Message*);
-Message*	eqcmd(Cmd*, Message*);
-Message*	hcmd(Cmd*, Message*);
-Message*	Hcmd(Cmd*, Message*);
-Message*	helpcmd(Cmd*, Message*);
-Message*	icmd(Cmd*, Message*);
-Message*	pcmd(Cmd*, Message*);
-Message*	qcmd(Cmd*, Message*);
-Message*	rcmd(Cmd*, Message*);
-Message*	scmd(Cmd*, Message*);
-Message*	ucmd(Cmd*, Message*);
-Message*	wcmd(Cmd*, Message*);
-Message*	xcmd(Cmd*, Message*);
-Message*	ycmd(Cmd*, Message*);
-Message*	pipecmd(Cmd*, Message*);
-Message*	rpipecmd(Cmd*, Message*);
-Message*	bangcmd(Cmd*, Message*);
-Message*	Pcmd(Cmd*, Message*);
-Message*	mcmd(Cmd*, Message*);
-Message*	fcmd(Cmd*, Message*);
-Message*	quotecmd(Cmd*, Message*);
+struct Dirstats {
+	int	new;
+	int	del;
+	int	old;
+	int	unread;
+};
 
+Mfn	acmd;
+Mfn	bangcmd;
+Mfn	bcmd;
+Mfn	dcmd;
+Mfn	eqcmd;
+Mfn	Fcmd;
+Mfn	fcmd;
+Mfn	fqcmd;
+Mfn	Hcmd;
+Mfn	hcmd;
+Mfn	helpcmd;
+Mfn	icmd;
+Mfn	Kcmd;
+Mfn	kcmd;
+Mfn	mbcmd;
+Mfn	mcmd;
+Mfn	Pcmd;
+Mfn	pcmd;
+Mfn	pipecmd;
+Mfn	qcmd;
+Mfn	quotecmd;
+Mfn	rcmd;
+Mfn	rpipecmd;
+Mfn	scmd;
+Mfn	tcmd;
+Mfn	ucmd;
+Mfn	wcmd;
+Mfn	xcmd;
+Mfn	ycmd;
+
 struct {
-	char		*cmd;
-	int		args;
-	Message*	(*f)(Cmd*, Message*);
-	char		*help;
+	char	*cmd;
+	int	args;
+	int	addr;
+	Mfn	*f;
+	char	*help;
 } cmdtab[] = {
-	{ "a",	1,	acmd,	"a        reply to sender and recipients" },
-	{ "A",	1,	acmd,	"A        reply to sender and recipients with copy" },
-	{ "b",	0,	bcmd,	"b        print the next 10 headers" },
-	{ "d",	0,	dcmd,	"d        mark for deletion" },
-	{ "f",	0,	fcmd,	"f        file message by from address" },
-	{ "h",	0,	hcmd,	"h        print elided message summary (,h for all)" },
-	{ "help", 0,	helpcmd, "help     print this info" },
-	{ "H",	0,	Hcmd,	"H        print message's MIME structure " },
-	{ "i",	0,	icmd,	"i        incorporate new mail" },
-	{ "m",	1,	mcmd,	"m addr   forward mail" },
-	{ "M",	1,	mcmd,	"M addr   forward mail with message" },
-	{ "p",	0,	pcmd,	"p        print the processed message" },
-	{ "P",	0,	Pcmd,	"P        print the raw message" },
-	{ "\"",	0,	quotecmd, "\"        print a quoted version of msg" },
-	{ "q",	0,	qcmd,	"q        exit and remove all deleted mail" },
-	{ "r",	1,	rcmd,	"r [addr] reply to sender plus any addrs specified" },
-	{ "rf",	1,	rcmd,	"rf [addr]file message and reply" },
-	{ "R",	1,	rcmd,	"R [addr] reply including copy of message" },
-	{ "Rf",	1,	rcmd,	"Rf [addr]file message and reply with copy" },
-	{ "s",	1,	scmd,	"s file   append raw message to file" },
-	{ "u",	0,	ucmd,	"u        remove deletion mark" },
-	{ "w",	1,	wcmd,	"w file   store message contents as file" },
-	{ "x",	0,	xcmd,	"x        exit without flushing deleted messages" },
-	{ "y",	0,	ycmd,	"y        synchronize with mail box" },
-	{ "=",	1,	eqcmd,	"=        print current message number" },
-	{ "|",	1,	pipecmd, "|cmd     pipe message body to a command" },
-	{ "||",	1,	rpipecmd, "||cmd     pipe raw message to a command" },
-	{ "!",	1,	bangcmd, "!cmd     run a command" },
-	{ nil,	0,	nil, 	nil },
+	{ "a",	1, 1,	acmd,	"a\t"		"reply to sender and recipients" },
+	{ "A",	1, 0,	acmd,	"A\t"		"reply to sender and recipients with copy" },
+	{ "b",	0, 0,	bcmd,	"b\t"		"print the next 10 headers" },
+	{ "d",	0, 1,	dcmd,	"d\t"		"mark for deletion" },
+	{ "F",	1, 1,	Fcmd,	"f\t"		"set message flags [+-][aDdfrSs]" },
+	{ "f",	0, 1,	fcmd,	"f\t"		"file message by from address" },
+	{ "fq",	0, 1,	fqcmd,	"fq\t"		"print mailbox f appends" },
+	{ "H",	0, 0,	Hcmd,	"H\t"		"print message's MIME structure" },
+	{ "h",	0, 0,	hcmd,	"h\t"		"print message summary (,h for all)" },
+	{ "help", 0, 0,	helpcmd, "help\t"		"print this info" },
+	{ "i",	0, 0,	icmd,	"i\t"		"incorporate new mail" },
+	{ "k",	1, 1,	kcmd,	"k [flags]\t"	"mark mail" },
+	{ "K",	1, 1,	Kcmd,	"K [flags]\t"	"unmark mail" },
+	{ "m",	1, 1,	mcmd,	"m addr\t"	"forward mail" },
+	{ "M",	1, 0,	mcmd,	"M addr\t"	"forward mail with message" },
+	{ "mb",	1, 0,	mbcmd,	"mb mbox\t"	"switch mailboxes" },
+	{ "p",	1, 0,	pcmd,	"p\t"		"print the processed message" },
+	{ "P",	0, 0,	Pcmd,	"P\t"		"print the raw message" },
+	{ "\"",	0, 0,	quotecmd, "\"\t"		"print a quoted version of msg" },
+	{ "\"\"",	0, 0,	quotecmd, "\"\"\t"		"format and quote message" },
+	{ "q",	0, 0,	qcmd,	"q\t"		"exit and remove all deleted mail" },
+	{ "r",	1, 1,	rcmd,	"r [addr]\t"	"reply to sender plus any addrs specified" },
+	{ "rf",	1, 0,	rcmd,	"rf [addr]\t"	"file message and reply" },
+	{ "R",	1, 0,	rcmd,	"R [addr]\t"	"reply including copy of message" },
+	{ "Rf",	1, 0,	rcmd,	"Rf [addr]\t"	"file message and reply with copy" },
+	{ "s",	1, 1,	scmd,	"s file\t"		"append raw message to file" },
+	{ "t",	1, 0,	tcmd,	"t\t"		"text formatter" },
+	{ "u",	0, 0,	ucmd,	"u\t"		"remove deletion mark" },
+	{ "w",	1, 1,	wcmd,	"w file\t"		"store message contents as file" },
+	{ "x",	0, 0,	xcmd,	"x\t"		"exit without flushing deleted messages" },
+	{ "y",	0, 0,	ycmd,	"y\t"		"synchronize with mail box" },
+	{ "=",	1, 0,	eqcmd,	"=\t"		"print current message number" },
+	{ "|",	1, 1,	pipecmd, "|cmd\t"		"pipe message body to a command" },
+	{ "||",	1, 1,	rpipecmd, "||cmd\t"	"pipe raw message to a command" },
+	{ "!",	1, 0,	bangcmd, "!cmd\t"		"run a command" },
 };
 
-enum
-{
-	NARG=	32,
-};
-
 struct Cmd {
 	Message	*msgs;
-	Message	*(*f)(Cmd*, Message*);
+	Mfn	*f;
 	int	an;
-	char	*av[NARG];
+	char	*av[Narg];
+	char	cmdline[2*1024];
 	int	delete;
 };
 
-Biobuf out;
-int startedfs;
-int reverse;
-int longestfrom = 12;
-
-String*		file2string(String*, char*);
-int		dir2message(Message*, int);
-int		filelen(String*, char*);
-String*		extendpath(String*, char*);
-void		snprintheader(char*, int, Message*);
-void		cracktime(char*, char*, int);
-int		cistrncmp(char*, char*, int);
-int		cistrcmp(char*, char*);
-Reprog*		parsesearch(char**);
-char*		parseaddr(char**, Message*, Message*, Message*, Message**);
+int		dir2message(Message*, int, Dirstats*);
+int		mdir2message(Message*);
+char*		extendp(char*, char*);
 char*		parsecmd(char*, Cmd*, Message*, Message*);
-char*		readline(char*, char*, int);
-void		messagecount(Message*);
 void		system(char*, char**, int);
-void		mkid(String*, Message*);
-int		switchmb(char*, char*);
+int		switchmb(char *, int);
 void		closemb(void);
-int		lineize(char*, char**, int);
-int		rawsearch(Message*, Reprog*);
 Message*	dosingleton(Message*, char*);
-String*		rooted(String*);
+char*		rooted(char*);
 int		plumb(Message*, Ctype*);
-String*		addrecolon(char*);
 void		exitfs(char*);
 Message*	flushdeleted(Message*);
 
+int	didopen;
+int	doflush;
+int	interrupted;
+int	longestfrom = 12;
+int	longestto = 12;
+int	hcmdfmt;
+Qid	mbqid;
+int	mbvers;
+char	mbname[Pathlen];
+char	mbpath[Pathlen];
+int	natural;
+Biobuf	out;
+int	reverse;
+char	root[Pathlen];
+int	rootlen;
+int	startedfs;
+Message	top;
+char	*user;
+char	homewd[Pathlen];
+char	wd[Pathlen];
+char	textfmt[Pathlen];
+
+char*
+idfmt(char *p, char *e, Message *m)
+{
+	char buf[32];
+	int sz, l;
+
+	for(; (sz = e - p) > 0; ){
+		l = snprint(buf, sizeof buf, "%d", m->id);
+		if(l + 1 > sz)
+			return "*GOK*";
+		e -= l;
+		memcpy(e, buf, l);
+		if((m = m->parent) == &top)
+			break;
+		e--;
+		*e = '.';
+	}
+	return e;
+}
+
+int
+eprint(char *fmt, ...)
+{
+	int n;
+	va_list args;
+
+	Bflush(&out);
+
+	va_start(args, fmt);
+	n = vfprint(2, fmt, args);
+	va_end(args);
+	return n;
+}
+
 void
+dissappeared(void)
+{
+	char buf[ERRMAX];
+
+	rerrstr(buf, sizeof buf);
+	if(strstr(buf, "hungup channel")){
+		eprint("\n!she's dead, jim\n");
+		exits(buf);
+	}
+	eprint("!message dissappeared\n");
+}
+
+int
+Dfmt(Fmt *f)
+{
+	char *e, buf[128];
+	Message *m;
+
+	m = va_arg(f->args, Message*);
+	if(m == nil)
+		return fmtstrcpy(f, "*GOK*");
+	if(m == &top)
+		return 0;
+	e = buf + sizeof buf - 1;
+	*e = 0;
+	return fmtstrcpy(f, idfmt(buf, e, m));
+}
+
+char*
+readline(char *prompt, char *line, int len)
+{
+	char *p, *e, *q;
+	int n, dump;
+
+	e = line + len;
+retry:
+	dump = 0;
+	interrupted = 0;
+	eprint("%s", prompt);
+	for(p = line;; p += n){
+		if(p == e){
+			dump = 1;
+			p = line;
+		}
+		n = read(0, p, e - p);
+		if(n < 0){
+			if(interrupted)
+				goto retry;
+			return nil;
+		}
+		if(n == 0)
+			return nil;
+		if(q = memchr(p, '\n', n)){
+			if(dump){
+				eprint("!line too long\n");
+				goto retry;
+			}
+			p = q;
+			break;
+		}
+	}
+	*p = 0;
+	return line;
+}
+
+void
 usage(void)
 {
-	fprint(2, "usage: %s [-nr] [-f mailfile] [-s mailfile]\n", argv0);
+	fprint(2, "usage: %s [-nrt] [-f mailfile] [-s mailfile]\n", argv0);
 	fprint(2, "       %s -c dir\n", argv0);
 	exits("usage");
 }
@@ -197,32 +326,27 @@
 	noted(NDFLT);
 }
 
-char *
+char*
 plural(int n)
 {
 	if (n == 1)
 		return "";
-
-	return "s";		
+	return "s";	
 }
 
 void
 main(int argc, char **argv)
 {
-	Message *cur, *m, *x;
-	char cmdline[4*1024];
+	char cmdline[2*1024], prompt[64], *err, *av[4], *mb;
+	int n, cflag, singleton;
 	Cmd cmd;
 	Ctype *cp;
-	char *err;
-	int n, cflag;
-	char *av[4];
-	String *prompt;
-	char *file, *singleton;
+	Message *cur, *m, *x;
 
 	Binit(&out, 1, OWRITE);
 
-	file = nil;
-	singleton = nil;
+	mb = nil;
+	singleton = 0;
 	reverse = 1;
 	cflag = 0;
 	ARGBEGIN {
@@ -229,12 +353,11 @@
 	case 'c':
 		cflag = 1;
 		break;
+	case 's':
+		singleton = 1;
 	case 'f':
-		file = EARGF(usage());
+		mb = EARGF(usage());
 		break;
-	case 's':
-		singleton = EARGF(usage());
-		break;
 	case 'r':
 		reverse = 0;
 		break;
@@ -242,11 +365,18 @@
 		natural = 1;
 		reverse = 0;
 		break;
+	case 't':
+		hcmdfmt = 1;
+		break;
 	default:
 		usage();
 		break;
 	} ARGEND;
 
+	fmtinstall('D', Dfmt);
+	quotefmtinstall();
+	doquote = needsrcquote;
+	getwd(homewd, sizeof homewd);
 	user = getlog();
 	if(user == nil || *user == 0)
 		sysfatal("can't read user name");
@@ -253,10 +383,10 @@
 
 	if(cflag){
 		if(argc > 0)
-			creatembox(user, argv[0]);
+			n = creatembox(user, argv[0]);
 		else
-			creatembox(user, nil);
-		exits(0);
+			n = creatembox(user, nil);
+		exits(n? 0: "fail");
 	}
 
 	if(argc)
@@ -270,57 +400,46 @@
 		system("/bin/upas/fs", av, -1);
 	}
 
-	switchmb(file, singleton);
+	switchmb(mb, singleton);
+	top.path = strdup(root);
+	for(cp = ctype; cp < ctype + nelem(ctype) - 1; cp++)
+		cp->next = cp + 1;
 
-	top.path = s_copy(root);
-
-	for(cp = ctype; cp < ctype+nelem(ctype)-1; cp++)
-		cp->next = cp+1;
-
-	if(singleton != nil){
-		cur = dosingleton(&top, singleton);
+	if(singleton){
+		cur = dosingleton(&top, mb);
 		if(cur == nil){
-			Bprint(&out, "no message\n");
+			eprint("no message\n");
 			exitfs(0);
 		}
 		pcmd(nil, cur);
 	} else {
 		cur = &top;
-		n = dir2message(&top, reverse);
-		if(n < 0)
-			sysfatal("can't read %s", s_to_c(top.path));
-		Bprint(&out, "%d message%s\n", n, plural(n));
+		if(icmd(nil, cur) == nil)
+			sysfatal("can't read %s", top.path);
 	}
 
-
 	notify(catchnote);
-	prompt = s_new();
 	for(;;){
-		s_reset(prompt);
-		if(cur == &top)
-			s_append(prompt, ": ");
-		else {
-			mkid(prompt, cur);
-			s_append(prompt, ": ");
-		}
+		snprint(prompt, sizeof prompt, "%D: ", cur);
 
-		// leave space at the end of cmd line in case parsecmd needs to
-		// add a space after a '|' or '!'
-		if(readline(s_to_c(prompt), cmdline, sizeof(cmdline)-1) == nil)
+		/*
+		 * leave space at the end of cmd line in case parsecmd needs to
+		 * add a space after a '|' or '!'
+		 */
+		if(readline(prompt, cmdline, sizeof cmdline - 1) == nil)
 			break;
 		err = parsecmd(cmdline, &cmd, top.child, cur);
 		if(err != nil){
-			Bprint(&out, "!%s\n", err);
+			eprint("!%s\n", err);
 			continue;
 		}
-		if(singleton != nil && cmd.f == icmd){
-			Bprint(&out, "!illegal command\n");
+		if(singleton && (cmd.f == icmd || cmd.f == ycmd)){
+			eprint("!illegal command\n");
 			continue;
 		}
 		interrupted = 0;
 		if(cmd.msgs == nil || cmd.msgs == &top){
-			x = (*cmd.f)(&cmd, &top);
-			if(x != nil)
+			if(x = cmd.f(&cmd, &top))
 				cur = x;
 		} else for(m = cmd.msgs; m != nil; m = m->cmd){
 			x = m;
@@ -327,13 +446,15 @@
 			if(cmd.delete){
 				dcmd(&cmd, x);
 
-				// dp acts differently than all other commands
-				// since its an old lesk idiom that people love.
-				// it deletes the current message, moves the current
-				// pointer ahead one and prints.
+				/*
+				 * dp acts differently than all other commands
+				 * since its an old lesk idiom that people love.
+				 * it deletes the current message, moves the current
+				 * pointer ahead one and prints.
+				 */
 				if(cmd.f == pcmd){
 					if(x->next == nil){
-						Bprint(&out, "!address\n");
+						eprint("!address\n");
 						cur = x;
 						break;
 					} else
@@ -340,12 +461,12 @@
 						x = x->next;
 				}
 			}
-			x = (*cmd.f)(&cmd, x);
+			x = cmd.f(&cmd, x);
 			if(x != nil)
 				cur = x;
 			if(interrupted)
 				break;
-			if(singleton != nil && (cmd.delete || cmd.f == dcmd))
+			if(singleton && (cmd.delete || cmd.f == dcmd))
 				qcmd(nil, nil);
 		}
 		if(doflush)
@@ -354,23 +475,76 @@
 	qcmd(nil, nil);
 }
 
-//
-// read the message info
-//
+char*
+file2string(char *dir, char *file)
+{
+	int fd, n;
+	char *s, *p, *e;
+
+	p = s = malloc(512);
+	e = p + 511;
+
+	fd = open(extendp(dir, file), OREAD);
+	while((n = read(fd, p, e - p)) > 0){
+		p += n;
+		if(p == e){
+			s = realloc(s, (n = p - s) + 512 + 1);
+			if(s == nil)
+				sysfatal("malloc: %r");
+			p = s + n;
+			e = p + 512;
+		}
+	}
+	close(fd);
+	*p = 0;
+	return s;
+}
+
+#define Fields 		18			/* terrible hack; worth 10% */
+#define Minfields	17
+
+void
+updateinfo(Message *m)
+{
+	char *s, *f[Fields + 1];
+	int i, n, sticky;
+
+	s = file2string(m->path, "info");
+	if(s == nil)
+		return;
+	if((n = getfields(s, f, nelem(f), 0, "\n")) < Minfields){
+		for(i = 0; i < n; i++)
+			fprint(2, "info: %s\n", f[i]);
+		sysfatal("info file invalid %s %D: %d fields", m->path, m, n);
+	}
+	if((m->nflags & Nnoflags) == 0){
+		sticky = m->flags & Fdeleted;
+		m->flags = buftoflags(f[17]) | sticky;
+	}
+	m->nflags &= ~Nmissing;
+	free(s);
+}
+
 Message*
 file2message(Message *parent, char *name)
 {
+	char *path, *f[Fields + 1];
+	int i, n;
 	Message *m;
-	String *path;
-	char *f[10];
 
-	m = mallocz(sizeof(Message), 1);
+	m = mallocz(sizeof *m, 1);
 	if(m == nil)
 		return nil;
-	m->path = path = extendpath(parent->path, name);
+	m->path = path = strdup(extendp(parent->path, name));
 	m->fileno = atoi(name);
 	m->info = file2string(path, "info");
-	lineize(s_to_c(m->info), f, nelem(f));
+	m->parent = parent;
+	n = getfields(m->info, f, nelem(f), 0, "\n");
+	if(n < Minfields){
+		for(i = 0; i < n; i++)
+			fprint(2, "info: [%s]\n", f[i]);
+		sysfatal("info file invalid %s %D: %d fields", path, m, n);
+	}
 	m->from = f[0];
 	m->to = f[1];
 	m->cc = f[2];
@@ -380,11 +554,15 @@
 	m->type = f[6];
 	m->disposition = f[7];
 	m->filename = f[8];
-	m->len = filelen(path, "raw");
-	if(strstr(m->type, "multipart") != nil || strcmp(m->type, "message/rfc822") == 0)
-		dir2message(m, 0);
-	m->parent = parent;
-
+	m->len = strtoul(f[16], 0, 0);
+	if(n > 17)
+		m->flags = buftoflags(f[17]);
+	else
+		m->nflags |= Nnoflags;
+
+	if(m->type)
+	if(strstr(m->type, "multipart") != nil || strcmp(m->type, "message/rfc822") == 0)
+		mdir2message(m);
 	return m;
 }
 
@@ -397,26 +575,27 @@
 		next = nm->next;
 		freemessage(nm);
 	}
-	s_free(m->path);
-	s_free(m->info);
+	free(m->path);
+	free(m->info);
 	free(m);
 }
 
-//
-//  read a directory into a list of messages
-//
+/*
+ * read a directory into a list of messages.  at the top level, there may be
+ * large gaps in message numbers.  so we need to read the whole directory.
+ * and pick out the messages we're interested in.  within a message, subparts
+ * are contiguous and if we don't read the header/body/rawbody, we can avoid forcing
+ * upas/fs to read the whole message.
+ */
 int
-dir2message(Message *parent, int reverse)
+mdir2message(Message *parent)
 {
-	int i, n, fd, highest, newmsgs;
+	char buf[Pathlen];
+	int i, highest, newmsgs;
 	Dir *d;
 	Message *first, *last, *m;
 
-	fd = open(s_to_c(parent->path), OREAD);
-	if(fd < 0)
-		return -1;
-
-	// count current entries
+	/* count current entries */
 	first = parent->child;
 	highest = newmsgs = 0;
 	for(last = parent->child; last != nil && last->next != nil; last = last->next)
@@ -425,51 +604,159 @@
 	if(last != nil)
 		if(last->fileno > highest)
 			highest = last->fileno;
-
-	n = dirreadall(fd, &d);
-	for(i = 0; i < n; i++){
-		if((d[i].qid.type & QTDIR) == 0)
+	for(i = highest + 1;; i++){
+		snprint(buf, sizeof buf, "%s/%d", parent->path, i);
+		if((d = dirstat(buf)) == nil)
+			break;
+		if((d->qid.type & QTDIR) == 0){
+			free(d);
 			continue;
-		if(atoi(d[i].name) <= highest)
-			continue;
-		m = file2message(parent, d[i].name);
+		}
+		free(d);
+		snprint(buf, sizeof buf, "%d", i);
+		m = file2message(parent, buf);
 		if(m == nil)
 			break;
+		m->id = m->fileno;
 		newmsgs++;
-		if(reverse){
-			m->next = first;
-			if(first != nil)
-				first->prev = m;
+		if(first == nil)
 			first = m;
-		} else {
-			if(first == nil)
+		else
+			last->next = m;
+		m->prev = last;
+		last = m;
+	}
+	parent->child = first;
+	return newmsgs;
+}
+
+/*
+ * 99.9% of the time, we don't need to sort this list.
+ * however, sometimes email is added to a mailbox
+ * out of order.  or, sape copies it back in from the
+ * dump.  in this case, we've got to sort.
+ *
+ * BOTCH.  we're not observing any sort of stable
+ * order.  if an old message comes in while upas/fs
+ * is running, it will appear out of order.  restarting
+ * upas/fs will reorder things.
+ */
+int
+dcmp(Dir *a, Dir *b)
+{
+	return atoi(a->name) - atoi(b->name);
+}
+
+void
+itsallsapesfault(Dir *d, int n)
+{
+	int c, i, r, t;
+
+	/* evade qsort suck */
+	r = -1;
+	for(i = 0; i < n; i++){
+		t = atol(d[i].name);
+		if(t > r){
+			c = d[i].name[0];
+			if(c >= '0' && c <= 9)
+				break;
+		}
+		r = t;
+	}
+	if(i != n)
+		qsort(d, n, sizeof *d, (int (*)(void*, void*))dcmp);
+}
+
+int
+dir2message(Message *parent, int reverse, Dirstats *s)
+{
+	int i, c, n, fd;
+	Dir *d;
+	Message *first, *last, *m, **ll;
+
+	memset(s, 0, sizeof *s);
+	fd = open(parent->path, OREAD);
+	if(fd < 0)
+		return -1;
+	first = parent->child;
+	last = nil;
+	if(first)
+		for(last = first; last->next; last = last->next)
+			;
+	n = dirreadall(fd, &d);
+	itsallsapesfault(d, n);
+	if(reverse)
+		ll = &last;
+	else
+		ll = &parent->child;
+	for(i = 0; *ll || i < n; ){
+		if(i < n && (d[i].qid.type & QTDIR) == 0){
+			i++;
+			continue;
+		}
+		c = -1;
+		if(i >= n)
+			c = 1;
+		else if(*ll)
+			c = atoi(d[i].name) - (*ll)->fileno;
+		if(c < 0){
+			m = file2message(parent, d[i].name);
+			if(m == nil)
+				break;
+			if(reverse){
+				m->next = first;
+				if(first != nil)
+					first->prev = m;
 				first = m;
-			else
-				last->next = m;
-			m->prev = last;
-			last = m;
+			}else{
+				if(first == nil)
+					first = m;
+				else
+					last->next = m;
+				m->prev = last;
+				last = m;
+			}
+			*ll = m;
+			s->new++;
+			s->unread += (m->flags & Fseen) == 0;
+			i++;
+		}else if(c > 0){
+			(*ll)->nflags |= Nmissing;
+			s->del++;
+		}else{
+			updateinfo(*ll);
+			s->old++;
+			i++;
 		}
+
+		if(reverse)
+			ll = &(*ll)->prev;
+		else
+			ll = &(*ll)->next;
 	}
 	free(d);
 	close(fd);
 	parent->child = first;
 
-	// renumber and file longest from
+	/* renumber and file longest from */
 	i = 1;
 	longestfrom = 12;
+	longestto = 12;
 	for(m = first; m != nil; m = m->next){
 		m->id = natural ? m->fileno : i++;
 		n = strlen(m->from);
 		if(n > longestfrom)
 			longestfrom = n;
+		n = strlen(m->to);
+		if(n > longestto)
+			longestto = n;
 	}
-
-	return newmsgs;
+	return 0;
 }
 
-//
-//  point directly to a message
-//
+/*
+ *   point directly to a message
+ */
 Message*
 dosingleton(Message *parent, char *path)
 {
@@ -476,12 +763,12 @@
 	char *p, *np;
 	Message *m;
 
-	// walk down to message and read it
+	/* walk down to message and read it */
 	if(strlen(path) < rootlen)
 		return nil;
 	if(path[rootlen] != '/')
 		return nil;
-	p = path+rootlen+1;
+	p = path + rootlen + 1;
 	np = strchr(p, '/');
 	if(np != nil)
 		*np = 0;
@@ -491,14 +778,14 @@
 	parent->child = m;
 	m->id = 1;
 
-	// walk down to requested component
+	/* walk down to requested component */
 	while(np != nil){
 		*np = '/';
-		np = strchr(np+1, '/');
+		np = strchr(np + 1, '/');
 		if(np != nil)
 			*np = 0;
 		for(m = m->child; m != nil; m = m->next)
-			if(strcmp(path, s_to_c(m->path)) == 0)
+			if(strcmp(path, m->path) == 0)
 				return m;
 		if(m == nil)
 			return nil;
@@ -506,103 +793,21 @@
 	return m;
 }
 
-//
-//  read a file into a string
-//
-String*
-file2string(String *dir, char *file)
+/*
+ *   walk the path name an element
+ */
+char*
+extendp(char *dir, char *name)
 {
-	String *s;
-	int fd, n, m;
+	static char buf[Pathlen];
 
-	s = extendpath(dir, file);
-	fd = open(s_to_c(s), OREAD);
-	s_grow(s, 512);			/* avoid multiple reads on info files */
-	s_reset(s);
-	if(fd < 0)
-		return s;
-
-	for(;;){
-		n = s->end - s->ptr;
-		if(n == 0){
-			s_grow(s, 128);
-			continue;
-		}
-		m = read(fd, s->ptr, n);
-		if(m <= 0)
-			break;
-		s->ptr += m;
-		if(m < n)
-			break;
-	}
-	s_terminate(s);
-	close(fd);
-
-	return s;
+	if(strcmp(dir, ".") == 0)
+		snprint(buf, sizeof buf, "%s", name);
+	else
+		snprint(buf, sizeof buf, "%s/%s", dir, name);
+	return buf;
 }
 
-//
-//  get the length of a file
-//
-int
-filelen(String *dir, char *file)
-{
-	String *path;
-	Dir *d;
-	int rv;
-
-	path = extendpath(dir, file);
-	d = dirstat(s_to_c(path));
-	if(d == nil){
-		s_free(path);
-		return -1;
-	}
-	s_free(path);
-	rv = d->length;
-	free(d);
-	return rv;
-}
-
-//
-//  walk the path name an element
-//
-String*
-extendpath(String *dir, char *name)
-{
-	String *path;
-
-	if(strcmp(s_to_c(dir), ".") == 0)
-		path = s_new();
-	else {
-		path = s_copy(s_to_c(dir));
-		s_append(path, "/");
-	}
-	s_append(path, name);
-	return path;
-}
-
-int
-cistrncmp(char *a, char *b, int n)
-{
-	while(n-- > 0){
-		if(tolower(*a++) != tolower(*b++))
-			return -1;
-	}
-	return 0;
-}
-
-int
-cistrcmp(char *a, char *b)
-{
-	for(;;){
-		if(tolower(*a) != tolower(*b++))
-			return -1;
-		if(*a++ == 0)
-			break;
-	}
-	return 0;
-}
-
 char*
 nosecs(char *t)
 {
@@ -611,7 +816,7 @@
 	p = strchr(t, ':');
 	if(p == nil)
 		return t;
-	p = strchr(p+1, ':');
+	p = strchr(p + 1, ':');
 	if(p != nil)
 		*p = 0;
 	return t;
@@ -630,33 +835,31 @@
 
 	for(i = 0; i < 12; i++)
 		if(cistrcmp(m, months[i]) == 0)
-			return i+1;
+			return i + 1;
 	return 1;
 }
 
 enum
 {
-	Yearsecs= 365*24*60*60
+	Yearsecs	= 365*24*60*60,
 };
 
 void
 cracktime(char *d, char *out, int len)
 {
-	char in[64];
-	char *f[6];
+	char in[64], *f[6], *dtime;
 	int n;
-	Tm tm;
 	long now, then;
-	char *dtime;
+	Tm tm;
 
 	*out = 0;
 	if(d == nil)
 		return;
-	strncpy(in, d, sizeof(in));
-	in[sizeof(in)-1] = 0;
+	strncpy(in, d, sizeof in);
+	in[sizeof in - 1] = 0;
 	n = getfields(in, f, 6, 1, " \t\r\n");
 	if(n != 6){
-		// unknown style
+		/* unknown style */
 		snprint(out, 16, "%10.10s", d);
 		return;
 	}
@@ -663,7 +866,7 @@
 	now = time(0);
 	memset(&tm, 0, sizeof tm);
 	if(strchr(f[0], ',') != nil && strchr(f[4], ':') != nil){
-		// 822 style
+		/* 822 style */
 		tm.year = atoi(f[3])-1900;
 		tm.mon = month(f[2]);
 		tm.mday = atoi(f[1]);
@@ -670,7 +873,7 @@
 		dtime = nosecs(f[4]);
 		then = tm2sec(&tm);
 	} else if(strchr(f[3], ':') != nil){
-		// unix style
+		/* unix style */
 		tm.year = atoi(f[5])-1900;
 		tm.mon = month(f[1]);
 		tm.mday = atoi(f[2]);
@@ -685,31 +888,31 @@
 	if(now - then < Yearsecs/2)
 		snprint(out, len, "%2d/%2.2d %s", tm.mon, tm.mday, dtime);
 	else
-		snprint(out, len, "%2d/%2.2d  %4d", tm.mon, tm.mday, tm.year+1900);
+		snprint(out, len, "%2d/%2.2d  %4d", tm.mon, tm.mday, tm.year + 1900);
 }
 
+int
+matchtype(char *s, Ctype *t)
+{
+	return strncmp(t->type, s, strlen(t->type)) == 0;
+}
+
 Ctype*
 findctype(Message *m)
 {
-	char *p;
-	char ftype[128];
+	char *p, ftype[256];
 	int n, pfd[2];
 	Ctype *a, *cp;
-	static Ctype nulltype	= { "", 0, 0, 0 };
-	static Ctype bintype 	= { "application/octet-stream", "bin", 0, 0 };
 
 	for(cp = ctype; cp; cp = cp->next)
-		if(strncmp(cp->type, m->type, strlen(cp->type)) == 0)
-			return cp;
+		if(matchtype(m->type, cp))
+			if((cp->flag & Rechk) == 0)
+				return cp;
+			else
+				break;
 
-/*	use file(1) for any unknown mimetypes
- *
- *	if (strcmp(m->type, bintype.type) != 0)
- *		return &nulltype;
- */
 	if(pipe(pfd) < 0)
-		return &bintype;
-
+		return ctype;
 	*ftype = 0;
 	switch(fork()){
 	case -1:
@@ -720,112 +923,102 @@
 		dup(pfd[0], 0);
 		close(1);
 		dup(pfd[0], 1);
-		execl("/bin/file", "file", "-m", s_to_c(extendpath(m->path, "body")), nil);
+		execl("/bin/file", "file", "-m", extendp(m->path, "body"), nil);
 		exits(0);
 	default:
 		close(pfd[0]);
-		n = read(pfd[1], ftype, sizeof(ftype));
-		if(n > 0)
-			ftype[n] = 0;
+		n = read(pfd[1], ftype, sizeof ftype - 1);
+		while(n > 0 && isspace(ftype[n - 1]))
+			n--;
+		ftype[n] = 0;
 		close(pfd[1]);
 		waitpid();
 		break;
 	}
-
-	if (*ftype=='\0' || (p = strchr(ftype, '/')) == nil)
-		return &bintype;
+	for(cp = ctype; cp; cp = cp->next)
+		if(matchtype(ftype, cp))
+			return cp;
+	if(*ftype == 0 || (p = strchr(ftype, '/')) == nil)
+		return ctype;
 	*p++ = 0;
 
-	a = mallocz(sizeof(Ctype), 1);
+	a = mallocz(sizeof *a, 1);
 	a->type = strdup(ftype);
 	a->ext = strdup(p);
-	a->display = 0;
+	a->flag = 0;
 	a->plumbdest = strdup(ftype);
 	for(cp = ctype; cp->next; cp = cp->next)
-		continue;
+		;
 	cp->next = a;
 	a->next = nil;
 	return a;
 }
 
+/*
+ * traditional
+ */
 void
-mkid(String *s, Message *m)
+hds(char *buf, Message *m)
 {
-	char buf[32];
-
-	if(m->parent != &top){
-		mkid(s, m->parent);
-		s_append(s, ".");
-	}
-	sprint(buf, "%d", m->id);
-	s_append(s, buf);
+	buf[0] = m->child? 'H': ' ';
+	buf[1] = m->flags & Fdeleted ? 'd' : ' ';
+	buf[2] = m->flags & Fstored? 's': ' ';
+	buf[3] = m->flags & Fseen? ' ': '*';
+	if(m->flags & Fanswered)
+		buf[3] = 'a';
+	if(m->flags & Fflagged)
+		buf[3] = '\'';
+	buf[4] = 0;
 }
 
 void
-snprintheader(char *buf, int len, Message *m)
+pheader0(char *buf, int len, Message *m)
 {
-	char timebuf[32];
-	String *id;
-	char *p, *q;
+	char *f, *p, *q, frombuf[40], timebuf[32], h[5];
+	int max;
 
-	// create id
-	id = s_new();
-	mkid(id, m);
+	hds(h, m);
+	if(hcmdfmt == 0){
+		f = m->from;
+		max = longestfrom;
+	}else{
+		snprint(frombuf, sizeof frombuf-5, "%s", m->to);
+		p = strchr(frombuf, ' ');
+		if(p != nil)
+			snprint(p, 5, " ...");
+		f = frombuf;
+		max = longestto;
+		if(max > sizeof frombuf)
+			max = sizeof frombuf;
+	}
 
-	if(*m->from == 0){
-		// no from
-		snprint(buf, len, "%-3s    %s %6d  %s",
-			s_to_c(id),
-			m->type,
-			m->len,
-			m->filename);
-	} else if(*m->subject){
+	if(*f == 0)
+		snprint(buf, len, "%3D    %s %6d  %s",
+			m, m->type, m->len, m->filename);
+	else if(*m->subject){
 		q = p = strdup(m->subject);
 		while(*p == ' ')
 			p++;
 		if(strlen(p) > 50)
 			p[50] = 0;
-		cracktime(m->date, timebuf, sizeof(timebuf));
-		snprint(buf, len, "%-3s %c%c%c %6d  %11.11s %-*.*s %s",
-			s_to_c(id),
-			m->child ? 'H' : ' ',
-			m->deleted ? 'd' : ' ',
-			m->stored ? 's' : ' ',
-			m->len,
-			timebuf,
-			longestfrom, longestfrom, m->from,
-			p);
+		cracktime(m->date, timebuf, sizeof timebuf);
+		snprint(buf, len, "%3D %s %6d  %11.11s %-*.*s %s",
+			m, h, m->len, timebuf, max, max, f, p);
 		free(q);
 	} else {
-		cracktime(m->date, timebuf, sizeof(timebuf));
-		snprint(buf, len, "%-3s %c%c%c %6d  %11.11s %s",
-			s_to_c(id),
-			m->child ? 'H' : ' ',
-			m->deleted ? 'd' : ' ',
-			m->stored ? 's' : ' ',
-			m->len,
-			timebuf,
-			m->from);
+		cracktime(m->date, timebuf, sizeof timebuf);
+		snprint(buf, len, "%3D %s %6d  %11.11s %s",
+			m, h, m->len, timebuf, f);
 	}
-	s_free(id);
 }
 
-char *spaces = "                                                                    ";
-
 void
-snprintHeader(char *buf, int len, int indent, Message *m)
+pheader(char *buf, int len, int indent, Message *m)
 {
-	String *id;
-	char typeid[64];
-	char *p, *e;
+	char *p, *e, typeid[80];
 
-	// create id
-	id = s_new();
-	mkid(id, m);
-
 	e = buf + len;
-
-	snprint(typeid, sizeof typeid, "%s    %s", s_to_c(id), m->type);
+	snprint(typeid, sizeof typeid, "%D    %s", m, m->type);
 	if(indent < 6)
 		p = seprint(buf, e, "%-32s %-6d ", typeid, m->len);
 	else
@@ -836,32 +1029,55 @@
 		p = seprint(p, e, "(from,%s)", m->from);
 	if(m->subject && *m->subject)
 		seprint(p, e, "(subj,%s)", m->subject);
-
-	s_free(id);
 }
 
 char sstring[256];
 
-//	cmd := range cmd ' ' arg-list ; 
-//	range := address
-//		| address ',' address
-//		| 'g' search ;
-//	address := msgno
-//		| search ;
-//	msgno := number
-//		| number '/' msgno ;
-//	search := '/' string '/'
-//		| '%' string '%' ;
-//
+/*
+ * 	cmd := range cmd ' ' arg-list ;
+ * 	range := address
+ * 		| address ',' address
+ * 		| 'g' search ;
+ * 	address := msgno
+ * 		| search ;
+ * 	msgno := number
+ * 		| number '/' msgno ;
+ * 	search := '/' string '/'
+ * 		| '%' string '%'
+ *		| '#' (field '#')? re '#'
+ *
+ */
+static char*
+qstrchr(char *s, int c)
+{
+	for(;; s++){
+		if(*s == '\\')
+			s++;
+		else if(*s == c)
+			return s;
+		if(*s == 0)
+			return nil;
+	}
+}
+
 Reprog*
-parsesearch(char **pp)
+parsesearch(char **pp, char *buf, int bufl)
 {
-	char *p, *np;
+	char *p, *np, *e;
 	int c, n;
 
+	buf[0] = 0;
 	p = *pp;
 	c = *p++;
-	np = strchr(p, c);
+	if(c == '#')
+		snprint(buf, bufl, "from");
+	np = qstrchr(p, c);
+	if(c == '#' && np)
+	if(e = qstrchr(np + 1, c)){
+		snprint(buf, bufl, "%.*s", (int)(np - p), p);
+		p = np + 1;
+		np = e;
+	}
 	if(np != nil){
 		*np++ = 0;
 		*pp = np;
@@ -872,57 +1088,94 @@
 	if(*p == 0)
 		p = sstring;
 	else{
-		strncpy(sstring, p, sizeof(sstring));
-		sstring[sizeof(sstring)-1] = 0;
+		strncpy(sstring, p, sizeof sstring);
+		sstring[sizeof sstring - 1] = 0;
 	}
 	return regcomp(p);
 }
 
-static char *
-num2msg(Message **mp, int sign, int n, Message *first, Message *cur)
+enum{
+	Comma = 1,
+};
+
+/*
+ *   search a message for a regular expression match
+ */
+int
+fsearch(Message *m, Reprog *prog, char *field)
 {
-	Message *m;
+	char buf[4096 + 1];
+	int i, fd, rv;
+	uvlong o;
 
-	m = nil;
-	switch(sign){
-	case 0:
-		for(m = first; m != nil; m = m->next)
-			if(m->id == n)
-				break;
-		break;
-	case -1:
-		if(cur != &top)
-			for(m = cur; m != nil && n > 0; n--)
-				m = m->prev;
-		break;
-	case 1:
-		if(cur == &top){
-			n--;
-			cur = first;
+	rv = 0;
+	fd = open(extendp(m->path, field), OREAD);
+	/*
+	 *  march through raw message 4096 bytes at a time
+	 *  with a 128 byte overlap to chain the re search.
+	 */
+	for(o = 0;; o += i - 128){
+		i = pread(fd, buf, sizeof buf - 1, o);
+		if(i <= 0)
+			break;
+		buf[i] = 0;
+		if(regexec(prog, buf, nil, 0)){
+			rv = 1;
+			break;
 		}
-		for(m = cur; m != nil && n > 0; n--)
-			m = m->next;
-		break;
+		if(i < sizeof buf - 1)
+			break;
 	}
-	if(m == nil)
-		return "address";
-	*mp = m;
-	return nil;
+	close(fd);
+	return rv;
 }
 
+int
+rsearch(Message *m, Reprog *prog, char*)
+{
+	return fsearch(m, prog, "raw");
+}
+
+int
+hsearch(Message *m, Reprog *prog, char*)
+{
+	char buf[256];
+
+	pheader0(buf, sizeof buf, m);
+	return regexec(prog, buf, nil, 0);
+}
+
+/*
+ * ack: returns int (*)(Message*, Reprog*, char*)
+ */
+int (*
+chartosearch(int c))(Message*, Reprog*, char*)
+{
+	switch(c){
+	case '%':
+		return rsearch;
+	case '/':
+	case '?':
+		return hsearch;
+	case '#':
+		return fsearch;
+	}
+	return 0;
+}
+
 char*
-parseaddr(char **pp, Message *first, Message *cur, Message *unspec, Message **mp)
+parseaddr(char **pp, Message *first, Message *cur, Message *unspec, Message **mp, int f)
 {
-	int n;
-	Message *m;
-	char *p, *err;
+	char *p, buf[256];
+	int n, c, sign;
+	Message *m, *m0;
 	Reprog *prog;
-	int c, sign;
-	char buf[256];
+	int (*fn)(Message*, Reprog*, char*);
 
 	*mp = nil;
 	p = *pp;
 
+	sign = 0;
 	if(*p == '+'){
 		sign = 1;
 		p++;
@@ -931,18 +1184,6 @@
 		sign = -1;
 		p++;
 		*pp = p;
-	} else
-		sign = 0;
-
-	/*
-	 * TODO: verify & install this.
-	 * make + and - mean +1 and -1, as in ed.  then -,.d won't
-	 * delete all messages up to the current one.  - geoff
-	 */
-	if(sign && (!isascii(*p) || !isdigit(*p))) {
-		err = num2msg(mp, sign, 1, first, cur);
-		if (err != nil)
-			return err;
 	}
 
 	switch(*p){
@@ -952,7 +1193,7 @@
 			goto number;
 		}
 		*mp = unspec;
-		break;	
+		break;
 	case '0': case '1': case '2': case '3': case '4':
 	case '5': case '6': case '7': case '8': case '9':
 		n = strtoul(p, pp, 10);
@@ -963,41 +1204,53 @@
 				*mp = &top;
 			break;
 		}
-		/* fall through */
 	number:
-		err = num2msg(mp, sign, n, first, cur);
-		if (err != nil)
-			return err;
+		m0 = m = nil;
+		switch(sign){
+		case 0:
+			for(m = first; m != nil; m0 = m, m = m->next)
+				if(m->id == n)
+					break;
+			break;
+		case -1:
+			if(cur != &top)
+				for(m = cur; m0 = m, m != nil && n > 0; n--)
+					m = m->prev;
+			break;
+		case 1:
+			if(cur == &top){
+				n--;
+				cur = first;
+			}
+			for(m = cur; m != nil && n > 0; m0 = m, n--)
+				m = m->next;
+			break;
+		}
+		if(m == nil && f&Comma)
+			m = m0;
+		if(m == nil)
+			return "address";
+		*mp = m;
 		break;
+	case '?':
+		/* legacy behavior.  no longer needed */
+		sign = -1;
 	case '%':
 	case '/':
-	case '?':
+	case '#':
 		c = *p;
-		prog = parsesearch(pp);
+		fn= chartosearch(c);
+		prog = parsesearch(pp, buf, sizeof buf);
 		if(prog == nil)
 			return "badly formed regular expression";
-		m = nil;
-		switch(c){
-		case '%':
-			for(m = cur == &top ? first : cur->next; m != nil; m = m->next){
-				if(rawsearch(m, prog))
+		if(sign == -1){
+			for(m = cur == &top ? nil : cur->prev; m; m = m->prev)
+				if(fn(m, prog, buf))
 					break;
-			}
-			break;
-		case '/':
-			for(m = cur == &top ? first : cur->next; m != nil; m = m->next){
-				snprintheader(buf, sizeof(buf), m);
-				if(regexec(prog, buf, nil, 0))
+		}else{
+			for(m = cur == &top ? first : cur->next; m; m = m->next)
+				if(fn(m, prog, buf))
 					break;
-			}
-			break;
-		case '?':
-			for(m = cur == &top ? nil : cur->prev; m != nil; m = m->prev){
-				snprintheader(buf, sizeof(buf), m);
-				if(regexec(prog, buf, nil, 0))
-					break;
-			}
-			break;
 		}
 		if(m == nil)
 			return "search";
@@ -1008,15 +1261,14 @@
 		for(m = first; m != nil && m->next != nil; m = m->next)
 			;
 		*mp = m;
-		*pp = p+1;
+		*pp = p + 1;
 		break;
 	case '.':
 		*mp = cur;
-		*pp = p+1;
+		*pp = p + 1;
 		break;
 	case ',':
-		if (*mp == nil)
-			*mp = first;
+		*mp = first;
 		*pp = p;
 		break;
 	}
@@ -1023,81 +1275,40 @@
 
 	if(*mp != nil && **pp == '.'){
 		(*pp)++;
-		if((*mp)->child == nil)
+		if((m = (*mp)->child) == nil)
 			return "no sub parts";
-		return parseaddr(pp, (*mp)->child, (*mp)->child, (*mp)->child, mp);
+		return parseaddr(pp, m, m, m, mp, 0);
 	}
-	if(**pp == '+' || **pp == '-' || **pp == '/' || **pp == '%')
-		return parseaddr(pp, first, *mp, *mp, mp);
+	c = **pp;
+	if(c == '+' || c == '-' || c == '/' || c == '%' || c == '#')
+		return parseaddr(pp, first, *mp, *mp, mp, 0);
 
 	return nil;
 }
 
-//
-//  search a message for a regular expression match
-//
-int
-rawsearch(Message *m, Reprog *prog)
-{
-	char buf[4096+1];
-	int i, fd, rv;
-	String *path;
-
-	path = extendpath(m->path, "raw");
-	fd = open(s_to_c(path), OREAD);
-	if(fd < 0)
-		return 0;
-
-	// march through raw message 4096 bytes at a time
-	// with a 128 byte overlap to chain the re search.
-	rv = 0;
-	for(;;){
-		i = read(fd, buf, sizeof(buf)-1);
-		if(i <= 0)
-			break;
-		buf[i] = 0;
-		if(regexec(prog, buf, nil, 0)){
-			rv = 1;
-			break;
-		}
-		if(i < sizeof(buf)-1)
-			break;
-		if(seek(fd, -128LL, 1) < 0)
-			break;
-	}
-
-	close(fd);
-	s_free(path);
-	return rv;
-}
-
-
 char*
 parsecmd(char *p, Cmd *cmd, Message *first, Message *cur)
 {
+	char buf[256], *err;
+	int i, c, r;
 	Reprog *prog;
 	Message *m, *s, *e, **l, *last;
-	char buf[256];
-	char *err;
-	int i, c;
-	char *q;
-	static char errbuf[Errlen];
+	int (*f)(Message*, Reprog*, char*);
+	static char errbuf[ERRMAX];
 
 	cmd->delete = 0;
 	l = &cmd->msgs;
 	*l = nil;
 
-	// eat white space
-	while(*p == ' ')
+	while(*p == ' ' || *p == '\t')
 		p++;
 
-	// null command is a special case (advance and print)
+	/* null command is a special case (advance and print) */
 	if(*p == 0){
-		if(cur == &top){
-			// special case
+		if(cur == &top)
 			m = first;
-		} else {
-			// walk to the next message even if we have to go up
+		else {
+			/* walk to the next message even if we have to go up */
 			m = cur->next;
 			while(m == nil && cur->parent != nil){
 				cur = cur->parent;
@@ -1113,73 +1324,76 @@
 		return nil;
 	}
 
-	// global search ?
+	/* global search ? */
 	if(*p == 'g'){
 		p++;
 
-		// no search string means all messages
-		if(*p != '/' && *p != '%'){
+		/* no search string means all messages */
+		if(*p == 'k'){
+			for(m = first; m != nil; m = m->next)
+			if(m->flags & Fflagged){
+				*l = m;
+				l = &m->cmd;
+				*l = nil;
+			}
+			p++;
+		}else if(*p != '/' && *p != '%' && *p != '#'){
 			for(m = first; m != nil; m = m->next){
 				*l = m;
 				l = &m->cmd;
 				*l = nil;
 			}
-		} else {
-			// mark all messages matching this search string
+		}else{
+			/* mark all messages matching this search string */
 			c = *p;
-			prog = parsesearch(&p);
+			f = chartosearch(c);
+			prog = parsesearch(&p, buf, sizeof buf);
 			if(prog == nil)
 				return "badly formed regular expression";
-			if(c == '%'){
-				for(m = first; m != nil; m = m->next){
-					if(rawsearch(m, prog)){
-						*l = m;
-						l = &m->cmd;
-						*l = nil;
-					}
+			for(m = first; m != nil; m = m->next){
+				if(f(m, prog, buf)){
+					*l = m;
+					l = &m->cmd;
+					*l = nil;
 				}
-			} else {
-				for(m = first; m != nil; m = m->next){
-					snprintheader(buf, sizeof(buf), m);
-					if(regexec(prog, buf, nil, 0)){
-						*l = m;
-						l = &m->cmd;
-						*l = nil;
-					}
-				}
 			}
 			free(prog);
 		}
-	} else {
-	
-		// parse an address
+	}else{
+		/* parse an address */
 		s = e = nil;
-		err = parseaddr(&p, first, cur, cur, &s);
+		err = parseaddr(&p, first, cur, cur, &s, 0);
 		if(err != nil)
 			return err;
 		if(*p == ','){
-			// this is an address range
+			/* this is an address range */
 			if(s == &top)
 				s = first;
 			p++;
 			for(last = s; last != nil && last->next != nil; last = last->next)
 				;
-			err = parseaddr(&p, first, cur, last, &e);
+			err = parseaddr(&p, first, cur, last, &e, Comma);
 			if(err != nil)
 				return err;
-
-			// select all messages in the range
-			for(; s != nil; s = s->next){
+			/* select all messages in the range */
+			r = 0;
+			if(s != nil && e != nil && s->id > e->id)
+				r = 1;
+			while(s != nil){
 				*l = s;
 				l = &s->cmd;
 				*l = nil;
 				if(s == e)
 					break;
+				if(r)
+					s = s->prev;
+				else
+					s = s->next;
 			}
 			if(s == nil)
 				return "null address range";
 		} else {
-			// single address
+			/* single address */
 			if(s != &top){
 				*l = s;
 				s->cmd = nil;
@@ -1187,92 +1401,49 @@
 		}
 	}
 
-	// insert a space after '!'s and '|'s
-	for(q = p; *q; q++)
-		if(*q != '!' && *q != '|')
-			break;
-	if(q != p && *q != ' '){
-		memmove(q+1, q, strlen(q)+1);
-		*q = ' ';
+	while(*p == ' ' || *p == '\t')
+		p++;
+	/* hack to allow all messages to start with 'd' */
+	if(*p == 'd' && p[1]){
+		cmd->delete = 1;
+		p++;
 	}
-
-	cmd->an = getfields(p, cmd->av, nelem(cmd->av) - 1, 1, " \t\r\n");
-	if(cmd->an == 0 || *cmd->av[0] == 0)
-		cmd->f = pcmd;
-	else {
-		// hack to allow all messages to start with 'd'
-		if(*(cmd->av[0]) == 'd' && *(cmd->av[0]+1) != 0){
-			cmd->delete = 1;
-			cmd->av[0]++;
-		}
-
-		// search command table
-		for(i = 0; cmdtab[i].cmd != nil; i++)
-			if(strcmp(cmd->av[0], cmdtab[i].cmd) == 0)
-				break;
-		if(cmdtab[i].cmd == nil)
-			return "illegal command";
-		if(cmdtab[i].args == 0 && cmd->an > 1){
-			snprint(errbuf, sizeof(errbuf), "%s doesn't take an argument", cmdtab[i].cmd);
-			return errbuf;
-		}
-		cmd->f = cmdtab[i].f;
+	while(*p == ' ' || *p == '\t')
+		p++;
+	if(*p == 0)
+		p = "p";
+	for(i = nelem(cmdtab) - 1; i >= 0; i--)
+		if(strncmp(p, cmdtab[i].cmd, strlen(cmdtab[i].cmd)) == 0)
+			goto found;
+	return "illegal command";
+found:
+	p += strlen(cmdtab[i].cmd);
+	snprint(cmd->cmdline, sizeof cmd->cmdline, "%s", p);
+	cmd->av[0] = cmdtab[i].cmd;
+	cmd->an = 1 + tokenize(p, cmd->av + 1, nelem(cmd->av) - 2);
+	if(cmdtab[i].args == 0 && cmd->an > 1){
+		snprint(errbuf, sizeof errbuf, "%s doesn't take an argument", cmdtab[i].cmd);
+		return errbuf;
 	}
-	return nil; 
-}
+	cmd->f = cmdtab[i].f;
 
-// inefficient read from standard input
-char*
-readline(char *prompt, char *line, int len)
-{
-	char *p, *e;
-	int n;
-
-retry:
-	interrupted = 0;
-	Bprint(&out, "%s", prompt);
-	Bflush(&out);
-	e = line + len;
-	for(p = line; p < e; p++){
-		n = read(0, p, 1);
-		if(n < 0){
-			if(interrupted)
-				goto retry;
-			return nil;
-		}
-		if(n == 0)
-			return nil;
-		if(*p == '\n')
-			break;
-	}
-	*p = 0;
-	return line;
+	if(cmdtab[i].addr && (cmd->msgs == nil || cmd->msgs == &top)){
+		snprint(errbuf, sizeof errbuf, "%s requires an address", cmdtab[i].cmd);
+		return errbuf;
+ 	}
+	return nil;
 }
 
-void
-messagecount(Message *m)
-{
-	int i;
-
-	i = 0;
-	for(; m != nil; m = m->next)
-		i++;
-	Bprint(&out, "%d message%s\n", i, plural(i));
-}
-
 Message*
 aichcmd(Message *m, int indent)
 {
-	char	hdr[256];
+	char hdr[256];
 
-	if(m == &top)
-		return nil;
-
-	snprintHeader(hdr, sizeof(hdr), indent, m);
+	pheader(hdr, sizeof hdr, indent, m);
 	Bprint(&out, "%s\n", hdr);
 	for(m = m->child; m != nil; m = m->next)
-		aichcmd(m, indent+1);
-	return nil;
+		aichcmd(m, indent + 1);
+	return m;
 }
 
 Message*
@@ -1280,21 +1451,19 @@
 {
 	if(m == &top)
 		return nil;
-	aichcmd(m, 0);
-	return nil;
+	return aichcmd(m, 0);
 }
 
 Message*
 hcmd(Cmd*, Message *m)
 {
-	char	hdr[256];
+	char hdr[256];
 
 	if(m == &top)
 		return nil;
-
-	snprintheader(hdr, sizeof(hdr), m);
+	pheader0(hdr, sizeof hdr, m);
 	Bprint(&out, "%s\n", hdr);
-	return nil;
+	return m;
 }
 
 Message*
@@ -1301,8 +1470,9 @@
 bcmd(Cmd*, Message *m)
 {
 	int i;
-	Message *om = m;
+	Message *om;
 
+	om = m;
 	if(m == &top)
 		m = top.child;
 	for(i = 0; i < 10 && m != nil; i++){
@@ -1311,7 +1481,7 @@
 		m = m->next;
 	}
 
-	return om;
+	return m != nil? m: om;
 }
 
 Message*
@@ -1323,21 +1493,54 @@
 }
 
 int
-printpart(String *s, char *part)
+writepart(char *m, char *part, char *s)
 {
+	char *e;
+	int fd, n;
+
+	fd = open(extendp(m, part), OWRITE);
+	if(fd < 0){
+		dissappeared();
+		return -1;
+	}
+	for(e = s + strlen(s); e - s > 0; s += n){
+		if((n = write(fd, s, e - s)) <= 0){
+			eprint("!writepart:%s: %r\n", part);
+			break;
+		}
+		if(interrupted)
+			break;
+	}
+	close(fd);
+	return s == e? 0: -1;
+}
+
+Message	*xpipecmd(Cmd*, Message*, char*);
+
+Message*
+printfmt(Message *m, char *part, char *cmd)
+{
+	Cmd c;
+
+	c.an = 2;
+	snprint(c.cmdline, sizeof c.cmdline, "%s", cmd);
+	Bflush(&out);
+	return xpipecmd(&c, m, part);
+}
+
+int
+printpart0(Message *m, char *part)
+{
 	char buf[4096];
 	int n, fd, tot;
-	String *path;
 
-	path = extendpath(s, part);
-	fd = open(s_to_c(path), OREAD);
-	s_free(path);
+	fd = open(extendp(m->path, part), OREAD);
 	if(fd < 0){
-		fprint(2, "!message disappeared\n");
+		dissappeared();
 		return 0;
 	}
 	tot = 0;
-	while((n = read(fd, buf, sizeof(buf))) > 0){
+	while((n = read(fd, buf, sizeof buf)) > 0){
 		if(interrupted)
 			break;
 		if(Bwrite(&out, buf, n) <= 0)
@@ -1349,15 +1552,23 @@
 }
 
 int
+printpart(Message *m, char *part, char *cmd)
+{
+	if(cmd == nil || cmd[0] == 0)
+		return printpart0(m, part);
+	printfmt(m, part, cmd);
+	return 1;
+}
+
+int
 printhtml(Message *m)
 {
 	Cmd c;
 
+	memset(&c, 0, sizeof c);
 	c.an = 3;
-	c.av[1] = "/bin/htmlfmt";
-	c.av[2] = "-l 40 -cutf-8";
-	Bprint(&out, "!%s\n", c.av[1]);
-	Bflush(&out);
+	snprint(c.cmdline, sizeof c.cmdline, "/bin/htmlfmt -l60 -cutf8");
+	eprint("!/bin/htmlfmt\n");
 	pipecmd(&c, m);
 	return 0;
 }
@@ -1368,8 +1579,8 @@
 	if(m == &top)
 		return &top;
 	if(m->parent == &top)
-		printpart(m->path, "unixheader");
-	printpart(m->path, "raw");
+		printpart(m, "unixheader", nil);
+	printpart(m, "raw", nil);
 	return m;
 }
 
@@ -1389,27 +1600,101 @@
 	*np = 0;
 }
 
+void
+setflags(Message *m, char *f)
+{
+	uchar f0;
+
+	f0 = m->flags;
+	txflags(f, &m->flags);
+	if(f0 != m->flags)
+		if((m->nflags & Nnoflags) == 0)
+			writepart(m->path, "flags", f);
+}
+
 Message*
-pcmd(Cmd*, Message *m)
+Fcmd(Cmd *c, Message *m)
 {
-	Message *nm;
+	int i;
+
+	for(i = 1; i < c->an; i++)
+		setflags(m, c->av[i]);
+	return m;
+}
+
+void
+seen(Message *m)
+{
+	setflags(m, "s");
+}
+
+/*
+ * sleeze
+ */
+int
+magicpart(Message *m, char *s, char *part)
+{
+	char buf[4096];
+	int n, fd, c;
+
+	fd = open(extendp(s, part), OREAD);
+	if(fd < 0){
+		if(strcmp(part, "id") == 0)
+			Bprint(&out, "%D ", m);
+		else if(strcmp(part, "fpath") == 0)
+			Bprint(&out, "%s ", rooted(m->path));
+		else
+			Bprint(&out, "%s ", part);
+		return 0;
+	}
+
+	c = 0;
+	while((n = read(fd, buf, sizeof buf)) > 0){
+		c = -1;
+		if(interrupted)
+			break;
+		if(Bwrite(&out, buf, n) <= 0)
+			break;
+		c = buf[n - 1];
+	}
+	close(fd);
+	if(!interrupted && n != -1 && c != -1)
+	if(strstr(part, "body") != nil || strcmp(part, "rawunix") == 0)
+		seen(m);
+	return c;
+}
+
+Message*
+pcmd0(Cmd *c, Message *m, int mayplumb, char *tfmt)
+{
+	char *s, buf[128];
+	int i, ch;
 	Ctype *cp;
-	String *s;
-	char buf[128];
+	Message *nm;
 
 	if(m == &top)
 		return &top;
-	if(m->parent == &top)
-		printpart(m->path, "unixheader");
-	if(printpart(m->path, "header") > 0)
+	if(c && c->an >= 2){
+		ch = 0;
+		for(i = 1; i < c->an; i++)
+			ch = magicpart(m, m->path, c->av[i]);
+		if(ch != '\n')
+			Bprint(&out, "\n");
+		return m;
+	}
+	if(m->parent == &top){
+		seen(m);
+		printpart(m, "unixheader", nil);
+	}
+	if(printpart(m, "header", nil) > 0)
 		Bprint(&out, "\n");
 	cp = findctype(m);
-	if(cp->display){
+	if(cp->flag & Display){
 		if(strcmp(m->type, "text/html") == 0)
 			printhtml(m);
 		else
-			printpart(m->path, "body");
-	} else if(strcmp(m->type, "multipart/alternative") == 0){
+			printpart(m, "body", tfmt);
+	}else if(strcmp(m->type, "multipart/alternative") == 0){
 		for(nm = m->child; nm != nil; nm = nm->next){
 			cp = findctype(nm);
 			if(cp->ext != nil && strncmp(cp->ext, "txt", 3) == 0)
@@ -1418,67 +1703,91 @@
 		if(nm == nil)
 			for(nm = m->child; nm != nil; nm = nm->next){
 				cp = findctype(nm);
-				if(cp->display)
+				if(cp->flag & Display)
 					break;
 			}
 		if(nm != nil)
-			pcmd(nil, nm);
+			pcmd0(nil, nm, mayplumb, tfmt);
 		else
 			hcmd(nil, m);
-	} else if(strncmp(m->type, "multipart/", 10) == 0){
+	}else if(strncmp(m->type, "multipart/", 10) == 0){
 		nm = m->child;
 		if(nm != nil){
-			// always print first part
-			pcmd(nil, nm);
+			/* always print first part */
+			pcmd0(nil, nm, mayplumb, tfmt);
 
 			for(nm = nm->next; nm != nil; nm = nm->next){
-				s = rooted(s_clone(nm->path));
+				s = rooted(nm->path);
 				cp = findctype(nm);
-				snprintHeader(buf, sizeof buf, -1, nm);
+				pheader(buf, sizeof buf, -1, nm);
 				compress(buf);
 				if(strcmp(nm->disposition, "inline") == 0){
 					if(cp->ext != nil)
 						Bprint(&out, "\n--- %s %s/body.%s\n\n",
-							buf, s_to_c(s), cp->ext);
+							buf, s, cp->ext);
 					else
 						Bprint(&out, "\n--- %s %s/body\n\n",
-							buf, s_to_c(s));
-					// pcmd(nil, nm);
+							buf, s);
+					pcmd0(nil, nm, 0, tfmt);
 				} else {
 					if(cp->ext != nil)
 						Bprint(&out, "\n!--- %s %s/body.%s\n",
-							buf, s_to_c(s), cp->ext);
+							buf, s, cp->ext);
 					else
 						Bprint(&out, "\n!--- %s %s/body\n",
-							buf, s_to_c(s));
+							buf, s);
 				}
-				s_free(s);
 			}
 		} else {
 			hcmd(nil, m);
 		}
-	} else if(strcmp(m->type, "message/rfc822") == 0){
+	}else if(strcmp(m->type, "message/rfc822") == 0)
 		pcmd(nil, m->child);
-	} else if(plumb(m, cp) >= 0)
-		Bprint(&out, "\n!--- using plumber to display message of type %s\n", m->type);
-	else
-		Bprint(&out, "\n!--- cannot display messages of type %s\n", m->type);
-		
+	else if(!mayplumb){
+	}else if(plumb(m, cp) >= 0){
+		Bprint(&out, "\n!--- using plumber to type %s", cp->type);
+		if(strcmp(cp->type, m->type) != 0)
+			Bprint(&out, " (was %s)", m->type);
+		Bprint(&out, "\n");
+	}else
+		Bprint(&out, "\n!--- cannot display %s\n", cp->type);
+
 	return m;
 }
 
+Message*
+pcmd(Cmd *c, Message *m)
+{
+	return pcmd0(c, m, 1, textfmt);
+}
+
+Message*
+tcmd(Cmd *c, Message *m)
+{
+	switch(c->an){
+	case 1:
+		if(textfmt[0] != 0)
+			textfmt[0] = 0;
+		else
+			snprint(textfmt, sizeof textfmt, "%s", "upas/tfmt");
+		break;
+	default:
+		snprint(textfmt, sizeof textfmt, "%s", c->cmdline);
+		break;
+	}
+	eprint("!textfmt %s\n", textfmt);
+	return m;
+}
+
 void
-printpartindented(String *s, char *part, char *indent)
+printpartindented(char *s, char *part, char *indent)
 {
 	char *p;
-	String *path;
 	Biobuf *b;
 
-	path = extendpath(s, part);
-	b = Bopen(s_to_c(path), OREAD);
-	s_free(path);
+	b = Bopen(extendp(s, part), OREAD);
 	if(b == nil){
-		fprint(2, "!message disappeared\n");
+		dissappeared();
 		return;
 	}
 	while((p = Brdline(b, '\n')) != nil){
@@ -1492,11 +1801,23 @@
 	Bterm(b);
 }
 
+void
+printpartindent2(char *s, char *part, char *indent)
+{
+	Cmd c;
+
+	memset(&c, 0, sizeof c);
+	snprint(c.cmdline, sizeof c.cmdline, "fmt -q '> ' %s | sed 's/^/%s/g' ",
+		rooted(extendp(s, part)), indent);
+	Bflush(&out);
+	bangcmd(&c, nil);
+}
+
 Message*
-quotecmd(Cmd*, Message *m)
+quotecmd0(Cmd *c, Message *m, void (*p)(char*, char*, char*))
 {
-	Message *nm;
 	Ctype *cp;
+	Message *nm;
 
 	if(m == &top)
 		return &top;
@@ -1504,9 +1825,9 @@
 	if(m->from != nil && *m->from)
 		Bprint(&out, "On %s, %s wrote:\n", m->date, m->from);
 	cp = findctype(m);
-	if(cp->display){
-		printpartindented(m->path, "body", "> ");
-	} else if(strcmp(m->type, "multipart/alternative") == 0){
+	if(cp->flag & Display)
+		p(m->path, "body", "> ");
+	else if(strcmp(m->type, "multipart/alternative") == 0){
 		for(nm = m->child; nm != nil; nm = nm->next){
 			cp = findctype(nm);
 			if(cp->ext != nil && strncmp(cp->ext, "txt", 3) == 0)
@@ -1515,30 +1836,41 @@
 		if(nm == nil)
 			for(nm = m->child; nm != nil; nm = nm->next){
 				cp = findctype(nm);
-				if(cp->display)
+				if(cp->flag & Display)
 					break;
 			}
 		if(nm != nil)
-			quotecmd(nil, nm);
-	} else if(strncmp(m->type, "multipart/", 10) == 0){
+			quotecmd(c, nm);
+	}else if(strncmp(m->type, "multipart/", 10) == 0){
 		nm = m->child;
 		if(nm != nil){
 			cp = findctype(nm);
-			if(cp->display || strncmp(m->type, "multipart/", 10) == 0)
-				quotecmd(nil, nm);
+			if(cp->flag & Display || strncmp(m->type, "multipart/", 10) == 0)
+				quotecmd(c, nm);
 		}
 	}
 	return m;
 }
 
-// really delete messages
 Message*
+quotecmd(Cmd *c, Message *m)
+{
+	void (*p)(char*, char*, char*);
+
+	p = printpartindented;
+	if(strstr(c->av[0], "\"\"") != nil)
+		p = printpartindent2;
+	return quotecmd0(c, m, p);
+}
+
+
+/* really delete messages */
+Message*
 flushdeleted(Message *cur)
 {
-	Message *m, **l;
 	char buf[1024], *p, *e, *msg;
-	int deld, n, fd;
-	int i;
+	int i, deld, n, fd;
+	Message *m, **l;
 
 	doflush = 0;
 	deld = 0;
@@ -1545,38 +1877,39 @@
 
 	fd = open("/mail/fs/ctl", ORDWR);
 	if(fd < 0){
-		fprint(2, "!can't delete mail, opening /mail/fs/ctl: %r\n");
+		eprint("!can't delete mail, opening /mail/fs/ctl: %r\n");
 		exitfs(0);
 	}
-	e = &buf[sizeof(buf)];
+	e = buf + sizeof buf;
 	p = seprint(buf, e, "delete %s", mbname);
 	n = 0;
 	for(l = &top.child; *l != nil;){
 		m = *l;
-		if(!m->deleted){
+		if((m->nflags & Nmissing) == 0)
+		if((m->flags & Fdeleted) == 0){
 			l = &(*l)->next;
 			continue;
 		}
 
-		// don't return a pointer to a deleted message
+		/* don't return a pointer to a deleted message */
 		if(m == cur)
 			cur = m->next;
-
 		deld++;
-		msg = strrchr(s_to_c(m->path), '/');
-		if(msg == nil)
-			msg = s_to_c(m->path);
-		else
-			msg++;
-		if(e-p < 10){
-			write(fd, buf, p-buf);
-			n = 0;
-			p = seprint(buf, e, "delete %s", mbname);
+		if(m->flags & Fdeleted){
+			msg = strrchr(m->path, '/');
+			if(msg == nil)
+				msg = m->path;
+			else
+				msg++;
+			if(e - p < 10){
+				write(fd, buf, p - buf);
+				n = 0;
+				p = seprint(buf, e, "delete %s", mbname);
+			}
+			p = seprint(p, e, " %s", msg);
+			n++;
 		}
-		p = seprint(p, e, " %s", msg);
-		n++;
-
-		// unchain and free
+		/* unchain and free */
 		*l = m->next;
 		if(m->next)
 			m->next->prev = m->prev;
@@ -1583,7 +1916,7 @@
 		freemessage(m);
 	}
 	if(n)
-		write(fd, buf, p-buf);
+		write(fd, buf, p - buf);
 
 	close(fd);
 
@@ -1590,13 +1923,15 @@
 	if(deld)
 		Bprint(&out, "!%d message%s deleted\n", deld, plural(deld));
 
-	// renumber
+	/* renumber */
 	i = 1;
 	for(m = top.child; m != nil; m = m->next)
 		m->id = natural ? m->fileno : i++;
 
-	// if we're out of messages, go back to first
-	// if no first, return the fake first
+	/*
+	 *  if we're out of messages, go back to first
+	 *  if no first, return the fake first
+	 */
 	if(cur == nil){
 		if(top.child)
 			return top.child;
@@ -1607,24 +1942,60 @@
 }
 
 Message*
+mbcmd(Cmd *c, Message*)
+{
+	char *mb, oldmb[Pathlen];
+	Message *m, **l;
+
+	switch(c->an){
+	case 1:
+		mb = "mbox";
+		break;
+	case 2:
+		mb = c->av[1];
+		break;
+	default:
+		eprint("!usage: mbcmd [mbox]\n");
+		return nil;	
+	}
+
+	/* flushdeleted(nil); ? */
+	for(l = &top.child; *l; ){
+		m = *l;
+		*l = m->next;
+		freemessage(m);
+	}
+	top.child = nil;
+
+	strcpy(oldmb, mbpath);
+	if(switchmb(mb, 0) < 0){
+		eprint("!no mb\n");
+		if(switchmb(oldmb, 0) < 0){
+			eprint("!mb disappeared\n");
+			exits("fail");
+		}
+	}
+	icmd(nil, nil);
+	interrupted = 1;	/* no looping */
+	return &top;
+}
+
+Message*
 qcmd(Cmd*, Message*)
 {
 	flushdeleted(nil);
-
 	if(didopen)
 		closemb();
 	Bflush(&out);
-
 	exitfs(0);
-	return nil;	// not reached
+	return nil;
 }
 
 Message*
-ycmd(Cmd*, Message *m)
+ycmd(Cmd *c, Message *m)
 {
 	doflush = 1;
-
-	return icmd(nil, m);
+	return icmd(c, m);
 }
 
 Message*
@@ -1631,29 +2002,22 @@
 xcmd(Cmd*, Message*)
 {
 	exitfs(0);
-	return nil;	// not reached
+	return nil;
 }
 
 Message*
 eqcmd(Cmd*, Message *m)
 {
-	if(m == &top)
-		Bprint(&out, "0\n");
-	else
-		Bprint(&out, "%d\n", m->id);
-	return nil;
+	Bprint(&out, "%D\n", m);
+	return m;
 }
 
 Message*
 dcmd(Cmd*, Message *m)
 {
-	if(m == &top){
-		Bprint(&out, "!address\n");
-		return nil;
-	}
 	while(m->parent != &top)
 		m = m->parent;
-	m->deleted = 1;
+	m->flags |= Fdeleted;
 	return m;
 }
 
@@ -1664,25 +2028,99 @@
 		return nil;
 	while(m->parent != &top)
 		m = m->parent;
-	if(m->deleted < 0)
-		Bprint(&out, "!can't undelete, already flushed\n");
-	m->deleted = 0;
+	m->flags &= ~Fdeleted;
 	return m;
 }
 
+int
+skipscan(void)
+{
+	int r;
+	Dir *d;
+	static int lastvers = -1;
 
+	d = dirstat(top.path);
+	r = d && d->qid.path == mbqid.path && d->qid.vers == mbqid.vers;
+	r = r && mbvers == lastvers;
+	if(d != nil){
+		mbqid = d->qid;
+		lastvers = mbvers;
+	}
+	free(d);
+	return r;
+}
+
 Message*
-icmd(Cmd*, Message *m)
+icmd(Cmd *c, Message *m)
 {
-	int n;
+	char buf[128], *p, *e;
+	Dirstats s;
 
-	n = dir2message(&top, reverse);
-	if(n > 0)
-		Bprint(&out, "%d new message%s\n", n, plural(n));
+	if(skipscan())
+		return m;
+	if(dir2message(&top, reverse, &s) < 0)
+		return nil;
+	p = buf;
+	e = buf + sizeof buf;
+	if(s.new > 0 && c == nil){
+		p = seprint(p, e, "%d message%s", s.new, plural(s.new));
+		if(s.unread > 0)
+			p = seprint(p, e, ", %d unread", s.unread);
+	}
+	else if(s.new > 0)
+		Bprint(&out, "%d new message%s", s.new, plural(s.new));
+	if(s.new && s.del)
+		p = seprint(p, e, "; ");
+	if(s.del > 0)
+		p = seprint(p, e, "%d deleted message%s", s.del, plural(s.del));
+	if(s.new + s.del)
+		p = seprint(p, e, "\n");
+	if(p > buf){
+		Bflush(&out);
+		eprint("%s", buf);
+	}
 	return m;
 }
 
 Message*
+kcmd0(Cmd *c, Message *m)
+{
+	char *f, *s;
+	int sticky;
+
+	if(c->an > 2){
+		eprint("!usage k [flags]\n");
+		return nil;
+	}
+	if(c->f == kcmd)
+		f = "f";
+	else
+		f = "-f";
+	if(c->an == 2)
+		f = c->av[1];
+	setflags(m, f);
+	if(c->an == 2 && (m->nflags & Nnoflags) == 0){
+		sticky = m->flags & Fdeleted;
+		s = file2string(m->path, "flags");
+		m->flags = buftoflags(s) | sticky;
+		free(s);
+	}
+	return m;
+}
+
+Message*
+kcmd(Cmd *c, Message *m)
+{
+	return kcmd0(c, m);
+}
+
+Message*
+Kcmd(Cmd *c, Message *m)
+{
+	return kcmd0(c, m);
+}
+
+Message*
 helpcmd(Cmd*, Message *m)
 {
 	int i;
@@ -1690,38 +2128,50 @@
 	Bprint(&out, "Commands are of the form [<range>] <command> [args]\n");
 	Bprint(&out, "<range> := <addr> | <addr>','<addr>| 'g'<search>\n");
 	Bprint(&out, "<addr> := '.' | '$' | '^' | <number> | <search> | <addr>'+'<addr> | <addr>'-'<addr>\n");
-	Bprint(&out, "<search> := '/'<regexp>'/' | '?'<regexp>'?' | '%%'<regexp>'%%'\n");
+	Bprint(&out, "<search> := 'k' | '/'<re>'/' | '?'<re>'?' | '%%'<re>'%%' | '#' <field> '#' <re> '#' \n");
 	Bprint(&out, "<command> :=\n");
-	for(i = 0; cmdtab[i].cmd != nil; i++)
+	for(i = 0; i < nelem(cmdtab); i++)
 		Bprint(&out, "%s\n", cmdtab[i].help);
 	return m;
 }
 
+/* ed thinks this is a good idea */
+void
+marshal(char **path, char **argv0)
+{
+	char *s;
+
+	s = getenv("marshal");
+	if(s == nil || *s == 0)
+		s = "/bin/upas/marshal";
+	*path = s;
+	*argv0 = strrchr(s, '/') + 1;
+	if(*argv0 == (char*)1)
+		*argv0 = s;
+}
+
 int
 tomailer(char **av)
 {
-	Waitmsg *w;
 	int pid, i;
+	char *p, *a;
+	Waitmsg *w;
 
-	// start the mailer and get out of the way
 	switch(pid = fork()){
 	case -1:
-		fprint(2, "can't fork: %r\n");
+		eprint("can't fork: %r\n");
 		return -1;
 	case 0:
-		Bprint(&out, "!/bin/upas/marshal");
-		for(i = 1; av[i]; i++){
-			if(strchr(av[i], ' ') != nil)
-				Bprint(&out, " '%s'", av[i]);
-			else
-				Bprint(&out, " %s", av[i]);
-		}
+		marshal(&p, &a);
+		Bprint(&out, "!%s", p);
+		for(i = 1; av[i]; i++)
+			Bprint(&out, " %q", av[i]);
 		Bprint(&out, "\n");
 		Bflush(&out);
-		av[0] = "marshal";
+		av[0] = a;
 		chdir(wd);
-		exec("/bin/upas/marshal", av);
-		fprint(2, "couldn't exec /bin/upas/marshal\n");
+		exec(p, av);
+		eprint("couldn't exec %s\n", p);
 		exits(0);
 	default:
 		w = wait();
@@ -1732,28 +2182,28 @@
 			return -1;
 		}
 		if(w->msg[0]){
-			fprint(2, "mailer failed: %s\n", w->msg);
+			eprint("mailer failed: %s\n", w->msg);
 			free(w);
 			return -1;
 		}
 		free(w);
-		Bprint(&out, "!\n");
+//		Bprint(&out, "!\n");
 		break;
 	}
 	return 0;
 }
 
-//
-// like tokenize but obey "" quoting
-//
+/*
+ *  like tokenize but obey "" quoting
+ */
 int
 tokenize822(char *str, char **args, int max)
 {
-	int na;
-	int intok = 0, inquote = 0;
+	int na, intok, inquote;
 
 	if(max <= 0)
-		return 0;	
+		return 0;
+	intok = inquote = 0;
 	for(na=0; ;str++)
 		switch(*str) {
 		case ' ':
@@ -1783,56 +2233,59 @@
 		}
 }
 
-/* return reply-to address & set *nmp to corresponding Message */
-static char *
-getreplyto(Message *m, Message **nmp)
+static char *rec[] = {"Re: ", "AW:", };
+static char *fwc[] = {"Fwd: ", };
+
+char*
+addrecolon(char **tab, int n, char *s)
 {
-	Message *nm;
+	char *prefix;
+	int i;
 
-	for(nm = m; nm != &top; nm = nm->parent)
- 		if(*nm->replyto != 0)
+	prefix = "";
+	for(i = 0; i < n; i++)
+		if(cistrncmp(s, tab[i], strlen(tab[i]) - 1) == 0)
 			break;
-	*nmp = nm;
-	return nm? nm->replyto: nil;
+	if(i == n)
+		prefix = tab[0];
+	return smprint("%s%s", prefix, s);
 }
 
 Message*
 rcmd(Cmd *c, Message *m)
 {
-	char *addr;
-	char *av[128];
-	int i, ai = 1;
-	String *from, *rpath, *path = nil, *subject = nil;
+	char *from, *path, *subject, *rpath, *addr, *av[128];
+	int i, ai;
 	Message *nm;
 
-	if(m == &top){
-		Bprint(&out, "!address\n");
-		return nil;
-	}
-
-	addr = getreplyto(m, &nm);
+	ai = 1;
+	av[ai++] = "-8";
+	addr = path = subject = nil;
+	for(nm = m; nm != &top; nm = nm->parent)
+ 		if(*nm->replyto != 0){
+			addr = nm->replyto;
+			break;
+		}
 	if(addr == nil){
-		Bprint(&out, "!no reply address\n");
+		eprint("!no reply address\n");
 		return nil;
 	}
+
 	if(nm == &top){
 		print("!noone to reply to\n");
 		return nil;
 	}
 
-	av[ai++] = "-8";
-	for(nm = m; nm != &top; nm = nm->parent){
+	for(nm = m; nm != &top; nm = nm->parent)
 		if(*nm->subject){
 			av[ai++] = "-s";
-			subject = addrecolon(nm->subject);
-			av[ai++] = s_to_c(subject);
+			subject = addrecolon(rec, nelem(rec), nm->subject);
+			av[ai++] = subject;
 			break;
 		}
-	}
 
 	av[ai++] = "-R";
-	rpath = rooted(s_clone(m->path));
-	av[ai++] = s_to_c(rpath);
+	av[ai++] = rpath = strdup(rooted(m->path));
 
 	if(strchr(c->av[0], 'f') != nil){
 		fcmd(c, m);
@@ -1843,21 +2296,22 @@
 		av[ai++] = "-t";
 		av[ai++] = "message/rfc822";
 		av[ai++] = "-A";
-		path = rooted(extendpath(m->path, "raw"));
-		av[ai++] = s_to_c(path);
+		path = strdup(rooted(extendp(m->path, "raw")));
+		av[ai++] = path;
 	}
 
 	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
 		av[ai++] = c->av[i];
-	from = s_copy(addr);
-	ai += tokenize822(s_to_c(from), &av[ai], nelem(av) - ai);
+	ai += tokenize822(from = strdup(addr), &av[ai], nelem(av) - ai);
 	av[ai] = 0;
-	if(tomailer(av) < 0)
+	if(tomailer(av) == -1)
 		m = nil;
-	s_free(path);
-	s_free(rpath);
-	s_free(subject);
-	s_free(from);
+	else
+		m->flags |= Fanswered;
+	free(path);
+	free(rpath);
+	free(subject);
+	free(from);
 	return m;
 }
 
@@ -1864,18 +2318,22 @@
 Message*
 mcmd(Cmd *c, Message *m)
 {
-	char *av[128];
-	int i, ai = 1;
-	String *path;
+	char *subject, *av[128];
+	int i, ai;
 
-	if(m == &top){
-		Bprint(&out, "!address\n");
+	if(c->an < 2){
+		eprint("!usage: M list-of addresses\n");
 		return nil;
 	}
 
-	if(c->an < 2){
-		fprint(2, "!usage: M list-of addresses\n");
-		return nil;
+	ai = 1;
+	av[ai++] = "-8";
+
+	subject = nil;
+	if(m->subject){
+		av[ai++] = "-s";
+		subject = addrecolon(fwc, nelem(fwc), m->subject);
+		av[ai++] = subject;
 	}
 
 	av[ai++] = "-t";
@@ -1885,22 +2343,18 @@
 		av[ai++] = "mime";
 
 	av[ai++] = "-A";
-	path = rooted(extendpath(m->path, "raw"));
-	av[ai++] = s_to_c(path);
-
+	av[ai++] = rooted(extendp(m->path, "raw"));
 	if(strchr(c->av[0], 'M') == nil)
 		av[ai++] = "-n";
-	else
-		av[ai++] = "-8";
-
 	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
 		av[ai++] = c->av[i];
 	av[ai] = 0;
 
-	if(tomailer(av) < 0)
+	if(tomailer(av) == -1)
 		m = nil;
-	if(path != nil)
-		s_free(path);
+	else
+		m->flags |= Fanswered;
+	free(subject);
 	return m;
 }
 
@@ -1907,132 +2361,85 @@
 Message*
 acmd(Cmd *c, Message *m)
 {
-	char *av[128];
-	int i, ai = 1;
-	String *from, *rpath, *path = nil, *subject = nil;
-	String *to, *cc;
+	char *av[128], *rpath, *subject, *from, *to, *cc;
+	int i, ai;
 
-	if(m == &top){
-		Bprint(&out, "!address\n");
+	if(m->from == nil || m->to == nil || m->cc == nil){
+		eprint("!bad message\n");
 		return nil;
 	}
 
+	ai = 1;
 	av[ai++] = "-8";
+	av[ai++] = "-R";
+	av[ai++] = rpath = strdup(rooted(m->path));
 
-	if(*m->subject){
+	subject = nil;
+	if(m->subject && *m->subject){
 		av[ai++] = "-s";
-		subject = addrecolon(m->subject);
-		av[ai++] = s_to_c(subject);
+		subject = addrecolon(rec, nelem(rec), m->subject);
+		av[ai++] = subject;
 	}
 
-	av[ai++] = "-R";
-	rpath = rooted(s_clone(m->path));
-	av[ai++] = s_to_c(rpath);
-
 	if(strchr(c->av[0], 'A') != nil){
 		av[ai++] = "-t";
 		av[ai++] = "message/rfc822";
 		av[ai++] = "-A";
-		path = rooted(extendpath(m->path, "raw"));
-		av[ai++] = s_to_c(path);
+		av[ai++] = rooted(extendp(m->path, "raw"));
 	}
 
 	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
 		av[ai++] = c->av[i];
-	from = s_copy(m->from);
-	ai += tokenize822(s_to_c(from), &av[ai], nelem(av) - ai);
-	to = s_copy(m->to);
-	ai += tokenize822(s_to_c(to), &av[ai], nelem(av) - ai);
-	cc = s_copy(m->cc);
-	ai += tokenize822(s_to_c(cc), &av[ai], nelem(av) - ai);
+	ai += tokenize822(from = strdup(m->from), &av[ai], nelem(av) - ai);
+	ai += tokenize822(to = strdup(m->to), &av[ai], nelem(av) - ai);
+	ai += tokenize822(cc = strdup(m->cc), &av[ai], nelem(av) - ai);
 	av[ai] = 0;
-	if(tomailer(av) < 0)
+	if(tomailer(av) == -1)
 		m = nil;
-	s_free(path);
-	s_free(rpath);
-	s_free(subject);
-	s_free(from);
-	s_free(to);
-	s_free(cc);
+	else
+		m->flags |= Fanswered;
+	free(from);
+	free(to);
+	free(cc);
+	free(subject);
+	free(rpath);
 	return m;
 }
 
-String *
-relpath(char *path, String *to)
-{
-	if (*path=='/' || strncmp(path, "./", 2) == 0
-			      || strncmp(path, "../", 3) == 0) {
-		to = s_append(to, path);
-	} else if(mbpath) {
-		to = s_append(to, s_to_c(mbpath));
-		to->ptr = strrchr(to->base, '/')+1;
-		s_append(to, path);
-	}
-	return to;
-}
-
 int
-appendtofile(Message *m, char *part, char *base, int mbox)
+appendtofile(Message *m, char *part, char *base, int mbox, int f)
 {
-	String *file, *h;
-	int in, out, rv;
+	char *folder, path[Pathlen];
+	int in, rv, rp;
 
-	file = extendpath(m->path, part);
-	in = open(s_to_c(file), OREAD);
-	if(in < 0){
-		fprint(2, "!message disappeared\n");
+	in = open(extendp(m->path, part), OREAD);
+	if(in == -1){
+		dissappeared();
 		return -1;
 	}
-
-	s_reset(file);
-
-	relpath(base, file);
-	if(sysisdir(s_to_c(file))){
-		s_append(file, "/");
-		if(m->filename && strchr(m->filename, '/') == nil)
-			s_append(file, m->filename);
-		else {
-			s_append(file, "att.XXXXXXXXXXX");
-			mktemp(s_to_c(file));
-		}
-	}
-	if(mbox)
-		out = open(s_to_c(file), OWRITE);
+	rp = 0;
+	if(*base == '/')
+		folder = base;
+	else if(!mbox){
+		snprint(path, sizeof path, "%s/%s", wd, base);
+		folder = path;
+		rp = 1;
+	}else if(f)
+		folder = ffoldername(mbpath, user, base);
 	else
-		out = open(s_to_c(file), OWRITE|OTRUNC);
-	if(out < 0){
-		out = create(s_to_c(file), OWRITE, 0666);
-		if(out < 0){
-			fprint(2, "!can't open %s: %r\n", s_to_c(file));
-			close(in);
-			s_free(file);
-			return -1;
-		}
-	}
+		folder = foldername(mbpath, user, base);
+	if(folder == nil)
+		return -1;
 	if(mbox)
-		seek(out, 0, 2);
-
-	// put on a 'From ' line
-	if(mbox){
-		while(m->parent != &top)
-			m = m->parent;
-		h = file2string(m->path, "unixheader");
-		fprint(out, "%s", s_to_c(h));
-		s_free(h);
-	}
-
-	// copy the message escaping what we have to ad adding newlines if we have to
-	if(mbox)
-		rv = appendfiletombox(in, out);
+		rv = fappendfolder(0, 0, folder, in);
 	else
-		rv = appendfiletofile(in, out);
-
+		rv = fappendfile(m->from, folder, in);
 	close(in);
-	close(out);
-
-	if(rv >= 0)
-		print("!saved in %s\n", s_to_c(file));
-	s_free(file);
+	if(rv >= 0){
+		eprint("!saved in %s\n", rp? base: folder);
+		setflags(m, "S");
+	}else
+		eprint("!error %r\n");
 	return rv;
 }
 
@@ -2041,11 +2448,6 @@
 {
 	char *file;
 
-	if(m == &top){
-		Bprint(&out, "!address\n");
-		return nil;
-	}
-
 	switch(c->an){
 	case 1:
 		file = "stored";
@@ -2054,14 +2456,12 @@
 		file = c->av[1];
 		break;
 	default:
-		fprint(2, "!usage: s filename\n");
+		eprint("!usage: s filename\n");
 		return nil;
 	}
 
-	if(appendtofile(m, "raw", file, 1) < 0)
+	if(appendtofile(m, "rawunix", file, 1, 0) < 0)
 		return nil;
-
-	m->stored = 1;
 	return m;
 }
 
@@ -2070,11 +2470,6 @@
 {
 	char *file;
 
-	if(m == &top){
-		Bprint(&out, "!address\n");
-		return nil;
-	}
-
 	switch(c->an){
 	case 2:
 		file = c->av[1];
@@ -2081,7 +2476,7 @@
 		break;
 	case 1:
 		if(*m->filename == 0){
-			fprint(2, "!usage: w filename\n");
+			eprint("!usage: w filename\n");
 			return nil;
 		}
 		file = strrchr(m->filename, '/');
@@ -2091,114 +2486,107 @@
 			file = m->filename;
 		break;
 	default:
-		fprint(2, "!usage: w filename\n");
+		eprint("!usage: w filename\n");
 		return nil;
 	}
 
-	if(appendtofile(m, "body", file, 0) < 0)
+	if(appendtofile(m, "body", file, 0, 0) < 0)
 		return nil;
-	m->stored = 1;
 	return m;
 }
 
-char *specialfile[] =
-{
-	"pipeto",
-	"pipefrom",
-	"L.mbox",
-	"forward",
-	"names"
+typedef struct Xtab Xtab;
+struct Xtab {
+	char	*a;
+	char	*b;
 };
+Xtab	*xtab;
+int	nxtab;
 
-// return 1 if this is a special file
-static int
-special(String *s)
+void
+loadxfrom(int fd)
 {
-	char *p;
-	int i;
+	char *f[3], *s, *p;
+	int n, a, inc;
+	Biobuf b;
+	Xtab *x;
 
-	p = strrchr(s_to_c(s), '/');
-	if(p == nil)
-		p = s_to_c(s);
-	else
-		p++;
-	for(i = 0; i < nelem(specialfile); i++)
-		if(strcmp(p, specialfile[i]) == 0)
-			return 1;
-	return 0;
+	Binit(&b, fd, OREAD);
+	a = 0;
+	inc = 100;
+	for(; s = Brdstr(&b, '\n', 1);){
+		if(p = strchr(s, '#'))
+			*p = 0;
+		n = tokenize(s, f, nelem(f));
+		if(n != 2){
+			free(s);
+			continue;
+		}
+		if(nxtab == a){
+			a += inc;
+			xtab = realloc(xtab, a*sizeof *xtab);
+			if(xtab == nil)
+				sysfatal("realloc: %r");
+			inc *= 2;
+		}
+		for(x = xtab+nxtab; x > xtab && strcmp(x[-1].a, f[0]) > 0; x--)
+			x[0] = x[-1];
+		x->a = f[0];
+		x->b = f[1];
+		nxtab++;
+	}
 }
 
-// open the folder using the recipients account name
-static String*
-foldername(char *rcvr)
+char*
+frombox(char *from)
 {
-	char *p;
-	int c;
-	String *file;
-	Dir *d;
-	int scarey;
+	char *s;
+	int n, m, fd;
+	Xtab *t, *p;
+	static int once;
 
-	file = s_new();
-	mboxpath("f", user, file, 0);
-	d = dirstat(s_to_c(file));
-
-	// if $mail/f exists, store there, otherwise in $mail
-	s_restart(file);
-	if(d && d->qid.type == QTDIR){
-		scarey = 0;
-		s_append(file, "f/");
-	} else {
-		scarey = 1;
+	if(once == 0){
+		once = 1;
+		s = foldername(mbpath, user, "fromtab-");
+		fd = open(s, OREAD);
+		if(fd != -1)
+			loadxfrom(fd);
+		close(fd);
 	}
-	free(d);
-
-	p = strrchr(rcvr, '!');
-	if(p != nil)
-		rcvr = p+1;
-
-	while(*rcvr && *rcvr != '@'){
-		c = *rcvr++;
-		if(c == '/')
-			c = '_';
-		s_putc(file, c);
+	t = xtab;
+	n = nxtab;
+	while(n > 1) {
+		m = n/2;
+		p = t + m;
+		if(strcmp(from, p->a) > 0){
+			t = p;
+			n = n - m;
+		} else
+			n = m;
 	}
-	s_terminate(file);
-
-	if(scarey && special(file)){
-		fprint(2, "!won't overwrite %s\n", s_to_c(file));
-		s_free(file);
-		return nil;
-	}
-
-	return file;
+	if(n && strcmp(from, t->a) == 0)
+		return t->b;
+	return from;
 }
 
 Message*
-fcmd(Cmd *c, Message *m)
+fcmd(Cmd*, Message *m)
 {
-	String *folder;
+	char *f;
 
-	if(c->an > 1){
-		fprint(2, "!usage: f takes no arguments\n");
+	f = frombox(m->from);
+	if(appendtofile(m, "rawunix", f, 1, 1) < 0)
 		return nil;
-	}
+	return m;
+}
 
-	if(m == &top){
-		Bprint(&out, "!address\n");
-		return nil;
-	}
+Message*
+fqcmd(Cmd*, Message *m)
+{
+	char *f;
 
-	folder = foldername(m->from);
-	if(folder == nil)
-		return nil;
-
-	if(appendtofile(m, "raw", s_to_c(folder), 1) < 0){
-		s_free(folder);
-		return nil;
-	}
-	s_free(folder);
-
-	m->stored = 1;
+	f = frombox(m->from);
+	Bprint(&out, "! %s\n", f);
 	return m;
 }
 
@@ -2219,7 +2607,7 @@
 		if(wd[0] != 0)
 			chdir(wd);
 		exec(cmd, av);
-		fprint(2, "!couldn't exec %s\n", cmd);
+		eprint("!couldn't exec %s\n", cmd);
 		exits(0);
 	default:
 		if(in >= 0)
@@ -2237,22 +2625,14 @@
 Message*
 bangcmd(Cmd *c, Message *m)
 {
-	char cmd[4*1024];
-	char *p, *e;
 	char *av[4];
-	int i;
 
-	cmd[0] = 0;
-	p = cmd;
-	e = cmd+sizeof(cmd);
-	for(i = 1; i < c->an; i++)
-		p = seprint(p, e, "%s ", c->av[i]);
 	av[0] = "rc";
 	av[1] = "-c";
-	av[2] = cmd;
+	av[2] = c->cmdline;
 	av[3] = 0;
 	system("/bin/rc", av, -1);
-	Bprint(&out, "!\n");
+//	Bprint(&out, "!\n");
 	return m;
 }
 
@@ -2259,46 +2639,26 @@
 Message*
 xpipecmd(Cmd *c, Message *m, char *part)
 {
-	char cmd[128];
-	char *p, *e;
 	char *av[4];
-	String *path;
-	int i, fd;
+	int fd;
 
 	if(c->an < 2){
-		Bprint(&out, "!usage: | cmd\n");
+		eprint("!usage: | cmd\n");
 		return nil;
 	}
 
-	if(m == &top){
-		Bprint(&out, "!address\n");
-		return nil;
-	}
-
-	path = extendpath(m->path, part);
-	fd = open(s_to_c(path), OREAD);
-	s_free(path);
-	if(fd < 0){	// compatibility with older upas/fs
-		path = extendpath(m->path, "raw");
-		fd = open(s_to_c(path), OREAD);
-		s_free(path);
-	}
+	fd = open(extendp(m->path, part), OREAD);
 	if(fd < 0){
-		fprint(2, "!message disappeared\n");
+		dissappeared();
 		return nil;
 	}
 
-	p = cmd;
-	e = cmd+sizeof(cmd);
-	cmd[0] = 0;
-	for(i = 1; i < c->an; i++)
-		p = seprint(p, e, "%s ", c->av[i]);
 	av[0] = "rc";
 	av[1] = "-c";
-	av[2] = cmd;
+	av[2] = c->cmdline;
 	av[3] = 0;
 	system("/bin/rc", av, fd);	/* system closes fd */
-	Bprint(&out, "!\n");
+//	Bprint(&out, "!\n");
 	return m;
 }
 
@@ -2323,172 +2683,128 @@
 	if(fd < 0)
 		sysfatal("can't open /mail/fs/ctl: %r");
 
-	// close current mailbox
+	/* close current mailbox */
 	if(*mbname && strcmp(mbname, "mbox") != 0)
-		fprint(fd, "close %s", mbname);
+	if(fprint(fd, "close %q", mbname) == -1)
+		eprint("!close %q: %r", mbname);
 
 	close(fd);
 }
 
-int
-switchmb(char *file, char *singleton)
+static char*
+chop(char *s, int c)
 {
 	char *p;
-	int n, fd;
-	String *path;
-	char buf[256];
 
-	// if the user didn't say anything and there
-	// is an mbox mounted already, use that one
-	// so that the upas/fs -fdefault default is honored.
-	if(file 
-	|| (singleton && access(singleton, 0)<0)
-	|| (!singleton && access("/mail/fs/mbox", 0)<0)){
-		if(file == nil)
-			file = "mbox";
+	p = strrchr(s, c);
+	if(p != nil && p > s) {
+		*p = 0;
+		return p - 1;
+	}
+	return 0;
+}
 
-		// close current mailbox
-		closemb();
-		didopen = 1;
+/* sometimes opens the file (or open mbox) intended. */
+int
+switchmb(char *mb, int singleton)
+{
+	char *p, *e, pbuf[Pathlen], buf[Pathlen], file[Pathlen];
+	int fd, abs;
 
+	closemb();
+	abs = 0;
+	if(mb == nil)
+		mb = "mbox";
+	if(strcmp(mb, ".") == 0)	/* botch */
+		mb = homewd;
+	if(*mb == '/' || strncmp(mb, "./", 2) == 0 || strncmp(mb, "../", 3) == 0){
+		snprint(file, sizeof file, "%s", mb);
+		abs = 1;
+	}else
+		snprint(file, sizeof file, "/mail/fs/%s", mb);
+	if(singleton){
+		if(chop(file, '/') == nil || (p = strrchr(file, '/')) == nil || p - file < 2){
+			eprint("!bad mbox name\n");
+			return -1;
+		}
+		mboxpathbuf(pbuf, sizeof pbuf, user, "mbox");
+		snprint(mbname, sizeof mbname, "%s", p + 1);
+	}else if(abs || access(file, 0) < 0){
 		fd = open("/mail/fs/ctl", ORDWR);
 		if(fd < 0)
 			sysfatal("can't open /mail/fs/ctl: %r");
-	
-		path = s_new();
-	
-		// get an absolute path to the mail box
-		if(strncmp(file, "./", 2) == 0){
-			// resolve path here since upas/fs doesn't know
-			// our working directory
-			if(getwd(buf, sizeof(buf)-strlen(file)) == nil){
-				fprint(2, "!can't get working directory: %s\n", buf);
-				return -1;
-			}
-			s_append(path, buf);
-			s_append(path, file+1);
-		} else {
-			mboxpath(file, user, path, 0);
-		}
-	
-		// make up a handle to use when talking to fs
-		p = strrchr(file, '/');
-		if(p == nil){
-			// if its in the mailbox directory, just use the name
-			strncpy(mbname, file, sizeof(mbname));
-			mbname[sizeof(mbname)-1] = 0;
-		} else {
-			// make up a mailbox name
-			p = strrchr(s_to_c(path), '/');
-			p++;
-			if(*p == 0){
-				fprint(2, "!bad mbox name");
-				return -1;
-			}
-			strncpy(mbname, p, sizeof(mbname));
-			mbname[sizeof(mbname)-1] = 0;
-			n = strlen(mbname);
-			if(n > Elemlen-12)
-				n = Elemlen-12;
-			sprint(mbname+n, "%ld", time(0));
-		}
-
-		if(fprint(fd, "open %s %s", s_to_c(path), mbname) < 0){
-			fprint(2, "!can't 'open %s %s': %r\n", file, mbname);
-			s_free(path);
+		p = pbuf;
+		e = pbuf + sizeof pbuf;
+		if(abs && *file != '/')
+			seprint(p, e, "%s/%s", getwd(buf, sizeof buf), mb);
+		else if(abs)
+			seprint(p, e, "%s", mb);
+		else
+			mboxpathbuf(pbuf, sizeof pbuf, user, mb);
+		/* make up a handle to use when talking to fs */
+		if((p = strrchr(mb, '/')) == nil)
+			p = mb - 1;
+		snprint(mbname, sizeof mbname, "%s%ld", p + 1, time(0));
+		if(fprint(fd, "open %q %q", pbuf, mbname) < 0){
+			eprint("!can't open %q %q: %r\n", pbuf, mbname);
 			return -1;
 		}
 		close(fd);
-	}else
-	if (singleton && access(singleton, 0)==0
-	    && strncmp(singleton, "/mail/fs/", 9) == 0){
-		if ((p = strchr(singleton +10, '/')) == nil){
-			fprint(2, "!bad mbox name");
-			return -1;
-		}
-		n = p-(singleton+9);
-		strncpy(mbname, singleton+9, n);
-		mbname[n+1] = 0;
-		path = s_reset(nil);
-		mboxpath(mbname, user, path, 0);
+		didopen = 1;
 	}else{
-		path = s_reset(nil);
-		mboxpath("mbox", user, path, 0);
-		strcpy(mbname, "mbox");
+		mboxpathbuf(pbuf, sizeof pbuf, user, mb);
+		strcpy(mbname, mb);
 	}
 
 	snprint(root, sizeof root, "/mail/fs/%s", mbname);
-	if(getwd(wd, sizeof(wd)) == 0)
+	if(getwd(wd, sizeof wd) == nil)
 		wd[0] = 0;
-	if(singleton == nil && chdir(root) >= 0)
+	if(!singleton && chdir(root) >= 0)
 		strcpy(root, ".");
 	rootlen = strlen(root);
+	snprint(mbpath, sizeof mbpath, "%s", pbuf);
+	memset(&mbqid, 0, sizeof mbqid);
+	mbvers++;
 
-	if(mbpath != nil)
-		s_free(mbpath);
-	mbpath = path;
 	return 0;
 }
 
-// like tokenize but for into lines
-int
-lineize(char *s, char **f, int n)
+char*
+rooted(char *s)
 {
-	int i;
+	static char buf[Pathlen];
 
-	for(i = 0; *s && i < n; i++){
-		f[i] = s;
-		s = strchr(s, '\n');
-		if(s == nil)
-			break;
-		*s++ = 0;
-	}
-	return i;
-}
-
-
-
-String*
-rooted(String *s)
-{
-	static char buf[256];
-
 	if(strcmp(root, ".") != 0)
 		return s;
-	snprint(buf, sizeof(buf), "/mail/fs/%s/%s", mbname, s_to_c(s));
-	s_free(s);
-	return s_copy(buf);
+	snprint(buf, sizeof buf, "/mail/fs/%s/%s", mbname, s);
+	return buf;
 }
 
 int
 plumb(Message *m, Ctype *cp)
 {
-	String *s;
+	char *s;
 	Plumbmsg *pm;
 	static int fd = -2;
 
 	if(cp->plumbdest == nil)
 		return -1;
-
 	if(fd < -1)
 		fd = plumbopen("send", OWRITE);
 	if(fd < 0)
 		return -1;
 
-	pm = mallocz(sizeof(Plumbmsg), 1);
+	pm = mallocz(sizeof *pm, 1);
 	pm->src = strdup("mail");
 	if(*cp->plumbdest)
 		pm->dst = strdup(cp->plumbdest);
-	pm->wdir = nil;
 	pm->type = strdup("text");
 	pm->ndata = -1;
-	s = rooted(extendpath(m->path, "body"));
-	if(cp->ext != nil){
-		s_append(s, ".");
-		s_append(s, cp->ext);
-	}
-	pm->data = strdup(s_to_c(s));
-	s_free(s);
+	s = rooted(extendp(m->path, "body"));
+	if(cp->ext != nil)
+		pm->data  = smprint("%s.%s", s, cp->ext);
+	else
+		pm->data  = strdup(s);
 	plumbsend(fd, pm);
 	plumbfree(pm);
 	return 0;
@@ -2499,24 +2815,11 @@
 {
 }
 
-String*
-addrecolon(char *s)
-{
-	String *str;
-
-	if(cistrncmp(s, "re:", 3) != 0){
-		str = s_copy("Re: ");
-		s_append(str, s);
-	} else
-		str = s_copy(s);
-	return str;
-}
-
 void
 exitfs(char *rv)
 {
 	if(startedfs)
 		unmount(nil, "/mail/fs");
-	chdir("/sys/src/cmd/upas/ned");		/* for profiling? */
+	chdir(homewd);			/* prof */
 	exits(rv);
 }
--- a/sys/src/cmd/upas/pop3/mkfile
+++ b/sys/src/cmd/upas/pop3/mkfile
@@ -1,10 +1,10 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=pop3
 
 OFILES=pop3.$O
 
-BIN=/$objtype/bin/upas
 LIB=../common/libcommon.a$O
 
 UPDATE=\
--- a/sys/src/cmd/upas/pop3/pop3.c
+++ b/sys/src/cmd/upas/pop3/pop3.c
@@ -96,6 +96,7 @@
 	int fd;
 	char *arg, cmdbuf[1024];
 	Cmd *c;
+	NetConnInfo *n;
 
 	rfork(RFNAMEG);
 	Binit(&in, 0, OREAD);
@@ -114,9 +115,6 @@
 			close(fd);
 		}
 		break;
-	case 'p':
-		passwordinclear = 1;
-		break;
 	case 'r':
 		strecpy(tmpaddr, tmpaddr+sizeof tmpaddr, EARGF(usage()));
 		if(arg = strchr(tmpaddr, '!'))
@@ -130,11 +128,17 @@
 			exits(nil);
 		}
 		break;
+	case 'p':
+		passwordinclear = 1;
+		break;
 	}ARGEND
 
 	/* do before TLS */
 	if(peeraddr == nil)
-		peeraddr = remoteaddr(0,0);
+	if(n = getnetconninfo(0, 0)){
+		peeraddr = strdup(n->rsys);
+		freenetconninfo(n);
+	}
 
 	hello();
 
@@ -167,7 +171,7 @@
 readmbox(char *box)
 {
 	int fd, i, n, nd, lines, pid;
-	char buf[100], err[Errlen];
+	char buf[100], err[ERRMAX];
 	char *p;
 	Biobuf *b;
 	Dir *d, *draw;
@@ -245,9 +249,8 @@
 		for(;;){
 			p = Brdline(b, '\n');
 			if(p == nil){
-				if((n = Blinelen(b)) == 0)
+				if(Blinelen(b) == 0)
 					break;
-				Bseek(b, n, 1);
 			}else
 				lines++;
 		}
@@ -434,9 +437,8 @@
 }
 
 static int
-noopcmd(char *arg)
+noopcmd(char*)
 {
-	USED(arg);
 	sendok("");
 	return 0;
 }
@@ -551,23 +553,21 @@
 static int
 stlscmd(char*)
 {
-	TLSconn conn;
 	int fd;
+	TLSconn conn;
 
 	if(didtls)
 		return senderr("tls already started");
 	if(!tlscert)
 		return senderr("don't have any tls credentials");
+	sendok("");
+	Bflush(&out);
+
 	memset(&conn, 0, sizeof conn);
+	conn.cert = tlscert;
 	conn.certlen = ntlscert;
-	conn.cert = malloc(ntlscert);
-	if(conn.cert == nil)
-		return senderr("out of memory");
-	memmove(conn.cert, tlscert, ntlscert);
 	if(debug)
 		conn.trace = trace;
-	sendok("");
-	Bflush(&out);
 	fd = tlsServer(0, &conn);
 	if(fd < 0)
 		sysfatal("tlsServer: %r");
@@ -574,8 +574,6 @@
 	dup(fd, 0);
 	dup(fd, 1);
 	close(fd);
-	free(conn.cert);
-	free(conn.sessionID);
 	Binit(&in, 0, OREAD);
 	Binit(&out, 1, OWRITE);
 	didtls = 1;
@@ -665,9 +663,9 @@
  * authentication
  */
 Chalstate *chs;
-char user[256];
-char box[256];
-char cbox[256];
+char user[Pathlen];
+char box[Pathlen];
+char cbox[Pathlen];
 
 static void
 hello(void)
@@ -686,6 +684,7 @@
 {
 	char *p;
 
+	*user = 0;
 	strcpy(box, "/mail/box/");
 	strecpy(box+strlen(box), box+sizeof box-7, arg);
 	strcpy(cbox, box);
@@ -707,8 +706,11 @@
 		return senderr("already authenticated");
 	if(*arg == 0)
 		return senderr("USER requires argument");
-	if(setuser(arg) < 0)
-		return -1;
+	if(setuser(arg) < 0){
+		sleep(15*1000);
+		senderr("you are not expected to understand this");	/* pop3 attack. */
+		exits("");
+	}
 	return sendok("");
 }
 
@@ -728,7 +730,7 @@
 	 * if the address is already there and the user owns it,
 	 * remove it and recreate it to give him a new time quanta.
 	 */
-	if(access(buf, 0) >= 0  && remove(buf) < 0)
+	if(access(buf, 0) >= 0 && remove(buf) < 0)
 		return;
 
 	fd = create(buf, OREAD, 0666);
@@ -776,7 +778,7 @@
 		senderr("newns failed: %r; server exiting");
 		exits(nil);
 	}
-	syslog(0, "pop3", "user %s logged in", user);
+
 	enableaddr();
 	if(readmbox(box) < 0)
 		exits(nil);
@@ -790,6 +792,12 @@
 	uchar digest[MD5dlen];
 	char response[2*MD5dlen+1];
 
+	if(*user == 0){
+		senderr("inscrutable phase error");	// pop3 attack.
+		sleep(15*1000);
+		exits("");
+	}
+
 	if(passwordinclear==0 && didtls==0)
 		return senderr("password in the clear disallowed");
 
@@ -810,8 +818,11 @@
 	char *resp;
 
 	resp = nextarg(arg);
-	if(setuser(arg) < 0)
-		return -1;
+	if(setuser(arg) < 0){
+		senderr("i before e except after c");	// pop3 attack.
+		sleep(15*1000);
+		exits("");
+	}
 	return dologin(resp);
 }
 
--- a/sys/src/cmd/upas/q/mkfile
+++ b/sys/src/cmd/upas/q/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG = qer\
 	runq\
@@ -9,8 +10,6 @@
 	../common/sys.h\
 
 LIB=../common/libcommon.a$O\
-
-BIN=/$objtype/bin/upas
 
 UPDATE=\
 	mkfile\
--- a/sys/src/cmd/upas/q/qer.c
+++ b/sys/src/cmd/upas/q/qer.c
@@ -23,7 +23,7 @@
 void
 error(char *f, char *a)
 {
-	char err[Errlen+1];
+	char err[ERRMAX];
 	char buf[256];
 
 	rerrstr(err, sizeof(err));
--- a/sys/src/cmd/upas/q/runq.c
+++ b/sys/src/cmd/upas/q/runq.c
@@ -136,7 +136,7 @@
 		if(debug)
 			fprint(2, "removing directory %s\n", name);
 		syslog(0, runqlog, "rmdir %s", name);
-		sysremove(name);
+		remove(name);
 		return 1;
 	}
 	return 0;
@@ -184,7 +184,7 @@
 		warning("reading %s", root);
 		return;
 	}
-	n = sysdirreadall(fd, &db);
+	n = dirreadall(fd, &db);
 	if(n > 0){
 		for(i=0; i<n; i++){
 			if(db[i].qid.type & QTDIR){
@@ -245,7 +245,7 @@
 		warning("reading %s", name);
 		return;
 	}
-	nfiles = sysdirreadall(fd, &dirbuf);
+	nfiles = dirreadall(fd, &dirbuf);
 	if(nfiles > 0){
 		for(i=0; i<nfiles; i++){
 			if(dirbuf[i].name[0]!='C' || dirbuf[i].name[1]!='.')
@@ -272,13 +272,13 @@
 
 	for(i=0; i<nfiles; i++){
 		if(strcmp(&dirbuf[i].name[1], &name[1]) == 0)
-			sysremove(dirbuf[i].name);
+			remove(dirbuf[i].name);
 	}
 
 	/* error file (may have) appeared after we read the directory */
 	/* stomp on data file in case of phase error */
-	sysremove(file(name, 'D'));
-	sysremove(file(name, 'E'));
+	remove(file(name, 'D'));
+	remove(file(name, 'E'));
 }
 
 /*
@@ -295,8 +295,7 @@
 	if(l == 0)
 		return 0;
 	l->fd = fd;
-	l->name = s_new();
-	s_append(l->name, path);
+	snprint(l->name, sizeof l->name, "%s", path);
 
 	/* fork process to keep lock alive until sysunlock(l) */
 	switch(l->pid = rfork(RFPROC)){
@@ -356,11 +355,7 @@
 	if(!Eflag && (d = dirstat(file(dp->name, 'E'))) != nil){
 		etime = d->mtime;
 		free(d);
-		if(etime - dtime < 15*60){
-			/* up to the first 15 minutes, every 30 seconds */
-			if(time(0) - etime < 30)
-				return;
-		} else if(etime - dtime < 60*60){
+		if(etime - dtime < 60*60){
 			/* up to the first hour, try every 15 minutes */
 			if(time(0) - etime < 15*60)
 				return;
@@ -517,11 +512,7 @@
 		if(wm->msg[0]){
 			if(debug)
 				fprint(2, "[%d] wm->msg == %s\n", getpid(), wm->msg);
-			syslog(0, runqlog, "message: %s", wm->msg);
-			if(strstr(wm->msg, "Ignore") != nil){
-				/* fix for fish/chips, leave message alone */
-				logit("ignoring", dp->name, av);
-			}else if(!Rflag && strstr(wm->msg, "Retry")==0){
+			if(!Rflag && strstr(wm->msg, "Retry")==0){
 				/* return the message and remove it */
 				if(returnmail(av, dp->name, wm->msg) != 0)
 					logit("returnmail failed", dp->name, av);
@@ -572,15 +563,11 @@
 int
 returnmail(char **av, char *name, char *msg)
 {
-	int pfd[2];
-	Waitmsg *wm;
-	int fd;
-	char buf[256];
-	char attachment[256];
-	int i;
+	char buf[256], attachment[Pathlen], *sender;
+	int i, fd, pfd[2];
 	long n;
+	Waitmsg *wm;
 	String *s;
-	char *sender;
 
 	if(av[1] == 0 || av[2] == 0){
 		logit("runq - dumping bad file", name, av);
@@ -665,7 +652,7 @@
 void
 warning(char *f, void *a)
 {
-	char err[65];
+	char err[ERRMAX];
 	char buf[256];
 
 	rerrstr(err, sizeof(err));
@@ -679,7 +666,7 @@
 void
 error(char *f, void *a)
 {
-	char err[Errlen];
+	char err[ERRMAX];
 	char buf[256];
 
 	rerrstr(err, sizeof(err));
--- /dev/null
+++ b/sys/src/cmd/upas/qfrom/mkfile
@@ -1,0 +1,13 @@
+</$objtype/mkfile
+
+TARG=qfrom
+OFILES=qfrom.$O\
+
+BIN=/$objtype/bin/upas
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/sys/src/cmd/upas/qfrom/qfrom.c
@@ -1,0 +1,63 @@
+/*
+ * quote from lines without messing with character encoding.
+ *	(might rather just undo the character encoding and use sed.)
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+void
+qfrom(int fd)
+{
+	Biobuf b, bo;
+	char *s;
+	int l;
+
+	if(Binit(&b, fd, OREAD) == -1)
+		sysfatal("Binit: %r");
+	if(Binit(&bo, 1, OWRITE) == -1)
+		sysfatal("Binit: %r");
+	
+	while(s = Brdstr(&b, '\n', 0)){
+		l = Blinelen(&b);
+		if(l >= 5)
+		if(memcmp(s, "From ", 5) == 0)
+			Bputc(&bo, ' ');
+		Bwrite(&bo, s, l);
+		free(s);
+	}
+	Bterm(&b);
+	Bterm(&bo);
+}
+	
+void
+usage(void)
+{
+	fprint(2, "usage: qfrom [files...]\n");
+	exits("");
+}
+
+void
+main(int argc, char **argv)
+{
+	int fd;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+	
+	if(*argv == 0){
+		qfrom(0);
+		exits("");
+	}
+	for(; *argv; argv++){
+		fd = open(*argv, OREAD);
+		if(fd == -1)
+			sysfatal("open: %r");
+		qfrom(fd);
+		close(fd);
+	}
+	exits("");
+}
--- a/sys/src/cmd/upas/scanmail/mkfile
+++ b/sys/src/cmd/upas/scanmail/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=scanmail\
 	testscan
@@ -10,7 +11,6 @@
 
 LIB=	../common/libcommon.a$O\
 
-BIN=/$objtype/bin/upas
 UPDATE=\
 	mkfile\
 	$HFILES\
@@ -21,4 +21,4 @@
 CFLAGS=$CFLAGS -I../common
 
 scanmail.$O:	scanmail.c
-	$CC $CFLAGS -D'SPOOL="/mail"' scanmail.c
+	$CC $CFLAGS scanmail.c
--- a/sys/src/cmd/upas/scanmail/scanmail.c
+++ b/sys/src/cmd/upas/scanmail/scanmail.c
@@ -1,4 +1,5 @@
 #include "common.h"
+#include <regexp.h>
 #include "spam.h"
 
 int	cflag;
@@ -35,7 +36,7 @@
 void
 usage(void)
 {
-	fprint(2, "missing or bad arguments to qer\n");
+	fprint(2, "usage: scanmail [-cdhnstv] [-p pattern] [-q queuename] sender dest sys\n");
 	exits("usage");
 }
 
@@ -51,8 +52,8 @@
 	void *p;
 
 	p = malloc(n);
-	if(p == 0)
-		exits("malloc");
+	if(p == nil)
+		sysfatal("malloc: %r");
 	return p;
 }
 
@@ -60,8 +61,9 @@
 Realloc(void *p, ulong n)
 {
 	p = realloc(p, n);
-	if(p == 0)
-		exits("realloc");
+	if(p == nil)
+		sysfatal("malloc: %r");
+	setrealloctag(p, getcallerpc(&p));
 	return p;
 }
 
@@ -76,10 +78,10 @@
 
 	optout = 1;
 	a = args = Malloc((argc+1)*sizeof(char*));
-	sprint(patfile, "%s/patterns", UPASLIB);
-	sprint(linefile, "%s/lines", UPASLOG);
-	sprint(holdqueue, "%s/queue.hold", SPOOL);
-	sprint(copydir, "%s/copy", SPOOL);
+	snprint(patfile, sizeof patfile, "%s/patterns", UPASLIB);
+	snprint(linefile, sizeof linefile, "%s/lines", UPASLOG);
+	snprint(holdqueue, sizeof holdqueue, "%s/queue.hold", SPOOL);
+	snprint(copydir, sizeof copydir, "%s/copy", SPOOL);
 
 	*a++ = argv[0];
 	for(argc--, argv++; argv[0] && argv[0][0] == '-'; argc--, argv++){
@@ -142,7 +144,7 @@
 	qdir = a;
 	sender = argv[2];
 
-		/* copy the rest of argv, acummulating the recipients as we go */
+	/* copy the rest of argv, acummulating the recipients as we go */
 	for(i = 0; argv[i]; i++){
 		*a++ = argv[i];
 		if(i < 4)	/* skip queue, 'mail', sender, dest sys */
@@ -161,17 +163,17 @@
 			optout = 0;
 	}
 	*a = 0;
-		/* construct a command string for matching */
+	/* construct a command string for matching */
 	snprint(cmd, sizeof(cmd)-1, "%s %s", sender, s_to_c(recips));
 	cmd[sizeof(cmd)-1] = 0;
 	for(cp = cmd; *cp; cp++)
 		*cp = tolower(*cp);
 
-		/* canonicalize a copy of the header and body.
-		 * buf points to orginal message and n contains
-		 * number of bytes of original message read during
-		 * canonicalization.
-		 */
+	/* canonicalize a copy of the header and body.
+	 * buf points to orginal message and n contains
+	 * number of bytes of original message read during
+	 * canonicalization.
+	 */
 	*body = 0;
 	*header = 0;
 	buf = canon(&bin, header+1, body+1, &n);
@@ -178,7 +180,7 @@
 	if (buf == 0)
 		exits("read");
 
-		/* if all users opt out, don't try matches */
+	/* if all users opt out, don't try matches */
 	if(optout){
 		if(cflag)
 			cout = opencopy(sender);
@@ -185,17 +187,17 @@
 		exits(qmail(args, buf, n, cout));
 	}
 
-		/* Turn off line logging, if command line matches */
+	/* Turn off line logging, if command line matches */
 	nolines = matchaction(Lineoff, cmd, match);
 
 	for(i = 0; patterns[i].action; i++){
-			/* Lineoff patterns were already done above */
+		/* Lineoff patterns were already done above */
 		if(i == Lineoff)
 			continue;
-			/* don't apply "Line" patterns if excluded above */
+		/* don't apply "Line" patterns if excluded above */
 		if(nolines && i == SaveLine)
 			continue;
-			/* apply patterns to the sender/recips, header and body */
+		/* apply patterns to the sender/recips, header and body */
 		if(matchaction(i, cmd, match))
 			break;
 		if(matchaction(i, header+1, match))
@@ -407,9 +409,9 @@
 	cp[7] = 0;
 	cp[10] = 0;
 	if(cp[8] == ' ')
-		sprint(buf, "%s/queue.dump/%s%c", SPOOL, cp+4, cp[9]);
+		snprint(buf, sizeof buf, "%s/queue.dump/%s%c", SPOOL, cp+4, cp[9]);
 	else
-		sprint(buf, "%s/queue.dump/%s%c%c", SPOOL, cp+4, cp[8], cp[9]);
+		snprint(buf, sizeof buf, "%s/queue.dump/%s%c%c", SPOOL, cp+4, cp[8], cp[9]);
 	cp = buf+strlen(buf);
 	if(access(buf, 0) < 0 && sysmkdir(buf, 0777) < 0){
 		syslog(0, "smtpd", "couldn't dump mail from %s: %r", sender);
@@ -421,7 +423,7 @@
 		h = h*257 + *sender++;
 	for(i = 0; i < 50; i++){
 		h += lrand();
-		sprint(cp, "/%lud", h);
+		seprint(cp, buf+sizeof buf, "/%lud", h);
 		b = sysopen(buf, "wlc", 0644);
 		if(b){
 			if(vflag)
@@ -445,7 +447,7 @@
 		h = h*257 + *sender++;
 	for(i = 0; i < 50; i++){
 		h += lrand();
-		sprint(buf, "%s/%lud", copydir, h);
+		snprint(buf, sizeof buf, "%s/%lud", copydir, h);
 		b = sysopen(buf, "wlc", 0600);
 		if(b)
 			return b;
--- a/sys/src/cmd/upas/scanmail/spam.h
+++ b/sys/src/cmd/upas/scanmail/spam.h
@@ -1,4 +1,3 @@
-
 enum{
 	Dump		= 0,		/* Actions must be in order of descending importance */
 	HoldHeader,
--- a/sys/src/cmd/upas/scanmail/testscan.c
+++ b/sys/src/cmd/upas/scanmail/testscan.c
@@ -1,4 +1,5 @@
-#include "sys.h"
+#include "common.h"
+#include <regexp.h>
 #include "spam.h"
 
 int 	debug;
@@ -13,7 +14,7 @@
 void
 usage(void)
 {
-	fprint(2, "missing or bad arguments to qer\n");
+	fprint(2, "usage: testscan -avd [-p pattern] ...\n");
 	exits("usage");
 }
 
@@ -23,10 +24,9 @@
 	void *p;
 
 	p = malloc(n);
-	if(p == 0){
-		fprint(2, "malloc error");
-		exits("malloc");
-	}
+	if(p == nil)
+		sysfatal("malloc: %r");
+	setmalloctag(p, getcallerpc(&n));
 	return p;
 }
 
@@ -34,10 +34,9 @@
 Realloc(void *p, ulong n)
 {
 	p = realloc(p, n);
-	if(p == 0){
-		fprint(2, "realloc error");
-		exits("realloc");
-	}
+	if(p == nil)
+		sysfatal("malloc: %r");
+	setrealloctag(p, getcallerpc(&p));
 	return p;
 }
 
@@ -79,7 +78,7 @@
 	char body[Bodysize+2], *raw, *ret;
 	Biobuf *bp;
 
-	sprint(patfile, "%s/patterns", UPASLIB);
+	snprint(patfile, sizeof patfile, "%s/patterns", UPASLIB);
 	aflag = -1;
 	vflag = 0;
 	ARGBEGIN {
@@ -93,7 +92,7 @@
 		debug++;
 		break;
 	case 'p':
-		strcpy(patfile,ARGF());
+		snprint(patfile, sizeof patfile, "%s", EARGF(usage()));
 		break;
 	} ARGEND
 
--- a/sys/src/cmd/upas/send/authorize.c
+++ b/sys/src/cmd/upas/send/authorize.c
@@ -12,7 +12,7 @@
 	String *errstr;
 
 	dp->authorized = 1;
-	pp = proc_start(s_to_c(dp->repl1), (stream *)0, (stream *)0, outstream(), 1, 0);
+	pp = proc_start(s_to_c(dp->repl1), 0, 0, outstream(), 1, 0);
 	if (pp == 0){
 		dp->status = d_noforward;
 		return;
--- a/sys/src/cmd/upas/send/bind.c
+++ b/sys/src/cmd/upas/send/bind.c
@@ -1,20 +1,34 @@
 #include "common.h"
 #include "send.h"
 
-static int forward_loop(char *, char *);
+/* Return TRUE if a forwarding loop exists, i.e., the String `system'
+ * is found more than 4 times in the return address.
+ */
+static int
+forward_loop(char *addr, char *system)
+{
+	int len, found;
 
+	found = 0;
+	len = strlen(system);
+	while(addr = strchr(addr, '!'))
+		if (!strncmp(++addr, system, len)
+		 && addr[len] == '!' && ++found == 4)
+			return 1;
+	return 0;
+}
+
+
 /* bind the destinations to the commands to be executed */
-extern dest *
+dest *
 up_bind(dest *destp, message *mp, int checkforward)
 {
-	dest *list[2];		/* lists of unbound destinations */
-	int li;			/* index into list[2] */
-	dest *bound=0;	/* bound destinations */
-	dest *dp;
-	int i;
+	int i, li;
+	dest *list[2], *bound, *dp;
 
+	bound = nil;
 	list[0] = destp;
-	list[1] = 0;
+	list[1] = nil;
 
 	/*
 	 *  loop once to check for:
@@ -24,7 +38,7 @@
 	 *	- characters that need escaping
 	 */
 	for (dp = d_rm(&list[0]); dp != 0; dp = d_rm(&list[0])) {
-		if (!checkforward)
+		if(!checkforward)
 			dp->authorized = 1;
 		dp->addr = escapespecial(dp->addr);
 		if (forward_loop(s_to_c(dp->addr), thissys)) {
@@ -115,19 +129,3 @@
 
 	return bound;
 }
-
-/* Return TRUE if a forwarding loop exists, i.e., the String `system'
- * is found more than 4 times in the return address.
- */
-static int
-forward_loop(char *addr, char *system)
-{
-	int len = strlen(system), found = 0;
-
-	while (addr = strchr(addr, '!'))
-		if (!strncmp(++addr, system, len)
-		 && addr[len] == '!' && ++found == 4)
-			return 1;
-	return 0;
-}
-
--- a/sys/src/cmd/upas/send/cat_mail.c
+++ b/sys/src/cmd/upas/send/cat_mail.c
@@ -1,57 +1,121 @@
 #include "common.h"
 #include "send.h"
 
+/*
+ * warning will robinson
+ *
+ * mbox and mdir should likely be merged with ../common/folder.c
+ * at a minimum, changes need to done in sync.
+ */
 
-/* dispose of local addresses */
-int
-cat_mail(dest *dp, message *mp)
+static int
+mbox(dest *dp, message *mp, char *s)
 {
-	Biobuf *fp;
-	char *rcvr, *cp;
+	char *tmp;
+	int i, n, e;
+	Biobuf *b;
 	Mlock *l;
-	String *tmp, *s;
-	int i, n;
 
-	s = unescapespecial(s_clone(dp->repl1));
-	if (nflg) {
-		if(!xflg)
-			print("cat >> %s\n", s_to_c(s));
-		else
-			print("%s\n", s_to_c(dp->addr));
-		s_free(s);
-		return 0;
-	}
 	for(i = 0;; i++){
-		l = syslock(s_to_c(s));
+		l = syslock(s);
 		if(l == 0)
 			return refuse(dp, mp, "can't lock mail file", 0, 0);
-
-		fp = sysopen(s_to_c(s), "al", MBOXMODE);
-		if(fp)
+		b = sysopen(s, "al", Mboxmode);
+		if(b)
 			break;
-		tmp = s_append(0, s_to_c(s));
-		s_append(tmp, ".tmp");
-		fp = sysopen(s_to_c(tmp), "al", MBOXMODE);
-		if(fp){
-			syslog(0, "mail", "error: used %s", s_to_c(tmp));
-			s_free(tmp);
+		b = sysopen(tmp = smprint("%s.tmp", s), "al", Mboxmode);
+		free(tmp);
+		sysunlock(l);
+		if(b){
+			syslog(0, "mail", "error: used %s.tmp", s);
 			break;
 		}
-		s_free(tmp);
-		sysunlock(l);
 		if(i >= 5)
 			return refuse(dp, mp, "mail file cannot be opened", 0, 0);
 		sleep(1000);
 	}
-	s_free(s);
-	n = m_print(mp, fp, (char *)0, 1);
-	if (Bprint(fp, "\n") < 0 || Bflush(fp) < 0 || n < 0){
-		sysclose(fp);
-		sysunlock(l);
+	e = 0;
+	n = m_print(mp, b, 0, 1);
+	if(n == -1 || Bprint(b, "\n") == -1 || Bflush(b) == -1)
+		e = 1;
+	sysclose(b);
+	sysunlock(l);
+	if(e)
 		return refuse(dp, mp, "error writing mail file", 0, 0);
+	return 0;
+}
+
+static int
+mdir(dest *dp, message *mp, char *s)
+{
+	char buf[100];
+	int fd, i, n, e;
+	ulong t;
+	Biobuf b;
+
+	t = time(0);
+	for(i = 0; i < 100; i++){
+		snprint(buf, sizeof buf, "%s/%lud.%.2d", s, t, i);
+		if((fd = create(buf, OWRITE|OEXCL, DMAPPEND|0660)) != -1)
+			goto found;
 	}
-	sysclose(fp);
-	sysunlock(l);
+	return refuse(dp, mp, "mdir file cannot be opened", 0, 0);
+found:
+	e = 0;
+	Binit(&b, fd, OWRITE);
+	n = m_print(mp, &b, 0, 1);
+	if(n == -1 || Bprint(&b, "\n") == -1 || Bflush(&b) == -1)
+		e = 1;
+	Bterm(&b);
+	close(fd);
+	if(e){
+		remove(buf);
+		return refuse(dp, mp, "error writing mail file", 0, 0);
+	}
+	return 0;
+}
+
+/* dispose of local addresses */
+int
+cat_mail(dest *dp, message *mp)
+{
+	char *rcvr, *cp, *s;
+	int e, isdir;
+	Dir *d;
+	String *ss;
+
+	ss = unescapespecial(s_clone(dp->repl1));
+	s = s_to_c(ss);
+	if (flagn) {
+		if(!flagx)
+			print("upas/mbappend %s\n", s);
+		else
+			print("%s\n", s_to_c(dp->addr));
+		s_free(ss);
+		return 0;
+	}
+	/* avoid lock errors */
+	if(strcmp(s, "/dev/null") == 0){
+		s_free(ss);
+		return(0);
+	}
+	if(d = dirstat(s)){
+		isdir = d->mode&DMDIR;
+		free(d);
+	}else{
+		isdir = create(s, OREAD, DMDIR|0777);
+		if(isdir == -1)
+			return refuse(dp, mp, "mdir cannot be created", 0, 0);
+		close(isdir);
+		isdir = 1;
+	}
+	if(isdir)
+		e = mdir(dp, mp, s);
+	else
+		e = mbox(dp, mp, s);
+	s_free(ss);
+	if(e != 0)
+		return e;
 	rcvr = s_to_c(dp->addr);
 	if(cp = strrchr(rcvr, '!'))
 		rcvr = cp+1;
--- a/sys/src/cmd/upas/send/dest.c
+++ b/sys/src/cmd/upas/send/dest.c
@@ -1,21 +1,17 @@
 #include "common.h"
 #include "send.h"
 
-static String* s_parseq(String*, String*);
-
 /* exports */
 dest *dlist;
 
-extern dest*
+dest*
 d_new(String *addr)
 {
 	dest *dp;
 
 	dp = (dest *)mallocz(sizeof(dest), 1);
-	if (dp == 0) {
-		perror("d_new");
-		exit(1);
-	}
+	if (dp == 0)
+		sysfatal("malloc: %r");
 	dp->same = dp;
 	dp->nsame = 1;
 	dp->nchar = 0;
@@ -27,7 +23,7 @@
 	return dp;
 }
 
-extern void
+void
 d_free(dest *dp)
 {
 	if (dp != 0) {
@@ -46,7 +42,7 @@
  */
 
 /*  Get first element from a circular list linked via 'next'. */
-extern dest *
+dest*
 d_rm(dest **listp)
 {
 	dest *dp;
@@ -63,7 +59,7 @@
 }
 
 /*  Insert a new entry at the end of the list linked via 'next'. */
-extern void
+void
 d_insert(dest **listp, dest *new)
 {
 	dest *head;
@@ -82,7 +78,7 @@
 }
 
 /*  Get first element from a circular list linked via 'same'. */
-extern dest *
+dest*
 d_rm_same(dest **listp)
 {
 	dest *dp;
@@ -114,10 +110,11 @@
 	return 0;
 }
 
-/* Insert an entry into the corresponding list linked by 'same'.  Note that
+/*
+ * Insert an entry into the corresponding list linked by 'same'.  Note that
  * the basic structure is a list of lists.
  */
-extern void
+void
 d_same_insert(dest **listp, dest *new)
 {
 	dest *dp;
@@ -124,7 +121,9 @@
 	int len;
 
 	if(new->status == d_pipe || new->status == d_cat) {
-		len = new->repl2 ? strlen(s_to_c(new->repl2)) : 0;
+		len = 0;
+		if(new->repl2)
+			len = strlen(s_to_c(new->repl2));
 		if(*listp != 0){
 			dp = (*listp)->next;
 			do {
@@ -146,7 +145,10 @@
 				dp = dp->next;
 			} while (dp != (*listp)->next);
 		}
-		new->nchar = strlen(s_to_c(new->repl1)) + len + 1;
+		if(s_to_c(new->repl1))
+			new->nchar = strlen(s_to_c(new->repl1)) + len + 1;
+		else
+			new->nchar = 0;
 	}
 	new->next = new;
 	d_insert(listp, new);
@@ -157,7 +159,7 @@
  *  The local! and !local! checks are artificial intelligence,
  *  there should be a better way.
  */
-extern String*
+String*
 d_to(dest *list)
 {
 	dest *np, *sp;
@@ -197,34 +199,7 @@
 	return unescapespecial(s);
 }
 
-/* expand a String of destinations into a linked list of destiniations */
-extern dest *
-s_to_dest(String *sp, dest *parent)
-{
-	String *addr;
-	dest *list=0;
-	dest *new;
 
-	if (sp == 0)
-		return 0;
-	addr = s_new();
-	while (s_parseq(sp, addr)!=0) {
-		addr = escapespecial(addr);
-		if(shellchars(s_to_c(addr))){
-			while(new = d_rm(&list))
-				d_free(new);
-			break;
-		}
-		new = d_new(addr);
-		new->parent = parent;
-		new->authorized = parent->authorized;
-		d_insert(&list, new);
-		addr = s_new();
-	}
-	s_free(addr);
-	return list;
-}
-
 #define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
 
 /*  Get the next field from a String.  The field is delimited by white space.
@@ -256,4 +231,32 @@
 		from->ptr++;
 
 	return to;
+}
+
+/* expand a String of destinations into a linked list of destiniations */
+dest*
+s_to_dest(String *sp, dest *parent)
+{
+	String *addr;
+	dest *list=0;
+	dest *new;
+
+	if (sp == 0)
+		return 0;
+	addr = s_new();
+	while (s_parseq(sp, addr)!=0) {
+		addr = escapespecial(addr);
+		if(shellchars(s_to_c(addr))){
+			while(new = d_rm(&list))
+				d_free(new);
+			break;
+		}
+		new = d_new(addr);
+		new->parent = parent;
+		new->authorized = parent->authorized;
+		d_insert(&list, new);
+		addr = s_new();
+	}
+	s_free(addr);
+	return list;
 }
--- a/sys/src/cmd/upas/send/filter.c
+++ b/sys/src/cmd/upas/send/filter.c
@@ -1,16 +1,40 @@
 #include "common.h"
 #include "send.h"
+#include <regexp.h>
 
 Biobuf	bin;
-int rmail, tflg;
-char *subjectarg;
+int	flagn;
+int	flagx;
+int	rmail;
+int	tflg;
+char	*subjectarg;
 
-char *findbody(char*);
+char*
+findbody(char *p)
+{
+	if(*p == '\n')
+		return p;
 
+	while(*p){
+		if(*p == '\n' && *(p+1) == '\n')
+			return p+1;
+		p++;
+	}
+	return p;
+}
+
+int
+refuse(dest*, message *, char *cp, int, int)
+{
+	fprint(2, "%s", cp);
+	exits("error");
+	return 0;
+}
+
 void
 usage(void)
 {
-	fprint(2, "usage: upas/filter [-bh] rcvr mailbox [regexp file] ...\n");
+	fprint(2, "usage: upas/filter [-nbh] rcvr mailbox [regexp file] ...\n");
 	exits("usage");
 }
 
@@ -17,20 +41,18 @@
 void
 main(int argc, char *argv[])
 {
+	char *cp, file[Pathlen];
+	int i, header, body;
 	message *mp;
 	dest *dp;
 	Reprog *p;
 	Resub match[10];
-	char file[MAXPATHLEN];
-	Biobuf *fp;
-	char *rcvr, *cp;
-	Mlock *l;
-	String *tmp;
-	int i;
-	int header, body;
 
 	header = body = 0;
 	ARGBEGIN {
+	case 'n':
+		flagn = 1;
+		break;
 	case 'h':
 		header = 1;
 		break;
@@ -54,7 +76,6 @@
 		mp->sender = s_copy(cp);
 	}
 
-	dp = d_new(s_copy(argv[0]));
 	strecpy(file, file+sizeof file, argv[1]);
 	cp = findbody(s_to_c(mp->body));
 	for(i = 2; i < argc; i += 2){
@@ -74,62 +95,9 @@
 			break;
 		}
 	}
-
-	/*
-	 *  always lock the normal mail file to avoid too many lock files
-	 *  lying about.  This isn't right but it's what the majority prefers.
-	 */
-	l = syslock(argv[1]);
-	if(l == 0){
-		fprint(2, "can't lock mail file %s\n", argv[1]);
-		exit(1);
-	}
-
-	/*
-	 *  open the destination mail file
-	 */
-	fp = sysopen(file, "ca", MBOXMODE);
-	if (fp == 0){
-		tmp = s_append(0, file);
-		s_append(tmp, ".tmp");
-		fp = sysopen(s_to_c(tmp), "cal", MBOXMODE);
-		if(fp == 0){
-			sysunlock(l);
-			fprint(2, "can't open mail file %s\n", file);
-			exit(1);
-		}
-		syslog(0, "mail", "error: used %s", s_to_c(tmp));
-		s_free(tmp);
-	}
-	Bseek(fp, 0, 2);
-	if(m_print(mp, fp, (char *)0, 1) < 0
-	|| Bprint(fp, "\n") < 0
-	|| Bflush(fp) < 0){
-		sysclose(fp);
-		sysunlock(l);
-		fprint(2, "can't write mail file %s\n", file);
-		exit(1);
-	}
-	sysclose(fp);
-
-	sysunlock(l);
-	rcvr = argv[0];
-	if(cp = strrchr(rcvr, '!'))
-		rcvr = cp+1;
-	logdelivery(dp, rcvr, mp);
-	exit(0);
-}
-
-char*
-findbody(char *p)
-{
-	if(*p == '\n')
-		return p;
-
-	while(*p){
-		if(*p == '\n' && *(p+1) == '\n')
-			return p+1;
-		p++;
-	}
-	return p;
+	dp = d_new(s_copy(argv[0]));
+	dp->repl1 = s_copy(file);
+	if(cat_mail(dp, mp) != 0)
+		exits("fail");
+	exits("");
 }
--- a/sys/src/cmd/upas/send/gateway.c
+++ b/sys/src/cmd/upas/send/gateway.c
@@ -1,13 +1,11 @@
 #include "common.h"
 #include "send.h"
 
-#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
-
 /*
  *  Translate the last component of the sender address.  If the translation
  *  yields the same address, replace the sender with its last component.
  */
-extern void
+void
 gateway(message *mp)
 {
 	char *base;
--- a/sys/src/cmd/upas/send/local.c
+++ b/sys/src/cmd/upas/send/local.c
@@ -1,12 +1,21 @@
 #include "common.h"
 #include "send.h"
 
+static String*
+mboxpath(char *path, char *user, String *to)
+{
+	char buf[Pathlen];
+
+	mboxpathbuf(buf, sizeof buf, user, path);
+	return s_append(to, buf);
+}
+
 static void
 mboxfile(dest *dp, String *user, String *path, char *file)
 {
 	char *cp;
 
-	mboxpath(s_to_c(user), s_to_c(dp->addr), path, 0);
+	mboxpath(s_to_c(user), s_to_c(dp->addr), path);
 	cp = strrchr(s_to_c(path), '/');
 	if(cp)
 		path->ptr = cp+1;
@@ -16,6 +25,52 @@
 }
 
 /*
+ * BOTCH, BOTCH
+ * the problem is that we don't want to say a user exists
+ * just because the user has a mail box directory.  that
+ * precludes using mode bits to disable mailboxes.
+ *
+ * botch #1: we pretend like we know that there must be a
+ * corresponding file or directory /mail/box/$user[/folder]/mbox
+ * this is wrong, but we get away with this for local mail boxes.
+ *
+ * botch #2: since the file server and not the auth server owns
+ * groups, it's not possible to get groups right.  this means that
+ * a mailbox that only allows members of a group to post but
+ * not read wouldn't work.
+ */
+static uint accesstx[] = {
+[OREAD]	1<<2,
+[OWRITE]	1<<1,
+[ORDWR]	3<<1,
+[OEXEC]	1<<0
+};
+
+static int
+accessmbox(char *f, int m)
+{
+	int r, n;
+	Dir *d;
+
+	d = dirstat(f);
+	if(d == nil)
+		return -1;
+	n = 0;
+	if(m < nelem(accesstx))
+		n = accesstx[m];
+	if(d->mode & DMDIR)
+		n |= OEXEC;
+	r = (d->mode & n<<0) == n<<0;
+//	if(r == 0 && inlist(mygids(), d->gid) == 0)
+//		r = (d->mode & n<<3) == n<<3;
+	if(r == 0 && strcmp(getlog(), d->uid) == 0)
+		r = (d->mode & n<<6) == n<<6;
+	r--;
+	free(d);
+	return r;
+}
+
+/*
  *  Check forwarding requests
  */
 extern dest*
@@ -43,7 +98,7 @@
 	/* if no replacement string, plug in user's name */
 	if(dp->repl1 == 0){
 		dp->repl1 = s_new();
-		mboxname(user, dp->repl1);
+		mboxpath("mbox", user, dp->repl1);
 	}
 
 	s = unescapespecial(s_clone(dp->repl1));
@@ -95,7 +150,7 @@
 	 *  name passes through a shell.  tdb.
 	 */
 	mboxfile(dp, dp->repl1, s_reset(file), "pipeto");
-	if(sysexist(s_to_c(file))){
+	if(access(s_to_c(file), AEXEC) == 0){
 		if(debug)
 			fprint(2, "found a pipeto file\n");
 		dp->status = d_pipeto;
@@ -118,8 +173,8 @@
 	/*
 	 *  see if the mailbox directory exists
 	 */
-	mboxfile(dp, s, s_reset(file), ".");
-	if(sysexist(s_to_c(file)))
+	mboxfile(dp, s, s_reset(file), "mbox");
+	if(accessmbox(s_to_c(file), OWRITE) != -1)
 		dp->status = d_cat;
 	else
 		dp->status = d_unknown;
--- a/sys/src/cmd/upas/send/log.c
+++ b/sys/src/cmd/upas/send/log.c
@@ -1,11 +1,8 @@
 #include "common.h"
 #include "send.h"
 
-/* configuration */
-#define LOGBiobuf "log/status"
-
 /* log mail delivery */
-extern void
+void
 logdelivery(dest *list, char *rcvr, message *mp)
 {
 	dest *parent;
@@ -29,7 +26,7 @@
 }
 
 /* log mail forwarding */
-extern void
+void
 loglist(dest *list, message *mp, char *tag)
 {
 	dest *next;
@@ -57,7 +54,7 @@
 }
 
 /* log a mail refusal */
-extern void
+void
 logrefusal(dest *dp, message *mp, char *msg)
 {
 	char buf[2048];
@@ -67,7 +64,7 @@
 	srcvr = unescapespecial(s_clone(dp->addr));
 	sender = unescapespecial(s_clone(mp->sender));
 
-	sprint(buf, "error %.256s From %.256s %.256s\nerror+ ", s_to_c(srcvr),
+	snprint(buf, sizeof buf, "error %.256s From %.256s %.256s\nerror+ ", s_to_c(srcvr),
 		s_to_c(sender), s_to_c(mp->date));
 	s_free(srcvr);
 	s_free(sender);
--- a/sys/src/cmd/upas/send/main.c
+++ b/sys/src/cmd/upas/send/main.c
@@ -2,40 +2,37 @@
 #include "send.h"
 
 /* globals to all files */
-int rmail;
-char *thissys, *altthissys;
-int nflg;
-int xflg;
-int debug;
-int rflg;
-int iflg = 1;
-int nosummary;
+int	flagn;
+int	flagx;
+int	debug;
+int	flagi  = 1;
+int	rmail;
+int	nosummary;
+char	*thissys;
+char	*altthissys;
 
 /* global to this file */
-static String *errstring;
-static message *mp;
-static int interrupt;
-static int savemail;
-static Biobuf in;
-static int forked;
-static int add822headers = 1;
-static String *arglist;
+static	String	*errstring;
+static	message	*mp;
+static	int	interrupt;
+static	int	savemail;
+static	Biobuf	in;
+static	int	forked;
+static	int	add822headers = 1;
+static	String	*arglist;
 
 /* predeclared */
-static int	send(dest *, message *, int);
-static void	lesstedious(void);
-static void	save_mail(message *);
-static int	complain_mail(dest *, message *);
-static int	pipe_mail(dest *, message *);
-static void	appaddr(String *, dest *);
-static void	mkerrstring(String *, message *, dest *, dest *, char *, int);
-static int	replymsg(String *, message *, dest *);
-static int	catchint(void*, char*);
+static	int	send(dest*, message*, int);
+static	void	lesstedious(void);
+static	void	save_mail(message*);
+static	int	complain_mail(dest*, message*);
+static	int	pipe_mail(dest*, message*);
+static	int	catchint(void*, char*);
 
 void
 usage(void)
 {
-	fprint(2, "usage: mail [-birtx] list-of-addresses\n");
+	fprint(2, "usage: send [-#bdirx] list-of-addresses\n");
 	exits("usage");
 }
 
@@ -42,54 +39,45 @@
 void
 main(int argc, char *argv[])
 {
-	dest *dp=0;
-	int checkforward;
-	char *base;
 	int rv;
+	dest *dp;
 
-	/* process args */
 	ARGBEGIN{
 	case '#':
-		nflg = 1;
+		flagn = 1;
 		break;
 	case 'b':
 		add822headers = 0;
 		break;
-	case 'x':
-		nflg = 1;
-		xflg = 1;
-		break;
 	case 'd':
 		debug = 1;
 		break;
 	case 'i':
-		iflg = 0;
+		flagi = 0;
 		break;
 	case 'r':
-		rflg = 1;
+		rmail++;
 		break;
+	case 'x':
+		flagn = 1;
+		flagx = 1;
+		break;
 	default:
 		usage();
 	}ARGEND
 
-	while(*argv){
+	if(*argv == 0)
+		usage();
+	dp = 0;
+	for(; *argv; argv++){
 		if(shellchars(*argv)){
 			fprint(2, "illegal characters in destination\n");
 			exits("syntax");
 		}
-		d_insert(&dp, d_new(s_copy(*argv++)));
+		d_insert(&dp, d_new(s_copy(*argv)));
 	}
-
-	if (dp == 0)
-		usage();
 	arglist = d_to(dp);
 
-	/*
-	 * get context:
-	 *	- whether we're rmail or mail
-	 */
-	base = basename(argv0);
-	checkforward = rmail = (strcmp(base, "rmail")==0) | rflg;
 	thissys = sysname_read();
 	altthissys = alt_sysname_read();
 	if(rmail)
@@ -99,22 +87,22 @@
 	 *  read the mail.  If an interrupt occurs while reading, save in
 	 *  dead.letter
 	 */
-	if (!nflg) {
+	if (!flagn) {
 		Binit(&in, 0, OREAD);
 		if(!rmail)
 			atnotify(catchint, 1);
-		mp = m_read(&in, rmail, !iflg);
+		mp = m_read(&in, rmail, !flagi);
 		if (mp == 0)
-			exit(0);
+			exits(0);
 		if (interrupt != 0) {
 			save_mail(mp);
-			exit(1);
+			exits("interrupt");
 		}
 	} else {
 		mp = m_new();
 		if(default_from(mp) < 0){
 			fprint(2, "%s: can't determine login name\n", argv0);
-			exit(1);
+			exits("fail");
 		}
 	}
 	errstring = s_new();
@@ -132,7 +120,7 @@
 	 *  security reasons.
 	 */
 	mp->sender = escapespecial(mp->sender);
-	if (shellchars(s_to_c(mp->sender)))
+	if(shellchars(s_to_c(mp->sender)))
 		mp->replyaddr = s_copy("postmaster");
 	else
 		mp->replyaddr = s_clone(mp->sender);
@@ -141,7 +129,7 @@
 	 *  reject messages that have been looping for too long
 	 */
 	if(mp->received > 32)
-		exit(refuse(dp, mp, "possible forward loop", 0, 0));
+		exits(refuse(dp, mp, "possible forward loop", 0, 0)? "refuse": "");
 
 	/*
 	 *  reject messages that are too long.  We don't do it earlier
@@ -148,14 +136,14 @@
 	 *  in m_read since we haven't set up enough things yet.
 	 */
 	if(mp->size < 0)
-		exit(refuse(dp, mp, "message too long", 0, 0));
+		exits(refuse(dp, mp, "message too long", 0, 0)? "refuse": "");
 
-	rv = send(dp, mp, checkforward);
+	rv = send(dp, mp, rmail);
 	if(savemail)
 		save_mail(mp);
 	if(mp)
 		m_free(mp);
-	exit(rv);
+	exits(rv? "fail": "");
 }
 
 /* send a message to a list of sites */
@@ -183,10 +171,7 @@
 			break;
 		case d_pipeto:
 		case d_pipe:
-			if (!rmail && !nflg && !forked) {
-				forked = 1;
-				lesstedious();
-			}
+			lesstedious();
 			errors += pipe_mail(dp, mp);
 			break;
 		default:
@@ -206,7 +191,8 @@
 
 	if(debug)
 		return;
-
+	if(rmail || flagn || forked)
+		return;
 	switch(fork()){
 	case -1:
 		break;
@@ -215,9 +201,10 @@
 		for(i=0; i<3; i++)
 			close(i);
 		savemail = 0;
+		forked = 1;
 		break;
 	default:
-		exit(0);
+		exits("");
 	}
 }
 
@@ -226,26 +213,23 @@
 static void
 save_mail(message *mp)
 {
+	char buf[Pathlen];
 	Biobuf *fp;
-	String *file;
 
-	file = s_new();
-	deadletter(file);
-	fp = sysopen(s_to_c(file), "cAt", 0660);
+	mboxpathbuf(buf, sizeof buf, getlog(), "dead.letter");
+	fp = sysopen(buf, "cAt", 0660);
 	if (fp == 0)
 		return;
 	m_bprint(mp, fp);
 	sysclose(fp);
-	fprint(2, "saved in %s\n", s_to_c(file));
-	s_free(file);
+	fprint(2, "saved in %s\n", buf);
 }
 
 /* remember the interrupt happened */
 
 static int
-catchint(void *a, char *msg)
+catchint(void*, char *msg)
 {
-	USED(a);
 	if(strstr(msg, "interrupt") || strstr(msg, "hangup")) {
 		interrupt = 1;
 		return 1;
@@ -303,7 +287,7 @@
 		msg = "unknown d_";
 		break;
 	}
-	if (nflg) {
+	if (flagn) {
 		print("%s: %s\n", msg, s_to_c(dp->addr));
 		return 0;
 	}
@@ -314,13 +298,23 @@
 static int
 pipe_mail(dest *dp, message *mp)
 {
-	dest *next, *list=0;
-	String *cmd;
-	process *pp;
-	int status, r;
+	int status;
 	char *none;
-	String *errstring=s_new();
+	dest *next, *list;
+	process *pp;
+	String *cmd;
+	String *errstring;
 
+	errstring = s_new();
+	list = 0;
+
+	/*
+	 * we're just protecting users from their own
+	 * pipeto scripts with this none business.
+	 * this depends on none being able to append
+	 * to a mail file.
+	 */
+
 	if (dp->status == d_pipeto)
 		none = "none";
 	else
@@ -329,12 +323,12 @@
 	 *  collect the arguments
 	 */
 	next = d_rm_same(&dp);
-	if(xflg)
+	if(flagx)
 		cmd = s_new();
 	else
 		cmd = s_clone(next->repl1);
 	for(; next != 0; next = d_rm_same(&dp)){
-		if(xflg){
+		if(flagx){
 			s_append(cmd, s_to_c(next->addr));
 			s_append(cmd, "\n");
 		} else {
@@ -346,8 +340,8 @@
 		d_insert(&list, next);
 	}
 
-	if (nflg) {
-		if(xflg)
+	if (flagn) {
+		if(flagx)
 			print("%s", s_to_c(cmd));
 		else
 			print("%s\n", s_to_c(cmd));
@@ -375,128 +369,13 @@
 	/*
 	 *  return status
 	 */
-	if (status != 0) {
-		r = refuse(list, mp, s_to_c(errstring), status, 0);
-		s_free(errstring);
-		return r;
-	}
-	s_free(errstring);
+	if (status != 0)
+		return refuse(list, mp, s_to_c(errstring), status, 0);
 	loglist(list, mp, "remote");
 	return 0;
 }
 
-static void
-appaddr(String *sp, dest *dp)
-{
-	dest *parent;
-	String *s;
-
-	if (dp->parent != 0) {
-		for(parent=dp->parent; parent->parent!=0; parent=parent->parent)
-			;
-		s = unescapespecial(s_clone(parent->addr));
-		s_append(sp, s_to_c(s));
-		s_free(s);
-		s_append(sp, "' alias `");
-	}
-	s = unescapespecial(s_clone(dp->addr));
-	s_append(sp, s_to_c(s));
-	s_free(s);
-}
-
 /*
- *  reject delivery
- *
- *  returns	0	- if mail has been disposed of
- *		other	- if mail has not been disposed
- */
-int
-refuse(dest *list, message *mp, char *cp, int status, int outofresources)
-{
-	String *errstring=s_new();
-	dest *dp;
-	int rv;
-
-	dp = d_rm(&list);
-	mkerrstring(errstring, mp, dp, list, cp, status);
-
-	/*
-	 *  log first in case we get into trouble
-	 */
-	logrefusal(dp, mp, s_to_c(errstring));
-
-	/*
-	 *  bulk mail is never replied to, if we're out of resources,
-	 *  let the sender try again
-	 */
-	if(rmail){
-		/* accept it or request a retry */
-		if(outofresources){
-			fprint(2, "Mail %s\n", s_to_c(errstring));
-			rv = 1;					/* try again later */
-		} else if(mp->bulk)
-			rv = 0;					/* silently discard bulk */
-		else
-			rv = replymsg(errstring, mp, dp);	/* try later if we can't reply */
-	} else {
-		/* aysnchronous delivery only happens if !rmail */
-		if(forked){
-			/*
-			 *  if spun off for asynchronous delivery, we own the mail now.
-			 *  return it or dump it on the floor.  rv really doesn't matter.
-			 */
-			rv = 0;
-			if(!outofresources && !mp->bulk)
-				replymsg(errstring, mp, dp);
-		} else {
-			fprint(2, "Mail %s\n", s_to_c(errstring));
-			savemail = 1;
-			rv = 1;
-		}
-	}
-
-	s_free(errstring);
-	return rv;
-}
-
-/* make the error message */
-static void
-mkerrstring(String *errstring, message *mp, dest *dp, dest *list, char *cp, int status)
-{
-	dest *next;
-	char smsg[64];
-	String *sender;
-
-	sender = unescapespecial(s_clone(mp->sender));
-
-	/* list all aliases */
-	s_append(errstring, " from '");
-	s_append(errstring, s_to_c(sender));
-	s_append(errstring, "'\nto '");
-	appaddr(errstring, dp);
-	for(next = d_rm(&list); next != 0; next = d_rm(&list)) {
-		s_append(errstring, "'\nand '");
-		appaddr(errstring, next);
-		d_insert(&dp, next);
-	}
-	s_append(errstring, "'\nfailed with error '");
-	s_append(errstring, cp);
-	s_append(errstring, "'.\n");
-
-	/* >> and | deserve different flavored messages */
-	switch(dp->status) {
-	case d_pipe:
-		s_append(errstring, "The mailer `");
-		s_append(errstring, s_to_c(dp->repl1));
-		sprint(smsg, "' returned error status %x.\n\n", status);
-		s_append(errstring, smsg);
-		break;
-	}
-
-	s_free(sender);
-}
-
-/*
  *  create a new boundary
  */
 static String*
@@ -542,30 +421,30 @@
 	refp->haveto = 1;
 	s_append(refp->body, "To: ");
 	s_append(refp->body, rcvr);
-	s_append(refp->body, "\n");
-	s_append(refp->body, "Subject: bounced mail\n");
-	s_append(refp->body, "MIME-Version: 1.0\n");
-	s_append(refp->body, "Content-Type: multipart/mixed;\n");
-	s_append(refp->body, "\tboundary=\"");
+	s_append(refp->body, "\n"
+		"Subject: bounced mail\n"
+		"MIME-Version: 1.0\n"
+		"Content-Type: multipart/mixed;\n"
+		"\tboundary=\"");
 	s_append(refp->body, s_to_c(boundary));
-	s_append(refp->body, "\"\n");
-	s_append(refp->body, "Content-Disposition: inline\n");
-	s_append(refp->body, "\n");
-	s_append(refp->body, "This is a multi-part message in MIME format.\n");
-	s_append(refp->body, "--");
+	s_append(refp->body, "\"\n"
+		"Content-Disposition: inline\n"
+		"\n"
+		"This is a multi-part message in MIME format.\n"
+		"--");
 	s_append(refp->body, s_to_c(boundary));
-	s_append(refp->body, "\n");
-	s_append(refp->body, "Content-Disposition: inline\n");
-	s_append(refp->body, "Content-Type: text/plain; charset=\"US-ASCII\"\n");
-	s_append(refp->body, "Content-Transfer-Encoding: 7bit\n");
-	s_append(refp->body, "\n");
-	s_append(refp->body, "The attached mail");
+	s_append(refp->body, "\n"
+		"Content-Disposition: inline\n"
+		"Content-Type: text/plain; charset=\"US-ASCII\"\n"
+		"Content-Transfer-Encoding: 7bit\n"
+		"\n"
+		"The attached mail");
 	s_append(refp->body, s_to_c(errstring));
 	s_append(refp->body, "--");
 	s_append(refp->body, s_to_c(boundary));
-	s_append(refp->body, "\n");
-	s_append(refp->body, "Content-Type: message/rfc822\n");
-	s_append(refp->body, "Content-Disposition: inline\n\n");
+	s_append(refp->body, "\n"
+		"Content-Type: message/rfc822\n"
+		"Content-Disposition: inline\n\n");
 	s_append(refp->body, s_to_c(mp->body));
 	s_append(refp->body, "--");
 	s_append(refp->body, s_to_c(boundary));
@@ -575,5 +454,115 @@
 	rv = send(ndp, refp, 0);
 	m_free(refp);
 	d_free(ndp);
+	return rv;
+}
+
+static void
+appaddr(String *sp, dest *dp)
+{
+	dest *p;
+	String *s;
+
+	if (dp->parent != 0) {
+		for(p = dp->parent; p->parent; p = p->parent)
+			;
+		s = unescapespecial(s_clone(p->addr));
+		s_append(sp, s_to_c(s));
+		s_free(s);
+		s_append(sp, "' alias `");
+	}
+	s = unescapespecial(s_clone(dp->addr));
+	s_append(sp, s_to_c(s));
+	s_free(s);
+}
+
+/* make the error message */
+static void
+mkerrstring(String *errstring, message *mp, dest *dp, dest *list, char *cp, int status)
+{
+	dest *next;
+	char smsg[64];
+	String *sender;
+
+	sender = unescapespecial(s_clone(mp->sender));
+
+	/* list all aliases */
+	s_append(errstring, " from '");
+	s_append(errstring, s_to_c(sender));
+	s_append(errstring, "'\nto '");
+	appaddr(errstring, dp);
+	for(next = d_rm(&list); next != 0; next = d_rm(&list)) {
+		s_append(errstring, "'\nand '");
+		appaddr(errstring, next);
+		d_insert(&dp, next);
+	}
+	s_append(errstring, "'\nfailed with error '");
+	s_append(errstring, cp);
+	s_append(errstring, "'.\n");
+
+	/* >> and | deserve different flavored messages */
+	switch(dp->status) {
+	case d_pipe:
+		s_append(errstring, "The mailer `");
+		s_append(errstring, s_to_c(dp->repl1));
+		sprint(smsg, "' returned error status %x.\n\n", status);
+		s_append(errstring, smsg);
+		break;
+	}
+
+	s_free(sender);
+}
+
+/*
+ *  reject delivery
+ *
+ *  returns	0	- if mail has been disposed of
+ *		other	- if mail has not been disposed
+ */
+int
+refuse(dest *list, message *mp, char *cp, int status, int outofresources)
+{
+	int rv;
+	String *errstring;
+	dest *dp;
+
+	errstring = s_new();
+	dp = d_rm(&list);
+	mkerrstring(errstring, mp, dp, list, cp, status);
+
+	/*
+	 *  log first in case we get into trouble
+	 */
+	logrefusal(dp, mp, s_to_c(errstring));
+
+	rv = 1;
+	if(rmail){
+		/* accept it or request a retry */
+		if(outofresources){
+			fprint(2, "Mail %s\n", s_to_c(errstring));
+		} else {
+			/*
+			 *  reject without generating a reply, smtpd returns
+			 *  5.0.0 status when it sees "mail refused"
+			 */
+			fprint(2, "mail refused: %s\n",  s_to_c(errstring));
+		}
+	} else {
+		/* aysnchronous delivery only happens if !rmail */
+		if(forked){
+			/*
+			 *  if spun off for asynchronous delivery, we own the mail now.
+			 *  return it or dump it on the floor.  rv really doesn't matter.
+			 */
+			rv = 0;
+			if(!outofresources && !mp->bulk)
+				replymsg(errstring, mp, dp);
+		} else {
+			fprint(2, "Mail %s\n", s_to_c(errstring));
+			savemail = 1;
+		}
+	}
+
+	s_free(errstring);
 	return rv;
 }
--- a/sys/src/cmd/upas/send/makefile
+++ /dev/null
@@ -1,46 +1,0 @@
-SSRC=	message.c main.c bind.c rewrite.c local.c dest.c process.c translate.c\
-	log.c chkfwd.c notify.c gateway.c authorize.o ../common/*.c
-SOBJ=	message.o main.o bind.o rewrite.o local.o dest.o process.o translate.o\
-	log.o chkfwd.o notify.o gateway.o authorize.o\
-	../config/config.o ../common/common.a ../libc/libc.a
-SINC=	../common/mail.h ../common/string.h ../common/aux.h
-CFLAGS=${UNIX} -g -I. -I../libc -I../common -I/usr/include ${SCFLAGS}
-LFLAGS=-g
-.c.o: ; $(CC) -c $(CFLAGS) $*.c
-LIB=/usr/lib/upas
-
-all: send
-
-send: $(SOBJ)
-	$(CC) $(SOBJ) $(LFLAGS) -o send
-
-chkfwd.o: $(SINC) message.h dest.h
-dest.o: $(SINC) dest.h
-local.o: $(SINC) dest.h process.h
-log.o: $(SINC) message.h
-main.o: $(SINC) message.h dest.h process.h
-bind.o: $(SINC) dest.h message.h
-process.o: $(SINC) process.h
-rewrite.o: $(SINC) dest.h
-translate.o: $(SINC) dest.h process.h
-message.o: $(SINC) message.h
-notify.o: $(SINC) message.h
-gateway.o: $(SINC) dest.h message.h
-
-prcan:
-	prcan $(SSRC)
-
-clean:
-	-rm -f send *.[oO] a.out core *.sL rmail
-
-cyntax:
-	cyntax $(CFLAGS) $(SSRC)
-
-install: send
-	rm -f $(LIB)/send /bin/rmail
-	cp send $(LIB)/send
-	cp send /bin/rmail
-	strip /bin/rmail
-	strip $(LIB)/send
-	chown root $(LIB)/send /bin/rmail
-	chmod 4755 $(LIB)/send /bin/rmail
--- a/sys/src/cmd/upas/send/message.c
+++ b/sys/src/cmd/upas/send/message.c
@@ -1,22 +1,24 @@
 #include "common.h"
 #include "send.h"
-
+#include <regexp.h>
 #include "../smtp/smtp.h"
 #include "../smtp/y.tab.h"
 
+enum{
+	VMLIMIT	= 64*1024,
+	MSGLIMIT	= 128*1024*1024,
+};
+
 /* global to this file */
 static Reprog *rfprog;
 static Reprog *fprog;
 
-#define VMLIMIT (64*1024)
-#define MSGLIMIT (128*1024*1024)
-
 int received;	/* from rfc822.y */
 
 static String*	getstring(Node *p);
 static String*	getaddr(Node *p);
 
-extern int
+int
 default_from(message *mp)
 {
 	char *cp, *lp;
@@ -23,27 +25,27 @@
 
 	cp = getenv("upasname");
 	lp = getlog();
-	if(lp == nil)
+	if(lp == nil){
+		free(cp);
 		return -1;
-
+	}
 	if(cp && *cp)
 		s_append(mp->sender, cp);
 	else
 		s_append(mp->sender, lp);
+	free(cp);
 	s_append(mp->date, thedate());
 	return 0;
 }
 
-extern message *
+message*
 m_new(void)
 {
 	message *mp;
 
-	mp = (message *)mallocz(sizeof(message), 1);
-	if (mp == 0) {
-		perror("message:");
-		exit(1);
-	}
+	mp = (message*)mallocz(sizeof(message), 1);
+	if (mp == 0)
+		sysfatal("m_new: %r");
 	mp->sender = s_new();
 	mp->replyaddr = s_new();
 	mp->date = s_new();
@@ -53,14 +55,11 @@
 	return mp;
 }
 
-extern void
+void
 m_free(message *mp)
 {
-	if(mp->fd >= 0){
+	if(mp->fd >= 0)
 		close(mp->fd);
-		sysremove(s_to_c(mp->tmp));
-		s_free(mp->tmp);
-	}
 	s_free(mp->sender);
 	s_free(mp->date);
 	s_free(mp->body);
@@ -68,7 +67,7 @@
 	s_free(mp->havesender);
 	s_free(mp->havereplyto);
 	s_free(mp->havesubject);
-	free((char *)mp);
+	free(mp);
 }
 
 /* read a message into a temp file, return an open fd to it in mp->fd */
@@ -75,27 +74,21 @@
 static int
 m_read_to_file(Biobuf *fp, message *mp)
 {
-	int fd;
-	int n;
-	String *file;
-	char buf[4*1024];
+	char buf[4*1024], file[Pathlen];
+	int fd, n;
 
-	file = s_new();
+	snprint(file, sizeof file, "%s/mtXXXXXX", UPASTMP);
 	/*
 	 *  create temp file to be removed on close
 	 */
-	abspath("mtXXXXXX", UPASTMP, file);
-	mktemp(s_to_c(file));
-	if((fd = syscreate(s_to_c(file), ORDWR|ORCLOSE, 0600))<0){
-		s_free(file);
+	mktemp(file);
+	if((fd = create(file, ORDWR|ORCLOSE, 0600))<0)
 		return -1;
-	}
-	mp->tmp = file;
 
 	/*
 	 *  read the rest into the temp file
 	 */
-	while((n = Bread(fp, buf, sizeof(buf))) > 0){
+	while((n = Bread(fp, buf, sizeof buf)) > 0){
 		if(write(fd, buf, n) != n){
 			close(fd);
 			return -1;
@@ -132,9 +125,9 @@
 		return s;
 
 	for(p = p->next; p; p = p->next){
-		if(p->s){
+		if(p->s)
 			s_append(s, s_to_c(p->s));
-		}else{
+		else{
 			s_putc(s, p->c);
 			s_terminate(s);
 		}
@@ -144,35 +137,6 @@
 	return s;
 }
 
-static char *fieldname[] =
-{
-[WORD-WORD]	"WORD",
-[DATE-WORD]	"DATE",
-[RESENT_DATE-WORD]	"RESENT_DATE",
-[RETURN_PATH-WORD]	"RETURN_PATH",
-[FROM-WORD]	"FROM",
-[SENDER-WORD]	"SENDER",
-[REPLY_TO-WORD]	"REPLY_TO",
-[RESENT_FROM-WORD]	"RESENT_FROM",
-[RESENT_SENDER-WORD]	"RESENT_SENDER",
-[RESENT_REPLY_TO-WORD]	"RESENT_REPLY_TO",
-[SUBJECT-WORD]	"SUBJECT",
-[TO-WORD]	"TO",
-[CC-WORD]	"CC",
-[BCC-WORD]	"BCC",
-[RESENT_TO-WORD]	"RESENT_TO",
-[RESENT_CC-WORD]	"RESENT_CC",
-[RESENT_BCC-WORD]	"RESENT_BCC",
-[REMOTE-WORD]	"REMOTE",
-[PRECEDENCE-WORD]	"PRECEDENCE",
-[MIMEVERSION-WORD]	"MIMEVERSION",
-[CONTENTTYPE-WORD]	"CONTENTTYPE",
-[MESSAGEID-WORD]	"MESSAGEID",
-[RECEIVED-WORD]	"RECEIVED",
-[MAILER-WORD]	"MAILER",
-[BADTOKEN-WORD]	"BADTOKEN",
-};
-
 /* fix 822 addresses */
 static void
 rfc822cruft(message *mp)
@@ -251,8 +215,35 @@
 	mp->body = body;
 }
 
+/* append a sub-expression match onto a String */
+static void
+append_match(Resub *subexp, String *sp, int se)
+{
+	char *cp, *ep;
+
+	cp = subexp[se].sp;
+	ep = subexp[se].ep;
+	for (; cp < ep; cp++)
+		s_putc(sp, *cp);
+	s_terminate(sp);
+}
+
+
+static char *REMFROMRE =
+	"^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)[ \t]+remote[ \t]+from[ \t]+(.*)\n$";
+static char *FROMRE =
+	"^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)\n$";
+
+enum{
+	REMSENDERMATCH 	= 1,
+	SENDERMATCH	= 1,
+	REMDATEMATCH 	= 4,
+	DATEMATCH		= 4,
+	REMSYSMATCH	= 5,
+};
+
 /* read in a message, interpret the 'From' header */
-extern message *
+message*
 m_read(Biobuf *fp, int rmail, int interactive)
 {
 	message *mp;
@@ -326,18 +317,12 @@
 		 */
 		mp->size = mp->body->ptr - mp->body->base;
 		n = s_read(fp, mp->body, VMLIMIT);
-		if(n < 0){
-			perror("m_read");
-			exit(1);
-		}
+		if(n < 0)
+			sysfatal("m_read: %r");
 		mp->size += n;
-		if(n == VMLIMIT){
-			if(m_read_to_file(fp, mp) < 0){
-				perror("m_read");
-				exit(1);
-			}
-		}
-
+		if(n == VMLIMIT)
+			if(m_read_to_file(fp, mp) < 0)
+				sysfatal("m_read: %r");
 	}
 
 	/*
@@ -352,8 +337,8 @@
 }
 
 /* return a piece of message starting at `offset' */
-extern int
-m_get(message *mp, long offset, char **pp)
+int
+m_get(message *mp, vlong offset, char **pp)
 {
 	static char buf[4*1024];
 
@@ -377,10 +362,8 @@
 	offset -= s_len(mp->body);
 	if(mp->fd < 0)
 		return -1;
-	if(seek(mp->fd, offset, 0)<0)
-		return -1;
 	*pp = buf;
-	return read(mp->fd, buf, sizeof buf);
+	return pread(mp->fd, buf, sizeof buf, offset);
 }
 
 /* output the message body without ^From escapes */
@@ -444,20 +427,22 @@
 static int
 printfrom(message *mp, Biobuf *fp)
 {
-	String *s;
+//	char *p;
 	int rv;
+	String *s;
 
 	if(!returnable(s_to_c(mp->sender)))
 		return Bprint(fp, "From: Postmaster\n");
 
-	s = username(mp->sender);
-	if(s) {
-		s_append(s, " <");
-		s_append(s, s_to_c(mp->sender));
-		s_append(s, ">");
-	} else {
+//	p = username(s_to_c(mp->sender));
+//	if(p) {
+//		s_append(s = s_new(), p);
+//		s_append(s, " <");
+//		s_append(s, s_to_c(mp->sender));
+//		s_append(s, ">");
+//	} else {
 		s = s_copy(s_to_c(mp->sender));
-	}
+//	}
 	s = unescapespecial(s);
 	rv = Bprint(fp, "From: %s\n", s_to_c(s));
 	s_free(s);
@@ -509,34 +494,30 @@
 }
 
 /* output a message */
-extern int
+int
 m_print(message *mp, Biobuf *fp, char *remote, int mbox)
 {
-	String *date, *sender;
-	char *f[6];
-	int n;
+	char *date, *d, *f[6];
+	int n, r;
+	String *sender;
 
 	sender = unescapespecial(s_clone(mp->sender));
-
-	if (remote != 0){
-		if(print_remote_header(fp,s_to_c(sender),s_to_c(mp->date),remote) < 0){
-			s_free(sender);
-			return -1;
-		}
-	} else {
-		if(print_header(fp, s_to_c(sender), s_to_c(mp->date)) < 0){
-			s_free(sender);
-			return -1;
-		}
-	}
+	date = s_to_c(mp->date);
+	if(remote)
+		r = Bprint(fp, "From %s %s remote from %s\n", s_to_c(sender), date, remote);
+	else
+		r = Bprint(fp, "From %s %s\n", s_to_c(sender), date);
 	s_free(sender);
+	if(r < 0)
+		return -1;
 	if(!rmail && !mp->havedate){
 		/* add a date: line Date: Sun, 19 Apr 1998 12:27:52 -0400 */
-		date = s_copy(s_to_c(mp->date));
-		n = getfields(s_to_c(date), f, 6, 1, " \t");
+		d = strdup(date);
+		n = getfields(date, f, 6, 1, " \t");
 		if(n == 6)
 			Bprint(fp, "Date: %s, %s %s %s %s %s\n", f[0], f[2], f[1],
 			 f[5], f[3], rewritezone(f[4]));
+		free(d);
 	}
 	if(!rmail && !mp->havemime && isutf8(mp->body))
 		printutf8mime(fp);
@@ -559,13 +540,13 @@
 				return -1;
 	}
 
-	if (!mbox)
+	if(!mbox)
 		return m_noescape(mp, fp);
 	return m_escape(mp, fp);
 }
 
 /* print just the message body */
-extern int
+int
 m_bprint(message *mp, Biobuf *fp)
 {
 	return m_noescape(mp, fp);
--- a/sys/src/cmd/upas/send/mkfile
+++ b/sys/src/cmd/upas/send/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=send\
 	filter
@@ -12,16 +13,19 @@
 	$UOFILES\
 	../smtp/rfc822.tab.$O\
 
-SMOBJ=main.$O\
+SOBJ=\
+	authorize.$O\
 	bind.$O\
-	rewrite.$O\
+	cat_mail.$O\
+	gateway.$O\
 	local.$O\
+	main.$O\
+	rewrite.$O\
 	translate.$O\
-	authorize.$O\
-	gateway.$O\
+
+FOBJ=\
 	cat_mail.$O\
 
-LIB=../common/libcommon.av\
 
 HFILES=send.h\
 	../common/common.h\
@@ -29,24 +33,22 @@
 
 LIB=../common/libcommon.a$O\
 
-BIN=/$objtype/bin/upas
 UPDATE=\
 	mkfile\
 	$HFILES\
 	${UOFILES:%.$O=%.c}\
-	${SMOBJ:%.$O=%.c}\
+	${SOBJ:%.$O=%.c}\
 	${TARG:%=%.c}\
 
 </sys/src/cmd/mkmany
 CFLAGS=$CFLAGS -I../common
 
-$O.send: $SMOBJ $OFILES
+$O.send: $SOBJ $OFILES
 	$LD $LDFLAGS -o $target $prereq $LIB
 
+$O.filter: $FOBJ
+
 message.$O:	../smtp/y.tab.h
 
 ../smtp/y.tab.h ../smtp/rfc822.tab.$O: ../smtp/rfc822.y
-	@{
-		cd ../smtp
-		mk rfc822.tab.$O
-	}
+	cd ../smtp && mk rfc822.tab.$O
--- a/sys/src/cmd/upas/send/rewrite.c
+++ b/sys/src/cmd/upas/send/rewrite.c
@@ -1,5 +1,6 @@
 #include "common.h"
 #include "send.h"
+#include <regexp.h>
 
 extern int debug;
 
@@ -32,7 +33,7 @@
  *  Get the next token from `line'.  The symbol `\l' is replaced by
  *  the name of the local system.
  */
-extern String *
+String*
 rule_parse(String *line, char *system, int *backl)
 {
 	String *token;
@@ -80,10 +81,8 @@
 	if(re == 0)
 		return 0;
 	rp = (rule *)malloc(sizeof(rule));
-	if(rp == 0) {
-		perror("getrules:");
-		exit(1);
-	}
+	if(rp == 0)
+		sysfatal("malloc: %r");
 	rp->next = 0;
 	s_tolower(re);
 	rp->matchre = s_new();
@@ -123,16 +122,15 @@
  *  rules are of the form:
  *	<reg exp> <String> <repl exp> [<repl exp>]
  */
-extern int
+int
 getrules(void)
 {
-	Biobuf	*rfp;
-	String	*line;
-	String	*type;
-	String	*file;
+	char file[Pathlen];
+	Biobuf *rfp;
+	String *line, *type;
 
-	file = abspath("rewrite", UPASLIB, (String *)0);
-	rfp = sysopen(s_to_c(file), "r", 0);
+	snprint(file, sizeof file, "%s/rewrite", UPASLIB);
+	rfp = sysopen(file, "r", 0);
 	if(rfp == 0) {
 		rulep = 0;
 		return -1;
@@ -145,7 +143,6 @@
 			getrule(s_restart(line), type, altthissys);
 	s_free(type);
 	s_free(line);
-	s_free(file);
 	sysclose(rfp);
 	return 0;
 }
@@ -168,8 +165,7 @@
 			continue;
 		memset(rp->subexp, 0, sizeof(rp->subexp));
 		if(debug)
-			fprint(2, "matching %s against %s\n", s_to_c(addrp),
-				rp->matchre->base);
+			fprint(2, "matching %s aginst %s\n", s_to_c(addrp), rp->matchre->base);
 		if(regexec(rp->program, s_to_c(addrp), rp->subexp, NSUBEXP))
 		if(s_to_c(addrp) == rp->subexp[0].sp)
 		if((s_to_c(addrp) + strlen(s_to_c(addrp))) == rp->subexp[0].ep)
@@ -183,7 +179,7 @@
  *		 0 ifaddress matched and ok to forward
  *		 1 ifaddress matched and not ok to forward
  */
-extern int
+int
 rewrite(dest *dp, message *mp)
 {
 	rule *rp;		/* rewriting rule */
@@ -279,40 +275,39 @@
 	return s_restart(stp);
 }
 
-extern void
+void
 regerror(char* s)
 {
 	fprint(2, "rewrite: %s\n", s);
 	/* make sure the message is seen locally */
-	syslog(0, "mail", "regexp error in rewrite: %s", s);
+	syslog(0, "mail", "error in rewrite: %s", s);
 }
 
-extern void
-dumprules(void)
-{
-	rule *rp;
-
-	for (rp = rulep; rp != 0; rp = rp->next) {
-		fprint(2, "'%s'", rp->matchre->base);
-		switch (rp->type) {
-		case d_pipe:
-			fprint(2, " |");
-			break;
-		case d_cat:
-			fprint(2, " >>");
-			break;
-		case d_alias:
-			fprint(2, " alias");
-			break;
-		case d_translate:
-			fprint(2, " translate");
-			break;
-		default:
-			fprint(2, " UNKNOWN");
-			break;
-		}
-		fprint(2, " '%s'", rp->repl1 ? rp->repl1->base:"...");
-		fprint(2, " '%s'\n", rp->repl2 ? rp->repl2->base:"...");
-	}
-}
-
+//void
+//dumprules(void)
+//{
+//	rule *rp;
+//
+//	for (rp = rulep; rp != 0; rp = rp->next) {
+//		fprint(2, "'%s'", rp->matchre->base);
+//		switch (rp->type) {
+//		case d_pipe:
+//			fprint(2, " |");
+//			break;
+//		case d_cat:
+//			fprint(2, " >>");
+//			break;
+//		case d_alias:
+//			fprint(2, " alias");
+//			break;
+//		case d_translate:
+//			fprint(2, " translate");
+//			break;
+//		default:
+//			fprint(2, " UNKNOWN");
+//			break;
+//		}
+//		fprint(2, " '%s'", rp->repl1 ? rp->repl1->base:"...");
+//		fprint(2, " '%s'\n", rp->repl2 ? rp->repl2->base:"...");
+//	}
+//}
--- a/sys/src/cmd/upas/send/send.h
+++ b/sys/src/cmd/upas/send/send.h
@@ -1,11 +1,5 @@
-/*
- * these limits are intended to stay within those imposed by SMTP
- * and avoid tickling bugs in other mail systems.
- * they both pertain to attempts to group recipients for the same
- * destination together in a single copy of a message.
- */
-#define MAXSAME 32	/* max recipients; was 16 */
-#define MAXSAMECHAR 1024 /* max chars in the list of recipients */
+#define MAXSAME 16
+#define MAXSAMECHAR 1024
 
 /* status of a destination*/
 typedef enum {
@@ -47,11 +41,9 @@
 	String	*replyaddr;
 	String	*date;
 	String	*body;
-	String	*tmp;		/* name of temp file */
 	String	*to;
 	int	size;
 	int	fd;		/* if >= 0, the file the message is stored in*/
-	char	haveto;
 	String	*havefrom;
 	String	*havesender;
 	String	*havereplyto;
@@ -58,32 +50,26 @@
 	char	havedate;
 	char	havemime;
 	String	*havesubject;
-	char	bulk;		/* if Precedence: Bulk in header */
 	char	rfc822headers;
-	int	received;	/* number of received lines */
 	char	*boundary;	/* bondary marker for attachments */
+	char	haveto;
+	char	bulk;		/* if Precedence: Bulk in header */
+	char	received;	/* number of received lines */
 };
 
-/*
- *  exported variables
- */
-extern int rmail;
-extern int onatty;
-extern char *thissys, *altthissys;
-extern int xflg;
-extern int nflg;
-extern int tflg;
-extern int debug;
-extern int nosummary;
+extern	int	rmail;
+extern	int	onatty;
+extern	char	*thissys;
+extern	char	*altthissys;
+extern	int	debug;
+extern	int	nosummary;
+extern	int	flagn;
+extern	int	flagx;
 
-/*
- *  exported procedures
- */
 extern void	authorize(dest*);
 extern int	cat_mail(dest*, message*);
 extern dest	*up_bind(dest*, message*, int);
 extern int	ok_to_forward(char*);
-extern int	lookup(char*, char*, Biobuf**, char*, Biobuf**);
 extern dest	*d_new(String*);
 extern void	d_free(dest*);
 extern dest	*d_rm(dest**);
@@ -101,7 +87,7 @@
 extern message	*m_new(void);
 extern void	m_free(message*);
 extern message	*m_read(Biobuf*, int, int);
-extern int	m_get(message*, long, char**);
+extern int	m_get(message*, vlong, char**);
 extern int	m_print(message*, Biobuf*, char*, int);
 extern int	m_bprint(message*, Biobuf*);
 extern String	*rule_parse(String*, char*, int*);
--- a/sys/src/cmd/upas/send/skipequiv.c
+++ b/sys/src/cmd/upas/send/skipequiv.c
@@ -3,10 +3,53 @@
 
 #define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
 
+static int
+okfile(char *s, Biobuf *b)
+{
+	char *buf, *p, *e;
+	int len, c;
+
+	len = strlen(s);
+	Bseek(b, 0, 0);
+	
+	/* one iteration per system name in the file */
+	while(buf = Brdline(b, '\n')) {
+		e = buf + Blinelen(b);
+		for(p = buf; p < e;){
+			while(isspace(*p) || *p==',')
+				p++;
+			if(strncmp(p, s, len) == 0) {
+				c = p[len];
+				if(isspace(c) || c==',')
+					return 1;
+			}
+			while(p < e && (!isspace(*p)) && *p!=',')
+				p++;
+		}
+	}
+	/* didn't find it, prohibit forwarding */
+	return 0;
+}
+
+/* return 1 if name found in file
+ *	  0 if name not found
+ *	  -1 if
+ */
+static int
+lookup(char *s, char *local, Biobuf **b)
+{
+	char file[Pathlen];
+
+	snprint(file, sizeof file, "%s/%s", UPASLIB, local);
+	if(*b != nil || (*b = sysopen(file, "r", 0)) != nil)
+		return okfile(s, *b);
+	return 0;
+}
+
 /*
  *  skip past all systems in equivlist
  */
-extern char*
+char*
 skipequiv(char *base)
 {
 	char *sp;
@@ -17,7 +60,7 @@
 		if(sp==0)
 			break;
 		*sp = '\0';
-		if(lookup(base, "equivlist", &fp, 0, 0)==1){
+		if(lookup(base, "equivlist", &fp)==1){
 			/* found or us, forget this system */
 			*sp='!';
 			base=sp+1;
@@ -28,65 +71,4 @@
 		}
 	}
 	return base;
-}
-
-static int
-okfile(char *cp, Biobuf *fp)
-{
-	char *buf;
-	int len;
-	char *bp, *ep;
-	int c;
-
-	len = strlen(cp);
-	Bseek(fp, 0, 0);
-	
-	/* one iteration per system name in the file */
-	while(buf = Brdline(fp, '\n')) {
-		ep = &buf[Blinelen(fp)];
-		for(bp=buf; bp < ep;){
-			while(isspace(*bp) || *bp==',')
-				bp++;
-			if(strncmp(bp, cp, len) == 0) {
-				c = *(bp+len);
-				if(isspace(c) || c==',')
-					return 1;
-			}
-			while(bp < ep && (!isspace(*bp)) && *bp!=',')
-				bp++;
-		}
-	}
-
-	/* didn't find it, prohibit forwarding */
-	return 0;
-}
-
-/* return 1 if name found in one of the files
- *	  0 if name not found in one of the files
- *	  -1 if neither file exists
- */
-extern int
-lookup(char *cp, char *local, Biobuf **lfpp, char *global, Biobuf **gfpp)
-{
-	static String *file = 0;
-
-	if (local) {
-		if (file == 0)
-			file = s_new();
-		abspath(local, UPASLIB, s_restart(file));
-		if (*lfpp != 0 || (*lfpp = sysopen(s_to_c(file), "r", 0)) != 0) {
-			if (okfile(cp, *lfpp))
-				return 1;
-		} else
-			local = 0;
-	}
-	if (global) {
-		abspath(global, UPASLIB, s_restart(file));
-		if (*gfpp != 0 || (*gfpp = sysopen(s_to_c(file), "r", 0)) != 0) {
-			if (okfile(cp, *gfpp))
-				return 1;
-		} else
-			global = 0;
-	}
-	return (local || global)? 0 : -1;
 }
--- a/sys/src/cmd/upas/send/translate.c
+++ b/sys/src/cmd/upas/send/translate.c
@@ -11,7 +11,7 @@
 	char *cp;
 	int n;
 
-	pp = proc_start(s_to_c(dp->repl1), (stream *)0, outstream(), outstream(), 1, 0);
+	pp = proc_start(s_to_c(dp->repl1), 0, outstream(), outstream(), 1, 0);
 	if (pp == 0) {
 		dp->status = d_resource;
 		return 0;
--- a/sys/src/cmd/upas/send/tryit
+++ /dev/null
@@ -1,29 +1,0 @@
-#!/bin/sh
-set -x
-
-> /usr/spool/mail/test.local
-echo "Forward to test.local" > /usr/spool/mail/test.forward
-echo "Pipe to cat > /tmp/test.mail" > /usr/spool/mail/test.pipe
-chmod 644 /usr/spool/mail/test.pipe
-
-mail test.local <<EOF
-mailed to test.local
-EOF
-mail test.forward <<EOF
-mailed to test.forward
-EOF
-mail test.pipe <<EOF
-mailed to test.pipe
-EOF
-mail dutoit!bowell!test.local <<EOF
-mailed to dutoit!bowell!test.local
-EOF
-
-sleep 60
-
-ls -l /usr/spool/mail/test.*
-ls -l /tmp/test.mail
-echo ">>>test.local<<<"
-cat /usr/spool/mail/test.local
-echo ">>>test.mail<<<"
-cat /tmp/test.mail
--- a/sys/src/cmd/upas/smtp/greylist.c
+++ b/sys/src/cmd/upas/smtp/greylist.c
@@ -53,7 +53,7 @@
 
 	wl = Bopen(whitelist, OREAD);
 	if (wl == nil)
-		return 1;
+		return 0;
 	while ((line = Brdline(wl, '\n')) != nil) {
 		lnlen = Blinelen(wl);
 		line[lnlen-1] = '\0';		/* clobber newline */
--- a/sys/src/cmd/upas/smtp/mkfile
+++ b/sys/src/cmd/upas/smtp/mkfile
@@ -1,8 +1,12 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG = smtpd\
 	smtp\
 
+TEST=\
+	parsetest
+
 OFILES=
 
 LIB=../common/libcommon.a$O\
@@ -12,13 +16,11 @@
 	smtpd.h\
 	smtp.h\
 
-BIN=/$objtype/bin/upas
 UPDATE=\
 	greylist.c\
 	mkfile\
 	mxdial.c\
 	rfc822.y\
-	rmtdns.c\
 	smtpd.y\
 	spam.c\
 	$HFILES\
@@ -26,24 +28,30 @@
 	${TARG:%=%.c}\
 
 </sys/src/cmd/mkmany
-CFLAGS=$CFLAGS -I../common -D'SPOOL="/mail"'
+CFLAGS=$CFLAGS -I../common
 
-$O.smtpd:	smtpd.tab.$O rmtdns.$O spam.$O rfc822.tab.$O greylist.$O
+$O.smtpd:\
+	smtpd.tab.$O\
+	spam.$O\
+	rfc822.tab.$O\
+	greylist.$O\
+
 $O.smtp:	rfc822.tab.$O mxdial.$O
 
-smtpd.tab.c:	smtpd.y
+smtpd.tab.c: smtpd.y
 	yacc -o xxx smtpd.y
 	sed 's/yy/zz/g' < xxx > $target
 	rm xxx
 
-rfc822.tab.c:	rfc822.y
+rfc822.tab.c: rfc822.y
 	yacc -d -o $target rfc822.y
 
+$O.parsetest: rfc822.tab.$O
+
+parsetest.$O: rfc822.tab.$O
+
 clean:V:
-	rm -f *.[$OS] [$OS].$TARG smtpd.tab.c rfc822.tab.c y.tab.? y.debug $TARG
+	rm -f *.[$OS] [$OS].^($TARG $TEST) smtpd.tab.c rfc822.tab.c y.tab.? y.debug $TARG
 
 ../common/libcommon.a$O:
-	@{ 	
-		cd ../common
-		mk
-	}
+	cd ../common && mk
--- a/sys/src/cmd/upas/smtp/mxdial.c
+++ b/sys/src/cmd/upas/smtp/mxdial.c
@@ -1,60 +1,96 @@
 #include "common.h"
+#include "smtp.h"
 #include <ndb.h>
-#include <smtp.h>	/* to publish dial_string_parse */
 
-enum
-{
-	Nmx=	16,
-	Maxstring=	256,
-};
+char	*bustedmxs[Maxbustedmx];
 
-typedef struct Mx	Mx;
-struct Mx
+static void
+expand(DS *ds)
 {
-	char host[256];
-	char ip[24];
-	int pref;
-};
+	char *s;
+	Ndbtuple *t;
 
-char	*bustedmxs[Maxbustedmx];
-Ndb *db;
+	s = ds->host + 1;
+	t = csipinfo(ds->netdir, "sys", sysname(), &s, 1);
+	if(t != nil){
+		strecpy(ds->expand, ds->expand+sizeof ds->expand, t->val);
+		ds->host = ds->expand;
+	}
+	ndbfree(t);
+}
 
-static int	mxlookup(DS*, char*);
-static int	mxlookup1(DS*, char*);
-static int	compar(void*, void*);
-static int	callmx(DS*, char*, char*);
-static void expand_meta(DS *ds);
+/* break up an address to its component parts */
+void
+dialstringparse(char *str, DS *ds)
+{
+	char *p, *p2;
 
-static Mx mx[Nmx];
+	strecpy(ds->buf, ds->buf + sizeof ds->buf, str);
+	p = strchr(ds->buf, '!');
+	if(p == 0) {
+		ds->netdir = 0;
+		ds->proto = "net";
+		ds->host = ds->buf;
+	} else {
+		if(*ds->buf != '/'){
+			ds->netdir = 0;
+			ds->proto = ds->buf;
+		} else {
+			for(p2 = p; *p2 != '/'; p2--)
+				;
+			*p2++ = 0;
+			ds->netdir = ds->buf;
+			ds->proto = p2;
+		}
+		*p = 0;
+		ds->host = p + 1;
+	}
+	ds->service = strchr(ds->host, '!');
+	if(ds->service)
+		*ds->service++ = 0;
+	if(*ds->host == '$')
+		expand(ds);
+}
 
-int
-mxdial(char *addr, char *ddomain, char *gdomain)
+void
+mxtabfree(Mxtab *mx)
 {
-	int fd;
-	DS ds;
+	free(mx->mx);
+	memset(mx, 0, sizeof *mx);
+}
 
-	addr = netmkaddr(addr, 0, "smtp");
-	dial_string_parse(addr, &ds);
-
-	/* try connecting to destination or any of it's mail routers */
-	fd = callmx(&ds, addr, ddomain);
-
-	/* try our mail gateway */
-	if(fd < 0 && gdomain)
-		fd = dial(netmkaddr(gdomain, 0, "smtp"), 0, 0, 0);
-
-	return fd;
+static void
+mxtabrealloc(Mxtab *mx)
+{
+	if(mx->nmx < mx->amx)
+		return;
+	if(mx->amx == 0)
+		mx->amx = 1;
+	mx->amx <<= 1;
+	mx->mx = realloc(mx->mx, sizeof mx->mx[0] * mx->amx);
+	if(mx->mx == nil)
+		sysfatal("no memory for mx");
 }
 
-static int
-busted(char *mx)
+static void
+mxtabadd(Mxtab *mx, char *host, char *ip, char *net, int pref)
 {
-	char **bmp;
+	int i;
+	Mx *x;
 
-	for (bmp = bustedmxs; *bmp != nil; bmp++)
-		if (strcmp(mx, *bmp) == 0)
-			return 1;
-	return 0;
+	mxtabrealloc(mx);
+	x = mx->mx;
+	for(i = mx->nmx; i>0 && x[i-1].pref>pref && x[i-1].netdir == net; i--)
+		x[i] = x[i-1];
+	strecpy(x[i].host, x[i].host + sizeof x[i].host, host);
+	if(ip != nil)
+		strecpy(x[i].ip, x[i].ip + sizeof x[i].ip, ip);
+	else
+		x[i].ip[0] = 0;
+	x[i].netdir = net;
+	x[i].pref = pref;
+	x[i].valid = 1;
+	mx->nmx++;
 }
 
 static int
@@ -65,7 +101,7 @@
 	return 0;
 }
 
-long
+static long
 timedwrite(int fd, void *buf, long len, long ms)
 {
 	long n, oalarm;
@@ -72,286 +108,228 @@
 
 	atnotify(timeout, 1);
 	oalarm = alarm(ms);
-	n = write(fd, buf, len);
+	n = pwrite(fd, buf, len, 0);
 	alarm(oalarm);
 	atnotify(timeout, 0);
 	return n;
 }
 
-/*
- *  take an address and return all the mx entries for it,
- *  most preferred first
- */
 static int
-callmx(DS *ds, char *dest, char *domain)
+dnslookup(Mxtab *mx, int fd, char *query, char *domain, char *net, int pref0)
 {
-	int fd, i, nmx;
-	char addr[Maxstring];
+	int n;
+	char buf[1024], *f[4];
 
-	/* get a list of mx entries */
-	nmx = mxlookup(ds, domain);
-	if(nmx < 0){
-		/* dns isn't working, don't just dial */
-		return -1;
-	}
-	if(nmx == 0){
-		if(debug)
-			fprint(2, "mxlookup returns nothing\n");
-		return dial(dest, 0, 0, 0);
-	}
-
-	/* refuse to honor loopback addresses given by dns */
-	for(i = 0; i < nmx; i++)
-		if(strcmp(mx[i].ip, "127.0.0.1") == 0){
-			if(debug)
-				fprint(2, "mxlookup returns loopback\n");
-			werrstr("illegal: domain lists 127.0.0.1 as mail server");
+	n = timedwrite(fd, query, strlen(query), 60*1000);
+	if(n < 0){
+		rerrstr(buf, sizeof buf);
+		dprint("dns: %s\n", buf);
+		if(strstr(buf, "dns failure")){
+			/* if dns fails for the mx lookup, we have to stop */
+			close(fd);
 			return -1;
 		}
+		return 0;
+	}
 
-	/* sort by preference */
-	if(nmx > 1)
-		qsort(mx, nmx, sizeof(Mx), compar);
-
-	/* dial each one in turn */
-	for(i = 0; i < nmx; i++){
-		if (busted(mx[i].host)) {
-			if (debug)
-				fprint(2, "mxdial skipping busted mx %s\n",
-					mx[i].host);
+	seek(fd, 0, 0);
+	for(;;){
+		if((n = read(fd, buf, sizeof buf - 1)) < 1)
+			break;
+		buf[n] = 0;
+	//	chat("dns: %s\n", buf);
+		n = tokenize(buf, f, nelem(f));
+		if(n < 2)
 			continue;
+		if(strcmp(f[1], "mx") == 0 && n == 4){
+			if(strchr(domain, '.') == 0)
+				strcpy(domain, f[0]);
+			mxtabadd(mx, f[3], nil, net, atoi(f[2]));
 		}
-		snprint(addr, sizeof(addr), "%s/%s!%s!%s", ds->netdir, ds->proto,
-			mx[i].host, ds->service);
-		if(debug)
-			fprint(2, "mxdial trying %s\n", addr);
-		atnotify(timeout, 1);
-		alarm(10*1000);
-		fd = dial(addr, 0, 0, 0);
-		alarm(0);
-		atnotify(timeout, 0);
-		if(fd >= 0)
-			return fd;
+		else if (strcmp(f[1], "ip") == 0 && n == 3){
+			if(strchr(domain, '.') == 0)
+				strcpy(domain, f[0]);
+			mxtabadd(mx, f[0], f[2], net, pref0);
+		}
 	}
-	return -1;
+
+	return 0;
 }
 
-/*
- *  call the dns process and have it try to resolve the mx request
- *
- *  this routine knows about the firewall and tries inside and outside
- *  dns's seperately.
- */
 static int
-mxlookup(DS *ds, char *domain)
+busted(char *mx)
 {
-	int n;
+	char **bmp;
 
-	/* just in case we find no domain name */
-	strcpy(domain, ds->host);
+	for (bmp = bustedmxs; *bmp != nil; bmp++)
+		if (strcmp(mx, *bmp) == 0)
+			return 1;
+	return 0;
+}
 
-	if(ds->netdir)
-		n = mxlookup1(ds, domain);
-	else {
-		ds->netdir = "/net";
-		n = mxlookup1(ds, domain);
-		if(n <= 0) {
-			ds->netdir = "/net.alt";
-			n = mxlookup1(ds, domain);
-		}
-	}
+static void
+complain(Mxtab *mx, char *domain)
+{
+	char buf[1024], *e, *p;
+	int i;
 
-	return n;
+	p = buf;
+	e = buf + sizeof buf;
+	for(i = 0; i < mx->nmx; i++)
+		p = seprint(p, e, "%s ", mx->mx[i].ip);
+	syslog(0, "smtpd.mx", "loopback for %s %s", domain, buf);
 }
 
 static int
-mxlookup1(DS *ds, char *domain)
+okaymx(Mxtab *mx, char *domain)
 {
-	int i, n, fd, nmx;
-	char buf[1024], dnsname[Maxstring];
-	char *fields[4];
+	int i;
+	Mx *x;
 
-	snprint(dnsname, sizeof dnsname, "%s/dns", ds->netdir);
-
-	fd = open(dnsname, ORDWR);
-	if(fd < 0)
-		return 0;
-
-	nmx = 0;
-	snprint(buf, sizeof buf, "%s mx", ds->host);
-	if(debug)
-		fprint(2, "sending %s '%s'\n", dnsname, buf);
-	/*
-	 * don't hang indefinitely in the write to /net/dns.
-	 */
-	n = timedwrite(fd, buf, strlen(buf), 60*1000);
-	if(n < 0){
-		rerrstr(buf, sizeof buf);
-		if(debug)
-			fprint(2, "dns: %s\n", buf);
-		if(strstr(buf, "dns failure")){
-			/* if dns fails for the mx lookup, we have to stop */
-			close(fd);
+	/* look for malicious dns entries; TODO use badcidr in ../spf/ to catch more than ip4 */
+	for(i = 0; i < mx->nmx; i++){
+		x = mx->mx + i;
+		if(x->valid && strcmp(x->ip, "127.0.0.1") == 0){
+			dprint("illegal: domain %s lists 127.0.0.1 as mail server", domain);
+			complain(mx, domain);
+			werrstr("illegal: domain %s lists 127.0.0.1 as mail server", domain);
 			return -1;
 		}
-	} else {
-		/*
-		 *  get any mx entries
-		 */
-		seek(fd, 0, 0);
-		while(nmx < Nmx && (n = read(fd, buf, sizeof buf-1)) > 0){
-			buf[n] = 0;
-			if(debug)
-				fprint(2, "dns mx: %s\n", buf);
-			n = getfields(buf, fields, 4, 1, " \t");
-			if(n < 4)
-				continue;
-
-			if(strchr(domain, '.') == 0)
-				strcpy(domain, fields[0]);
-
-			strncpy(mx[nmx].host, fields[3], sizeof(mx[n].host)-1);
-			mx[nmx].pref = atoi(fields[2]);
-			nmx++;
+		if(x->valid && busted(x->host)){
+			dprint("lookup: skipping busted mx %s\n", x->host);
+			x->valid = 0;
 		}
-		if(debug)
-			fprint(2, "dns mx; got %d entries\n", nmx);
 	}
+	return 0;
+}
 
-	/*
-	 * no mx record? try name itself.
-	 */
-	/*
-	 * BUG? If domain has no dots, then we used to look up ds->host
-	 * but return domain instead of ds->host in the list.  Now we return
-	 * ds->host.  What will this break?
-	 */
-	if(nmx == 0){
-		mx[0].pref = 1;
-		strncpy(mx[0].host, ds->host, sizeof(mx[0].host));
-		nmx++;
+static int
+lookup(Mxtab *mx, char *net, char *host, char *domain, char *type)
+{
+	char dns[128], buf[1024];
+	int fd, i;
+	Mx *x;
+
+	snprint(dns, sizeof dns, "%s/dns", net);
+	fd = open(dns, ORDWR);
+	if(fd == -1)
+		return -1;
+
+	snprint(buf, sizeof buf, "%s %s", host, type);
+	dprint("sending %s '%s'\n", dns, buf);
+	dnslookup(mx, fd, buf, domain, net, 10000);
+
+	for(i = 0; i < mx->nmx; i++){
+		x = mx->mx + i;
+		if(x->ip[0] != 0)
+			continue;
+		x->valid = 0;
+
+		snprint(buf, sizeof buf, "%s %s", x->host, "ip");
+		dprint("sending %s '%s'\n", dns, buf);
+		dnslookup(mx, fd, buf, domain, net, x->pref);
 	}
 
-	/*
-	 * look up all ip addresses
-	 */
-	for(i = 0; i < nmx; i++){
-		seek(fd, 0, 0);
-		snprint(buf, sizeof buf, "%s ip", mx[i].host);
-		mx[i].ip[0] = 0;
-		/*
-		 * don't hang indefinitely in the write to /net/dns.
-		 */
-		if(timedwrite(fd, buf, strlen(buf), 60*1000) < 0)
-			goto no;
-		seek(fd, 0, 0);
-		if((n = read(fd, buf, sizeof buf-1)) < 0)
-			goto no;
-		buf[n] = 0;
-		if(getfields(buf, fields, 4, 1, " \t") < 3)
-			goto no;
-		strncpy(mx[i].ip, fields[2], sizeof(mx[i].ip)-1);
-		continue;
+	close(fd);
 
-	no:
-		/* remove mx[i] and go around again */
-		nmx--;
-		mx[i] = mx[nmx];
-		i--;
+	if(strcmp(type, "mx") == 0){
+		if(okaymx(mx, domain) == -1)
+			return -1;
+		for(i = 0; i < mx->nmx; i++){
+			x = mx->mx + i;
+			dprint("mx list: %s	%d	%s\n", x->host, x->pref, x->ip);
+		}
+		dprint("\n");
 	}
-	return nmx;
+
+	return 0;
 }
 
 static int
-compar(void *a, void *b)
+lookcall(Mxtab *mx, DS *d, char *domain, char *type)
 {
-	return ((Mx*)a)->pref - ((Mx*)b)->pref;
-}
+	char buf[1024];
+	int i;
+	Mx *x;
 
-/* break up an address to its component parts */
-void
-dial_string_parse(char *str, DS *ds)
-{
-	char *p, *p2;
+	if(lookup(mx, d->netdir, d->host, domain, type) == -1){
+		for(i = 0; i < mx->nmx; i++)
+			if(mx->mx[i].netdir == d->netdir)
+				mx->mx[i].valid = 0;
+		return -1;
+	}
 
-	strncpy(ds->buf, str, sizeof(ds->buf));
-	ds->buf[sizeof(ds->buf)-1] = 0;
-
-	p = strchr(ds->buf, '!');
-	if(p == 0) {
-		ds->netdir = 0;
-		ds->proto = "net";
-		ds->host = ds->buf;
-	} else {
-		if(*ds->buf != '/'){
-			ds->netdir = 0;
-			ds->proto = ds->buf;
-		} else {
-			for(p2 = p; *p2 != '/'; p2--)
-				;
-			*p2++ = 0;
-			ds->netdir = ds->buf;
-			ds->proto = p2;
+	for(i = 0; i < mx->nmx; i++){
+		x = mx->mx + i;
+		if(x->ip[0] == 0 || x->valid == 0){
+			x->valid = 0;
+			continue;
 		}
-		*p = 0;
-		ds->host = p + 1;
+		snprint(buf, sizeof buf, "%s/%s!%s!%s", d->netdir, d->proto,
+			x->ip /*x->host*/, d->service);
+		dprint("mxdial trying %s	[%s]\n", x->host, buf);
+		atnotify(timeout, 1);
+		alarm(10*1000);
+		mx->fd = dial(buf, 0, 0, 0);
+		alarm(0);
+		atnotify(timeout, 0);
+		if(mx->fd >= 0){
+			mx->pmx = i;
+			return mx->fd;
+		}
+		dprint("	failed %r\n");
+		x->valid = 0;
 	}
-	ds->service = strchr(ds->host, '!');
-	if(ds->service)
-		*ds->service++ = 0;
-	if(*ds->host == '$')
-		expand_meta(ds);
+
+	return -1;
 }
 
-static void
-expand_meta(DS *ds)
+int
+mxdial0(char *addr, char *ddomain, char *gdomain, Mxtab *mx)
 {
-	char buf[128], cs[128], *net, *p;
-	int fd, n;
+	int nd, i, j;
+	DS *d;
+	static char *tab[] = {"mx", "ip", };
 
-	net = ds->netdir;
-	if(!net)
-		net = "/net";
-
-	if(debug)
-		fprint(2, "expanding %s!%s\n", net, ds->host);
-	snprint(cs, sizeof(cs), "%s/cs", net);
-	if((fd = open(cs, ORDWR)) == -1){
-		if(debug)
-			fprint(2, "open %s: %r\n", cs);
-		syslog(0, "smtp", "cannot open %s: %r", cs);
-		return;
+	dprint("mxdial(%s, %s, %s, mx)\n", addr, ddomain, gdomain);
+	memset(mx, 0, sizeof *mx);
+	addr = netmkaddr(addr, 0, "smtp");
+	d = mx->ds;
+	dialstringparse(addr, d + 0);
+	nd = 1;
+	if(d[0].netdir == nil){
+		d[1] = d[0];
+		d[0].netdir = "/net";
+		d[1].netdir = "/net.alt";
+		nd = 2;
 	}
 
-	snprint(buf, sizeof buf, "!ipinfo %s", ds->host+1);	// +1 to skip $
-	if(write(fd, buf, strlen(buf)) <= 0){
-		if(debug)
-			fprint(2, "write %s: %r\n", cs);
-		syslog(0, "smtp", "%s to %s - write failed: %r", buf, cs);
-		close(fd);
-		return;
-	}
+	/* search all networks for mx records; then ip records */
+	for(j = 0; j < nelem(tab); j++)
+		for(i = 0; i < nd; i++)
+			if(lookcall(mx, d + i, ddomain, tab[j]) != -1)
+				return mx->fd;
 
-	seek(fd, 0, 0);
-	if((n = read(fd, ds->expand, sizeof(ds->expand)-1)) < 0){
-		if(debug)
-			fprint(2, "read %s: %r\n", cs);
-		syslog(0, "smtp", "%s - read failed: %r", cs);
-		close(fd);
-		return;
+	/* grotty: try gateway machine by ip only (fixme: try cs lookup) */
+	if(gdomain != nil){
+		dialstringparse(netmkaddr(gdomain, 0, "smtp"), d + 0);
+		if(lookcall(mx, d + 0, gdomain, "ip") != -1)
+			return mx->fd;
 	}
-	close(fd);
 
-	ds->expand[n] = 0;
-	if((p = strchr(ds->expand, '=')) == nil){
-		if(debug)
-			fprint(2, "response %s: %s\n", cs, ds->expand);
-		syslog(0, "smtp", "%q from %s - bad response: %r", ds->expand, cs);
-		return;
-	}
-	ds->host = p+1;
+	return -1;
+}
 
-	/* take only first one returned (quasi-bug) */
-	if((p = strchr(ds->host, ' ')) != nil)
-		*p = 0;
+int
+mxdial(char *addr, char *ddomain, char *gdomain, Mx *x)
+{
+	int fd;
+	Mxtab mx;
+
+	memset(x, 0, sizeof *x);
+	fd = mxdial0(addr, ddomain, gdomain, &mx);
+	if(fd >= 0 && mx.pmx >= 0)
+		*x = mx.mx[mx.pmx];
+	mxtabfree(&mx);
+	return fd;
 }
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/parsetest.c
@@ -1,0 +1,86 @@
+#include <u.h>
+#include <libc.h>
+#include <String.h>
+#include <bio.h>
+#include "smtp.h"
+
+Biobuf o;
+
+void
+freefields(void)
+{
+	Field *f, *fn;
+	Node *n, *nn;
+
+	for(f = firstfield; f != nil; f = fn){
+		fn = f->next;
+		for(n = f->node; n != nil; n = nn){
+			nn = n->next;
+			s_free(n->s);
+			s_free(n->white);
+			free(n);
+		}
+		free(f);
+	}
+	firstfield = nil;
+}
+
+void
+printhdr(void)
+{
+	Field *f;
+	Node *n;
+
+	for(f = firstfield; f != nil; f = f->next){
+		for(n = f->node; n != nil; n = n->next){
+			if(n->s != nil)
+				Bprint(&o, "%s", s_to_c(n->s));
+			else
+				Bprint(&o, "%c", n->c);
+			if(n->white != nil)
+				Bprint(&o, "%s", s_to_c(n->white));
+		}
+		Bprint(&o, "\n");
+	}
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: parsetest file ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, fd, nbuf;
+	char *buf;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+
+	if(Binit(&o, 1, OWRITE) == -1)
+		sysfatal("Binit: %r");
+	for(i = 0; i < argc; i++){
+		fd = open(argv[i], OREAD);
+		if(fd == -1)
+			sysfatal("open: %r");
+		buf = malloc(128*1024);
+		if(buf == nil)
+			sysfatal("malloc: %r");
+		nbuf = read(fd, buf, 128*1024);
+		if(nbuf == -1)
+			sysfatal("read: %r");
+		close(fd);
+		yyinit(buf, nbuf);
+		yyparse();
+		printhdr();
+		freefields();
+		free(buf);
+		Bflush(&o);
+	}
+	exits("");
+}
--- a/sys/src/cmd/upas/smtp/rfc822.y
+++ b/sys/src/cmd/upas/smtp/rfc822.y
@@ -3,8 +3,6 @@
 #include "smtp.h"
 #include <ctype.h>
 
-#define YYMAXDEPTH	500		/* was default 150 */
-
 char	*yylp;		/* next character to be lex'd */
 int	yydone;		/* tell yylex to give up */
 char	*yybuffer;	/* first parsed character */
@@ -54,11 +52,12 @@
 msg		: fields
 		| unixfrom '\n' fields
 		;
-fields		: '\n'
+fields		: fieldlist '\n'
 			{ yydone = 1; }
-		| field '\n'
-		| field '\n' fields
 		;
+fieldlist		: field '\n'
+		| fieldlist field '\n'
+		;
 field		: dates
 			{ date = 1; }
 		| originator
@@ -304,14 +303,12 @@
  */
 yylex(void)
 {
-	String *t;
-	int quoting;
-	int escaping;
 	char *start;
+	int quoting, escaping, c, d;
+	String *t;
 	Keyword *kp;
-	int c, d;
 
-/*	print("lexing\n"); /**/
+//	print("lexing\n");
 	if(yylp >= yyend)
 		return 0;
 	if(yydone)
@@ -331,15 +328,15 @@
 		if(c == 0)
 			continue;
 
-		if(escaping) {
+		if(escaping)
 			escaping = 0;
-		} else if(quoting) {
+		else if(quoting){
 			switch(c){
 			case '\\':
 				escaping = 1;
 				break;
 			case '\n':
-				d = (*(yylp+1))&0xff;
+				d = yylp[1] & 0xff;
 				if(d != ' ' && d != '\t'){
 					quoting = 0;
 					yylp--;
@@ -350,7 +347,7 @@
 				quoting = 0;
 				break;
 			}
-		} else {
+		}else{
 			switch(c){
 			case '\\':
 				escaping = 1;
@@ -363,7 +360,7 @@
 			case '\n':
 				if(yylp == start){
 					yylp++;
-/*					print("lex(c %c)\n", c); /**/
+//					print("lex(c %c)\n", c);
 					yylval->end = yylp;
 					return yylval->c = c;
 				}
@@ -377,7 +374,7 @@
 				if(yylp == start){
 					yylp++;
 					yylval->white = yywhite();
-/*					print("lex(c %c)\n", c); /**/
+//					print("lex(c %c)\n", c);
 					yylval->end = yylp;
 					return yylval->c = c;
 				}
@@ -395,25 +392,23 @@
 	}
 out:
 	yylval->white = yywhite();
-	if(t) {
+	if(t)
 		s_terminate(t);
-	} else				/* message begins with white-space! */
+	else				/* message begins with white-space! */
 		return yylval->c = '\n';
 	yylval->s = t;
 	for(kp = key; kp->val != WORD; kp++)
-		if(cistrcmp(s_to_c(t), kp->rep)==0)
+		if(cistrcmp(s_to_c(t), kp->rep) == 0)
 			break;
-/*	print("lex(%d) %s\n", kp->val-WORD, s_to_c(t)); /**/
+//	print("lex(%d) %s\n", kp->val - WORD, s_to_c(t));
 	yylval->end = yylp;
 	return yylval->c = kp->val;
 }
 
 void
-yyerror(char *x)
+yyerror(char*)
 {
-	USED(x);
-
-	/*fprint(2, "parse err: %s\n", x);/**/
+//	fprint(2, "parse err: %s\n", x);
 }
 
 /*
@@ -423,9 +418,7 @@
 yywhite(void)
 {
 	String *w;
-	int clevel;
-	int c;
-	int escaping;
+	int clevel, c, escaping;
 
 	escaping = clevel = 0;
 	for(w = 0; yylp < yyend; yylp++){
@@ -435,15 +428,15 @@
 		if(c == 0)
 			continue;
 
-		if(escaping){
+		if(escaping)
 			escaping = 0;
-		} else if(clevel) {
+		else if(clevel){
 			switch(c){
 			case '\n':
 				/*
 				 *  look for multiline fields
 				 */
-				if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+				if(yylp[1] == ' ' || yylp[1] == '\t')
 					break;
 				else
 					goto out;
@@ -473,7 +466,7 @@
 				/*
 				 *  look for multiline fields
 				 */
-				if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+				if(yylp[1] == ' ' || yylp[1] == '\t')
 					break;
 				else
 					goto out;
@@ -533,7 +526,7 @@
 	if(p1->white){
 		if(p2->white)
 			s_append(p1->white, s_to_c(p2->white));
-	} else {
+	}else{
 		p1->white = p2->white;
 		p2->white = 0;
 	}
@@ -573,7 +566,7 @@
 
 	if(p2->s)
 		s_append(p1->s, s_to_c(p2->s));
-	else {
+	else{
 		buf[0] = p2->c;
 		buf[1] = 0;
 		s_append(p1->s, buf);
@@ -610,23 +603,6 @@
 }
 
 /*
- *  case independent string compare
- */
-int
-cistrcmp(char *s1, char *s2)
-{
-	int c1, c2;
-
-	for(; *s1; s1++, s2++){
-		c1 = isupper(*s1) ? tolower(*s1) : *s1;
-		c2 = isupper(*s2) ? tolower(*s2) : *s2;
-		if (c1 != c2)
-			return -1;
-	}
-	return *s2;
-}
-
-/*
  *  free a node
  */
 void
@@ -673,10 +649,10 @@
 	start = yybuffer;
 	if(lastfield != nil){
 		for(np = lastfield->node; np; np = np->next)
-			start = np->end+1;
+			start = np->end + 1;
 	}
 
-	end = p->start-1;
+	end = p->start - 1;
 
 	if(end <= start)
 		return;
@@ -689,7 +665,7 @@
 	np->end = end;
 	np->white = nil;
 	s = s_copy("BadHeader: ");
-	np->s = s_nappend(s, start, end-start);
+	np->s = s_nappend(s, start, end - start);
 	np->next = nil;
 
 	f = malloc(sizeof(Field));
--- a/sys/src/cmd/upas/smtp/rmtdns.c
+++ /dev/null
@@ -1,59 +1,0 @@
-#include	"common.h"
-#include	<ndb.h>
-
-int
-rmtdns(char *net, char *path)
-{
-	int fd, n, nb, r;
-	char *domain, *cp, buf[1024];
-
-	if(net == 0 || path == 0)
-		return 0;
-
-	domain = strdup(path);
-	cp = strchr(domain, '!');
-	if(cp){
-		*cp = 0;
-		n = cp-domain;
-	} else
-		n = strlen(domain);
-
-	if(*domain == '[' && domain[n-1] == ']'){ /* accept [nnn.nnn.nnn.nnn] */
-		domain[n-1] = 0;
-		r = strcmp(ipattr(domain+1), "ip");
-		domain[n-1] = ']';
-	} else
-		r = strcmp(ipattr(domain), "ip"); /* accept nnn.nnn.nnn.nnn */
-	if(r == 0){
-		free(domain);
-		return 0;
-	}
-
-	snprint(buf, sizeof buf, "%s/dns", net);
-	fd = open(buf, ORDWR);			/* look up all others */
-	if(fd < 0){				/* dns screw up - can't check */
-		free(domain);
-		return 0;
-	}
-
-	n = snprint(buf, sizeof buf, "%s all", domain);
-	free(domain);
-	seek(fd, 0, 0);
-	nb = write(fd, buf, n);
-	close(fd);
-	if(nb != n){
-		rerrstr(buf, sizeof buf);
-		if (strcmp(buf, "dns: name does not exist") == 0)
-			return -1;
-	}
-	return 0;
-}
-
-/*
-void
-main(int, char *argv[])
-{
-	print("return = %d\n", rmtdns("/net.alt", argv[1]));
-	exits(0);
-}
-*/
--- a/sys/src/cmd/upas/smtp/smtp.c
+++ b/sys/src/cmd/upas/smtp/smtp.c
@@ -5,7 +5,7 @@
 #include <libsec.h>
 #include <auth.h>
 
-static	char*	connect(char*);
+static	char*	connect(char*, Mx*);
 static	char*	wraptls(void);
 static	char*	dotls(char*);
 static	char*	doauth(char*);
@@ -15,7 +15,7 @@
 String*	convertheader(String*);
 int	dBprint(char*, ...);
 int	dBputc(int);
-char*	data(String*, Biobuf*);
+char*	data(String*, Biobuf*, Mx*);
 char*	domainify(char*, char*);
 String*	fixrouteaddr(String*, Node*, Node*);
 char*	getcrnl(String*);
@@ -27,7 +27,7 @@
 void	putcrnl(char*, int);
 void	quit(char*);
 char*	rcptto(char*);
-char*	rewritezone(char *);
+char	*rewritezone(char *);
 
 #define Retry	"Retry, Temporary Failure"
 #define Giveup	"Permanent Failure"
@@ -53,6 +53,7 @@
 char	*farend;	/* system we are trying to send to */
 char	*user;		/* user we are authenticating as, if authenticating */
 char	hostdomain[256];
+Mx	*tmmx;		/* global for timeout */
 
 Biobuf	bin;
 Biobuf	bout;
@@ -59,8 +60,27 @@
 Biobuf	berr;
 Biobuf	bfile;
 
-static int bustedmx;
+int
+Dfmt(Fmt *fmt)
+{
+	Mx *mx;
 
+	mx = va_arg(fmt->args, Mx*);
+	if(mx == nil || mx->host[0] == 0)
+		return fmtstrcpy(fmt, "");
+	else
+		return fmtprint(fmt, "(%s:%s)", mx->host, mx->ip);
+}
+#pragma	varargck	type	"D"	Mx*
+
+char*
+deliverytype(void)
+{
+	if(ping)
+		return "ping";
+	return "delivery";
+}
+
 void
 usage(void)
 {
@@ -70,10 +90,9 @@
 }
 
 int
-timeout(void *x, char *msg)
+timeout(void *, char *msg)
 {
-	USED(x);
-	syslog(0, "smtp.fail", "interrupt: %s: %s", farend,  msg);
+	syslog(0, "smtp.fail", "%s interrupt: %s: %s %D", deliverytype(), farend,  msg, tmmx);
 	if(strstr(msg, "alarm")){
 		fprint(2, "smtp timeout: connection to %s timed out\n", farend);
 		if(quitting)
@@ -81,12 +100,12 @@
 		exits(Retry);
 	}
 	if(strstr(msg, "closed pipe")){
-			/* call _exits() to prevent Bio from trying to flush closed pipe */
 		fprint(2, "smtp timeout: connection closed to %s\n", farend);
 		if(quitting){
-			syslog(0, "smtp.fail", "closed pipe to %s", farend);
+			syslog(0, "smtp.fail", "%s closed pipe to %s %D", deliverytype(), farend, tmmx);
 			_exits(quitrv);
 		}
+		/* call _exits() to prevent Bio from trying to flush closed pipe */
 		_exits(Retry);
 	}
 	return 0;
@@ -95,7 +114,7 @@
 void
 removenewline(char *p)
 {
-	int n = strlen(p)-1;
+	int n = strlen(p) - 1;
 
 	if(n < 0)
 		return;
@@ -106,18 +125,21 @@
 void
 main(int argc, char **argv)
 {
-	int i, ok, rcvrs;
-	char *addr, *rv, *trv, *host, *domain;
-	char **errs;
-	char hellodomain[256];
+	char *phase, *addr, *rv, *trv, *host, *domain;
+	char **errs, *p, *e, hellodomain[256], allrx[512];
+	int i, ok, rcvrs, bustedmx;
 	String *from, *fromm, *sender;
+	Mx mx;
 
 	alarmscale = 60*1000;	/* minutes */
 	quotefmtinstall();
+	mailfmtinstall();		/* 2047 encoding */
+	fmtinstall('D', Dfmt);
 	fmtinstall('[', encodefmt);
 	errs = malloc(argc*sizeof(char*));
 	reply = s_new();
 	host = 0;
+	bustedmx = 0;
 	ARGBEGIN{
 	case 'a':
 		tryauth = 1;
@@ -128,7 +150,7 @@
 		autistic = 1;
 		break;
 	case 'b':
-		if (bustedmx >= Maxbustedmx)
+		if(bustedmx >= Maxbustedmx)
 			sysfatal("more than %d busted mxs given", Maxbustedmx);
 		bustedmxs[bustedmx++] = EARGF(usage());
 		break;
@@ -172,7 +194,7 @@
 	/*
 	 *  get domain and add to host name
 	 */
-	if(*argv && **argv=='.') {
+	if(*argv && **argv=='.'){
 		domain = *argv;
 		argv++; argc--;
 	} else
@@ -189,6 +211,11 @@
 		usage();
 	addr = *argv++; argc--;
 	farend = addr;
+	if((rv = strrchr(addr, '!')) && rv[1] == '['){
+		syslog(0, "smtp.fail", "%s to %s failed: illegal address",
+			deliverytype(), addr);
+		exits(Giveup);
+	}
 
 	/*
 	 *  get sender's machine.
@@ -209,30 +236,40 @@
 	/*
 	 *  send the mail
 	 */
+	rcvrs = 0;
+	phase = "";
+	USED(phase);			/* just in case */
 	if(filter){
 		Binit(&bout, 1, OWRITE);
-		rv = data(from, &bfile);
-		if(rv != 0)
+		rv = data(from, &bfile, nil);
+		if(rv != 0){
+			phase = "filter";
 			goto error;
+		}
 		exits(0);
 	}
 
 	/* mxdial uses its own timeout handler */
-	if((rv = connect(addr)) != 0)
+	if((rv = connect(addr, &mx)) != 0)
 		exits(rv);
 
+	tmmx = &mx;
 	/* 10 minutes to get through the initial handshake */
 	atnotify(timeout, 1);
 	alarm(10*alarmscale);
-	if((rv = hello(hellodomain, 0)) != 0)
+	if((rv = hello(hellodomain, 0)) != 0){
+		phase = "hello";
 		goto error;
+	}
 	alarm(10*alarmscale);
-	if((rv = mailfrom(s_to_c(from))) != 0)
+	if((rv = mailfrom(s_to_c(from))) != 0){
+		phase = "mailfrom";
 		goto error;
+	}
 
 	ok = 0;
-	rcvrs = 0;
 	/* if any rcvrs are ok, we try to send the message */
+	phase = "rcptto";
 	for(i = 0; i < argc; i++){
 		if((trv = rcptto(argv[i])) != 0){
 			/* remember worst error */
@@ -248,6 +285,8 @@
 	}
 
 	/* if no ok rcvrs or worst error is retry, give up */
+	if(ok == 0 && rcvrs == 0)
+		phase = "rcptto; no addresses";
 	if(ok == 0 || rv == Retry)
 		goto error;
 
@@ -256,7 +295,7 @@
 		exits(0);
 	}
 
-	rv = data(from, &bfile);
+	rv = data(from, &bfile, &mx);
 	if(rv != 0)
 		goto error;
 	quit(0);
@@ -266,10 +305,11 @@
 	/*
 	 *  here when some but not all rcvrs failed
 	 */
-	fprint(2, "%s connect to %s:\n", thedate(), addr);
+	fprint(2, "%s connect to %s: %D %s:\n", thedate(), addr, &mx, phase);
 	for(i = 0; i < rcvrs; i++){
 		if(errs[i]){
-			syslog(0, "smtp.fail", "delivery to %s at %s failed: %s", argv[i], addr, errs[i]);
+			syslog(0, "smtp.fail", "delivery to %s at %s %D %s, failed: %s",
+				argv[i], addr, &mx, phase, errs[i]);
 			fprint(2, "  mail to %s failed: %s", argv[i], errs[i]);
 		}
 	}
@@ -279,11 +319,19 @@
 	 *  here when all rcvrs failed
 	 */
 error:
+	alarm(0);
 	removenewline(s_to_c(reply));
-	syslog(0, "smtp.fail", "%s to %s failed: %s",
-		ping ? "ping" : "delivery",
-		addr, s_to_c(reply));
-	fprint(2, "%s connect to %s:\n%s\n", thedate(), addr, s_to_c(reply));
+	if(rcvrs > 0){
+		p = allrx;
+		e = allrx + sizeof allrx;
+		seprint(p, e, "to ");
+		for(i = 0; i < rcvrs - 1; i++)
+			p = seprint(p, e, "%s,", argv[i]);
+		seprint(p, e, "%s ", argv[i]);
+	}
+	syslog(0, "smtp.fail", "%s %s at %s %D %s failed: %s",
+		deliverytype(), allrx, addr, &mx, phase, s_to_c(reply));
+	fprint(2, "%s connect to %s %D %s:\n%s\n", thedate(), addr, &mx, phase, s_to_c(reply));
 	if(!filter)
 		quit(rv);
 	exits(rv);
@@ -293,17 +341,17 @@
  *  connect to the remote host
  */
 static char *
-connect(char* net)
+connect(char* net, Mx *mx)
 {
-	char buf[Errlen];
+	char buf[ERRMAX];
 	int fd;
 
-	fd = mxdial(net, ddomain, gdomain);
+	fd = mxdial(net, ddomain, gdomain, mx);
 
 	if(fd < 0){
-		rerrstr(buf, sizeof(buf));
-		Bprint(&berr, "smtp: %s (%s)\n", buf, net);
-		syslog(0, "smtp.fail", "%s (%s)", buf, net);
+		rerrstr(buf, sizeof buf);
+		Bprint(&berr, "smtp: %s (%s) %D\n", buf, net, mx);
+		syslog(0, "smtp.fail", "%s %s (%s) %D", deliverytype(), buf, net, mx);
 		if(strstr(buf, "illegal")
 		|| strstr(buf, "unknown")
 		|| strstr(buf, "can't translate"))
@@ -320,7 +368,20 @@
 static char smtpthumbs[] =	"/sys/lib/tls/smtp";
 static char smtpexclthumbs[] =	"/sys/lib/tls/smtp.exclude";
 
-static char *
+static int
+tracetls(char *fmt, ...)
+{
+	va_list ap;
+	
+	va_start(ap, fmt);
+	Bvprint(&berr, fmt, ap);
+	Bprint(&berr, "\n");
+	Bflush(&berr);
+	va_end(ap);
+	return 0;
+}
+
+static char*
 wraptls(void)
 {
 	TLSconn *c;
@@ -335,6 +396,9 @@
 	if (c == nil)
 		return err;
 
+	if (debug)
+		c->trace = tracetls;
+
 	fd = tlsClient(Bfildes(&bout), c);
 	if (fd < 0) {
 		syslog(0, "smtp", "tlsClient to %q: %r", ddomain);
@@ -396,6 +460,36 @@
 	return(hello(me, 1));
 }
 
+static char*
+smtpcram(DS *ds)
+{
+	char *p, ch[128], usr[64], rbuf[128], ubuf[128], ebuf[192];
+	int i, n, l;
+
+	dBprint("AUTH CRAM-MD5\r\n");
+	if(getreply() != 3)
+		return Retry;
+	p = s_to_c(reply) + 4;
+	l = dec64((uchar*)ch, sizeof ch, p, strlen(p));
+	ch[l] = 0;
+	n = auth_respond(ch, l, usr, sizeof usr, rbuf, sizeof rbuf, auth_getkey,
+		user?"proto=cram role=client server=%q user=%q":"proto=cram role=client server=%q",
+		ds->host, user);
+	if(n == -1)
+		return "cannot find SMTP password";
+	if(usr[0] == 0)
+		return "cannot find user name";
+	for(i = 0; i < n; i++)
+		rbuf[i] = tolower(rbuf[i]);
+	l = snprint(ubuf, sizeof ubuf, "%s %.*s", usr, n, rbuf);
+	snprint(ebuf, sizeof ebuf, "%.*[", l, ubuf);
+
+	dBprint("%s\r\n", ch);
+	if(getreply() != 2)
+		return Retry;
+	return nil;
+}
+
 static char *
 doauth(char *methods)
 {
@@ -404,14 +498,13 @@
 	int n;
 	DS ds;
 
-	dial_string_parse(ddomain, &ds);
+	dialstringparse(ddomain, &ds);
+	if(strstr(methods, "CRAM-MD5"))
+		return smtpcram(&ds);
 
-	if(user != nil)
-		p = auth_getuserpasswd(nil,
-	  	  "proto=pass service=smtp server=%q user=%q", ds.host, user);
-	else
-		p = auth_getuserpasswd(nil,
-	  	  "proto=pass service=smtp server=%q", ds.host);
+	p = auth_getuserpasswd(nil,
+		user?"proto=pass service=smtp server=%q user=%q":"proto=pass service=smtp server=%q",
+		ds.host, user);
 	if (p == nil)
 		return Giveup;
 
@@ -455,14 +548,14 @@
 	return err;
 }
 
-char *
+char*
 hello(char *me, int encrypted)
 {
+	char *ret, *s, *t;
 	int ehlo;
 	String *r;
-	char *ret, *s, *t;
 
-	if (!encrypted) {
+	if(!encrypted){
 		if(trysecure > 1){
 			if((ret = wraptls()) != nil)
 				return ret;
@@ -474,7 +567,7 @@
 		 * answers a call.  Send a no-op in the hope of making it
 		 * talk.
 		 */
-		if (autistic) {
+		if(autistic){
 			dBprint("NOOP\r\n");
 			getreply();	/* consume the smtp greeting */
 			/* next reply will be response to noop */
@@ -495,7 +588,7 @@
 		dBprint("EHLO %s\r\n", me);
 	else
 		dBprint("HELO %s\r\n", me);
-	switch (getreply()) {
+	switch(getreply()){
 	case 2:
 		break;
 	case 5:
@@ -514,17 +607,15 @@
 	/* Invariant: every line has a newline, a result of getcrlf() */
 	for(s = s_to_c(r); (t = strchr(s, '\n')) != nil; s = t + 1){
 		*t = '\0';
-		for (t = s; *t != '\0'; t++)
-			*t = toupper(*t);
 		if(!encrypted && trysecure &&
-		    (strcmp(s, "250-STARTTLS") == 0 ||
-		     strcmp(s, "250 STARTTLS") == 0)){
+		    (cistrcmp(s, "250-STARTTLS") == 0 ||
+		     cistrcmp(s, "250 STARTTLS") == 0)){
 			s_free(r);
 			return dotls(me);
 		}
 		if(tryauth && (encrypted || insecure) &&
-		    (strncmp(s, "250 AUTH", strlen("250 AUTH")) == 0 ||
-		     strncmp(s, "250-AUTH", strlen("250 AUTH")) == 0)){
+		    (cistrncmp(s, "250 AUTH", strlen("250 AUTH")) == 0 ||
+		     cistrncmp(s, "250-AUTH", strlen("250 AUTH")) == 0)){
 			ret = doauth(s + strlen("250 AUTH "));
 			s_free(r);
 			return ret;
@@ -542,20 +633,18 @@
 {
 	if(!returnable(from))
 		dBprint("MAIL FROM:<>\r\n");
-	else
-	if(strchr(from, '@'))
+	else if(strchr(from, '@'))
 		dBprint("MAIL FROM:<%s>\r\n", from);
 	else
 		dBprint("MAIL FROM:<%s@%s>\r\n", from, hostdomain);
 	switch(getreply()){
 	case 2:
-		break;
+		return 0;
 	case 5:
 		return Giveup;
 	default:
 		return Retry;
 	}
-	return 0;
 }
 
 /*
@@ -597,13 +686,11 @@
  *  send the damn thing
  */
 char *
-data(String *from, Biobuf *b)
+data(String *from, Biobuf *b, Mx *mx)
 {
-	char *buf, *cp;
+	char *buf, *cp, errmsg[ERRMAX], id[40];
 	int i, n, nbytes, bufsize, eof, r;
 	String *fromline;
-	char errmsg[Errlen];
-	char id[40];
 
 	/*
 	 *  input the header.
@@ -623,12 +710,12 @@
 			break;
 		}
 		nbytes = Blinelen(b);
-		buf = realloc(buf, n+nbytes+1);
+		buf = realloc(buf, n + nbytes + 1);
 		if(buf == 0){
 			s_append(s_restart(reply), "out of memory");
 			return Retry;
 		}
-		strncpy(buf+n, cp, nbytes);
+		strncpy(buf + n, cp, nbytes);
 		n += nbytes;
 		if(nbytes == 1)		/* end of header */
 			break;
@@ -669,10 +756,10 @@
 
 	srand(truerand());
 	if(messageid == 0){
-		for(i=0; i<16; i++){
-			r = rand()&0xFF;
-			id[2*i] = hex[r&0xF];
-			id[2*i+1] = hex[(r>>4)&0xF];
+		for(i = 0; i < 16; i++){
+			r = rand() & 0xff;
+			id[2*i] = hex[r & 0xf];
+			id[2*i + 1] = hex[(r>>4) & 0xf];
 		}
 		id[2*i] = '\0';
 		nbytes += Bprint(&bout, "Message-ID: <%s@%s>\r\n", id, hostdomain);
@@ -680,7 +767,7 @@
 			Bprint(&berr, "Message-ID: <%s@%s>\r\n", id, hostdomain);
 	}
 
-	if(originator==0){
+	if(originator == 0){
 		nbytes += Bprint(&bout, "From: %s\r\n", s_to_c(fromline));
 		if(debug)
 			Bprint(&berr, "From: %s\r\n", s_to_c(fromline));
@@ -698,12 +785,12 @@
 				Bprint(&berr, "To: %s\r\n", s_to_c(toline));
 		}
 
-	if(date==0 && udate)
+	if(date == 0 && udate)
 		nbytes += printdate(udate);
-	if (usys)
+	if(usys)
 		uneaten = usys->end + 1;
 	nbytes += printheader();
-	if (*uneaten != '\n')
+	if(*uneaten != '\n')
 		putcrnl("\n", 1);
 
 	/*
@@ -710,8 +797,8 @@
 	 *  send body
 	 */
 
-	putcrnl(uneaten, buf+n - uneaten);
-	nbytes += buf+n - uneaten;
+	putcrnl(uneaten, buf + n - uneaten);
+	nbytes += buf + n - uneaten;
 	if(eof == 0){
 		for(;;){
 			n = Bread(b, buf, bufsize);
@@ -743,8 +830,8 @@
 		default:
 			return Retry;
 		}
-		syslog(0, "smtp", "%s sent %d bytes to %s", s_to_c(from),
-				nbytes, s_to_c(toline));/**/
+		syslog(0, "smtp", "%s sent %d bytes to %s %D", s_to_c(from),
+				nbytes, s_to_c(toline), mx);
 	}
 	return 0;
 }
@@ -806,10 +893,9 @@
 String *
 bangtoat(char *addr)
 {
-	String *buf;
-	register int i;
-	int j, d;
 	char *field[128];
+	int i, j, d;
+	String *buf;
 
 	/* parse the '!' format address */
 	buf = s_new();
@@ -819,7 +905,7 @@
 		if(addr)
 			*addr++ = 0;
 	}
-	if (i==1) {
+	if(i == 1){
 		s_append(buf, field[0]);
 		return buf;
 	}
@@ -827,8 +913,8 @@
 	/*
 	 *  count leading domain fields (non-domains don't count)
 	 */
-	for(d = 0; d<i-1; d++)
-		if(strchr(field[d], '.')==0)
+	for(d = 0; d < i - 1; d++)
+		if(strchr(field[d], '.') == 0)
 			break;
 	/*
 	 *  if there are more than 1 leading domain elements,
@@ -836,7 +922,7 @@
 	 */
 	if(d > 1){
 		addhostdom(buf, field[0]);
-		for(j=1; j<d-1; j++){
+		for(j = 1; j< d - 1; j++){
 			s_append(buf, ",");
 			s_append(buf, "@");
 			s_append(buf, field[j]);
@@ -848,7 +934,7 @@
 	 *  throw in the non-domain elements separated by '!'s
 	 */
 	s_append(buf, field[d]);
-	for(j=d+1; j<=i-1; j++) {
+	for(j = d + 1; j <= i - 1; j++){
 		s_append(buf, "!");
 		s_append(buf, field[j]);
 	}
@@ -865,6 +951,7 @@
 String*
 convertheader(String *from)
 {
+	char *s, buf[64];
 	Field *f;
 	Node *p, *lastp;
 	String *a;
@@ -875,8 +962,10 @@
 		addhostdom(from, hostdomain);
 	} else
 	if(strchr(s_to_c(from), '@') == 0){
-		a = username(from);
-		if(a) {
+		if(s = username(s_to_c(from))){
+			/* this has always been here, but username() was broken */
+			snprint(buf, sizeof buf, "%U", s);
+			s_append(a = s_new(), buf);
 			s_append(a, " <");
 			s_append(a, s_to_c(from));
 			addhostdom(a, hostdomain);
@@ -929,11 +1018,10 @@
 int
 printheader(void)
 {
+	char *cp, c[1];
 	int n, len;
 	Field *f;
 	Node *p;
-	char *cp;
-	char c[1];
 
 	n = 0;
 	for(f = firstfield; f; f = f->next){
@@ -966,10 +1054,10 @@
 char *
 domainify(char *name, char *domain)
 {
-	static String *s;
 	char *p;
+	static String *s;
 
-	if(domain==0 || strchr(name, '.')!=0)
+	if(domain == 0 || strchr(name, '.') != 0)
 		return name;
 
 	s = s_reset(s);
@@ -1008,8 +1096,7 @@
 char *
 getcrnl(String *s)
 {
-	int c;
-	int count;
+	int c, count;
 
 	count = 0;
 	for(;;){
@@ -1052,16 +1139,17 @@
 int
 printdate(Node *p)
 {
-	int n, sep = 0;
+	int n, sep;
 
 	n = dBprint("Date: %s,", s_to_c(p->s));
+	sep = 0;
 	for(p = p->next; p; p = p->next){
 		if(p->s){
-			if(sep == 0) {
+			if(sep == 0){
 				dBputc(' ');
 				n++;
 			}
-			if (p->next)
+			if(p->next)
 				n += dBprint("%s", s_to_c(p->s));
 			else
 				n += dBprint("%s", rewritezone(s_to_c(p->s)));
@@ -1079,8 +1167,8 @@
 char *
 rewritezone(char *z)
 {
-	int mindiff;
 	char s;
+	int mindiff;
 	Tm *tm;
 	static char x[7];
 
@@ -1104,22 +1192,22 @@
 /*
  *  stolen from libc/port/print.c
  */
-#define	SIZE	4096
+
 int
 dBprint(char *fmt, ...)
 {
-	char buf[SIZE], *out;
-	va_list arg;
+	char buf[4096], *out;
 	int n;
+	va_list arg;
 
 	va_start(arg, fmt);
-	out = vseprint(buf, buf+SIZE, fmt, arg);
+	out = vseprint(buf, buf + sizeof buf, fmt, arg);
 	va_end(arg);
 	if(debug){
-		Bwrite(&berr, buf, (long)(out-buf));
+		Bwrite(&berr, buf, out - buf);
 		Bflush(&berr);
 	}
-	n = Bwrite(&bout, buf, (long)(out-buf));
+	n = Bwrite(&bout, buf,out - buf);
 	Bflush(&bout);
 	return n;
 }
--- a/sys/src/cmd/upas/smtp/smtp.h
+++ b/sys/src/cmd/upas/smtp/smtp.h
@@ -24,8 +24,11 @@
 };
 
 typedef struct DS	DS;
+typedef struct Mx	Mx;
+typedef struct Mxtab	Mxtab;
+
 struct DS {
-	/* dist string */
+	/* dial string */
 	char	buf[128];
 	char	expand[128];
 	char	*netdir;
@@ -34,6 +37,25 @@
 	char	*service;
 };
 
+struct Mx
+{
+	char	*netdir;
+	char	host[256];
+	char	ip[24];
+	int	pref;
+	int	valid;
+};
+
+struct Mxtab
+{
+	DS	ds[2];
+	int	nmx;
+	int	amx;
+	int	pmx;
+	int	fd;
+	Mx	*mx;
+};
+
 extern Field	*firstfield;
 extern Field	*lastfield;
 extern Node	*usender;
@@ -51,7 +73,6 @@
 int	badfieldname(Node*);
 Node*	bang(Node*, Node*);
 Node*	colon(Node*, Node*);
-int	cistrcmp(char*, char*);
 Node*	link2(Node*, Node*);
 Node*	link3(Node*, Node*, Node*);
 void	freenode(Node*);
@@ -63,5 +84,9 @@
 String*	yywhite(void);
 Node*	whiten(Node*);
 void	yycleanup(void);
-int	mxdial(char*, char*, char*);
-void	dial_string_parse(char*, DS*);
+int	mxdial0(char*, char*, char*, Mxtab*);
+int	mxdial(char*, char*, char*, Mx*);
+void	mxtabfree(Mxtab*);
+void	dialstringparse(char*, DS*);
+
+#define dprint(...)	do if(debug)print(__VA_ARGS__); while(0)
--- a/sys/src/cmd/upas/smtp/smtpd.c
+++ b/sys/src/cmd/upas/smtp/smtpd.c
@@ -22,14 +22,15 @@
 int	rejectcount;
 int	hardreject;
 
-ulong	starttime;
-
 Biobuf	bin;
 
 int	debug;
 int	Dflag;
+int	Eflag;
+int	eflag;
 int	fflag;
 int	gflag;
+int	qflag;
 int	rflag;
 int	sflag;
 int	authenticate;
@@ -50,41 +51,28 @@
 int	rejectcheck(void);
 String*	startcmd(void);
 
-static void	logmsg(char *action);
-
 static int
-catchalarm(void *a, char *msg)
+catchalarm(void*, char *msg)
 {
-	int rv;
+	int ign;
+	static int chattycathy;
 
-	USED(a);
-
-	/* log alarms but continue */
-	if(strstr(msg, "alarm") != nil){
-		if(senders.first && senders.first->p &&
-		    rcvers.first && rcvers.first->p)
+	ign = strstr(msg, "closed pipe") != nil;
+	if(ign)
+		return 0;
+	if(chattycathy++ < 5){
+		if(senders.first && rcvers.first)
 			syslog(0, "smtpd", "note: %s->%s: %s",
 				s_to_c(senders.first->p),
 				s_to_c(rcvers.first->p), msg);
 		else
 			syslog(0, "smtpd", "note: %s", msg);
-		rv = Atnoterecog;
-	} else
-		rv = Atnoteunknown;
-	if (debug) {
-		seek(2, 0, 2);
-		fprint(2, "caught note: %s\n", msg);
 	}
-
-	/* kill the children if there are any */
-	if(pp && pp->pid > 0) {
-		syskillpg(pp->pid);
-		/* none can't syskillpg, so try a variant */
-		sleep(500);
+	if(pp){
 		syskill(pp->pid);
+	//	pp = 0;
 	}
-
-	return rv;
+	return strstr(msg, "alarm") != nil;
 }
 
 /* override string error functions to do something reasonable */
@@ -91,7 +79,7 @@
 void
 s_error(char *f, char *status)
 {
-	char errbuf[Errlen];
+	char errbuf[ERRMAX];
 
 	errbuf[0] = 0;
 	rerrstr(errbuf, sizeof(errbuf));
@@ -106,8 +94,7 @@
 static void
 usage(void)
 {
-	fprint(2,
-	  "usage: smtpd [-adDfghprs] [-c cert] [-k ip] [-m mailer] [-n net]\n");
+	fprint(2, "usage: smtpd [-DEadefghpqrs] [-c cert] [-k ip] [-m mailer] [-n net]\n");
 	exits("usage");
 }
 
@@ -121,7 +108,6 @@
 	quotefmtinstall();
 	fmtinstall('I', eipfmt);
 	fmtinstall('[', encodefmt);
-	starttime = time(0);
 	ARGBEGIN{
 	case 'a':
 		authenticate = 1;
@@ -135,6 +121,12 @@
 	case 'd':
 		debug++;
 		break;
+	case 'E':
+		Eflag = 1;
+		break;			/* if you fail extra helo checks, you must authenticate */
+	case 'e':
+		eflag = 1;		/* disable extra helo checks */
+		break;
 	case 'f':				/* disallow relaying */
 		fflag = 1;
 		break;
@@ -156,6 +148,9 @@
 	case 'p':
 		passwordinclear = 1;
 		break;
+	case 'q':
+		qflag = 1;		/* don't log invalid hello */
+		break;
 	case 'r':
 		rflag = 1;			/* verify sender's domain */
 		break;
@@ -162,11 +157,6 @@
 	case 's':				/* save blocked messages */
 		sflag = 1;
 		break;
-	case 't':
-		fprint(2, "%s: the -t option is no longer supported, see -c\n",
-			argv0);
-		tlscert = "/sys/lib/ssl/smtpd-cert.pem";
-		break;
 	default:
 		usage();
 	}ARGEND;
@@ -173,7 +163,7 @@
 
 	nci = getnetconninfo(netdir, 0);
 	if(nci == nil)
-		sysfatal("can't get remote system's address: %r");
+		sysfatal("can't get remote system's address");
 	parseip(rsysip, nci->rsys);
 
 	if(mailer == nil)
@@ -180,9 +170,9 @@
 		mailer = mailerpath("send");
 
 	if(debug){
-		snprint(buf, sizeof buf, "%s/smtpdb/%ld", UPASLOG, time(0));
 		close(2);
-		if (create(buf, OWRITE | OEXCL, 0662) >= 0) {
+		snprint(buf, sizeof(buf), "%s/smtpd.db", UPASLOG);
+		if (open(buf, OWRITE) >= 0) {
 			seek(2, 0, 2);
 			fprint(2, "%d smtpd %s\n", getpid(), thedate());
 		} else
@@ -189,8 +179,8 @@
 			debug = 0;
 	}
 	getconf();
-	if (isbadguy())
-		exits("banned");
+	if(isbadguy())
+		exits("");
 	Binit(&bin, 0, OREAD);
 
 	if (chdir(UPASLOG) < 0)
@@ -239,32 +229,20 @@
 	l->last = lp;
 }
 
-void
-stamp(void)
-{
-	if(debug) {
-		seek(2, 0, 2);
-		fprint(2, "%3lud ", time(0) - starttime);
-	}
-}
-
-#define	SIZE	4096
-
 int
 reply(char *fmt, ...)
 {
-	long n;
-	char buf[SIZE], *out;
+	char buf[4096], *out;
+	int n;
 	va_list arg;
 
 	va_start(arg, fmt);
-	out = vseprint(buf, buf+SIZE, fmt, arg);
+	out = vseprint(buf, buf + 4096, fmt, arg);
 	va_end(arg);
 
 	n = out - buf;
 	if(debug) {
 		seek(2, 0, 2);
-		stamp();
 		write(2, buf, n);
 	}
 	write(1, buf, n);
@@ -291,6 +269,36 @@
 	reply("220 %s ESMTP\r\n", dom);
 }
 
+Ndbtuple*
+rquery(char *d)
+{
+	Ndbtuple *t, *p;
+
+	t = dnsquery(nci->root, nci->rsys, "ptr");
+	for(p = t; p != nil; p = p->entry)
+		if(strcmp(p->attr, "dom") == 0
+		&& strcmp(p->val, d) == 0){
+			syslog(0, "smtpd", "ptr only from %s as %s",
+				nci->rsys, d);
+			return t;
+		}
+	ndbfree(t);
+	return nil;
+}
+
+int
+dnsexists(char *d)
+{
+	int r;
+	Ndbtuple *t;
+
+	r = -1;
+	if((t = dnsquery(nci->root, d, "any")) != nil || (t = rquery(d)) != nil)
+		r = 0;
+	ndbfree(t);
+	return r;
+}
+
 /*
  * make callers from class A networks infested by spammers
  * wait longer.
@@ -324,102 +332,153 @@
 static int
 delaysecs(void)
 {
-	if (trusted)
+	if (netaspam[rsysip[0]])
+		return 60;
+	return 15;
+}
+
+static char *badtld[] = {
+	"localdomain",
+	"localhost",
+	"local",
+	"example",
+	"invalid",
+	"lan",
+	"test",
+};
+
+static char *bad2ld[] = {
+	"example.com",
+	"example.net",
+	"example.org"
+};
+
+int
+badname(void)
+{
+	char *p;
+
+	/*
+	 * similarly, if the claimed domain is not an address-literal,
+	 * require at least one letter, which there will be in
+	 * at least the last component (e.g., .com, .net) if it's real.
+	 * this rejects non-address-literal IP addresses,
+	 * among other bogosities.
+	 */
+	for (p = him; *p; p++)
+		if(isascii(*p) && isalpha(*p))
+			return 0;
+	return -1;
+}
+
+int
+ckhello(void)
+{
+	char *ldot, *rdot;
+	int i;
+
+	/*
+	 * it is unacceptable to claim any string that doesn't look like
+	 * a domain name (e.g., has at least one dot in it), but
+	 * Microsoft mail client software gets this wrong, so let trusted
+	 * (local) clients omit the dot.
+	 */
+	rdot = strrchr(him, '.');
+	if(rdot && rdot[1] == '\0') {
+		*rdot = '\0';			/* clobber trailing dot */
+		rdot = strrchr(him, '.');	/* try again */
+	}
+	if(rdot == nil)
+		return -1;
+	/*
+	 * Reject obviously bogus domains and those reserved by RFC 2606.
+	 */
+	if(rdot == nil)
+		rdot = him;
+	else
+		rdot++;
+	for(i = 0; i < nelem(badtld); i++)
+		if(!cistrcmp(rdot, badtld[i]))
+			return -1;
+	/* check second-level RFC 2606 domains: example\.(com|net|org) */
+	if(rdot != him)
+		*--rdot = '\0';
+	ldot = strrchr(him, '.');
+	if(rdot != him)
+		*rdot = '.';
+	if(ldot == nil)
+		ldot = him;
+	else
+		ldot++;
+	for(i = 0; i < nelem(bad2ld); i++)
+		if(!cistrcmp(ldot, bad2ld[i]))
+			return -1;
+	if(badname() == -1)
+		return -1;
+	if(dnsexists(him) == -1)
+		return -1;
+	return 0;
+}
+
+int
+heloclaims(void)
+{
+	char **s;
+
+	/*
+	 * We don't care if he lies about who he is, but it is
+	 * not okay to pretend to be us.  Many viruses do this,
+	 * just parroting back what we say in the greeting.
+	 */
+	if(strcmp(nci->rsys, nci->lsys) == 0)
 		return 0;
-	if (0 && netaspam[rsysip[0]])
-		return 20;
-	return 12;
+	if(strcmp(him, dom) == 0)
+		return -1;
+	for(s = sysnames_read(); s && *s; s++)
+		if(cistrcmp(*s, him) == 0)
+			return -1;
+	if(him[0] != '[' && badname() == -1)
+		return -1;
+
+	return 0;
 }
 
 void
 hello(String *himp, int extended)
 {
-	char **mynames;
-	char *ldot, *rdot;
-	char *p;
+	int ck;
 
 	him = s_to_c(himp);
-	syslog(0, "smtpd", "%s from %s as %s", extended? "ehlo": "helo",
-		nci->rsys, him);
+	if(!qflag)
+		syslog(0, "smtpd", "%s from %s as %s", extended? "ehlo": "helo",
+			nci->rsys, him);
 	if(rejectcheck())
 		return;
 
-	if (him[0] == '[') {
-		/*
-		 * reject literal ip addresses when not trusted.
-		 */
-		if (!trusted)
-			goto Liarliar;
-		him = nci->rsys;
-	} else {
-		if (!trusted && fflag && nci && strcmp(nci->rsys, nci->lsys) != 0){
-			/*
-			 * We don't care if he lies about who he is, but it is
-			 * not okay to pretend to be us.  Many viruses do this,
-			 * just parroting back what we say in the greeting.
-			 */
-			if(cistrcmp(him, dom) == 0)
-				goto Liarliar;
-			for(mynames = sysnames_read(); mynames && *mynames; mynames++)
-				if(cistrcmp(*mynames, him) == 0)
-					goto Liarliar;
-		}
-
-		/*
-		 * require at least one letter, which there will be in
-		 * at least the last component (e.g., .com, .net) if it's real.
-		 * this rejects non-address-literal IP addresses,
-		 * among other bogosities.
-		 */
-		for (p = him; *p != '\0'; p++)
-			if (isascii(*p) && isalpha(*p))
-				break;
-		if (*p == '\0')
-			goto Liarliar;
-
-		/*
-		 * it is unacceptable to claim any string that doesn't look like
-		 * a domain name (e.g., has at least one dot in it), but
-		 * Microsoft mail client software gets this wrong, so let trusted
-		 * (local) clients omit the dot.
-		 */
-		rdot = strrchr(him, '.');
-		if (rdot && rdot[1] == '\0') {
-			*rdot = '\0';			/* clobber trailing dot */
-			rdot = strrchr(him, '.');	/* try again */
-		}
-		if (!trusted && rdot == nil)
-			goto Liarliar;
-
-		/*
-		 * Reject obviously bogus domains and those reserved by RFC 2606.
-		 */
-		if (rdot == nil)
-			rdot = him;
-		else
-			rdot++;
-		if (cistrcmp(rdot, "localdomain") == 0 ||
-		    cistrcmp(rdot, "localhost") == 0 ||
-		    cistrcmp(rdot, "example") == 0 ||
-		    cistrcmp(rdot, "invalid") == 0 ||
-		    cistrcmp(rdot, "test") == 0)
-			goto Liarliar;			/* bad top-level domain */
-		/* check second-level RFC 2606 domains: example\.(com|net|org) */
-		if (rdot != him)
-			*--rdot = '\0';
-		ldot = strrchr(him, '.');
-		if (rdot != him)
-			*rdot = '.';
-		if (ldot == nil)
-			ldot = him;
-		else
-			ldot++;
-		if (cistrcmp(ldot, "example.com") == 0 ||
-		    cistrcmp(ldot, "example.net") == 0 ||
-		    cistrcmp(ldot, "example.org") == 0)
-			goto Liarliar;
+	ck = -1;
+	if(!trusted && nci)
+	if(heloclaims() || (!eflag && (ck = ckhello())))
+	if(ck && Eflag){
+		reply("250-you lie.  authentication required.\r\n");
+		authenticate = 1;
+	}else{
+		if(Dflag)
+			sleep(delaysecs()*1000);
+		if(!qflag)
+			syslog(0, "smtpd", "Hung up on %s; claimed to be %s",
+				nci->rsys, him);
+		rejectcount++;
+		reply("554 5.7.0 Liar!\r\n");
+		exits("client pretended to be us");
+		return;
 	}
 
+	if(strchr(him, '.') == 0 && nci != nil && strchr(nci->rsys, '.') != nil)
+		him = nci->rsys;
+
+	if(qflag)
+		syslog(0, "smtpd", "%s from %s as %s", extended? "ehlo": "helo",
+			nci->rsys, him);
 	if(Dflag)
 		sleep(delaysecs()*1000);
 	reply("250%c%s you are %s\r\n", extended ? '-' : ' ', dom, him);
@@ -432,15 +491,6 @@
 		else
 			reply("250 AUTH CRAM-MD5\r\n");
 	}
-	return;
-
-Liarliar:
-	syslog(0, "smtpd", "Hung up on %s; claimed to be %s",
-		nci->rsys, him);
-	if(Dflag)
-		sleep(delaysecs()*1000);
-	reply("554 5.7.0 Liar!\r\n");
-	exits("client pretended to be us");
 }
 
 void
@@ -481,33 +531,9 @@
 	/*
 	 * see if this ip address, domain name, user name or account is blocked
 	 */
-	logged = 0;
 	filterstate = blocked(path);
-	/*
-	 * permanently reject what we can before trying smtp ping, which
-	 * often leads to merely temporary rejections.
-	 */
-	switch (filterstate){
-	case DENIED:
-		syslog(0, "smtpd", "Denied %s (%s/%s)",
-			s_to_c(path), him, nci->rsys);
-		rejectcount++;
-		logged++;
-		reply("554-5.7.1 We don't accept mail from %s.\r\n",
-			s_to_c(path));
-		reply("554 5.7.1 Contact postmaster@%s for more information.\r\n",
-			dom);
-		return;
-	case REFUSED:
-		syslog(0, "smtpd", "Refused %s (%s/%s)",
-			s_to_c(path), him, nci->rsys);
-		rejectcount++;
-		logged++;
-		reply("554 5.7.1 Sender domain must exist: %s\r\n",
-			s_to_c(path));
-		return;
-	}
 
+	logged = 0;
 	listadd(&senders, path);
 	reply("250 2.0.0 sender is %s\r\n", s_to_c(path));
 }
@@ -642,7 +668,7 @@
 	if(!recipok(s_to_c(path))){
 		rejectcount++;
 		syslog(0, "smtpd",
-		 "Disallowed %s (%s/%s) to blocked, unknown or invalid name %s",
+		 "Disallowed %s (%s/%s) to blocked name %s",
 			sender, him, nci->rsys, s_to_c(path));
 		reply("550 5.1.1 %s ... user unknown\r\n", s_to_c(path));
 		return;
@@ -660,11 +686,9 @@
 
 	/* forwarding() can modify 'path' on loopback request */
 	if(filterstate == ACCEPT && fflag && !authenticated && forwarding(path)) {
-		syslog(0, "smtpd", "Bad Forward %s (%s/%s) (%s)",
-			senders.last && senders.last->p?
-				s_to_c(senders.last->p): sender,
-			him, nci->rsys, path? s_to_c(path): rcpt);
 		rejectcount++;
+		syslog(0, "smtpd", "Bad Forward %s (%s/%s) (%s)",
+			sender, him, nci->rsys, rcpt);
 		reply("550 5.7.1 we don't relay.  send to your-path@[] for "
 			"loopback.\r\n");
 		return;
@@ -677,12 +701,6 @@
 quit(void)
 {
 	reply("221 2.0.0 Successful termination\r\n");
-	if(debug){
-		seek(2, 0, 2);
-		stamp();
-		fprint(2, "# %d sent 221 reply to QUIT %s\n",
-			getpid(), thedate());
-	}
 	close(0);
 	exits(0);
 }
@@ -710,10 +728,14 @@
 {
 	char *p, *q;
 	char *av[4];
+	static uint nverify;
 
 	if(rejectcheck())
 		return;
+	if(nverify++ >= 2)
+		sleep(1000 * (4 << nverify - 2));
 	if(shellchars(s_to_c(path))){
+		rejectcount++;
 		reply("503 5.1.3 Bad character in address %s.\r\n", s_to_c(path));
 		return;
 	}
@@ -722,7 +744,7 @@
 	av[2] = s_to_c(path);
 	av[3] = 0;
 
-	pp = noshell_proc_start(av, (stream *)0, outstream(),  (stream *)0, 1, 0);
+	pp = noshell_proc_start(av, 0, outstream(), 0, 1, 0);
 	if (pp == 0) {
 		reply("450 4.3.2 We're busy right now, try later\r\n");
 		return;
@@ -732,13 +754,13 @@
 	if(p == 0){
 		reply("550 5.1.0 String does not match anything.\r\n");
 	} else {
-		p[Blinelen(pp->std[1]->fp)-1] = 0;
+		p[Blinelen(pp->std[1]->fp) - 1] = 0;
 		if(strchr(p, ':'))
 			reply("550 5.1.0  String does not match anything.\r\n");
 		else{
 			q = strrchr(p, '!');
 			if(q)
-				p = q+1;
+				p = q + 1;
 			reply("250 2.0.0 %s <%s@%s>\r\n", s_to_c(path), p, dom);
 		}
 	}
@@ -765,6 +787,7 @@
 		}
 		switch(c){
 		case 0:
+			/* idiot html email! */
 			break;
 		case -1:
 			goto out;
@@ -774,7 +797,6 @@
 				if(debug) {
 					seek(2, 0, 2);
 					fprint(2, "%c", c);
-					stamp();
 				}
 				s_putc(s, '\n');
 				goto out;
@@ -912,6 +934,7 @@
 			dom);
 		return 0;
 	case ACCEPT:
+	case TRUSTED:
 		/*
 		 * now that all other filters have been passed,
 		 * do grey-list processing.
@@ -918,9 +941,7 @@
 		 */
 		if(gflag)
 			vfysenderhostok();
-		/* fall through */
 
-	case TRUSTED:
 		/*
 		 *  set up mail command
 		 */
@@ -962,34 +983,24 @@
  *  address@him
  */
 char*
-bprintnode(Biobuf *b, Node *p, int *cntp)
+bprintnode(Biobuf *b, Node *p, int *nbytes)
 {
-	int len;
+	int n, m;
 
-	*cntp = 0;
 	if(p->s){
-		if(p->addr && strchr(s_to_c(p->s), '@') == nil){
-			if(Bprint(b, "%s@%s", s_to_c(p->s), him) < 0)
-				return nil;
-			*cntp += s_len(p->s) + 1 + strlen(him);
-		} else {
-			len = s_len(p->s);
-			if(Bwrite(b, s_to_c(p->s), len) < 0)
-				return nil;
-			*cntp += len;
-		}
-	}else{
-		if(Bputc(b, p->c) < 0)
-			return nil;
-		++*cntp;
-	}
-	if(p->white) {
-		len = s_len(p->white);
-		if(Bwrite(b, s_to_c(p->white), len) < 0)
-			return nil;
-		*cntp += len;
-	}
-	return p->end+1;
+		if(p->addr && strchr(s_to_c(p->s), '@') == nil)
+			n = Bprint(b, "%s@%s", s_to_c(p->s), him);
+		else
+			n = Bwrite(b, s_to_c(p->s), s_len(p->s));
+	}else
+		n = Bputc(b, p->c) == -1? -1: 1;
+	m = 0;
+	if(n != -1 && p->white)
+		m = Bwrite(b, s_to_c(p->white), s_len(p->white));
+	if(n == -1 || m == -1)
+		return nil;
+	*nbytes += n + m;
+	return p->end + 1;
 }
 
 static String*
@@ -1017,8 +1028,7 @@
 	nbytes = 0;
 
 	/* warn about envelope sender */
-	if(senders.last != nil && senders.last->p != nil &&
-	    strcmp(s_to_c(senders.last->p), "/dev/null") != 0 &&
+	if(strcmp(s_to_c(senders.last->p), "/dev/null") != 0 &&
 	    masquerade(senders.last->p, nil))
 		nbytes += Bprint(pp->std[0]->fp,
 			"X-warning: suspect envelope domain\n");
@@ -1044,68 +1054,16 @@
 	return nbytes;
 }
 
-/*
- *  pipe message to mailer with the following transformations:
- *	- change \r\n into \n.
- *	- add sender's domain to any addrs with no domain
- *	- add a From: if none of From:, Sender:, or Replyto: exists
- *	- add a Received: line
- */
-int
-pipemsg(int *byteswritten)
+static int
+parseheader(String *hdr, int *nbytesp, int *status)
 {
-	int n, nbytes, sawdot, status, nonhdr, bpr;
 	char *cp;
+	int nbytes, n;
 	Field *f;
 	Link *l;
 	Node *p;
-	String *hdr, *line;
 
-	pipesig(&status);	/* set status to 1 on write to closed pipe */
-	sawdot = 0;
-	status = 0;
-
-	/*
-	 *  add a 'From ' line as envelope
-	 */
-	nbytes = 0;
-	nbytes += Bprint(pp->std[0]->fp, "From %s %s remote from \n",
-		s_to_c(senders.first->p), thedate());
-
-	/*
-	 *  add our own Received: stamp
-	 */
-	nbytes += Bprint(pp->std[0]->fp, "Received: from %s ", him);
-	if(nci->rsys)
-		nbytes += Bprint(pp->std[0]->fp, "([%s]) ", nci->rsys);
-	nbytes += Bprint(pp->std[0]->fp, "by %s; %s\n", me, thedate());
-
-	/*
-	 *  read first 16k obeying '.' escape.  we're assuming
-	 *  the header will all be there.
-	 */
-	line = s_new();
-	hdr = s_new();
-	while(sawdot == 0 && s_len(hdr) < 16*1024){
-		n = getcrnl(s_reset(line), &bin);
-
-		/* eof or error ends the message */
-		if(n <= 0)
-			break;
-
-		/* a line with only a '.' ends the message */
-		cp = s_to_c(line);
-		if(n == 2 && *cp == '.' && *(cp+1) == '\n'){
-			sawdot = 1;
-			break;
-		}
-
-		s_append(hdr, *cp == '.' ? cp+1 : cp);
-	}
-
-	/*
-	 *  parse header
-	 */
+	nbytes = *nbytesp;
 	yyinit(s_to_c(hdr), s_len(hdr));
 	yyparse();
 
@@ -1120,9 +1078,8 @@
 	 *  add an orginator and/or destination if either is missing
 	 */
 	if(originator == 0){
-		if(senders.last == nil || senders.last->p == nil)
-			nbytes += Bprint(pp->std[0]->fp, "From: /dev/null@%s\n",
-				him);
+		if(senders.last == nil)
+			nbytes += Bprint(pp->std[0]->fp, "From: /dev/null@%s\n", him);
 		else
 			nbytes += Bprint(pp->std[0]->fp, "From: %s\n",
 				s_to_c(senders.last->p));
@@ -1143,29 +1100,132 @@
 	 */
 	cp = s_to_c(hdr);
 	for(f = firstfield; cp != nil && f; f = f->next){
-		for(p = f->node; cp != 0 && p; p = p->next) {
-			bpr = 0;
-			cp = bprintnode(pp->std[0]->fp, p, &bpr);
-			nbytes += bpr;
-		}
-		if(status == 0 && Bprint(pp->std[0]->fp, "\n") < 0){
+		for(p = f->node; cp != 0 && p; p = p->next)
+			cp = bprintnode(pp->std[0]->fp, p, &nbytes);
+		if(*status == 0 && Bprint(pp->std[0]->fp, "\n") < 0){
 			piperror = "write error";
-			status = 1;
+			*status = 1;
 		}
-		nbytes++;		/* for newline */
+		nbytes++;
 	}
 	if(cp == nil){
 		piperror = "sender domain";
-		status = 1;
+		*status = 1;
 	}
-
 	/* write anything we read following the header */
-	nonhdr = s_to_c(hdr) + s_len(hdr) - cp;
-	if(status == 0 && Bwrite(pp->std[0]->fp, cp, nonhdr) < 0){
-		piperror = "write error 2";
-		status = 1;
+	if(*status == 0){
+		n = Bwrite(pp->std[0]->fp, cp, s_to_c(hdr) + s_len(hdr) - cp);
+		if(n == -1){
+			piperror = "write error 2";
+			*status = 1;
+		}
+		nbytes += n;
 	}
-	nbytes += nonhdr;
+
+	*nbytesp = nbytes;
+	return *status;
+}
+
+static int
+chkhdr(char *s, int n)
+{
+	int i;
+	Rune r;
+
+	for(i = 0; i < n; ){
+		if(!fullrune(s + i, n - i))
+			return -1;
+		i += chartorune(&r, s + i);
+		if(r == Runeerror)
+			return -1;
+	}
+	return 0;
+}
+
+static void
+fancymsg(int status)
+{
+	static char msg[2*ERRMAX], *p, *e;
+
+	if(!status)
+		return;
+
+	p = seprint(msg, msg+ERRMAX, "%s: ", piperror);
+	rerrstr(p, ERRMAX);
+	piperror = msg;
+}
+
+/*
+ *  pipe message to mailer with the following transformations:
+ *	- change \r\n into \n.
+ *	- add sender's domain to any addrs with no domain
+ *	- add a From: if none of From:, Sender:, or Replyto: exists
+ *	- add a Received: line
+ *	- elide leading dot
+ */
+int
+pipemsg(int *byteswritten)
+{
+	char *cp;
+	int n, nbytes, sawdot, status;
+	String *hdr, *line;
+
+	pipesig(&status);	/* set status to 1 on write to closed pipe */
+	sawdot = 0;
+	status = 0;
+	werrstr("");
+	piperror = nil;
+
+	/*
+	 *  add a 'From ' line as envelope and Received: stamp
+	 */
+	nbytes = 0;
+	nbytes += Bprint(pp->std[0]->fp, "From %s %s remote from \n",
+		s_to_c(senders.first->p), thedate());
+	nbytes += Bprint(pp->std[0]->fp, "Received: from %s ", him);
+	if(nci->rsys)
+		nbytes += Bprint(pp->std[0]->fp, "([%s]) ", nci->rsys);
+	nbytes += Bprint(pp->std[0]->fp, "by %s; %s\n", me, thedate());
+
+	/*
+	 *  read first 16k obeying '.' escape.  we're assuming
+	 *  the header will all be there.
+	 */
+	line = s_new();
+	hdr = s_new();
+	while(s_len(hdr) < 16*1024){
+		n = getcrnl(s_reset(line), &bin);
+
+		/* eof or error ends the message */
+		if(n <= 0){
+			piperror = "header read error";
+			status = 1;
+			break;
+		}
+
+		cp = s_to_c(line);
+		if(chkhdr(cp, s_len(line)) == -1){
+			status = 1;
+			piperror = "mail refused: illegal header chars";
+			break;
+		}
+
+		/* a line with only a '.' ends the message */
+		if(cp[0] == '.' && cp[1] == '\n'){
+			sawdot = 1;
+			break;
+		}
+		if(cp[0] == '.'){
+			cp++;
+			n--;
+		}
+		s_append(hdr, cp);
+		nbytes += n;
+		if(*cp == '\n')
+			break;
+	}
+	if(status == 0)
+		parseheader(hdr, &nbytes, &status);
 	s_free(hdr);
 
 	/*
@@ -1172,16 +1232,20 @@
 	 *  pass rest of message to mailer.  take care of '.'
 	 *  escapes.
 	 */
-	while(sawdot == 0){
+	for(;;){
 		n = getcrnl(s_reset(line), &bin);
 
 		/* eof or error ends the message */
+		if(n < 0){
+			piperror = "body read error";
+			status = 1;
+		}
 		if(n <= 0)
 			break;
 
 		/* a line with only a '.' ends the message */
 		cp = s_to_c(line);
-		if(n == 2 && *cp == '.' && *(cp+1) == '\n'){
+		if(cp[0] == '.' && cp[1] == '\n'){
 			sawdot = 1;
 			break;
 		}
@@ -1193,37 +1257,29 @@
 		if(status == 0 && Bwrite(pp->std[0]->fp, cp, n) < 0){
 			piperror = "write error 3";
 			status = 1;
+			break;
 		}
 	}
 	s_free(line);
-	if(sawdot == 0){
+	if(status == 0 && sawdot == 0){
 		/* message did not terminate normally */
-		snprint(pipbuf, sizeof pipbuf, "network eof: %r");
+		snprint(pipbuf, sizeof pipbuf, "network eof no dot: %r");
 		piperror = pipbuf;
-		if (pp->pid > 0) {
-			syskillpg(pp->pid);
-			/* none can't syskillpg, so try a variant */
-			sleep(500);
-			syskill(pp->pid);
-		}
 		status = 1;
 	}
-
 	if(status == 0 && Bflush(pp->std[0]->fp) < 0){
 		piperror = "write error 4";
 		status = 1;
 	}
-	if (debug) {
-		stamp();
-		fprint(2, "at end of message; %s .\n",
-			(sawdot? "saw": "didn't see"));
-	}
+	if(status != 0)
+		syskill(pp->pid);
 	stream_free(pp->std[0]);
 	pp->std[0] = 0;
 	*byteswritten = nbytes;
 	pipesigoff();
-	if(status && !piperror)
+	if(status && piperror == nil)
 		piperror = "write on closed pipe";
+	fancymsg(status);
 	return status;
 }
 
@@ -1233,8 +1289,8 @@
 	char *p;
 	static char buf[128];
 
-	strncpy(buf, x, sizeof(buf));
-	buf[sizeof(buf)-1] = 0;
+	strncpy(buf, x, sizeof buf);
+	buf[sizeof buf - 1] = 0;
 	p = strchr(buf, '\n');
 	if(p)
 		*p = 0;
@@ -1248,6 +1304,7 @@
 	char *cp, *senddom, *user, *who;
 	Waitmsg *w;
 
+	senddom = 0;
 	who = s_to_c(senders.first->p);
 	if(strcmp(who, "/dev/null") == 0){
 		/* /dev/null can only send to one rcpt at a time */
@@ -1256,13 +1313,14 @@
 				"recipients");
 			return -1;
 		}
-		return 0;
+		/* 4408 spf §2.2 notes that 2821 says /dev/null == postmaster@domain */
+		senddom = smprint("%s!postmaster", him);
 	}
 
 	if(access("/mail/lib/validatesender", AEXEC) < 0)
 		return 0;
-
-	senddom = strdup(who);
+	if(!senddom)
+		senddom = strdup(who);
 	if((cp = strchr(senddom, '!')) == nil){
 		werrstr("rejected: domainless sender %s", who);
 		free(senddom);
@@ -1270,6 +1328,12 @@
 	}
 	*cp++ = 0;
 	user = cp;
+	/* shellchars isn't restrictive.  should probablly disallow specialchars */
+	if(shellchars(senddom) || shellchars(user) || shellchars(him)){
+		werrstr("rejected: evil sender/domain/helo");
+		free(senddom);
+		return -1;
+	}
 
 	switch(pid = fork()){
 	case -1:
@@ -1281,7 +1345,7 @@
 		 * to allow validatesender to implement SPF eventually.
 		 */
 		execl("/mail/lib/validatesender", "validatesender",
-			"-n", nci->root, senddom, user, nil);
+			"-n", nci->root, senddom, user, nci->rsys, him, nil);
 		_exits("exec validatesender: %r");
 	default:
 		break;
@@ -1307,8 +1371,8 @@
 	 * skip over validatesender 143123132: prefix from rc.
 	 */
 	cp = strchr(w->msg, ':');
-	if(cp && *(cp+1) == ' ')
-		werrstr("%s", cp+2);
+	if(cp && cp[1] == ' ')
+		werrstr("%s", cp + 2);
 	else
 		werrstr("%s", w->msg);
 	free(w);
@@ -1315,12 +1379,34 @@
 	return -1;
 }
 
+int
+refused(char *e)
+{
+	return e && strstr(e, "mail refused") != nil;
+}
+
+/*
+ * if a message appeared on stderr, despite good status,
+ * log it.  this can happen if rewrite.in contains a bad
+ * r.e., for example.
+ */
 void
+logerrors(String *err)
+{
+	char *s;
+
+	s = s_to_c(err);
+	if(*s == 0)
+		return;
+	syslog(0, "smtpd", "%s returned good status, but said: %s",
+		s_to_c(mailer), s);
+}
+
+void
 data(void)
 {
+	char *cp, *ep, *e, buf[ERRMAX];
 	int status, nbytes;
-	char *cp, *ep;
-	char errx[ERRMAX];
 	Link *l;
 	String *cmd, *err;
 
@@ -1337,122 +1423,82 @@
 		return;
 	}
 	if(!trusted && sendermxcheck()){
-		rerrstr(errx, sizeof errx);
-		if(strncmp(errx, "rejected:", 9) == 0)
-			reply("554 5.7.1 %s\r\n", errx);
+		rerrstr(buf, sizeof buf);
+		if(strncmp(buf, "rejected:", 9) == 0)
+			reply("554 5.7.1 %s\r\n", buf);
 		else
-			reply("450 4.7.0 %s\r\n", errx);
+			reply("450 4.7.0 %s\r\n", buf);
 		for(l=rcvers.first; l; l=l->next)
 			syslog(0, "smtpd", "[%s/%s] %s -> %s sendercheck: %s",
 				him, nci->rsys, s_to_c(senders.first->p),
-				s_to_c(l->p), errx);
+				s_to_c(l->p), buf);
 		rejectcount++;
 		return;
 	}
 
+	/*
+	 *  allow 145 more minutes to move the data
+	 */
 	cmd = startcmd();
 	if(cmd == 0)
 		return;
-
 	reply("354 Input message; end with <CRLF>.<CRLF>\r\n");
-	if(debug){
-		seek(2, 0, 2);
-		stamp();
-		fprint(2, "# sent 354; accepting DATA %s\n", thedate());
-	}
-
-
-	/*
-	 *  allow 145 more minutes to move the data
-	 */
 	alarm(145*60*1000);
-
+	piperror = nil;
 	status = pipemsg(&nbytes);
-
-	/*
-	 *  read any error messages
-	 */
 	err = s_new();
-	if (debug) {
-		stamp();
-		fprint(2, "waiting for upas/send to close stderr\n");
-	}
 	while(s_read_line(pp->std[2]->fp, err))
 		;
-
 	alarm(0);
 	atnotify(catchalarm, 0);
 
-	if (debug) {
-		stamp();
-		fprint(2, "waiting for upas/send to exit\n");
-	}
 	status |= proc_wait(pp);
 	if(debug){
 		seek(2, 0, 2);
-		stamp();
-		fprint(2, "# %d upas/send status %#ux at %s\n",
-			getpid(), status, thedate());
+		fprint(2, "%d status %ux\n", getpid(), status);
 		if(*s_to_c(err))
-			fprint(2, "# %d error %s\n", getpid(), s_to_c(err));
+			fprint(2, "%d error %s\n", getpid(), s_to_c(err));
 	}
 
 	/*
 	 *  if process terminated abnormally, send back error message
 	 */
+	if(status && (refused(piperror) || refused(s_to_c(err)))){
+		filterstate = BLOCKED;
+		status = 0;
+	}
 	if(status){
-		int code;
-		char *ecode;
-
-		if(strstr(s_to_c(err), "mail refused")){
-			syslog(0, "smtpd", "++[%s/%s] %s %s refused: %s",
-				him, nci->rsys, s_to_c(senders.first->p),
-				s_to_c(cmd), firstline(s_to_c(err)));
-			code = 554;
-			ecode = "5.0.0";
-		} else {
-			syslog(0, "smtpd", "++[%s/%s] %s %s %s%s%sreturned %#q %s",
-				him, nci->rsys,
-				s_to_c(senders.first->p), s_to_c(cmd),
-				piperror? "error during pipemsg: ": "",
-				piperror? piperror: "",
-				piperror? "; ": "",
-				pp->waitmsg->msg, firstline(s_to_c(err)));
-			code = 450;
-			ecode = "4.0.0";
-		}
+		buf[0] = 0;
+		if(piperror != nil)
+			snprint(buf, sizeof buf, "pipemesg: %s; ", piperror);
+		syslog(0, "smtpd", "++[%s/%s] %s %s %sreturned %#q %s",
+			him, nci->rsys, s_to_c(senders.first->p),
+			s_to_c(cmd), buf,
+			pp->waitmsg->msg, firstline(s_to_c(err)));
 		for(cp = s_to_c(err); ep = strchr(cp, '\n'); cp = ep){
 			*ep++ = 0;
-			reply("%d-%s %s\r\n", code, ecode, cp);
+			reply("450-4.0.0 %s\r\n", cp);
 		}
-		reply("%d %s mail process terminated abnormally\r\n",
-			code, ecode);
+		reply("450 4.0.0 mail process terminated abnormally\r\n");
+		rejectcount++;
 	} else {
-		/*
-		 * if a message appeared on stderr, despite good status,
-		 * log it.  this can happen if rewrite.in contains a bad
-		 * r.e., for example.
-		 */
-		if(*s_to_c(err))
-			syslog(0, "smtpd",
-				"%s returned good status, but said: %s",
-				s_to_c(mailer), s_to_c(err));
-
-		if(filterstate == BLOCKED)
-			reply("554 5.7.1 we believe this is spam.  "
-				"we don't accept it.\r\n");
-		else if(filterstate == DELAY)
+		if(filterstate == BLOCKED){
+			e = firstline(s_to_c(err));
+			if(e[0] == 0)
+				e = piperror;
+			if(e == nil)
+				e = "we believe this is spam.";
+			syslog(0, "smtpd", "++[%s/%s] blocked: %s", him, nci->rsys, e);
+			reply("554 5.7.1 %s\r\n", e);
+			rejectcount++;
+		}else if(filterstate == DELAY){
+			logerrors(err);
 			reply("450 4.3.0 There will be a delay in delivery "
 				"of this message.\r\n");
-		else {
+		}else{
+			logerrors(err);
 			reply("250 2.5.0 sent\r\n");
 			logcall(nbytes);
-			if(debug){
-				seek(2, 0, 2);
-				stamp();
-				fprint(2, "# %d sent 250 reply %s\n",
-					getpid(), thedate());
-			}
 		}
 	}
 	proc_free(pp);
@@ -1475,6 +1521,8 @@
 int
 rejectcheck(void)
 {
+	if(rejectcount)
+		sleep(1000 * (4<<rejectcount));
 	if(rejectcount > MAXREJECTS){
 		syslog(0, "smtpd", "Rejected (%s/%s)", him, nci->rsys);
 		reply("554 5.5.0 too many errors.  transaction failed.\r\n");
@@ -1518,9 +1566,9 @@
 	 * if the string is coming from smtpd.y, it will have no nl.
 	 * if it is coming from getcrnl below, it will have an nl.
 	 */
-	if (*(s_to_c(sin)+lin-1) == '\n')
+	if (*(s_to_c(sin) + lin - 1) == '\n')
 		lin--;
-	sout = s_newalloc(lin+1);
+	sout = s_newalloc(lin + 1);
 	lout = dec64((uchar *)s_to_c(sout), lin, s_to_c(sin), lin);
 	if (lout < 0) {
 		s_free(sout);
@@ -1567,6 +1615,32 @@
 	syslog(0, "smtpd", "started TLS with %s", him);
 }
 
+int
+passauth(char *u, char *secret)
+{
+	char response[2*MD5dlen + 1];
+	uchar digest[MD5dlen];
+	int i;
+	AuthInfo *ai;
+	Chalstate *cs;
+
+	if((cs = auth_challenge("proto=cram role=server")) == nil)
+		return -1;
+	hmac_md5((uchar*)cs->chal, strlen(cs->chal),
+		(uchar*)secret, strlen(secret), digest, nil);
+	for(i = 0; i < MD5dlen; i++)
+		snprint(response + 2*i, sizeof response - 2*i, "%2.2ux", digest[i]);
+	cs->user = u;
+	cs->resp = response;
+	cs->nresp = strlen(response);
+	ai = auth_response(cs);
+	if(ai == nil)
+		return -1;
+	auth_freechal(cs);
+	auth_freeAI(ai);
+	return 0;
+}
+
 void
 auth(String *mech, String *resp)
 {
@@ -1576,19 +1650,19 @@
 	String *s_resp1_64 = nil, *s_resp2_64 = nil, *s_resp1 = nil;
 	String *s_resp2 = nil;
 
-	if (rejectcheck())
+	if(rejectcheck())
 		goto bomb_out;
 
 	syslog(0, "smtpd", "auth(%s, %s) from %s", s_to_c(mech),
 		"(protected)", him);
 
-	if (authenticated) {
+	if(authenticated) {
 	bad_sequence:
 		rejectcount++;
 		reply("503 5.5.2 Bad sequence of commands\r\n");
 		goto bomb_out;
 	}
-	if (cistrcmp(s_to_c(mech), "plain") == 0) {
+	if(cistrcmp(s_to_c(mech), "plain") == 0){
 		if (!passwordinclear) {
 			rejectcount++;
 			reply("538 5.7.1 Encryption required for requested "
@@ -1611,12 +1685,13 @@
 		memset(s_to_c(s_resp1_64), 'X', s_len(s_resp1_64));
 		user = s_to_c(s_resp1) + strlen(s_to_c(s_resp1)) + 1;
 		pass = user + strlen(user) + 1;
-		ai = auth_userpasswd(user, pass);
-		authenticated = ai != nil;
+//		ai = auth_userpasswd(user, pass);
+//		authenticated = ai != nil;
+authenticated = passauth(user, pass) != -1;
 		memset(pass, 'X', strlen(pass));
 		goto windup;
 	}
-	else if (cistrcmp(s_to_c(mech), "login") == 0) {
+	else if(cistrcmp(s_to_c(mech), "login") == 0){
 		if (!passwordinclear) {
 			rejectcount++;
 			reply("538 5.7.1 Encryption required for requested "
@@ -1628,7 +1703,8 @@
 			s_resp1_64 = s_new();
 			if (getcrnl(s_resp1_64, &bin) <= 0)
 				goto bad_sequence;
-		}
+		}else
+			s_resp1_64 = resp;
 		reply("334 UGFzc3dvcmQ6\r\n");
 		s_resp2_64 = s_new();
 		if (getcrnl(s_resp2_64, &bin) <= 0)
@@ -1656,7 +1732,7 @@
 		}
 		goto bomb_out;
 	}
-	else if (cistrcmp(s_to_c(mech), "cram-md5") == 0) {
+	else if(cistrcmp(s_to_c(mech), "cram-md5") == 0){
 		char *resp, *t;
 
 		chs = auth_challenge("proto=cram role=server");
--- a/sys/src/cmd/upas/smtp/spam.c
+++ b/sys/src/cmd/upas/smtp/spam.c
@@ -65,10 +65,10 @@
 	static char buf[32];
 	Keyword *p;
 
-	for(p=actions; p->name; p++)
+	for(p = actions; p->name; p++)
 		if(p->code == a)
 			return p->name;
-	if(a==NONE)
+	if(a == NONE)
 		return "none";
 	sprint(buf, "%d", a);
 	return buf;
@@ -94,13 +94,13 @@
 int
 istrusted(char *s)
 {
-	char buf[1024];
+	char buf[Pathlen];
 
 	if(s == nil || *s == 0)
 		return 0;
 
 	snprint(buf, sizeof buf, "/mail/ratify/trusted/%s", s);
-	return access(buf,0) >= 0;
+	return access(buf, 0) >= 0;
 }
 
 void
@@ -130,7 +130,7 @@
 		cp = getline(bp);
 		if(cp == 0)
 			break;
-		p = cp+strlen(cp)+1;
+		p = cp + strlen(cp) + 1;
 		switch(findkey(cp, options)){
 		case NORELAY:
 			if(fflag == 0 && strcmp(p, "on") == 0)
@@ -157,7 +157,7 @@
 				s = s_new();
 				s_append(s, p);
 				listadd(&ourdoms, s);
-				p += strlen(p)+1;
+				p += strlen(p) + 1;
 			}
 			break;
 		default:
@@ -178,7 +178,7 @@
 {
 	int n;
 
-	n = strlen(specuser)-1;
+	n = strlen(specuser) - 1;
 	if(specuser[n] == '*'){
 		if(n == 0)		/* match everything */
 			return 0;
@@ -195,9 +195,9 @@
 	if (*specdom == '*'){
 		if (specdom[1] == '.' && specdom[2]){
 			specdom += 2;
-			n = strlen(pathdom)-strlen(specdom);
+			n = strlen(pathdom) - strlen(specdom);
 			if(n == 0 || (n > 0 && pathdom[n-1] == '.'))
-				return strcmp(pathdom+n, specdom);
+				return strcmp(pathdom + n, specdom);
 			return n;
 		}
 	}
@@ -261,10 +261,10 @@
 			return 0;
 		n = Blinelen(bp);
 		cp[n-1] = 0;
-		if(buf == 0 || bufsize < n+1){
+		if(buf == 0 || bufsize < n + 1){
 			bufsize += 512;
-			if(bufsize < n+1)
-				bufsize = n+1;
+			if(bufsize < n + 1)
+				bufsize = n + 1;
 			buf = realloc(buf, bufsize);
 			if(buf == 0)
 				break;
@@ -328,7 +328,7 @@
 			s_append(path, "[");
 			s_append(path, nci->rsys);
 			s_append(path, "]!");
-			s_append(path, cp+3);
+			s_append(path, cp + 3);
 			s_terminate(path);
 			s_free(lpath);
 			return 0;
@@ -348,7 +348,7 @@
 	for(cp = s_to_c(lpath); *cp; cp++)		/* convert receiver lc */
 		*cp = tolower(*cp);
 
-	for(s = s_to_c(lpath); cp = strchr(s, '!'); s = cp+1){
+	for(s = s_to_c(lpath); cp = strchr(s, '!'); s = cp + 1){
 		*cp = 0;
 		if(!isourdom(s)){
 			s_free(lpath);
@@ -367,13 +367,12 @@
 	int rv = 0;
 
 	if(debug)
-		fprint(2, "masquerade(%s) ", s_to_c(path));
+		fprint(2, "masquerade(%s)\n", s_to_c(path));
 
-	if(trusted || path == nil) {
-		if(debug)
-			fprint(2, "0\n");
+	if(trusted)
 		return 0;
-	}
+	if(path == nil)
+		return 0;
 
 	lpath = s_copy(s_to_c(path));
 
@@ -388,7 +387,7 @@
 		if(isourdom(s))
 			rv = 1;
 	} else if((cp = strrchr(s, '@')) != nil){
-		if(isourdom(cp+1))
+		if(isourdom(cp + 1))
 			rv = 1;
 	} else {
 		if(isourdom(him))
@@ -396,8 +395,6 @@
 	}
 
 	s_free(lpath);
-	if (debug)
-		fprint(2, "%d\n", rv);
 	return rv;
 }
 
@@ -426,15 +423,15 @@
 		if(strchr(cp, '/') == 0){
 			m = 0xff000000;
 			p = cp;
-			for(p = strchr(p, '.'); p && p[1]; p = strchr(p+1, '.'))
-					m = (m>>8)|0xff000000;
+			for(p = strchr(p, '.'); p && p[1]; p = strchr(p + 1, '.'))
+				m = (m>>8)|0xff000000;
 
 			/* force at least a class B */
 			m |= 0xffff0000;
 		}
-		if((v4peerip&m) == a)
+		if((v4peerip & m) == a)
 			return 1;
-		cp += strlen(cp)+1;
+		cp += strlen(cp) + 1;
 	}		
 	return 0;
 }
@@ -470,10 +467,10 @@
 		cp = ctime(time(0));
 		cp[7] = 0;
 		if(cp[8] == ' ')
-			sprint(buf, "%s/queue.dump/%s%c", SPOOL, cp+4, cp[9]);
+			sprint(buf, "%s/queue.dump/%s%c", SPOOL, cp + 4, cp[9]);
 		else
-			sprint(buf, "%s/queue.dump/%s%c%c", SPOOL, cp+4, cp[8], cp[9]);
-		cp = buf+strlen(buf);
+			sprint(buf, "%s/queue.dump/%s%c%c", SPOOL, cp + 4, cp[8], cp[9]);
+		cp = buf + strlen(buf);
 		if(access(buf, 0) < 0 && sysmkdir(buf, 0777) < 0)
 			return "/dev/null";
 		h = 0;
@@ -484,7 +481,7 @@
 			sprint(cp, "/%lud", h);
 			if(access(buf, 0) >= 0)
 				continue;
-			fd = syscreate(buf, ORDWR, 0666);
+			fd = create(buf, ORDWR, 0666);
 			if(fd >= 0){
 				if(debug)
 					fprint(2, "saving in %s\n", buf);
@@ -586,7 +583,7 @@
 	rv = 0;
 	f = smprint("/mail/box/%s/nospamfiltering", p);
 	if(f != nil){
-		rv = access(f, 0)==0;
+		rv = access(f, 0) == 0;
 		free(f);
 	}
 
--- /dev/null
+++ b/sys/src/cmd/upas/spf/dns.c
@@ -1,0 +1,81 @@
+#include "spf.h"
+
+extern char	dflag;
+extern char	vflag;
+extern char	*netroot;
+
+static int
+timeout(void*, char *msg)
+{
+	if(strstr(msg, "alarm")){
+		fprint(2, "deferred: dns timeout");
+		exits("deferred: dns timeout");
+	}
+	return 0;
+}
+
+static Ndbtuple*
+tdnsquery(char *r, char *s, char *v)
+{
+	long a;
+	Ndbtuple *t;
+
+	atnotify(timeout, 1);
+	a = alarm(15*1000);
+	t = dnsquery(r, s, v);
+	alarm(a);
+	atnotify(timeout, 0);
+	return t;
+}
+
+Ndbtuple*
+vdnsquery(char *s, char *v, int recur)
+{
+	Ndbtuple *n, *t;
+	static int nquery;
+
+	/* conflicts with standard: must limit to 10 and -> fail */
+	if(recur > 5 || ++nquery == 25){
+		fprint(2, "dns query limited %d %d\n", recur, nquery);
+		return 0;
+	}
+	if(dflag)
+		fprint(2, "dnsquery(%s, %s, %s) ->\n", netroot, s, v);
+	t = tdnsquery(netroot, s, v);
+	if(dflag)
+		for(n = t; n; n = n->entry)
+			fprint(2, "\t%s\t%s\n", n->attr, n->val);
+	return t;
+}
+
+void
+dnreverse(char *s, int l, char *d)
+{
+	char *p, *e, buf[100], *f[15];
+	int i, n;
+
+	n = getfields(d, f, nelem(f), 0, ".");
+	p = e = buf;
+	if(l < sizeof buf)
+		e += l;
+	else
+		e += sizeof buf;
+	for(i = 1; i <= n; i++)
+		p = seprint(p, e, "%s.", f[n-i]);
+	if(p > buf)
+		p = seprint(p-1, e, ".in-addr.arpa");
+	memmove(s, buf, p-buf+1);
+}
+
+int
+dncontains(char *d, char *s)
+{
+loop:
+	if(!strcmp(d, s))
+		return 1;
+	if(!(s = strchr(s, '.')))
+		return 0;
+	s++;
+	goto loop;	
+}
+
--- /dev/null
+++ b/sys/src/cmd/upas/spf/macro.c
@@ -1,0 +1,304 @@
+#include "spf.h"
+
+#define mrprint(...)	snprint(m->mreg, sizeof m->mreg, __VA_ARGS__)
+
+typedef struct Mfmt Mfmt;
+typedef struct Macro Macro;
+
+struct Mfmt{
+	char	buf[0xff];
+	char	*p;
+	char	*e;
+
+	char	mreg[0xff];
+	int	f1;
+	int	f2;
+	int	f3;
+
+	char	*sender;
+	char	*domain;
+	char	*ip;
+	char	*helo;
+	uchar	ipa[IPaddrlen];
+};
+
+struct Macro{
+	char	c;
+	void	(*f)(Mfmt*);
+};
+
+static void
+ms(Mfmt *m)
+{
+	mrprint("%s", m->sender);
+}
+
+static void
+ml(Mfmt *m)
+{
+	char *p;
+
+	mrprint("%s", m->sender);
+	if(p = strchr(m->mreg, '@'))
+		*p = 0;
+}
+
+static void
+mo(Mfmt *m)
+{
+	mrprint("%s", m->domain);
+}
+
+static void
+md(Mfmt *m)
+{
+	mrprint("%s", m->domain);
+}
+
+static void
+mi(Mfmt *m)
+{
+	uint i, c;
+
+	if(isv4(m->ipa))
+		mrprint("%s", m->ip);
+	else{
+		for(i = 0; i < 32; i++){
+			c = m->ipa[i / 2];
+			if((i & 1) == 0)
+				c >>= 4;
+			sprint(m->mreg+2*i, "%ux.", c & 0xf);
+		}
+		m->mreg[2*32 - 1] = 0;
+	}
+}
+
+static int
+maquery(Mfmt *m, char *d, char *match, int recur)
+{
+	int r;
+	Ndbtuple *t, *n;
+
+	r = 0;
+	t = vdnsquery(d, "any", recur);
+	for(n = t; n; n = n->entry)
+		if(!strcmp(n->attr, "ip") || !strcmp(n->attr, "ipv6")){
+			if(!strcmp(n->val, match)){
+				r = 1;
+				break;
+			}
+		}else if(!strcmp(n->attr, "cname"))
+			maquery(m, d, match, recur+1);
+	ndbfree(t);
+	return r;
+}
+
+static int
+lrcmp(char *a, char *b)
+{
+	return strlen(b) - strlen(a);
+}
+
+static void
+mptrquery(Mfmt *m, char *d, int recur)
+{
+	char *s, buf[64], *a, *list[11];
+	int nlist, i;
+	Ndbtuple *t, *n;
+
+	nlist = 0;
+	dnreverse(buf, sizeof buf, s = strdup(m->ip));
+	t = vdnsquery(buf, "ptr", recur);
+	for(n = t; n; n = n->entry){
+		if(!strcmp(n->attr, "dom") || !strcmp(n->attr, "cname"))
+		if(dncontains(n->val, d) && maquery(m, n->val, m->ip, recur+1))
+			list[nlist++] = strdup(n->val);
+	}
+	ndbfree(t);
+	free(s);
+	qsort(list, nlist, sizeof *list, (int(*)(void*,void*))lrcmp);
+	a = "unknown";
+	for(i = 0; i < nlist; i++)
+		if(!strcmp(list[i], d)){
+			a = list[i];
+			break;
+		}else if(dncontains(list[i], d))
+			a = list[i];
+	mrprint("%s", a);
+	for(i = 0; i < nlist; i++)
+		free(list[i]);
+}
+
+static void
+mp(Mfmt *m)
+{
+	/*
+	 * we're supposed to do a reverse lookup on the ip & compare.
+	 * this is a very bad idea.
+	 */
+//	mrprint("unknown);	/* simulate dns failure */
+	mptrquery(m, m->domain, 0);
+}
+
+static void
+mv(Mfmt *m)
+{
+	if(isv4(m->ipa))
+		mrprint("in-addr");
+	else
+		mrprint("ip6");
+}
+
+static void
+mh(Mfmt *m)
+{
+	mrprint("%s", m->helo);
+}
+
+static Macro tab[] = {
+'s',	ms,	/* sender */
+'l',	ml,	/* local part of sender */
+'o',	mo,	/* domain of sender */
+'d',	md,	/* domain */
+'i',	mi,	/* ip */
+'p',	mp,	/* validated domain name of ip */
+'v',	mv,	/* "in-addr" if ipv4, or "ip6" if ipv6 */
+'h',	mh,	/* helo/ehol domain */
+};
+
+static void
+reverse(Mfmt *m)
+{
+	char *p, *e, buf[100], *f[32], sep[2];
+	int i, n;
+
+	sep[0] = m->f2;
+	sep[1] = 0;
+	n = getfields(m->mreg, f, nelem(f), 0, sep);
+	p = e = buf;
+	e += sizeof buf-1;
+	for(i = 0; i < n; i++)
+		p = seprint(p, e, "%s.", f[n-i-1]);
+	if(p > buf)
+		p--;
+	*p = 0;
+	memmove(m->mreg, buf, p-buf+1);
+	m->f2 = '.';
+}
+
+static void
+chop(Mfmt *m)
+{
+	char *p, *e, buf[100], *f[32], sep[2];
+	int i, n;
+
+	sep[0] = m->f2;
+	sep[1] = 0;
+	n = getfields(m->mreg, f, nelem(f), 0, sep);
+	p = e = buf;
+	e += sizeof buf-1;
+	if(m->f1 == 0)
+		i = 0;
+	else
+		i = n-m->f1;
+	if(i < 0)
+		i = 0;
+	for(; i < n; i++)
+		p = seprint(p, e, "%s.", f[i]);
+	if(p > buf)
+		p--;
+	*p = 0;
+	memmove(m->mreg, buf, p-buf+1);
+	m->f2 = '.';
+}
+
+static void
+mfmtinit(Mfmt *m, char *s, char *d, char *h, char *i)
+{
+	memset(m, 0, sizeof *m);
+	m->p = m->buf;
+	m->e = m->p + sizeof m->buf-1;
+	m->sender = s? s: "Unsets";
+	m->domain = d? d: "Unsetd";
+	m->helo = h? h: "Unseth";
+	m->ip = i? i: "127.0.0.2";
+	parseip(m->ipa, m->ip);
+}
+
+/* url escaping? rfc3986 */
+static void
+mputc(Mfmt *m, int c)
+{
+	if(m->p < m->e)
+		*m->p++ = c;
+}
+
+static void
+mputs(Mfmt *m, char *s)
+{
+	int c;
+
+	while(c = *s++)
+		mputc(m, c);
+}
+
+char*
+macro(char *f, char *sender, char *dom, char *hdom, char *ip)
+{
+	char *p;
+	int i, c;
+	Mfmt m;
+
+	mfmtinit(&m, sender, dom, hdom, ip);
+	while(*f){
+		while((c = *f++) && c != '%')
+			mputc(&m, c);
+		if(c == 0)
+			break;
+		switch(*f++){
+		case '%':
+			mputc(&m, '%');
+			break;
+		case '-':
+			mputs(&m, "%20");
+			break;
+		case '_':
+			mputc(&m, ' ');
+			break;
+		case '{':
+			m.f1 = 0;
+			m.f2 = '.';
+			m.f3 = 0;
+			c = *f++;
+			if(c >= 'A' && c <= 'Z')
+				c += 0x20;
+			for(i = 0; i < nelem(tab); i++)
+				if(tab[i].c == c)
+					break;
+			if(i == nelem(tab))
+				return 0;
+			for(c = *f++; c >= '0' && c <= '9'; c = *f++)
+				m.f1 = m.f1*10 + c-'0';
+			if(c == 'R' || c == 'r'){
+				m.f3 = 'r';
+				c = *f++;
+			}
+			for(; p = strchr(".-+,_=", c); c = *f++)
+				m.f2 = *p;
+			if(c == '}'){
+				tab[i].f(&m);
+				if(m.f1 || m.f2 != '.')
+					chop(&m);
+				if(m.f3 == 'r')
+					reverse(&m);
+				mputs(&m, m.mreg);
+				m.mreg[0] = 0;
+				break;
+			}
+		default:
+			return 0;
+		}
+	}
+	mputc(&m, 0);
+	return strdup(m.buf);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/spf/mkfile
@@ -1,0 +1,14 @@
+</$objtype/mkfile
+<../mkupas
+
+TARG=spf
+
+OFILES=\
+	dns.$O\
+	macro.$O\
+	spf.$O\
+
+</sys/src/cmd/mkone
+
+mtest: dns.$O macro.$O mtest.$O
+	$LD $LDFLAGS -o $target $prereq
--- /dev/null
+++ b/sys/src/cmd/upas/spf/mtest.c
@@ -1,0 +1,39 @@
+#include "spf.h"
+
+char	dflag;
+char	vflag;
+char	*netroot = "/net";
+
+void
+usage(void)
+{
+	fprint(2, "usage: mtest [-dv] sender dom hello ip\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *a[5], *s;
+	int i;
+
+	ARGBEGIN{
+	case 'd':
+		dflag = 1;
+		break;
+	case 'v':
+		vflag = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	fmtinstall('I', eipfmt);
+	memset(a, 0, sizeof a);
+	for(i = 0; i < argc && i < nelem(a); i++)
+		a[i] = argv[i];
+	s = macro(a[0], a[1], a[2], a[3], a[4]);
+	print("%s\n", s);
+	free(s);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/spf/spf.c
@@ -1,0 +1,800 @@
+#include "spf.h"
+
+#define	vprint(...) if(vflag) fprint(2, __VA_ARGS__)
+
+enum{
+	Traw,
+	Tip4,
+	Tip6,
+	Texists,
+	Tall,
+	Tbegin,
+	Tend,
+};
+
+char *typetab[] = {
+	"raw",
+	"ip4",
+	"ip6",
+	"exists",
+	"all",
+	"begin",
+	"end",
+};
+
+typedef struct Squery Squery;
+struct Squery{
+	char	ver;
+	char	sabort;
+	char	mod;
+	char	*cidrtail;
+	char	*ptrmatch;
+	char	*ip;
+	char	*domain;
+	char	*sender;
+	char	*hello;
+};
+
+typedef struct Spf Spf;
+struct Spf{
+	char	mod;
+	char	type;
+	char	s[100];
+};
+#pragma	varargck type	"§"	Spf*
+
+char	*txt;
+char	*netroot = "/net";
+char	dflag;
+char	eflag;
+char	mflag;
+char	pflag;
+char	rflag;
+char	vflag;
+
+char *vtab[] = {0, "v=spf1", "spf2.0/"};
+
+char*
+isvn(Squery *q, char *s, int i)
+{
+	char *p, *t;
+
+	t = vtab[i];
+	if(cistrncmp(s, t, strlen(t)))
+		return 0;
+	p = s + strlen(t);
+	if(i == 2){
+		p = strchr(p, ' ');
+		if(p == nil)
+			return 0;
+	}
+	if(*p && *p++ != ' ')
+		return 0;
+	q->ver = i;
+	return p;
+}
+
+char*
+pickspf(Squery *s, char *v1, char *v2)
+{
+	switch(s->ver){
+	default:
+	case 0:
+		if(v1)
+			return v1;
+		return v2;
+	case 1:
+		if(v1)
+			return v1;
+		return 0;
+	case 2:
+		if(v2)
+			return v2;
+		return v1;	/* spf2.0/pra,mfrom */
+	}
+}
+
+char *ftab[] = {"txt", "spf"};	/* p. 9 */
+
+char*
+spffetch(Squery *s, char *d)
+{
+	char *p, *v1, *v2;
+	int i;
+	Ndbtuple *t, *n;
+
+	if(txt){
+		p = strdup(txt);
+		txt = 0;
+		return p;
+	}
+	v1 = v2 = 0;
+	for(i = 0; i < nelem(ftab); i++){
+		t = vdnsquery(d, ftab[i], 0);
+		for(n = t; n; n = n->entry){
+			if(strcmp(n->attr, ftab[i]))
+				continue;
+			v1 = isvn(s, n->val, 1);
+			v2 = isvn(s, n->val, 2);
+		}
+		if(p = pickspf(s, v1, v2))
+			p = strdup(p);
+		ndbfree(t);
+		if(p)
+			return p;
+	}
+	return 0;
+}
+
+Spf	spftab[200];
+int	nspf;
+int	mod;
+
+Spf*
+spfadd(int type, char *s)
+{
+	Spf *p;
+
+	if(nspf >= nelem(spftab))
+		return 0;
+	p = spftab+nspf;
+	p->s[0] = 0;
+	if(s)
+		snprint(p->s, sizeof p->s, "%s", s);
+	p->type = type;
+	p->mod = mod;
+	nspf++;
+	return p;
+}
+
+char *badcidr[] = {
+	"0.0.0.0/8",
+	"1.0.0.0/8",
+	"2.0.0.0/8",
+	"5.0.0.0/8",
+	"10.0.0.0/8",
+	"127.0.0.0/8",
+	"255.0.0.0/8",
+	"192.168.0.0/16",
+	"169.254.0.0/16",
+	"172.16.0.0/20",
+	"224.0.0.0/24",		/*rfc 3330 says this is /4.  not sure */
+	"fc00::/7",
+};
+
+char *okcidr[] = {
+	"17.0.0.0/8",		/* apple.  seems dubious. */
+};
+
+int
+parsecidr(uchar *addr, uchar *mask, char *from)
+{
+	char *p, buf[50];
+	int i, bits, z;
+	vlong v;
+	uchar *a;
+
+	strecpy(buf, buf+sizeof buf, from);
+	if(p = strchr(buf, '/'))
+		*p = 0;
+	v = parseip(addr, buf);
+	if(v == -1)
+		return -1;
+	switch((ulong)v){
+	default:
+		bits = 32;
+		z = 96;
+		break;
+	case 6:
+		bits = 128;
+		z = 0;
+		break;
+	}
+
+	if(p){
+		i = strtoul(p+1, &p, 0);
+		if(i > bits)
+			i = bits;
+		i += z;
+		memset(mask, 0, 128/8);
+		for(a = mask; i >= 8; i -= 8)
+			*a++ = 0xff;
+		if(i > 0)
+			*a = ~((1<<(8-i))-1);
+	}else
+		memset(mask, 0xff, IPaddrlen);
+	return 0;
+}
+
+/*
+ * match x.y.z.w to x1.y1.z1.w1/m
+ */
+int
+cidrmatch(char *x, char *y)
+{
+	uchar a[IPaddrlen], b[IPaddrlen], m[IPaddrlen];
+
+	if(parseip(a, x) == -1)
+		return 0;
+	parsecidr(b, m, y);
+	maskip(a, m, a);
+	maskip(b, m, b);
+	if(!memcmp(a, b, IPaddrlen))
+		return 1;
+	return 0;
+}
+
+int
+cidrmatchtab(char *addr, char **tab, int ntab)
+{
+	int i;
+
+	for(i = 0; i < ntab; i++)
+		if(cidrmatch(addr, tab[i]))
+			return 1;
+	return 0;
+}
+
+int
+okcidrlen(char *cidr, int i)
+{
+	if(i >= 14 && i <= 128)
+		return 1;
+	if(cidrmatchtab(cidr, okcidr, nelem(okcidr)))
+		return 1;
+	return 0;
+}
+
+int
+cidrokay0(char *cidr)
+{
+	char *p, buf[40];
+	uchar addr[IPaddrlen];
+	int l, i;
+
+	p = strchr(cidr, '/');
+	if(p)
+		l = p-cidr;
+	else
+		l = strlen(cidr);
+	if(l > 39)
+		return 0;
+	if(p){
+		i = atoi(p+1);
+		if(!okcidrlen(cidr, i))
+			return 0;
+	}
+	memcpy(buf, cidr, l);
+	buf[l] = 0;
+	if(parseip(addr, buf) == -1)
+		return 0;
+	if(cidrmatchtab(cidr, badcidr, nelem(badcidr)))
+		return 0;
+	return 1;
+}
+
+int
+cidrokay(char *cidr)
+{
+	if(!cidrokay0(cidr)){
+		fprint(2, "spf: naughty cidr %s\n", cidr);
+		return 0;
+	}
+	return 1;
+}
+
+int
+ptrmatch(Squery *q, char *s)
+{
+	if(!q->ptrmatch || !strcmp(q->ptrmatch, s))
+		return 1;
+	return 0;
+}
+
+Spf*
+spfaddcidr(Squery *q, int type, char *s)
+{
+	char buf[64];
+
+	if(q->cidrtail){
+		snprint(buf, sizeof buf, "%s/%s", s, q->cidrtail);
+		s = buf;
+	}
+	if(cidrokay(s) && ptrmatch(q, s))
+		return spfadd(type, s);
+	return 0;
+}
+
+char*
+qpluscidr(Squery *q, char *d, int recur, int *y)
+{
+	char *p;
+
+	*y = 0;
+	if(!recur && (p = strchr(d, '/'))){
+		q->cidrtail = p + 1;
+		*p = 0;
+		*y = 1;
+	}
+	return d;
+}
+
+void
+cidrtail(Squery *q, char *, int y)
+{
+	if(!y)
+		return;
+	q->cidrtail[-1] = '/';
+	q->cidrtail = 0;
+}
+
+void
+aquery(Squery *q, char *d, int recur)
+{
+	int y;
+	Ndbtuple *t, *n;
+
+	d = qpluscidr(q, d, recur, &y);
+	t = vdnsquery(d, "any", recur);
+	for(n = t; n; n = n->entry){
+		if(!strcmp(n->attr, "ip"))
+			spfaddcidr(q, Tip4, n->val);
+		else if(!strcmp(n->attr, "ipv6"))
+			spfaddcidr(q, Tip6, n->val);
+		else if(!strcmp(n->attr, "cname"))
+			aquery(q, d, recur+1);
+	}
+	cidrtail(q, d, y);
+	ndbfree(t);
+}
+
+void
+mxquery(Squery *q, char *d, int recur)
+{
+	int i, y;
+	Ndbtuple *t, *n;
+
+	d = qpluscidr(q, d, recur, &y);
+	i = 0;
+	t = vdnsquery(d, "mx", recur);
+	for(n = t; n; n = n->entry)
+		if(i++ < 10 && !strcmp(n->attr, "mx"))
+			aquery(q, n->val, recur+1);
+	ndbfree(t);
+	cidrtail(q, d, y);
+}
+
+void
+ptrquery(Squery *q, char *d, int recur)
+{
+	char *s, buf[64];
+	int i, y;
+	Ndbtuple *t, *n;
+
+	if(!q->ip){
+		fprint(2, "spf: ptr query; no ip\n");
+		return;
+	}
+	d = qpluscidr(q, d, recur, &y);
+	i = 0;
+	dnreverse(buf, sizeof buf, s = strdup(q->ip));
+	t = vdnsquery(buf, "ptr", recur);
+	for(n = t; n; n = n->entry){
+		if(!strcmp(n->attr, "dom") || !strcmp(n->attr, "cname"))
+		if(i++ < 10 && dncontains(d, n->val)){
+			q->ptrmatch = q->ip;
+			aquery(q, n->val, recur+1);
+			q->ptrmatch = 0;
+		}
+	}
+	ndbfree(t);
+	free(s);
+	cidrtail(q, d, y);
+}
+
+/*
+ * this looks very wrong; see §5.7 which says only a records match.
+ */
+void
+exists(Squery*, char *d, int recur)
+{
+	Ndbtuple *t;
+
+	if(t = vdnsquery(d, "ip", recur))
+		spfadd(Texists, "1");
+	else
+		spfadd(Texists, 0);
+	ndbfree(t);
+}
+
+void
+addfail(void)
+{
+	mod = '-';
+	spfadd(Tall, 0);
+}
+
+void
+addend(char *s)
+{
+	spfadd(Tend, s);
+	spftab[nspf-1].mod = 0;
+}
+
+Spf*
+includeloop(char *s1, int n)
+{
+	char *s, *p;
+	int i;
+
+	for(i = 0; i < n; i++){
+		s = spftab[i].s;
+		if(s)
+		if(p = strstr(s, " -> "))
+		if(!strcmp(p+4, s1))
+			return spftab+i;
+	}
+	return nil;
+}
+
+void
+addbegin(int c, char *s0, char *s1)
+{
+	char buf[0xff];
+
+	snprint(buf, sizeof buf, "%s -> %s", s0, s1);
+	spfadd(Tbegin, buf);
+	spftab[nspf-1].mod = c;
+}
+
+void
+ditch(void)
+{
+	if(nspf > 0)
+		nspf--;
+}
+
+static void
+lower(char *s)
+{
+	int c;
+
+	for(; c = *s; s++)
+		if(c >= 'A' && c <= 'Z')
+			*s = c + 0x20;
+}
+
+int
+spfquery(Squery *x, char *d, int include)
+{
+	char *s, **t, *r, *p, *q, buf[10];
+	int i, n, c;
+	Spf *inc;
+
+	if(include)
+	if(inc = includeloop(d, nspf-1)){
+		fprint(2, "spf: include loop: %s (%s)\n", d, inc->s);
+		return -1;
+	}
+	s = spffetch(x, d);
+	if(!s)
+		return -1;
+	t = malloc(500*sizeof *t);
+	n = getfields(s, t, 500, 1, " ");
+	x->sabort = 0;
+	for(i = 0; i < n && !x->sabort; i++){
+		if(!strncmp(t[i], "v=", 2))
+			continue;
+		c = *t[i];
+		r = t[i]+1;
+		switch(c){
+		default:
+			mod = '+';
+			r--;
+			break;
+		case '-':
+		case '~':
+		case '+':
+		case '?':
+			mod = c;
+			break;
+		}
+		if(!strcmp(r, "all")){
+			spfadd(Tall, 0);
+			continue;
+		}
+		strecpy(buf, buf+sizeof buf, r);
+		p = strchr(buf, ':');
+		if(p == 0)
+			p = strchr(buf, '=');
+		q = d;
+		if(p){
+			*p = 0;
+			q = p+1;
+			q = r+(q-buf);
+		}
+		if(!mflag)
+			q = macro(q, x->sender, x->domain, x->hello, x->ip);
+		else
+			q = strdup(q);
+		lower(buf);
+		if(!strcmp(buf, "ip4"))
+			spfaddcidr(x, Tip4, q);
+		else if(!strcmp(buf, "ip6"))
+			spfaddcidr(x, Tip6, q);
+		else if(!strcmp(buf, "a"))
+			aquery(x, q, 0);
+		else if(!strcmp(buf, "mx"))
+			mxquery(x, d, 0);
+		else if(!strcmp(buf, "ptr"))
+			ptrquery(x, d, 0);
+		else if(!strcmp(buf, "exists"))
+			exists(x, q, 0);
+		else if(!strcmp(buf, "include") || !strcmp(buf, "redirect")){
+			if(q && *q){
+				if(rflag)
+					fprint(2, "I> %s\n", q);
+				addbegin(mod, r, q);
+				if(spfquery(x, q, 1) == -1){
+					ditch();
+					addfail();
+				}else
+					addend(r);
+			}
+		}
+		free(q);
+	}
+	free(t);
+	free(s);
+	return 0;
+}
+
+char*
+url(char *s)
+{
+	char buf[64], *p, *e;
+	int c;
+
+	p = buf;
+	e = p + sizeof buf;
+	*p = 0;
+	while(c = *s++){
+		if(c >= 'A' && c <= 'Z')
+			c += 0x20;
+		if(c <= ' ' || c == '%' || c & 0x80)
+			p = seprint(p, e, "%%%2.2X", c);
+		else
+			p = seprint(p, e, "%c", c);
+	}
+	return strdup(buf);
+}
+
+void
+spfinit(Squery *q, char *dom, int argc, char **argv)
+{
+	uchar a[IPaddrlen];
+
+	memset(q, 0, sizeof q);
+	q->ip = argc>0? argv[1]: 0;
+	if(q->ip && parseip(a, q->ip) == -1)
+		sysfatal("bogus ip");
+	q->domain = url(dom);
+	q->sender = argc>2? url(argv[2]): 0;
+	q->hello = argc>3? url(argv[3]): 0;
+	mod = 0;				/* BOTCH */
+}
+
+int
+§fmt(Fmt *f)
+{
+	char *p, *e, buf[115];
+	Spf *spf;
+
+	spf = va_arg(f->args, Spf*);
+	if(!spf)
+		return fmtstrcpy(f, "<nil>");
+	e = buf+sizeof buf;
+	p = buf;
+	if(spf->mod && spf->mod != '+')
+		*p++ = spf->mod;
+	p = seprint(p, e, "%s", typetab[spf->type]);
+	if(spf->s[0])
+		seprint(p, e, " : %s", spf->s);
+	return fmtstrcpy(f, buf);
+}
+
+static Spf head;
+
+struct{
+	int	i;
+}walk;
+
+int
+invertmod(int c)
+{
+	switch(c){
+	case '?':
+		return '?';
+	case '+':
+		return '-';
+	case '-':
+		return '+';
+	case '~':
+		return '?';
+	}
+	return 0;
+}
+
+#define reprint(...) if(vflag && recur == 0) fprint(2, __VA_ARGS__)
+
+int
+spfwalk(int all, int recur, char *ip)
+{
+	int match, bias, mod, r;
+	Spf *s;
+
+	r = 0;
+	bias = 0;
+	if(recur == 0)
+		walk.i = 0;
+	for(; walk.i < nspf; walk.i++){
+		s = spftab+walk.i;
+		mod = s->mod;
+		switch(s->type){
+		default:
+			abort();
+		case Tbegin:
+			walk.i++;
+			match = spfwalk(s->s[0] == 'r', recur+1, ip);
+			if(match < 0)
+				mod = invertmod(mod);
+			break;
+		case Tend:
+			return r;
+		case Tall:
+			match = 1;
+			break;
+		case Texists:
+			match = s->s[0];
+			break;
+		case Tip4:
+		case Tip6:
+			match = cidrmatch(ip, s->s);
+			break;
+		}
+		if(!r && match)
+			switch(mod){
+			case '~':
+				reprint("bias %§\n", s);
+				bias = '~';
+			case '?':
+				break;
+			case '-':
+				if(all || s->type !=Tall){
+					vprint("fail %§\n", s);
+					r = -1;
+				}
+				break;
+			case '+':
+			default:
+				vprint("match %§\n", s);
+				r = 1;
+			}
+	}
+	/* recur == 0 */
+	if(r == 0 && bias == '~')
+		r = -1;
+	return r;
+}
+
+/* ad hoc and noncomprehensive */
+char *tccld[] = {"au", "ca", "gt", "id", "pk",  "uk", "ve", };
+int
+is3cctld(char *s)
+{
+	int i;
+
+	if(strlen(s) != 2)
+		return 0;
+	for(i = 0; i < nelem(tccld); i++)
+		if(!strcmp(tccld[i], s))
+			return 1;
+	return 0;
+}
+
+char*
+rootify(char *d)
+{
+	char *p, *q;
+
+	if(!(p = strchr(d, '.')))
+		return 0;
+	p++;
+	if(!(q = strchr(p, '.')))
+		return 0;
+	q++;
+	if(!strchr(q, '.') && is3cctld(q))
+		return 0;
+	return p;
+}
+
+void
+usage(void)
+{
+	fprint(2, "spf [-demrpv] [-n netroot] dom [ip sender helo]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *s, *d, *e;
+	int i, j, t[] = {0, 3};
+	Squery q;
+
+	ARGBEGIN{
+	case 'd':
+		dflag = 1;
+		break;
+	case 'e':
+		eflag = 1;
+		break;
+	case 'm':
+		mflag = 1;
+		break;
+	case 'n':
+		netroot = EARGF(usage());
+		break;
+	case 'p':
+		pflag = 1;
+		break;
+	case 'r':
+		rflag = 1;
+		break;
+	case 't':
+		txt = EARGF(usage());
+		break;
+	case 'v':
+		vflag = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc < 1 || argc > 4)
+		usage();
+	if(argc == 1)
+		pflag = 1;
+	fmtinstall(L'§', §fmt);
+	fmtinstall('I', eipfmt);
+	fmtinstall('M', eipfmt);
+
+	e = "none";
+	for(i = 0; i < nelem(t); i++){
+		if(argc <= t[i])
+			break;
+		d = argv[t[i]];
+		for(j = 0; j < i; j++)
+			if(!strcmp(argv[t[j]], d))
+				goto loop;
+		for(s = d; ; s = rootify(s)){
+			if(!s)
+				goto loop;
+			spfinit(&q, d, argc, argv);	/* or s? */
+			addbegin('+', ".", s);
+			if(spfquery(&q, s, 0) != -1)
+				break;
+		}
+		if(eflag && nspf)
+			addfail();
+		e = "";
+		if(pflag)
+		for(j = 0; j < nspf; j++)
+			print("%§\n", spftab+j);
+		if(argc >= t[i] && argc > 1)
+		if(spfwalk(1, 0, argv[1]) == -1)
+			exits("fail");
+loop:;
+	}
+	exits(e);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/spf/spf.h
@@ -1,0 +1,12 @@
+/* © 2008 erik quanstrom; plan 9 license */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ip.h>
+
+char		*macro(char*, char*, char*, char*, char*);
+
+Ndbtuple*	vdnsquery(char*, char*, int);
+int		dncontains(char*, char *);
+void		dnreverse(char*, int, char*);
--- /dev/null
+++ b/sys/src/cmd/upas/spf/testsuite
@@ -1,0 +1,9 @@
+#!/bin/rc
+for(i in '%{s}' '%{o}' '%{d}' '%{d4}' '%{d3}' '%{d2}' '%{d1}' '%{dr}' '%{d2r}' '%{l}' '%{l-}' '%{lr}' '%{lr-}' '%{l1r-}')
+	mtest $i '[email protected]' email.example.com helounknown 192.0.2.3 
+for(i in '%{i}')
+	mtest $i '[email protected]' email.example.com helounknown 2001:db8::cb01
+for(i in '%{ir}.%{v}._spf.%{d2}' '%{lr-}.lp._spf.%{d2}' '%{lr-}.lp.%{ir}.%{v}._spf.%{d2}' '%{ir}.%{v}.%{lr-}.lp._spf.%{d2}')	
+	mtest $i '[email protected]' email.example.com helounknown 2001:db8::cb01
+for(i in '%{ir}.%{v}._spf.%{d2}' '%{lr-}.lp._spf.%{d2}' '%{lr-}.lp.%{ir}.%{v}._spf.%{d2}' '%{ir}.%{v}.%{lr-}.lp._spf.%{d2}')	
+	mtest $i '[email protected]' email.example.com helounknown 192.0.2.3 
--- a/sys/src/cmd/upas/unesc/mkfile
+++ b/sys/src/cmd/upas/unesc/mkfile
@@ -1,9 +1,8 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=unesc
 OFILES=unesc.$O\
-
-BIN=/$objtype/bin/upas
 
 UPDATE=\
 	mkfile\
--- a/sys/src/cmd/upas/vf/mkfile
+++ b/sys/src/cmd/upas/vf/mkfile
@@ -1,4 +1,5 @@
 </$objtype/mkfile
+<../mkupas
 
 TARG=vf
 
@@ -9,8 +10,6 @@
 HFILES=../common/common.h\
 	 ../common/sys.h\
 
-
-BIN=/$objtype/bin/upas
 UPDATE=\
 	mkfile\
 	$HFILES\
--- a/sys/src/cmd/upas/vf/vf.c
+++ b/sys/src/cmd/upas/vf/vf.c
@@ -954,7 +954,7 @@
 {
 	String *s;
 	char decoded[1024];
-	char utfbuf[UTFmax*1024];
+	char utfbuf[2*1024];
 	int i, len;
 	char *e;
 	char *token;
@@ -986,6 +986,8 @@
 	if(cistrncmp(token, "b?", 2) == 0){
 		token += 2;
 		len = dec64((uchar*)decoded, sizeof(decoded), token, e-token);
+		if(len == -1)
+			goto err;
 		decoded[len] = 0;
 	} else if(cistrncmp(token, "q?", 2) == 0){
 		token += 2;