greenhouse

greenhouse

minimal software forge

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

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