ref: 893c8afdd86aa3c4840f996bfe8dbb25587bc0ea
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) 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); }