shithub: riscv

ref: 5d202d345616907d511b46e395226958fe66a2c3
dir: /sys/src/cmd/upas/ned/nedmail.c/

View raw version
#include "common.h"
#include <ctype.h>
#include <plumb.h>
#include <regexp.h>

typedef struct Cmd Cmd;
typedef struct Ctype Ctype;
typedef struct Dirstats Dirstats;
typedef struct Message Message;
typedef Message* (Mfn)(Cmd*,Message*);

enum{
	/* nflags */
	Nmissing	= 1<<0,
	Nnoflags	= 1<<1,

	Narg	= 32,
};

struct Message {
	Message	*next;
	Message	*prev;
	Message	*cmd;
	Message	*child;
	Message	*parent;
	char	*path;
	int	id;
	int	len;
	int	fileno;	/* number of directory */
	char	*info;
	char	*from;
	char	*to;
	char	*cc;
	char	*replyto;
	char	*date;
	char	*subject;
	char	*type;
	char	*disposition;
	char	*filename;
	uchar	flags;
	uchar	nflags;
};
#pragma varargck	type	"D"	Message*

enum{
	Display	= 1<<0,
	Rechk	= 1<<1,	/* always use file to check content type */
};

struct Ctype {
	char	*type;
	char 	*ext;
	uchar	flag;
	char	*plumbdest;
	Ctype	*next;
};

/* first element is the default return value */
Ctype ctype[] = {
	{ "application/octet-stream", 	"bin", 	Rechk, 	0,	0,	},
	{ "text/plain",			"txt",	Display,	0	},
	{ "text/html",			"htm",	Display,	0	},
	{ "text/html",			"html",	Display,	0	},
	{ "text/tab-separated-values",	"tsv",	Display,	0	},
	{ "text/richtext",			"rtx",	Display,	0	},
	{ "text/rtf",			"rtf",	Display,	0	},
	{ "text",				"txt",	Display,	0	},
	{ "message/rfc822",		"msg",	0,	0	},
	{ "image/bmp",			"bmp",	0,	"image"	},
	{ "image/jpg",			"jpg",	0,	"image"	},
	{ "image/jpeg",			"jpg",	0,	"image"	},
	{ "image/gif",			"gif",	0,	"image"	},
	{ "image/png",			"png",	0,	"image"	},
	{ "image/x-png",			"png",	0,	"image"	},
	{ "image/tiff",			"tif",	0,	"image"	},
	{ "application/pdf",		"pdf",	0,	"postscript"	},
	{ "application/postscript",		"ps",	0,	"postscript"	},
	{ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",		"docx",	0,	"docx"	},
	{ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",		"xlsx",	0,	"xlsx"	},
	{ "application/",			0,	0,	0	},
	{ "image/",			0,	0,	0	},
	{ "multipart/",			"mul",	0,	0	},

};

struct Dirstats {
	int	new;
	int	del;
	int	old;
	int	unread;
};

Mfn	acmd;
Mfn	bangcmd;
Mfn	bcmd;
Mfn	dcmd;
Mfn	eqcmd;
Mfn	Fcmd;
Mfn	fcmd;
Mfn	fqcmd;
Mfn	Hcmd;
Mfn	hcmd;
Mfn	helpcmd;
Mfn	icmd;
Mfn	Kcmd;
Mfn	kcmd;
Mfn	mbcmd;
Mfn	mcmd;
Mfn	Pcmd;
Mfn	pcmd;
Mfn	pipecmd;
Mfn	qcmd;
Mfn	quotecmd;
Mfn	rcmd;
Mfn	rpipecmd;
Mfn	scmd;
Mfn	tcmd;
Mfn	ucmd;
Mfn	wcmd;
Mfn	xcmd;
Mfn	ycmd;

struct {
	char	*cmd;
	int	args;
	int	addr;
	Mfn	*f;
	char	*help;
} cmdtab[] = {
	{ "a",	1, 1,	acmd,	"a\t"		"reply to sender and recipients" },
	{ "A",	1, 0,	acmd,	"A\t"		"reply to sender and recipients with copy" },
	{ "b",	0, 0,	bcmd,	"b\t"		"print the next 10 headers" },
	{ "d",	0, 1,	dcmd,	"d\t"		"mark for deletion" },
	{ "F",	1, 1,	Fcmd,	"f\t"		"set message flags [+-][aDdfrSs]" },
	{ "f",	0, 1,	fcmd,	"f\t"		"file message by from address" },
	{ "fq",	0, 1,	fqcmd,	"fq\t"		"print mailbox f appends" },
	{ "H",	0, 0,	Hcmd,	"H\t"		"print message's MIME structure" },
	{ "h",	0, 0,	hcmd,	"h\t"		"print message summary (,h for all)" },
	{ "help", 0, 0,	helpcmd, "help\t"		"print this info" },
	{ "i",	0, 0,	icmd,	"i\t"		"incorporate new mail" },
	{ "k",	1, 1,	kcmd,	"k [flags]\t"	"mark mail" },
	{ "K",	1, 1,	Kcmd,	"K [flags]\t"	"unmark mail" },
	{ "m",	1, 1,	mcmd,	"m addr\t"	"forward mail" },
	{ "M",	1, 0,	mcmd,	"M addr\t"	"forward mail with message" },
	{ "mb",	1, 0,	mbcmd,	"mb mbox\t"	"switch mailboxes" },
	{ "p",	1, 0,	pcmd,	"p\t"		"print the processed message" },
	{ "P",	0, 0,	Pcmd,	"P\t"		"print the raw message" },
	{ "\"",	0, 0,	quotecmd, "\"\t"		"print a quoted version of msg" },
	{ "\"\"",	0, 0,	quotecmd, "\"\"\t"		"format and quote message" },
	{ "q",	0, 0,	qcmd,	"q\t"		"exit and remove all deleted mail" },
	{ "r",	1, 1,	rcmd,	"r [addr]\t"	"reply to sender plus any addrs specified" },
	{ "rf",	1, 0,	rcmd,	"rf [addr]\t"	"file message and reply" },
	{ "R",	1, 0,	rcmd,	"R [addr]\t"	"reply including copy of message" },
	{ "Rf",	1, 0,	rcmd,	"Rf [addr]\t"	"file message and reply with copy" },
	{ "s",	1, 1,	scmd,	"s file\t"		"append raw message to file" },
	{ "t",	1, 0,	tcmd,	"t\t"		"text formatter" },
	{ "u",	0, 0,	ucmd,	"u\t"		"remove deletion mark" },
	{ "w",	1, 1,	wcmd,	"w file\t"		"store message contents as file" },
	{ "x",	0, 0,	xcmd,	"x\t"		"exit without flushing deleted messages" },
	{ "y",	0, 0,	ycmd,	"y\t"		"synchronize with mail box" },
	{ "=",	1, 0,	eqcmd,	"=\t"		"print current message number" },
	{ "|",	1, 1,	pipecmd, "|cmd\t"		"pipe message body to a command" },
	{ "||",	1, 1,	rpipecmd, "||cmd\t"	"pipe raw message to a command" },
	{ "!",	1, 0,	bangcmd, "!cmd\t"		"run a command" },
};

struct Cmd {
	Message	*msgs;
	Mfn	*f;
	int	an;
	char	*av[Narg];
	char	cmdline[2*1024];
	int	delete;
};

int		dir2message(Message*, int, Dirstats*);
int		mdir2message(Message*);
char*		extendp(char*, char*);
char*		parsecmd(char*, Cmd*, Message*, Message*);
void		system(char*, char**, int);
int		switchmb(char *, int);
void		closemb(void);
Message*	dosingleton(Message*, char*);
char*		rooted(char*);
int		plumb(Message*, Ctype*);
void		exitfs(char*);
Message*	flushdeleted(Message*);

int	didopen;
int	doflush;
int	interrupted;
int	longestfrom = 12;
int	longestto = 12;
int	hcmdfmt;
Qid	mbqid;
int	mbvers;
char	mbname[Pathlen];
char	mbpath[Pathlen];
int	natural;
Biobuf	out;
int	reverse;
char	root[Pathlen];
int	rootlen;
int	startedfs;
Message	top;
char	*user;
char	homewd[Pathlen];
char	wd[Pathlen];
char	textfmt[Pathlen];

char*
idfmt(char *p, char *e, Message *m)
{
	char buf[32];
	int sz, l;

	for(; (sz = e - p) > 0; ){
		l = snprint(buf, sizeof buf, "%d", m->id);
		if(l + 1 > sz)
			return "*GOK*";
		e -= l;
		memcpy(e, buf, l);
		if((m = m->parent) == &top)
			break;
		e--;
		*e = '.';
	}
	return e;
}

int
eprint(char *fmt, ...)
{
	int n;
	va_list args;

	Bflush(&out);

	va_start(args, fmt);
	n = vfprint(2, fmt, args);
	va_end(args);
	return n;
}

void
dissappeared(void)
{
	char buf[ERRMAX];

	rerrstr(buf, sizeof buf);
	if(strstr(buf, "hungup channel")){
		eprint("\n!she's dead, jim\n");
		exits(buf);
	}
	eprint("!message dissappeared\n");
}

int
Dfmt(Fmt *f)
{
	char *e, buf[128];
	Message *m;

	m = va_arg(f->args, Message*);
	if(m == nil)
		return fmtstrcpy(f, "*GOK*");
	if(m == &top)
		return 0;
	e = buf + sizeof buf - 1;
	*e = 0;
	return fmtstrcpy(f, idfmt(buf, e, m));
}

char*
readline(char *prompt, char *line, int len)
{
	char *p, *e, *q;
	int n, dump;

	e = line + len;
retry:
	dump = 0;
	interrupted = 0;
	eprint("%s", prompt);
	for(p = line;; p += n){
		if(p == e){
			dump = 1;
			p = line;
		}
		n = read(0, p, e - p);
		if(n < 0){
			if(interrupted)
				goto retry;
			return nil;
		}
		if(n == 0)
			return nil;
		if(q = memchr(p, '\n', n)){
			if(dump){
				eprint("!line too long\n");
				goto retry;
			}
			p = q;
			break;
		}
	}
	*p = 0;
	return line;
}

void
usage(void)
{
	fprint(2, "usage: %s [-nrt] [-f mailfile] [-s mailfile]\n", argv0);
	fprint(2, "       %s -c dir\n", argv0);
	exits("usage");
}

void
catchnote(void*, char *note)
{
	if(strstr(note, "interrupt") != nil){
		interrupted = 1;
		noted(NCONT);
	}
	noted(NDFLT);
}

char*
plural(int n)
{
	if (n == 1)
		return "";
	return "s";	
}

void
main(int argc, char **argv)
{
	char cmdline[2*1024], prompt[64], *err, *av[4], *mb;
	int n, cflag, singleton;
	Cmd cmd;
	Ctype *cp;
	Message *cur, *m, *x;

	Binit(&out, 1, OWRITE);

	mb = nil;
	singleton = 0;
	reverse = 1;
	cflag = 0;
	ARGBEGIN {
	case 'c':
		cflag = 1;
		break;
	case 's':
		singleton = 1;
	case 'f':
		mb = EARGF(usage());
		break;
	case 'r':
		reverse = 0;
		break;
	case 'n':
		natural = 1;
		reverse = 0;
		break;
	case 't':
		hcmdfmt = 1;
		break;
	default:
		usage();
		break;
	} ARGEND;

	fmtinstall('D', Dfmt);
	quotefmtinstall();
	doquote = needsrcquote;
	getwd(homewd, sizeof homewd);
	user = getlog();
	if(user == nil || *user == 0)
		sysfatal("can't read user name");

	if(cflag){
		if(argc > 0)
			n = creatembox(user, argv[0]);
		else
			n = creatembox(user, nil);
		exits(n? 0: "fail");
	}

	if(argc)
		usage();

	if(access("/mail/fs/ctl", 0) < 0){
		startedfs = 1;
		av[0] = "fs";
		av[1] = "-p";
		av[2] = 0;
		system("/bin/upas/fs", av, -1);
	}

	switchmb(mb, singleton);
	top.path = strdup(root);
	for(cp = ctype; cp < ctype + nelem(ctype) - 1; cp++)
		cp->next = cp + 1;

	if(singleton){
		cur = dosingleton(&top, mb);
		if(cur == nil){
			eprint("no message\n");
			exitfs(0);
		}
		pcmd(nil, cur);
	} else {
		cur = &top;
		if(icmd(nil, cur) == nil)
			sysfatal("can't read %s", top.path);
	}

	notify(catchnote);
	for(;;){
		snprint(prompt, sizeof prompt, "%D: ", cur);

		/*
		 * leave space at the end of cmd line in case parsecmd needs to
		 * add a space after a '|' or '!'
		 */
		if(readline(prompt, cmdline, sizeof cmdline - 1) == nil)
			break;
		err = parsecmd(cmdline, &cmd, top.child, cur);
		if(err != nil){
			eprint("!%s\n", err);
			continue;
		}
		if(singleton && (cmd.f == icmd || cmd.f == ycmd)){
			eprint("!illegal command\n");
			continue;
		}
		interrupted = 0;
		if(cmd.msgs == nil || cmd.msgs == &top){
			if(x = cmd.f(&cmd, &top))
				cur = x;
		} else for(m = cmd.msgs; m != nil; m = m->cmd){
			x = m;
			if(cmd.delete){
				dcmd(&cmd, x);

				/*
				 * dp acts differently than all other commands
				 * since its an old lesk idiom that people love.
				 * it deletes the current message, moves the current
				 * pointer ahead one and prints.
				 */
				if(cmd.f == pcmd){
					if(x->next == nil){
						eprint("!address\n");
						cur = x;
						break;
					} else
						x = x->next;
				}
			}
			x = cmd.f(&cmd, x);
			if(x != nil)
				cur = x;
			if(interrupted)
				break;
			if(singleton && (cmd.delete || cmd.f == dcmd))
				qcmd(nil, nil);
		}
		if(doflush)
			cur = flushdeleted(cur);
	}
	qcmd(nil, nil);
}

char*
file2string(char *dir, char *file)
{
	int fd, n;
	char *s, *p, *e;

	p = s = malloc(512);
	e = p + 511;

	fd = open(extendp(dir, file), OREAD);
	while((n = read(fd, p, e - p)) > 0){
		p += n;
		if(p == e){
			s = realloc(s, (n = p - s) + 512 + 1);
			if(s == nil)
				sysfatal("malloc: %r");
			p = s + n;
			e = p + 512;
		}
	}
	close(fd);
	*p = 0;
	return s;
}

#define Fields 		18			/* terrible hack; worth 10% */
#define Minfields	17

void
updateinfo(Message *m)
{
	char *s, *f[Fields + 1];
	int i, n, sticky;

	s = file2string(m->path, "info");
	if(s == nil)
		return;
	if((n = getfields(s, f, nelem(f), 0, "\n")) < Minfields){
		for(i = 0; i < n; i++)
			fprint(2, "info: %s\n", f[i]);
		sysfatal("info file invalid %s %D: %d fields", m->path, m, n);
	}
	if((m->nflags & Nnoflags) == 0){
		sticky = m->flags & Fdeleted;
		m->flags = buftoflags(f[17]) | sticky;
	}
	m->nflags &= ~Nmissing;
	free(s);
}

Message*
file2message(Message *parent, char *name)
{
	char *path, *f[Fields + 1];
	int i, n;
	Message *m;

	m = mallocz(sizeof *m, 1);
	if(m == nil)
		return nil;
	m->path = path = strdup(extendp(parent->path, name));
	m->fileno = atoi(name);
	m->info = file2string(path, "info");
	m->parent = parent;
	n = getfields(m->info, f, nelem(f), 0, "\n");
	if(n < Minfields){
		for(i = 0; i < n; i++)
			fprint(2, "info: [%s]\n", f[i]);
		sysfatal("info file invalid %s %D: %d fields", path, m, n);
	}
	m->from = f[0];
	m->to = f[1];
	m->cc = f[2];
	m->replyto = f[3];
	m->date = f[4];
	m->subject = f[5];
	m->type = f[6];
	m->disposition = f[7];
	m->filename = f[8];
	m->len = strtoul(f[16], 0, 0);
	if(n > 17)
		m->flags = buftoflags(f[17]);
	else
		m->nflags |= Nnoflags;

	if(m->type)
	if(strstr(m->type, "multipart") != nil || strcmp(m->type, "message/rfc822") == 0)
		mdir2message(m);
	return m;
}

void
freemessage(Message *m)
{
	Message *nm, *next;

	for(nm = m->child; nm != nil; nm = next){
		next = nm->next;
		freemessage(nm);
	}
	free(m->path);
	free(m->info);
	free(m);
}

/*
 * read a directory into a list of messages.  at the top level, there may be
 * large gaps in message numbers.  so we need to read the whole directory.
 * and pick out the messages we're interested in.  within a message, subparts
 * are contiguous and if we don't read the header/body/rawbody, we can avoid forcing
 * upas/fs to read the whole message.
 */
int
mdir2message(Message *parent)
{
	char buf[Pathlen];
	int i, highest, newmsgs;
	Dir *d;
	Message *first, *last, *m;

	/* count current entries */
	first = parent->child;
	highest = newmsgs = 0;
	for(last = parent->child; last != nil && last->next != nil; last = last->next)
		if(last->fileno > highest)
			highest = last->fileno;
	if(last != nil)
		if(last->fileno > highest)
			highest = last->fileno;
	for(i = highest + 1;; i++){
		snprint(buf, sizeof buf, "%s/%d", parent->path, i);
		if((d = dirstat(buf)) == nil)
			break;
		if((d->qid.type & QTDIR) == 0){
			free(d);
			continue;
		}
		free(d);
		snprint(buf, sizeof buf, "%d", i);
		m = file2message(parent, buf);
		if(m == nil)
			break;
		m->id = m->fileno;
		newmsgs++;
		if(first == nil)
			first = m;
		else
			last->next = m;
		m->prev = last;
		last = m;
	}
	parent->child = first;
	return newmsgs;
}

/*
 * 99.9% of the time, we don't need to sort this list.
 * however, sometimes email is added to a mailbox
 * out of order.  or, sape copies it back in from the
 * dump.  in this case, we've got to sort.
 *
 * BOTCH.  we're not observing any sort of stable
 * order.  if an old message comes in while upas/fs
 * is running, it will appear out of order.  restarting
 * upas/fs will reorder things.
 */
int
dcmp(Dir *a, Dir *b)
{
	return atoi(a->name) - atoi(b->name);
}

void
itsallsapesfault(Dir *d, int n)
{
	int c, i, r, t;

	/* evade qsort suck */
	r = -1;
	for(i = 0; i < n; i++){
		t = atol(d[i].name);
		if(t > r){
			c = d[i].name[0];
			if(c >= '0' && c <= 9)
				break;
		}
		r = t;
	}
	if(i != n)
		qsort(d, n, sizeof *d, (int (*)(void*, void*))dcmp);
}

int
dir2message(Message *parent, int reverse, Dirstats *s)
{
	int i, c, n, fd;
	Dir *d;
	Message *first, *last, *m, **ll;

	memset(s, 0, sizeof *s);
	fd = open(parent->path, OREAD);
	if(fd < 0)
		return -1;
	first = parent->child;
	last = nil;
	if(first)
		for(last = first; last->next; last = last->next)
			;
	n = dirreadall(fd, &d);
	itsallsapesfault(d, n);
	if(reverse)
		ll = &last;
	else
		ll = &parent->child;
	for(i = 0; *ll || i < n; ){
		if(i < n && (d[i].qid.type & QTDIR) == 0){
			i++;
			continue;
		}
		c = -1;
		if(i >= n)
			c = 1;
		else if(*ll)
			c = atoi(d[i].name) - (*ll)->fileno;
		if(c < 0){
			m = file2message(parent, d[i].name);
			if(m == nil)
				break;
			if(reverse){
				m->next = first;
				if(first != nil)
					first->prev = m;
				first = m;
			}else{
				if(first == nil)
					first = m;
				else
					last->next = m;
				m->prev = last;
				last = m;
			}
			*ll = m;
			s->new++;
			s->unread += (m->flags & Fseen) == 0;
			i++;
		}else if(c > 0){
			(*ll)->nflags |= Nmissing;
			s->del++;
		}else{
			updateinfo(*ll);
			s->old++;
			i++;
		}

		if(reverse)
			ll = &(*ll)->prev;
		else
			ll = &(*ll)->next;
	}
	free(d);
	close(fd);
	parent->child = first;

	/* renumber and file longest from */
	i = 1;
	longestfrom = 12;
	longestto = 12;
	for(m = first; m != nil; m = m->next){
		m->id = natural ? m->fileno : i++;
		n = strlen(m->from);
		if(n > longestfrom)
			longestfrom = n;
		n = strlen(m->to);
		if(n > longestto)
			longestto = n;
	}
	return 0;
}

/*
 *   point directly to a message
 */
Message*
dosingleton(Message *parent, char *path)
{
	char *p, *np;
	Message *m;

	/* walk down to message and read it */
	if(strlen(path) < rootlen)
		return nil;
	if(path[rootlen] != '/')
		return nil;
	p = path + rootlen + 1;
	np = strchr(p, '/');
	if(np != nil)
		*np = 0;
	m = file2message(parent, p);
	if(m == nil)
		return nil;
	parent->child = m;
	m->id = 1;

	/* walk down to requested component */
	while(np != nil){
		*np = '/';
		np = strchr(np + 1, '/');
		if(np != nil)
			*np = 0;
		for(m = m->child; m != nil; m = m->next)
			if(strcmp(path, m->path) == 0)
				return m;
		if(m == nil)
			return nil;
	}
	return m;
}

/*
 *   walk the path name an element
 */
char*
extendp(char *dir, char *name)
{
	static char buf[Pathlen];

	if(strcmp(dir, ".") == 0)
		snprint(buf, sizeof buf, "%s", name);
	else
		snprint(buf, sizeof buf, "%s/%s", dir, name);
	return buf;
}

char*
nosecs(char *t)
{
	char *p;

	p = strchr(t, ':');
	if(p == nil)
		return t;
	p = strchr(p + 1, ':');
	if(p != nil)
		*p = 0;
	return t;
}

char *months[12] =
{
	"jan", "feb", "mar", "apr", "may", "jun",
	"jul", "aug", "sep", "oct", "nov", "dec"
};

int
month(char *m)
{
	int i;

	for(i = 0; i < 12; i++)
		if(cistrcmp(m, months[i]) == 0)
			return i + 1;
	return 1;
}

enum
{
	Yearsecs	= 365*24*60*60,
};

void
cracktime(char *d, char *out, int len)
{
	char in[64], *f[6], *dtime;
	int n;
	long now, then;
	Tm tm;

	*out = 0;
	if(d == nil)
		return;
	strncpy(in, d, sizeof in);
	in[sizeof in - 1] = 0;
	n = getfields(in, f, 6, 1, " \t\r\n");
	if(n != 6){
		/* unknown style */
		snprint(out, 16, "%10.10s", d);
		return;
	}
	now = time(0);
	memset(&tm, 0, sizeof tm);
	if(strchr(f[0], ',') != nil && strchr(f[4], ':') != nil){
		/* 822 style */
		tm.year = atoi(f[3])-1900;
		tm.mon = month(f[2]);
		tm.mday = atoi(f[1]);
		dtime = nosecs(f[4]);
		then = tm2sec(&tm);
	} else if(strchr(f[3], ':') != nil){
		/* unix style */
		tm.year = atoi(f[5])-1900;
		tm.mon = month(f[1]);
		tm.mday = atoi(f[2]);
		dtime = nosecs(f[3]);
		then = tm2sec(&tm);
	} else {
		then = now;
		tm = *localtime(now);
		dtime = "";
	}

	if(now - then < Yearsecs/2)
		snprint(out, len, "%2d/%2.2d %s", tm.mon, tm.mday, dtime);
	else
		snprint(out, len, "%2d/%2.2d  %4d", tm.mon, tm.mday, tm.year + 1900);
}

int
matchtype(char *s, Ctype *t)
{
	return strncmp(t->type, s, strlen(t->type)) == 0;
}

Ctype*
findctype(Message *m)
{
	char *p, ftype[256];
	int n, pfd[2];
	Ctype *a, *cp;

	for(cp = ctype; cp; cp = cp->next)
		if(matchtype(m->type, cp))
			if((cp->flag & Rechk) == 0)
				return cp;
			else
				break;

	if(pipe(pfd) < 0)
		return ctype;
	*ftype = 0;
	switch(fork()){
	case -1:
		break;
	case 0:
		close(pfd[1]);
		close(0);
		dup(pfd[0], 0);
		close(1);
		dup(pfd[0], 1);
		execl("/bin/file", "file", "-m", extendp(m->path, "body"), nil);
		exits(0);
	default:
		close(pfd[0]);
		n = read(pfd[1], ftype, sizeof ftype - 1);
		while(n > 0 && isspace(ftype[n - 1]))
			n--;
		ftype[n] = 0;
		close(pfd[1]);
		waitpid();
		break;
	}
	for(cp = ctype; cp; cp = cp->next)
		if(matchtype(ftype, cp))
			return cp;
	if(*ftype == 0 || (p = strchr(ftype, '/')) == nil)
		return ctype;
	*p++ = 0;

	a = mallocz(sizeof *a, 1);
	a->type = strdup(ftype);
	a->ext = strdup(p);
	a->flag = 0;
	a->plumbdest = strdup(ftype);
	for(cp = ctype; cp->next; cp = cp->next)
		;
	cp->next = a;
	a->next = nil;
	return a;
}

/*
 * traditional
 */
void
hds(char *buf, Message *m)
{
	buf[0] = m->child? 'H': ' ';
	buf[1] = m->flags & Fdeleted ? 'd' : ' ';
	buf[2] = m->flags & Fstored? 's': ' ';
	buf[3] = m->flags & Fseen? ' ': '*';
	if(m->flags & Fanswered)
		buf[3] = 'a';
	if(m->flags & Fflagged)
		buf[3] = '\'';
	buf[4] = 0;
}

void
pheader0(char *buf, int len, Message *m)
{
	char *f, *p, *q, frombuf[40], timebuf[32], h[5];
	int max;

	hds(h, m);
	if(hcmdfmt == 0){
		f = m->from;
		max = longestfrom;
	}else{
		snprint(frombuf, sizeof frombuf-5, "%s", m->to);
		p = strchr(frombuf, ' ');
		if(p != nil)
			snprint(p, 5, " ...");
		f = frombuf;
		max = longestto;
		if(max > sizeof frombuf)
			max = sizeof frombuf;
	}

	if(*f == 0)
		snprint(buf, len, "%3D    %s %6d  %s",
			m, m->type, m->len, m->filename);
	else if(*m->subject){
		q = p = strdup(m->subject);
		while(*p == ' ')
			p++;
		if(strlen(p) > 50)
			p[50] = 0;
		cracktime(m->date, timebuf, sizeof timebuf);
		snprint(buf, len, "%3D %s %6d  %11.11s %-*.*s %s",
			m, h, m->len, timebuf, max, max, f, p);
		free(q);
	} else {
		cracktime(m->date, timebuf, sizeof timebuf);
		snprint(buf, len, "%3D %s %6d  %11.11s %s",
			m, h, m->len, timebuf, f);
	}
}

void
pheader(char *buf, int len, int indent, Message *m)
{
	char *p, *e, typeid[80];

	e = buf + len;
	snprint(typeid, sizeof typeid, "%D    %s", m, m->type);
	if(indent < 6)
		p = seprint(buf, e, "%-32s %-6d ", typeid, m->len);
	else
		p = seprint(buf, e, "%-64s %-6d ", typeid, m->len);
	if(m->filename && *m->filename)
		p = seprint(p, e, "(file,%s)", m->filename);
	if(m->from && *m->from)
		p = seprint(p, e, "(from,%s)", m->from);
	if(m->subject && *m->subject)
		seprint(p, e, "(subj,%s)", m->subject);
}

char sstring[256];

/*
 * 	cmd := range cmd ' ' arg-list ;
 * 	range := address
 * 		| address ',' address
 * 		| 'g' search ;
 * 	address := msgno
 * 		| search ;
 * 	msgno := number
 * 		| number '/' msgno ;
 * 	search := '/' string '/'
 * 		| '%' string '%'
 *		| '#' (field '#')? re '#'
 *
 */
static char*
qstrchr(char *s, int c)
{
	for(;; s++){
		if(*s == '\\')
			s++;
		else if(*s == c)
			return s;
		if(*s == 0)
			return nil;
	}
}

Reprog*
parsesearch(char **pp, char *buf, int bufl)
{
	char *p, *np, *e;
	int c, n;

	buf[0] = 0;
	p = *pp;
	c = *p++;
	if(c == '#')
		snprint(buf, bufl, "from");
	np = qstrchr(p, c);
	if(c == '#' && np)
	if(e = qstrchr(np + 1, c)){
		snprint(buf, bufl, "%.*s", (int)(np - p), p);
		p = np + 1;
		np = e;
	}
	if(np != nil){
		*np++ = 0;
		*pp = np;
	} else {
		n = strlen(p);
		*pp = p + n;
	}
	if(*p == 0)
		p = sstring;
	else{
		strncpy(sstring, p, sizeof sstring);
		sstring[sizeof sstring - 1] = 0;
	}
	return regcomp(p);
}

enum{
	Comma = 1,
};

/*
 *   search a message for a regular expression match
 */
int
fsearch(Message *m, Reprog *prog, char *field)
{
	char buf[4096 + 1];
	int i, fd, rv;
	uvlong o;

	rv = 0;
	fd = open(extendp(m->path, field), OREAD);
	/*
	 *  march through raw message 4096 bytes at a time
	 *  with a 128 byte overlap to chain the re search.
	 */
	for(o = 0;; o += i - 128){
		i = pread(fd, buf, sizeof buf - 1, o);
		if(i <= 0)
			break;
		buf[i] = 0;
		if(regexec(prog, buf, nil, 0)){
			rv = 1;
			break;
		}
		if(i < sizeof buf - 1)
			break;
	}
	close(fd);
	return rv;
}

int
rsearch(Message *m, Reprog *prog, char*)
{
	return fsearch(m, prog, "raw");
}

int
hsearch(Message *m, Reprog *prog, char*)
{
	char buf[256];

	pheader0(buf, sizeof buf, m);
	return regexec(prog, buf, nil, 0);
}

/*
 * ack: returns int (*)(Message*, Reprog*, char*)
 */
int (*
chartosearch(int c))(Message*, Reprog*, char*)
{
	switch(c){
	case '%':
		return rsearch;
	case '/':
	case '?':
		return hsearch;
	case '#':
		return fsearch;
	}
	return 0;
}

char*
parseaddr(char **pp, Message *first, Message *cur, Message *unspec, Message **mp, int f)
{
	char *p, buf[256];
	int n, c, sign;
	Message *m, *m0;
	Reprog *prog;
	int (*fn)(Message*, Reprog*, char*);

	*mp = nil;
	p = *pp;

	sign = 0;
	if(*p == '+'){
		sign = 1;
		p++;
		*pp = p;
	} else if(*p == '-'){
		sign = -1;
		p++;
		*pp = p;
	}

	switch(*p){
	default:
		if(sign){
			n = 1;
			goto number;
		}
		*mp = unspec;
		break;
	case '0': case '1': case '2': case '3': case '4':
	case '5': case '6': case '7': case '8': case '9':
		n = strtoul(p, pp, 10);
		if(n == 0){
			if(sign)
				*mp = cur;
			else
				*mp = &top;
			break;
		}
	number:
		m0 = m = nil;
		switch(sign){
		case 0:
			for(m = first; m != nil; m0 = m, m = m->next)
				if(m->id == n)
					break;
			break;
		case -1:
			if(cur != &top)
				for(m = cur; m0 = m, m != nil && n > 0; n--)
					m = m->prev;
			break;
		case 1:
			if(cur == &top){
				n--;
				cur = first;
			}
			for(m = cur; m != nil && n > 0; m0 = m, n--)
				m = m->next;
			break;
		}
		if(m == nil && f&Comma)
			m = m0;
		if(m == nil)
			return "address";
		*mp = m;
		break;
	case '?':
		/* legacy behavior.  no longer needed */
		sign = -1;
	case '%':
	case '/':
	case '#':
		c = *p;
		fn= chartosearch(c);
		prog = parsesearch(pp, buf, sizeof buf);
		if(prog == nil)
			return "badly formed regular expression";
		if(sign == -1){
			for(m = cur == &top ? nil : cur->prev; m; m = m->prev)
				if(fn(m, prog, buf))
					break;
		}else{
			for(m = cur == &top ? first : cur->next; m; m = m->next)
				if(fn(m, prog, buf))
					break;
		}
		if(m == nil)
			return "search";
		*mp = m;
		free(prog);
		break;
	case '$':
		for(m = first; m != nil && m->next != nil; m = m->next)
			;
		*mp = m;
		*pp = p + 1;
		break;
	case '.':
		*mp = cur;
		*pp = p + 1;
		break;
	case ',':
		*mp = first;
		*pp = p;
		break;
	}

	if(*mp != nil && **pp == '.'){
		(*pp)++;
		if((m = (*mp)->child) == nil)
			return "no sub parts";
		return parseaddr(pp, m, m, m, mp, 0);
	}
	c = **pp;
	if(c == '+' || c == '-' || c == '/' || c == '%' || c == '#')
		return parseaddr(pp, first, *mp, *mp, mp, 0);

	return nil;
}

char*
parsecmd(char *p, Cmd *cmd, Message *first, Message *cur)
{
	char buf[256], *err;
	int i, c, r;
	Reprog *prog;
	Message *m, *s, *e, **l, *last;
	int (*f)(Message*, Reprog*, char*);
	static char errbuf[ERRMAX];

	cmd->delete = 0;
	l = &cmd->msgs;
	*l = nil;

	while(*p == ' ' || *p == '\t')
		p++;

	/* null command is a special case (advance and print) */
	if(*p == 0){
		if(cur == &top)
			m = first;
		else {
			/* walk to the next message even if we have to go up */
			m = cur->next;
			while(m == nil && cur->parent != nil){
				cur = cur->parent;
				m = cur->next;
			}
		}
		if(m == nil)
			return "address";
		*l = m;
		m->cmd = nil;
		cmd->an = 0;
		cmd->f = pcmd;
		return nil;
	}

	/* global search ? */
	if(*p == 'g'){
		p++;

		/* no search string means all messages */
		if(*p == 'k'){
			for(m = first; m != nil; m = m->next)
			if(m->flags & Fflagged){
				*l = m;
				l = &m->cmd;
				*l = nil;
			}
			p++;
		}else if(*p != '/' && *p != '%' && *p != '#'){
			for(m = first; m != nil; m = m->next){
				*l = m;
				l = &m->cmd;
				*l = nil;
			}
		}else{
			/* mark all messages matching this search string */
			c = *p;
			f = chartosearch(c);
			prog = parsesearch(&p, buf, sizeof buf);
			if(prog == nil)
				return "badly formed regular expression";
			for(m = first; m != nil; m = m->next){
				if(f(m, prog, buf)){
					*l = m;
					l = &m->cmd;
					*l = nil;
				}
			}
			free(prog);
		}
	}else{
		/* parse an address */
		s = e = nil;
		err = parseaddr(&p, first, cur, cur, &s, 0);
		if(err != nil)
			return err;
		if(*p == ','){
			/* this is an address range */
			if(s == &top)
				s = first;
			p++;
			for(last = s; last != nil && last->next != nil; last = last->next)
				;
			err = parseaddr(&p, first, cur, last, &e, Comma);
			if(err != nil)
				return err;
			/* select all messages in the range */
			r = 0;
			if(s != nil && e != nil && s->id > e->id)
				r = 1;
			while(s != nil){
				*l = s;
				l = &s->cmd;
				*l = nil;
				if(s == e)
					break;
				if(r)
					s = s->prev;
				else
					s = s->next;
			}
			if(s == nil)
				return "null address range";
		} else {
			/* single address */
			if(s != &top){
				*l = s;
				s->cmd = nil;
			}
		}
	}

	while(*p == ' ' || *p == '\t')
		p++;
	/* hack to allow all messages to start with 'd' */
	if(*p == 'd' && p[1]){
		cmd->delete = 1;
		p++;
	}
	while(*p == ' ' || *p == '\t')
		p++;
	if(*p == 0)
		p = "p";
	for(i = nelem(cmdtab) - 1; i >= 0; i--)
		if(strncmp(p, cmdtab[i].cmd, strlen(cmdtab[i].cmd)) == 0)
			goto found;
	return "illegal command";
found:
	p += strlen(cmdtab[i].cmd);
	snprint(cmd->cmdline, sizeof cmd->cmdline, "%s", p);
	cmd->av[0] = cmdtab[i].cmd;
	cmd->an = 1 + tokenize(p, cmd->av + 1, nelem(cmd->av) - 2);
	if(cmdtab[i].args == 0 && cmd->an > 1){
		snprint(errbuf, sizeof errbuf, "%s doesn't take an argument", cmdtab[i].cmd);
		return errbuf;
	}
	cmd->f = cmdtab[i].f;

	if(cmdtab[i].addr && (cmd->msgs == nil || cmd->msgs == &top)){
		snprint(errbuf, sizeof errbuf, "%s requires an address", cmdtab[i].cmd);
		return errbuf;
 	}
	return nil;
}

Message*
aichcmd(Message *m, int indent)
{
	char hdr[256];

	pheader(hdr, sizeof hdr, indent, m);
	Bprint(&out, "%s\n", hdr);
	for(m = m->child; m != nil; m = m->next)
		aichcmd(m, indent + 1);
	return m;
}

Message*
Hcmd(Cmd*, Message *m)
{
	if(m == &top)
		return nil;
	return aichcmd(m, 0);
}

Message*
hcmd(Cmd*, Message *m)
{
	char hdr[256];

	if(m == &top)
		return nil;
	pheader0(hdr, sizeof hdr, m);
	Bprint(&out, "%s\n", hdr);
	return m;
}

Message*
bcmd(Cmd*, Message *m)
{
	int i;
	Message *om;

	om = m;
	if(m == &top)
		m = top.child;
	for(i = 0; i < 10 && m != nil; i++){
		hcmd(nil, m);
		om = m;
		m = m->next;
	}

	return m != nil? m: om;
}

Message*
ncmd(Cmd*, Message *m)
{
	if(m == &top)
		return m->child;
	return m->next;
}

int
writepart(char *m, char *part, char *s)
{
	char *e;
	int fd, n;

	fd = open(extendp(m, part), OWRITE);
	if(fd < 0){
		dissappeared();
		return -1;
	}
	for(e = s + strlen(s); e - s > 0; s += n){
		if((n = write(fd, s, e - s)) <= 0){
			eprint("!writepart:%s: %r\n", part);
			break;
		}
		if(interrupted)
			break;
	}
	close(fd);
	return s == e? 0: -1;
}

Message	*xpipecmd(Cmd*, Message*, char*);

Message*
printfmt(Message *m, char *part, char *cmd)
{
	Cmd c;

	c.an = 2;
	snprint(c.cmdline, sizeof c.cmdline, "%s", cmd);
	Bflush(&out);
	return xpipecmd(&c, m, part);
}

int
printpart0(Message *m, char *part)
{
	char buf[4096];
	int n, fd, tot;

	fd = open(extendp(m->path, part), OREAD);
	if(fd < 0){
		dissappeared();
		return 0;
	}
	tot = 0;
	while((n = read(fd, buf, sizeof buf)) > 0){
		if(interrupted)
			break;
		if(Bwrite(&out, buf, n) <= 0)
			break;
		tot += n;
	}
	close(fd);
	return tot;
}

int
printpart(Message *m, char *part, char *cmd)
{
	if(cmd == nil || cmd[0] == 0)
		return printpart0(m, part);
	printfmt(m, part, cmd);
	return 1;
}

int
printhtml(Message *m)
{
	Cmd c;

	memset(&c, 0, sizeof c);
	c.an = 3;
	snprint(c.cmdline, sizeof c.cmdline, "/bin/htmlfmt -l60 -cutf8");
	eprint("!/bin/htmlfmt\n");
	pipecmd(&c, m);
	return 0;
}

Message*
Pcmd(Cmd*, Message *m)
{
	if(m == &top)
		return &top;
	if(m->parent == &top)
		printpart(m, "unixheader", nil);
	printpart(m, "raw", nil);
	return m;
}

void
compress(char *p)
{
	char *np;
	int last;

	last = ' ';
	for(np = p; *p; p++){
		if(*p != ' ' || last != ' '){
			last = *p;
			*np++ = last;
		}
	}
	*np = 0;
}

void
setflags(Message *m, char *f)
{
	uchar f0;

	f0 = m->flags;
	txflags(f, &m->flags);
	if(f0 != m->flags)
		if((m->nflags & Nnoflags) == 0)
			writepart(m->path, "flags", f);
}

Message*
Fcmd(Cmd *c, Message *m)
{
	int i;

	for(i = 1; i < c->an; i++)
		setflags(m, c->av[i]);
	return m;
}

void
seen(Message *m)
{
	setflags(m, "s");
}

/*
 * sleeze
 */
int
magicpart(Message *m, char *s, char *part)
{
	char buf[4096];
	int n, fd, c;

	fd = open(extendp(s, part), OREAD);
	if(fd < 0){
		if(strcmp(part, "id") == 0)
			Bprint(&out, "%D ", m);
		else if(strcmp(part, "fpath") == 0)
			Bprint(&out, "%s ", rooted(m->path));
		else
			Bprint(&out, "%s ", part);
		return 0;
	}

	c = 0;
	while((n = read(fd, buf, sizeof buf)) > 0){
		c = -1;
		if(interrupted)
			break;
		if(Bwrite(&out, buf, n) <= 0)
			break;
		c = buf[n - 1];
	}
	close(fd);
	if(!interrupted && n != -1 && c != -1)
	if(strstr(part, "body") != nil || strcmp(part, "rawunix") == 0)
		seen(m);
	return c;
}

Message*
pcmd0(Cmd *c, Message *m, int mayplumb, char *tfmt)
{
	char *s, buf[128];
	int i, ch;
	Ctype *cp;
	Message *nm;

	if(m == &top)
		return &top;
	if(c && c->an >= 2){
		ch = 0;
		for(i = 1; i < c->an; i++)
			ch = magicpart(m, m->path, c->av[i]);
		if(ch != '\n')
			Bprint(&out, "\n");
		return m;
	}
	if(m->parent == &top){
		seen(m);
		printpart(m, "unixheader", nil);
	}
	if(printpart(m, "header", nil) > 0)
		Bprint(&out, "\n");
	cp = findctype(m);
	if(cp->flag & Display){
		if(strcmp(m->type, "text/html") == 0)
			printhtml(m);
		else
			printpart(m, "body", tfmt);
	}else if(strcmp(m->type, "multipart/alternative") == 0){
		for(nm = m->child; nm != nil; nm = nm->next){
			cp = findctype(nm);
			if(cp->ext != nil && strncmp(cp->ext, "txt", 3) == 0)
				break;
		}
		if(nm == nil)
			for(nm = m->child; nm != nil; nm = nm->next){
				cp = findctype(nm);
				if(cp->flag & Display)
					break;
			}
		if(nm != nil)
			pcmd0(nil, nm, mayplumb, tfmt);
		else
			hcmd(nil, m);
	}else if(strncmp(m->type, "multipart/", 10) == 0){
		nm = m->child;
		if(nm != nil){
			/* always print first part */
			pcmd0(nil, nm, mayplumb, tfmt);

			for(nm = nm->next; nm != nil; nm = nm->next){
				s = rooted(nm->path);
				cp = findctype(nm);
				pheader(buf, sizeof buf, -1, nm);
				compress(buf);
				if(strcmp(nm->disposition, "inline") == 0){
					if(cp->ext != nil)
						Bprint(&out, "\n--- %s %s/body.%s\n\n",
							buf, s, cp->ext);
					else
						Bprint(&out, "\n--- %s %s/body\n\n",
							buf, s);
					pcmd0(nil, nm, 0, tfmt);
				} else {
					if(cp->ext != nil)
						Bprint(&out, "\n!--- %s %s/body.%s\n",
							buf, s, cp->ext);
					else
						Bprint(&out, "\n!--- %s %s/body\n",
							buf, s);
				}
			}
		} else {
			hcmd(nil, m);
		}
	}else if(strcmp(m->type, "message/rfc822") == 0)
		pcmd(nil, m->child);
	else if(!mayplumb){
	}else if(plumb(m, cp) >= 0){
		Bprint(&out, "\n!--- using plumber to type %s", cp->type);
		if(strcmp(cp->type, m->type) != 0)
			Bprint(&out, " (was %s)", m->type);
		Bprint(&out, "\n");
	}else
		Bprint(&out, "\n!--- cannot display %s\n", cp->type);

	return m;
}

Message*
pcmd(Cmd *c, Message *m)
{
	return pcmd0(c, m, 1, textfmt);
}

Message*
tcmd(Cmd *c, Message *m)
{
	switch(c->an){
	case 1:
		if(textfmt[0] != 0)
			textfmt[0] = 0;
		else
			snprint(textfmt, sizeof textfmt, "%s", "upas/tfmt");
		break;
	default:
		snprint(textfmt, sizeof textfmt, "%s", c->cmdline);
		break;
	}
	eprint("!textfmt %s\n", textfmt);
	return m;
}

void
printpartindented(char *s, char *part, char *indent)
{
	char *p;
	Biobuf *b;

	b = Bopen(extendp(s, part), OREAD);
	if(b == nil){
		dissappeared();
		return;
	}
	while((p = Brdline(b, '\n')) != nil){
		if(interrupted)
			break;
		p[Blinelen(b)-1] = 0;
		if(Bprint(&out, "%s%s\n", indent, p) < 0)
			break;
	}
	Bprint(&out, "\n");
	Bterm(b);
}

void
printpartindent2(char *s, char *part, char *indent)
{
	Cmd c;

	memset(&c, 0, sizeof c);
	snprint(c.cmdline, sizeof c.cmdline, "fmt -q '> ' %s | sed 's/^/%s/g' ",
		rooted(extendp(s, part)), indent);
	Bflush(&out);
	bangcmd(&c, nil);
}

Message*
quotecmd0(Cmd *c, Message *m, void (*p)(char*, char*, char*))
{
	Ctype *cp;
	Message *nm;

	if(m == &top)
		return &top;
	Bprint(&out, "\n");
	if(m->from != nil && *m->from)
		Bprint(&out, "On %s, %s wrote:\n", m->date, m->from);
	cp = findctype(m);
	if(cp->flag & Display)
		p(m->path, "body", "> ");
	else if(strcmp(m->type, "multipart/alternative") == 0){
		for(nm = m->child; nm != nil; nm = nm->next){
			cp = findctype(nm);
			if(cp->ext != nil && strncmp(cp->ext, "txt", 3) == 0)
				break;
		}
		if(nm == nil)
			for(nm = m->child; nm != nil; nm = nm->next){
				cp = findctype(nm);
				if(cp->flag & Display)
					break;
			}
		if(nm != nil)
			quotecmd(c, nm);
	}else if(strncmp(m->type, "multipart/", 10) == 0){
		nm = m->child;
		if(nm != nil){
			cp = findctype(nm);
			if(cp->flag & Display || strncmp(m->type, "multipart/", 10) == 0)
				quotecmd(c, nm);
		}
	}
	return m;
}

Message*
quotecmd(Cmd *c, Message *m)
{
	void (*p)(char*, char*, char*);

	p = printpartindented;
	if(strstr(c->av[0], "\"\"") != nil)
		p = printpartindent2;
	return quotecmd0(c, m, p);
}


/* really delete messages */
Message*
flushdeleted(Message *cur)
{
	char buf[1024], *p, *e, *msg;
	int i, deld, n, fd;
	Message *m, **l;

	doflush = 0;
	deld = 0;

	fd = open("/mail/fs/ctl", ORDWR);
	if(fd < 0){
		eprint("!can't delete mail, opening /mail/fs/ctl: %r\n");
		exitfs(0);
	}
	e = buf + sizeof buf;
	p = seprint(buf, e, "delete %s", mbname);
	n = 0;
	for(l = &top.child; *l != nil;){
		m = *l;
		if((m->nflags & Nmissing) == 0)
		if((m->flags & Fdeleted) == 0){
			l = &(*l)->next;
			continue;
		}

		/* don't return a pointer to a deleted message */
		if(m == cur)
			cur = m->next;
		deld++;
		if(m->flags & Fdeleted){
			msg = strrchr(m->path, '/');
			if(msg == nil)
				msg = m->path;
			else
				msg++;
			if(e - p < 10){
				write(fd, buf, p - buf);
				n = 0;
				p = seprint(buf, e, "delete %s", mbname);
			}
			p = seprint(p, e, " %s", msg);
			n++;
		}
		/* unchain and free */
		*l = m->next;
		if(m->next)
			m->next->prev = m->prev;
		freemessage(m);
	}
	if(n)
		write(fd, buf, p - buf);

	close(fd);

	if(deld)
		Bprint(&out, "!%d message%s deleted\n", deld, plural(deld));

	/* renumber */
	i = 1;
	for(m = top.child; m != nil; m = m->next)
		m->id = natural ? m->fileno : i++;

	/*
	 *  if we're out of messages, go back to first
	 *  if no first, return the fake first
	 */
	if(cur == nil){
		if(top.child)
			return top.child;
		else
			return &top;
	}
	return cur;
}

Message*
mbcmd(Cmd *c, Message*)
{
	char *mb, oldmb[Pathlen];
	Message *m, **l;

	switch(c->an){
	case 1:
		mb = "mbox";
		break;
	case 2:
		mb = c->av[1];
		break;
	default:
		eprint("!usage: mbcmd [mbox]\n");
		return nil;	
	}

	/* flushdeleted(nil); ? */
	for(l = &top.child; *l; ){
		m = *l;
		*l = m->next;
		freemessage(m);
	}
	top.child = nil;

	strcpy(oldmb, mbpath);
	if(switchmb(mb, 0) < 0){
		eprint("!no mb\n");
		if(switchmb(oldmb, 0) < 0){
			eprint("!mb disappeared\n");
			exits("fail");
		}
	}
	icmd(nil, nil);
	interrupted = 1;	/* no looping */
	return &top;
}

Message*
qcmd(Cmd*, Message*)
{
	flushdeleted(nil);
	if(didopen)
		closemb();
	Bflush(&out);
	exitfs(0);
	return nil;
}

Message*
ycmd(Cmd *c, Message *m)
{
	doflush = 1;
	return icmd(c, m);
}

Message*
xcmd(Cmd*, Message*)
{
	exitfs(0);
	return nil;
}

Message*
eqcmd(Cmd*, Message *m)
{
	Bprint(&out, "%D\n", m);
	return m;
}

Message*
dcmd(Cmd*, Message *m)
{
	while(m->parent != &top)
		m = m->parent;
	m->flags |= Fdeleted;
	return m;
}

Message*
ucmd(Cmd*, Message *m)
{
	if(m == &top)
		return nil;
	while(m->parent != &top)
		m = m->parent;
	m->flags &= ~Fdeleted;
	return m;
}

int
skipscan(void)
{
	int r;
	Dir *d;
	static int lastvers = -1;

	d = dirstat(top.path);
	r = d && d->qid.path == mbqid.path && d->qid.vers == mbqid.vers;
	r = r && mbvers == lastvers;
	if(d != nil){
		mbqid = d->qid;
		lastvers = mbvers;
	}
	free(d);
	return r;
}

Message*
icmd(Cmd *c, Message *m)
{
	char buf[128], *p, *e;
	Dirstats s;

	if(skipscan())
		return m;
	if(dir2message(&top, reverse, &s) < 0)
		return nil;
	p = buf;
	e = buf + sizeof buf;
	if(s.new > 0 && c == nil){
		p = seprint(p, e, "%d message%s", s.new, plural(s.new));
		if(s.unread > 0)
			p = seprint(p, e, ", %d unread", s.unread);
	}
	else if(s.new > 0)
		Bprint(&out, "%d new message%s", s.new, plural(s.new));
	if(s.new && s.del)
		p = seprint(p, e, "; ");
	if(s.del > 0)
		p = seprint(p, e, "%d deleted message%s", s.del, plural(s.del));
	if(s.new + s.del)
		p = seprint(p, e, "\n");
	if(p > buf){
		Bflush(&out);
		eprint("%s", buf);
	}
	return m;
}

Message*
kcmd0(Cmd *c, Message *m)
{
	char *f, *s;
	int sticky;

	if(c->an > 2){
		eprint("!usage k [flags]\n");
		return nil;
	}
	if(c->f == kcmd)
		f = "f";
	else
		f = "-f";
	if(c->an == 2)
		f = c->av[1];
	setflags(m, f);
	if(c->an == 2 && (m->nflags & Nnoflags) == 0){
		sticky = m->flags & Fdeleted;
		s = file2string(m->path, "flags");
		m->flags = buftoflags(s) | sticky;
		free(s);
	}
	return m;
}

Message*
kcmd(Cmd *c, Message *m)
{
	return kcmd0(c, m);
}

Message*
Kcmd(Cmd *c, Message *m)
{
	return kcmd0(c, m);
}

Message*
helpcmd(Cmd*, Message *m)
{
	int i;

	Bprint(&out, "Commands are of the form [<range>] <command> [args]\n");
	Bprint(&out, "<range> := <addr> | <addr>','<addr>| 'g'<search>\n");
	Bprint(&out, "<addr> := '.' | '$' | '^' | <number> | <search> | <addr>'+'<addr> | <addr>'-'<addr>\n");
	Bprint(&out, "<search> := 'k' | '/'<re>'/' | '?'<re>'?' | '%%'<re>'%%' | '#' <field> '#' <re> '#' \n");
	Bprint(&out, "<command> :=\n");
	for(i = 0; i < nelem(cmdtab); i++)
		Bprint(&out, "%s\n", cmdtab[i].help);
	return m;
}

/* ed thinks this is a good idea */
void
marshal(char **path, char **argv0)
{
	char *s;

	s = getenv("marshal");
	if(s == nil || *s == 0)
		s = "/bin/upas/marshal";
	*path = s;
	*argv0 = strrchr(s, '/') + 1;
	if(*argv0 == (char*)1)
		*argv0 = s;
}

int
tomailer(char **av)
{
	int pid, i;
	char *p, *a;
	Waitmsg *w;

	switch(pid = fork()){
	case -1:
		eprint("can't fork: %r\n");
		return -1;
	case 0:
		marshal(&p, &a);
		Bprint(&out, "!%s", p);
		for(i = 1; av[i]; i++)
			Bprint(&out, " %q", av[i]);
		Bprint(&out, "\n");
		Bflush(&out);
		av[0] = a;
		chdir(wd);
		exec(p, av);
		eprint("couldn't exec %s\n", p);
		exits(0);
	default:
		w = wait();
		if(w == nil){
			if(interrupted)
				postnote(PNPROC, pid, "die");
			waitpid();
			return -1;
		}
		if(w->msg[0]){
			eprint("mailer failed: %s\n", w->msg);
			free(w);
			return -1;
		}
		free(w);
//		Bprint(&out, "!\n");
		break;
	}
	return 0;
}

/*
 *  like tokenize but obey "" quoting
 */
int
tokenize822(char *str, char **args, int max)
{
	int na, intok, inquote;

	if(max <= 0)
		return 0;
	intok = inquote = 0;
	for(na=0; ;str++)
		switch(*str) {
		case ' ':
		case '\t':
			if(inquote)
				goto Default;
			/* fall through */
		case '\n':
			*str = 0;
			if(!intok)
				continue;
			intok = 0;
			if(na < max)
				continue;
			/* fall through */
		case 0:
			return na;
		case '"':
			inquote ^= 1;
			/* fall through */
		Default:
		default:
			if(intok)
				continue;
			args[na++] = str;
			intok = 1;
		}
}

static char *rec[] = {"Re: ", "AW:", };
static char *fwc[] = {"Fwd: ", };

char*
addrecolon(char **tab, int n, char *s)
{
	char *prefix;
	int i;

	prefix = "";
	for(i = 0; i < n; i++)
		if(cistrncmp(s, tab[i], strlen(tab[i]) - 1) == 0)
			break;
	if(i == n)
		prefix = tab[0];
	return smprint("%s%s", prefix, s);
}

Message*
rcmd(Cmd *c, Message *m)
{
	char *from, *path, *subject, *rpath, *addr, *av[128];
	int i, ai;
	Message *nm;

	ai = 1;
	av[ai++] = "-8";
	addr = path = subject = nil;
	for(nm = m; nm != &top; nm = nm->parent)
 		if(*nm->replyto != 0){
			addr = nm->replyto;
			break;
		}
	if(addr == nil){
		eprint("!no reply address\n");
		return nil;
	}

	if(nm == &top){
		print("!noone to reply to\n");
		return nil;
	}

	for(nm = m; nm != &top; nm = nm->parent)
		if(*nm->subject){
			av[ai++] = "-s";
			subject = addrecolon(rec, nelem(rec), nm->subject);
			av[ai++] = subject;
			break;
		}

	av[ai++] = "-R";
	av[ai++] = rpath = strdup(rooted(m->path));

	if(strchr(c->av[0], 'f') != nil){
		fcmd(c, m);
		av[ai++] = "-F";
	}

	if(strchr(c->av[0], 'R') != nil){
		av[ai++] = "-t";
		av[ai++] = "message/rfc822";
		av[ai++] = "-A";
		path = strdup(rooted(extendp(m->path, "raw")));
		av[ai++] = path;
	}

	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
		av[ai++] = c->av[i];
	ai += tokenize822(from = strdup(addr), &av[ai], nelem(av) - ai);
	av[ai] = 0;
	if(tomailer(av) == -1)
		m = nil;
	else
		m->flags |= Fanswered;
	free(path);
	free(rpath);
	free(subject);
	free(from);
	return m;
}

Message*
mcmd(Cmd *c, Message *m)
{
	char *subject, *av[128];
	int i, ai;

	if(c->an < 2){
		eprint("!usage: M list-of addresses\n");
		return nil;
	}

	ai = 1;
	subject = nil;
	if(m->subject){
		av[ai++] = "-s";
		subject = addrecolon(fwc, nelem(fwc), m->subject);
		av[ai++] = subject;
	}

	av[ai++] = "-t";
	if(m->parent == &top)
		av[ai++] = "message/rfc822";
	else
		av[ai++] = "mime";

	av[ai++] = "-A";
	av[ai++] = rooted(extendp(m->path, "raw"));
	if(strchr(c->av[0], 'M') == nil)
		av[ai++] = "-n";
	else
		av[ai++] = "-8";
	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
		av[ai++] = c->av[i];
	av[ai] = 0;

	if(tomailer(av) == -1)
		m = nil;
	else
		m->flags |= Fanswered;
	free(subject);
	return m;
}

Message*
acmd(Cmd *c, Message *m)
{
	char *av[128], *rpath, *subject, *from, *to, *cc;
	int i, ai;

	if(m->from == nil || m->to == nil || m->cc == nil){
		eprint("!bad message\n");
		return nil;
	}

	ai = 1;
	av[ai++] = "-8";
	av[ai++] = "-R";
	av[ai++] = rpath = strdup(rooted(m->path));

	subject = nil;
	if(m->subject && *m->subject){
		av[ai++] = "-s";
		subject = addrecolon(rec, nelem(rec), m->subject);
		av[ai++] = subject;
	}

	if(strchr(c->av[0], 'A') != nil){
		av[ai++] = "-t";
		av[ai++] = "message/rfc822";
		av[ai++] = "-A";
		av[ai++] = rooted(extendp(m->path, "raw"));
	}

	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
		av[ai++] = c->av[i];
	ai += tokenize822(from = strdup(m->from), &av[ai], nelem(av) - ai);
	ai += tokenize822(to = strdup(m->to), &av[ai], nelem(av) - ai);
	ai += tokenize822(cc = strdup(m->cc), &av[ai], nelem(av) - ai);
	av[ai] = 0;
	if(tomailer(av) == -1)
		m = nil;
	else
		m->flags |= Fanswered;
	free(from);
	free(to);
	free(cc);
	free(subject);
	free(rpath);
	return m;
}

int
appendtofile(Message *m, char *part, char *base, int mbox, int f)
{
	char *folder, path[Pathlen];
	int in, rv, rp;

	in = open(extendp(m->path, part), OREAD);
	if(in == -1){
		dissappeared();
		return -1;
	}
	rp = 0;
	if(*base == '/')
		folder = base;
	else if(!mbox){
		snprint(path, sizeof path, "%s/%s", wd, base);
		folder = path;
		rp = 1;
	}else if(f)
		folder = ffoldername(mbpath, user, base);
	else
		folder = foldername(mbpath, user, base);
	if(folder == nil)
		return -1;
	if(mbox)
		rv = fappendfolder(0, 0, folder, in);
	else
		rv = fappendfile(m->from, folder, in);
	close(in);
	if(rv >= 0){
		eprint("!saved in %s\n", rp? base: folder);
		setflags(m, "S");
	}else
		eprint("!error %r\n");
	return rv;
}

Message*
scmd(Cmd *c, Message *m)
{
	char *file;

	switch(c->an){
	case 1:
		file = "stored";
		break;
	case 2:
		file = c->av[1];
		break;
	default:
		eprint("!usage: s filename\n");
		return nil;
	}

	if(appendtofile(m, "rawunix", file, 1, 0) < 0)
		return nil;
	return m;
}

Message*
wcmd(Cmd *c, Message *m)
{
	char *file;

	switch(c->an){
	case 2:
		file = c->av[1];
		break;
	case 1:
		if(*m->filename == 0){
			eprint("!usage: w filename\n");
			return nil;
		}
		file = strrchr(m->filename, '/');
		if(file != nil)
			file++;
		else
			file = m->filename;
		break;
	default:
		eprint("!usage: w filename\n");
		return nil;
	}

	if(appendtofile(m, "body", file, 0, 0) < 0)
		return nil;
	return m;
}

typedef struct Xtab Xtab;
struct Xtab {
	char	*a;
	char	*b;
};
Xtab	*xtab;
int	nxtab;

void
loadxfrom(int fd)
{
	char *f[3], *s, *p;
	int n, a, inc;
	Biobuf b;
	Xtab *x;

	Binit(&b, fd, OREAD);
	a = 0;
	inc = 100;
	for(; s = Brdstr(&b, '\n', 1);){
		if(p = strchr(s, '#'))
			*p = 0;
		n = tokenize(s, f, nelem(f));
		if(n != 2){
			free(s);
			continue;
		}
		if(nxtab == a){
			a += inc;
			xtab = realloc(xtab, a*sizeof *xtab);
			if(xtab == nil)
				sysfatal("realloc: %r");
			inc *= 2;
		}
		for(x = xtab+nxtab; x > xtab && strcmp(x[-1].a, f[0]) > 0; x--)
			x[0] = x[-1];
		x->a = f[0];
		x->b = f[1];
		nxtab++;
	}
}

char*
frombox(char *from)
{
	char *s;
	int n, m, fd;
	Xtab *t, *p;
	static int once;

	if(once == 0){
		once = 1;
		s = foldername(mbpath, user, "fromtab-");
		fd = open(s, OREAD);
		if(fd != -1)
			loadxfrom(fd);
		close(fd);
	}
	t = xtab;
	n = nxtab;
	while(n > 1) {
		m = n/2;
		p = t + m;
		if(strcmp(from, p->a) > 0){
			t = p;
			n = n - m;
		} else
			n = m;
	}
	if(n && strcmp(from, t->a) == 0)
		return t->b;
	return from;
}

Message*
fcmd(Cmd*, Message *m)
{
	char *f;

	f = frombox(m->from);
	if(appendtofile(m, "rawunix", f, 1, 1) < 0)
		return nil;
	return m;
}

Message*
fqcmd(Cmd*, Message *m)
{
	char *f;

	f = frombox(m->from);
	Bprint(&out, "! %s\n", f);
	return m;
}

void
system(char *cmd, char **av, int in)
{
	int pid;

	switch(pid=fork()){
	case -1:
		return;
	case 0:
		if(in >= 0){
			close(0);
			dup(in, 0);
			close(in);
		}
		if(wd[0] != 0)
			chdir(wd);
		exec(cmd, av);
		eprint("!couldn't exec %s\n", cmd);
		exits(0);
	default:
		if(in >= 0)
			close(in);
		while(waitpid() < 0){
			if(!interrupted)
				break;
			postnote(PNPROC, pid, "die");
			continue;
		}
		break;
	}
}

Message*
bangcmd(Cmd *c, Message *m)
{
	char *av[4];

	av[0] = "rc";
	av[1] = "-c";
	av[2] = c->cmdline;
	av[3] = 0;
	system("/bin/rc", av, -1);
//	Bprint(&out, "!\n");
	return m;
}

Message*
xpipecmd(Cmd *c, Message *m, char *part)
{
	char *av[4];
	int fd;

	if(c->an < 2){
		eprint("!usage: | cmd\n");
		return nil;
	}

	fd = open(extendp(m->path, part), OREAD);
	if(fd < 0){
		dissappeared();
		return nil;
	}

	av[0] = "rc";
	av[1] = "-c";
	av[2] = c->cmdline;
	av[3] = 0;
	system("/bin/rc", av, fd);	/* system closes fd */
//	Bprint(&out, "!\n");
	return m;
}

Message*
pipecmd(Cmd *c, Message *m)
{
	return xpipecmd(c, m, "body");
}

Message*
rpipecmd(Cmd *c, Message *m)
{
	return xpipecmd(c, m, "rawunix");
}

void
closemb(void)
{
	int fd;

	fd = open("/mail/fs/ctl", ORDWR);
	if(fd < 0)
		sysfatal("can't open /mail/fs/ctl: %r");

	/* close current mailbox */
	if(*mbname && strcmp(mbname, "mbox") != 0)
	if(fprint(fd, "close %q", mbname) == -1)
		eprint("!close %q: %r", mbname);

	close(fd);
}

static char*
chop(char *s, int c)
{
	char *p;

	p = strrchr(s, c);
	if(p != nil && p > s) {
		*p = 0;
		return p - 1;
	}
	return 0;
}

/* sometimes opens the file (or open mbox) intended. */
int
switchmb(char *mb, int singleton)
{
	char *p, *e, pbuf[Pathlen], buf[Pathlen], file[Pathlen];
	int fd, abs;

	closemb();
	abs = 0;
	if(mb == nil)
		mb = "mbox";
	if(strcmp(mb, ".") == 0)	/* botch */
		mb = homewd;
	if(*mb == '/' || strncmp(mb, "./", 2) == 0 || strncmp(mb, "../", 3) == 0){
		snprint(file, sizeof file, "%s", mb);
		abs = 1;
	}else
		snprint(file, sizeof file, "/mail/fs/%s", mb);
	if(singleton){
		if(chop(file, '/') == nil || (p = strrchr(file, '/')) == nil || p - file < 2){
			eprint("!bad mbox name\n");
			return -1;
		}
		mboxpathbuf(pbuf, sizeof pbuf, user, "mbox");
		snprint(mbname, sizeof mbname, "%s", p + 1);
	}else if(abs || access(file, 0) < 0){
		fd = open("/mail/fs/ctl", ORDWR);
		if(fd < 0)
			sysfatal("can't open /mail/fs/ctl: %r");
		p = pbuf;
		e = pbuf + sizeof pbuf;
		if(abs && *file != '/')
			seprint(p, e, "%s/%s", getwd(buf, sizeof buf), mb);
		else if(abs)
			seprint(p, e, "%s", mb);
		else
			mboxpathbuf(pbuf, sizeof pbuf, user, mb);
		/* make up a handle to use when talking to fs */
		if((p = strrchr(mb, '/')) == nil)
			p = mb - 1;
		snprint(mbname, sizeof mbname, "%s%ld", p + 1, time(0));
		if(fprint(fd, "open %q %q", pbuf, mbname) < 0){
			eprint("!can't open %q %q: %r\n", pbuf, mbname);
			return -1;
		}
		close(fd);
		didopen = 1;
	}else{
		mboxpathbuf(pbuf, sizeof pbuf, user, mb);
		strcpy(mbname, mb);
	}

	snprint(root, sizeof root, "/mail/fs/%s", mbname);
	if(getwd(wd, sizeof wd) == nil)
		wd[0] = 0;
	if(!singleton && chdir(root) >= 0)
		strcpy(root, ".");
	rootlen = strlen(root);
	snprint(mbpath, sizeof mbpath, "%s", pbuf);
	memset(&mbqid, 0, sizeof mbqid);
	mbvers++;

	return 0;
}

char*
rooted(char *s)
{
	static char buf[Pathlen];

	if(strcmp(root, ".") != 0)
		return s;
	snprint(buf, sizeof buf, "/mail/fs/%s/%s", mbname, s);
	return buf;
}

int
plumb(Message *m, Ctype *cp)
{
	char *s;
	Plumbmsg *pm;
	static int fd = -2;

	if(cp->plumbdest == nil)
		return -1;
	if(fd < -1)
		fd = plumbopen("send", OWRITE);
	if(fd < 0)
		return -1;

	pm = mallocz(sizeof *pm, 1);
	pm->src = strdup("mail");
	if(*cp->plumbdest)
		pm->dst = strdup(cp->plumbdest);
	pm->type = strdup("text");
	pm->ndata = -1;
	s = rooted(extendp(m->path, "body"));
	if(cp->ext != nil)
		pm->data  = smprint("%s.%s", s, cp->ext);
	else
		pm->data  = strdup(s);
	plumbsend(fd, pm);
	plumbfree(pm);
	return 0;
}

void
regerror(char*)
{
}

void
exitfs(char *rv)
{
	if(startedfs)
		unmount(nil, "/mail/fs");
	chdir(homewd);			/* prof */
	exits(rv);
}