greenhouse

greenhouse

minimal software forge

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

greenhouse / pollen / pollen.c980 lines · 23814B · 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+help@%s\n", 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}