feat(herbarium): init
9 files changed, -5/+2528
.gitignore
@@ -1,7 +1,8 @@ .dev/ test.sh-*/.o+*/*.o */config.h */hedge */petal */pollen+*/herbarium
herbarium/Makefile
@@ -0,0 +1,36 @@+include config.mk++SRC = herbarium.c+OBJ = ${SRC:.c=.o}++all: herbarium++.c.o:+ ${CC} -c ${CFLAGS} $<++${OBJ}: config.h config.mk++config.h:+ cp config.def.h $@++herbarium: ${OBJ}+ ${CC} -o $@ ${OBJ} ${LDFLAGS}++clean:+ rm -f herbarium ${OBJ}++install: all+ mkdir -p ${DESTDIR}${PREFIX}/bin+ cp -f herbarium ${DESTDIR}${PREFIX}/bin+ chmod 755 ${DESTDIR}${PREFIX}/bin/herbarium+ mkdir -p ${DESTDIR}${PREFIX}/share/herbarium+ cp -f style.css icon.svg ${DESTDIR}${PREFIX}/share/herbarium+ chmod 644 ${DESTDIR}${PREFIX}/share/herbarium/style.css\+ ${DESTDIR}${PREFIX}/share/herbarium/icon.svg++uninstall:+ rm -f ${DESTDIR}${PREFIX}/bin/herbarium\+ ${DESTDIR}${PREFIX}/share/herbarium/style.css\+ ${DESTDIR}${PREFIX}/share/herbarium/icon.svg++.PHONY: all clean install uninstall
herbarium/config.def.h
@@ -0,0 +1,17 @@+static const char *root = "/srv/lists";+static const char *domain = "lists.lostduk.com";+static const char *css = "/style.css";+static const char *title = "greenhouse";+static const char *icon = "/icon.svg";+static const char *service = "lists";+static const int perpage = 50;+static const int maxmsgs = 20000;+static const int maxhdr = 64 * 1024;+static const int maxbody = 512 * 1024;+static const int maxdepth = 8;+static const int maxcpu = 10;++static const Service services[] = {+ { "git", "https://git.lostduk.com/" },+ { "lists", "https://lists.lostduk.com/" },+};
herbarium/config.mk
@@ -0,0 +1,7 @@+PREFIX = /usr/local++CPPFLAGS = -D_XOPEN_SOURCE=700+CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os ${CPPFLAGS}+LDFLAGS =++CC = cc
herbarium/herbarium.c
@@ -0,0 +1,2001 @@+/* imports */+#include <ctype.h>+#include <dirent.h>+#include <errno.h>+#include <fcntl.h>+#include <iconv.h>+#include <limits.h>+#include <stdarg.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <strings.h>+#include <time.h>+#include <unistd.h>+#include <sys/resource.h>+#include <sys/stat.h>++/* macros */+#define LENGTH(X) (sizeof(X) / sizeof((X)[0]))+#define HOUR 3600L+#define DAY (24 * HOUR)+#define MAXTIME 253402300799LL+#define FIELDLEN 998+#define MAXREFS 16+#define NONE ((size_t)-1)+#define REPL "\xef\xbf\xbd"++/* enums */+typedef struct {+ char *s;+ size_t n, cap;+} Buf;++typedef struct {+ char *file;+ char *id;+ char *irt;+ char *refs[MAXREFS];+ size_t nrefs;+ char *subject;+ char *name;+ char *addr;+ long long mtime;+ long long date;+ int tz;+ size_t parent;+ size_t root;+ size_t seen;+} Msg;++typedef struct {+ const char *body;+ size_t len;+ char type[64];+ char cte[32];+ char charset[64];+ char boundary[128];+ char name[512];+} Part;++typedef struct {+ const char *name;+ const char *url;+} Service;++typedef struct {+ size_t root;+ size_t n;+ long long last;+} Thread;++/* function declarations */+static void attachment(const Part *p);+static void b64(const char *s, size_t n, Buf *out);+static const char *boundary(const char *s, const char *end, const char *b,+ size_t blen);+static int bprintf(char *buf, size_t len, const char *fmt, ...);+static void bufadd(Buf *b, const char *s, size_t n);+static void bufc(Buf *b, int c);+static size_t byid(const char *id);+static char *clean(const char *s, size_t n);+static int cmpdate(const void *a, const void *b);+static int cmpfirst(const void *a, const void *b);+static int cmpid(const void *a, const void *b);+static int cmpmtime(const void *a, const void *b);+static int cmpthread(const void *a, const void *b);+static void decode(const Part *p, Buf *out);+static void decode2047(const char *s, Buf *out);+static void die(const char *msg);+static size_t digits(const char **s, size_t max, long *v);+static const char *encword(const char *p, Buf *out);+static const char *envor(const char *name, const char *fallback);+static char *field(const char *v);+static void freemsg(Msg *m);+static void freemsgs(void);+static size_t hdrend(const char *s, size_t n, size_t *body);+static char *hdrget(const char *h, size_t n, const char *name);+static int hex(int c);+static void httpheader(int status, const char *type);+static void indexpage(void);+static void indexrow(const char *name);+static int islist(const char *name);+static int isstat(const char *s, const char *end);+static const char *lineclass(const char *l, size_t len, const char *next,+ const char *end, int *patch);+static void listheader(int addrs);+static void listpage(void);+static int loadmsg(Msg *m);+static void mboxpage(size_t t);+static void msgurl(const char *id, const char *page);+static void multipart(const Part *p, int depth, size_t m);+static char *nextid(const char **sp);+static void notfound(void);+static FILE *openmsg(const char *file);+static void pagebegin(int status, const char *pagetitle);+static void pageend(void);+static int param(const char *v, const char *attr, char *buf, size_t len);+static int parsedate(const char *s, long long *t, int *tz);+static void parsefrom(const char *v, Msg *m);+static void parsepart(const char *s, size_t n, Part *p);+static void printage(long long t);+static void printbody(const char *s, size_t n);+static void printmsg(size_t i, int sel);+static void printtime(long long t, int tz, const char *fmt);+static void qp(const char *s, size_t n, int q, Buf *out);+static void rawpage(size_t t);+static int readfile(const char *file, size_t max, Buf *b, int hdronly);+static void readline(const char *name, const char *file, char *buf,+ size_t len);+static void renderpart(const char *s, size_t n, int depth, size_t m);+static void route(char *path);+static void sanitize(const char *s, size_t n, Buf *out, int multi);+static void scan(const char *name);+static const char *skipcfws(const char *s);+static void threadmsgs(void);+static void threadpage(size_t t);+static int tou8(const char *cs, const char *s, size_t n, Buf *out);+static void urlencode(const char *s);+static int validid(const char *s, size_t n);+static int validlist(const char *s);+static void xmlencode(const char *s);+static void xmlencoden(const char *s, size_t len);+static char *xstrndup(const char *s, size_t n);++/* variables */+static const char *base;+static const char *list;+static Msg *msgs;+static size_t nmsgs;+static size_t *ids;+static size_t left;+static int cut;+static long ofs;++#include "config.h"++/* function implementations */+void+attachment(const Part *p)+{+ char *name;+ size_t i, n = p->len;++ if (!strcmp(p->cte, "base64")) {+ for (i = n = 0; i < p->len; i++)+ if (isalnum((unsigned char)p->body[i])+ || p->body[i] == '+' || p->body[i] == '/')+ n++;+ n = n * 3 / 4;+ }+ name = field(*p->name ? p->name : NULL);+ if (strlen(name) + 64 > left) {+ cut = 1;+ free(name);+ return;+ }+ left -= strlen(name) + 64;+ fputs("<p class=\"att\">attachment: ", stdout);+ xmlencode(*name ? name : "(unnamed)");+ fputs(" (", stdout);+ xmlencode(p->type);+ printf(", %zuB)</p>\n", n);+ free(name);+}++void+b64(const char *s, size_t n, Buf *out)+{+ unsigned long v = 0;+ size_t i;+ int c, bits = 0;++ for (i = 0; i < n; i++) {+ c = (unsigned char)s[i];+ if (c >= 'A' && c <= 'Z')+ c -= 'A';+ else if (c >= 'a' && c <= 'z')+ c -= 'a' - 26;+ else if (c >= '0' && c <= '9')+ c -= '0' - 52;+ else if (c == '+')+ c = 62;+ else if (c == '/')+ c = 63;+ else {+ if (c == '=')+ bits = 0;+ continue;+ }+ v = (v << 6 | (unsigned long)c) & 0xffffff;+ if ((bits += 6) >= 8) {+ bits -= 8;+ bufc(out, (int)(v >> bits & 0xff));+ }+ }+}++const char *+boundary(const char *s, const char *end, const char *b, size_t blen)+{+ const char *p;++ while (s < end) {+ if ((size_t)(end - s) >= blen + 2 && s[0] == '-' && s[1] == '-'+ && !memcmp(s + 2, b, blen)+ && (s + blen + 2 == end+ || (s[blen + 2] && strchr("\r\n \t-", s[blen + 2]))))+ return s;+ if (!(p = memchr(s, '\n', end - s)))+ break;+ s = p + 1;+ }+ return NULL;+}++int+bprintf(char *buf, size_t len, const char *fmt, ...)+{+ va_list ap;+ int n;++ va_start(ap, fmt);+ n = vsnprintf(buf, len, fmt, ap);+ va_end(ap);+ if (n < 0 || (size_t)n >= len)+ return -1;+ return 0;+}++void+bufadd(Buf *b, const char *s, size_t n)+{+ size_t cap;+ char *p;++ if (b->n + n + 1 > b->cap) {+ for (cap = b->cap ? b->cap : 256; cap < b->n + n + 1; cap *= 2)+ ;+ if (!(p = realloc(b->s, cap)))+ die("out of memory");+ b->s = p;+ b->cap = cap;+ }+ if (n)+ memcpy(b->s + b->n, s, n);+ b->n += n;+ b->s[b->n] = '\0';+}++void+bufc(Buf *b, int c)+{+ char ch = (char)c;++ bufadd(b, &ch, 1);+}++size_t+byid(const char *id)+{+ size_t lo = 0, hi = nmsgs, mid;++ while (lo < hi) {+ mid = lo + (hi - lo) / 2;+ if (strcmp(msgs[ids[mid]].id, id) < 0)+ lo = mid + 1;+ else+ hi = mid;+ }+ if (lo < nmsgs && !strcmp(msgs[ids[lo]].id, id))+ return ids[lo];+ return NONE;+}++char *+clean(const char *s, size_t n)+{+ Buf b = { 0 };+ char *r;+ size_t i;++ sanitize(s, n, &b, 0);+ bufadd(&b, "", 0);+ for (i = 0; b.s[i] == ' '; i++)+ ;+ for (n = b.n; n > i && b.s[n - 1] == ' '; n--)+ ;+ if (n - i > FIELDLEN) {+ for (n = i + FIELDLEN; n > i+ && ((unsigned char)b.s[n] & 0xc0) == 0x80; n--)+ ;+ }+ r = xstrndup(b.s + i, n - i);+ free(b.s);+ return r;+}++int+cmpdate(const void *a, const void *b)+{+ const Msg *x = a, *y = b;++ if (x->date != y->date)+ return x->date < y->date ? -1 : 1;+ if (x->mtime != y->mtime)+ return x->mtime < y->mtime ? -1 : 1;+ return strcmp(x->file, y->file);+}++int+cmpfirst(const void *a, const void *b)+{+ const Msg *x = a, *y = b;+ int c;++ if ((c = strcmp(x->id, y->id)))+ return c;+ if (x->mtime != y->mtime)+ return x->mtime < y->mtime ? -1 : 1;+ return strcmp(x->file, y->file);+}++int+cmpid(const void *a, const void *b)+{+ size_t x = *(const size_t *)a, y = *(const size_t *)b;+ int c;++ if ((c = strcmp(msgs[x].id, msgs[y].id)))+ return c;+ return x < y ? -1 : x > y;+}++int+cmpmtime(const void *a, const void *b)+{+ const Msg *x = a, *y = b;++ if (x->mtime != y->mtime)+ return x->mtime > y->mtime ? -1 : 1;+ return strcmp(x->file, y->file);+}++int+cmpthread(const void *a, const void *b)+{+ const Thread *x = a, *y = b;++ if (x->last != y->last)+ return x->last > y->last ? -1 : 1;+ return strcmp(msgs[x->root].id, msgs[y->root].id);+}++void+decode(const Part *p, Buf *out)+{+ if (!strcmp(p->cte, "base64"))+ b64(p->body, p->len, out);+ else if (!strcmp(p->cte, "quoted-printable"))+ qp(p->body, p->len, 0, out);+ else+ bufadd(out, p->body, p->len);+}++void+decode2047(const char *s, Buf *out)+{+ const char *q, *r;+ int enc = 0;++ while (*s) {+ if (s[0] == '=' && s[1] == '?' && (q = encword(s, out))) {+ s = q;+ enc = 1;+ continue;+ }+ if (enc && isspace((unsigned char)*s)) {+ for (q = s; isspace((unsigned char)*q); q++)+ ;+ if (q[0] == '=' && q[1] == '?'+ && (r = encword(q, out))) {+ s = r;+ continue;+ }+ bufadd(out, s, q - s);+ s = q;+ enc = 0;+ continue;+ }+ bufc(out, *s++);+ enc = 0;+ }+}++void+die(const char *msg)+{+ fprintf(stderr, "herbarium: %s\n", msg);+ exit(1);+}++size_t+digits(const char **s, size_t max, long *v)+{+ size_t n;++ *v = 0;+ for (n = 0; isdigit((unsigned char)(*s)[n]); n++)+ if (n < max)+ *v = *v * 10 + (*s)[n] - '0';+ *s += n;+ return n > max ? 0 : n;+}++const char *+encword(const char *p, Buf *out)+{+ Buf raw = { 0 };+ char cs[64];+ const char *q, *e;+ size_t n;+ int enc, r;++ q = p + 2;+ for (n = 0; q[n] != '?'; n++)+ if (!q[n] || n + 1 >= sizeof(cs)+ || isspace((unsigned char)q[n]))+ return NULL;+ memcpy(cs, q, n);+ cs[n] = '\0';+ q += n;+ enc = tolower((unsigned char)q[1]);+ if ((enc != 'b' && enc != 'q') || q[2] != '?')+ return NULL;+ q += 3;+ for (e = q; !(e[0] == '?' && e[1] == '='); e++)+ if (!*e || isspace((unsigned char)*e))+ return NULL;+ if (enc == 'b')+ b64(q, e - q, &raw);+ else+ qp(q, e - q, 1, &raw);+ r = tou8(cs, raw.s, raw.n, out);+ free(raw.s);+ return r < 0 ? NULL : e + 2;+}++const char *+envor(const char *name, const char *fallback)+{+ const char *v;++ v = getenv(name);+ return v && *v ? v : fallback;+}++char *+field(const char *v)+{+ Buf b = { 0 };+ char *s, *r;+ size_t n;++ if (!v)+ return xstrndup("", 0);+ n = strlen(v);+ s = xstrndup(v, n > 4 * FIELDLEN ? 4 * FIELDLEN : n);+ decode2047(s, &b);+ r = clean(b.s, b.n);+ free(s);+ free(b.s);+ return r;+}++void+freemsg(Msg *m)+{+ size_t i;++ free(m->file);+ free(m->id);+ free(m->irt);+ for (i = 0; i < m->nrefs; i++)+ free(m->refs[i]);+ free(m->subject);+ free(m->name);+ free(m->addr);+}++void+freemsgs(void)+{+ size_t i;++ for (i = 0; i < nmsgs; i++)+ freemsg(&msgs[i]);+ free(msgs);+ free(ids);+ msgs = NULL;+ ids = NULL;+ nmsgs = 0;+}++size_t+hdrend(const char *s, size_t n, size_t *body)+{+ const char *p;+ size_t pos = 0;++ while (pos < n) {+ if (s[pos] == '\n') {+ *body = pos + 1;+ return pos;+ }+ if (s[pos] == '\r' && pos + 1 < n && s[pos + 1] == '\n') {+ *body = pos + 2;+ return pos;+ }+ if (!(p = memchr(s + pos, '\n', n - pos)))+ break;+ pos = p - s + 1;+ }+ *body = n;+ return n;+}++char *+hdrget(const char *h, size_t n, const char *name)+{+ Buf b = { 0 };+ const char *p;+ size_t pos, k, len = strlen(name);+ char *r;++ for (pos = 0; pos < n; pos = p ? (size_t)(p - h) + 1 : n) {+ p = memchr(h + pos, '\n', n - pos);+ if (n - pos <= len || strncasecmp(h + pos, name, len))+ continue;+ for (k = pos + len; k < n && (h[k] == ' ' || h[k] == '\t');+ k++)+ ;+ if (k >= n || h[k] != ':')+ continue;+ bufadd(&b, "", 0);+ for (k++; k < n; k++) {+ if (h[k] == '\n') {+ if (k + 1 >= n || (h[k + 1] != ' '+ && h[k + 1] != '\t'))+ break;+ } else if (h[k] != '\r' && h[k]) {+ bufc(&b, h[k]);+ }+ }+ for (k = 0; isspace((unsigned char)b.s[k]); k++)+ ;+ while (b.n > k && isspace((unsigned char)b.s[b.n - 1]))+ b.n--;+ r = xstrndup(b.s + k, b.n - k);+ free(b.s);+ return r;+ }+ return NULL;+}++int+hex(int c)+{+ if (c >= '0' && c <= '9')+ return c - '0';+ if (c >= 'a' && c <= 'f')+ return c - 'a' + 10;+ if (c >= 'A' && c <= 'F')+ return c - 'A' + 10;+ return -1;+}++void+httpheader(int status, const char *type)+{+ if (status == 404)+ fputs("Status: 404 Not Found\r\n", stdout);+ printf("Content-Type: %s\r\n", type);+ fputs("Content-Security-Policy: default-src 'none'; "+ "style-src 'self'; img-src 'self'\r\n"+ "X-Content-Type-Options: nosniff\r\n\r\n", stdout);+}++void+indexpage(void)+{+ struct dirent **ents;+ int i, n;++ pagebegin(200, title);+ fputs("<main>\n<table class=\"lists\">\n<thead><tr><th>Name</th>"+ "<th class=\"sm\">Description</th>"+ "<th class=\"sm\">Address</th>"+ "<th class=\"r\">Threads</th><th class=\"r\">Updated</th>"+ "</tr></thead>\n<tbody>\n", stdout);+ if ((n = scandir(root, &ents, NULL, alphasort)) >= 0) {+ for (i = 0; i < n; i++) {+ if (islist(ents[i]->d_name))+ indexrow(ents[i]->d_name);+ free(ents[i]);+ }+ free(ents);+ }+ fputs("</tbody>\n</table>\n", stdout);+ pageend();+}++void+indexrow(const char *name)+{+ char desc[256];+ long long last = -1;+ size_t i, threads = 0;++ list = name;+ scan(name);+ for (i = 0; i < nmsgs; i++) {+ if (msgs[i].root == i)+ threads++;+ if (msgs[i].date > last)+ last = msgs[i].date;+ }+ readline(name, "description", desc, sizeof(desc));+ fputs("<tr><td class=\"name\"><a href=\"", stdout);+ xmlencode(base);+ putchar('/');+ urlencode(name);+ fputs("/\">", stdout);+ xmlencode(name);+ fputs("</a></td><td class=\"desc sm\">", stdout);+ xmlencode(desc);+ fputs("</td><td class=\"sm\"><a href=\"mailto:", stdout);+ urlencode(name);+ putchar('@');+ xmlencode(domain);+ fputs("\">", stdout);+ xmlencode(name);+ putchar('@');+ xmlencode(domain);+ printf("</a></td><td class=\"r num\">%zu</td>", threads);+ printage(last);+ fputs("</tr>\n", stdout);+ freemsgs();+}++int+islist(const char *name)+{+ char path[PATH_MAX];+ struct stat st;++ return validlist(name)+ && bprintf(path, sizeof(path), "%s/%s", root, name) == 0+ && stat(path, &st) == 0 && S_ISDIR(st.st_mode)+ && bprintf(path, sizeof(path), "%s/%s/public", root, name) == 0+ && stat(path, &st) == 0;+}++int+isstat(const char *s, const char *end)+{+ const char *nl;+ size_t i, len;++ nl = memchr(s, '\n', end - s);+ len = nl ? (size_t)(nl - s) : (size_t)(end - s);+ if (!len || *s != ' ')+ return 0;+ for (i = 0; i + 3 <= len; i++)+ if (!memcmp(s + i, " | ", 3))+ return 1;+ return 0;+}++const char *+lineclass(const char *l, size_t len, const char *next, const char *end,+ int *patch)+{+ if (len >= 11 && !memcmp(l, "diff --git ", 11)) {+ *patch = 1;+ return "f";+ }+ if (!*patch) {+ if (len == 3 && !memcmp(l, "---", 3) && next+ && isstat(next, end)) {+ *patch = 1;+ return "f";+ }+ return len && *l == '>' ? "q" : NULL;+ }+ if (len == 3 && !memcmp(l, "-- ", 3)) {+ *patch = 0;+ return NULL;+ }+ if ((len == 3 && !memcmp(l, "---", 3))+ || (len >= 4 && (!memcmp(l, "--- ", 4) || !memcmp(l, "+++ ", 4))))+ return "f";+ if (len >= 2 && !memcmp(l, "@@", 2))+ return "h";+ if (len && *l == '+')+ return "i";+ if (len && *l == '-')+ return "d";+ if (len && isalpha((unsigned char)*l))+ return "f";+ return NULL;+}++void+listheader(int addrs)+{+ static const char *cmds[] = { "", "subscribe", "unsubscribe", "help" };+ char desc[256];+ size_t i;++ readline(list, "description", desc, sizeof(desc));+ fputs("<header>\n<h1><a href=\"", stdout);+ xmlencode(base);+ putchar('/');+ urlencode(list);+ fputs("/\">", stdout);+ xmlencode(list);+ fputs("</a></h1>\n", stdout);+ if (*desc) {+ fputs("<p class=\"desc\">", stdout);+ xmlencode(desc);+ fputs("</p>\n", stdout);+ }+ if (addrs) {+ fputs("<table class=\"addrs\">\n", stdout);+ for (i = 0; i < LENGTH(cmds); i++) {+ printf("<tr><td>%s</td><td><a href=\"mailto:", *cmds[i]+ ? cmds[i] : "post");+ urlencode(list);+ printf("%s%s@", *cmds[i] ? "+" : "", cmds[i]);+ xmlencode(domain);+ fputs("\">", stdout);+ xmlencode(list);+ printf("%s%s@", *cmds[i] ? "+" : "", cmds[i]);+ xmlencode(domain);+ fputs("</a></td></tr>\n", stdout);+ }+ fputs("</table>\n", stdout);+ }+ fputs("</header>\n<main>\n", stdout);+}++void+listpage(void)+{+ Thread *t;+ const Msg *m;+ size_t *slot, i, r, n = 0, shown = 0;++ scan(list);+ if (!(t = calloc(nmsgs + 1, sizeof(*t)))+ || !(slot = calloc(nmsgs + 1, sizeof(*slot))))+ die("out of memory");+ for (i = 0; i < nmsgs; i++)+ slot[i] = NONE;+ for (i = 0; i < nmsgs; i++) {+ r = msgs[i].root;+ if (slot[r] == NONE) {+ slot[r] = n;+ t[n].root = r;+ t[n].last = msgs[i].date;+ n++;+ }+ t[slot[r]].n++;+ if (msgs[i].date > t[slot[r]].last)+ t[slot[r]].last = msgs[i].date;+ }+ qsort(t, n, sizeof(*t), cmpthread);++ pagebegin(200, list);+ listheader(1);+ fputs("<table class=\"threads\">\n<thead><tr><th>Subject</th>"+ "<th class=\"sm\">From</th><th class=\"r\">Msgs</th>"+ "<th class=\"r\">Last</th></tr></thead>\n<tbody>\n", stdout);+ for (i = (size_t)ofs; i < n && shown < (size_t)perpage; i++, shown++) {+ m = &msgs[t[i].root];+ fputs("<tr><td class=\"msg\"><a href=\"", stdout);+ msgurl(m->id, "");+ fputs("\">", stdout);+ xmlencode(*m->subject ? m->subject : "(no subject)");+ fputs("</a></td><td class=\"sm\">", stdout);+ xmlencode(*m->name ? m->name : m->addr);+ printf("</td><td class=\"r num\">%zu</td><td class=\"r date\" "+ "title=\"", t[i].n);+ printtime(t[i].last, 0, "%Y-%m-%d %H:%M UTC");+ fputs("\">", stdout);+ printtime(t[i].last, 0, "%Y-%m-%d");+ fputs("</td></tr>\n", stdout);+ }+ fputs("</tbody>\n</table>\n", stdout);+ if (!shown) {+ fputs("<p>No messages.</p>\n", stdout);+ } else if (ofs || i < n) {+ fputs("<nav class=\"pager\">", stdout);+ if (ofs) {+ fputs("<a href=\"", stdout);+ xmlencode(base);+ putchar('/');+ urlencode(list);+ putchar('/');+ if (ofs > perpage)+ printf("?ofs=%ld", ofs - perpage);+ fputs("\">← newer</a>", stdout);+ }+ printf("<span>%ld–%ld</span>", ofs + 1,+ ofs + (long)shown);+ if (i < n) {+ fputs("<a href=\"", stdout);+ xmlencode(base);+ putchar('/');+ urlencode(list);+ printf("/?ofs=%zu\">older →</a>", i);+ }+ fputs("</nav>\n", stdout);+ }+ pageend();+ free(slot);+ free(t);+}++int+loadmsg(Msg *m)+{+ Buf b = { 0 };+ const char *p;+ char *v;+ size_t h, body;++ if (readfile(m->file, (size_t)maxhdr, &b, 1) < 0)+ return -1;+ h = hdrend(b.s, b.n, &body);+ if ((v = hdrget(b.s, h, "Message-ID"))) {+ p = v;+ if (!(m->id = nextid(&p)) && validid(v, strlen(v)))+ m->id = xstrndup(v, strlen(v));+ free(v);+ }+ if (!m->id) {+ free(b.s);+ return -1;+ }+ if ((v = hdrget(b.s, h, "In-Reply-To"))) {+ p = v;+ m->irt = nextid(&p);+ free(v);+ }+ if ((v = hdrget(b.s, h, "References"))) {+ for (p = v; *p;) {+ if (!(m->refs[m->nrefs] = nextid(&p)))+ break;+ if (++m->nrefs == MAXREFS) {+ free(m->refs[0]);+ memmove(m->refs, m->refs + 1,+ (MAXREFS - 1) * sizeof(*m->refs));+ m->nrefs--;+ }+ }+ free(v);+ }+ v = hdrget(b.s, h, "Subject");+ m->subject = field(v);+ free(v);+ v = hdrget(b.s, h, "From");+ parsefrom(v ? v : "", m);+ free(v);+ v = hdrget(b.s, h, "Date");+ if (!v || parsedate(v, &m->date, &m->tz) < 0+ || m->date > m->mtime + DAY) {+ m->date = m->mtime;+ m->tz = 0;+ }+ free(v);+ free(b.s);+ return 0;+}++void+mboxpage(size_t t)+{+ char *line = NULL;+ size_t i, k, cap = 0, r = msgs[t].root;+ ssize_t len;+ FILE *fp;+ int last;++ httpheader(200, "application/mbox");+ for (i = 0; i < nmsgs; i++) {+ if (msgs[i].root != r || !(fp = openmsg(msgs[i].file)))+ continue;+ fputs("From mboxrd@z Thu Jan 1 00:00:00 1970\n", stdout);+ last = '\n';+ while ((len = getline(&line, &cap, fp)) > 0) {+ for (k = 0; k < (size_t)len && line[k] == '>'; k++)+ ;+ if ((size_t)len - k >= 5+ && !memcmp(line + k, "From ", 5))+ putchar('>');+ fwrite(line, 1, len, stdout);+ last = line[len - 1];+ }+ if (last != '\n')+ putchar('\n');+ putchar('\n');+ fclose(fp);+ }+ free(line);+}++void+msgurl(const char *id, const char *page)+{+ xmlencode(base);+ putchar('/');+ urlencode(list);+ putchar('/');+ urlencode(id);+ putchar('/');+ fputs(page, stdout);+}++void+multipart(const Part *p, int depth, size_t m)+{+ const char *s, *end, *next, *pick = NULL;+ size_t blen, len, plen = 0;+ int alt, rank, best = 0;+ Part sub;++ end = p->body + p->len;+ blen = strlen(p->boundary);+ alt = !strcmp(p->type, "multipart/alternative");+ for (s = boundary(p->body, end, p->boundary, blen); s && !cut;+ s = next) {+ s += blen + 2;+ if (end - s >= 2 && s[0] == '-' && s[1] == '-')+ break;+ if (!(s = memchr(s, '\n', end - s)))+ break;+ s++;+ next = boundary(s, end, p->boundary, blen);+ len = (next ? next : end) - s;+ if (len && s[len - 1] == '\n')+ len--;+ if (len && s[len - 1] == '\r')+ len--;+ if (!alt) {+ renderpart(s, len, depth + 1, m);+ continue;+ }+ parsepart(s, len, &sub);+ if (!strcmp(sub.type, "text/plain"))+ rank = 3;+ else if (!strncmp(sub.type, "multipart/", 10))+ rank = 2;+ else+ rank = 1;+ if (rank > best || (rank == 1 && best == 1)) {+ best = rank;+ pick = s;+ plen = len;+ }+ }+ if (pick)+ renderpart(pick, plen, depth + 1, m);+}++char *+nextid(const char **sp)+{+ const char *a, *b;++ for (a = strchr(*sp, '<'); a; a = b) {+ if (!(b = strpbrk(a + 1, "<>")))+ break;+ if (*b == '<')+ continue;+ *sp = b + 1;+ if (validid(a + 1, b - a - 1))+ return xstrndup(a + 1, b - a - 1);+ b = strchr(b + 1, '<');+ }+ *sp += strlen(*sp);+ return NULL;+}++void+notfound(void)+{+ pagebegin(404, "Not found");+ fputs("<main>\n<p>Not found.</p>\n", stdout);+ pageend();+}++FILE *+openmsg(const char *file)+{+ struct stat st;+ FILE *fp;+ int fd;++ if ((fd = open(file, O_RDONLY | O_NOFOLLOW | O_NONBLOCK)) < 0)+ return NULL;+ if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)+ || !(fp = fdopen(fd, "r"))) {+ close(fd);+ return NULL;+ }+ return fp;+}++void+pagebegin(int status, const char *pagetitle)+{+ size_t i;++ httpheader(status, "text/html; charset=utf-8");+ fputs("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n"+ "<meta name=\"viewport\" "+ "content=\"width=device-width, initial-scale=1\">\n<title>",+ stdout);+ xmlencode(pagetitle);+ fputs("</title>\n<link rel=\"stylesheet\" href=\"", stdout);+ xmlencode(css);+ fputs("\">\n<link rel=\"icon\" href=\"", stdout);+ xmlencode(icon);+ fputs("\">\n</head>\n<body>\n<div id=\"top\"><a class=\"brand\" "+ "href=\"", stdout);+ xmlencode(base);+ fputs("/\"><img src=\"", stdout);+ xmlencode(icon);+ fputs("\" alt=\"\"> ", stdout);+ xmlencode(title);+ fputs("</a><nav>", stdout);+ for (i = 0; i < LENGTH(services); i++) {+ if (!strcmp(services[i].name, service)) {+ fputs("<a class=\"on\" href=\"", stdout);+ xmlencode(base);+ putchar('/');+ } else {+ fputs("<a href=\"", stdout);+ xmlencode(services[i].url);+ }+ fputs("\">", stdout);+ xmlencode(services[i].name);+ fputs("</a>", stdout);+ }+ fputs("</nav></div>\n", stdout);+}++void+pageend(void)+{+ fputs("</main>\n</body>\n</html>\n", stdout);+}++int+param(const char *v, const char *attr, char *buf, size_t len)+{+ Buf raw = { 0 }, d = { 0 }, u = { 0 };+ const Buf *src;+ const char *p = v, *q;+ char *a, *l;+ size_t n, alen = strlen(attr);+ int quote, ext, ret = -1;++ while (ret < 0 && *p) {+ for (quote = 0; *p && (quote || *p != ';'); p++) {+ if (quote && *p == '\\' && p[1])+ p++;+ else if (*p == '"')+ quote = !quote;+ }+ if (!*p)+ break;+ for (p++; isspace((unsigned char)*p); p++)+ ;+ for (q = p; *q && *q != '=' && *q != ';'+ && !isspace((unsigned char)*q); q++)+ ;+ n = q - p;+ ext = n == alen + 1 && p[alen] == '*';+ if ((n != alen && !ext) || strncasecmp(p, attr, alen))+ continue;+ for (; isspace((unsigned char)*q); q++)+ ;+ if (*q != '=')+ continue;+ for (q++; isspace((unsigned char)*q); q++)+ ;+ bufadd(&raw, "", 0);+ if (*q == '"') {+ for (q++; *q && *q != '"'; q++) {+ if (*q == '\\' && q[1])+ q++;+ bufc(&raw, *q);+ }+ } else {+ for (; *q && *q != ';'+ && !isspace((unsigned char)*q); q++)+ bufc(&raw, *q);+ }+ src = &raw;+ if (ext && (a = strchr(raw.s, '\''))+ && (l = strchr(a + 1, '\''))) {+ for (q = l + 1; *q; q++) {+ if (*q == '%' && hex(q[1]) >= 0+ && hex(q[2]) >= 0) {+ bufc(&d, hex(q[1]) << 4 | hex(q[2]));+ q += 2;+ } else {+ bufc(&d, *q);+ }+ }+ *a = '\0';+ if (tou8(raw.s, d.s, d.n, &u) < 0)+ bufadd(&u, d.s, d.n);+ bufadd(&u, "", 0);+ src = &u;+ }+ n = src->n < len - 1 ? src->n : len - 1;+ memcpy(buf, src->s, n);+ buf[n] = '\0';+ ret = 0;+ }+ free(raw.s);+ free(d.s);+ free(u.s);+ return ret;+}++int+parsedate(const char *s, long long *t, int *tz)+{+ static const char months[] = "janfebmaraprmayjunjulaugsepoctnovdec";+ static const int mdays[] = {+ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31+ };+ static const struct {+ const char *name;+ int off;+ } zones[] = {+ { "edt", -240 }, { "est", -300 }, { "cdt", -300 },+ { "cst", -360 }, { "mdt", -360 }, { "mst", -420 },+ { "pdt", -420 }, { "pst", -480 },+ };+ long day, mon, year, hh, mm, ss = 0, z = 0, y, era, yoe, doy;+ const char *p;+ size_t n, i;+ int neg;++ s = skipcfws(s);+ if (isalpha((unsigned char)*s)) {+ while (isalpha((unsigned char)*s))+ s++;+ s = skipcfws(s);+ }+ if (!digits(&s, 2, &day))+ return -1;+ s = skipcfws(s);+ for (n = 0; isalpha((unsigned char)s[n]); n++)+ ;+ if (n < 3)+ return -1;+ for (mon = 0; mon < 12 && strncasecmp(s, months + 3 * mon, 3); mon++)+ ;+ if (mon == 12)+ return -1;+ s = skipcfws(s + n);+ if ((n = digits(&s, 4, &year)) < 2)+ return -1;+ if (n == 2)+ year += year < 50 ? 2000 : 1900;+ else if (n == 3)+ year += 1900;+ s = skipcfws(s);+ if (!digits(&s, 2, &hh))+ return -1;+ if (*(s = skipcfws(s)) != ':')+ return -1;+ s = skipcfws(s + 1);+ if (!digits(&s, 2, &mm))+ return -1;+ if (*(p = skipcfws(s)) == ':') {+ s = skipcfws(p + 1);+ if (!digits(&s, 2, &ss))+ return -1;+ }+ s = skipcfws(s);+ if (*s == '+' || *s == '-') {+ neg = *s++ == '-';+ if (digits(&s, 4, &z) != 4 || z / 100 > 23 || z % 100 > 59)+ return -1;+ z = z / 100 * 60 + z % 100;+ if (neg)+ z = -z;+ } else if (isalpha((unsigned char)*s)) {+ for (n = 0; isalpha((unsigned char)s[n]); n++)+ ;+ for (i = 0; i < LENGTH(zones); i++)+ if (n == 3 && !strncasecmp(s, zones[i].name, 3))+ z = zones[i].off;+ }+ if (year < 1900 || year > 9999 || day < 1 || day > mdays[mon]+ || (mon == 1 && day == 29 && (year % 4 || (!(year % 100)+ && year % 400))) || hh > 23 || mm > 59 || ss > 60)+ return -1;+ y = year - (mon < 2);+ era = y / 400;+ yoe = y - era * 400;+ doy = (153 * (mon + (mon > 1 ? -2 : 10)) + 2) / 5 + day - 1;+ *t = (long long)(era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy+ - 719468) * 86400 + hh * 3600 + mm * 60 + ss - z * 60;+ *tz = (int)z;+ return 0;+}++void+parsefrom(const char *v, Msg *m)+{+ Buf ph = { 0 }, cm = { 0 }, ad = { 0 };+ const char *p;+ int depth = 0, quote = 0, angle = 0, done = 0;++ for (p = v; *p; p++) {+ if (depth) {+ if (*p == '\\' && p[1])+ p++;+ else if (*p == '(')+ depth++;+ else if (*p == ')' && !--depth)+ continue;+ bufc(&cm, *p);+ } else if (quote) {+ if (*p == '\\' && p[1])+ p++;+ else if (*p == '"') {+ quote = 0;+ continue;+ }+ bufc(&ph, *p);+ } else if (angle) {+ if (*p == '>') {+ angle = 0;+ done = 1;+ } else {+ bufc(&ad, *p);+ }+ } else if (*p == '(') {+ depth = 1;+ } else if (*p == '"') {+ quote = 1;+ } else if (*p == '<' && !done) {+ angle = 1;+ } else if (!done) {+ bufc(&ph, *p);+ }+ }+ bufadd(&ph, "", 0);+ bufadd(&cm, "", 0);+ if (ad.n || done) {+ m->addr = clean(ad.s, ad.n);+ m->name = field(*ph.s ? ph.s : cm.s);+ } else {+ m->addr = clean(ph.s, ph.n);+ m->name = field(cm.s);+ }+ free(ph.s);+ free(cm.s);+ free(ad.s);+}++void+parsepart(const char *s, size_t n, Part *p)+{+ char *ct, *cte, *cd;+ size_t h, b, i;++ h = hdrend(s, n, &b);+ p->body = s + b;+ p->len = n - b;+ ct = hdrget(s, h, "Content-Type");+ cte = hdrget(s, h, "Content-Transfer-Encoding");+ cd = hdrget(s, h, "Content-Disposition");+ for (i = 0; ct && ct[i] && i + 1 < sizeof(p->type)+ && (isalnum((unsigned char)ct[i]) || strchr("/-+._", ct[i])); i++)+ p->type[i] = tolower((unsigned char)ct[i]);+ p->type[i] = '\0';+ if (!strchr(p->type, '/'))+ strcpy(p->type, "text/plain");+ for (i = 0; cte && cte[i] && i + 1 < sizeof(p->cte)+ && (isalnum((unsigned char)cte[i]) || cte[i] == '-'); i++)+ p->cte[i] = tolower((unsigned char)cte[i]);+ p->cte[i] = '\0';+ if (!ct || param(ct, "charset", p->charset, sizeof(p->charset)) < 0)+ p->charset[0] = '\0';+ if (!ct || param(ct, "boundary", p->boundary,+ sizeof(p->boundary)) < 0)+ p->boundary[0] = '\0';+ if ((!cd || param(cd, "filename", p->name, sizeof(p->name)) < 0)+ && (!ct || param(ct, "name", p->name, sizeof(p->name)) < 0))+ p->name[0] = '\0';+ free(ct);+ free(cte);+ free(cd);+}++void+printage(long long t)+{+ static const char *units[] = {+ "minute", "hour", "day", "week", "month", "year"+ };+ static const long secs[] = {+ 60, HOUR, DAY, 7 * DAY, 30 * DAY, 365 * DAY+ };+ static const long upto[] = {+ HOUR, DAY, 14 * DAY, 60 * DAY, 730 * DAY, LONG_MAX+ };+ static const long fade[] = { 7 * DAY, 30 * DAY, 180 * DAY, 730 * DAY };+ long d, n;+ size_t i, level;++ if (t < 0 || t > MAXTIME) {+ fputs("<td class=\"r age a4\">—</td>", stdout);+ return;+ }+ if ((d = (long)((long long)time(NULL) - t)) < 0)+ d = 0;+ for (i = 0; d >= upto[i]; i++)+ ;+ if ((n = d / secs[i]) < 1)+ n = 1;+ for (level = 0; level < LENGTH(fade) && d >= fade[level]; level++)+ ;+ printf("<td class=\"r age a%zu\" title=\"", level);+ printtime(t, 0, "%Y-%m-%d %H:%M UTC");+ printf("\">%ld %s%s</td>", n, units[i], n == 1 ? "" : "s");+}++void+printbody(const char *s, size_t n)+{+ const char *l, *nl, *cls;+ size_t len;+ int patch = 0;++ if (n > left) {+ for (n = left; n && ((unsigned char)s[n] & 0xc0) == 0x80; n--)+ ;+ cut = 1;+ }+ left -= n;+ fputs("<pre class=\"body\">", stdout);+ for (l = s; l < s + n; l = nl + 1) {+ nl = memchr(l, '\n', s + n - l);+ len = nl ? (size_t)(nl - l) : (size_t)(s + n - l);+ cls = lineclass(l, len, nl ? nl + 1 : NULL, s + n, &patch);+ if (cls)+ printf("<span class=\"%s\">", cls);+ xmlencoden(l, len);+ if (cls)+ fputs("</span>", stdout);+ if (!nl)+ break;+ putchar('\n');+ }+ fputs("</pre>\n", stdout);+}++void+printmsg(size_t i, int sel)+{+ const Msg *m = &msgs[i];+ Buf b = { 0 };+ int trunc;++ printf("<article class=\"mail%s\" id=\"", sel ? " sel" : "");+ xmlencode(m->id);+ fputs("\">\n<table class=\"meta\">\n<tr><td>From</td><td>", stdout);+ if (*m->name) {+ xmlencode(m->name);+ fputs(" <", stdout);+ xmlencode(m->addr);+ fputs(">", stdout);+ } else {+ xmlencode(m->addr);+ }+ fputs("</td></tr>\n<tr><td>Date</td><td>", stdout);+ printtime(m->date, m->tz, "%Y-%m-%d %H:%M");+ printf(" %c%02d%02d</td></tr>\n<tr><td>Subject</td><td>",+ m->tz < 0 ? '-' : '+', abs(m->tz) / 60, abs(m->tz) % 60);+ xmlencode(*m->subject ? m->subject : "(no subject)");+ fputs("</td></tr>\n</table>\n<p class=\"links\"><a href=\"", stdout);+ msgurl(m->id, "");+ fputs("\">permalink</a><a href=\"", stdout);+ msgurl(m->id, "raw");+ fputs("\">raw</a><a href=\"mailto:", stdout);+ urlencode(list);+ putchar('@');+ xmlencode(domain);+ fputs("?In-Reply-To=%3C", stdout);+ urlencode(m->id);+ fputs("%3E&subject=", stdout);+ if (strncasecmp(m->subject, "re:", 3))+ fputs("Re%3A%20", stdout);+ urlencode(m->subject);+ fputs("\">reply</a></p>\n", stdout);++ left = (size_t)maxbody;+ cut = 0;+ if ((trunc = readfile(m->file, (size_t)maxhdr + 2 * (size_t)maxbody,+ &b, 0)) < 0) {+ fputs("<p class=\"att\">Message unreadable.</p>\n", stdout);+ } else {+ renderpart(b.s, b.n, 0, i);+ if (cut || trunc) {+ fputs("<p class=\"att\">truncated, see <a href=\"",+ stdout);+ msgurl(m->id, "raw");+ fputs("\">raw</a></p>\n", stdout);+ }+ }+ fputs("</article>\n", stdout);+ free(b.s);+}++void+printtime(long long t, int tz, const char *fmt)+{+ struct tm tm;+ time_t secs;+ char buf[64];++ if (t < 0 || t > MAXTIME)+ return;+ secs = (time_t)(t + tz * 60LL);+ if (!gmtime_r(&secs, &tm))+ return;+ if (strftime(buf, sizeof(buf), fmt, &tm))+ fputs(buf, stdout);+}++void+qp(const char *s, size_t n, int q, Buf *out)+{+ size_t i, j;++ for (i = 0; i < n; i++) {+ if (q && s[i] == '_') {+ bufc(out, ' ');+ } else if (s[i] != '=') {+ bufc(out, s[i]);+ } else if (i + 2 < n && hex(s[i + 1]) >= 0+ && hex(s[i + 2]) >= 0) {+ bufc(out, hex(s[i + 1]) << 4 | hex(s[i + 2]));+ i += 2;+ } else {+ for (j = i + 1; j < n && (s[j] == ' ' || s[j] == '\t');+ j++)+ ;+ if (j < n && s[j] == '\n')+ i = j;+ else if (j + 1 < n && s[j] == '\r' && s[j + 1] == '\n')+ i = j + 1;+ else+ bufc(out, '=');+ }+ }+}++void+rawpage(size_t t)+{+ char buf[8192];+ size_t n;+ FILE *fp;++ if (!(fp = openmsg(msgs[t].file))) {+ notfound();+ return;+ }+ httpheader(200, "text/plain; charset=utf-8");+ while ((n = fread(buf, 1, sizeof(buf), fp)))+ fwrite(buf, 1, n, stdout);+ fclose(fp);+}++int+readfile(const char *file, size_t max, Buf *b, int hdronly)+{+ char chunk[4096];+ size_t want, r, i;+ FILE *fp;+ int trunc = 0;++ bufadd(b, "", 0);+ if (!(fp = openmsg(file)))+ return -1;+ for (;;) {+ if (!(want = max - b->n)) {+ trunc = fgetc(fp) != EOF;+ break;+ }+ if (want > sizeof(chunk))+ want = sizeof(chunk);+ if (!(r = fread(chunk, 1, want, fp)))+ break;+ i = b->n > 2 ? b->n - 2 : 0;+ bufadd(b, chunk, r);+ for (; hdronly && i + 1 < b->n; i++)+ if (b->s[i] == '\n' && (b->s[i + 1] == '\n'+ || (b->s[i + 1] == '\r' && i + 2 < b->n+ && b->s[i + 2] == '\n')))+ hdronly = 2;+ if (hdronly == 2)+ break;+ }+ fclose(fp);+ return trunc;+}++void+readline(const char *name, const char *file, char *buf, size_t len)+{+ char path[PATH_MAX];+ FILE *fp;++ buf[0] = '\0';+ if (bprintf(path, sizeof(path), "%s/%s/%s", root, name, file) < 0+ || !(fp = fopen(path, "r")))+ return;+ if (fgets(buf, (int)len, fp))+ buf[strcspn(buf, "\r\n")] = '\0';+ fclose(fp);+}++void+renderpart(const char *s, size_t n, int depth, size_t m)+{+ Buf raw = { 0 }, u = { 0 }, t = { 0 };+ Part p;++ if (cut)+ return;+ parsepart(s, n, &p);+ if (!strncmp(p.type, "multipart/", 10) && *p.boundary+ && depth < maxdepth) {+ multipart(&p, depth, m);+ } else if (!strcmp(p.type, "text/plain")+ || !strcmp(p.type, "text/x-diff") || !strcmp(p.type, "text/x-patch")) {+ decode(&p, &raw);+ if (tou8(p.charset, raw.s, raw.n, &u) < 0)+ bufadd(&u, raw.s, raw.n);+ sanitize(u.s, u.n, &t, 1);+ printbody(t.s, t.n);+ } else if (!strcmp(p.type, "text/html")) {+ fputs("<p class=\"att\">HTML part not shown, see <a href=\"",+ stdout);+ msgurl(msgs[m].id, "raw");+ fputs("\">raw</a></p>\n", stdout);+ } else {+ attachment(&p);+ }+ free(raw.s);+ free(u.s);+ free(t.s);+}++void+route(char *path)+{+ char *rest;+ size_t n, t;+ int page = 0;++ while (*path == '/')+ path++;+ if (!*path) {+ indexpage();+ return;+ }+ if ((rest = strchr(path, '/')))+ *rest++ = '\0';+ if (!islist(path)) {+ notfound();+ return;+ }+ list = path;+ if (!rest || !*rest) {+ listpage();+ return;+ }+ n = strlen(rest);+ if (rest[n - 1] == '/') {+ rest[--n] = '\0';+ } else if (n > 4 && !strcmp(rest + n - 4, "/raw")) {+ rest[n -= 4] = '\0';+ page = 1;+ } else if (n > 7 && !strcmp(rest + n - 7, "/t.mbox")) {+ rest[n -= 7] = '\0';+ page = 2;+ }+ if (!validid(rest, n)) {+ notfound();+ return;+ }+ scan(list);+ if ((t = byid(rest)) == NONE)+ notfound();+ else if (page == 1)+ rawpage(t);+ else if (page == 2)+ mboxpage(t);+ else+ threadpage(t);+}++void+sanitize(const char *s, size_t n, Buf *out, int multi)+{+ const unsigned char *p = (const unsigned char *)s, *e = p + n;+ unsigned long c;+ size_t i, len;++ while (p < e) {+ c = *p;+ if (c < 0x80) {+ if (c == '\n' || c == '\t' || (c == '\r' && !multi))+ bufc(out, multi ? (int)c : ' ');+ else if ((c < 0x20 && c != '\r') || c == 0x7f)+ bufadd(out, REPL, 3);+ else if (c != '\r')+ bufc(out, (int)c);+ p++;+ continue;+ }+ if (c >= 0xc2 && c <= 0xdf) {+ len = 2;+ c &= 0x1f;+ } else if (c >= 0xe0 && c <= 0xef) {+ len = 3;+ c &= 0x0f;+ } else if (c >= 0xf0 && c <= 0xf4) {+ len = 4;+ c &= 0x07;+ } else {+ len = 0;+ }+ for (i = 1; i < len; i++) {+ if (p + i >= e || (p[i] & 0xc0) != 0x80)+ len = 0;+ else+ c = c << 6 | (p[i] & 0x3f);+ }+ if (!len || (len == 3 && c < 0x800)+ || (len == 4 && (c < 0x10000 || c > 0x10ffff))+ || (c >= 0xd800 && c <= 0xdfff)) {+ bufadd(out, REPL, 3);+ p++;+ continue;+ }+ if (c < 0xa0 || (c >= 0x202a && c <= 0x202e)+ || (c >= 0x2066 && c <= 0x2069))+ bufadd(out, REPL, 3);+ else+ bufadd(out, (const char *)p, len);+ p += len;+ }+}++void+scan(const char *name)+{+ static const char *sub[] = { "new", "cur" };+ char dir[PATH_MAX], path[PATH_MAX];+ struct dirent *e;+ struct stat st;+ Msg *v;+ DIR *dp;+ size_t i, j, n = 0, cap = 0;++ freemsgs();+ for (i = 0; i < LENGTH(sub); i++) {+ if (bprintf(dir, sizeof(dir), "%s/%s/archive/%s", root, name,+ sub[i]) < 0 || !(dp = opendir(dir)))+ continue;+ while ((e = readdir(dp))) {+ if (e->d_name[0] == '.'+ || bprintf(path, sizeof(path), "%s/%s", dir,+ e->d_name) < 0+ || lstat(path, &st) < 0 || !S_ISREG(st.st_mode))+ continue;+ if (n == cap) {+ cap = cap ? cap * 2 : 256;+ if (!(v = realloc(msgs, cap * sizeof(*v))))+ die("out of memory");+ msgs = v;+ }+ memset(&msgs[n], 0, sizeof(*msgs));+ msgs[n].file = xstrndup(path, strlen(path));+ msgs[n].mtime = (long long)st.st_mtime;+ n++;+ }+ closedir(dp);+ }+ if (n)+ qsort(msgs, n, sizeof(*msgs), cmpmtime);+ for (i = j = 0; i < n; i++) {+ if (i < (size_t)maxmsgs && loadmsg(&msgs[i]) >= 0)+ msgs[j++] = msgs[i];+ else+ freemsg(&msgs[i]);+ }+ nmsgs = j;+ if (nmsgs)+ qsort(msgs, nmsgs, sizeof(*msgs), cmpfirst);+ for (i = j = 0; i < nmsgs; i++) {+ if (j && !strcmp(msgs[j - 1].id, msgs[i].id))+ freemsg(&msgs[i]);+ else+ msgs[j++] = msgs[i];+ }+ nmsgs = j;+ if (nmsgs)+ qsort(msgs, nmsgs, sizeof(*msgs), cmpdate);+ if (!(ids = calloc(nmsgs + 1, sizeof(*ids))))+ die("out of memory");+ for (i = 0; i < nmsgs; i++)+ ids[i] = i;+ if (nmsgs)+ qsort(ids, nmsgs, sizeof(*ids), cmpid);+ threadmsgs();+}++const char *+skipcfws(const char *s)+{+ int depth = 0;++ for (; *s; s++) {+ if (depth && *s == '\\' && s[1])+ s++;+ else if (*s == '(')+ depth++;+ else if (depth && *s == ')')+ depth--;+ else if (!depth && !isspace((unsigned char)*s) && *s != ',')+ break;+ }+ return s;+}++void+threadmsgs(void)+{+ size_t *stack, i, j, k, n, p, r;++ for (i = 0; i < nmsgs; i++) {+ msgs[i].parent = msgs[i].root = NONE;+ if (msgs[i].irt && (p = byid(msgs[i].irt)) != NONE && p != i)+ msgs[i].parent = p;+ for (k = msgs[i].nrefs; msgs[i].parent == NONE && k; k--)+ if ((p = byid(msgs[i].refs[k - 1])) != NONE && p != i)+ msgs[i].parent = p;+ }+ if (!(stack = calloc(nmsgs + 1, sizeof(*stack))))+ die("out of memory");+ for (i = 0; i < nmsgs; i++) {+ n = 0;+ for (j = i; msgs[j].root == NONE && msgs[j].seen != i + 1;+ j = msgs[j].parent) {+ msgs[j].seen = i + 1;+ stack[n++] = j;+ if (msgs[j].parent == NONE)+ break;+ }+ r = msgs[j].root != NONE ? msgs[j].root : j;+ for (k = 0; k < n; k++)+ msgs[stack[k]].root = r;+ }+ free(stack);+}++void+threadpage(size_t t)+{+ const Msg *r = &msgs[msgs[t].root];+ size_t i, n = 0;++ if (msgs[t].root != t) {+ printf("Status: 302 Found\r\nLocation: %s/", base);+ urlencode(list);+ putchar('/');+ urlencode(r->id);+ fputs("/#", stdout);+ urlencode(msgs[t].id);+ fputs("\r\n", stdout);+ httpheader(302, "text/plain; charset=utf-8");+ return;+ }+ for (i = 0; i < nmsgs; i++)+ if (msgs[i].root == t)+ n++;+ pagebegin(200, *r->subject ? r->subject : "(no subject)");+ listheader(0);+ fputs("<h2 class=\"subject\">", stdout);+ xmlencode(*r->subject ? r->subject : "(no subject)");+ printf("</h2>\n<p class=\"info\">%zu message%s · <a href=\"",+ n, n == 1 ? "" : "s");+ msgurl(r->id, "t.mbox");+ fputs("\">t.mbox</a></p>\n", stdout);+ for (i = 0; i < nmsgs; i++)+ if (msgs[i].root == t)+ printmsg(i, i == t);+ pageend();+}++int+tou8(const char *cs, const char *s, size_t n, Buf *out)+{+ char name[64], tmp[1024], *in, *o;+ size_t i, il, ol, r;+ iconv_t cd;+ unsigned char c;++ for (i = 0; cs[i] && cs[i] != '*'; i++) {+ if (i + 1 >= sizeof(name) || !(isalnum((unsigned char)cs[i])+ || strchr("-_.:", cs[i])))+ return -1;+ name[i] = cs[i];+ }+ name[i] = '\0';+ if (!*name || !strcasecmp(name, "utf-8") || !strcasecmp(name, "utf8")+ || !strcasecmp(name, "us-ascii") || !strcasecmp(name, "ascii")) {+ bufadd(out, s, n);+ return 0;+ }+ if (!strcasecmp(name, "iso-8859-1") || !strcasecmp(name, "latin1")) {+ for (i = 0; i < n; i++) {+ c = (unsigned char)s[i];+ if (c < 0x80) {+ bufc(out, c);+ } else {+ bufc(out, 0xc0 | c >> 6);+ bufc(out, 0x80 | (c & 0x3f));+ }+ }+ return 0;+ }+ if ((cd = iconv_open("UTF-8", name)) == (iconv_t)-1)+ return -1;+ in = (char *)s;+ il = n;+ while (il) {+ o = tmp;+ ol = sizeof(tmp);+ r = iconv(cd, &in, &il, &o, &ol);+ bufadd(out, tmp, o - tmp);+ if (r != (size_t)-1 || errno == E2BIG)+ continue;+ bufadd(out, REPL, 3);+ if (errno != EILSEQ)+ break;+ in++;+ il--;+ }+ o = tmp;+ ol = sizeof(tmp);+ iconv(cd, NULL, NULL, &o, &ol);+ bufadd(out, tmp, o - tmp);+ iconv_close(cd);+ return 0;+}++void+urlencode(const char *s)+{+ for (; *s; s++) {+ if (isalnum((unsigned char)*s) || strchr("-._~@", *s))+ putchar(*s);+ else+ printf("%%%02X", (unsigned char)*s);+ }+}++int+validid(const char *s, size_t n)+{+ size_t i;++ if (!n || n > FIELDLEN)+ return 0;+ for (i = 0; i < n; i++)+ if ((unsigned char)s[i] <= ' ' || s[i] == 0x7f || s[i] == '<'+ || s[i] == '>')+ return 0;+ return 1;+}++int+validlist(const char *s)+{+ size_t n;++ for (n = 0; s[n]; n++)+ if (!islower((unsigned char)s[n])+ && !isdigit((unsigned char)s[n]) && !(n && s[n] == '-'))+ return 0;+ return n && n <= 64;+}++void+xmlencode(const char *s)+{+ if (s)+ xmlencoden(s, strlen(s));+}++void+xmlencoden(const char *s, size_t len)+{+ for (; len; s++, len--) {+ switch (*s) {+ case '<': fputs("<", stdout); break;+ case '>': fputs(">", stdout); break;+ case '&': fputs("&", stdout); break;+ case '"': fputs(""", stdout); break;+ case '\'': fputs("'", stdout); break;+ default: putchar(*s);+ }+ }+}++char *+xstrndup(const char *s, size_t n)+{+ char *r;++ if (!(r = malloc(n + 1)))+ die("out of memory");+ if (n)+ memcpy(r, s, n);+ r[n] = '\0';+ return r;+}++/* main */+int+main(void)+{+ char path[2048];+ const char *q;+ struct rlimit rl;++ rl.rlim_cur = rl.rlim_max = maxcpu;+ setrlimit(RLIMIT_CPU, &rl);+ root = envor("HERBARIUM_ROOT", root);+ domain = envor("HERBARIUM_DOMAIN", domain);+ css = envor("HERBARIUM_CSS", css);+ icon = envor("HERBARIUM_ICON", icon);+ title = envor("HERBARIUM_TITLE", title);+ base = envor("SCRIPT_NAME", "");+ q = envor("QUERY_STRING", "");+ if (!strncmp(q, "ofs=", 4) && (ofs = strtol(q + 4, NULL, 10)) < 0)+ ofs = 0;++ if (bprintf(path, sizeof(path), "%s", envor("PATH_INFO", "")) < 0)+ notfound();+ else+ route(path);+ return 0;+}
herbarium/icon.svg
@@ -0,0 +1,26 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">+<style>+.g { stroke: #8cc56b; }+.f { fill: #8cc56b; }+.p { fill: #e58fa6; }+.c { fill: #f6e7a1; }+@media (prefers-color-scheme: light) {+ .g { stroke: #2f6d1a; }+ .f { fill: #2f6d1a; }+ .p { fill: #c2456b; }+ .c { fill: #e8b931; }+}+</style>+<path class="g f" d="M3 28V18a13 13 0 0 1 26 0v10z" fill-opacity=".14" stroke-width="2" stroke-linejoin="round"/>+<path class="g" d="M10 28V6.6M22 28V6.6M3 18h26" stroke-width="1.25" stroke-opacity=".55"/>+<path class="g" d="M16 28v-11" stroke-width="1.6" stroke-linecap="round"/>+<path class="f" d="M16 25.5c-2.6 0-3.6-1.6-3.6-3.1 2.1 0 3.6 1 3.6 3.1zM16 23.5c2.6 0 3.6-1.6 3.6-3.1-2.1 0-3.6 1-3.6 3.1z"/>+<g class="p" transform="translate(16 13.5)">+<ellipse rx="1.6" ry="2.7" transform="translate(0 -2.6)"/>+<ellipse rx="1.6" ry="2.7" transform="rotate(72) translate(0 -2.6)"/>+<ellipse rx="1.6" ry="2.7" transform="rotate(144) translate(0 -2.6)"/>+<ellipse rx="1.6" ry="2.7" transform="rotate(216) translate(0 -2.6)"/>+<ellipse rx="1.6" ry="2.7" transform="rotate(288) translate(0 -2.6)"/>+</g>+<circle class="c" cx="16" cy="13.5" r="1.2"/>+</svg>
herbarium/style.css
@@ -0,0 +1,432 @@+:root {+ --bg: #0d0f0d;+ --fg: #c8cec3;+ --hi: #eef2ea;+ --dim: #6c7569;+ --faint: #1c201c;+ --leaf: #9fd47c;+ --petal: #f08fae;+ --a0: #9fd47c;+ --a1: #b9cfa8;+ --a2: #8d9689;+ --a3: #626a5f;+ --a4: #424840;+}++@media (prefers-color-scheme: light) {+ :root {+ --bg: #fafaf7;+ --fg: #2a2e28;+ --hi: #000;+ --dim: #7a8277;+ --faint: #ecefe8;+ --leaf: #2f6d1a;+ --petal: #b0345c;+ --a0: #2f6d1a;+ --a1: #577a47;+ --a2: #7a8277;+ --a3: #a2a89f;+ --a4: #c4c8c1;+ }+}++body {+ max-width: 980px;+ margin: 0 auto;+ padding: 1.25rem 1.5rem 3rem;+ background: var(--bg);+ color: var(--fg);+ font: 14px/1.6 ui-monospace, "JetBrains Mono", "DejaVu Sans Mono", monospace;+}++a {+ color: var(--hi);+ text-decoration: none;+}++a:hover {+ color: var(--leaf);+}++#top {+ display: flex;+ gap: 2ch;+ padding-bottom: 0.75rem;+ border-bottom: 1px dashed var(--dim);+}++#top .brand img {+ width: 1.3em;+ height: 1.3em;+ vertical-align: -0.3em;+}++#top nav, header nav {+ display: flex;+ gap: 1ch;+}++#top nav a, header nav a {+ padding: 0 1ch;+ color: var(--dim);+}++#top nav a.on, header nav a.on {+ background: var(--leaf);+ color: var(--bg);+}++header {+ padding: 2.25rem 0 0;+}++h1 {+ margin: 0;+ font-size: 1.75rem;+ line-height: 1.2;+ letter-spacing: -0.02em;+ color: var(--hi);+}++.desc {+ color: var(--dim);+}++header .desc {+ margin: 0.35rem 0 0;+}++.clone {+ display: inline-block;+ margin: 1.1rem 0 0;+ padding: 0.4rem 1.5ch 0.4rem 1.25ch;+ background: var(--faint);+ border-left: 2px solid var(--leaf);+}++.clone span {+ color: var(--dim);+ user-select: none;+}++.clone span::before {+ content: "$ ";+ color: var(--leaf);+}++header nav {+ margin: 1.25rem 0 0;+}++header nav a {+ margin-left: -1ch;+ margin-right: 1ch;+}++main {+ padding-top: 1.5rem;+}++table {+ width: 100%;+ border-collapse: collapse;+}++th {+ padding: 0 2ch 0.4rem 0;+ text-align: left;+ font-weight: normal;+ color: var(--dim);+}++td {+ padding: 0.1rem 2ch 0.1rem 0;+ white-space: nowrap;+ vertical-align: top;+}++th:last-child, td:last-child {+ padding-right: 0;+}++tbody tr:hover td {+ background: var(--faint);+}++.r {+ text-align: right;+}++.desc, .msg {+ width: 100%;+ white-space: normal;+}++.msg a {+ color: var(--fg);+}++.dir, .owner, .date, .num {+ color: var(--dim);+}++.ins { color: var(--leaf); }+.del { color: var(--petal); }+.a0 { color: var(--a0); }+.a1 { color: var(--a1); }+.a2 { color: var(--a2); }+.a3 { color: var(--a3); }+.a4 { color: var(--a4); }++header nav .ref {+ margin-left: auto;+ color: var(--dim);+}++header nav .ref a {+ margin: 0;+ padding: 0;+ color: var(--hi);+}++h2 {+ margin: 1.75rem 0 0.5rem;+ font-size: 1em;+ color: var(--dim);+}++h2:first-child {+ margin-top: 0;+}++h2:not(.subject)::before {+ content: "## ";+}++.pager {+ display: flex;+ gap: 3ch;+ margin-top: 1.25rem;+ color: var(--dim);+}++.pager a {+ color: var(--leaf);+}++.path {+ margin: 0 0 1rem;+ color: var(--dim);+}++.path .info {+ float: right;+}++.mode, .num {+ color: var(--dim);+}++.dirent a {+ color: var(--leaf);+}++pre {+ margin: 0;+ font: inherit;+ overflow-x: auto;+}++.blob {+ position: relative;+ padding-left: 7ch;+}++.blob a {+ position: absolute;+ left: 0;+ width: 5ch;+ text-align: right;+ color: var(--a4);+ user-select: none;+}++.blob a:hover, .blob a:target {+ color: var(--leaf);+}++.subject {+ margin: 0 0 1rem;+ font-size: 1.15rem;+ color: var(--hi);+}++.body {+ margin: 0 0 1.25rem;+ color: var(--fg);+ white-space: pre-wrap;+}++.meta {+ width: auto;+ margin-bottom: 1.5rem;+}++.meta td:first-child {+ color: var(--dim);+}++.stat {+ width: auto;+}++.stat td {+ padding-top: 0;+ padding-bottom: 0;+}++.bar {+ letter-spacing: -0.05em;+}++.total {+ margin: 0.5rem 0 2rem;+ color: var(--dim);+}++h3 {+ margin: 1.75rem 0 0;+ padding: 0.3rem 1ch;+ font-size: 1em;+ font-weight: normal;+ color: var(--hi);+ background: var(--faint);+}++.diff {+ padding: 0.5rem 0;+}++.diff span {+ display: block;+}++.diff .h {+ color: var(--dim);+}++.diff .i {+ color: var(--leaf);+ background: color-mix(in srgb, var(--leaf) 8%, transparent);+}++.diff .d {+ color: var(--petal);+ background: color-mix(in srgb, var(--petal) 8%, transparent);+}++@media (max-width: 640px) {+ .sm {+ display: none;+ }+}++.diff span {+ min-height: 1.6em;+}++.tree .name, .tree .dirent {+ width: 100%;+}++.refs .name {+ width: 100%;+}++h1 a {+ color: inherit;+}++.addrs {+ width: auto;+ margin: 1.1rem 0 0;+ padding: 0.4rem 1.5ch 0.4rem 1.25ch;+ background: var(--faint);+ border-left: 2px solid var(--leaf);+ border-collapse: separate;+}++.addrs td {+ padding: 0 2ch 0 0;+}++.addrs td:first-child {+ color: var(--dim);+ user-select: none;+}++.addrs tr:hover td {+ background: none;+}++.info {+ margin: 0 0 1.5rem;+ color: var(--dim);+}++.info a, .links a {+ color: var(--leaf);+}++.mail {+ margin: 0 0 1.75rem;+ padding: 0.5rem 0 0.25rem 1.5ch;+ border-left: 2px solid var(--faint);+}++.mail:target, .mail.sel {+ border-left-color: var(--leaf);+}++.mail .meta {+ margin-bottom: 0.5rem;+}++.mail .meta td {+ white-space: normal;+ overflow-wrap: anywhere;+}++.mail .meta tr:hover td {+ background: none;+}++.links {+ display: flex;+ gap: 2ch;+ margin: 0 0 1rem;+}++.mail .body {+ margin: 0 0 1rem;+ overflow-wrap: anywhere;+}++.body .q { color: var(--dim); }+.body .f { color: var(--hi); font-weight: bold; }+.body .h { color: var(--a2); }+.body .i { color: var(--leaf); }+.body .d { color: var(--petal); }++.att {+ margin: 0 0 1rem;+ color: var(--dim);+}++.att a {+ color: var(--leaf);+}++.threads .msg a, .lists .name a {+ white-space: normal;+}
pollen/pollen.1
@@ -134,6 +134,8 @@ for each open request, written by pollen. .TP .B archive/ Maildir of every distributed post, exactly as sent.+The archive is group-readable for archive readers such as herbarium;+everything else pollen writes is private to its user. .TP .B .lock Lock file serializing changes to
pollen/pollen.c
@@ -785,13 +785,13 @@ request(const char *act) } if (newtoken(tok) < 0 || bprintf(d, sizeof(d), "%s/pending", dir) < 0- || (mkdir(d, 0750) < 0 && errno != EEXIST)+ || (mkdir(d, 0700) < 0 && errno != EEXIST) || bprintf(tmp, sizeof(tmp), "%s/.%s", d, tok) < 0 || bprintf(path, sizeof(path), "%s/%s", d, tok) < 0 || bprintf(reply, sizeof(reply), "%s+confirm-%s@%s", list, tok, domain) < 0) die(EX_TEMPFAIL, "cannot create request");- fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0640);+ fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600); ok = fd >= 0 && dprintf(fd, "%s %s %lld\n", act, addr, (long long)time(NULL)) >= 0 && !fsync(fd); if (fd >= 0 && close(fd) < 0)@@ -858,7 +858,7 @@ setsub(const char *addr, int add) if (bprintf(lock, sizeof(lock), "%s/.lock", dir) < 0 || bprintf(file, sizeof(file), "%s/subscribers", dir) < 0 || bprintf(tmp, sizeof(tmp), "%s/.subscribers", dir) < 0- || (lfd = open(lock, O_RDWR | O_CREAT, 0640)) < 0)+ || (lfd = open(lock, O_RDWR | O_CREAT, 0600)) < 0) return -1; memset(&fl, 0, sizeof(fl)); fl.l_type = F_WRLCK;@@ -868,7 +868,7 @@ setsub(const char *addr, int add) goto end; if (!(in = fopen(file, "r")) && errno != ENOENT) goto end;- if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0640)) < 0)+ if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0600)) < 0) goto end; if (!(out = fdopen(fd, "w"))) { close(fd);@@ -941,6 +941,7 @@ main(int argc, char *argv[]) char ext[256]; struct stat st; + umask(027); signal(SIGPIPE, SIG_IGN); openlog("pollen", LOG_PID, LOG_MAIL); if (argc < 2 || argc > 3)