feat(pollen): init
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;+}