greenhouse

greenhouse

minimal software forge

git clone git@git.lostduk.com:greenhouse.git

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}