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