shithub: Nail

Download patch

ref: 2d076037d8dddd20eaeb72f6ed31c8a359ce978b
author: Ori Bernstein <ori@eigenstate.org>
date: Tue Nov 3 01:39:20 EST 2020

initial commit

--- /dev/null
+++ b/comp.c
@@ -1,0 +1,190 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+typedef struct Fn	Fn;
+
+struct Fn {
+	char *name;
+	void (*fn)(Comp *, char **, int);
+};
+
+void
+execmarshal(void *p)
+{
+	Comp *c;
+
+	c = p;
+	rfork(RFFDG);
+	dup(c->fd[0], 0);
+	close(c->fd[0]);
+	close(c->fd[1]);
+	sendul(c->sync, 0);
+	procexecl(nil, "/bin/upas/marshal", "marshal", "-8", nil);
+}
+
+static void
+postmesg(Comp *c, char **, int nf)
+{
+	char *buf, wpath[64], *path;
+	int n, fd;
+	Mesg *m;
+
+	snprint(wpath, sizeof(wpath), "/mnt/acme/%d/body", c->id);
+	if(nf != 0){
+		fprint(2, "Post: too many args\n");
+		return;
+	}
+	if((fd = open(wpath, OREAD)) == -1){
+		fprint(2, "open body: %r\n");
+		return;
+	}
+	if(pipe(c->fd) == -1){
+		fprint(2, "pipe: %r\n");
+		close(fd);
+		return;
+	}
+
+	proccreate(execmarshal, c, Stack);
+	recvul(c->sync);
+	close(c->fd[0]);
+
+	buf = emalloc(Bufsz);
+	while((n = read(fd, buf, Bufsz)) > 0)
+		if(write(c->fd[1], buf, n) != n)
+			break;
+	close(c->fd[1]);
+	close(fd);
+	if(n == -1)
+		return;
+
+	if(c->replyto != nil){
+		if((m = mesglookup(c->rname, c->rdigest)) == nil)
+			return;
+		m->flags |= Fresp;
+		path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
+		if((fd = open(path, OWRITE)) != -1){
+			fprint(fd, "+a");
+			close(fd);
+		}
+		mbredraw(m, 0, 0);
+		free(path);
+	}
+
+}
+
+static Fn compfn[] = {
+	{"Post", postmesg},
+	{nil},
+};
+
+static void
+compmain(void *cp)
+{
+	char *a, *f[32];
+	int nf;
+	Event ev;
+	Comp *c;
+	Fn *p;
+
+	c = cp;
+	c->quitting = 0;
+	fprint(c->ctl, "clean\n");
+	mbox.nopen++;
+	while(!c->quitting){
+		if(winevent(c, &ev) != 'M')
+			continue;
+		if(strcmp(ev.text, "Del") == 0)
+			break;
+		switch(ev.type){
+		case 'l':
+		case 'L':
+			if((a = matchaddr(&mbox, &ev)) != nil)
+				compose1(a, nil, 0);
+			else if(matchmesg(&mbox, ev.text))
+				mesgopen(ev.text, nil);
+			else if(!(ev.flags & 0x2))
+				winsendevent(&mbox, &ev);
+			free(a);
+			break;
+		case 'x':
+		case 'X':
+			if((nf = tokenize(ev.text, f, nelem(f))) == 0)
+				continue;
+			for(p = compfn; p->name != nil; p++)
+				if(strcmp(p->name, f[0]) == 0)
+					p->fn(c, &f[1], nf - 1);
+			if(p->name == nil && !(ev.flags & 0x2))
+				winsendevent(&mbox, &ev);
+			break;
+		break;
+		}
+	}
+	mbox.nopen--;
+	winclose(c);
+	free(c->replyto);
+	free(c->rname);
+	free(c->rdigest);
+	threadexits(nil);
+}
+
+void
+compose(char **to, int nto, char **cc, int ncc, Mesg *r, int quote)
+{
+	static int ncompose;
+	char *path, *ln;
+	Biobuf *rfd, *wfd;
+	Comp *c;
+	int i;
+
+	c = emalloc(sizeof(Comp));
+	c->sync = chancreate(sizeof(ulong), 0);
+	if(r != nil)
+		path = esmprint("%s%s%s.%d", mbox.path, r->name, "Reply", ncompose++);
+	else
+		path = esmprint("%sCompose.%d", mbox.path, ncompose++);
+	wininit(c, path);
+	free(path);
+
+	wintagwrite(c, "Delmesg Save Post ");
+	wfd = bwinopen(c, "body", OWRITE);
+	for(i = 0; i < nto; i++)
+		Bprint(wfd, "To: %s\n", to[i]);
+	for(i = 0; i < ncc; i++)
+		Bprint(wfd, "Cc: %s\n", cc[i]);
+	if(r == nil){
+		Bprint(wfd, "\n");
+	}else{
+		if(r->messageid != nil)
+			c->replyto = estrdup(r->messageid);
+		c->rname = estrdup(r->name);
+		c->rdigest = estrdup(r->digest);
+		Bprint(wfd, "Subject: ");
+		if(r->subject != nil && cistrncmp(r->subject, "Re", 2) == 0)
+			Bprint(wfd, "Re: ");
+		Bprint(wfd, "%s\n\n", r->subject);
+		if(quote){
+			path = estrjoin(mbox.path, r->name, "body", nil);
+			rfd = Bopen(path, OREAD);
+			free(path);
+			if(rfd != nil)
+				while((ln = Brdstr(rfd, '\n', 0)) != nil)
+					if(Bprint(wfd, "> %s", ln) == -1)
+						break;
+			Bterm(rfd);
+		}
+		Bterm(wfd);
+	}
+	Bterm(wfd);
+	proccreate(compmain, c, Stack);
+}
+
+void
+compose1(char *to, Mesg *resp, int quote)
+{
+	compose(&to, 1, nil, 0, resp, quote);
+}
--- /dev/null
+++ b/mail.h
@@ -1,0 +1,188 @@
+typedef struct Event	Event;
+typedef struct Win	Win;
+typedef struct Mesg	Mesg;
+typedef struct Mbox	Mbox;
+typedef struct Comp	Comp;
+
+enum {
+	Stack	= 64*1024,
+	Bufsz	= 8192,
+	Eventsz	= 256*UTFmax,
+	Subjlen	= 56,
+};
+
+enum {
+	Fdummy	= 1<<0,	/* message placeholder */
+	Ftoplev	= 1<<1,	/* not a response to anything */
+	Fopen	= 1<<2,	/* opened for viewing */
+
+	Fresp	= 1<<3,	/* has been responded to */
+	Funseen	= 1<<4,	/* has been viewed */
+	Fdel	= 1<<5, /* was deleted */
+	Ftodel	= 1<<6,	/* pending deletion */
+};
+
+enum {
+	Vflat,
+	Vgroup,
+};
+
+struct Event {
+	char	action;
+	char	type;
+	int	q0;
+	int	q1;
+	int	flags;
+	int	ntext;
+	char	text[Eventsz + 1];
+};
+
+struct Win {
+	Ioproc	*io;
+	Biobuf	*event;
+	int	id;
+	int	ctl;
+	int	addr;
+	int	data;
+};
+
+/*
+ * A composing message.
+ */
+struct Comp {
+	Win;
+
+	/* exec setup */
+	Channel *sync;
+	int	fd[2];
+
+	/* to relate back the message */
+	char	*replyto;
+	char	*rname;
+	char	*rdigest;
+
+	char	**to;
+	int	nto;
+	char	**cc;
+	int	ncc;
+	char	**bcc;
+	int	nbcc;
+
+	int	quitting;
+};
+
+/*
+ * A message in the mailbox
+ */
+struct Mesg {
+	Win;
+
+	/* bookkeeping */
+	char	*name;
+	int	flags;
+	u32int	hash;
+	Mesg	*hnext;
+	char	quitting;
+
+	Mesg	*parent;
+	Mesg	**child;
+	int	nchild;
+	int	nsub;	/* transitive children */
+	Mesg	**attachments;
+	int	nattachments;
+
+	/* info fields */
+	char	*from;
+	char	*to;
+	char	*cc;
+	char	*replyto;
+	char	*date;
+	char	*subject;
+	char	*type;
+	char	*disposition;
+	char	*messageid;
+	char	*filename;
+	char	*digest;
+	char	*mflags;
+	char	*fromcolon;
+	char	*inreplyto;
+
+	vlong	time;
+};
+
+/*
+ *The mailbox we're showing.
+ */
+struct Mbox {
+	Win;
+
+	/* lock protects mesg, hash */
+	Mesg	**mesg;
+	Mesg	**hash;
+	int	mesgsz;
+	int	hashsz;
+	int	nmesg;
+	int	ndead;
+
+	Channel	*see;
+	Channel	*show;
+	Channel	*event;
+
+	int	view;
+	int	nopen;
+	char	*path;
+};
+
+extern Mbox	mbox;
+extern int	threadsort;
+extern int	plumbsendfd;
+extern int	plumbseemailfd;
+extern int	plumbshowmailfd;
+extern int	plumbsendmailfd;
+extern Reprog	*addrpat;
+extern Reprog	*mesgpat;
+
+/* window management */
+void	wininit(Win*, char*);
+int	winopen(Win*, char*, int);
+Biobuf	*bwinopen(Win*, char*, int);
+Biobuf	*bwindata(Win*, int);
+void	winclose(Win*);
+void	wintagwrite(Win*, char*);
+int	winevent(Win*, Event*);
+void	winsendevent(Win*, Event*);
+int	wineval(Win*, char*, ...);
+int	winread(Win*, int, int, char*, int);
+char	*matchaddr(Win*, Event*);
+int	matchmesg(Win*, char*);
+char	*winreadsel(Win*);
+void	wingetsel(Win*, int*, int*);
+void	winsetsel(Win*, int, int);
+
+/* messages */
+Mesg	*mblookupid(char*);
+Mesg	*mesglookup(char*, char*);
+Mesg	*mesgload(char*);
+int	mesgmatch(Mesg*, char*, char*);
+void	mesgopen(char*, char*);
+void	mesgclear(Mesg*);
+void	mesgfree(Mesg*);
+void	mesgpath2name(char*, int, char*);
+
+/* mailbox */
+void	mbredraw(Mesg*, int, int);
+
+/* composition */
+void	compose(char**, int, char**, int, Mesg*, int);
+void	compose1(char*, Mesg*, int);
+
+/* utils */
+void	*emalloc(ulong);
+void	*erealloc(void*, ulong);
+char	*estrdup(char*);
+char	*estrjoin(char*, ...);
+char	*esmprint(char*, ...);
+char	*rslurp(Mesg*, char*, int*);
+char	*fslurp(int, int*);
+u32int	strhash(char*);
+
--- /dev/null
+++ b/mbox.c
@@ -1,0 +1,767 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include <ctype.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+typedef struct Fn	Fn;
+
+struct Fn {
+	char *name;
+	void (*fn)(char **, int);
+};
+
+enum {
+	Cevent,
+	Cseemail,
+	Cshowmail,
+	Nchan,
+};
+
+
+char	*maildir	= "/mail/fs";
+char	*mailbox	= "tmptest";
+Mesg	dead = {.messageid="", .hash=42};
+
+Reprog	*addrpat;
+Reprog	*mesgpat;
+
+int	threadsort = 1;
+
+int	plumbsendfd;
+int	plumbseemailfd;
+int	plumbshowmailfd;
+Channel *cwait;
+
+Mbox	mbox;
+
+static void	showmesg(Biobuf*, Mesg*, int, int);
+
+static void
+plumbloop(Channel *ch, int fd)
+{
+	Plumbmsg *m;
+
+	while(1){
+		if((m = plumbrecv(fd)) == nil)
+			threadexitsall("plumber gone");
+		sendp(ch, m);
+	}
+}
+
+static void
+plumbshow(void*)
+{
+	threadsetname("plumbshow");
+	plumbloop(mbox.show, plumbshowmailfd);
+}
+
+static void
+plumbsee(void*)
+{
+	threadsetname("plumbsee");
+	plumbloop(mbox.see, plumbseemailfd);
+}
+
+static void
+eventread(void*)
+{
+	Event *ev;
+
+	while(1){
+		ev = emalloc(sizeof(Event));
+		if(winevent(&mbox, ev) == -1)
+			threadexitsall(nil);
+		sendp(mbox.event, ev);
+	}
+}
+
+static int
+ideq(Mesg *a, Mesg *b)
+{
+	if(a->messageid == nil || b->messageid == nil)
+		return 0;
+	return strcmp(a->messageid, b->messageid) == 0;
+}
+
+static int
+cmpmesg(void *pa, void *pb)
+{
+	Mesg *a, *b;
+
+	a = *(Mesg**)pa;
+	b = *(Mesg**)pb;
+
+	return b->time - a->time;
+}
+
+static void
+mbsort(void)
+{
+	qsort(mbox.mesg, mbox.nmesg, sizeof(Mesg*), cmpmesg);
+}
+
+static int
+mesglineno(Mesg *msg, int *depth)
+{
+	Mesg *r, *m;
+	int i, o, n, d;
+
+	o = 0;
+	n = 0;
+	d = 0;
+	r = msg;
+	if(msg->parent != nil) {
+		m = msg->parent;
+		for(i = 0; i < m->nchild; i++){
+			if(m->child[i] == msg)
+				break;
+			o += m->child[i]->nsub;
+		}
+	}
+	while(r->parent != nil){
+		r = r->parent;
+		o++;
+		d++;
+	}
+	for(i = 0; i < mbox.nmesg; i++){
+		m = mbox.mesg[i];
+		if(m == r)
+			break;
+		if(m->parent == nil){
+			n += mbox.mesg[i]->nsub;
+			if(!(m->flags & Fdummy))
+				n++;
+		}
+
+	}
+	if(depth != nil)
+		*depth = d;
+	assert(n + o < mbox.nmesg);
+	return n + o;
+}
+
+static int
+addchild(Mesg *p, Mesg *m)
+{
+	Mesg *q;
+
+	assert(m->parent == nil);
+	for(q = p; q != nil; q = q->parent){
+		if(ideq(m, q)){
+			fprint(2, "wonky message replies to self\n");
+			return 0;
+		}
+		if(m->time > q->time)
+			q->time = m->time;
+	}
+	for(q = p; q != nil; q = q->parent)
+		q->nsub++;
+	p->child = erealloc(p->child, ++p->nchild*sizeof(Mesg*));
+	p->child[p->nchild - 1] = m;
+	m->parent = p;
+	return 1;
+}
+
+static int
+slotfor(Mesg *m)
+{
+	int i;
+
+	for(i = 0; i < mbox.nmesg; i++)
+		if(cmpmesg(&mbox.mesg[i], &m) >= 0)
+			break;
+	return i;
+}
+
+static void
+removeid(Mesg *m)
+{
+	Mesg *e;
+	int i;
+
+	/* Dummies don't go in the table */
+	if(m->flags & Fdummy)
+		return;
+	i = m->hash % mbox.hashsz;
+	while(1){
+		e = mbox.hash[i];
+		if(e == nil)
+			return;
+		if(e == &dead)
+			continue;
+		if(e->hash == m->hash && strcmp(e->messageid, m->messageid) == 0){
+			mbox.hash[i] = &dead;
+			mbox.ndead++;
+		}
+		i = (i + 1) % mbox.hashsz;
+	}
+}
+
+Mesg*
+lookupid(char *msgid)
+{
+	u32int h, i;
+	Mesg *e;
+
+	if(msgid == nil)
+		return nil;
+	h = strhash(msgid);
+	i = h % mbox.hashsz;
+	while(1){
+		e = mbox.hash[i];
+		if(e == nil)
+			return nil;
+		if(e == &dead)
+			continue;
+		if(e->hash == h && strcmp(e->messageid, msgid) == 0)
+			return e;
+		i = (i + 1) % mbox.hashsz;
+	}
+}
+
+static void
+addmesg(Mesg *m, int ins)
+{
+	Mesg *o, *e, **oldh;
+	int i, oldsz, idx;
+
+	/* add to flat list */
+	if(mbox.nmesg == mbox.mesgsz){
+		mbox.mesgsz *= 2;
+		mbox.mesg = erealloc(mbox.mesg, mbox.mesgsz*sizeof(Mesg*));
+	}
+	/* 
+	 * on initial load, it's faster to append everything then sort,
+	 * but on subsequent messages it's better to just put it in the
+	 * right place; we don't want to shuffle the already-sorted
+	 * messages.
+	 */
+	if(ins)
+		idx = slotfor(m);
+	else
+		idx = mbox.nmesg;
+	memmove(&mbox.mesg[idx + 1], &mbox.mesg[idx], mbox.nmesg - idx);
+	mbox.mesg[idx] = m;
+	mbox.nmesg++;
+	if(m->messageid == nil)
+		return;
+
+	/* grow hash table, or squeeze out deadwood */
+	if(mbox.hashsz <= 2*(mbox.nmesg + mbox.ndead)){
+		oldsz = mbox.hashsz;
+		oldh = mbox.hash;
+		if(mbox.hashsz <= 2*mbox.nmesg)
+			mbox.hashsz *= 2;
+		mbox.ndead = 0;
+		mbox.hash = emalloc(mbox.hashsz*sizeof(Mesg*));
+		for(i = 0; i < oldsz; i++){
+			if((o = oldh[i]) == nil)
+				continue;
+			mbox.hash[o->hash % mbox.hashsz] = o;
+		}
+		free(oldh);
+	}
+	i = m->hash % mbox.hashsz;
+	while(1){
+		e = mbox.hash[i % mbox.hashsz];
+		if(e == nil || e == &dead)
+			break;
+		i = (i + 1) % mbox.hashsz;
+	}
+	mbox.hash[i] = m;
+}
+
+static Mesg *
+placeholder(char *msgid, vlong time, int ins)
+{
+	Mesg *m;
+
+	m = emalloc(sizeof(Mesg));
+	m->flags |= Fdummy|Ftoplev;
+	m->messageid = estrdup(msgid);
+	m->hash = strhash(msgid);
+	m->time = time;
+	addmesg(m, ins);
+	return m;
+}
+
+static Mesg*
+change(char *name, char *digest)
+{
+	Mesg *m;
+	char *f;
+
+	if((m = mesglookup(name, digest)) == nil)
+		return nil;
+	if((f = rslurp(m, "flags", nil)) == nil)
+		return nil;
+	free(m->mflags);
+	m->mflags = f;
+	m->flags = Funseen;
+	if(strchr(m->mflags, 'd')) m->flags |= Fdel;
+	if(strchr(m->mflags, 's')) m->flags &= ~Funseen;
+	if(strchr(m->mflags, 'a')) m->flags |= Fresp;
+	return m;
+}
+
+static Mesg*
+delete(char *name, char *digest)
+{
+	Mesg *m;
+
+	if((m = mesglookup(name, digest)) == nil)
+		return nil;
+	m->flags |= Fdel;
+	return m;
+}
+
+static Mesg*
+load(char *name, char *digest, int ins)
+{
+	Mesg *m, *p, **c;
+	char *n;
+	int nc;
+
+	if((n = strrchr(name, '/')) == nil)
+		n = name;
+	if((m = mesgload(n)) == nil)
+		goto error;
+
+	if(digest != nil && strcmp(digest, m->digest) != 0)
+		goto error;
+	/* if we already have a dummy, populate it */
+	if((p = lookupid(m->messageid)) != nil){
+		c = p->child;
+		nc = p->nchild;
+		mesgclear(p);
+		memcpy(p, m, sizeof(*p));
+		free(m);
+
+		m = p;
+		m->child = c;
+		m->nchild = nc;
+	}else
+		addmesg(m, ins);
+
+	if(!threadsort || m->inreplyto == nil){
+		m->flags |= Ftoplev;
+		return 0;
+	}
+
+	p = lookupid(m->inreplyto);
+	if(p == nil)
+		p = placeholder(m->inreplyto, m->time, ins);
+	addchild(p, m);
+	return m;
+error:
+	fprint(2, "load failed: %r\n");
+	mesgfree(m);
+	return nil;
+}
+
+void
+mbredraw(Mesg *m, int add, int rec)
+{
+	Biobuf *bfd;
+	int ln, depth;
+
+	ln = mesglineno(m, &depth);
+	werrstr("");
+	fprint(mbox.addr, "%d%s", ln+1, add ? "-#0" : "");
+	fprint(mbox.ctl, "dot=addr\n");
+	bfd = bwindata(&mbox, OWRITE);
+	showmesg(bfd, m, depth, rec);
+	Bterm(bfd);	
+}
+
+static void
+mbload(void)
+{
+	int i, n, fd;
+	Dir *d;
+
+	mbox.mesgsz = 128;
+	mbox.hashsz = 128;
+	mbox.mesg = emalloc(mbox.mesgsz*sizeof(Mesg*));
+	mbox.hash = emalloc(mbox.hashsz*sizeof(Mesg*));
+	mbox.path = esmprint("%s/%s/", maildir, mailbox);
+	cleanname(mbox.path);
+	n = strlen(mbox.path);
+	if(mbox.path[n - 1] != '/')
+		mbox.path[n] = '/';
+	if((fd = open(mbox.path, OREAD)) == -1)
+		sysfatal("%s: open: %r", mbox.path);
+	while(1){
+		n = dirread(fd, &d);
+		if(n == -1)
+			sysfatal("%s read: %r", mbox.path);
+		if(n == 0)
+			break;
+		for(i = 0; i < n; i++)
+			if(strcmp(d[i].name, "ctl") != 0)
+				load(d[i].name, nil, 0);
+		free(d);
+	}
+}
+
+static void
+showmesg(Biobuf *bfd, Mesg *m, int depth, int recurse)
+{
+	char *sep, *flag, *dots;
+	int i, width;
+
+	if(!(m->flags & Fdummy)){
+		dots = "";
+		flag = " ";
+		sep = depth ? "\t" : "";
+		width = depth ? Subjlen - 4 : Subjlen;
+		if(m->flags & Funseen)	flag = "★";
+		if(m->flags & Fresp)	flag = "←";
+		if(m->flags & Fdel)	flag = "∉";
+		if(m->flags & Ftodel)	flag = "∉";
+		if(utflen(m->subject) > Subjlen){
+			width -= 3;
+			dots = "...";
+		}
+
+		Bprint(bfd, "%-6s\t%s %s%*.*s%s\t«%s»\n",
+			m->name,
+			flag, sep, -width, width,
+			m->subject,
+			dots,
+			m->fromcolon);
+		depth++;
+	}
+	if(recurse && mbox.view != Vflat)
+		for(i = 0; i < m->nchild; i++)
+			showmesg(bfd, m->child[i], depth, recurse);
+}
+
+static void
+mark(char **f, int nf, int flags, int add)
+{
+	char *sel, *p, *q, *e;
+	int i, q0, q1;
+	Mesg *m;
+
+	wingetsel(&mbox, &q0, &q1);
+	if(nf == 0){
+		sel = winreadsel(&mbox);
+		for(p = sel; p != nil; p = e){
+			if((e = strchr(p, '\n')) != nil)
+				*e++ = 0;
+			if(!matchmesg(&mbox, p))
+				continue;
+			if((q = strchr(p, '/')) != nil)
+				q[1] = 0;
+			if((m = mesglookup(p, nil)) != nil){
+				if(add)
+					m->flags |= flags;
+				else
+					m->flags &= ~flags;
+				mbredraw(m, 0, 0);
+			}
+		}
+		free(sel);
+	}else for(i = 0; i < nf; i++){
+		if((m = mesglookup(f[i], nil)) != nil){
+			m->flags |= Ftodel;
+			mbredraw(m, 0, 0);
+		}
+	}
+	winsetsel(&mbox, q0, q1);
+}
+
+static void
+removemesg(Mesg *m)
+{
+	Mesg *c, *p;
+	int i, j;
+
+	/* remove child, preserving order */
+	j = 0;
+	p = m->parent;
+	for(i = 0; p && i < p->nchild; i++){
+		if(p->child[i] != m)
+			j++;
+		p->child[j] = p->child[i];
+	}
+
+	/* reparent children */
+	for(i = 0; i < m->nchild; i++){
+		c = m->child[i];
+		c->parent = nil;
+		if(c->parent != nil)
+			addchild(p, c);
+		else
+			c->flags |= Ftoplev;
+	}
+}
+
+static void
+mbflush(char **, int)
+{
+	Mesg *m, **p;
+	int i, j, ln;
+
+	p = mbox.mesg;
+	for(i = 0; i < mbox.nmesg; i++){
+		m = mbox.mesg[i];
+		if((m->flags & Fopen) || !(m->flags & (Fdel|Ftodel))){
+			*p++ = m;
+			continue;
+		}
+		ln = mesglineno(m, nil);
+		fprint(mbox.addr, "%d,%d", ln+1, ln+1+m->nsub);
+		write(mbox.data, "", 0);
+
+		removemesg(m);
+		removeid(m);
+		for(j = 0; j < m->nchild; j++)
+			mbredraw(m->child[j], 1, 1);
+	//	if(m->flags & Ftodel)
+	//		deletemesg(m);
+		mesgfree(m);
+		
+		*p = m;
+	}
+}
+
+static void
+mbdelmesg(char **f, int nf)
+{
+	mark(f, nf, Ftodel, 1);
+}
+
+static void
+mbmarkmesg(char **f, int nf)
+{
+	int flg, add;
+
+	if(nf != 1)
+		return;
+	if(strlen(f[0]) != 1){
+		fprint(2, "unknown mark %s", f[0]);
+		return;
+	}
+	switch(*f[0]){
+	case 'D':
+		flg = Ftodel;
+		add = 1;
+		break;
+	case 'K':
+		flg = Ftodel;
+		add = 0;
+		break;
+	case 'U':
+		flg = Funseen;
+		add = 1;
+		break;
+	case 'R':
+		flg = Funseen;
+		add = 0;
+		break;
+	default:
+		fprint(2, "unknown mark %s", f[0]);
+		return;
+	}
+	mark(f, nf, flg, add);
+		
+}
+
+static void
+mbshow(void)
+{
+	Biobuf *bfd;
+	Mesg *m;
+	int i;
+
+	bfd = bwinopen(&mbox, "body", OWRITE);
+	for(i = 0; i < mbox.nmesg; i++){
+		m = mbox.mesg[i];
+		if(mbox.view == Vflat || m->flags & (Fdummy|Ftoplev))
+			showmesg(bfd, m, 0, 1);
+	}
+	Bterm(bfd);
+}
+
+static void
+mbquit(char **, int)
+{
+	if(mbox.nopen == 0)
+		threadexitsall(nil);
+	fprint(2, "Del: %d open messages", mbox.nopen);
+}
+
+static void
+changemesg(Plumbmsg *pm)
+{
+	char *digest, *action;
+	Mesg *m;
+	int add;
+
+	m = nil;
+	add = 0;
+
+	digest = plumblookup(pm->attr, "digest");
+	action = plumblookup(pm->attr, "mailtype");
+	fprint(2, "changing message %s, %s %s\n", action, pm->data, digest);
+	if(strcmp(action, "new") == 0){
+		m = load(pm->data, digest, 1);
+		add = 1;
+	}else if(strcmp(action, "delete") == 0)
+		m = delete(pm->data, digest);
+	else if(strcmp(action, "modify") == 0)
+		m = change(pm->data, digest);
+	if(m == nil)
+		return;
+	mbredraw(m, add, 0);
+}
+
+static void
+viewmesg(Plumbmsg *pm)
+{
+	mesgopen(pm->data, plumblookup(pm->attr, "digest"));
+}
+
+Fn mboxfn[] = {
+	{"Put",	mbflush},
+	{"Delmesg", mbdelmesg},
+	{"Mark", mbmarkmesg},
+	{"Del", mbquit},
+#ifdef NOTYET
+	{"Get", mbrefresh},
+	{"Sort", mbsort},
+	{"Next", mboxnext},
+#endif
+	{nil}
+};
+
+
+static void
+doevent(Event *ev)
+{
+	char *a, *f[32];
+	int nf;
+	Fn *p;
+
+	if(ev->action != 'M')
+		return;
+	switch(ev->type){
+	case 'l':
+	case 'L':
+		if((a = matchaddr(&mbox, ev)) != nil)
+			compose1(a, nil, 0);
+		else if(matchmesg(&mbox, ev->text))
+			mesgopen(ev->text, nil);
+		else
+			winsendevent(&mbox, ev);
+		free(a);
+		break;
+	case 'x':
+	case 'X':
+		if((nf = tokenize(ev->text, f, nelem(f))) == 0)
+			return;
+		for(p = mboxfn; p->name != nil; p++)
+			if(strcmp(p->name, f[0]) == 0 && p->fn != nil)
+				p->fn(&f[1], nf - 1);
+		if(p->fn == nil && !(ev->flags & 0x2))
+			winsendevent(&mbox, ev);
+		break;
+	break;
+	}
+}
+
+static void
+mbmain(void *)
+{
+	Event *ev;
+	Plumbmsg *pm;
+	Alt a[] = {
+	[Cevent]	= {mbox.event, &ev, CHANRCV},
+	[Cseemail]	= {mbox.see, &pm, CHANRCV},
+	[Cshowmail]	= {mbox.show, &pm, CHANRCV},
+	[Nchan]		= {nil,	nil, CHANEND},
+	};
+
+	wininit(&mbox, mbox.path);
+	wintagwrite(&mbox, "Put Mail Delmesg Save Next ");
+	mbshow();
+	fprint(2, "shown\n");
+	fprint(mbox.ctl, "clean\n");
+	proccreate(eventread, nil, Stack);
+	fprint(2, "started\n");
+	while(1){
+		switch(alt(a)){
+		case Cevent:
+			doevent(ev);
+			free(ev);
+			break;
+		case Cseemail:
+			changemesg(pm);
+			plumbfree(pm);
+			break;
+		case Cshowmail:
+			viewmesg(pm);
+			plumbfree(pm);
+			break;
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-T] [-f mailfs] [mbox]\n", argv0);
+	exits("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	mbox.view = Vgroup;
+
+	ARGBEGIN{
+	case 'f':
+		maildir = EARGF(usage());
+		break;
+	case 'T':
+		mbox.view = Vflat;
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	doquote = needsrcquote;
+	quotefmtinstall();
+	tmfmtinstall();
+	/* open these early so we won't miss notification of new mail messages while we read mbox */
+	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
+	plumbseemailfd = plumbopen("seemail", OREAD|OCEXEC);
+	plumbshowmailfd = plumbopen("showmail", OREAD|OCEXEC);
+	mbox.event = chancreate(sizeof(Event*), 1);
+	mbox.see = chancreate(sizeof(Plumbmsg*), 1);
+	mbox.show = chancreate(sizeof(Plumbmsg*), 1);
+
+	addrpat = regcomp("[^ \t]*@[^ \t]*\\.[^ \t]*");
+	mesgpat = regcomp("(\\(deleted\\)-)?[0-9]+/.*");
+	cwait = threadwaitchan();
+
+	if(argc > 1)
+		usage();
+	if(argc == 1)
+		mailbox = argv[0];
+	mbload();
+	mbsort();
+	
+	threadcreate(mbmain, nil, Stack);
+	proccreate(plumbsee, nil, Stack);
+	proccreate(plumbshow, nil, Stack);
+//	threadexitsall(nil);
+}
--- /dev/null
+++ b/mesg.c
@@ -1,0 +1,323 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+#define Datefmt		"?WWW, ?MMM ?DD hh:mm:ss ?Z YYYY"
+
+typedef struct Fn	Fn;
+
+struct Fn {
+	char *name;
+	void (*fn)(Mesg *, char **, int);
+};
+
+void
+mesgclear(Mesg *m)
+{
+	free(m->name);
+	free(m->from);
+	free(m->to);
+	free(m->cc);
+	free(m->replyto);
+	free(m->date);
+	free(m->subject);
+	free(m->type);
+	free(m->disposition);
+	free(m->messageid);
+	free(m->filename);
+	free(m->digest);
+	free(m->mflags);
+	free(m->fromcolon);
+}
+
+void
+mesgfree(Mesg *m)
+{
+	if(m == nil)
+		return;
+	mesgclear(m);
+	free(m);
+}
+
+static char*
+line(char *data, char **pp)
+{
+	char *p, *q;
+
+	for(p=data; *p!='\0' && *p!='\n'; p++)
+		;
+	if(*p == '\n')
+		*pp = p+1;
+	else
+		*pp = p;
+	if(p == data)
+		return nil;
+	q = emalloc(p-data + 1);
+	memmove(q, data, p-data);
+	return q;
+}
+
+static char*
+fc(Mesg *m, char *s)
+{
+	char *r;
+
+	if(s != nil && m->from != nil){
+		r = smprint("%s <%s>", s, m->from);
+		free(s);
+		return r;
+	}
+	if(m->from != nil)
+		return estrdup(m->from);
+	if(s != nil)
+		return s;
+	return estrdup("??");
+}
+
+Mesg*
+mesgload(char *name)
+{
+	char *info, *p;
+	int ninfo;
+	Mesg *m;
+	Tm tm;
+
+	m = emalloc(sizeof(Mesg));
+	m->name = estrjoin(name, "/", nil);
+	if((info = rslurp(m, "info", &ninfo)) == nil)
+		return nil;
+
+	p = info;
+	m->from = line(p, &p);
+	m->to = line(p, &p);
+	m->cc = line(p, &p);
+	m->replyto = line(p, &p);
+	m->date = line(p, &p);
+	m->subject = line(p, &p);
+	m->type = line(p, &p);
+	m->disposition = line(p, &p);
+	m->filename = line(p, &p);
+	m->digest = line(p, &p);
+	/* m->bcc = */ free(line(p, &p));
+	m->inreplyto = line(p, &p);
+	/* m->date = */ free(line(p, &p));
+	/* m->sender = */ free(line(p, &p));
+	m->messageid = line(p, &p);
+	/* m->lines = */ free(line(p, &p));
+	/* m->size = */ free(line(p, &p));
+	m->mflags = line(p, &p);
+	/* m->fileid = */ free(line(p, &p));
+	m->fromcolon = fc(m, line(p, &p));
+	free(info);
+
+	m->flags = Funseen;
+	if(strchr(m->mflags, 'd')) m->flags |= Fdel;
+	if(strchr(m->mflags, 's')) m->flags &= ~Funseen;
+	if(strchr(m->mflags, 'a')) m->flags |= Fresp;
+
+	m->time = time(nil);
+	if(tmparse(&tm, Datefmt, m->date, nil, nil) != nil)
+		m->time = tmnorm(&tm);
+	m->hash = 0;
+	if(m->messageid != nil)
+		m->hash = strhash(m->messageid);
+	return m;
+}
+
+static int
+mesgshow(Mesg *m)
+{
+	int rfd, wfd;
+	char *buf, *path;
+	int n;
+
+	buf = emalloc(Bufsz);
+	path = estrjoin(mbox.path, m->name, "body", nil);
+	if((wfd = winopen(m, "body", OWRITE)) == -1)
+		return -1;
+	if((rfd = open(path, OREAD)) == -1)
+		return -1;
+	fprint(wfd, "From: %s\n", m->from);
+	fprint(wfd, "Date: %s\n", m->to);
+	fprint(wfd, "Subject: %s\n\n", m->subject);
+	while(1){
+		n = read(rfd, buf, Bufsz);
+		if(n <= 0)
+			break;
+		if(write(wfd, buf, n) != n)
+			break;
+	}
+	close(rfd);
+	close(wfd);
+	free(buf);
+	free(path);
+	return n;
+}
+
+static void
+reply(Mesg *m, char **f, int nf)
+{
+	if(nf >= 2
+	|| nf >= 1 &&  strcmp(f[0], "All") != 0){
+		fprint(2, "Q: invaid args\n");
+		return;
+	}
+
+	/* FIXME: get all recievers of the message */
+	compose1(m->from, m, 0);
+}
+
+static void
+qreply(Mesg *m, char **f, int nf)
+{
+	if(nf >= 3
+	|| nf >= 2 && strcmp(f[1], "All") != 0
+	|| nf >= 1 && strcmp(f[0], "Reply") != 0
+	|| nf == 0){
+		fprint(2, "Q: invaid args\n");
+		return;
+	}
+
+	/* FIXME: get all recievers of the message */
+	compose1(m->from, m, 1);
+}
+
+static void
+mesgquit(Mesg *m, char **, int)
+{
+	m->quitting = 1;
+}
+
+static Fn mesgfn[] = {
+	{"Q",		qreply},
+	{"Reply",	reply},
+//	{"Delmesg",	delmesg},
+	{"Save",	nil},
+	{"Del", 	mesgquit},
+	{nil}
+};
+
+static void
+mesgmain(void *mp)
+{
+	char *a, *path, *f[32];
+	Event ev;
+	Mesg *m;
+	Fn *p;
+	int nf;
+
+	m = mp;
+	m->quitting = 0;
+
+	path = estrjoin(mbox.path, m->name, nil);
+	wininit(m, path);
+	free(path);
+
+	wintagwrite(m, "Q Reply all Delmesg Save  ");
+	mesgshow(m);
+	fprint(m->ctl, "clean\n");
+	mbox.nopen++;
+	while(!m->quitting){
+		if(winevent(m, &ev) != 'M')
+			continue;
+		fprint(2, "%s\n", ev.text);
+		if(strcmp(ev.text, "Del") == 0)
+			break;
+		switch(ev.type){
+		case 'l':
+		case 'L':
+			if((a = matchaddr(m, &ev)) != nil)
+				compose1(a, nil, 0);
+			else if(matchmesg(m, ev.text))
+				mesgopen(ev.text, nil);
+			else
+				winsendevent(m, &ev);
+			free(a);
+			break;
+		case 'x':
+		case 'X':
+			if((nf = tokenize(ev.text, f, nelem(f))) == 0)
+				continue;
+			for(p = mesgfn; p->name != nil; p++){
+				if(strcmp(p->name, f[0]) == 0 && p->fn != nil){
+					p->fn(m, &f[1], nf - 1);
+					break;
+				}
+			}
+			if(p->fn == nil)
+				winsendevent(m, &ev);
+			break;
+		}
+	}
+	mbox.nopen--;
+	m->flags &= ~Fopen;
+	winclose(m);
+	threadexits(nil);
+}
+
+void
+mesgpath2name(char *buf, int nbuf, char *name)
+{
+	char *e;
+	int n;
+
+	n = strlen(mbox.path);
+	if(strncmp(name, mbox.path, n) == 0){
+		e = strecpy(buf, buf + nbuf - 2, name + n);
+		e[0] = '/';
+		e[1] = 0;
+	}else
+		strecpy(buf, buf+nbuf, name);
+}
+
+int
+mesgmatch(Mesg *m, char *name, char *digest)
+{
+	if(m->flags & Fdummy)
+		return 0;
+	if(strcmp(m->name, name) == 0)
+		return digest == nil || strcmp(m->digest, digest) == 0;
+	return 0;
+}
+
+Mesg*
+mesglookup(char *name, char *digest)
+{
+	char buf[32];
+	int i;
+
+	mesgpath2name(buf, sizeof(buf), name);
+	for(i = 0; i < mbox.nmesg; i++)
+		if(mesgmatch(mbox.mesg[i], buf, digest))
+			return mbox.mesg[i];
+	return nil;
+}
+
+void
+mesgopen(char *name, char *digest)
+{
+	Mesg *m;
+	char *path;
+	int fd;
+
+	m = mesglookup(name, digest);
+	if(m == nil || (m->flags & Fopen))
+		return;
+	assert(!(m->flags & Fdummy));
+	m->flags |= Fopen;
+	if(m->flags & Funseen){
+		m->flags &= ~Funseen;
+		path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
+		if((fd = open(path, OWRITE)) != -1){
+			fprint(fd, "+s");
+			close(fd);
+		}
+		mbredraw(m, 0, 0);
+		free(path);
+	}
+	threadcreate(mesgmain, m, Stack);
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,15 @@
+</$objtype/mkfile
+
+TARG=Nail
+OFILES=\
+	mbox.$O\
+	mesg.$O\
+	comp.$O\
+	util.$O\
+	win.$O
+
+HFILES=mail.h
+
+BIN=/acme/bin/$objtype
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/util.c
@@ -1,0 +1,140 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+void *
+emalloc(ulong n)
+{
+	void *v;
+	
+	v = mallocz(n, 1);
+	if(v == nil)
+		sysfatal("malloc: %r");
+	setmalloctag(v, getcallerpc(&n));
+	return v;
+}
+
+void *
+erealloc(void *p, ulong n)
+{
+	void *v;
+	
+	v = realloc(p, n);
+	if(v == nil)
+		sysfatal("realloc: %r");
+	setmalloctag(v, getcallerpc(&p));
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	s = strdup(s);
+	if(s == nil)
+		sysfatal("strdup: %r");
+	setmalloctag(s, getcallerpc(&s));
+	return s;
+}
+
+char*
+estrjoin(char *s, ...)
+{
+	va_list ap;
+	char *r, *t, *p, *e;
+	int n;
+
+	va_start(ap, s);
+	n = strlen(s) + 1;
+	while((p = va_arg(ap, char*)) != nil)
+		n += strlen(p);
+	va_end(ap);
+
+	r = emalloc(n);
+	e = r + n;
+	va_start(ap, s);
+	t = strecpy(r, e, s);
+	while((p = va_arg(ap, char*)) != nil)
+		t = strecpy(t, e, p);
+	va_end(ap);
+	return r;
+}
+
+char*
+esmprint(char *fmt, ...)
+{
+	char *s;
+	va_list ap;
+
+	va_start(ap, fmt);
+	s = vsmprint(fmt, ap);
+	va_end(ap);
+	if(s == nil)
+		sysfatal("smprint: %r");
+	setmalloctag(s, getcallerpc(&fmt));
+	return s;
+}
+
+char*
+fslurp(int fd, int *nbuf)
+{
+	int n, sz, r;
+	char *buf;
+
+	n = 0;
+	sz = 128;
+	buf = emalloc(sz);
+	while(1){
+		r = read(fd, buf + n, sz - n);
+		if(r == 0)
+			break;
+		if(r == -1)
+			goto error;
+		n += r;
+		if(n == sz){
+			sz += sz/2;
+			buf = erealloc(buf, sz);
+		}
+	}
+	buf[n] = 0;
+	if(nbuf)
+		*nbuf = n;
+	return buf;
+error:
+	free(buf);
+	return nil;
+}
+
+char *
+rslurp(Mesg *m, char *f, int *nbuf)
+{
+	char *path;
+	int fd;
+	char *r;
+
+	if(m == nil)
+		path = estrjoin(mbox.path, "/", f, nil);
+	else
+		path = estrjoin(mbox.path, "/", m->name, "/", f, nil);
+	fd = open(path, OREAD);
+	free(path);
+	if(fd == -1)
+		return nil;
+	r = fslurp(fd, nbuf);
+	close(fd);
+	return r;
+}
+
+u32int
+strhash(char *s)
+{
+	u32int h, c;
+
+	h = 5381;
+	while(c = *s++ & 0xff)
+		h = ((h << 5) + h) + c;
+	return h;
+}
--- /dev/null
+++ b/win.c
@@ -1,0 +1,308 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+static int
+procrd(Biobufhdr *f, void *buf, long len)
+{
+	return ioread(f->aux, f->fid, buf, len);
+}
+
+static int
+procwr(Biobufhdr *f, void *buf, long len)
+{
+	return iowrite(f->aux, f->fid, buf, len);
+}
+
+/*
+ * NB: this function consumes integers with
+ * a trailing space, as generated by acme;
+ * it's not a general purpose number parsing
+ * function.
+ */
+static int
+evgetnum(Biobuf *f)
+{
+	int c, n;
+
+	n = 0;
+	while('0'<=(c=Bgetc(f)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		sysfatal("event number syntax: %c(%d)", c, c);
+	return n;
+}
+
+static int
+evgetdata(Biobuf *f, Event *e)
+{
+	int i, n, o;
+	Rune r;
+
+	o = 0;
+	n = evgetnum(f);
+	for(i = 0; i < n; i++){
+		if((r = Bgetrune(f)) == -1)
+			break;
+		o += runetochar(e->text + o, &r);
+	}
+	e->text[o] = 0;
+	return o;
+}
+
+int
+winevent(Win *w, Event *e)
+{
+	e->action = Bgetc(w->event);
+	e->type = Bgetc(w->event);
+	e->q0 = evgetnum(w->event);
+	e->q1 = evgetnum(w->event);
+	e->flags = evgetnum(w->event);
+	e->ntext = evgetdata(w->event, e);
+	if(Bgetc(w->event) != '\n')
+		sysfatal("unterminated message");
+	return e->action;
+}
+
+void
+winsendevent(Win *w, Event *e)
+{
+	Bprint(w->event, "%c%c%d %d\n", e->action, e->type, e->q0, e->q1);
+	Bflush(w->event);
+}
+
+int
+winopen(Win *w, char *f, int mode)
+{
+	char buf[128];
+	int fd;
+
+	snprint(buf, sizeof(buf), "/mnt/wsys/%d/%s", w->id, f);
+	if((fd = open(buf, mode|OCEXEC)) == -1)
+		sysfatal("open %s: %r", buf);
+	return fd;
+}
+
+Biobuf*
+bwinopen(Win *w, char *f, int mode)
+{
+	char buf[128];
+	Biobuf *bfd;
+
+	snprint(buf, sizeof(buf), "/mnt/wsys/%d/%s", w->id, f);
+	if((bfd = Bopen(buf, mode|OCEXEC)) == nil)
+		sysfatal("open %s: %r", buf);
+	bfd->aux = w->io;
+	Biofn(bfd, (mode == OREAD)?procrd:procwr);
+	return bfd;
+}
+
+Biobuf*
+bwindata(Win *w, int mode)
+{
+	int fd;
+
+	if((fd = dup(w->data, -1)) == -1)
+		sysfatal("dup: %r");
+	return Bfdopen(fd, mode);
+}
+
+void
+wininit(Win *w, char *name)
+{
+	char buf[12];
+
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl < 0)
+		sysfatal("winopen: %r");
+	if(read(w->ctl, buf, 12)!=12)
+		sysfatal("read ctl: %r");
+	if(fprint(w->ctl, "name %s\n", name) == -1)
+		sysfatal("write ctl: %r");
+	if(fprint(w->ctl, "noscroll\n") == -1)
+		sysfatal("write ctl: %r");
+	if((w->io = ioproc()) == nil)
+		sysfatal("ioproc alloc: %r");
+	w->id = atoi(buf);
+	w->event = bwinopen(w, "event", OREAD);
+	w->addr = winopen(w, "addr", ORDWR);
+	w->data = winopen(w, "data", ORDWR);
+}
+
+void
+winclose(Win *w)
+{
+	fprint(w->ctl, "del\n");
+	if(w->data != -1)
+		close(w->data);
+	if(w->addr != -1)
+		close(w->addr);
+	if(w->event != nil)
+		Bterm(w->event);
+	if(w->io)
+		closeioproc(w->io);
+	if(w->ctl != -1)
+		close(w->ctl);
+}
+
+void
+wintagwrite(Win *w, char *s)
+{
+	int fd, n;
+
+	n = strlen(s);
+	fd = winopen(w, "tag", OWRITE);
+	if(write(fd, s, n) != n)
+		sysfatal("tag write: %r");
+	close(fd);
+}
+
+int
+wineval(Win *w, char *s, ...)
+{
+	char buf[25];
+	va_list arg;
+
+	va_start(arg, s);
+	vfprint(w->addr, s, arg);
+	va_end(arg);
+	if(pread(w->addr, buf, 24, 0) != 24)
+		return -1;
+	buf[24] = 0;
+	return strtol(buf, nil, 10);
+}
+
+int
+winread(Win *w, int q0, int q1, char *data, int ndata)
+{
+	int m, n, nr;
+	char *buf;
+
+	m = q0;
+	buf = emalloc(Bufsz);
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n){
+			fprint(2, "error writing addr: %r");
+			goto err;
+		}
+		n = read(w->data, buf, Bufsz);
+		if(n <= 0){
+			fprint(2, "reading data: %r");
+			goto err;
+		}
+		nr = utfnlen(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0 || n > ndata)
+			break;
+		memmove(data, buf, n);
+		ndata -= n;
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+	free(buf);
+	return 0;
+err:
+	free(buf);
+	return -1;
+}
+
+char*
+winreadsel(Win *w)
+{
+	int n, q0, q1;
+	char *r;
+
+	wingetsel(w, &q0, &q1);
+	n = UTFmax*(q1-q0);
+	r = emalloc(n + 1);
+	if(winread(w, q0, q1, r, n) == -1){
+		free(r);
+		return nil;
+	}
+	return r;
+}
+
+void
+wingetsel(Win *w, int *q0, int *q1)
+{
+	char *e, buf[25];
+
+	fprint(w->ctl, "addr=dot");
+	if(pread(w->addr, buf, 24, 0) != 24)
+		sysfatal("read addr: %r");
+	buf[24] = 0;
+	*q0 = strtol(buf, &e, 10);
+	*q1 = strtol(e, nil, 10);
+}
+
+void
+winsetsel(Win *w, int q0, int q1)
+{
+	fprint(w->addr, "#%d,#%d", q0, q1);
+	fprint(w->ctl, "dot=addr");
+}
+
+static char*
+expandaddr(Win *w, Event *e)
+{
+	static char *delim = "/[ \t\\n<>()\\[\\]]/";
+	char *s;
+	int q0, q1, ns;
+
+	if(e->q0 != e->q1)
+		return nil;
+
+	q0 = wineval(w, "#%d-%s", e->q0, delim);
+	if(q0 == -1)	/* bad char not found */
+		q0 = 0;
+	else			/* increment past bad char */
+		q0++;
+
+	q1 = wineval(w, "#%d+%s", e->q0, delim);
+	if(q1 < 0){
+		q1 = wineval(w, "$");
+		if(q1 < 0)
+			return nil;
+	}
+	if(q0 >= q1)
+		return nil;
+	ns = (q1-q0)*UTFmax+1;
+	s = emalloc(ns);
+	winread(w, q0, q1, s, ns);
+	return s;
+}
+
+char*
+matchaddr(Win *w, Event *e)
+{
+	char *s;
+
+	if((s = expandaddr(w, e)) != nil)
+		if(regexec(addrpat, s, nil, 0))
+			return s;
+	return nil;
+}
+
+int
+matchmesg(Win *, char *text)
+{
+	char *p;
+
+	if(strncmp(text, mbox.path, strlen(mbox.path)) == 0)
+		return 1;
+	if(regexec(mesgpat, text, nil, 0)){
+		if((p = strchr(text, '/')) != nil)
+			p[1] = 0;
+		return 1;
+	}
+	return 0;
+}