shithub: Nail

ref: 893c8afdd86aa3c4840f996bfe8dbb25587bc0ea
dir: /mbox.c/

View raw version
#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	= "mbox";
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)
			break;
		sendp(mbox.event, ev);
	}
	closeioproc(mbox.io);
	sendp(mbox.event, nil);
	threadexits(nil);
}

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 int
rcmpmesg(void *pa, void *pb)
{
	Mesg *a, *b;

	a = *(Mesg**)pa;
	b = *(Mesg**)pb;

	return a->time - b->time;
}

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;
	qsort(p->child, p->nchild, sizeof(Mesg*), rcmpmesg);
	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)*sizeof(Mesg*));
	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;
	int nc;

	if(strncmp(name, mbox.path, strlen(mbox.path)) == 0)
		name += strlen(mbox.path);
	if((m = mesgload(name)) == nil)
		goto error;

	if(digest != nil && strcmp(digest, m->digest) != 0){
		fprint(2, "mismatched digest: %s %s\n", m->digest, digest);
		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 m;
	}

	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);
	fprint(mbox.addr, "%d%s", ln+1, add ? "-#0" : "");
	bfd = bwindata(&mbox, OWRITE);
	showmesg(bfd, m, depth, rec);
	Bterm(bfd);

	/* highlight the redrawn message */
	fprint(mbox.addr, "%d%s", ln+1, add ? "-#0" : "");
	fprint(mbox.ctl, "dot=addr\n");
}

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);
	}
	qsort(mbox.mesg, mbox.nmesg, sizeof(Mesg*), cmpmesg);	
}

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->from);
		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(p != nil)
			addchild(p, c);
		else
			c->flags |= Ftoplev;
	}
}

static void
mbflush(char **, int)
{
	int i, j, ln, fd;
	char *path;
	Mesg *m;

	i = 0;
	path = estrjoin(maildir, "/ctl", nil);
	fd = open(path, OWRITE);
	free(path);
	if(fd == -1)
		sysfatal("open mbox: %r");
	while(i < mbox.nmesg){
		m = mbox.mesg[i];
		if((m->flags & Fopen) || !(m->flags & (Fdel|Ftodel))){
			i++;
			continue;
		}
		ln = mesglineno(m, nil);
		fprint(2, "remove %s@%d,%d\n", m->name, ln+1, ln+1+m->nsub);
		fprint(mbox.addr, "%d,%d", ln+1, ln+1+m->nsub);
		write(mbox.data, "", 0);
		if(m->flags & Ftodel)
			fprint(fd, "delete %s %d", mailbox, atoi(m->name));

		removemesg(m);
		removeid(m);
		for(j = 0; j < m->nchild; j++)
			mbredraw(m->child[j], 1, 1);
		mesgfree(m);
		memmove(&mbox.mesg[i], &mbox.mesg[i+1], (mbox.nmesg - i)*sizeof(Mesg*));
		mbox.nmesg--;
	}
	close(fd);

}

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)
{
	Event *e;
	Ioproc *io;

	if(mbox.nopen > 0){
		fprint(2, "Del: %d open messages\n", mbox.nopen);
		return;
	}

	/*
	 * Do a bit of a dance to ensure that we don't
	 * try to read from the ioproc after we close
	 * it while exiting.
	 */
	io = mbox.io;
	mbox.io = nil;

	winclose(&mbox);
	iointerrupt(io);
	while((e = recvp(mbox.event)) != nil)	
		free(e);
	closeioproc(io);		
	threadexitsall(nil);
}

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
	{"Redisplay", redisplay},
	{"Filter", filter},
	{"Get", mbrefresh},
	{"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)
			compose(a, nil, 0, 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)
			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(mbox.ctl, "clean\n");
	proccreate(eventread, nil, Stack);
	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();

	threadcreate(mbmain, nil, Stack);
	proccreate(plumbsee, nil, Stack);
	proccreate(plumbshow, nil, Stack);
}