greenhouse

greenhouse

minimal software forge

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

feat(pollen): init

commit8002f5e133a2d5a6bcec54e80cdb67a94ea0963b
parentc2eaba29b18c2a3ce95da933debc3d6511bc9aaf
authorlostduk <hi@lostduk.com>
date2026-10-17 17:16 +0700
.gitignore12+
hedge/hedge.c1+
pollen/Makefile34++
pollen/config.def.h6+
pollen/config.mk8+
pollen/pollen.1194++++++++
pollen/pollen.c980++++++++++++++++++++++++++++++++++++++++

7 files changed, -6/+1229

.gitignore

@@ -1,7 +1,7 @@ .dev/-hedge/hedge-hedge/*.o-hedge/config.h-petal/petal-petal/*.o-petal/config.h+test.sh+*/.o+*/config.h+*/hedge+*/petal+*/pollen

hedge/hedge.c

@@ -137,6 +137,7 @@ validname(const char *s) 	return !start; } +/* main */ int main(int argc, char *argv[]) {

pollen/Makefile

@@ -0,0 +1,34 @@+include config.mk++SRC = pollen.c+OBJ = ${SRC:.c=.o}++all: pollen++.c.o:+	${CC} -c ${CFLAGS} $<++${OBJ}: config.h config.mk++config.h:+	cp config.def.h $@++pollen: ${OBJ}+	${CC} -o $@ ${OBJ} ${LDFLAGS}++clean:+	rm -f pollen ${OBJ}++install: all+	mkdir -p ${DESTDIR}${PREFIX}/bin+	cp -f pollen ${DESTDIR}${PREFIX}/bin+	chmod 755 ${DESTDIR}${PREFIX}/bin/pollen+	mkdir -p ${DESTDIR}${MANPREFIX}/man1+	cp -f pollen.1 ${DESTDIR}${MANPREFIX}/man1+	chmod 644 ${DESTDIR}${MANPREFIX}/man1/pollen.1++uninstall:+	rm -f ${DESTDIR}${PREFIX}/bin/pollen\+		${DESTDIR}${MANPREFIX}/man1/pollen.1++.PHONY: all clean install uninstall

pollen/config.def.h

@@ -0,0 +1,6 @@+static const char *root     = "/srv/lists";+static const char *domain   = "lists.lostduk.com";+static const char *web      = "https://lists.lostduk.com/";+static const char *sendmail = "/usr/sbin/sendmail";+static const long maxsize   = 10 * 1024 * 1024;+static const long expiry    = 7 * 24 * 3600;

pollen/config.mk

@@ -0,0 +1,8 @@+PREFIX    = /usr/local+MANPREFIX = ${PREFIX}/share/man++CPPFLAGS = -D_XOPEN_SOURCE=700+CFLAGS   = -std=c99 -pedantic -Wall -Wextra -Os ${CPPFLAGS}+LDFLAGS  =++CC = cc

pollen/pollen.1

@@ -0,0 +1,194 @@+.TH POLLEN 1 pollen+.SH NAME+pollen \- mailing list manager+.SH SYNOPSIS+.B pollen+.I list+.RI [ extension ]+.SH DESCRIPTION+.B pollen+delivers one message, read from standard input, to the mailing list+.IR list .+It is meant to run as a Postfix+.BR pipe (8)+transport.+The envelope sender is taken from the+.B Return-Path+header that Postfix prepends.+.P+Without an+.IR extension ,+the message is a post.+It is rejected if it has no+.BR Message-ID ,+if it already carries this list's+.BR List-Id ,+if it is larger than+.BR maxsize ,+or if the address in its single+.B From+header may not post.+Otherwise the+.BR List-Id ,+.BR List-Post ,+.BR List-Subscribe ,+.BR List-Unsubscribe ,+.B List-Help+and+.B Precedence+headers are prepended, plus+.B List-Archive+if+.B web+is set and the list is+.BR public ,+incoming+.BR List-* ,+.B Return-Path+and+.B Delivered-To+headers are dropped and everything else is kept byte for byte.+The result is stored in the archive and then handed to+.B sendmail+in a single call for all subscribers, with+.IB list +bounces@ domain+as envelope sender.+If that call fails, the archived copy is removed again so Postfix can+retry cleanly.+.SH COMMANDS+Commands are addressed as+.IB list + command @ domain .+.TP+.B subscribe, unsubscribe+Mail a confirmation request to the+.B From+address.+For subscriptions to a closed list the request goes to the list owner+instead.+.TP+.BI confirm- token+Apply a pending request and notify the subscriber.+Requests expire after+.B expiry+seconds.+.TP+.B bounces+Log the bounce to syslog.+Subscribers are not removed.+.TP+.B help+Reply with the list of command addresses, as does any other extension.+.P+No reply is ever sent to a message with an+.B Auto-Submitted+header other than+.BR no ,+a+.B Precedence+of+.BR bulk ,+.B list+or+.BR junk ,+or a null or+.B MAILER-DAEMON+envelope sender.+.SH FILES+Lists live in+.IR root /+.IR list /,+where the list name matches+.BR ^[a-z0-9][a-z0-9-]*$ .+.TP+.B description+One line describing the list.+.TP+.B owner+Owner address.+.TP+.B post+Who may post:+.BR anyone ,+.B subscribers+or+.BR owner .+The owner may always post.+.TP+.B subscribe+.B open+or+.BR closed .+.TP+.B public+Present if the archive is public; only then does pollen advertise it+in+.B List-Archive+and help replies.+.TP+.B subscribers+One lowercase address per line, written by pollen.+.TP+.BI pending/ token+.RI \(dq action " " address " " time \(dq+for each open request, written by pollen.+.TP+.B archive/+Maildir of every distributed post, exactly as sent.+.TP+.B .lock+Lock file serializing changes to+.BR subscribers .+.SH EXIT STATUS+.TP+.B 0+The message was accepted.+.TP+.B EX_NOUSER+No such list.+.TP+.B EX_NOPERM+The message was rejected; the reason is printed to standard output.+.TP+.B EX_TEMPFAIL+Transient failure; Postfix will retry.+.SH POSTFIX+.B master.cf:+.P+.nf+pollen unix - n n - - pipe+  flags=Rq user=pollen argv=/usr/local/bin/pollen ${user} ${extension}+.fi+.P+.B main.cf:+.P+.nf+recipient_delimiter = ++transport_maps = hash:/etc/postfix/transport+pollen_destination_recipient_limit = 1+relay_domains = lists.lostduk.com+relay_recipient_maps = hash:/etc/postfix/lists+.fi+.P+.BR transport :+.P+.nf+lists.lostduk.com pollen:+.fi+.P+.B relay_recipient_maps+lists every+.IR list ,+so Postfix rejects unknown lists before queueing.+.B pollen_destination_recipient_limit+must be 1.+The+.B pollen+user needs write access to every list directory.+.SH CUSTOMIZATION+.B pollen+is configured by editing+.I config.h+and recompiling.+.SH SEE ALSO+.BR pipe (8),+.BR sendmail (1)

pollen/pollen.c

@@ -0,0 +1,980 @@+/* imports */+#include <ctype.h>+#include <dirent.h>+#include <errno.h>+#include <fcntl.h>+#include <limits.h>+#include <signal.h>+#include <stdarg.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <strings.h>+#include <syslog.h>+#include <sysexits.h>+#include <time.h>+#include <unistd.h>+#include <sys/stat.h>+#include <sys/wait.h>++/* macros */+#define LENGTH(X) (sizeof(X) / sizeof((X)[0]))+#define ADDRLEN   256+#define HDRLEN    4096+#define TOKLEN    32++/* function declarations */+static int addrok(const char *s);+static int archive(const char *buf, size_t len, char *path, size_t plen);+static int automated(void);+static void bounce(const char *ext);+static int bprintf(char *buf, size_t len, const char *fmt, ...);+static void confirm(const char *tok);+static int deliver(const char *const *rcpts, size_t n, const char *buf,+                   size_t len);+static void die(int code, const char *fmt, ...);+static int dropped(size_t pos);+static size_t fieldend(size_t pos);+static int getheader(const char *name, char *buf, size_t len);+static void help(void);+static int isfield(size_t pos, const char *name);+static int ispublic(void);+static int loadsubs(char ***subs, size_t *n);+static void lower(char *s);+static int newtoken(char *tok);+static int notify(const char *to, const char *replyto, const char *subject,+                  const char *fmt, ...);+static int parseaddr(const char *s, char *buf, size_t len);+static void post(void);+static void prune(void);+static int readline(const char *file, char *buf, size_t len);+static void readmsg(void);+static int readpending(const char *path, char *act, char *addr,+                       long long *t);+static void request(const char *act);+static int sender(char *buf, size_t len);+static int setsub(const char *addr, int add);+static void usage(void);+static int validlist(const char *s);+static int validtoken(const char *s);++/* variables */+static const char *list;+static char dir[PATH_MAX];+static char *msg;+static size_t msglen;+static size_t hdrlen;++#include "config.h"++/* function implementations */+int+addrok(const char *s)+{+	const char *at, *p;+	unsigned char c;+	size_t n = strlen(s);++	if (!n || n > 254 || *s == '-' || s[n - 1] == '.'+	|| !(at = strchr(s, '@')) || at == s || !at[1]+	|| at[1] == '.' || at[1] == '-' || strchr(at + 1, '@')+	|| !strcasecmp(at + 1, domain))+		return 0;+	for (p = s; *p; p++) {+		c = *p;+		if (p < at && !isalnum(c)+		&& !strchr("!#$%&'*+./=?^_`{|}~-", c))+			return 0;+		if (p > at && !isalnum(c) && c != '.' && c != '-')+			return 0;+	}+	return 1;+}++int+archive(const char *buf, size_t len, char *path, size_t plen)+{+	static const char *sub[] = {+		"archive", "archive/tmp", "archive/new", "archive/cur"+	};+	char host[256], name[512], d[PATH_MAX], tmp[PATH_MAX];+	struct timespec ts;+	size_t i, off;+	ssize_t w;+	int fd, ok;++	for (i = 0; i < LENGTH(sub); i++) {+		if (bprintf(d, sizeof(d), "%s/%s", dir, sub[i]) < 0+		|| (mkdir(d, 0750) < 0 && errno != EEXIST))+			return -1;+	}+	if (gethostname(host, sizeof(host)) < 0)+		strcpy(host, "localhost");+	host[sizeof(host) - 1] = '\0';+	for (i = 0; host[i]; i++)+		if (host[i] == '/' || host[i] == ':')+			host[i] = '_';+	clock_gettime(CLOCK_REALTIME, &ts);+	if (bprintf(name, sizeof(name), "%lld.M%ldP%ld.%s",+	            (long long)ts.tv_sec, ts.tv_nsec / 1000, (long)getpid(),+	            host) < 0+	|| bprintf(tmp, sizeof(tmp), "%s/archive/tmp/%s", dir, name) < 0+	|| bprintf(path, plen, "%s/archive/new/%s", dir, name) < 0)+		return -1;+	if ((fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0640)) < 0)+		return -1;+	for (off = 0; off < len; off += w) {+		if ((w = write(fd, buf + off, len - off)) < 0) {+			if (errno != EINTR)+				break;+			w = 0;+		}+	}+	ok = off == len && !fsync(fd);+	if (close(fd) < 0)+		ok = 0;+	if (ok && link(tmp, path) < 0)+		ok = 0;+	unlink(tmp);+	if (!ok || bprintf(d, sizeof(d), "%s/archive/new", dir) < 0)+		return -1;+	if ((fd = open(d, O_RDONLY)) >= 0) {+		fsync(fd);+		close(fd);+	}+	return 0;+}++int+automated(void)+{+	char v[HDRLEN], rp[ADDRLEN];+	int n;++	if ((n = getheader("Auto-Submitted", v, sizeof(v)))) {+		if (n < 0 || strcspn(v, " \t(;") != 2+		|| strncasecmp(v, "no", 2))+			return 1;+	}+	if ((n = getheader("Precedence", v, sizeof(v)))) {+		if (n < 0 || (strcspn(v, " \t(;") == 4+		&& (!strncasecmp(v, "bulk", 4) || !strncasecmp(v, "list", 4)+		|| !strncasecmp(v, "junk", 4))))+			return 1;+	}+	if (getheader("Return-Path", v, sizeof(v)) < 1+	|| parseaddr(v, rp, sizeof(rp)) < 0 || !*rp)+		return 1;+	lower(rp);+	return !strncmp(rp, "mailer-daemon@", 14);+}++void+bounce(const char *ext)+{+	char v[HDRLEN];++	if (getheader("Return-Path", v, sizeof(v)) < 1)+		v[0] = '\0';+	syslog(LOG_NOTICE, "%s: bounce %s from %s", list, ext, v);+}++int+bprintf(char *buf, size_t len, const char *fmt, ...)+{+	va_list ap;+	int n;++	va_start(ap, fmt);+	n = vsnprintf(buf, len, fmt, ap);+	va_end(ap);+	if (n < 0 || (size_t)n >= len)+		return -1;+	return 0;+}++void+confirm(const char *tok)+{+	char path[PATH_MAX], act[16], addr[ADDRLEN], sub[ADDRLEN + 64];+	long long t = 0;+	int add, r = 0;++	if (validtoken(tok)+	&& bprintf(path, sizeof(path), "%s/pending/%s", dir, tok) == 0+	&& (r = readpending(path, act, addr, &t)) < 0 && errno != ENOENT)+		die(EX_TEMPFAIL, "cannot read confirmation token");+	if (r != 1 || t + expiry < (long long)time(NULL))+		die(EX_NOPERM, "unknown or expired confirmation token");+	add = !strcmp(act, "subscribe");+	if (setsub(addr, add) < 0)+		die(EX_TEMPFAIL, "cannot update subscribers");+	if (bprintf(sub, sizeof(sub), "%s %s@%s",+	            add ? "Subscribed to" : "Unsubscribed from",+	            list, domain) < 0)+		die(EX_TEMPFAIL, "list name too long");+	if (add)+		r = notify(addr, NULL, sub,+		           "You are now subscribed to %s@%s.\n\n"+		           "To unsubscribe, send an empty message to\n"+		           "%s+unsubscribe@%s.\n",+		           list, domain, list, domain);+	else+		r = notify(addr, NULL, sub,+		           "You are no longer subscribed to %s@%s.\n",+		           list, domain);+	if (r < 0)+		die(EX_TEMPFAIL, "cannot send notice");+	if (unlink(path) < 0 && errno != ENOENT)+		die(EX_TEMPFAIL, "cannot remove confirmation token");+	syslog(LOG_INFO, "%s: %s %s", list,+	       add ? "subscribed" : "unsubscribed", addr);+}++int+deliver(const char *const *rcpts, size_t n, const char *buf, size_t len)+{+	char from[ADDRLEN + 64], **argv;+	size_t i, off;+	ssize_t w;+	pid_t pid;+	int fd[2], st, ret = -1;++	if (!n)+		return 0;+	if (bprintf(from, sizeof(from), "%s+bounces@%s", list, domain) < 0+	|| !(argv = calloc(n + 6, sizeof(*argv))))+		return -1;+	argv[0] = (char *)sendmail;+	argv[1] = "-i";+	argv[2] = "-f";+	argv[3] = from;+	argv[4] = "--";+	for (i = 0; i < n; i++)+		argv[5 + i] = (char *)rcpts[i];+	if (pipe(fd) < 0)+		goto end;+	if ((pid = fork()) < 0) {+		close(fd[0]);+		close(fd[1]);+		goto end;+	}+	if (!pid) {+		dup2(fd[0], 0);+		close(fd[0]);+		close(fd[1]);+		execv(sendmail, argv);+		_exit(127);+	}+	close(fd[0]);+	for (off = 0; off < len; off += w) {+		if ((w = write(fd[1], buf + off, len - off)) < 0) {+			if (errno != EINTR)+				break;+			w = 0;+		}+	}+	close(fd[1]);+	while (waitpid(pid, &st, 0) < 0)+		if (errno != EINTR)+			goto end;+	if (off == len && WIFEXITED(st) && !WEXITSTATUS(st))+		ret = 0;+end:+	free(argv);+	return ret;+}++void+die(int code, const char *fmt, ...)+{+	char buf[512];+	va_list ap;++	va_start(ap, fmt);+	vsnprintf(buf, sizeof(buf), fmt, ap);+	va_end(ap);+	printf("%s\n", buf);+	fflush(stdout);+	syslog(code == EX_TEMPFAIL ? LOG_ERR : LOG_NOTICE, "%s: %s",+	       list ? list : "-", buf);+	exit(code);+}++int+dropped(size_t pos)+{+	return (hdrlen - pos >= 5 && !strncasecmp(msg + pos, "List-", 5))+	    || isfield(pos, "Return-Path") || isfield(pos, "Delivered-To");+}++size_t+fieldend(size_t pos)+{+	const char *p;++	while ((p = memchr(msg + pos, '\n', hdrlen - pos))) {+		pos = p - msg + 1;+		if (pos == hdrlen || (msg[pos] != ' ' && msg[pos] != '\t'))+			return pos;+	}+	return hdrlen;+}++int+getheader(const char *name, char *buf, size_t len)+{+	size_t pos, end, i, n = 0;+	const char *p;+	int found = 0;++	buf[0] = '\0';+	for (pos = 0; pos < hdrlen; pos = end) {+		end = fieldend(pos);+		if (!isfield(pos, name) || found++)+			continue;+		p = (const char *)memchr(msg + pos, ':', end - pos) + 1;+		for (; p < msg + end; p++) {+			if (*p == '\r' || *p == '\n')+				continue;+			if (!*p || n + 1 >= len)+				return -1;+			buf[n++] = *p;+		}+		while (n && (buf[n - 1] == ' ' || buf[n - 1] == '\t'))+			n--;+		buf[n] = '\0';+		for (i = 0; buf[i] == ' ' || buf[i] == '\t'; i++)+			;+		memmove(buf, buf + i, n - i + 1);+	}+	return found;+}++void+help(void)+{+	char addr[ADDRLEN], desc[256], sub[ADDRLEN + 64], arch[512] = "";++	if (sender(addr, sizeof(addr)) < 0)+		die(EX_NOPERM, "invalid or missing From address");+	readline("description", desc, sizeof(desc));+	if (bprintf(sub, sizeof(sub), "Help for %s@%s", list, domain) < 0+	|| (ispublic() && bprintf(arch, sizeof(arch), "archive      %s%s/\n",+	                          web, list) < 0))+		die(EX_TEMPFAIL, "list name too long");+	if (notify(addr, NULL, sub,+	           "%s@%s: %s\n\n"+	           "post         %s@%s\n"+	           "subscribe    %s+subscribe@%s\n"+	           "unsubscribe  %s+unsubscribe@%s\n"+	           "help         %s+help@%s\n"+	           "%s",+	           list, domain, desc, list, domain, list, domain,+	           list, domain, list, domain, arch) < 0)+		die(EX_TEMPFAIL, "cannot send help");+}++int+isfield(size_t pos, const char *name)+{+	size_t n = strlen(name);++	if (hdrlen - pos < n || strncasecmp(msg + pos, name, n))+		return 0;+	for (pos += n; pos < hdrlen && (msg[pos] == ' ' || msg[pos] == '\t');+	     pos++)+		;+	return pos < hdrlen && msg[pos] == ':';+}++int+ispublic(void)+{+	char path[PATH_MAX];+	struct stat st;++	return *web && bprintf(path, sizeof(path), "%s/public", dir) == 0+	    && stat(path, &st) == 0;+}++int+loadsubs(char ***subs, size_t *n)+{+	char path[PATH_MAX], *line = NULL, **v;+	size_t cap = 0, max = 0;+	FILE *fp;+	int ret = -1;++	*subs = NULL;+	*n = 0;+	if (bprintf(path, sizeof(path), "%s/subscribers", dir) < 0)+		return -1;+	if (!(fp = fopen(path, "r")))+		return errno == ENOENT ? 0 : -1;+	while (getline(&line, &cap, fp) > 0) {+		line[strcspn(line, "\r\n")] = '\0';+		lower(line);+		if (!*line)+			continue;+		if (!addrok(line)) {+			syslog(LOG_WARNING, "%s: skipping invalid subscriber",+			       list);+			continue;+		}+		if (*n == max) {+			max = max ? max * 2 : 64;+			if (!(v = realloc(*subs, max * sizeof(*v))))+				goto end;+			*subs = v;+		}+		if (!((*subs)[*n] = strdup(line)))+			goto end;+		(*n)++;+	}+	if (!ferror(fp))+		ret = 0;+end:+	free(line);+	fclose(fp);+	return ret;+}++void+lower(char *s)+{+	for (; *s; s++)+		*s = tolower((unsigned char)*s);+}++int+newtoken(char *tok)+{+	unsigned char b[TOKLEN / 2];+	FILE *fp;+	size_t i;+	int ok;++	if (!(fp = fopen("/dev/urandom", "r")))+		return -1;+	ok = fread(b, 1, sizeof(b), fp) == sizeof(b);+	fclose(fp);+	if (!ok)+		return -1;+	for (i = 0; i < sizeof(b); i++)+		sprintf(tok + 2 * i, "%02x", b[i]);+	return 0;+}++int+notify(const char *to, const char *replyto, const char *subject,+       const char *fmt, ...)+{+	char date[64], id[TOKLEN + 1], *buf = NULL;+	size_t len = 0;+	time_t t = time(NULL);+	va_list ap;+	FILE *fp;+	int ret;++	if (newtoken(id) < 0+	|| !strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S +0000",+	             gmtime(&t))+	|| !(fp = open_memstream(&buf, &len)))+		return -1;+	fprintf(fp, "From: %s+help@%s\n", list, domain);+	fprintf(fp, "To: %s\n", to);+	if (replyto)+		fprintf(fp, "Reply-To: %s\n", replyto);+	fprintf(fp, "Subject: %s\n", subject);+	fprintf(fp, "Date: %s\n", date);+	fprintf(fp, "Message-ID: <%s@%s>\n", id, domain);+	fprintf(fp, "List-Id: <%s.%s>\n", list, domain);+	fputs("Auto-Submitted: auto-replied\n"+	      "MIME-Version: 1.0\n"+	      "Content-Type: text/plain; charset=utf-8\n\n", fp);+	va_start(ap, fmt);+	vfprintf(fp, fmt, ap);+	va_end(ap);+	if (fclose(fp) == EOF) {+		free(buf);+		return -1;+	}+	ret = deliver(&to, 1, buf, len);+	free(buf);+	return ret;+}++int+parseaddr(const char *s, char *buf, size_t len)+{+	char bare[ADDRLEN];+	size_t nb = 0, na = 0;+	int depth = 0, quote = 0, angle = 0, seen = 0, gap = 0, bad = 0;+	unsigned char c;++	for (; *s; s++) {+		c = *s;+		if (quote || depth) {+			if (c == '\\' && s[1])+				s++;+			else if (quote && c == '"')+				quote = 0;+			else if (depth && c == '(')+				depth++;+			else if (depth && c == ')')+				depth--;+			continue;+		}+		if (c == '(') {+			depth = 1;+			continue;+		}+		if (isspace(c)) {+			gap = 1;+			continue;+		}+		if (angle) {+			if (c == '>') {+				angle = 0;+				seen = 1;+				continue;+			}+			if (strchr("<\",;:", c) || na + 1 >= len)+				return -1;+			buf[na++] = c;+			continue;+		}+		if (seen || strchr(">,;:", c))+			return -1;+		if (c == '<') {+			angle = 1;+			continue;+		}+		if (c == '"') {+			quote = bad = 1;+			continue;+		}+		if ((gap && nb) || nb + 1 >= sizeof(bare))+			bad = 1;+		else+			bare[nb++] = c;+		gap = 0;+	}+	if (quote || depth || angle)+		return -1;+	if (seen) {+		buf[na] = '\0';+		return 0;+	}+	if (bad || !nb || nb >= len)+		return -1;+	memcpy(buf, bare, nb);+	buf[nb] = '\0';+	return 0;+}++void+post(void)+{+	char addr[ADDRLEN], owner[ADDRLEN], policy[32], v[HDRLEN];+	char needle[512], path[PATH_MAX], **subs, *out = NULL;+	size_t i, n, pos, end, len, outlen = 0;+	FILE *fp;+	int ok = 0;++	if (!getheader("Message-ID", v, sizeof(v)))+		die(EX_NOPERM, "missing Message-ID");+	if (bprintf(needle, sizeof(needle), "<%s.%s>", list, domain) < 0)+		die(EX_TEMPFAIL, "list name too long");+	len = strlen(needle);+	for (pos = 0; pos < hdrlen; pos = end) {+		end = fieldend(pos);+		if (!isfield(pos, "List-Id"))+			continue;+		for (i = pos; i + len <= end; i++)+			if (!strncasecmp(msg + i, needle, len))+				die(EX_NOPERM, "mail loop: already sent "+				    "through %s@%s", list, domain);+	}+	if (sender(addr, sizeof(addr)) < 0)+		die(EX_NOPERM, "invalid or missing From address");+	if (readline("post", policy, sizeof(policy)) < 0)+		die(EX_TEMPFAIL, "cannot read post policy");+	readline("owner", owner, sizeof(owner));+	lower(owner);+	if (loadsubs(&subs, &n) < 0)+		die(EX_TEMPFAIL, "cannot read subscribers");+	if (!strcmp(policy, "anyone") || !strcmp(addr, owner))+		ok = 1;+	else if (!strcmp(policy, "subscribers"))+		for (i = 0; i < n && !ok; i++)+			ok = !strcmp(subs[i], addr);+	else if (strcmp(policy, "owner"))+		die(EX_TEMPFAIL, "unknown post policy");+	if (!ok && !strcmp(policy, "owner"))+		die(EX_NOPERM, "only the owner may post to %s@%s", list,+		    domain);+	if (!ok)+		die(EX_NOPERM, "%s is not subscribed to %s@%s", addr, list,+		    domain);++	if (!(fp = open_memstream(&out, &outlen)))+		die(EX_TEMPFAIL, "out of memory");+	fprintf(fp, "List-Id: <%s.%s>\n", list, domain);+	if (strcmp(policy, "owner"))+		fprintf(fp, "List-Post: <mailto:%s@%s>\n", list, domain);+	else+		fputs("List-Post: NO\n", fp);+	fprintf(fp, "List-Subscribe: <mailto:%s+subscribe@%s>\n", list,+	        domain);+	fprintf(fp, "List-Unsubscribe: <mailto:%s+unsubscribe@%s>\n", list,+	        domain);+	fprintf(fp, "List-Help: <mailto:%s+help@%s>\n", list, domain);+	if (ispublic())+		fprintf(fp, "List-Archive: <%s%s/>\n", web, list);+	fputs("Precedence: list\n", fp);+	for (pos = 0; pos < hdrlen; pos = end) {+		end = fieldend(pos);+		if (!dropped(pos))+			fwrite(msg + pos, 1, end - pos, fp);+	}+	fwrite(msg + hdrlen, 1, msglen - hdrlen, fp);+	if (fclose(fp) == EOF)+		die(EX_TEMPFAIL, "out of memory");++	if (archive(out, outlen, path, sizeof(path)) < 0)+		die(EX_TEMPFAIL, "cannot archive message");+	if (deliver((const char *const *)subs, n, out, outlen) < 0) {+		unlink(path);+		die(EX_TEMPFAIL, "cannot deliver message");+	}+	syslog(LOG_INFO, "%s: post from %s to %zu subscribers", list, addr, n);+}++void+prune(void)+{+	char path[PATH_MAX], act[16], addr[ADDRLEN];+	long long t, now = time(NULL);+	struct dirent *e;+	struct stat st;+	DIR *dp;+	int old, r;++	if (bprintf(path, sizeof(path), "%s/pending", dir) < 0+	|| !(dp = opendir(path)))+		return;+	while ((e = readdir(dp))) {+		if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")+		|| bprintf(path, sizeof(path), "%s/pending/%s", dir,+		           e->d_name) < 0)+			continue;+		t = 0;+		if (e->d_name[0] == '.')+			old = !stat(path, &st) && st.st_mtime + expiry < now;+		else if ((r = readpending(path, act, addr, &t)) < 0)+			old = 0;+		else+			old = !r || t + expiry < now;+		if (old)+			unlink(path);+	}+	closedir(dp);+}++int+readline(const char *file, char *buf, size_t len)+{+	char path[PATH_MAX];+	FILE *fp;+	size_t n;++	buf[0] = '\0';+	if (bprintf(path, sizeof(path), "%s/%s", dir, file) < 0+	|| !(fp = fopen(path, "r")))+		return -1;+	if (!fgets(buf, (int)len, fp))+		buf[0] = '\0';+	fclose(fp);+	n = strcspn(buf, "\r\n");+	while (n && (buf[n - 1] == ' ' || buf[n - 1] == '\t'))+		n--;+	buf[n] = '\0';+	return 0;+}++void+readmsg(void)+{+	size_t cap = 0, pos;+	ssize_t r;+	char *p;++	for (;;) {+		if (msglen == cap) {+			if (cap > (size_t)maxsize)+				die(EX_NOPERM, "message larger than %ld bytes",+				    maxsize);+			cap = cap ? cap * 2 : 65536;+			if (cap > (size_t)maxsize + 1)+				cap = maxsize + 1;+			if (!(p = realloc(msg, cap + 1)))+				die(EX_TEMPFAIL, "out of memory");+			msg = p;+		}+		if ((r = read(0, msg + msglen, cap - msglen)) < 0) {+			if (errno == EINTR)+				continue;+			die(EX_TEMPFAIL, "cannot read message");+		}+		if (!r)+			break;+		msglen += r;+	}+	msg[msglen] = '\0';+	for (pos = 0; pos < msglen; pos = p ? (size_t)(p - msg) + 1 : msglen) {+		if (msg[pos] == '\n'+		|| (msg[pos] == '\r' && msg[pos + 1] == '\n'))+			break;+		p = memchr(msg + pos, '\n', msglen - pos);+	}+	hdrlen = pos;+}++int+readpending(const char *path, char *act, char *addr, long long *t)+{+	char line[512];+	FILE *fp;+	int ok;++	if (!(fp = fopen(path, "r")))+		return -1;+	ok = fgets(line, sizeof(line), fp)+	  && sscanf(line, "%15s %255s %lld", act, addr, t) == 3;+	fclose(fp);+	return ok && (!strcmp(act, "subscribe") || !strcmp(act, "unsubscribe"))+	    && addrok(addr);+}++void+request(const char *act)+{+	char addr[ADDRLEN], to[ADDRLEN], mode[32], tok[TOKLEN + 1];+	char d[PATH_MAX], tmp[PATH_MAX], path[PATH_MAX];+	char reply[ADDRLEN + 64], sub[2 * ADDRLEN + 64];+	int add = !strcmp(act, "subscribe"), approve = 0, fd, ok, r;++	if (sender(addr, sizeof(addr)) < 0)+		die(EX_NOPERM, "invalid or missing From address");+	if (add) {+		if (readline("subscribe", mode, sizeof(mode)) < 0)+			die(EX_TEMPFAIL, "cannot read subscribe policy");+		if (!strcmp(mode, "closed"))+			approve = 1;+		else if (strcmp(mode, "open"))+			die(EX_TEMPFAIL, "unknown subscribe policy");+	}+	strcpy(to, addr);+	if (approve) {+		readline("owner", to, sizeof(to));+		lower(to);+		if (!addrok(to))+			die(EX_TEMPFAIL, "invalid list owner");+	}+	if (newtoken(tok) < 0+	|| bprintf(d, sizeof(d), "%s/pending", dir) < 0+	|| (mkdir(d, 0750) < 0 && errno != EEXIST)+	|| bprintf(tmp, sizeof(tmp), "%s/.%s", d, tok) < 0+	|| bprintf(path, sizeof(path), "%s/%s", d, tok) < 0+	|| bprintf(reply, sizeof(reply), "%s+confirm-%s@%s", list, tok,+	           domain) < 0)+		die(EX_TEMPFAIL, "cannot create request");+	fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0640);+	ok = fd >= 0 && dprintf(fd, "%s %s %lld\n", act, addr,+	                        (long long)time(NULL)) >= 0 && !fsync(fd);+	if (fd >= 0 && close(fd) < 0)+		ok = 0;+	if (!ok || rename(tmp, path) < 0) {+		unlink(tmp);+		die(EX_TEMPFAIL, "cannot write request");+	}++	if (approve) {+		r = bprintf(sub, sizeof(sub), "Approve subscription of %s to "+		            "%s@%s", addr, list, domain);+		if (!r)+			r = notify(to, reply, sub,+			           "%s asked to subscribe to %s@%s.\n\n"+			           "To approve, reply to this message or send "+			           "an empty message to\n%s.\n"+			           "The request expires in %ld days; ignore "+			           "this message to decline.\n",+			           addr, list, domain, reply, expiry / 86400);+	} else {+		r = bprintf(sub, sizeof(sub), "Confirm %s %s %s@%s",+		            add ? "subscription" : "unsubscription",+		            add ? "to" : "from", list, domain);+		if (!r)+			r = notify(to, reply, sub,+			           "Someone, hopefully you, asked to %s %s %s "+			           "%s@%s.\n\n"+			           "To confirm, reply to this message or send "+			           "an empty message to\n%s.\n"+			           "The request expires in %ld days; ignore "+			           "this message otherwise.\n",+			           act, addr, add ? "to" : "from", list,+			           domain, reply, expiry / 86400);+	}+	if (r < 0) {+		unlink(path);+		die(EX_TEMPFAIL, "cannot send confirmation");+	}+	syslog(LOG_INFO, "%s: %s request for %s", list, act, addr);+}++int+sender(char *buf, size_t len)+{+	char v[HDRLEN];++	if (getheader("From", v, sizeof(v)) != 1+	|| parseaddr(v, buf, len) < 0)+		return -1;+	lower(buf);+	return addrok(buf) ? 0 : -1;+}++int+setsub(const char *addr, int add)+{+	char lock[PATH_MAX], file[PATH_MAX], tmp[PATH_MAX], *line = NULL;+	size_t cap = 0;+	struct flock fl;+	FILE *in = NULL, *out = NULL;+	int lfd = -1, fd, found = 0, ret = -1;++	if (bprintf(lock, sizeof(lock), "%s/.lock", dir) < 0+	|| bprintf(file, sizeof(file), "%s/subscribers", dir) < 0+	|| bprintf(tmp, sizeof(tmp), "%s/.subscribers", dir) < 0+	|| (lfd = open(lock, O_RDWR | O_CREAT, 0640)) < 0)+		return -1;+	memset(&fl, 0, sizeof(fl));+	fl.l_type = F_WRLCK;+	fl.l_whence = SEEK_SET;+	while (fcntl(lfd, F_SETLKW, &fl) < 0)+		if (errno != EINTR)+			goto end;+	if (!(in = fopen(file, "r")) && errno != ENOENT)+		goto end;+	if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0640)) < 0)+		goto end;+	if (!(out = fdopen(fd, "w"))) {+		close(fd);+		goto end;+	}+	while (in && getline(&line, &cap, in) > 0) {+		line[strcspn(line, "\r\n")] = '\0';+		if (!*line)+			continue;+		if (!strcmp(line, addr)) {+			if (!add || found)+				continue;+			found = 1;+		}+		fprintf(out, "%s\n", line);+	}+	if (in && ferror(in))+		goto end;+	if (add && !found)+		fprintf(out, "%s\n", addr);+	if (fflush(out) == EOF || fsync(fileno(out)) < 0)+		goto end;+	fd = fclose(out);+	out = NULL;+	if (fd == EOF || rename(tmp, file) < 0)+		goto end;+	ret = 0;+end:+	if (out)+		fclose(out);+	if (ret < 0)+		unlink(tmp);+	if (in)+		fclose(in);+	free(line);+	close(lfd);+	return ret;+}++void+usage(void)+{+	fputs("usage: pollen list [extension]\n", stderr);+	exit(EX_USAGE);+}++int+validlist(const char *s)+{+	size_t n;++	for (n = 0; s[n]; n++)+		if (!islower((unsigned char)s[n])+		&& !isdigit((unsigned char)s[n]) && !(n && s[n] == '-'))+			return 0;+	return n && n <= 64;+}++int+validtoken(const char *s)+{+	return strlen(s) == TOKLEN && strspn(s, "0123456789abcdef") == TOKLEN;+}+++/* main */+int+main(int argc, char *argv[])+{+	char ext[256];+	struct stat st;++	signal(SIGPIPE, SIG_IGN);+	openlog("pollen", LOG_PID, LOG_MAIL);+	if (argc < 2 || argc > 3)+		usage();+	lower(argv[1]);+	list = argv[1];+	if (!validlist(list)+	|| bprintf(dir, sizeof(dir), "%s/%s", root, list) < 0)+		die(EX_NOUSER, "no such list");+	if (stat(dir, &st) < 0) {+		if (errno == ENOENT || errno == ENOTDIR)+			die(EX_NOUSER, "no such list");+		die(EX_TEMPFAIL, "cannot access list");+	}+	if (!S_ISDIR(st.st_mode))+		die(EX_NOUSER, "no such list");+	if (bprintf(ext, sizeof(ext), "%s", argc > 2 ? argv[2] : "") < 0)+		die(EX_NOPERM, "invalid address extension");+	lower(ext);++	readmsg();+	prune();+	if (!*ext)+		post();+	else if (!strcmp(ext, "bounces") || !strncmp(ext, "bounces+", 8))+		bounce(ext);+	else if (automated())+		syslog(LOG_INFO, "%s: ignoring automated message to +%s", list,+		       ext);+	else if (!strcmp(ext, "subscribe") || !strcmp(ext, "unsubscribe"))+		request(ext);+	else if (!strncmp(ext, "confirm-", 8))+		confirm(ext + 8);+	else+		help();+	return 0;+}