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