ref: 8f73c6436a0a0a39f6e1de79b4fd6f21af38d841
dir: /mbox.c/
#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)
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 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->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(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)
{
if(mbox.nopen > 0)
fprint(2, "Del: %d open messages", mbox.nopen);
winclose(&mbox);
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 && !(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(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);
}