greenhouse / pollen / pollen.c982 lines · 23887B · 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 || isfield(pos, "Precedence");
310}
311
312size_t
313fieldend(size_t pos)
314{
315 const char *p;
316
317 while ((p = memchr(msg + pos, '\n', hdrlen - pos))) {
318 pos = p - msg + 1;
319 if (pos == hdrlen || (msg[pos] != ' ' && msg[pos] != '\t'))
320 return pos;
321 }
322 return hdrlen;
323}
324
325int
326getheader(const char *name, char *buf, size_t len)
327{
328 size_t pos, end, i, n = 0;
329 const char *p;
330 int found = 0;
331
332 buf[0] = '\0';
333 for (pos = 0; pos < hdrlen; pos = end) {
334 end = fieldend(pos);
335 if (!isfield(pos, name) || found++)
336 continue;
337 p = (const char *)memchr(msg + pos, ':', end - pos) + 1;
338 for (; p < msg + end; p++) {
339 if (*p == '\r' || *p == '\n')
340 continue;
341 if (!*p || n + 1 >= len)
342 return -1;
343 buf[n++] = *p;
344 }
345 while (n && (buf[n - 1] == ' ' || buf[n - 1] == '\t'))
346 n--;
347 buf[n] = '\0';
348 for (i = 0; buf[i] == ' ' || buf[i] == '\t'; i++)
349 ;
350 memmove(buf, buf + i, n - i + 1);
351 }
352 return found;
353}
354
355void
356help(void)
357{
358 char addr[ADDRLEN], desc[256], sub[ADDRLEN + 64], arch[512] = "";
359
360 if (sender(addr, sizeof(addr)) < 0)
361 die(EX_NOPERM, "invalid or missing From address");
362 readline("description", desc, sizeof(desc));
363 if (bprintf(sub, sizeof(sub), "Help for %s@%s", list, domain) < 0
364 || (ispublic() && bprintf(arch, sizeof(arch), "archive %s%s/\n",
365 web, list) < 0))
366 die(EX_TEMPFAIL, "list name too long");
367 if (notify(addr, NULL, sub,
368 "%s@%s: %s\n\n"
369 "post %s@%s\n"
370 "subscribe %s+subscribe@%s\n"
371 "unsubscribe %s+unsubscribe@%s\n"
372 "help %s+help@%s\n"
373 "%s",
374 list, domain, desc, list, domain, list, domain,
375 list, domain, list, domain, arch) < 0)
376 die(EX_TEMPFAIL, "cannot send help");
377}
378
379int
380isfield(size_t pos, const char *name)
381{
382 size_t n = strlen(name);
383
384 if (hdrlen - pos < n || strncasecmp(msg + pos, name, n))
385 return 0;
386 for (pos += n; pos < hdrlen && (msg[pos] == ' ' || msg[pos] == '\t');
387 pos++)
388 ;
389 return pos < hdrlen && msg[pos] == ':';
390}
391
392int
393ispublic(void)
394{
395 char path[PATH_MAX];
396 struct stat st;
397
398 return *web && bprintf(path, sizeof(path), "%s/public", dir) == 0
399 && stat(path, &st) == 0;
400}
401
402int
403loadsubs(char ***subs, size_t *n)
404{
405 char path[PATH_MAX], *line = NULL, **v;
406 size_t cap = 0, max = 0;
407 FILE *fp;
408 int ret = -1;
409
410 *subs = NULL;
411 *n = 0;
412 if (bprintf(path, sizeof(path), "%s/subscribers", dir) < 0)
413 return -1;
414 if (!(fp = fopen(path, "r")))
415 return errno == ENOENT ? 0 : -1;
416 while (getline(&line, &cap, fp) > 0) {
417 line[strcspn(line, "\r\n")] = '\0';
418 lower(line);
419 if (!*line)
420 continue;
421 if (!addrok(line)) {
422 syslog(LOG_WARNING, "%s: skipping invalid subscriber",
423 list);
424 continue;
425 }
426 if (*n == max) {
427 max = max ? max * 2 : 64;
428 if (!(v = realloc(*subs, max * sizeof(*v))))
429 goto end;
430 *subs = v;
431 }
432 if (!((*subs)[*n] = strdup(line)))
433 goto end;
434 (*n)++;
435 }
436 if (!ferror(fp))
437 ret = 0;
438end:
439 free(line);
440 fclose(fp);
441 return ret;
442}
443
444void
445lower(char *s)
446{
447 for (; *s; s++)
448 *s = tolower((unsigned char)*s);
449}
450
451int
452newtoken(char *tok)
453{
454 unsigned char b[TOKLEN / 2];
455 FILE *fp;
456 size_t i;
457 int ok;
458
459 if (!(fp = fopen("/dev/urandom", "r")))
460 return -1;
461 ok = fread(b, 1, sizeof(b), fp) == sizeof(b);
462 fclose(fp);
463 if (!ok)
464 return -1;
465 for (i = 0; i < sizeof(b); i++)
466 sprintf(tok + 2 * i, "%02x", b[i]);
467 return 0;
468}
469
470int
471notify(const char *to, const char *replyto, const char *subject,
472 const char *fmt, ...)
473{
474 char date[64], id[TOKLEN + 1], *buf = NULL;
475 size_t len = 0;
476 time_t t = time(NULL);
477 va_list ap;
478 FILE *fp;
479 int ret;
480
481 if (newtoken(id) < 0
482 || !strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S +0000",
483 gmtime(&t))
484 || !(fp = open_memstream(&buf, &len)))
485 return -1;
486 fprintf(fp, "From: %s <%s+help@%s>\n", list, list, domain);
487 fprintf(fp, "To: %s\n", to);
488 if (replyto)
489 fprintf(fp, "Reply-To: %s\n", replyto);
490 fprintf(fp, "Subject: %s\n", subject);
491 fprintf(fp, "Date: %s\n", date);
492 fprintf(fp, "Message-ID: <%s@%s>\n", id, domain);
493 fprintf(fp, "List-Id: <%s.%s>\n", list, domain);
494 fputs("Auto-Submitted: auto-replied\n"
495 "MIME-Version: 1.0\n"
496 "Content-Type: text/plain; charset=utf-8\n\n", fp);
497 va_start(ap, fmt);
498 vfprintf(fp, fmt, ap);
499 va_end(ap);
500 if (fclose(fp) == EOF) {
501 free(buf);
502 return -1;
503 }
504 ret = deliver(&to, 1, buf, len);
505 free(buf);
506 return ret;
507}
508
509int
510parseaddr(const char *s, char *buf, size_t len)
511{
512 char bare[ADDRLEN];
513 size_t nb = 0, na = 0;
514 int depth = 0, quote = 0, angle = 0, seen = 0, gap = 0, bad = 0;
515 unsigned char c;
516
517 for (; *s; s++) {
518 c = *s;
519 if (quote || depth) {
520 if (c == '\\' && s[1])
521 s++;
522 else if (quote && c == '"')
523 quote = 0;
524 else if (depth && c == '(')
525 depth++;
526 else if (depth && c == ')')
527 depth--;
528 continue;
529 }
530 if (c == '(') {
531 depth = 1;
532 continue;
533 }
534 if (isspace(c)) {
535 gap = 1;
536 continue;
537 }
538 if (angle) {
539 if (c == '>') {
540 angle = 0;
541 seen = 1;
542 continue;
543 }
544 if (strchr("<\",;:", c) || na + 1 >= len)
545 return -1;
546 buf[na++] = c;
547 continue;
548 }
549 if (seen || strchr(">,;:", c))
550 return -1;
551 if (c == '<') {
552 angle = 1;
553 continue;
554 }
555 if (c == '"') {
556 quote = bad = 1;
557 continue;
558 }
559 if ((gap && nb) || nb + 1 >= sizeof(bare))
560 bad = 1;
561 else
562 bare[nb++] = c;
563 gap = 0;
564 }
565 if (quote || depth || angle)
566 return -1;
567 if (seen) {
568 buf[na] = '\0';
569 return 0;
570 }
571 if (bad || !nb || nb >= len)
572 return -1;
573 memcpy(buf, bare, nb);
574 buf[nb] = '\0';
575 return 0;
576}
577
578void
579post(void)
580{
581 char addr[ADDRLEN], owner[ADDRLEN], policy[32], v[HDRLEN];
582 char needle[512], path[PATH_MAX], **subs, *out = NULL;
583 size_t i, n, pos, end, len, outlen = 0;
584 FILE *fp;
585 int ok = 0;
586
587 if (!getheader("Message-ID", v, sizeof(v)))
588 die(EX_NOPERM, "missing Message-ID");
589 if (bprintf(needle, sizeof(needle), "<%s.%s>", list, domain) < 0)
590 die(EX_TEMPFAIL, "list name too long");
591 len = strlen(needle);
592 for (pos = 0; pos < hdrlen; pos = end) {
593 end = fieldend(pos);
594 if (!isfield(pos, "List-Id"))
595 continue;
596 for (i = pos; i + len <= end; i++)
597 if (!strncasecmp(msg + i, needle, len))
598 die(EX_NOPERM, "mail loop: already sent "
599 "through %s@%s", list, domain);
600 }
601 if (sender(addr, sizeof(addr)) < 0)
602 die(EX_NOPERM, "invalid or missing From address");
603 if (readline("post", policy, sizeof(policy)) < 0)
604 die(EX_TEMPFAIL, "cannot read post policy");
605 readline("owner", owner, sizeof(owner));
606 lower(owner);
607 if (loadsubs(&subs, &n) < 0)
608 die(EX_TEMPFAIL, "cannot read subscribers");
609 if (!strcmp(policy, "anyone") || !strcmp(addr, owner))
610 ok = 1;
611 else if (!strcmp(policy, "subscribers"))
612 for (i = 0; i < n && !ok; i++)
613 ok = !strcmp(subs[i], addr);
614 else if (strcmp(policy, "owner"))
615 die(EX_TEMPFAIL, "unknown post policy");
616 if (!ok && !strcmp(policy, "owner"))
617 die(EX_NOPERM, "only the owner may post to %s@%s", list,
618 domain);
619 if (!ok)
620 die(EX_NOPERM, "%s is not subscribed to %s@%s", addr, list,
621 domain);
622
623 if (!(fp = open_memstream(&out, &outlen)))
624 die(EX_TEMPFAIL, "out of memory");
625 fprintf(fp, "List-Id: <%s.%s>\n", list, domain);
626 if (strcmp(policy, "owner"))
627 fprintf(fp, "List-Post: <mailto:%s@%s>\n", list, domain);
628 else
629 fputs("List-Post: NO\n", fp);
630 fprintf(fp, "List-Subscribe: <mailto:%s+subscribe@%s>\n", list,
631 domain);
632 fprintf(fp, "List-Unsubscribe: <mailto:%s+unsubscribe@%s>\n", list,
633 domain);
634 fprintf(fp, "List-Help: <mailto:%s+help@%s>\n", list, domain);
635 if (ispublic())
636 fprintf(fp, "List-Archive: <%s%s/>\n", web, list);
637 fputs("Precedence: list\n", fp);
638 for (pos = 0; pos < hdrlen; pos = end) {
639 end = fieldend(pos);
640 if (!dropped(pos))
641 fwrite(msg + pos, 1, end - pos, fp);
642 }
643 fwrite(msg + hdrlen, 1, msglen - hdrlen, fp);
644 if (fclose(fp) == EOF)
645 die(EX_TEMPFAIL, "out of memory");
646
647 if (archive(out, outlen, path, sizeof(path)) < 0)
648 die(EX_TEMPFAIL, "cannot archive message");
649 if (deliver((const char *const *)subs, n, out, outlen) < 0) {
650 unlink(path);
651 die(EX_TEMPFAIL, "cannot deliver message");
652 }
653 syslog(LOG_INFO, "%s: post from %s to %zu subscribers", list, addr, n);
654}
655
656void
657prune(void)
658{
659 char path[PATH_MAX], act[16], addr[ADDRLEN];
660 long long t, now = time(NULL);
661 struct dirent *e;
662 struct stat st;
663 DIR *dp;
664 int old, r;
665
666 if (bprintf(path, sizeof(path), "%s/pending", dir) < 0
667 || !(dp = opendir(path)))
668 return;
669 while ((e = readdir(dp))) {
670 if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")
671 || bprintf(path, sizeof(path), "%s/pending/%s", dir,
672 e->d_name) < 0)
673 continue;
674 t = 0;
675 if (e->d_name[0] == '.')
676 old = !stat(path, &st) && st.st_mtime + expiry < now;
677 else if ((r = readpending(path, act, addr, &t)) < 0)
678 old = 0;
679 else
680 old = !r || t + expiry < now;
681 if (old)
682 unlink(path);
683 }
684 closedir(dp);
685}
686
687int
688readline(const char *file, char *buf, size_t len)
689{
690 char path[PATH_MAX];
691 FILE *fp;
692 size_t n;
693
694 buf[0] = '\0';
695 if (bprintf(path, sizeof(path), "%s/%s", dir, file) < 0
696 || !(fp = fopen(path, "r")))
697 return -1;
698 if (!fgets(buf, (int)len, fp))
699 buf[0] = '\0';
700 fclose(fp);
701 n = strcspn(buf, "\r\n");
702 while (n && (buf[n - 1] == ' ' || buf[n - 1] == '\t'))
703 n--;
704 buf[n] = '\0';
705 return 0;
706}
707
708void
709readmsg(void)
710{
711 size_t cap = 0, pos;
712 ssize_t r;
713 char *p;
714
715 for (;;) {
716 if (msglen == cap) {
717 if (cap > (size_t)maxsize)
718 die(EX_NOPERM, "message larger than %ld bytes",
719 maxsize);
720 cap = cap ? cap * 2 : 65536;
721 if (cap > (size_t)maxsize + 1)
722 cap = maxsize + 1;
723 if (!(p = realloc(msg, cap + 1)))
724 die(EX_TEMPFAIL, "out of memory");
725 msg = p;
726 }
727 if ((r = read(0, msg + msglen, cap - msglen)) < 0) {
728 if (errno == EINTR)
729 continue;
730 die(EX_TEMPFAIL, "cannot read message");
731 }
732 if (!r)
733 break;
734 msglen += r;
735 }
736 msg[msglen] = '\0';
737 for (pos = 0; pos < msglen; pos = p ? (size_t)(p - msg) + 1 : msglen) {
738 if (msg[pos] == '\n'
739 || (msg[pos] == '\r' && msg[pos + 1] == '\n'))
740 break;
741 p = memchr(msg + pos, '\n', msglen - pos);
742 }
743 hdrlen = pos;
744}
745
746int
747readpending(const char *path, char *act, char *addr, long long *t)
748{
749 char line[512];
750 FILE *fp;
751 int ok;
752
753 if (!(fp = fopen(path, "r")))
754 return -1;
755 ok = fgets(line, sizeof(line), fp)
756 && sscanf(line, "%15s %255s %lld", act, addr, t) == 3;
757 fclose(fp);
758 return ok && (!strcmp(act, "subscribe") || !strcmp(act, "unsubscribe"))
759 && addrok(addr);
760}
761
762void
763request(const char *act)
764{
765 char addr[ADDRLEN], to[ADDRLEN], mode[32], tok[TOKLEN + 1];
766 char d[PATH_MAX], tmp[PATH_MAX], path[PATH_MAX];
767 char reply[ADDRLEN + 64], sub[2 * ADDRLEN + 64];
768 int add = !strcmp(act, "subscribe"), approve = 0, fd, ok, r;
769
770 if (sender(addr, sizeof(addr)) < 0)
771 die(EX_NOPERM, "invalid or missing From address");
772 if (add) {
773 if (readline("subscribe", mode, sizeof(mode)) < 0)
774 die(EX_TEMPFAIL, "cannot read subscribe policy");
775 if (!strcmp(mode, "closed"))
776 approve = 1;
777 else if (strcmp(mode, "open"))
778 die(EX_TEMPFAIL, "unknown subscribe policy");
779 }
780 strcpy(to, addr);
781 if (approve) {
782 readline("owner", to, sizeof(to));
783 lower(to);
784 if (!addrok(to))
785 die(EX_TEMPFAIL, "invalid list owner");
786 }
787 if (newtoken(tok) < 0
788 || bprintf(d, sizeof(d), "%s/pending", dir) < 0
789 || (mkdir(d, 0700) < 0 && errno != EEXIST)
790 || bprintf(tmp, sizeof(tmp), "%s/.%s", d, tok) < 0
791 || bprintf(path, sizeof(path), "%s/%s", d, tok) < 0
792 || bprintf(reply, sizeof(reply), "%s+confirm-%s@%s", list, tok,
793 domain) < 0)
794 die(EX_TEMPFAIL, "cannot create request");
795 fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
796 ok = fd >= 0 && dprintf(fd, "%s %s %lld\n", act, addr,
797 (long long)time(NULL)) >= 0 && !fsync(fd);
798 if (fd >= 0 && close(fd) < 0)
799 ok = 0;
800 if (!ok || rename(tmp, path) < 0) {
801 unlink(tmp);
802 die(EX_TEMPFAIL, "cannot write request");
803 }
804
805 if (approve) {
806 r = bprintf(sub, sizeof(sub), "Approve subscription of %s to "
807 "%s@%s", addr, list, domain);
808 if (!r)
809 r = notify(to, reply, sub,
810 "%s asked to subscribe to %s@%s.\n\n"
811 "To approve, reply to this message or send "
812 "an empty message to\n%s.\n"
813 "The request expires in %ld days; ignore "
814 "this message to decline.\n",
815 addr, list, domain, reply, expiry / 86400);
816 } else {
817 r = bprintf(sub, sizeof(sub), "Confirm %s %s %s@%s",
818 add ? "subscription" : "unsubscription",
819 add ? "to" : "from", list, domain);
820 if (!r)
821 r = notify(to, reply, sub,
822 "Someone, hopefully you, asked to %s %s %s "
823 "%s@%s.\n\n"
824 "To confirm, reply to this message or send "
825 "an empty message to\n%s.\n"
826 "The request expires in %ld days; ignore "
827 "this message otherwise.\n",
828 act, addr, add ? "to" : "from", list,
829 domain, reply, expiry / 86400);
830 }
831 if (r < 0) {
832 unlink(path);
833 die(EX_TEMPFAIL, "cannot send confirmation");
834 }
835 syslog(LOG_INFO, "%s: %s request for %s", list, act, addr);
836}
837
838int
839sender(char *buf, size_t len)
840{
841 char v[HDRLEN];
842
843 if (getheader("From", v, sizeof(v)) != 1
844 || parseaddr(v, buf, len) < 0)
845 return -1;
846 lower(buf);
847 return addrok(buf) ? 0 : -1;
848}
849
850int
851setsub(const char *addr, int add)
852{
853 char lock[PATH_MAX], file[PATH_MAX], tmp[PATH_MAX], *line = NULL;
854 size_t cap = 0;
855 struct flock fl;
856 FILE *in = NULL, *out = NULL;
857 int lfd = -1, fd, found = 0, ret = -1;
858
859 if (bprintf(lock, sizeof(lock), "%s/.lock", dir) < 0
860 || bprintf(file, sizeof(file), "%s/subscribers", dir) < 0
861 || bprintf(tmp, sizeof(tmp), "%s/.subscribers", dir) < 0
862 || (lfd = open(lock, O_RDWR | O_CREAT, 0600)) < 0)
863 return -1;
864 memset(&fl, 0, sizeof(fl));
865 fl.l_type = F_WRLCK;
866 fl.l_whence = SEEK_SET;
867 while (fcntl(lfd, F_SETLKW, &fl) < 0)
868 if (errno != EINTR)
869 goto end;
870 if (!(in = fopen(file, "r")) && errno != ENOENT)
871 goto end;
872 if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0600)) < 0)
873 goto end;
874 if (!(out = fdopen(fd, "w"))) {
875 close(fd);
876 goto end;
877 }
878 while (in && getline(&line, &cap, in) > 0) {
879 line[strcspn(line, "\r\n")] = '\0';
880 lower(line);
881 if (!*line)
882 continue;
883 if (!strcmp(line, addr)) {
884 if (!add || found)
885 continue;
886 found = 1;
887 }
888 fprintf(out, "%s\n", line);
889 }
890 if (in && ferror(in))
891 goto end;
892 if (add && !found)
893 fprintf(out, "%s\n", addr);
894 if (fflush(out) == EOF || fsync(fileno(out)) < 0)
895 goto end;
896 fd = fclose(out);
897 out = NULL;
898 if (fd == EOF || rename(tmp, file) < 0)
899 goto end;
900 ret = 0;
901end:
902 if (out)
903 fclose(out);
904 if (ret < 0)
905 unlink(tmp);
906 if (in)
907 fclose(in);
908 free(line);
909 close(lfd);
910 return ret;
911}
912
913void
914usage(void)
915{
916 fputs("usage: pollen list [extension]\n", stderr);
917 exit(EX_USAGE);
918}
919
920int
921validlist(const char *s)
922{
923 size_t n;
924
925 for (n = 0; s[n]; n++)
926 if (!islower((unsigned char)s[n])
927 && !isdigit((unsigned char)s[n]) && !(n && s[n] == '-'))
928 return 0;
929 return n && n <= 64;
930}
931
932int
933validtoken(const char *s)
934{
935 return strlen(s) == TOKLEN && strspn(s, "0123456789abcdef") == TOKLEN;
936}
937
938/* main */
939int
940main(int argc, char *argv[])
941{
942 char ext[256];
943 struct stat st;
944
945 umask(027);
946 signal(SIGPIPE, SIG_IGN);
947 openlog("pollen", LOG_PID, LOG_MAIL);
948 if (argc < 2 || argc > 3)
949 usage();
950 lower(argv[1]);
951 list = argv[1];
952 if (!validlist(list)
953 || bprintf(dir, sizeof(dir), "%s/%s", root, list) < 0)
954 die(EX_NOUSER, "no such list");
955 if (stat(dir, &st) < 0) {
956 if (errno == ENOENT || errno == ENOTDIR)
957 die(EX_NOUSER, "no such list");
958 die(EX_TEMPFAIL, "cannot access list");
959 }
960 if (!S_ISDIR(st.st_mode))
961 die(EX_NOUSER, "no such list");
962 if (bprintf(ext, sizeof(ext), "%s", argc > 2 ? argv[2] : "") < 0)
963 die(EX_NOPERM, "invalid address extension");
964 lower(ext);
965
966 readmsg();
967 prune();
968 if (!*ext)
969 post();
970 else if (!strcmp(ext, "bounces") || !strncmp(ext, "bounces+", 8))
971 bounce(ext);
972 else if (automated())
973 syslog(LOG_INFO, "%s: ignoring automated message to +%s", list,
974 ext);
975 else if (!strcmp(ext, "subscribe") || !strcmp(ext, "unsubscribe"))
976 request(ext);
977 else if (!strncmp(ext, "confirm-", 8))
978 confirm(ext + 8);
979 else
980 help();
981 return 0;
982}