shithub: riscv

ref: 985b2457cda207c2c65edeb647aedbb6e92dac46
dir: /sys/src/cmd/upas/ned/nedmail.c/

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

typedef struct Message Message;
typedef struct Ctype Ctype;
typedef struct Cmd Cmd;

char	root[Pathlen];
char	mbname[Elemlen];
int	rootlen;
int	didopen;
char	*user;
char	wd[2048];
String	*mbpath;
int	natural;
int	doflush;

int interrupted;

struct Message {
	Message	*next;
	Message	*prev;
	Message	*cmd;
	Message	*child;
	Message	*parent;
	String	*path;
	int	id;
	int	len;
	int	fileno;	// number of directory
	String	*info;
	char	*from;
	char	*to;
	char	*cc;
	char	*replyto;
	char	*date;
	char	*subject;
	char	*type;
	char	*disposition;
	char	*filename;
	char	deleted;
	char	stored;
};

Message top;

struct Ctype {
	char	*type;
	char 	*ext;
	int	display;
	char	*plumbdest;
	Ctype	*next;
};

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	},
	{ "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"	},
	{ "application/pdf",		"pdf",	0,	"postscript"	},
	{ "application/postscript",	"ps",	0,	"postscript"	},
	{ "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 {
	char		*cmd;
	int		args;
	Message*	(*f)(Cmd*, Message*);
	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 },
};

enum
{
	NARG=	32,
};

struct Cmd {
	Message	*msgs;
	Message	*(*f)(Cmd*, Message*);
	int	an;
	char	*av[NARG];
	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**);
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*);
void		closemb(void);
int		lineize(char*, char**, int);
int		rawsearch(Message*, Reprog*);
Message*	dosingleton(Message*, char*);
String*		rooted(String*);
int		plumb(Message*, Ctype*);
String*		addrecolon(char*);
void		exitfs(char*);
Message*	flushdeleted(Message*);

void
usage(void)
{
	fprint(2, "usage: %s [-nr] [-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)
{
	Message *cur, *m, *x;
	char cmdline[4*1024];
	Cmd cmd;
	Ctype *cp;
	char *err;
	int n, cflag;
	char *av[4];
	String *prompt;
	char *file, *singleton;

	Binit(&out, 1, OWRITE);

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

	user = getlog();
	if(user == nil || *user == 0)
		sysfatal("can't read user name");

	if(cflag){
		if(argc > 0)
			creatembox(user, argv[0]);
		else
			creatembox(user, nil);
		exits(0);
	}

	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(file, singleton);

	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(cur == nil){
			Bprint(&out, "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));
	}


	notify(catchnote);
	prompt = s_new();
	for(;;){
		s_reset(prompt);
		if(cur == &top)
			s_append(prompt, ": ");
		else {
			mkid(prompt, cur);
			s_append(prompt, ": ");
		}

		// 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)
			break;
		err = parsecmd(cmdline, &cmd, top.child, cur);
		if(err != nil){
			Bprint(&out, "!%s\n", err);
			continue;
		}
		if(singleton != nil && cmd.f == icmd){
			Bprint(&out, "!illegal command\n");
			continue;
		}
		interrupted = 0;
		if(cmd.msgs == nil || cmd.msgs == &top){
			x = (*cmd.f)(&cmd, &top);
			if(x != nil)
				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){
						Bprint(&out, "!address\n");
						cur = x;
						break;
					} else
						x = x->next;
				}
			}
			x = (*cmd.f)(&cmd, x);
			if(x != nil)
				cur = x;
			if(interrupted)
				break;
			if(singleton != nil && (cmd.delete || cmd.f == dcmd))
				qcmd(nil, nil);
		}
		if(doflush)
			cur = flushdeleted(cur);
	}
	qcmd(nil, nil);
}

//
// read the message info
//
Message*
file2message(Message *parent, char *name)
{
	Message *m;
	String *path;
	char *f[10];

	m = mallocz(sizeof(Message), 1);
	if(m == nil)
		return nil;
	m->path = path = extendpath(parent->path, name);
	m->fileno = atoi(name);
	m->info = file2string(path, "info");
	lineize(s_to_c(m->info), f, nelem(f));
	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 = filelen(path, "raw");
	if(strstr(m->type, "multipart") != nil || strcmp(m->type, "message/rfc822") == 0)
		dir2message(m, 0);
	m->parent = parent;

	return m;
}

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

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

//
//  read a directory into a list of messages
//
int
dir2message(Message *parent, int reverse)
{
	int i, n, fd, highest, newmsgs;
	Dir *d;
	Message *first, *last, *m;

	fd = open(s_to_c(parent->path), OREAD);
	if(fd < 0)
		return -1;

	// 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;

	n = dirreadall(fd, &d);
	for(i = 0; i < n; i++){
		if((d[i].qid.type & QTDIR) == 0)
			continue;
		if(atoi(d[i].name) <= highest)
			continue;
		m = file2message(parent, d[i].name);
		if(m == nil)
			break;
		newmsgs++;
		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;
		}
	}
	free(d);
	close(fd);
	parent->child = first;

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

	return newmsgs;
}

//
//  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, s_to_c(m->path)) == 0)
				return m;
		if(m == nil)
			return nil;
	}
	return m;
}

//
//  read a file into a string
//
String*
file2string(String *dir, char *file)
{
	String *s;
	int fd, n, m;

	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;
}

//
//  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)
{
	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];
	char *f[6];
	int n;
	Tm tm;
	long now, then;
	char *dtime;

	*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);
}

Ctype*
findctype(Message *m)
{
	char *p;
	char ftype[128];
	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;

/*	use file(1) for any unknown mimetypes
 *
 *	if (strcmp(m->type, bintype.type) != 0)
 *		return &nulltype;
 */
	if(pipe(pfd) < 0)
		return &bintype;

	*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", s_to_c(extendpath(m->path, "body")), nil);
		exits(0);
	default:
		close(pfd[0]);
		n = read(pfd[1], ftype, sizeof(ftype));
		if(n > 0)
			ftype[n] = 0;
		close(pfd[1]);
		waitpid();
		break;
	}

	if (*ftype=='\0' || (p = strchr(ftype, '/')) == nil)
		return &bintype;
	*p++ = 0;

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

void
mkid(String *s, 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);
}

void
snprintheader(char *buf, int len, Message *m)
{
	char timebuf[32];
	String *id;
	char *p, *q;

	// create id
	id = s_new();
	mkid(id, m);

	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){
		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);
		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);
	}
	s_free(id);
}

char *spaces = "                                                                    ";

void
snprintHeader(char *buf, int len, int indent, Message *m)
{
	String *id;
	char typeid[64];
	char *p, *e;

	// create id
	id = s_new();
	mkid(id, m);

	e = buf + len;

	snprint(typeid, sizeof typeid, "%s    %s", s_to_c(id), 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);

	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 '%' ;
//
Reprog*
parsesearch(char **pp)
{
	char *p, *np;
	int c, n;

	p = *pp;
	c = *p++;
	np = strchr(p, c);
	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);
}

static char *
num2msg(Message **mp, int sign, int n, Message *first, Message *cur)
{
	Message *m;

	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;
		}
		for(m = cur; m != nil && n > 0; n--)
			m = m->next;
		break;
	}
	if(m == nil)
		return "address";
	*mp = m;
	return nil;
}

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

	*mp = nil;
	p = *pp;

	if(*p == '+'){
		sign = 1;
		p++;
		*pp = p;
	} else if(*p == '-'){
		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){
	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;
		}
		/* fall through */
	number:
		err = num2msg(mp, sign, n, first, cur);
		if (err != nil)
			return err;
		break;
	case '%':
	case '/':
	case '?':
		c = *p;
		prog = parsesearch(pp);
		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))
					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))
					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";
		*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 ',':
		if (*mp == nil)
			*mp = first;
		*pp = p;
		break;
	}

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

	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)
{
	Reprog *prog;
	Message *m, *s, *e, **l, *last;
	char buf[256];
	char *err;
	int i, c;
	char *q;
	static char errbuf[Errlen];

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

	// eat white space
	while(*p == ' ')
		p++;

	// null command is a special case (advance and print)
	if(*p == 0){
		if(cur == &top){
			// special case
			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 != '/' && *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;
			prog = parsesearch(&p);
			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;
					}
				}
			} 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
		s = e = nil;
		err = parseaddr(&p, first, cur, cur, &s);
		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);
			if(err != nil)
				return err;

			// select all messages in the range
			for(; s != nil; s = s->next){
				*l = s;
				l = &s->cmd;
				*l = nil;
				if(s == e)
					break;
			}
			if(s == nil)
				return "null address range";
		} else {
			// single address
			if(s != &top){
				*l = s;
				s->cmd = nil;
			}
		}
	}

	// 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 = ' ';
	}

	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;
	}
	return nil; 
}

// 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;
}

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];

	if(m == &top)
		return nil;

	snprintHeader(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;
}

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

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

	if(m == &top)
		return nil;

	snprintheader(hdr, sizeof(hdr), m);
	Bprint(&out, "%s\n", hdr);
	return nil;
}

Message*
bcmd(Cmd*, Message *m)
{
	int i;
	Message *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 om;
}

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

int
printpart(String *s, 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);
	if(fd < 0){
		fprint(2, "!message disappeared\n");
		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
printhtml(Message *m)
{
	Cmd 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);
	pipecmd(&c, m);
	return 0;
}

Message*
Pcmd(Cmd*, Message *m)
{
	if(m == &top)
		return &top;
	if(m->parent == &top)
		printpart(m->path, "unixheader");
	printpart(m->path, "raw");
	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;
}

Message*
pcmd(Cmd*, Message *m)
{
	Message *nm;
	Ctype *cp;
	String *s;
	char buf[128];

	if(m == &top)
		return &top;
	if(m->parent == &top)
		printpart(m->path, "unixheader");
	if(printpart(m->path, "header") > 0)
		Bprint(&out, "\n");
	cp = findctype(m);
	if(cp->display){
		if(strcmp(m->type, "text/html") == 0)
			printhtml(m);
		else
			printpart(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->display)
					break;
			}
		if(nm != nil)
			pcmd(nil, nm);
		else
			hcmd(nil, m);
	} else if(strncmp(m->type, "multipart/", 10) == 0){
		nm = m->child;
		if(nm != nil){
			// always print first part
			pcmd(nil, nm);

			for(nm = nm->next; nm != nil; nm = nm->next){
				s = rooted(s_clone(nm->path));
				cp = findctype(nm);
				snprintHeader(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);
					else
						Bprint(&out, "\n--- %s %s/body\n\n",
							buf, s_to_c(s));
					// pcmd(nil, nm);
				} else {
					if(cp->ext != nil)
						Bprint(&out, "\n!--- %s %s/body.%s\n",
							buf, s_to_c(s), cp->ext);
					else
						Bprint(&out, "\n!--- %s %s/body\n",
							buf, s_to_c(s));
				}
				s_free(s);
			}
		} else {
			hcmd(nil, m);
		}
	} 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);
		
	return m;
}

void
printpartindented(String *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);
	if(b == nil){
		fprint(2, "!message disappeared\n");
		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);
}

Message*
quotecmd(Cmd*, Message *m)
{
	Message *nm;
	Ctype *cp;

	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->display){
		printpartindented(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->display)
					break;
			}
		if(nm != nil)
			quotecmd(nil, 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);
		}
	}
	return m;
}

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

	doflush = 0;
	deld = 0;

	fd = open("/mail/fs/ctl", ORDWR);
	if(fd < 0){
		fprint(2, "!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->deleted){
			l = &(*l)->next;
			continue;
		}

		// 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);
		}
		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*
qcmd(Cmd*, Message*)
{
	flushdeleted(nil);

	if(didopen)
		closemb();
	Bflush(&out);

	exitfs(0);
	return nil;	// not reached
}

Message*
ycmd(Cmd*, Message *m)
{
	doflush = 1;

	return icmd(nil, m);
}

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

Message*
eqcmd(Cmd*, Message *m)
{
	if(m == &top)
		Bprint(&out, "0\n");
	else
		Bprint(&out, "%d\n", m->id);
	return nil;
}

Message*
dcmd(Cmd*, Message *m)
{
	if(m == &top){
		Bprint(&out, "!address\n");
		return nil;
	}
	while(m->parent != &top)
		m = m->parent;
	m->deleted = 1;
	return m;
}

Message*
ucmd(Cmd*, Message *m)
{
	if(m == &top)
		return nil;
	while(m->parent != &top)
		m = m->parent;
	if(m->deleted < 0)
		Bprint(&out, "!can't undelete, already flushed\n");
	m->deleted = 0;
	return m;
}


Message*
icmd(Cmd*, Message *m)
{
	int n;

	n = dir2message(&top, reverse);
	if(n > 0)
		Bprint(&out, "%d new message%s\n", n, plural(n));
	return 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> := '/'<regexp>'/' | '?'<regexp>'?' | '%%'<regexp>'%%'\n");
	Bprint(&out, "<command> :=\n");
	for(i = 0; cmdtab[i].cmd != nil; i++)
		Bprint(&out, "%s\n", cmdtab[i].help);
	return m;
}

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

	// start the mailer and get out of the way
	switch(pid = fork()){
	case -1:
		fprint(2, "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]);
		}
		Bprint(&out, "\n");
		Bflush(&out);
		av[0] = "marshal";
		chdir(wd);
		exec("/bin/upas/marshal", av);
		fprint(2, "couldn't exec /bin/upas/marshal\n");
		exits(0);
	default:
		w = wait();
		if(w == nil){
			if(interrupted)
				postnote(PNPROC, pid, "die");
			waitpid();
			return -1;
		}
		if(w->msg[0]){
			fprint(2, "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;
	int intok = 0, inquote = 0;

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

/* return reply-to address & set *nmp to corresponding Message */
static char *
getreplyto(Message *m, Message **nmp)
{
	Message *nm;

	for(nm = m; nm != &top; nm = nm->parent)
 		if(*nm->replyto != 0)
			break;
	*nmp = nm;
	return nm? nm->replyto: nil;
}

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

	if(m == &top){
		Bprint(&out, "!address\n");
		return nil;
	}

	addr = getreplyto(m, &nm);
	if(addr == nil){
		Bprint(&out, "!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){
		if(*nm->subject){
			av[ai++] = "-s";
			subject = addrecolon(nm->subject);
			av[ai++] = s_to_c(subject);
			break;
		}
	}

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

	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 = rooted(extendpath(m->path, "raw"));
		av[ai++] = s_to_c(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);
	av[ai] = 0;
	if(tomailer(av) < 0)
		m = nil;
	s_free(path);
	s_free(rpath);
	s_free(subject);
	s_free(from);
	return m;
}

Message*
mcmd(Cmd *c, Message *m)
{
	char *av[128];
	int i, ai = 1;
	String *path;

	if(m == &top){
		Bprint(&out, "!address\n");
		return nil;
	}

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

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

	av[ai++] = "-A";
	path = rooted(extendpath(m->path, "raw"));
	av[ai++] = s_to_c(path);

	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)
		m = nil;
	if(path != nil)
		s_free(path);
	return m;
}

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

	if(m == &top){
		Bprint(&out, "!address\n");
		return nil;
	}

	av[ai++] = "-8";

	if(*m->subject){
		av[ai++] = "-s";
		subject = addrecolon(m->subject);
		av[ai++] = s_to_c(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);
	}

	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);
	av[ai] = 0;
	if(tomailer(av) < 0)
		m = nil;
	s_free(path);
	s_free(rpath);
	s_free(subject);
	s_free(from);
	s_free(to);
	s_free(cc);
	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)
{
	String *file, *h;
	int in, out, rv;

	file = extendpath(m->path, part);
	in = open(s_to_c(file), OREAD);
	if(in < 0){
		fprint(2, "!message disappeared\n");
		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);
	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;
		}
	}
	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);
	else
		rv = appendfiletofile(in, out);

	close(in);
	close(out);

	if(rv >= 0)
		print("!saved in %s\n", s_to_c(file));
	s_free(file);
	return rv;
}

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

	if(m == &top){
		Bprint(&out, "!address\n");
		return nil;
	}

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

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

	m->stored = 1;
	return m;
}

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

	if(m == &top){
		Bprint(&out, "!address\n");
		return nil;
	}

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

	if(appendtofile(m, "body", file, 0) < 0)
		return nil;
	m->stored = 1;
	return m;
}

char *specialfile[] =
{
	"pipeto",
	"pipefrom",
	"L.mbox",
	"forward",
	"names"
};

// return 1 if this is a special file
static int
special(String *s)
{
	char *p;
	int i;

	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;
}

// open the folder using the recipients account name
static String*
foldername(char *rcvr)
{
	char *p;
	int c;
	String *file;
	Dir *d;
	int scarey;

	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;
	}
	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, "!won't overwrite %s\n", s_to_c(file));
		s_free(file);
		return nil;
	}

	return file;
}

Message*
fcmd(Cmd *c, Message *m)
{
	String *folder;

	if(c->an > 1){
		fprint(2, "!usage: f takes no arguments\n");
		return nil;
	}

	if(m == &top){
		Bprint(&out, "!address\n");
		return nil;
	}

	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;
	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);
		fprint(2, "!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 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[3] = 0;
	system("/bin/rc", av, -1);
	Bprint(&out, "!\n");
	return m;
}

Message*
xpipecmd(Cmd *c, Message *m, char *part)
{
	char cmd[128];
	char *p, *e;
	char *av[4];
	String *path;
	int i, fd;

	if(c->an < 2){
		Bprint(&out, "!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);
	}
	if(fd < 0){
		fprint(2, "!message disappeared\n");
		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[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)
		fprint(fd, "close %s", mbname);

	close(fd);
}

int
switchmb(char *file, char *singleton)
{
	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";

		// close current mailbox
		closemb();
		didopen = 1;

		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);
			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);
	}else{
		path = s_reset(nil);
		mboxpath("mbox", user, path, 0);
		strcpy(mbname, "mbox");
	}

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

	if(mbpath != nil)
		s_free(mbpath);
	mbpath = path;
	return 0;
}

// like tokenize but for into lines
int
lineize(char *s, char **f, int n)
{
	int i;

	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);
}

int
plumb(Message *m, Ctype *cp)
{
	String *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->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);
	plumbsend(fd, pm);
	plumbfree(pm);
	return 0;
}

void
regerror(char*)
{
}

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? */
	exits(rv);
}