/* imports */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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 <%s+help@%s>\n", list, 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: \n", list, domain); else fputs("List-Post: NO\n", fp); fprintf(fp, "List-Subscribe: \n", list, domain); fprintf(fp, "List-Unsubscribe: \n", list, domain); fprintf(fp, "List-Help: \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; }