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