greenhouse

greenhouse

minimal software forge

git clone git@git.lostduk.com:greenhouse.git

greenhouse / herbarium / herbarium.c2037 lines · 44218B · raw

1/* imports */
2#include <ctype.h>
3#include <dirent.h>
4#include <errno.h>
5#include <fcntl.h>
6#include <iconv.h>
7#include <limits.h>
8#include <stdarg.h>
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12#include <strings.h>
13#include <time.h>
14#include <unistd.h>
15#include <sys/stat.h>
16
17/* macros */
18#define LENGTH(X) (sizeof(X) / sizeof((X)[0]))
19#define HOUR      3600L
20#define DAY       (24 * HOUR)
21#define MAXTIME   253402300799LL
22#define FIELDLEN  998
23#define MAXREFS   16
24#define NONE      ((size_t)-1)
25#define REPL      "\xef\xbf\xbd"
26
27/* enums */
28typedef struct {
29	char *s;
30	size_t n, cap;
31} Buf;
32
33typedef struct {
34	char *file;
35	char *id;
36	char *irt;
37	char *refs[MAXREFS];
38	size_t nrefs;
39	char *subject;
40	char *name;
41	char *addr;
42	long long mtime;
43	long long date;
44	int tz;
45	size_t parent;
46	size_t root;
47	size_t seen;
48} Msg;
49
50typedef struct {
51	const char *body;
52	size_t len;
53	char type[64];
54	char cte[32];
55	char charset[64];
56	char boundary[128];
57	char name[512];
58} Part;
59
60typedef struct {
61	const char *name;
62	const char *url;
63} Service;
64
65typedef struct {
66	size_t root;
67	size_t n;
68	long long last;
69} Thread;
70
71/* function declarations */
72static void attachment(const Part *p);
73static void b64(const char *s, size_t n, Buf *out);
74static const char *boundary(const char *s, const char *end, const char *b,
75                            size_t blen);
76static int bprintf(char *buf, size_t len, const char *fmt, ...);
77static void bufadd(Buf *b, const char *s, size_t n);
78static void bufc(Buf *b, int c);
79static size_t byid(const char *id);
80static char *clean(const char *s, size_t n);
81static int cmpdate(const void *a, const void *b);
82static int cmpfirst(const void *a, const void *b);
83static int cmpid(const void *a, const void *b);
84static int cmpmtime(const void *a, const void *b);
85static int cmpthread(const void *a, const void *b);
86static void decode(const Part *p, Buf *out);
87static void decode2047(const char *s, Buf *out);
88static void die(const char *msg);
89static size_t digits(const char **s, size_t max, long *v);
90static const char *encword(const char *p, Buf *out);
91static const char *envor(const char *name, const char *fallback);
92static char *field(const char *v);
93static void freemsg(Msg *m);
94static void freemsgs(void);
95static void gzblock(int last);
96static void gzput(const char *s, size_t n);
97static size_t hdrend(const char *s, size_t n, size_t *body);
98static char *hdrget(const char *h, size_t n, const char *name);
99static int hex(int c);
100static void httpheader(int status, const char *type);
101static void indexpage(void);
102static void indexrow(const char *name);
103static int islist(const char *name);
104static int isstat(const char *s, const char *end);
105static const char *lineclass(const char *l, size_t len, const char *next,
106                             const char *end, int *patch);
107static void listheader(int addrs);
108static void listpage(void);
109static int loadmsg(Msg *m);
110static void mboxpage(size_t t);
111static void msgurl(const char *id, const char *page);
112static void multipart(const Part *p, int depth, size_t m);
113static char *nextid(const char **sp);
114static void notfound(void);
115static FILE *openmsg(const char *file);
116static void pagebegin(int status, const char *pagetitle);
117static void pageend(void);
118static int param(const char *v, const char *attr, char *buf, size_t len);
119static int parsedate(const char *s, long long *t, int *tz);
120static void parsefrom(const char *v, Msg *m);
121static void parsepart(const char *s, size_t n, Part *p);
122static void printage(long long t);
123static void printbody(const char *s, size_t n);
124static void printmsg(size_t i, int sel);
125static void printtime(long long t, int tz, const char *fmt);
126static void qp(const char *s, size_t n, int q, Buf *out);
127static void rawpage(size_t t);
128static int readfile(const char *file, size_t max, Buf *b, int hdronly);
129static void readline(const char *name, const char *file, char *buf,
130                     size_t len);
131static void renderpart(const char *s, size_t n, int depth, size_t m);
132static void route(char *path);
133static void sanitize(const char *s, size_t n, Buf *out, int multi);
134static void scan(const char *name);
135static const char *skipcfws(const char *s);
136static void threadmsgs(void);
137static void threadpage(size_t t);
138static int tou8(const char *cs, const char *s, size_t n, Buf *out);
139static void urlencode(const char *s, const char *safe);
140static int validid(const char *s, size_t n);
141static int validlist(const char *s);
142static void xmlencode(const char *s);
143static void xmlencoden(const char *s, size_t len);
144static char *xstrndup(const char *s, size_t n);
145
146/* variables */
147static const char pathsafe[] = "-._~!$()*+,;=:@";
148static const char mailsafe[] = "-._~@";
149static unsigned char gzbuf[65535];
150static size_t gzlen;
151static unsigned long gzcrc, gzsize;
152static const char *base;
153static const char *list;
154static Msg *msgs;
155static size_t nmsgs;
156static size_t *ids;
157static size_t left;
158static int cut;
159static long ofs;
160
161#include "config.h"
162
163/* function implementations */
164void
165attachment(const Part *p)
166{
167	char *name;
168	size_t i, n = p->len;
169
170	if (!strcmp(p->cte, "base64")) {
171		for (i = n = 0; i < p->len; i++)
172			if (isalnum((unsigned char)p->body[i])
173			|| p->body[i] == '+' || p->body[i] == '/')
174				n++;
175		n = n * 3 / 4;
176	}
177	name = field(*p->name ? p->name : NULL);
178	if (strlen(name) + 64 > left) {
179		cut = 1;
180		free(name);
181		return;
182	}
183	left -= strlen(name) + 64;
184	fputs("<p class=\"att\">attachment: ", stdout);
185	xmlencode(*name ? name : "(unnamed)");
186	fputs(" (", stdout);
187	xmlencode(p->type);
188	printf(", %zuB)</p>\n", n);
189	free(name);
190}
191
192void
193b64(const char *s, size_t n, Buf *out)
194{
195	unsigned long v = 0;
196	size_t i;
197	int c, bits = 0;
198
199	for (i = 0; i < n; i++) {
200		c = (unsigned char)s[i];
201		if (c >= 'A' && c <= 'Z')
202			c -= 'A';
203		else if (c >= 'a' && c <= 'z')
204			c -= 'a' - 26;
205		else if (c >= '0' && c <= '9')
206			c -= '0' - 52;
207		else if (c == '+')
208			c = 62;
209		else if (c == '/')
210			c = 63;
211		else {
212			if (c == '=')
213				bits = 0;
214			continue;
215		}
216		v = (v << 6 | (unsigned long)c) & 0xffffff;
217		if ((bits += 6) >= 8) {
218			bits -= 8;
219			bufc(out, (int)(v >> bits & 0xff));
220		}
221	}
222}
223
224const char *
225boundary(const char *s, const char *end, const char *b, size_t blen)
226{
227	const char *p;
228
229	while (s < end) {
230		if ((size_t)(end - s) >= blen + 2 && s[0] == '-' && s[1] == '-'
231		&& !memcmp(s + 2, b, blen)
232		&& (s + blen + 2 == end
233		|| (s[blen + 2] && strchr("\r\n \t-", s[blen + 2]))))
234			return s;
235		if (!(p = memchr(s, '\n', end - s)))
236			break;
237		s = p + 1;
238	}
239	return NULL;
240}
241
242int
243bprintf(char *buf, size_t len, const char *fmt, ...)
244{
245	va_list ap;
246	int n;
247
248	va_start(ap, fmt);
249	n = vsnprintf(buf, len, fmt, ap);
250	va_end(ap);
251	if (n < 0 || (size_t)n >= len)
252		return -1;
253	return 0;
254}
255
256void
257bufadd(Buf *b, const char *s, size_t n)
258{
259	size_t cap;
260	char *p;
261
262	if (b->n + n + 1 > b->cap) {
263		for (cap = b->cap ? b->cap : 256; cap < b->n + n + 1; cap *= 2)
264			;
265		if (!(p = realloc(b->s, cap)))
266			die("out of memory");
267		b->s = p;
268		b->cap = cap;
269	}
270	if (n)
271		memcpy(b->s + b->n, s, n);
272	b->n += n;
273	b->s[b->n] = '\0';
274}
275
276void
277bufc(Buf *b, int c)
278{
279	char ch = (char)c;
280
281	bufadd(b, &ch, 1);
282}
283
284size_t
285byid(const char *id)
286{
287	size_t lo = 0, hi = nmsgs, mid;
288
289	while (lo < hi) {
290		mid = lo + (hi - lo) / 2;
291		if (strcmp(msgs[ids[mid]].id, id) < 0)
292			lo = mid + 1;
293		else
294			hi = mid;
295	}
296	if (lo < nmsgs && !strcmp(msgs[ids[lo]].id, id))
297		return ids[lo];
298	return NONE;
299}
300
301char *
302clean(const char *s, size_t n)
303{
304	Buf b = { 0 };
305	char *r;
306	size_t i;
307
308	sanitize(s, n, &b, 0);
309	bufadd(&b, "", 0);
310	for (i = 0; b.s[i] == ' '; i++)
311		;
312	for (n = b.n; n > i && b.s[n - 1] == ' '; n--)
313		;
314	if (n - i > FIELDLEN) {
315		for (n = i + FIELDLEN; n > i
316		     && ((unsigned char)b.s[n] & 0xc0) == 0x80; n--)
317			;
318	}
319	r = xstrndup(b.s + i, n - i);
320	free(b.s);
321	return r;
322}
323
324int
325cmpdate(const void *a, const void *b)
326{
327	const Msg *x = a, *y = b;
328
329	if (x->date != y->date)
330		return x->date < y->date ? -1 : 1;
331	if (x->mtime != y->mtime)
332		return x->mtime < y->mtime ? -1 : 1;
333	return strcmp(x->file, y->file);
334}
335
336int
337cmpfirst(const void *a, const void *b)
338{
339	const Msg *x = a, *y = b;
340	int c;
341
342	if ((c = strcmp(x->id, y->id)))
343		return c;
344	if (x->mtime != y->mtime)
345		return x->mtime < y->mtime ? -1 : 1;
346	return strcmp(x->file, y->file);
347}
348
349int
350cmpid(const void *a, const void *b)
351{
352	size_t x = *(const size_t *)a, y = *(const size_t *)b;
353	int c;
354
355	if ((c = strcmp(msgs[x].id, msgs[y].id)))
356		return c;
357	return x < y ? -1 : x > y;
358}
359
360int
361cmpmtime(const void *a, const void *b)
362{
363	const Msg *x = a, *y = b;
364
365	if (x->mtime != y->mtime)
366		return x->mtime > y->mtime ? -1 : 1;
367	return strcmp(x->file, y->file);
368}
369
370int
371cmpthread(const void *a, const void *b)
372{
373	const Thread *x = a, *y = b;
374
375	if (x->last != y->last)
376		return x->last > y->last ? -1 : 1;
377	return strcmp(msgs[x->root].id, msgs[y->root].id);
378}
379
380void
381decode(const Part *p, Buf *out)
382{
383	if (!strcmp(p->cte, "base64"))
384		b64(p->body, p->len, out);
385	else if (!strcmp(p->cte, "quoted-printable"))
386		qp(p->body, p->len, 0, out);
387	else
388		bufadd(out, p->body, p->len);
389}
390
391void
392decode2047(const char *s, Buf *out)
393{
394	const char *q, *r;
395	int enc = 0;
396
397	while (*s) {
398		if (s[0] == '=' && s[1] == '?' && (q = encword(s, out))) {
399			s = q;
400			enc = 1;
401			continue;
402		}
403		if (enc && isspace((unsigned char)*s)) {
404			for (q = s; isspace((unsigned char)*q); q++)
405				;
406			if (q[0] == '=' && q[1] == '?'
407			&& (r = encword(q, out))) {
408				s = r;
409				continue;
410			}
411			bufadd(out, s, q - s);
412			s = q;
413			enc = 0;
414			continue;
415		}
416		bufc(out, *s++);
417		enc = 0;
418	}
419}
420
421void
422die(const char *msg)
423{
424	fprintf(stderr, "herbarium: %s\n", msg);
425	exit(1);
426}
427
428size_t
429digits(const char **s, size_t max, long *v)
430{
431	size_t n;
432
433	*v = 0;
434	for (n = 0; isdigit((unsigned char)(*s)[n]); n++)
435		if (n < max)
436			*v = *v * 10 + (*s)[n] - '0';
437	*s += n;
438	return n > max ? 0 : n;
439}
440
441const char *
442encword(const char *p, Buf *out)
443{
444	Buf raw = { 0 };
445	char cs[64];
446	const char *q, *e;
447	size_t n;
448	int enc, r;
449
450	q = p + 2;
451	for (n = 0; q[n] != '?'; n++)
452		if (!q[n] || n + 1 >= sizeof(cs)
453		|| isspace((unsigned char)q[n]))
454			return NULL;
455	memcpy(cs, q, n);
456	cs[n] = '\0';
457	q += n;
458	enc = tolower((unsigned char)q[1]);
459	if ((enc != 'b' && enc != 'q') || q[2] != '?')
460		return NULL;
461	q += 3;
462	for (e = q; !(e[0] == '?' && e[1] == '='); e++)
463		if (!*e || isspace((unsigned char)*e))
464			return NULL;
465	if (enc == 'b')
466		b64(q, e - q, &raw);
467	else
468		qp(q, e - q, 1, &raw);
469	r = tou8(cs, raw.s, raw.n, out);
470	free(raw.s);
471	return r < 0 ? NULL : e + 2;
472}
473
474const char *
475envor(const char *name, const char *fallback)
476{
477	const char *v;
478
479	v = getenv(name);
480	return v && *v ? v : fallback;
481}
482
483char *
484field(const char *v)
485{
486	Buf b = { 0 };
487	char *s, *r;
488	size_t n;
489
490	if (!v)
491		return xstrndup("", 0);
492	n = strlen(v);
493	s = xstrndup(v, n > 4 * FIELDLEN ? 4 * FIELDLEN : n);
494	decode2047(s, &b);
495	r = clean(b.s, b.n);
496	free(s);
497	free(b.s);
498	return r;
499}
500
501void
502freemsg(Msg *m)
503{
504	size_t i;
505
506	free(m->file);
507	free(m->id);
508	free(m->irt);
509	for (i = 0; i < m->nrefs; i++)
510		free(m->refs[i]);
511	free(m->subject);
512	free(m->name);
513	free(m->addr);
514}
515
516void
517freemsgs(void)
518{
519	size_t i;
520
521	for (i = 0; i < nmsgs; i++)
522		freemsg(&msgs[i]);
523	free(msgs);
524	free(ids);
525	msgs = NULL;
526	ids = NULL;
527	nmsgs = 0;
528}
529
530void
531gzblock(int last)
532{
533	unsigned char h[5];
534
535	h[0] = last ? 1 : 0;
536	h[1] = gzlen & 0xff;
537	h[2] = gzlen >> 8 & 0xff;
538	h[3] = ~gzlen & 0xff;
539	h[4] = ~gzlen >> 8 & 0xff;
540	fwrite(h, 1, sizeof(h), stdout);
541	fwrite(gzbuf, 1, gzlen, stdout);
542	gzlen = 0;
543}
544
545void
546gzput(const char *s, size_t n)
547{
548	static unsigned long tab[256];
549	unsigned long c;
550	size_t i;
551	int k;
552
553	if (!tab[1]) {
554		for (i = 0; i < LENGTH(tab); i++) {
555			for (c = i, k = 0; k < 8; k++)
556				c = c & 1 ? 0xedb88320UL ^ c >> 1 : c >> 1;
557			tab[i] = c;
558		}
559	}
560	for (i = 0; i < n; i++) {
561		gzcrc = tab[(gzcrc ^ (unsigned char)s[i]) & 0xff] ^ gzcrc >> 8;
562		gzbuf[gzlen++] = s[i];
563		if (gzlen == sizeof(gzbuf))
564			gzblock(0);
565	}
566	gzsize += n;
567}
568
569size_t
570hdrend(const char *s, size_t n, size_t *body)
571{
572	const char *p;
573	size_t pos = 0;
574
575	while (pos < n) {
576		if (s[pos] == '\n') {
577			*body = pos + 1;
578			return pos;
579		}
580		if (s[pos] == '\r' && pos + 1 < n && s[pos + 1] == '\n') {
581			*body = pos + 2;
582			return pos;
583		}
584		if (!(p = memchr(s + pos, '\n', n - pos)))
585			break;
586		pos = p - s + 1;
587	}
588	*body = n;
589	return n;
590}
591
592char *
593hdrget(const char *h, size_t n, const char *name)
594{
595	Buf b = { 0 };
596	const char *p;
597	size_t pos, k, len = strlen(name);
598	char *r;
599
600	for (pos = 0; pos < n; pos = p ? (size_t)(p - h) + 1 : n) {
601		p = memchr(h + pos, '\n', n - pos);
602		if (n - pos <= len || strncasecmp(h + pos, name, len))
603			continue;
604		for (k = pos + len; k < n && (h[k] == ' ' || h[k] == '\t');
605		     k++)
606			;
607		if (k >= n || h[k] != ':')
608			continue;
609		bufadd(&b, "", 0);
610		for (k++; k < n; k++) {
611			if (h[k] == '\n') {
612				if (k + 1 >= n || (h[k + 1] != ' '
613				&& h[k + 1] != '\t'))
614					break;
615			} else if (h[k] != '\r' && h[k]) {
616				bufc(&b, h[k]);
617			}
618		}
619		for (k = 0; isspace((unsigned char)b.s[k]); k++)
620			;
621		while (b.n > k && isspace((unsigned char)b.s[b.n - 1]))
622			b.n--;
623		r = xstrndup(b.s + k, b.n - k);
624		free(b.s);
625		return r;
626	}
627	return NULL;
628}
629
630int
631hex(int c)
632{
633	if (c >= '0' && c <= '9')
634		return c - '0';
635	if (c >= 'a' && c <= 'f')
636		return c - 'a' + 10;
637	if (c >= 'A' && c <= 'F')
638		return c - 'A' + 10;
639	return -1;
640}
641
642void
643httpheader(int status, const char *type)
644{
645	if (status == 404)
646		fputs("Status: 404 Not Found\r\n", stdout);
647	printf("Content-Type: %s\r\n", type);
648	fputs("Content-Security-Policy: default-src 'none'; "
649		"style-src 'self'; img-src 'self'\r\n"
650		"X-Content-Type-Options: nosniff\r\n\r\n", stdout);
651}
652
653void
654indexpage(void)
655{
656	struct dirent **ents;
657	int i, n;
658
659	pagebegin(200, title);
660	fputs("<main>\n<table class=\"lists\">\n<thead><tr><th>Name</th>"
661		"<th class=\"sm\">Description</th>"
662		"<th class=\"sm\">Address</th>"
663		"<th class=\"r\">Threads</th><th class=\"r\">Updated</th>"
664		"</tr></thead>\n<tbody>\n", stdout);
665	if ((n = scandir(root, &ents, NULL, alphasort)) >= 0) {
666		for (i = 0; i < n; i++) {
667			if (islist(ents[i]->d_name))
668				indexrow(ents[i]->d_name);
669			free(ents[i]);
670		}
671		free(ents);
672	}
673	fputs("</tbody>\n</table>\n", stdout);
674	pageend();
675}
676
677void
678indexrow(const char *name)
679{
680	char desc[256];
681	long long last = -1;
682	size_t i, threads = 0;
683
684	list = name;
685	scan(name);
686	for (i = 0; i < nmsgs; i++) {
687		if (msgs[i].root == i)
688			threads++;
689		if (msgs[i].date > last)
690			last = msgs[i].date;
691	}
692	readline(name, "description", desc, sizeof(desc));
693	fputs("<tr><td class=\"name\"><a href=\"", stdout);
694	xmlencode(base);
695	putchar('/');
696	urlencode(name, pathsafe);
697	fputs("/\">", stdout);
698	xmlencode(name);
699	fputs("</a></td><td class=\"desc sm\">", stdout);
700	xmlencode(desc);
701	fputs("</td><td class=\"sm\"><a href=\"mailto:", stdout);
702	urlencode(name, mailsafe);
703	putchar('@');
704	xmlencode(domain);
705	fputs("\">", stdout);
706	xmlencode(name);
707	putchar('@');
708	xmlencode(domain);
709	printf("</a></td><td class=\"r num\">%zu</td>", threads);
710	printage(last);
711	fputs("</tr>\n", stdout);
712	freemsgs();
713}
714
715int
716islist(const char *name)
717{
718	char path[PATH_MAX];
719	struct stat st;
720
721	return validlist(name)
722	    && bprintf(path, sizeof(path), "%s/%s", root, name) == 0
723	    && stat(path, &st) == 0 && S_ISDIR(st.st_mode)
724	    && bprintf(path, sizeof(path), "%s/%s/public", root, name) == 0
725	    && stat(path, &st) == 0;
726}
727
728int
729isstat(const char *s, const char *end)
730{
731	const char *nl;
732	size_t i, len;
733
734	nl = memchr(s, '\n', end - s);
735	len = nl ? (size_t)(nl - s) : (size_t)(end - s);
736	if (!len || *s != ' ')
737		return 0;
738	for (i = 0; i + 3 <= len; i++)
739		if (!memcmp(s + i, " | ", 3))
740			return 1;
741	return 0;
742}
743
744const char *
745lineclass(const char *l, size_t len, const char *next, const char *end,
746          int *patch)
747{
748	if (len >= 11 && !memcmp(l, "diff --git ", 11)) {
749		*patch = 1;
750		return "f";
751	}
752	if (!*patch) {
753		if (len == 3 && !memcmp(l, "---", 3) && next
754		&& isstat(next, end)) {
755			*patch = 1;
756			return "f";
757		}
758		return len && *l == '>' ? "q" : NULL;
759	}
760	if (len == 3 && !memcmp(l, "-- ", 3)) {
761		*patch = 0;
762		return NULL;
763	}
764	if ((len == 3 && !memcmp(l, "---", 3))
765	|| (len >= 4 && (!memcmp(l, "--- ", 4) || !memcmp(l, "+++ ", 4))))
766		return "f";
767	if (len >= 2 && !memcmp(l, "@@", 2))
768		return "h";
769	if (len && *l == '+')
770		return "i";
771	if (len && *l == '-')
772		return "d";
773	if (len && isalpha((unsigned char)*l))
774		return "f";
775	return NULL;
776}
777
778void
779listheader(int addrs)
780{
781	static const char *cmds[] = { "", "subscribe", "unsubscribe", "help" };
782	char desc[256];
783	size_t i;
784
785	readline(list, "description", desc, sizeof(desc));
786	fputs("<header>\n<h1><a href=\"", stdout);
787	xmlencode(base);
788	putchar('/');
789	urlencode(list, pathsafe);
790	fputs("/\">", stdout);
791	xmlencode(list);
792	fputs("</a></h1>\n", stdout);
793	if (*desc) {
794		fputs("<p class=\"desc\">", stdout);
795		xmlencode(desc);
796		fputs("</p>\n", stdout);
797	}
798	if (addrs) {
799		fputs("<table class=\"addrs\">\n", stdout);
800		for (i = 0; i < LENGTH(cmds); i++) {
801			printf("<tr><td>%s</td><td><a href=\"mailto:", *cmds[i]
802				? cmds[i] : "post");
803			urlencode(list, mailsafe);
804			printf("%s%s@", *cmds[i] ? "+" : "", cmds[i]);
805			xmlencode(domain);
806			fputs("\">", stdout);
807			xmlencode(list);
808			printf("%s%s@", *cmds[i] ? "+" : "", cmds[i]);
809			xmlencode(domain);
810			fputs("</a></td></tr>\n", stdout);
811		}
812		fputs("</table>\n", stdout);
813	}
814	fputs("</header>\n<main>\n", stdout);
815}
816
817void
818listpage(void)
819{
820	Thread *t;
821	const Msg *m;
822	size_t *slot, i, r, n = 0, shown = 0;
823
824	scan(list);
825	if (!(t = calloc(nmsgs + 1, sizeof(*t)))
826	|| !(slot = calloc(nmsgs + 1, sizeof(*slot))))
827		die("out of memory");
828	for (i = 0; i < nmsgs; i++)
829		slot[i] = NONE;
830	for (i = 0; i < nmsgs; i++) {
831		r = msgs[i].root;
832		if (slot[r] == NONE) {
833			slot[r] = n;
834			t[n].root = r;
835			t[n].last = msgs[i].date;
836			n++;
837		}
838		t[slot[r]].n++;
839		if (msgs[i].date > t[slot[r]].last)
840			t[slot[r]].last = msgs[i].date;
841	}
842	qsort(t, n, sizeof(*t), cmpthread);
843
844	pagebegin(200, list);
845	listheader(1);
846	fputs("<table class=\"threads\">\n<thead><tr><th>Subject</th>"
847		"<th class=\"sm\">From</th><th class=\"r\">Msgs</th>"
848		"<th class=\"r\">Last</th></tr></thead>\n<tbody>\n", stdout);
849	for (i = (size_t)ofs; i < n && shown < (size_t)perpage; i++, shown++) {
850		m = &msgs[t[i].root];
851		fputs("<tr><td class=\"msg\"><a href=\"", stdout);
852		msgurl(m->id, "");
853		fputs("\">", stdout);
854		xmlencode(*m->subject ? m->subject : "(no subject)");
855		fputs("</a></td><td class=\"sm\">", stdout);
856		xmlencode(*m->name ? m->name : m->addr);
857		printf("</td><td class=\"r num\">%zu</td><td class=\"r date\" "
858			"title=\"", t[i].n);
859		printtime(t[i].last, 0, "%Y-%m-%d %H:%M UTC");
860		fputs("\">", stdout);
861		printtime(t[i].last, 0, "%Y-%m-%d");
862		fputs("</td></tr>\n", stdout);
863	}
864	fputs("</tbody>\n</table>\n", stdout);
865	if (!shown) {
866		fputs("<p>No messages.</p>\n", stdout);
867	} else if (ofs || i < n) {
868		fputs("<nav class=\"pager\">", stdout);
869		if (ofs) {
870			fputs("<a href=\"", stdout);
871			xmlencode(base);
872			putchar('/');
873			urlencode(list, pathsafe);
874			putchar('/');
875			if (ofs > perpage)
876				printf("?ofs=%ld", ofs - perpage);
877			fputs("\">&larr; newer</a>", stdout);
878		}
879		printf("<span>%ld&ndash;%ld</span>", ofs + 1,
880			ofs + (long)shown);
881		if (i < n) {
882			fputs("<a href=\"", stdout);
883			xmlencode(base);
884			putchar('/');
885			urlencode(list, pathsafe);
886			printf("/?ofs=%zu\">older &rarr;</a>", i);
887		}
888		fputs("</nav>\n", stdout);
889	}
890	pageend();
891	free(slot);
892	free(t);
893}
894
895int
896loadmsg(Msg *m)
897{
898	Buf b = { 0 };
899	const char *p;
900	char *v;
901	size_t h, body;
902
903	if (readfile(m->file, (size_t)maxhdr, &b, 1) < 0)
904		return -1;
905	h = hdrend(b.s, b.n, &body);
906	if ((v = hdrget(b.s, h, "Message-ID"))) {
907		p = v;
908		if (!(m->id = nextid(&p)) && validid(v, strlen(v)))
909			m->id = xstrndup(v, strlen(v));
910		free(v);
911	}
912	if (!m->id) {
913		free(b.s);
914		return -1;
915	}
916	if ((v = hdrget(b.s, h, "In-Reply-To"))) {
917		p = v;
918		m->irt = nextid(&p);
919		free(v);
920	}
921	if ((v = hdrget(b.s, h, "References"))) {
922		for (p = v; *p;) {
923			if (!(m->refs[m->nrefs] = nextid(&p)))
924				break;
925			if (++m->nrefs == MAXREFS) {
926				free(m->refs[0]);
927				memmove(m->refs, m->refs + 1,
928					(MAXREFS - 1) * sizeof(*m->refs));
929				m->nrefs--;
930			}
931		}
932		free(v);
933	}
934	v = hdrget(b.s, h, "Subject");
935	m->subject = field(v);
936	free(v);
937	v = hdrget(b.s, h, "From");
938	parsefrom(v ? v : "", m);
939	free(v);
940	v = hdrget(b.s, h, "Date");
941	if (!v || parsedate(v, &m->date, &m->tz) < 0
942	|| m->date > m->mtime + DAY) {
943		m->date = m->mtime;
944		m->tz = 0;
945	}
946	free(v);
947	free(b.s);
948	return 0;
949}
950
951void
952mboxpage(size_t t)
953{
954	char *line = NULL;
955	size_t i, k, cap = 0, r = msgs[t].root;
956	ssize_t len;
957	FILE *fp;
958	int last;
959
960	httpheader(200, "application/gzip");
961	fwrite("\x1f\x8b\x08\0\0\0\0\0\0\xff", 1, 10, stdout);
962	gzcrc = 0xffffffffUL;
963	for (i = 0; i < nmsgs; i++) {
964		if (msgs[i].root != r || !(fp = openmsg(msgs[i].file)))
965			continue;
966		gzput("From mboxrd@z Thu Jan  1 00:00:00 1970\n", 39);
967		last = '\n';
968		while ((len = getline(&line, &cap, fp)) > 0) {
969			for (k = 0; k < (size_t)len && line[k] == '>'; k++)
970				;
971			if ((size_t)len - k >= 5
972			&& !memcmp(line + k, "From ", 5))
973				gzput(">", 1);
974			gzput(line, len);
975			last = line[len - 1];
976		}
977		if (last != '\n')
978			gzput("\n", 1);
979		gzput("\n", 1);
980		fclose(fp);
981	}
982	free(line);
983	gzblock(1);
984	gzcrc ^= 0xffffffffUL;
985	for (k = 0; k < 4; k++)
986		putchar((int)(gzcrc >> 8 * k & 0xff));
987	for (k = 0; k < 4; k++)
988		putchar((int)(gzsize >> 8 * k & 0xff));
989}
990
991void
992msgurl(const char *id, const char *page)
993{
994	xmlencode(base);
995	putchar('/');
996	urlencode(list, pathsafe);
997	putchar('/');
998	urlencode(id, pathsafe);
999	putchar('/');
1000	fputs(page, stdout);
1001}
1002
1003void
1004multipart(const Part *p, int depth, size_t m)
1005{
1006	const char *s, *end, *next, *pick = NULL;
1007	size_t blen, len, plen = 0;
1008	int alt, rank, best = 0;
1009	Part sub;
1010
1011	end = p->body + p->len;
1012	blen = strlen(p->boundary);
1013	alt = !strcmp(p->type, "multipart/alternative");
1014	for (s = boundary(p->body, end, p->boundary, blen); s && !cut;
1015	     s = next) {
1016		s += blen + 2;
1017		if (end - s >= 2 && s[0] == '-' && s[1] == '-')
1018			break;
1019		if (!(s = memchr(s, '\n', end - s)))
1020			break;
1021		s++;
1022		next = boundary(s, end, p->boundary, blen);
1023		len = (next ? next : end) - s;
1024		if (len && s[len - 1] == '\n')
1025			len--;
1026		if (len && s[len - 1] == '\r')
1027			len--;
1028		if (!alt) {
1029			renderpart(s, len, depth + 1, m);
1030			continue;
1031		}
1032		parsepart(s, len, &sub);
1033		if (!strcmp(sub.type, "text/plain"))
1034			rank = 3;
1035		else if (!strncmp(sub.type, "multipart/", 10))
1036			rank = 2;
1037		else
1038			rank = 1;
1039		if (rank > best || (rank == 1 && best == 1)) {
1040			best = rank;
1041			pick = s;
1042			plen = len;
1043		}
1044	}
1045	if (pick)
1046		renderpart(pick, plen, depth + 1, m);
1047}
1048
1049char *
1050nextid(const char **sp)
1051{
1052	const char *a, *b;
1053
1054	for (a = strchr(*sp, '<'); a; a = b) {
1055		if (!(b = strpbrk(a + 1, "<>")))
1056			break;
1057		if (*b == '<')
1058			continue;
1059		*sp = b + 1;
1060		if (validid(a + 1, b - a - 1))
1061			return xstrndup(a + 1, b - a - 1);
1062		b = strchr(b + 1, '<');
1063	}
1064	*sp += strlen(*sp);
1065	return NULL;
1066}
1067
1068void
1069notfound(void)
1070{
1071	pagebegin(404, "Not found");
1072	fputs("<main>\n<p>Not found.</p>\n", stdout);
1073	pageend();
1074}
1075
1076FILE *
1077openmsg(const char *file)
1078{
1079	struct stat st;
1080	FILE *fp;
1081	int fd;
1082
1083	if ((fd = open(file, O_RDONLY | O_NOFOLLOW | O_NONBLOCK)) < 0)
1084		return NULL;
1085	if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)
1086	|| !(fp = fdopen(fd, "r"))) {
1087		close(fd);
1088		return NULL;
1089	}
1090	return fp;
1091}
1092
1093void
1094pagebegin(int status, const char *pagetitle)
1095{
1096	size_t i;
1097
1098	httpheader(status, "text/html; charset=utf-8");
1099	fputs("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n"
1100		"<meta name=\"viewport\" "
1101		"content=\"width=device-width, initial-scale=1\">\n<title>",
1102		stdout);
1103	xmlencode(pagetitle);
1104	fputs("</title>\n<link rel=\"stylesheet\" href=\"", stdout);
1105	xmlencode(css);
1106	fputs("\">\n<link rel=\"icon\" href=\"", stdout);
1107	xmlencode(icon);
1108	fputs("\">\n</head>\n<body>\n<div id=\"top\"><a class=\"brand\" "
1109		"href=\"", stdout);
1110	xmlencode(base);
1111	fputs("/\"><img src=\"", stdout);
1112	xmlencode(icon);
1113	fputs("\" alt=\"\"> ", stdout);
1114	xmlencode(title);
1115	fputs("</a><nav>", stdout);
1116	for (i = 0; i < LENGTH(services); i++) {
1117		if (!strcmp(services[i].name, service)) {
1118			fputs("<a class=\"on\" href=\"", stdout);
1119			xmlencode(base);
1120			putchar('/');
1121		} else {
1122			fputs("<a href=\"", stdout);
1123			xmlencode(services[i].url);
1124		}
1125		fputs("\">", stdout);
1126		xmlencode(services[i].name);
1127		fputs("</a>", stdout);
1128	}
1129	fputs("</nav></div>\n", stdout);
1130}
1131
1132void
1133pageend(void)
1134{
1135	fputs("</main>\n</body>\n</html>\n", stdout);
1136}
1137
1138int
1139param(const char *v, const char *attr, char *buf, size_t len)
1140{
1141	Buf raw = { 0 }, d = { 0 }, u = { 0 };
1142	const Buf *src;
1143	const char *p = v, *q;
1144	char *a, *l;
1145	size_t n, alen = strlen(attr);
1146	int quote, ext, ret = -1;
1147
1148	while (ret < 0 && *p) {
1149		for (quote = 0; *p && (quote || *p != ';'); p++) {
1150			if (quote && *p == '\\' && p[1])
1151				p++;
1152			else if (*p == '"')
1153				quote = !quote;
1154		}
1155		if (!*p)
1156			break;
1157		for (p++; isspace((unsigned char)*p); p++)
1158			;
1159		for (q = p; *q && *q != '=' && *q != ';'
1160		     && !isspace((unsigned char)*q); q++)
1161			;
1162		n = q - p;
1163		ext = n == alen + 1 && p[alen] == '*';
1164		if ((n != alen && !ext) || strncasecmp(p, attr, alen))
1165			continue;
1166		for (; isspace((unsigned char)*q); q++)
1167			;
1168		if (*q != '=')
1169			continue;
1170		for (q++; isspace((unsigned char)*q); q++)
1171			;
1172		bufadd(&raw, "", 0);
1173		if (*q == '"') {
1174			for (q++; *q && *q != '"'; q++) {
1175				if (*q == '\\' && q[1])
1176					q++;
1177				bufc(&raw, *q);
1178			}
1179		} else {
1180			for (; *q && *q != ';'
1181			     && !isspace((unsigned char)*q); q++)
1182				bufc(&raw, *q);
1183		}
1184		src = &raw;
1185		if (ext && (a = strchr(raw.s, '\''))
1186		&& (l = strchr(a + 1, '\''))) {
1187			for (q = l + 1; *q; q++) {
1188				if (*q == '%' && hex(q[1]) >= 0
1189				&& hex(q[2]) >= 0) {
1190					bufc(&d, hex(q[1]) << 4 | hex(q[2]));
1191					q += 2;
1192				} else {
1193					bufc(&d, *q);
1194				}
1195			}
1196			*a = '\0';
1197			if (tou8(raw.s, d.s, d.n, &u) < 0)
1198				bufadd(&u, d.s, d.n);
1199			bufadd(&u, "", 0);
1200			src = &u;
1201		}
1202		n = src->n < len - 1 ? src->n : len - 1;
1203		memcpy(buf, src->s, n);
1204		buf[n] = '\0';
1205		ret = 0;
1206	}
1207	free(raw.s);
1208	free(d.s);
1209	free(u.s);
1210	return ret;
1211}
1212
1213int
1214parsedate(const char *s, long long *t, int *tz)
1215{
1216	static const char months[] = "janfebmaraprmayjunjulaugsepoctnovdec";
1217	static const int mdays[] = {
1218		31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
1219	};
1220	static const struct {
1221		const char *name;
1222		int off;
1223	} zones[] = {
1224		{ "edt", -240 }, { "est", -300 }, { "cdt", -300 },
1225		{ "cst", -360 }, { "mdt", -360 }, { "mst", -420 },
1226		{ "pdt", -420 }, { "pst", -480 },
1227	};
1228	long day, mon, year, hh, mm, ss = 0, z = 0, y, era, yoe, doy;
1229	const char *p;
1230	size_t n, i;
1231	int neg;
1232
1233	s = skipcfws(s);
1234	if (isalpha((unsigned char)*s)) {
1235		while (isalpha((unsigned char)*s))
1236			s++;
1237		s = skipcfws(s);
1238	}
1239	if (!digits(&s, 2, &day))
1240		return -1;
1241	s = skipcfws(s);
1242	for (n = 0; isalpha((unsigned char)s[n]); n++)
1243		;
1244	if (n < 3)
1245		return -1;
1246	for (mon = 0; mon < 12 && strncasecmp(s, months + 3 * mon, 3); mon++)
1247		;
1248	if (mon == 12)
1249		return -1;
1250	s = skipcfws(s + n);
1251	if ((n = digits(&s, 4, &year)) < 2)
1252		return -1;
1253	if (n == 2)
1254		year += year < 50 ? 2000 : 1900;
1255	else if (n == 3)
1256		year += 1900;
1257	s = skipcfws(s);
1258	if (!digits(&s, 2, &hh))
1259		return -1;
1260	if (*(s = skipcfws(s)) != ':')
1261		return -1;
1262	s = skipcfws(s + 1);
1263	if (!digits(&s, 2, &mm))
1264		return -1;
1265	if (*(p = skipcfws(s)) == ':') {
1266		s = skipcfws(p + 1);
1267		if (!digits(&s, 2, &ss))
1268			return -1;
1269	}
1270	s = skipcfws(s);
1271	if (*s == '+' || *s == '-') {
1272		neg = *s++ == '-';
1273		if (digits(&s, 4, &z) != 4 || z / 100 > 23 || z % 100 > 59)
1274			return -1;
1275		z = z / 100 * 60 + z % 100;
1276		if (neg)
1277			z = -z;
1278	} else if (isalpha((unsigned char)*s)) {
1279		for (n = 0; isalpha((unsigned char)s[n]); n++)
1280			;
1281		for (i = 0; i < LENGTH(zones); i++)
1282			if (n == 3 && !strncasecmp(s, zones[i].name, 3))
1283				z = zones[i].off;
1284	}
1285	if (year < 1900 || year > 9999 || day < 1 || day > mdays[mon]
1286	|| (mon == 1 && day == 29 && (year % 4 || (!(year % 100)
1287	&& year % 400))) || hh > 23 || mm > 59 || ss > 60)
1288		return -1;
1289	y = year - (mon < 2);
1290	era = y / 400;
1291	yoe = y - era * 400;
1292	doy = (153 * (mon + (mon > 1 ? -2 : 10)) + 2) / 5 + day - 1;
1293	*t = (long long)(era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy
1294		- 719468) * 86400 + hh * 3600 + mm * 60 + ss - z * 60;
1295	*tz = (int)z;
1296	return 0;
1297}
1298
1299void
1300parsefrom(const char *v, Msg *m)
1301{
1302	Buf ph = { 0 }, cm = { 0 }, ad = { 0 };
1303	const char *p;
1304	int depth = 0, quote = 0, angle = 0, done = 0;
1305
1306	for (p = v; *p; p++) {
1307		if (depth) {
1308			if (*p == '\\' && p[1])
1309				p++;
1310			else if (*p == '(')
1311				depth++;
1312			else if (*p == ')' && !--depth)
1313				continue;
1314			bufc(&cm, *p);
1315		} else if (quote) {
1316			if (*p == '\\' && p[1])
1317				p++;
1318			else if (*p == '"') {
1319				quote = 0;
1320				continue;
1321			}
1322			bufc(&ph, *p);
1323		} else if (angle) {
1324			if (*p == '>') {
1325				angle = 0;
1326				done = 1;
1327			} else {
1328				bufc(&ad, *p);
1329			}
1330		} else if (*p == '(') {
1331			depth = 1;
1332		} else if (*p == '"') {
1333			quote = 1;
1334		} else if (*p == '<' && !done) {
1335			angle = 1;
1336		} else if (!done) {
1337			bufc(&ph, *p);
1338		}
1339	}
1340	bufadd(&ph, "", 0);
1341	bufadd(&cm, "", 0);
1342	if (ad.n || done) {
1343		m->addr = clean(ad.s, ad.n);
1344		m->name = field(*ph.s ? ph.s : cm.s);
1345	} else {
1346		m->addr = clean(ph.s, ph.n);
1347		m->name = field(cm.s);
1348	}
1349	free(ph.s);
1350	free(cm.s);
1351	free(ad.s);
1352}
1353
1354void
1355parsepart(const char *s, size_t n, Part *p)
1356{
1357	char *ct, *cte, *cd;
1358	size_t h, b, i;
1359
1360	h = hdrend(s, n, &b);
1361	p->body = s + b;
1362	p->len = n - b;
1363	ct = hdrget(s, h, "Content-Type");
1364	cte = hdrget(s, h, "Content-Transfer-Encoding");
1365	cd = hdrget(s, h, "Content-Disposition");
1366	for (i = 0; ct && ct[i] && i + 1 < sizeof(p->type)
1367	     && (isalnum((unsigned char)ct[i]) || strchr("/-+._", ct[i])); i++)
1368		p->type[i] = tolower((unsigned char)ct[i]);
1369	p->type[i] = '\0';
1370	if (!strchr(p->type, '/'))
1371		strcpy(p->type, "text/plain");
1372	for (i = 0; cte && cte[i] && i + 1 < sizeof(p->cte)
1373	     && (isalnum((unsigned char)cte[i]) || cte[i] == '-'); i++)
1374		p->cte[i] = tolower((unsigned char)cte[i]);
1375	p->cte[i] = '\0';
1376	if (!ct || param(ct, "charset", p->charset, sizeof(p->charset)) < 0)
1377		p->charset[0] = '\0';
1378	if (!ct || param(ct, "boundary", p->boundary,
1379	    sizeof(p->boundary)) < 0)
1380		p->boundary[0] = '\0';
1381	if ((!cd || param(cd, "filename", p->name, sizeof(p->name)) < 0)
1382	&& (!ct || param(ct, "name", p->name, sizeof(p->name)) < 0))
1383		p->name[0] = '\0';
1384	free(ct);
1385	free(cte);
1386	free(cd);
1387}
1388
1389void
1390printage(long long t)
1391{
1392	static const char *units[] = {
1393		"minute", "hour", "day", "week", "month", "year"
1394	};
1395	static const long secs[] = {
1396		60, HOUR, DAY, 7 * DAY, 30 * DAY, 365 * DAY
1397	};
1398	static const long upto[] = {
1399		HOUR, DAY, 14 * DAY, 60 * DAY, 730 * DAY, LONG_MAX
1400	};
1401	static const long fade[] = { 7 * DAY, 30 * DAY, 180 * DAY, 730 * DAY };
1402	long d, n;
1403	size_t i, level;
1404
1405	if (t < 0 || t > MAXTIME) {
1406		fputs("<td class=\"r age a4\">&mdash;</td>", stdout);
1407		return;
1408	}
1409	if ((d = (long)((long long)time(NULL) - t)) < 0)
1410		d = 0;
1411	for (i = 0; d >= upto[i]; i++)
1412		;
1413	if ((n = d / secs[i]) < 1)
1414		n = 1;
1415	for (level = 0; level < LENGTH(fade) && d >= fade[level]; level++)
1416		;
1417	printf("<td class=\"r age a%zu\" title=\"", level);
1418	printtime(t, 0, "%Y-%m-%d %H:%M UTC");
1419	printf("\">%ld %s%s</td>", n, units[i], n == 1 ? "" : "s");
1420}
1421
1422void
1423printbody(const char *s, size_t n)
1424{
1425	const char *l, *nl, *cls;
1426	size_t len;
1427	int patch = 0;
1428
1429	if (n > left) {
1430		for (n = left; n && ((unsigned char)s[n] & 0xc0) == 0x80; n--)
1431			;
1432		cut = 1;
1433	}
1434	left -= n;
1435	fputs("<pre class=\"body\">", stdout);
1436	for (l = s; l < s + n; l = nl + 1) {
1437		nl = memchr(l, '\n', s + n - l);
1438		len = nl ? (size_t)(nl - l) : (size_t)(s + n - l);
1439		cls = lineclass(l, len, nl ? nl + 1 : NULL, s + n, &patch);
1440		if (cls)
1441			printf("<span class=\"%s\">", cls);
1442		xmlencoden(l, len);
1443		if (cls)
1444			fputs("</span>", stdout);
1445		if (!nl)
1446			break;
1447		putchar('\n');
1448	}
1449	fputs("</pre>\n", stdout);
1450}
1451
1452void
1453printmsg(size_t i, int sel)
1454{
1455	const Msg *m = &msgs[i];
1456	Buf b = { 0 };
1457	int trunc;
1458
1459	printf("<article class=\"mail%s\" id=\"", sel ? " sel" : "");
1460	xmlencode(m->id);
1461	fputs("\">\n<table class=\"meta\">\n<tr><td>From</td><td>", stdout);
1462	if (*m->name) {
1463		xmlencode(m->name);
1464		fputs(" &lt;", stdout);
1465		xmlencode(m->addr);
1466		fputs("&gt;", stdout);
1467	} else {
1468		xmlencode(m->addr);
1469	}
1470	fputs("</td></tr>\n<tr><td>Date</td><td>", stdout);
1471	printtime(m->date, m->tz, "%Y-%m-%d %H:%M");
1472	printf(" %c%02d%02d</td></tr>\n<tr><td>Subject</td><td>",
1473		m->tz < 0 ? '-' : '+', abs(m->tz) / 60, abs(m->tz) % 60);
1474	xmlencode(*m->subject ? m->subject : "(no subject)");
1475	fputs("</td></tr>\n</table>\n<p class=\"links\"><a href=\"", stdout);
1476	msgurl(m->id, "");
1477	fputs("\">permalink</a><a href=\"", stdout);
1478	msgurl(m->id, "raw");
1479	fputs("\">raw</a><a href=\"mailto:", stdout);
1480	urlencode(list, mailsafe);
1481	putchar('@');
1482	xmlencode(domain);
1483	fputs("?In-Reply-To=%3C", stdout);
1484	urlencode(m->id, mailsafe);
1485	fputs("%3E&amp;subject=", stdout);
1486	if (strncasecmp(m->subject, "re:", 3))
1487		fputs("Re%3A%20", stdout);
1488	urlencode(m->subject, mailsafe);
1489	fputs("\">reply</a></p>\n", stdout);
1490
1491	left = (size_t)maxbody;
1492	cut = 0;
1493	if ((trunc = readfile(m->file, (size_t)maxhdr + 2 * (size_t)maxbody,
1494	    &b, 0)) < 0) {
1495		fputs("<p class=\"att\">Message unreadable.</p>\n", stdout);
1496	} else {
1497		renderpart(b.s, b.n, 0, i);
1498		if (cut || trunc) {
1499			fputs("<p class=\"att\">truncated, see <a href=\"",
1500				stdout);
1501			msgurl(m->id, "raw");
1502			fputs("\">raw</a></p>\n", stdout);
1503		}
1504	}
1505	fputs("</article>\n", stdout);
1506	free(b.s);
1507}
1508
1509void
1510printtime(long long t, int tz, const char *fmt)
1511{
1512	struct tm tm;
1513	time_t secs;
1514	char buf[64];
1515
1516	if (t < 0 || t > MAXTIME)
1517		return;
1518	secs = (time_t)(t + tz * 60LL);
1519	if (!gmtime_r(&secs, &tm))
1520		return;
1521	if (strftime(buf, sizeof(buf), fmt, &tm))
1522		fputs(buf, stdout);
1523}
1524
1525void
1526qp(const char *s, size_t n, int q, Buf *out)
1527{
1528	size_t i, j;
1529
1530	for (i = 0; i < n; i++) {
1531		if (q && s[i] == '_') {
1532			bufc(out, ' ');
1533		} else if (s[i] != '=') {
1534			bufc(out, s[i]);
1535		} else if (i + 2 < n && hex(s[i + 1]) >= 0
1536		&& hex(s[i + 2]) >= 0) {
1537			bufc(out, hex(s[i + 1]) << 4 | hex(s[i + 2]));
1538			i += 2;
1539		} else {
1540			for (j = i + 1; j < n && (s[j] == ' ' || s[j] == '\t');
1541			     j++)
1542				;
1543			if (j < n && s[j] == '\n')
1544				i = j;
1545			else if (j + 1 < n && s[j] == '\r' && s[j + 1] == '\n')
1546				i = j + 1;
1547			else
1548				bufc(out, '=');
1549		}
1550	}
1551}
1552
1553void
1554rawpage(size_t t)
1555{
1556	char buf[8192];
1557	size_t n;
1558	FILE *fp;
1559
1560	if (!(fp = openmsg(msgs[t].file))) {
1561		notfound();
1562		return;
1563	}
1564	httpheader(200, "text/plain; charset=utf-8");
1565	while ((n = fread(buf, 1, sizeof(buf), fp)))
1566		fwrite(buf, 1, n, stdout);
1567	fclose(fp);
1568}
1569
1570int
1571readfile(const char *file, size_t max, Buf *b, int hdronly)
1572{
1573	char chunk[4096];
1574	size_t want, r, i;
1575	FILE *fp;
1576	int trunc = 0;
1577
1578	bufadd(b, "", 0);
1579	if (!(fp = openmsg(file)))
1580		return -1;
1581	for (;;) {
1582		if (!(want = max - b->n)) {
1583			trunc = fgetc(fp) != EOF;
1584			break;
1585		}
1586		if (want > sizeof(chunk))
1587			want = sizeof(chunk);
1588		if (!(r = fread(chunk, 1, want, fp)))
1589			break;
1590		i = b->n > 2 ? b->n - 2 : 0;
1591		bufadd(b, chunk, r);
1592		for (; hdronly && i + 1 < b->n; i++)
1593			if (b->s[i] == '\n' && (b->s[i + 1] == '\n'
1594			|| (b->s[i + 1] == '\r' && i + 2 < b->n
1595			&& b->s[i + 2] == '\n')))
1596				hdronly = 2;
1597		if (hdronly == 2)
1598			break;
1599	}
1600	fclose(fp);
1601	return trunc;
1602}
1603
1604void
1605readline(const char *name, const char *file, char *buf, size_t len)
1606{
1607	char path[PATH_MAX];
1608	FILE *fp;
1609
1610	buf[0] = '\0';
1611	if (bprintf(path, sizeof(path), "%s/%s/%s", root, name, file) < 0
1612	|| !(fp = fopen(path, "r")))
1613		return;
1614	if (fgets(buf, (int)len, fp))
1615		buf[strcspn(buf, "\r\n")] = '\0';
1616	fclose(fp);
1617}
1618
1619void
1620renderpart(const char *s, size_t n, int depth, size_t m)
1621{
1622	Buf raw = { 0 }, u = { 0 }, t = { 0 };
1623	Part p;
1624
1625	if (cut)
1626		return;
1627	parsepart(s, n, &p);
1628	if (!strncmp(p.type, "multipart/", 10) && *p.boundary
1629	&& depth < maxdepth) {
1630		multipart(&p, depth, m);
1631	} else if (!strcmp(p.type, "text/plain")
1632	|| !strcmp(p.type, "text/x-diff") || !strcmp(p.type, "text/x-patch")) {
1633		decode(&p, &raw);
1634		if (tou8(p.charset, raw.s, raw.n, &u) < 0)
1635			bufadd(&u, raw.s, raw.n);
1636		sanitize(u.s, u.n, &t, 1);
1637		printbody(t.s, t.n);
1638	} else if (!strcmp(p.type, "text/html")) {
1639		fputs("<p class=\"att\">HTML part not shown, see <a href=\"",
1640			stdout);
1641		msgurl(msgs[m].id, "raw");
1642		fputs("\">raw</a></p>\n", stdout);
1643	} else {
1644		attachment(&p);
1645	}
1646	free(raw.s);
1647	free(u.s);
1648	free(t.s);
1649}
1650
1651void
1652route(char *path)
1653{
1654	char *rest;
1655	size_t n, t;
1656	int page = 0;
1657
1658	while (*path == '/')
1659		path++;
1660	if (!*path) {
1661		indexpage();
1662		return;
1663	}
1664	if ((rest = strchr(path, '/')))
1665		*rest++ = '\0';
1666	if (!islist(path)) {
1667		notfound();
1668		return;
1669	}
1670	list = path;
1671	if (!rest || !*rest) {
1672		listpage();
1673		return;
1674	}
1675	n = strlen(rest);
1676	if (rest[n - 1] == '/') {
1677		rest[--n] = '\0';
1678	} else if (n > 4 && !strcmp(rest + n - 4, "/raw")) {
1679		rest[n -= 4] = '\0';
1680		page = 1;
1681	} else if (n > 10 && !strcmp(rest + n - 10, "/t.mbox.gz")) {
1682		rest[n -= 10] = '\0';
1683		page = 2;
1684	}
1685	if (!validid(rest, n)) {
1686		notfound();
1687		return;
1688	}
1689	scan(list);
1690	if ((t = byid(rest)) == NONE)
1691		notfound();
1692	else if (page == 1)
1693		rawpage(t);
1694	else if (page == 2)
1695		mboxpage(t);
1696	else
1697		threadpage(t);
1698}
1699
1700void
1701sanitize(const char *s, size_t n, Buf *out, int multi)
1702{
1703	const unsigned char *p = (const unsigned char *)s, *e = p + n;
1704	unsigned long c;
1705	size_t i, len;
1706
1707	while (p < e) {
1708		c = *p;
1709		if (c < 0x80) {
1710			if (c == '\n' || c == '\t' || (c == '\r' && !multi))
1711				bufc(out, multi ? (int)c : ' ');
1712			else if ((c < 0x20 && c != '\r') || c == 0x7f)
1713				bufadd(out, REPL, 3);
1714			else if (c != '\r')
1715				bufc(out, (int)c);
1716			p++;
1717			continue;
1718		}
1719		if (c >= 0xc2 && c <= 0xdf) {
1720			len = 2;
1721			c &= 0x1f;
1722		} else if (c >= 0xe0 && c <= 0xef) {
1723			len = 3;
1724			c &= 0x0f;
1725		} else if (c >= 0xf0 && c <= 0xf4) {
1726			len = 4;
1727			c &= 0x07;
1728		} else {
1729			len = 0;
1730		}
1731		for (i = 1; i < len; i++) {
1732			if (p + i >= e || (p[i] & 0xc0) != 0x80)
1733				len = 0;
1734			else
1735				c = c << 6 | (p[i] & 0x3f);
1736		}
1737		if (!len || (len == 3 && c < 0x800)
1738		|| (len == 4 && (c < 0x10000 || c > 0x10ffff))
1739		|| (c >= 0xd800 && c <= 0xdfff)) {
1740			bufadd(out, REPL, 3);
1741			p++;
1742			continue;
1743		}
1744		if (c < 0xa0 || (c >= 0x202a && c <= 0x202e)
1745		|| (c >= 0x2066 && c <= 0x2069))
1746			bufadd(out, REPL, 3);
1747		else
1748			bufadd(out, (const char *)p, len);
1749		p += len;
1750	}
1751}
1752
1753void
1754scan(const char *name)
1755{
1756	static const char *sub[] = { "new", "cur" };
1757	char dir[PATH_MAX], path[PATH_MAX];
1758	struct dirent *e;
1759	struct stat st;
1760	Msg *v;
1761	DIR *dp;
1762	size_t i, j, n = 0, cap = 0;
1763
1764	freemsgs();
1765	for (i = 0; i < LENGTH(sub); i++) {
1766		if (bprintf(dir, sizeof(dir), "%s/%s/archive/%s", root, name,
1767		    sub[i]) < 0 || !(dp = opendir(dir)))
1768			continue;
1769		while ((e = readdir(dp))) {
1770			if (e->d_name[0] == '.'
1771			|| bprintf(path, sizeof(path), "%s/%s", dir,
1772			   e->d_name) < 0
1773			|| lstat(path, &st) < 0 || !S_ISREG(st.st_mode))
1774				continue;
1775			if (n == cap) {
1776				cap = cap ? cap * 2 : 256;
1777				if (!(v = realloc(msgs, cap * sizeof(*v))))
1778					die("out of memory");
1779				msgs = v;
1780			}
1781			memset(&msgs[n], 0, sizeof(*msgs));
1782			msgs[n].file = xstrndup(path, strlen(path));
1783			msgs[n].mtime = (long long)st.st_mtime;
1784			n++;
1785		}
1786		closedir(dp);
1787	}
1788	if (n)
1789		qsort(msgs, n, sizeof(*msgs), cmpmtime);
1790	for (i = j = 0; i < n; i++) {
1791		if (i < (size_t)maxmsgs && loadmsg(&msgs[i]) >= 0)
1792			msgs[j++] = msgs[i];
1793		else
1794			freemsg(&msgs[i]);
1795	}
1796	nmsgs = j;
1797	if (nmsgs)
1798		qsort(msgs, nmsgs, sizeof(*msgs), cmpfirst);
1799	for (i = j = 0; i < nmsgs; i++) {
1800		if (j && !strcmp(msgs[j - 1].id, msgs[i].id))
1801			freemsg(&msgs[i]);
1802		else
1803			msgs[j++] = msgs[i];
1804	}
1805	nmsgs = j;
1806	if (nmsgs)
1807		qsort(msgs, nmsgs, sizeof(*msgs), cmpdate);
1808	if (!(ids = calloc(nmsgs + 1, sizeof(*ids))))
1809		die("out of memory");
1810	for (i = 0; i < nmsgs; i++)
1811		ids[i] = i;
1812	if (nmsgs)
1813		qsort(ids, nmsgs, sizeof(*ids), cmpid);
1814	threadmsgs();
1815}
1816
1817const char *
1818skipcfws(const char *s)
1819{
1820	int depth = 0;
1821
1822	for (; *s; s++) {
1823		if (depth && *s == '\\' && s[1])
1824			s++;
1825		else if (*s == '(')
1826			depth++;
1827		else if (depth && *s == ')')
1828			depth--;
1829		else if (!depth && !isspace((unsigned char)*s) && *s != ',')
1830			break;
1831	}
1832	return s;
1833}
1834
1835void
1836threadmsgs(void)
1837{
1838	size_t *stack, i, j, k, n, p, r;
1839
1840	for (i = 0; i < nmsgs; i++) {
1841		msgs[i].parent = msgs[i].root = NONE;
1842		if (msgs[i].irt && (p = byid(msgs[i].irt)) != NONE && p != i)
1843			msgs[i].parent = p;
1844		for (k = msgs[i].nrefs; msgs[i].parent == NONE && k; k--)
1845			if ((p = byid(msgs[i].refs[k - 1])) != NONE && p != i)
1846				msgs[i].parent = p;
1847	}
1848	if (!(stack = calloc(nmsgs + 1, sizeof(*stack))))
1849		die("out of memory");
1850	for (i = 0; i < nmsgs; i++) {
1851		n = 0;
1852		for (j = i; msgs[j].root == NONE && msgs[j].seen != i + 1;
1853		     j = msgs[j].parent) {
1854			msgs[j].seen = i + 1;
1855			stack[n++] = j;
1856			if (msgs[j].parent == NONE)
1857				break;
1858		}
1859		r = msgs[j].root != NONE ? msgs[j].root : j;
1860		for (k = 0; k < n; k++)
1861			msgs[stack[k]].root = r;
1862	}
1863	free(stack);
1864}
1865
1866void
1867threadpage(size_t t)
1868{
1869	const Msg *r = &msgs[msgs[t].root];
1870	size_t i, n = 0;
1871
1872	if (msgs[t].root != t) {
1873		printf("Status: 302 Found\r\nLocation: %s/", base);
1874		urlencode(list, pathsafe);
1875		putchar('/');
1876		urlencode(r->id, pathsafe);
1877		fputs("/#", stdout);
1878		urlencode(msgs[t].id, pathsafe);
1879		fputs("\r\n", stdout);
1880		httpheader(302, "text/plain; charset=utf-8");
1881		return;
1882	}
1883	for (i = 0; i < nmsgs; i++)
1884		if (msgs[i].root == t)
1885			n++;
1886	pagebegin(200, *r->subject ? r->subject : "(no subject)");
1887	listheader(0);
1888	fputs("<h2 class=\"subject\">", stdout);
1889	xmlencode(*r->subject ? r->subject : "(no subject)");
1890	printf("</h2>\n<p class=\"info\">%zu message%s &middot; <a href=\"",
1891		n, n == 1 ? "" : "s");
1892	msgurl(r->id, "t.mbox.gz");
1893	fputs("\">t.mbox.gz</a></p>\n", stdout);
1894	for (i = 0; i < nmsgs; i++)
1895		if (msgs[i].root == t)
1896			printmsg(i, i == t);
1897	pageend();
1898}
1899
1900int
1901tou8(const char *cs, const char *s, size_t n, Buf *out)
1902{
1903	char name[64], tmp[1024], *in, *o;
1904	size_t i, il, ol, r;
1905	iconv_t cd;
1906
1907	for (i = 0; cs[i] && cs[i] != '*'; i++) {
1908		if (i + 1 >= sizeof(name) || !(isalnum((unsigned char)cs[i])
1909		|| strchr("-_.:", cs[i])))
1910			return -1;
1911		name[i] = cs[i];
1912	}
1913	name[i] = '\0';
1914	if (!*name || !strcasecmp(name, "utf-8") || !strcasecmp(name, "utf8")
1915	|| !strcasecmp(name, "us-ascii") || !strcasecmp(name, "ascii")) {
1916		bufadd(out, s, n);
1917		return 0;
1918	}
1919	if ((cd = iconv_open("UTF-8", name)) == (iconv_t)-1)
1920		return -1;
1921	in = (char *)s;
1922	il = n;
1923	while (il) {
1924		o = tmp;
1925		ol = sizeof(tmp);
1926		r = iconv(cd, &in, &il, &o, &ol);
1927		bufadd(out, tmp, o - tmp);
1928		if (r != (size_t)-1 || errno == E2BIG)
1929			continue;
1930		bufadd(out, REPL, 3);
1931		if (errno != EILSEQ)
1932			break;
1933		in++;
1934		il--;
1935	}
1936	o = tmp;
1937	ol = sizeof(tmp);
1938	iconv(cd, NULL, NULL, &o, &ol);
1939	bufadd(out, tmp, o - tmp);
1940	iconv_close(cd);
1941	return 0;
1942}
1943
1944void
1945urlencode(const char *s, const char *safe)
1946{
1947	for (; *s; s++) {
1948		if (isalnum((unsigned char)*s) || strchr(safe, *s))
1949			putchar(*s);
1950		else
1951			printf("%%%02X", (unsigned char)*s);
1952	}
1953}
1954
1955int
1956validid(const char *s, size_t n)
1957{
1958	size_t i;
1959
1960	if (!n || n > FIELDLEN)
1961		return 0;
1962	for (i = 0; i < n; i++)
1963		if ((unsigned char)s[i] <= ' ' || s[i] == 0x7f || s[i] == '<'
1964		|| s[i] == '>')
1965			return 0;
1966	return 1;
1967}
1968
1969int
1970validlist(const char *s)
1971{
1972	size_t n;
1973
1974	for (n = 0; s[n]; n++)
1975		if (!islower((unsigned char)s[n])
1976		&& !isdigit((unsigned char)s[n]) && !(n && s[n] == '-'))
1977			return 0;
1978	return n && n <= 64;
1979}
1980
1981void
1982xmlencode(const char *s)
1983{
1984	if (s)
1985		xmlencoden(s, strlen(s));
1986}
1987
1988void
1989xmlencoden(const char *s, size_t len)
1990{
1991	for (; len; s++, len--) {
1992		switch (*s) {
1993		case '<':  fputs("&lt;", stdout);   break;
1994		case '>':  fputs("&gt;", stdout);   break;
1995		case '&':  fputs("&amp;", stdout);  break;
1996		case '"':  fputs("&quot;", stdout); break;
1997		case '\'': fputs("&#39;", stdout);  break;
1998		default:   putchar(*s);
1999		}
2000	}
2001}
2002
2003char *
2004xstrndup(const char *s, size_t n)
2005{
2006	char *r;
2007
2008	if (!(r = malloc(n + 1)))
2009		die("out of memory");
2010	if (n)
2011		memcpy(r, s, n);
2012	r[n] = '\0';
2013	return r;
2014}
2015
2016/* main */
2017int
2018main(int argc, char *argv[])
2019{
2020	char path[2048];
2021	const char *q;
2022
2023	if (argc == 2 && !strcmp(argv[1], "-v")) {
2024		fputs("herbarium-"VERSION"\n", stderr);
2025		return 1;
2026	}
2027	base = envor("SCRIPT_NAME", "");
2028	q = envor("QUERY_STRING", "");
2029	if (!strncmp(q, "ofs=", 4) && (ofs = strtol(q + 4, NULL, 10)) < 0)
2030		ofs = 0;
2031
2032	if (bprintf(path, sizeof(path), "%s", envor("PATH_INFO", "")) < 0)
2033		notfound();
2034	else
2035		route(path);
2036	return 0;
2037}