/* imports */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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("

attachment: ", stdout); xmlencode(*name ? name : "(unnamed)"); fputs(" (", stdout); xmlencode(p->type); printf(", %zuB)

\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("
\n\n" "" "" "" "\n\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("\n
NameDescriptionAddressThreadsUpdated
\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("", stdout); xmlencode(name); fputs("", stdout); xmlencode(desc); fputs("", stdout); xmlencode(name); putchar('@'); xmlencode(domain); printf("%zu", threads); printage(last); fputs("\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("
\n

", stdout); xmlencode(list); fputs("

\n", stdout); if (*desc) { fputs("

", stdout); xmlencode(desc); fputs("

\n", stdout); } if (addrs) { fputs("\n", stdout); for (i = 0; i < LENGTH(cmds); i++) { printf("\n", stdout); } fputs("
%s", stdout); xmlencode(list); printf("%s%s@", *cmds[i] ? "+" : "", cmds[i]); xmlencode(domain); fputs("
\n", stdout); } fputs("
\n
\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("\n" "" "\n\n", stdout); for (i = (size_t)ofs; i < n && shown < (size_t)perpage; i++, shown++) { m = &msgs[t[i].root]; fputs("\n", stdout); } fputs("\n
SubjectFromMsgsLast
id, ""); fputs("\">", stdout); xmlencode(*m->subject ? m->subject : "(no subject)"); fputs("", stdout); xmlencode(*m->name ? m->name : m->addr); printf("%zu", stdout); printtime(t[i].last, 0, "%Y-%m-%d"); fputs("
\n", stdout); if (!shown) { fputs("

No messages.

\n", stdout); } else if (ofs || i < n) { fputs("\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("
\n

Not found.

\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("\n\n\n\n" "\n", stdout); xmlencode(pagetitle); fputs("\n\n\n\n\n
\"\" ", stdout); xmlencode(title); fputs("
\n", stdout); } void pageend(void) { fputs("
\n\n\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("—", 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("%ld %s%s", 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("
", 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("", cls);
		xmlencoden(l, len);
		if (cls)
			fputs("", stdout);
		if (!nl)
			break;
		putchar('\n');
	}
	fputs("
\n", stdout); } void printmsg(size_t i, int sel) { const Msg *m = &msgs[i]; Buf b = { 0 }; int trunc; printf("
id); fputs("\">\n\n\n\n\n
From", stdout); if (*m->name) { xmlencode(m->name); fputs(" <", stdout); xmlencode(m->addr); fputs(">", stdout); } else { xmlencode(m->addr); } fputs("
Date", stdout); printtime(m->date, m->tz, "%Y-%m-%d %H:%M"); printf(" %c%02d%02d
Subject", m->tz < 0 ? '-' : '+', abs(m->tz) / 60, abs(m->tz) % 60); xmlencode(*m->subject ? m->subject : "(no subject)"); fputs("
\n

id, ""); fputs("\">permalinkid, "raw"); fputs("\">rawid); fputs("%3E&subject=", stdout); if (strncasecmp(m->subject, "re:", 3)) fputs("Re%3A%20", stdout); urlencode(m->subject); fputs("\">reply

\n", stdout); left = (size_t)maxbody; cut = 0; if ((trunc = readfile(m->file, (size_t)maxhdr + 2 * (size_t)maxbody, &b, 0)) < 0) { fputs("

Message unreadable.

\n", stdout); } else { renderpart(b.s, b.n, 0, i); if (cut || trunc) { fputs("

truncated, see id, "raw"); fputs("\">raw

\n", stdout); } } fputs("
\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("

HTML part not shown, see raw

\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("

", stdout); xmlencode(*r->subject ? r->subject : "(no subject)"); printf("

\n

%zu message%s · id, "t.mbox"); fputs("\">t.mbox

\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; 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 ((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; 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; }