greenhouse

greenhouse

minimal software forge

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

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