greenhouse / pollen / pollen.c980 lines · 23825B · raw
1/* imports */
2#include <ctype.h>
3#include <dirent.h>
4#include <errno.h>
5#include <fcntl.h>
6#include <limits.h>
7#include <signal.h>
8#include <stdarg.h>
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12#include <strings.h>
13#include <syslog.h>
14#include <sysexits.h>
15#include <time.h>
16#include <unistd.h>
17#include <sys/stat.h>
18#include <sys/wait.h>
19
20/* macros */
21#define LENGTH(X) (sizeof(X) / sizeof((X)[0]))
22#define ADDRLEN 256
23#define HDRLEN 4096
24#define TOKLEN 32
25
26/* function declarations */
27static int addrok(const char *s);
28static int archive(const char *buf, size_t len, char *path, size_t plen);
29static int automated(void);
30static void bounce(const char *ext);
31static int bprintf(char *buf, size_t len, const char *fmt, ...);
32static void confirm(const char *tok);
33static int deliver(const char *const *rcpts, size_t n, const char *buf,
34 size_t len);
35static void die(int code, const char *fmt, ...);
36static int dropped(size_t pos);
37static size_t fieldend(size_t pos);
38static int getheader(const char *name, char *buf, size_t len);
39static void help(void);
40static int isfield(size_t pos, const char *name);
41static int ispublic(void);
42static int loadsubs(char ***subs, size_t *n);
43static void lower(char *s);
44static int newtoken(char *tok);
45static int notify(const char *to, const char *replyto, const char *subject,
46 const char *fmt, ...);
47static int parseaddr(const char *s, char *buf, size_t len);
48static void post(void);
49static void prune(void);
50static int readline(const char *file, char *buf, size_t len);
51static void readmsg(void);
52static int readpending(const char *path, char *act, char *addr,
53 long long *t);
54static void request(const char *act);
55static int sender(char *buf, size_t len);
56static int setsub(const char *addr, int add);
57static void usage(void);
58static int validlist(const char *s);
59static int validtoken(const char *s);
60
61/* variables */
62static const char *list;
63static char dir[PATH_MAX];
64static char *msg;
65static size_t msglen;
66static size_t hdrlen;
67
68#include "config.h"
69
70/* function implementations */
71int
72addrok(const char *s)
73{
74 const char *at, *p;
75 unsigned char c;
76 size_t n = strlen(s);
77
78 if (!n || n > 254 || *s == '-' || s[n - 1] == '.'
79 || !(at = strchr(s, '@')) || at == s || !at[1]
80 || at[1] == '.' || at[1] == '-' || strchr(at + 1, '@')
81 || !strcasecmp(at + 1, domain))
82 return 0;
83 for (p = s; *p; p++) {
84 c = *p;
85 if (p < at && !isalnum(c)
86 && !strchr("!#$%&'*+./=?^_`{|}~-", c))
87 return 0;
88 if (p > at && !isalnum(c) && c != '.' && c != '-')
89 return 0;
90 }
91 return 1;
92}
93
94int
95archive(const char *buf, size_t len, char *path, size_t plen)
96{
97 static const char *sub[] = {
98 "archive", "archive/tmp", "archive/new", "archive/cur"
99 };
100 char host[256], name[512], d[PATH_MAX], tmp[PATH_MAX];
101 struct timespec ts;
102 size_t i, off;
103 ssize_t w;
104 int fd, ok;
105
106 for (i = 0; i < LENGTH(sub); i++) {
107 if (bprintf(d, sizeof(d), "%s/%s", dir, sub[i]) < 0
108 || (mkdir(d, 0750) < 0 && errno != EEXIST))
109 return -1;
110 }
111 if (gethostname(host, sizeof(host)) < 0)
112 strcpy(host, "localhost");
113 host[sizeof(host) - 1] = '\0';
114 for (i = 0; host[i]; i++)
115 if (host[i] == '/' || host[i] == ':')
116 host[i] = '_';
117 clock_gettime(CLOCK_REALTIME, &ts);
118 if (bprintf(name, sizeof(name), "%lld.M%ldP%ld.%s",
119 (long long)ts.tv_sec, ts.tv_nsec / 1000, (long)getpid(),
120 host) < 0
121 || bprintf(tmp, sizeof(tmp), "%s/archive/tmp/%s", dir, name) < 0
122 || bprintf(path, plen, "%s/archive/new/%s", dir, name) < 0)
123 return -1;
124 if ((fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0640)) < 0)
125 return -1;
126 for (off = 0; off < len; off += w) {
127 if ((w = write(fd, buf + off, len - off)) < 0) {
128 if (errno != EINTR)
129 break;
130 w = 0;
131 }
132 }
133 ok = off == len && !fsync(fd);
134 if (close(fd) < 0)
135 ok = 0;
136 if (ok && link(tmp, path) < 0)
137 ok = 0;
138 unlink(tmp);
139 if (!ok || bprintf(d, sizeof(d), "%s/archive/new", dir) < 0)
140 return -1;
141 if ((fd = open(d, O_RDONLY)) >= 0) {
142 fsync(fd);
143 close(fd);
144 }
145 return 0;
146}
147
148int
149automated(void)
150{
151 char v[HDRLEN], rp[ADDRLEN];
152 int n;
153
154 if ((n = getheader("Auto-Submitted", v, sizeof(v)))) {
155 if (n < 0 || strcspn(v, " \t(;") != 2
156 || strncasecmp(v, "no", 2))
157 return 1;
158 }
159 if ((n = getheader("Precedence", v, sizeof(v)))) {
160 if (n < 0 || (strcspn(v, " \t(;") == 4
161 && (!strncasecmp(v, "bulk", 4) || !strncasecmp(v, "list", 4)
162 || !strncasecmp(v, "junk", 4))))
163 return 1;
164 }
165 if (getheader("Return-Path", v, sizeof(v)) < 1
166 || parseaddr(v, rp, sizeof(rp)) < 0 || !*rp)
167 return 1;
168 lower(rp);
169 return !strncmp(rp, "mailer-daemon@", 14);
170}
171
172void
173bounce(const char *ext)
174{
175 char v[HDRLEN];
176
177 if (getheader("Return-Path", v, sizeof(v)) < 1)
178 v[0] = '\0';
179 syslog(LOG_NOTICE, "%s: bounce %s from %s", list, ext, v);
180}
181
182int
183bprintf(char *buf, size_t len, const char *fmt, ...)
184{
185 va_list ap;
186 int n;
187
188 va_start(ap, fmt);
189 n = vsnprintf(buf, len, fmt, ap);
190 va_end(ap);
191 if (n < 0 || (size_t)n >= len)
192 return -1;
193 return 0;
194}
195
196void
197confirm(const char *tok)
198{
199 char path[PATH_MAX], act[16], addr[ADDRLEN], sub[ADDRLEN + 64];
200 long long t = 0;
201 int add, r = 0;
202
203 if (validtoken(tok)
204 && bprintf(path, sizeof(path), "%s/pending/%s", dir, tok) == 0
205 && (r = readpending(path, act, addr, &t)) < 0 && errno != ENOENT)
206 die(EX_TEMPFAIL, "cannot read confirmation token");
207 if (r != 1 || t + expiry < (long long)time(NULL))
208 die(EX_NOPERM, "unknown or expired confirmation token");
209 add = !strcmp(act, "subscribe");
210 if (setsub(addr, add) < 0)
211 die(EX_TEMPFAIL, "cannot update subscribers");
212 if (bprintf(sub, sizeof(sub), "%s %s@%s",
213 add ? "Subscribed to" : "Unsubscribed from",
214 list, domain) < 0)
215 die(EX_TEMPFAIL, "list name too long");
216 if (add)
217 r = notify(addr, NULL, sub,
218 "You are now subscribed to %s@%s.\n\n"
219 "To unsubscribe, send an empty message to\n"
220 "%s+unsubscribe@%s.\n",
221 list, domain, list, domain);
222 else
223 r = notify(addr, NULL, sub,
224 "You are no longer subscribed to %s@%s.\n",
225 list, domain);
226 if (r < 0)
227 die(EX_TEMPFAIL, "cannot send notice");
228 if (unlink(path) < 0 && errno != ENOENT)
229 die(EX_TEMPFAIL, "cannot remove confirmation token");
230 syslog(LOG_INFO, "%s: %s %s", list,
231 add ? "subscribed" : "unsubscribed", addr);
232}
233
234int
235deliver(const char *const *rcpts, size_t n, const char *buf, size_t len)
236{
237 char from[ADDRLEN + 64], **argv;
238 size_t i, off;
239 ssize_t w;
240 pid_t pid;
241 int fd[2], st, ret = -1;
242
243 if (!n)
244 return 0;
245 if (bprintf(from, sizeof(from), "%s+bounces@%s", list, domain) < 0
246 || !(argv = calloc(n + 6, sizeof(*argv))))
247 return -1;
248 argv[0] = (char *)sendmail;
249 argv[1] = "-i";
250 argv[2] = "-f";
251 argv[3] = from;
252 argv[4] = "--";
253 for (i = 0; i < n; i++)
254 argv[5 + i] = (char *)rcpts[i];
255 if (pipe(fd) < 0)
256 goto end;
257 if ((pid = fork()) < 0) {
258 close(fd[0]);
259 close(fd[1]);
260 goto end;
261 }
262 if (!pid) {
263 dup2(fd[0], 0);
264 close(fd[0]);
265 close(fd[1]);
266 execv(sendmail, argv);
267 _exit(127);
268 }
269 close(fd[0]);
270 for (off = 0; off < len; off += w) {
271 if ((w = write(fd[1], buf + off, len - off)) < 0) {
272 if (errno != EINTR)
273 break;
274 w = 0;
275 }
276 }
277 close(fd[1]);
278 while (waitpid(pid, &st, 0) < 0)
279 if (errno != EINTR)
280 goto end;
281 if (off == len && WIFEXITED(st) && !WEXITSTATUS(st))
282 ret = 0;
283end:
284 free(argv);
285 return ret;
286}
287
288void
289die(int code, const char *fmt, ...)
290{
291 char buf[512];
292 va_list ap;
293
294 va_start(ap, fmt);
295 vsnprintf(buf, sizeof(buf), fmt, ap);
296 va_end(ap);
297 printf("%s\n", buf);
298 fflush(stdout);
299 syslog(code == EX_TEMPFAIL ? LOG_ERR : LOG_NOTICE, "%s: %s",
300 list ? list : "-", buf);
301 exit(code);
302}
303
304int
305dropped(size_t pos)
306{
307 return (hdrlen - pos >= 5 && !strncasecmp(msg + pos, "List-", 5))
308 || isfield(pos, "Return-Path") || isfield(pos, "Delivered-To");
309}
310
311size_t
312fieldend(size_t pos)
313{
314 const char *p;
315
316 while ((p = memchr(msg + pos, '\n', hdrlen - pos))) {
317 pos = p - msg + 1;
318 if (pos == hdrlen || (msg[pos] != ' ' && msg[pos] != '\t'))
319 return pos;
320 }
321 return hdrlen;
322}
323
324int
325getheader(const char *name, char *buf, size_t len)
326{
327 size_t pos, end, i, n = 0;
328 const char *p;
329 int found = 0;
330
331 buf[0] = '\0';
332 for (pos = 0; pos < hdrlen; pos = end) {
333 end = fieldend(pos);
334 if (!isfield(pos, name) || found++)
335 continue;
336 p = (const char *)memchr(msg + pos, ':', end - pos) + 1;
337 for (; p < msg + end; p++) {
338 if (*p == '\r' || *p == '\n')
339 continue;
340 if (!*p || n + 1 >= len)
341 return -1;
342 buf[n++] = *p;
343 }
344 while (n && (buf[n - 1] == ' ' || buf[n - 1] == '\t'))
345 n--;
346 buf[n] = '\0';
347 for (i = 0; buf[i] == ' ' || buf[i] == '\t'; i++)
348 ;
349 memmove(buf, buf + i, n - i + 1);
350 }
351 return found;
352}
353
354void
355help(void)
356{
357 char addr[ADDRLEN], desc[256], sub[ADDRLEN + 64], arch[512] = "";
358
359 if (sender(addr, sizeof(addr)) < 0)
360 die(EX_NOPERM, "invalid or missing From address");
361 readline("description", desc, sizeof(desc));
362 if (bprintf(sub, sizeof(sub), "Help for %s@%s", list, domain) < 0
363 || (ispublic() && bprintf(arch, sizeof(arch), "archive %s%s/\n",
364 web, list) < 0))
365 die(EX_TEMPFAIL, "list name too long");
366 if (notify(addr, NULL, sub,
367 "%s@%s: %s\n\n"
368 "post %s@%s\n"
369 "subscribe %s+subscribe@%s\n"
370 "unsubscribe %s+unsubscribe@%s\n"
371 "help %s+help@%s\n"
372 "%s",
373 list, domain, desc, list, domain, list, domain,
374 list, domain, list, domain, arch) < 0)
375 die(EX_TEMPFAIL, "cannot send help");
376}
377
378int
379isfield(size_t pos, const char *name)
380{
381 size_t n = strlen(name);
382
383 if (hdrlen - pos < n || strncasecmp(msg + pos, name, n))
384 return 0;
385 for (pos += n; pos < hdrlen && (msg[pos] == ' ' || msg[pos] == '\t');
386 pos++)
387 ;
388 return pos < hdrlen && msg[pos] == ':';
389}
390
391int
392ispublic(void)
393{
394 char path[PATH_MAX];
395 struct stat st;
396
397 return *web && bprintf(path, sizeof(path), "%s/public", dir) == 0
398 && stat(path, &st) == 0;
399}
400
401int
402loadsubs(char ***subs, size_t *n)
403{
404 char path[PATH_MAX], *line = NULL, **v;
405 size_t cap = 0, max = 0;
406 FILE *fp;
407 int ret = -1;
408
409 *subs = NULL;
410 *n = 0;
411 if (bprintf(path, sizeof(path), "%s/subscribers", dir) < 0)
412 return -1;
413 if (!(fp = fopen(path, "r")))
414 return errno == ENOENT ? 0 : -1;
415 while (getline(&line, &cap, fp) > 0) {
416 line[strcspn(line, "\r\n")] = '\0';
417 lower(line);
418 if (!*line)
419 continue;
420 if (!addrok(line)) {
421 syslog(LOG_WARNING, "%s: skipping invalid subscriber",
422 list);
423 continue;
424 }
425 if (*n == max) {
426 max = max ? max * 2 : 64;
427 if (!(v = realloc(*subs, max * sizeof(*v))))
428 goto end;
429 *subs = v;
430 }
431 if (!((*subs)[*n] = strdup(line)))
432 goto end;
433 (*n)++;
434 }
435 if (!ferror(fp))
436 ret = 0;
437end:
438 free(line);
439 fclose(fp);
440 return ret;
441}
442
443void
444lower(char *s)
445{
446 for (; *s; s++)
447 *s = tolower((unsigned char)*s);
448}
449
450int
451newtoken(char *tok)
452{
453 unsigned char b[TOKLEN / 2];
454 FILE *fp;
455 size_t i;
456 int ok;
457
458 if (!(fp = fopen("/dev/urandom", "r")))
459 return -1;
460 ok = fread(b, 1, sizeof(b), fp) == sizeof(b);
461 fclose(fp);
462 if (!ok)
463 return -1;
464 for (i = 0; i < sizeof(b); i++)
465 sprintf(tok + 2 * i, "%02x", b[i]);
466 return 0;
467}
468
469int
470notify(const char *to, const char *replyto, const char *subject,
471 const char *fmt, ...)
472{
473 char date[64], id[TOKLEN + 1], *buf = NULL;
474 size_t len = 0;
475 time_t t = time(NULL);
476 va_list ap;
477 FILE *fp;
478 int ret;
479
480 if (newtoken(id) < 0
481 || !strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S +0000",
482 gmtime(&t))
483 || !(fp = open_memstream(&buf, &len)))
484 return -1;
485 fprintf(fp, "From: %s <%s+help@%s>\n", list, list, domain);
486 fprintf(fp, "To: %s\n", to);
487 if (replyto)
488 fprintf(fp, "Reply-To: %s\n", replyto);
489 fprintf(fp, "Subject: %s\n", subject);
490 fprintf(fp, "Date: %s\n", date);
491 fprintf(fp, "Message-ID: <%s@%s>\n", id, domain);
492 fprintf(fp, "List-Id: <%s.%s>\n", list, domain);
493 fputs("Auto-Submitted: auto-replied\n"
494 "MIME-Version: 1.0\n"
495 "Content-Type: text/plain; charset=utf-8\n\n", fp);
496 va_start(ap, fmt);
497 vfprintf(fp, fmt, ap);
498 va_end(ap);
499 if (fclose(fp) == EOF) {
500 free(buf);
501 return -1;
502 }
503 ret = deliver(&to, 1, buf, len);
504 free(buf);
505 return ret;
506}
507
508int
509parseaddr(const char *s, char *buf, size_t len)
510{
511 char bare[ADDRLEN];
512 size_t nb = 0, na = 0;
513 int depth = 0, quote = 0, angle = 0, seen = 0, gap = 0, bad = 0;
514 unsigned char c;
515
516 for (; *s; s++) {
517 c = *s;
518 if (quote || depth) {
519 if (c == '\\' && s[1])
520 s++;
521 else if (quote && c == '"')
522 quote = 0;
523 else if (depth && c == '(')
524 depth++;
525 else if (depth && c == ')')
526 depth--;
527 continue;
528 }
529 if (c == '(') {
530 depth = 1;
531 continue;
532 }
533 if (isspace(c)) {
534 gap = 1;
535 continue;
536 }
537 if (angle) {
538 if (c == '>') {
539 angle = 0;
540 seen = 1;
541 continue;
542 }
543 if (strchr("<\",;:", c) || na + 1 >= len)
544 return -1;
545 buf[na++] = c;
546 continue;
547 }
548 if (seen || strchr(">,;:", c))
549 return -1;
550 if (c == '<') {
551 angle = 1;
552 continue;
553 }
554 if (c == '"') {
555 quote = bad = 1;
556 continue;
557 }
558 if ((gap && nb) || nb + 1 >= sizeof(bare))
559 bad = 1;
560 else
561 bare[nb++] = c;
562 gap = 0;
563 }
564 if (quote || depth || angle)
565 return -1;
566 if (seen) {
567 buf[na] = '\0';
568 return 0;
569 }
570 if (bad || !nb || nb >= len)
571 return -1;
572 memcpy(buf, bare, nb);
573 buf[nb] = '\0';
574 return 0;
575}
576
577void
578post(void)
579{
580 char addr[ADDRLEN], owner[ADDRLEN], policy[32], v[HDRLEN];
581 char needle[512], path[PATH_MAX], **subs, *out = NULL;
582 size_t i, n, pos, end, len, outlen = 0;
583 FILE *fp;
584 int ok = 0;
585
586 if (!getheader("Message-ID", v, sizeof(v)))
587 die(EX_NOPERM, "missing Message-ID");
588 if (bprintf(needle, sizeof(needle), "<%s.%s>", list, domain) < 0)
589 die(EX_TEMPFAIL, "list name too long");
590 len = strlen(needle);
591 for (pos = 0; pos < hdrlen; pos = end) {
592 end = fieldend(pos);
593 if (!isfield(pos, "List-Id"))
594 continue;
595 for (i = pos; i + len <= end; i++)
596 if (!strncasecmp(msg + i, needle, len))
597 die(EX_NOPERM, "mail loop: already sent "
598 "through %s@%s", list, domain);
599 }
600 if (sender(addr, sizeof(addr)) < 0)
601 die(EX_NOPERM, "invalid or missing From address");
602 if (readline("post", policy, sizeof(policy)) < 0)
603 die(EX_TEMPFAIL, "cannot read post policy");
604 readline("owner", owner, sizeof(owner));
605 lower(owner);
606 if (loadsubs(&subs, &n) < 0)
607 die(EX_TEMPFAIL, "cannot read subscribers");
608 if (!strcmp(policy, "anyone") || !strcmp(addr, owner))
609 ok = 1;
610 else if (!strcmp(policy, "subscribers"))
611 for (i = 0; i < n && !ok; i++)
612 ok = !strcmp(subs[i], addr);
613 else if (strcmp(policy, "owner"))
614 die(EX_TEMPFAIL, "unknown post policy");
615 if (!ok && !strcmp(policy, "owner"))
616 die(EX_NOPERM, "only the owner may post to %s@%s", list,
617 domain);
618 if (!ok)
619 die(EX_NOPERM, "%s is not subscribed to %s@%s", addr, list,
620 domain);
621
622 if (!(fp = open_memstream(&out, &outlen)))
623 die(EX_TEMPFAIL, "out of memory");
624 fprintf(fp, "List-Id: <%s.%s>\n", list, domain);
625 if (strcmp(policy, "owner"))
626 fprintf(fp, "List-Post: <mailto:%s@%s>\n", list, domain);
627 else
628 fputs("List-Post: NO\n", fp);
629 fprintf(fp, "List-Subscribe: <mailto:%s+subscribe@%s>\n", list,
630 domain);
631 fprintf(fp, "List-Unsubscribe: <mailto:%s+unsubscribe@%s>\n", list,
632 domain);
633 fprintf(fp, "List-Help: <mailto:%s+help@%s>\n", list, domain);
634 if (ispublic())
635 fprintf(fp, "List-Archive: <%s%s/>\n", web, list);
636 fputs("Precedence: list\n", fp);
637 for (pos = 0; pos < hdrlen; pos = end) {
638 end = fieldend(pos);
639 if (!dropped(pos))
640 fwrite(msg + pos, 1, end - pos, fp);
641 }
642 fwrite(msg + hdrlen, 1, msglen - hdrlen, fp);
643 if (fclose(fp) == EOF)
644 die(EX_TEMPFAIL, "out of memory");
645
646 if (archive(out, outlen, path, sizeof(path)) < 0)
647 die(EX_TEMPFAIL, "cannot archive message");
648 if (deliver((const char *const *)subs, n, out, outlen) < 0) {
649 unlink(path);
650 die(EX_TEMPFAIL, "cannot deliver message");
651 }
652 syslog(LOG_INFO, "%s: post from %s to %zu subscribers", list, addr, n);
653}
654
655void
656prune(void)
657{
658 char path[PATH_MAX], act[16], addr[ADDRLEN];
659 long long t, now = time(NULL);
660 struct dirent *e;
661 struct stat st;
662 DIR *dp;
663 int old, r;
664
665 if (bprintf(path, sizeof(path), "%s/pending", dir) < 0
666 || !(dp = opendir(path)))
667 return;
668 while ((e = readdir(dp))) {
669 if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")
670 || bprintf(path, sizeof(path), "%s/pending/%s", dir,
671 e->d_name) < 0)
672 continue;
673 t = 0;
674 if (e->d_name[0] == '.')
675 old = !stat(path, &st) && st.st_mtime + expiry < now;
676 else if ((r = readpending(path, act, addr, &t)) < 0)
677 old = 0;
678 else
679 old = !r || t + expiry < now;
680 if (old)
681 unlink(path);
682 }
683 closedir(dp);
684}
685
686int
687readline(const char *file, char *buf, size_t len)
688{
689 char path[PATH_MAX];
690 FILE *fp;
691 size_t n;
692
693 buf[0] = '\0';
694 if (bprintf(path, sizeof(path), "%s/%s", dir, file) < 0
695 || !(fp = fopen(path, "r")))
696 return -1;
697 if (!fgets(buf, (int)len, fp))
698 buf[0] = '\0';
699 fclose(fp);
700 n = strcspn(buf, "\r\n");
701 while (n && (buf[n - 1] == ' ' || buf[n - 1] == '\t'))
702 n--;
703 buf[n] = '\0';
704 return 0;
705}
706
707void
708readmsg(void)
709{
710 size_t cap = 0, pos;
711 ssize_t r;
712 char *p;
713
714 for (;;) {
715 if (msglen == cap) {
716 if (cap > (size_t)maxsize)
717 die(EX_NOPERM, "message larger than %ld bytes",
718 maxsize);
719 cap = cap ? cap * 2 : 65536;
720 if (cap > (size_t)maxsize + 1)
721 cap = maxsize + 1;
722 if (!(p = realloc(msg, cap + 1)))
723 die(EX_TEMPFAIL, "out of memory");
724 msg = p;
725 }
726 if ((r = read(0, msg + msglen, cap - msglen)) < 0) {
727 if (errno == EINTR)
728 continue;
729 die(EX_TEMPFAIL, "cannot read message");
730 }
731 if (!r)
732 break;
733 msglen += r;
734 }
735 msg[msglen] = '\0';
736 for (pos = 0; pos < msglen; pos = p ? (size_t)(p - msg) + 1 : msglen) {
737 if (msg[pos] == '\n'
738 || (msg[pos] == '\r' && msg[pos + 1] == '\n'))
739 break;
740 p = memchr(msg + pos, '\n', msglen - pos);
741 }
742 hdrlen = pos;
743}
744
745int
746readpending(const char *path, char *act, char *addr, long long *t)
747{
748 char line[512];
749 FILE *fp;
750 int ok;
751
752 if (!(fp = fopen(path, "r")))
753 return -1;
754 ok = fgets(line, sizeof(line), fp)
755 && sscanf(line, "%15s %255s %lld", act, addr, t) == 3;
756 fclose(fp);
757 return ok && (!strcmp(act, "subscribe") || !strcmp(act, "unsubscribe"))
758 && addrok(addr);
759}
760
761void
762request(const char *act)
763{
764 char addr[ADDRLEN], to[ADDRLEN], mode[32], tok[TOKLEN + 1];
765 char d[PATH_MAX], tmp[PATH_MAX], path[PATH_MAX];
766 char reply[ADDRLEN + 64], sub[2 * ADDRLEN + 64];
767 int add = !strcmp(act, "subscribe"), approve = 0, fd, ok, r;
768
769 if (sender(addr, sizeof(addr)) < 0)
770 die(EX_NOPERM, "invalid or missing From address");
771 if (add) {
772 if (readline("subscribe", mode, sizeof(mode)) < 0)
773 die(EX_TEMPFAIL, "cannot read subscribe policy");
774 if (!strcmp(mode, "closed"))
775 approve = 1;
776 else if (strcmp(mode, "open"))
777 die(EX_TEMPFAIL, "unknown subscribe policy");
778 }
779 strcpy(to, addr);
780 if (approve) {
781 readline("owner", to, sizeof(to));
782 lower(to);
783 if (!addrok(to))
784 die(EX_TEMPFAIL, "invalid list owner");
785 }
786 if (newtoken(tok) < 0
787 || bprintf(d, sizeof(d), "%s/pending", dir) < 0
788 || (mkdir(d, 0750) < 0 && errno != EEXIST)
789 || bprintf(tmp, sizeof(tmp), "%s/.%s", d, tok) < 0
790 || bprintf(path, sizeof(path), "%s/%s", d, tok) < 0
791 || bprintf(reply, sizeof(reply), "%s+confirm-%s@%s", list, tok,
792 domain) < 0)
793 die(EX_TEMPFAIL, "cannot create request");
794 fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0640);
795 ok = fd >= 0 && dprintf(fd, "%s %s %lld\n", act, addr,
796 (long long)time(NULL)) >= 0 && !fsync(fd);
797 if (fd >= 0 && close(fd) < 0)
798 ok = 0;
799 if (!ok || rename(tmp, path) < 0) {
800 unlink(tmp);
801 die(EX_TEMPFAIL, "cannot write request");
802 }
803
804 if (approve) {
805 r = bprintf(sub, sizeof(sub), "Approve subscription of %s to "
806 "%s@%s", addr, list, domain);
807 if (!r)
808 r = notify(to, reply, sub,
809 "%s asked to subscribe to %s@%s.\n\n"
810 "To approve, reply to this message or send "
811 "an empty message to\n%s.\n"
812 "The request expires in %ld days; ignore "
813 "this message to decline.\n",
814 addr, list, domain, reply, expiry / 86400);
815 } else {
816 r = bprintf(sub, sizeof(sub), "Confirm %s %s %s@%s",
817 add ? "subscription" : "unsubscription",
818 add ? "to" : "from", list, domain);
819 if (!r)
820 r = notify(to, reply, sub,
821 "Someone, hopefully you, asked to %s %s %s "
822 "%s@%s.\n\n"
823 "To confirm, reply to this message or send "
824 "an empty message to\n%s.\n"
825 "The request expires in %ld days; ignore "
826 "this message otherwise.\n",
827 act, addr, add ? "to" : "from", list,
828 domain, reply, expiry / 86400);
829 }
830 if (r < 0) {
831 unlink(path);
832 die(EX_TEMPFAIL, "cannot send confirmation");
833 }
834 syslog(LOG_INFO, "%s: %s request for %s", list, act, addr);
835}
836
837int
838sender(char *buf, size_t len)
839{
840 char v[HDRLEN];
841
842 if (getheader("From", v, sizeof(v)) != 1
843 || parseaddr(v, buf, len) < 0)
844 return -1;
845 lower(buf);
846 return addrok(buf) ? 0 : -1;
847}
848
849int
850setsub(const char *addr, int add)
851{
852 char lock[PATH_MAX], file[PATH_MAX], tmp[PATH_MAX], *line = NULL;
853 size_t cap = 0;
854 struct flock fl;
855 FILE *in = NULL, *out = NULL;
856 int lfd = -1, fd, found = 0, ret = -1;
857
858 if (bprintf(lock, sizeof(lock), "%s/.lock", dir) < 0
859 || bprintf(file, sizeof(file), "%s/subscribers", dir) < 0
860 || bprintf(tmp, sizeof(tmp), "%s/.subscribers", dir) < 0
861 || (lfd = open(lock, O_RDWR | O_CREAT, 0640)) < 0)
862 return -1;
863 memset(&fl, 0, sizeof(fl));
864 fl.l_type = F_WRLCK;
865 fl.l_whence = SEEK_SET;
866 while (fcntl(lfd, F_SETLKW, &fl) < 0)
867 if (errno != EINTR)
868 goto end;
869 if (!(in = fopen(file, "r")) && errno != ENOENT)
870 goto end;
871 if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0640)) < 0)
872 goto end;
873 if (!(out = fdopen(fd, "w"))) {
874 close(fd);
875 goto end;
876 }
877 while (in && getline(&line, &cap, in) > 0) {
878 line[strcspn(line, "\r\n")] = '\0';
879 if (!*line)
880 continue;
881 if (!strcmp(line, addr)) {
882 if (!add || found)
883 continue;
884 found = 1;
885 }
886 fprintf(out, "%s\n", line);
887 }
888 if (in && ferror(in))
889 goto end;
890 if (add && !found)
891 fprintf(out, "%s\n", addr);
892 if (fflush(out) == EOF || fsync(fileno(out)) < 0)
893 goto end;
894 fd = fclose(out);
895 out = NULL;
896 if (fd == EOF || rename(tmp, file) < 0)
897 goto end;
898 ret = 0;
899end:
900 if (out)
901 fclose(out);
902 if (ret < 0)
903 unlink(tmp);
904 if (in)
905 fclose(in);
906 free(line);
907 close(lfd);
908 return ret;
909}
910
911void
912usage(void)
913{
914 fputs("usage: pollen list [extension]\n", stderr);
915 exit(EX_USAGE);
916}
917
918int
919validlist(const char *s)
920{
921 size_t n;
922
923 for (n = 0; s[n]; n++)
924 if (!islower((unsigned char)s[n])
925 && !isdigit((unsigned char)s[n]) && !(n && s[n] == '-'))
926 return 0;
927 return n && n <= 64;
928}
929
930int
931validtoken(const char *s)
932{
933 return strlen(s) == TOKLEN && strspn(s, "0123456789abcdef") == TOKLEN;
934}
935
936
937/* main */
938int
939main(int argc, char *argv[])
940{
941 char ext[256];
942 struct stat st;
943
944 signal(SIGPIPE, SIG_IGN);
945 openlog("pollen", LOG_PID, LOG_MAIL);
946 if (argc < 2 || argc > 3)
947 usage();
948 lower(argv[1]);
949 list = argv[1];
950 if (!validlist(list)
951 || bprintf(dir, sizeof(dir), "%s/%s", root, list) < 0)
952 die(EX_NOUSER, "no such list");
953 if (stat(dir, &st) < 0) {
954 if (errno == ENOENT || errno == ENOTDIR)
955 die(EX_NOUSER, "no such list");
956 die(EX_TEMPFAIL, "cannot access list");
957 }
958 if (!S_ISDIR(st.st_mode))
959 die(EX_NOUSER, "no such list");
960 if (bprintf(ext, sizeof(ext), "%s", argc > 2 ? argv[2] : "") < 0)
961 die(EX_NOPERM, "invalid address extension");
962 lower(ext);
963
964 readmsg();
965 prune();
966 if (!*ext)
967 post();
968 else if (!strcmp(ext, "bounces") || !strncmp(ext, "bounces+", 8))
969 bounce(ext);
970 else if (automated())
971 syslog(LOG_INFO, "%s: ignoring automated message to +%s", list,
972 ext);
973 else if (!strcmp(ext, "subscribe") || !strcmp(ext, "unsubscribe"))
974 request(ext);
975 else if (!strncmp(ext, "confirm-", 8))
976 confirm(ext + 8);
977 else
978 help();
979 return 0;
980}