diff --git Makefile Makefile
index 0f0e31a..04df5e5 100644
--- Makefile
+++ Makefile
@@ -4,6 +4,224 @@ SHELL=/bin/sh
 
 default: it
 
+acceptutils: \
+reup authup checknotroot fixsmtpio
+
+acceptutils_base64.o: \
+compile99 acceptutils_base64.c acceptutils_base64.h
+	./compile99 acceptutils_base64.c
+
+acceptutils_stralloc.o: \
+compile99 acceptutils_stralloc.c acceptutils_stralloc.h
+	./compile99 acceptutils_stralloc.c
+
+acceptutils_ucspitls.o: \
+compile99 acceptutils_ucspitls.c acceptutils_ucspitls.h acceptutils_stralloc.h case.h env.h fd.h readwrite.h scan.h
+	./compile99 acceptutils_ucspitls.c
+
+acceptutils_unistd.o: \
+compile99 acceptutils_unistd.c acceptutils_unistd.h
+	./compile99 acceptutils_unistd.c
+
+acceptutils-memcheck: \
+test_fixsmtpio
+	valgrind --track-origins=yes --leak-check=full --error-exitcode=99 ./test_fixsmtpio >/dev/null
+	#valgrind --track-origins=yes --leak-check=full --error-exitcode=88 ./fixsmtpio echo hi >/dev/null
+
+acceptutils-tests: \
+test_fixsmtpio
+
+acceptutils-tests-run: \
+acceptutils-tests
+	@prove -v -e '' ./test_fixsmtpio | grep -v '^ok'
+
+authup: \
+load authup.o auto_qmail.o acceptutils_base64.o acceptutils_unistd.o acceptutils_ucspitls.o \
+acceptutils_stralloc.o commands.o control.o timeoutread.o timeoutwrite.o now.o \
+case.a env.a fd.a getln.a open.a sig.a wait.a stralloc.a alloc.a \
+substdio.a error.a str.a fs.a \
+socket.lib
+	./load authup auto_qmail.o acceptutils_base64.o acceptutils_unistd.o acceptutils_ucspitls.o \
+	acceptutils_stralloc.o commands.o control.o timeoutread.o timeoutwrite.o now.o \
+	case.a env.a fd.a getln.a open.a sig.a wait.a stralloc.a alloc.a \
+	substdio.a error.a str.a fs.a \
+	`cat socket.lib`
+
+authup.o: \
+compile99 authup.c commands.h fd.h sig.h stralloc.h gen_alloc.h \
+substdio.h alloc.h wait.h str.h byte.h now.h datetime.h fmt.h exit.h \
+readwrite.h timeoutread.h timeoutwrite.h acceptutils_base64.h case.h \
+env.h control.h error.h scan.h auto_qmail.h acceptutils_unistd.h \
+acceptutils_stralloc.h
+	./compile99 authup.c
+
+check.h: \
+conf-check check_stdint.h
+	cat `head -1 conf-check`/include/check.h \
+	| sed 's}<check_stdint\.h>}"check_stdint.h"}g' \
+	> check.h
+
+check_stdint.h: \
+conf-check
+	cp `head -1 conf-check`/include/check_stdint.h .
+
+checknotroot: \
+load checknotroot.o acceptutils_unistd.o substdio.a error.a str.a
+	./load checknotroot acceptutils_unistd.o substdio.a error.a str.a
+
+checknotroot.o: \
+compile99 checknotroot.c exit.h readwrite.h substdio.h acceptutils_unistd.h
+	./compile99 checknotroot.c
+
+fixsmtpio: \
+load fixsmtpio.o fixsmtpio_control.o fixsmtpio_die.o fixsmtpio_filter.o \
+fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \
+fixsmtpio_proxy.o acceptutils_ucspitls.o auto_qmail.o control.o \
+acceptutils_unistd.o acceptutils_stralloc.o \
+getln.a substdio.a stralloc.a env.a str.a error.a fd.a sig.a \
+alloc.a wait.a case.a open.a fs.a
+	./load fixsmtpio fixsmtpio_control.o fixsmtpio_die.o fixsmtpio_filter.o \
+	fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \
+	fixsmtpio_proxy.o acceptutils_ucspitls.o auto_qmail.o control.o \
+	acceptutils_unistd.o acceptutils_stralloc.o \
+	getln.a substdio.a stralloc.a env.a str.a error.a fd.a sig.a \
+	alloc.a wait.a case.a open.a fs.a
+
+fixsmtpio.o: \
+compile99 fixsmtpio.c fixsmtpio.h fixsmtpio_die.h fixsmtpio_filter.h \
+fixsmtpio_proxy.h alloc.h auto_qmail.h case.h control.h env.h \
+fd.h scan.h str.h substdio.h wait.h acceptutils_stralloc.h acceptutils_unistd.h
+	./compile99 fixsmtpio.c
+
+fixsmtpio_die.o: \
+compile99 fixsmtpio_die.c fixsmtpio.h fixsmtpio_die.h readwrite.h \
+acceptutils_stralloc.h acceptutils_unistd.h
+	./compile99 fixsmtpio_die.c
+
+test_acceptutils_stralloc.o: \
+compile99 acceptutils_stralloc.c fixsmtpio.h acceptutils_stralloc.h readwrite.h \
+check.h \
+test_acceptutils_stralloc.c
+	./compile99 test_acceptutils_stralloc.c
+
+fixsmtpio_control.o: \
+compile99 fixsmtpio_control.c fixsmtpio.h fixsmtpio_control.h \
+acceptutils_stralloc.h
+	./compile99 fixsmtpio_control.c
+
+test_fixsmtpio_control.o: \
+compile99 fixsmtpio_control.c fixsmtpio.h fixsmtpio_control.h \
+check.h \
+test_fixsmtpio_control.c
+	./compile99 test_fixsmtpio_control.c
+
+fixsmtpio_eventq.o: \
+compile99 fixsmtpio_eventq.c alloc.h str.h fixsmtpio.h fixsmtpio_die.h fixsmtpio_eventq.h
+	./compile99 fixsmtpio_eventq.c
+
+test_fixsmtpio_eventq.o: \
+compile99 fixsmtpio_eventq.c alloc.h str.h fixsmtpio.h fixsmtpio_die.h fixsmtpio_eventq.h \
+check.h \
+test_fixsmtpio_eventq.c
+	./compile99 test_fixsmtpio_eventq.c
+
+fixsmtpio_filter.o: \
+compile99 fixsmtpio_filter.c fixsmtpio_filter.h fixsmtpio_die.h fixsmtpio_munge.h fixsmtpio_glob.h \
+acceptutils_stralloc.h
+	./compile99 fixsmtpio_filter.c
+
+test_fixsmtpio_filter.o: \
+compile99 fixsmtpio_filter.c fixsmtpio_filter.h fixsmtpio_die.h fixsmtpio_munge.h fixsmtpio_glob.h \
+check.h \
+test_fixsmtpio_filter.c
+	./compile99 test_fixsmtpio_filter.c
+
+fixsmtpio_glob.o: \
+compile99 fixsmtpio_glob.c fixsmtpio_glob.h
+	./compile99 fixsmtpio_glob.c
+
+test_fixsmtpio_glob.o: \
+compile99 fixsmtpio_glob.c fixsmtpio_glob.h \
+check.h \
+test_fixsmtpio_glob.c
+	./compile99 test_fixsmtpio_glob.c
+
+fixsmtpio_munge.o: \
+compile99 fixsmtpio_munge.c fixsmtpio_munge.h fixsmtpio_die.h \
+acceptutils_stralloc.h
+	./compile99 fixsmtpio_munge.c
+
+test_fixsmtpio_munge.o: \
+compile99 fixsmtpio_munge.c fixsmtpio_munge.h fixsmtpio_die.h \
+check.h \
+test_fixsmtpio_munge.c
+	./compile99 test_fixsmtpio_munge.c
+
+fixsmtpio_proxy.o: \
+compile99 fixsmtpio_proxy.c fixsmtpio_proxy.h fixsmtpio_readwrite.h \
+fixsmtpio_die.h fixsmtpio_eventq.h fixsmtpio_filter.h fixsmtpio_die.h \
+acceptutils_stralloc.h acceptutils_unistd.h fmt.h
+	./compile99 fixsmtpio_proxy.c
+
+test_fixsmtpio_proxy.o: \
+compile99 fixsmtpio_proxy.c fixsmtpio_proxy.h fixsmtpio_readwrite.h \
+fixsmtpio_die.h fixsmtpio_eventq.h fixsmtpio_filter.h \
+acceptutils_stralloc.h \
+check.h \
+test_fixsmtpio_proxy.c
+	./compile99 test_fixsmtpio_proxy.c
+
+fixsmtpio_readwrite.o: \
+compile99 fixsmtpio_readwrite.c fixsmtpio_readwrite.h fixsmtpio_die.h error.h readwrite.h select.h \
+acceptutils_stralloc.h
+	./compile99 fixsmtpio_readwrite.c
+
+test_fixsmtpio: \
+fixsmtpio \
+load test_fixsmtpio.o fixsmtpio_control.o fixsmtpio_die.o acceptutils_unistd.o \
+fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \
+fixsmtpio_filter.o fixsmtpio_proxy.o acceptutils_stralloc.o acceptutils_ucspitls.o \
+test_fixsmtpio_control.o \
+test_acceptutils_stralloc.o test_fixsmtpio_eventq.o test_fixsmtpio_filter.o \
+test_fixsmtpio_glob.o test_fixsmtpio_munge.o test_fixsmtpio_proxy.o \
+auto_qmail.o control.o getln.a \
+substdio.a stralloc.a env.a str.a error.a fd.a sig.a alloc.a wait.a \
+case.a open.a fs.a \
+libcheck.a rt.lib
+	./load test_fixsmtpio fixsmtpio_control.o fixsmtpio_die.o acceptutils_unistd.o \
+	fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \
+	fixsmtpio_filter.o fixsmtpio_proxy.o acceptutils_stralloc.o acceptutils_ucspitls.o \
+	test_fixsmtpio_control.o \
+	test_acceptutils_stralloc.o test_fixsmtpio_eventq.o test_fixsmtpio_filter.o \
+	test_fixsmtpio_glob.o test_fixsmtpio_munge.o test_fixsmtpio_proxy.o \
+	auto_qmail.o control.o getln.a \
+	substdio.a stralloc.a env.a str.a error.a fd.a sig.a alloc.a wait.a \
+	case.a open.a fs.a \
+	libcheck.a -lpthread -lm `cat rt.lib`
+
+test_fixsmtpio.o: \
+compile99 test_fixsmtpio.c fixsmtpio.h check.h
+	./compile99 test_fixsmtpio.c
+
+libcheck.a: \
+conf-check
+	cp `head -1 conf-check`/lib/libcheck.a .
+
+reup: \
+load reup.o acceptutils_unistd.o wait.a error.a getopt.a substdio.a env.a alloc.a str.a fs.a
+	./load reup acceptutils_unistd.o wait.a error.a getopt.a substdio.a env.a alloc.a str.a fs.a
+
+reup.o: \
+compile99 reup.c acceptutils_unistd.h env.h fmt.h readwrite.h scan.h sgetopt.h str.h substdio.h wait.h
+	./compile99 reup.c
+
+rt.lib: \
+compile load
+	( ( echo 'main() { ; }' > tryrt.c && ./compile tryrt.c && \
+	./load tryrt -lrt ) >/dev/null 2>&1 \
+	&& echo -lrt || exit 0 ) > rt.lib
+	rm -f tryrt.c tryrt.o tryrt
+
 addresses.0: \
 addresses.5
 	nroff -man addresses.5 > addresses.0
@@ -303,6 +521,7 @@ exit.h auto_spawn.h
 clean: \
 TARGETS
 	rm -f `cat TARGETS`
+	git checkout -- INSTALL SENDMAIL
 
 coe.o: \
 compile coe.c coe.h
@@ -319,6 +538,12 @@ make-compile warn-auto.sh systype
 	compile
 	chmod 755 compile
 
+compile99: \
+make-compile99 warn-auto.sh systype
+	( cat warn-auto.sh; ./make-compile99 "`cat systype`" ) > \
+	compile99
+	chmod 755 compile99
+
 condredirect: \
 load condredirect.o qmail.o strerr.a fd.a sig.a wait.a seek.a env.a \
 substdio.a error.a str.a fs.a auto_qmail.o
@@ -908,6 +1133,11 @@ make-compile.sh auto-ccld.sh
 	cat auto-ccld.sh make-compile.sh > make-compile
 	chmod 755 make-compile
 
+make-compile99: \
+make-compile99.sh auto-ccld.sh
+	cat auto-ccld.sh make-compile99.sh > make-compile99
+	chmod 755 make-compile99
+
 make-load: \
 make-load.sh auto-ccld.sh
 	cat auto-ccld.sh make-load.sh > make-load
diff --git TARGETS TARGETS
index facdad7..29eb50b 100644
--- TARGETS
+++ TARGETS
@@ -385,3 +385,38 @@ forgeries.0
 man
 setup
 check
+make-compile99
+compile99
+acceptutils_base64.o
+acceptutils_stralloc.o
+acceptutils_ucspitls.o
+acceptutils_unistd.o
+authup
+authup.o
+checknotroot
+checknotroot.o
+reup
+reup.o
+fixsmtpio
+fixsmtpio.o
+test_fixsmtpio
+test_fixsmtpio.o
+fixsmtpio_filter.o
+fixsmtpio_proxy.o
+fixsmtpio_die.o
+fixsmtpio_eventq.o
+fixsmtpio_readwrite.o
+fixsmtpio_munge.o
+fixsmtpio_glob.o
+fixsmtpio_control.o
+check.h
+check_stdint.h
+libcheck.a
+rt.lib
+test_acceptutils_stralloc.o
+test_fixsmtpio_control.o
+test_fixsmtpio_eventq.o
+test_fixsmtpio_filter.o
+test_fixsmtpio_glob.o
+test_fixsmtpio_munge.o
+test_fixsmtpio_proxy.o
diff --git acceptutils_base64.c acceptutils_base64.c
new file mode 100644
index 0000000..8b9d622
--- /dev/null
+++ acceptutils_base64.c
@@ -0,0 +1,119 @@
+#include "acceptutils_base64.h"
+#include "stralloc.h"
+#include "substdio.h"
+#include "str.h"
+
+static char *b64alpha =
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+#define B64PAD '='
+
+/* returns 0 ok, 1 illegal, -1 problem */
+
+int b64decode(const unsigned char *in,int l,stralloc *out)
+/* stralloc *out =>  not null terminated */
+{
+  int p = 0;
+  int n;
+  unsigned int x;
+  int i, j;
+  char *s;
+  unsigned char b[3];
+
+  if (l == 0) {
+    if (!stralloc_copys(out,"")) return -1;
+    return 0;
+  }
+
+  while (in[l-1] == B64PAD) {
+    p ++;
+    l--;
+  }
+
+  n = (l + p) / 4;
+  i = (n * 3) - p;
+  if (!stralloc_ready(out,i)) return -1;
+  out->len = i;
+  s = out->s;
+
+  for (i = 0; i < n - 1; i++) {
+    x = 0;
+    for (j = 0; j < 4; j++) {
+      if (in[j] >= 'A' && in[j] <= 'Z')
+        x = (x << 6) + (unsigned int)(in[j] - 'A' + 0);
+      else if (in[j] >= 'a' && in[j] <= 'z')
+        x = (x << 6) + (unsigned int)(in[j] - 'a' + 26);
+      else if (in[j] >= '0' && in[j] <= '9')
+        x = (x << 6) + (unsigned int)(in[j] - '0' + 52);
+      else if (in[j] == '+')
+        x = (x << 6) + 62;
+      else if (in[j] == '/')
+        x = (x << 6) + 63;
+      else if (in[j] == '=')
+        x = (x << 6);
+    }
+
+    s[2] = (unsigned char)(x & 255); x >>= 8;
+    s[1] = (unsigned char)(x & 255); x >>= 8;
+    s[0] = (unsigned char)(x & 255); x >>= 8;
+    s += 3; in += 4;
+  }
+
+  x = 0;
+  for (j = 0; j < 4; j++) {
+    if (in[j] >= 'A' && in[j] <= 'Z')
+      x = (x << 6) + (unsigned int)(in[j] - 'A' + 0);
+    else if (in[j] >= 'a' && in[j] <= 'z')
+      x = (x << 6) + (unsigned int)(in[j] - 'a' + 26);
+    else if (in[j] >= '0' && in[j] <= '9')
+      x = (x << 6) + (unsigned int)(in[j] - '0' + 52);
+    else if (in[j] == '+')
+      x = (x << 6) + 62;
+    else if (in[j] == '/')
+      x = (x << 6) + 63;
+    else if (in[j] == '=')
+      x = (x << 6);
+  }
+
+  b[2] = (unsigned char)(x & 255); x >>= 8;
+  b[1] = (unsigned char)(x & 255); x >>= 8;
+  b[0] = (unsigned char)(x & 255); x >>= 8;
+
+  for (i = 0; i < 3 - p; i++)
+    s[i] = b[i];
+
+  return 0;
+}
+
+int b64encode(stralloc *in,stralloc *out)
+{
+  unsigned char a, b, c;
+  int i;
+  char *s;
+
+  if (in->len == 0)
+  {
+    if (!stralloc_copys(out,"")) return -1;
+    return 0;
+  }
+
+  i = in->len / 3 * 4 + 4;
+  if (!stralloc_ready(out,i)) return -1;
+  s = out->s;
+
+  for (i = 0; i < in->len; i += 3) {
+    a = in->s[i];
+    b = i + 1 < in->len ? in->s[i + 1] : 0;
+    c = i + 2 < in->len ? in->s[i + 2] : 0;
+
+    *s++ = b64alpha[a >> 2];
+    *s++ = b64alpha[((a & 3 ) << 4) | (b >> 4)];
+
+    if (i + 1 >= in->len) *s++ = B64PAD;
+    else *s++ = b64alpha[((b & 15) << 2) | (c >> 6)];
+
+    if (i + 2 >= in->len) *s++ = B64PAD;
+    else *s++ = b64alpha[c & 63];
+  }
+  out->len = s - out->s;
+  return 0;
+}
diff --git acceptutils_base64.h acceptutils_base64.h
new file mode 100644
index 0000000..4d6ab92
--- /dev/null
+++ acceptutils_base64.h
@@ -0,0 +1,9 @@
+#ifndef BASE64_H
+#define BASE64_H
+
+#include "stralloc.h"
+
+int b64decode(const unsigned char *,int,stralloc *);
+int b64encode(stralloc *,stralloc *);
+
+#endif
diff --git acceptutils_stralloc.c acceptutils_stralloc.c
new file mode 100644
index 0000000..947e40c
--- /dev/null
+++ acceptutils_stralloc.c
@@ -0,0 +1,44 @@
+#include "acceptutils_stralloc.h"
+
+static void (*die_sa)(const char *,const char *);
+
+void stralloc_set_die(void (*die_nomem)(const char *,const char *)) {
+  die_sa = die_nomem;
+}
+
+void contextlogging_append(const char *caller,stralloc *to,char *from) {
+  if (!stralloc_append(to,from)) die_sa(caller,__func__);
+}
+void contextlogging_append0(const char *caller,stralloc *to) {
+  if (!stralloc_0(to)) die_sa(caller,__func__);
+}
+void contextlogging_cat(const char *caller,stralloc *to,stralloc *from) {
+  if (!stralloc_cat(to,from)) die_sa(caller,__func__);
+}
+void contextlogging_catb(const char *caller,stralloc *to,char *buf,int len) {
+  if (!stralloc_catb(to,buf,len)) die_sa(caller,__func__);
+}
+void contextlogging_cats(const char *caller,stralloc *to,char *from) {
+  if (!stralloc_cats(to,from)) die_sa(caller,__func__);
+}
+void contextlogging_copy(const char *caller,stralloc *to,stralloc *from) {
+  if (!stralloc_copy(to,from)) die_sa(caller,__func__);
+}
+void contextlogging_copyb(const char *caller,stralloc *to,char *buf,int len) {
+  if (!stralloc_copyb(to,buf,len)) die_sa(caller,__func__);
+}
+void contextlogging_copys(const char *caller,stralloc *to,char *from) {
+  if (!stralloc_copys(to,from)) die_sa(caller,__func__);
+}
+
+void prepends(stralloc *to,char *from) {
+  stralloc tmp = {0};
+  copy(&tmp,to);
+  copys(to,(char *)from);
+  cat(to,&tmp);
+}
+int starts(stralloc *haystack,char *needle) { return stralloc_starts(haystack,needle); }
+int ends_with_newline(stralloc *sa) {
+  return sa->len > 0 && sa->s[sa->len - 1] == '\n';
+}
+void blank(stralloc *sa) { copys(sa,""); }
diff --git acceptutils_stralloc.h acceptutils_stralloc.h
new file mode 100644
index 0000000..6dbb1bc
--- /dev/null
+++ acceptutils_stralloc.h
@@ -0,0 +1,25 @@
+#include "stralloc.h"
+
+void stralloc_set_die(void (*)(const char *,const char *));
+
+void contextlogging_append(const char *,stralloc *,char *);
+#define append(a,b) contextlogging_append(__func__,a,b)
+void contextlogging_append0(const char *,stralloc *);
+#define append0(a) contextlogging_append0(__func__,a)
+void contextlogging_cat(const char *,stralloc *,stralloc *);
+#define cat(a,b) contextlogging_cat(__func__,a,b)
+void contextlogging_catb(const char *,stralloc *,char *,int);
+#define catb(a,b,c) contextlogging_catb(__func__,a,b,c)
+void contextlogging_cats(const char *,stralloc *,char *);
+#define cats(a,b) contextlogging_cats(__func__,a,b)
+void contextlogging_copy(const char *,stralloc *,stralloc *);
+#define copy(a,b) contextlogging_copy(__func__,a,b)
+void contextlogging_copyb(const char *,stralloc *,char *,int);
+#define copyb(a,b,c) contextlogging_copyb(__func__,a,b,c)
+void contextlogging_copys(const char *,stralloc *,char *);
+#define copys(a,b) contextlogging_copys(__func__,a,b)
+
+void prepends(stralloc *,char *);
+int  starts(stralloc *,char *);
+int  ends_with_newline(stralloc *);
+void blank(stralloc *);
diff --git acceptutils_ucspitls.c acceptutils_ucspitls.c
new file mode 100644
index 0000000..f27b5ee
--- /dev/null
+++ acceptutils_ucspitls.c
@@ -0,0 +1,110 @@
+#include "case.h"
+#include "env.h"
+#include "fd.h"
+#include "readwrite.h"
+#include "scan.h"
+
+#include "acceptutils_stralloc.h"
+#include "acceptutils_ucspitls.h"
+
+int ucspitls_level(void) {
+  char *ucspitls = env_get("UCSPITLS");
+  char *disabletls = env_get("DISABLETLS");
+  env_unset("UCSPITLS");
+  env_unset("DISABLETLS");
+  if (disabletls || !ucspitls || !case_diffs(ucspitls,"-"))
+    return UCSPITLS_UNAVAILABLE;
+  if (!case_diffs(ucspitls,"!")) return UCSPITLS_REQUIRED;
+  return UCSPITLS_AVAILABLE;
+}
+
+static int get_fd_for(char *name) {
+  unsigned long fd;
+  char *fdstr;
+
+  if (!(fdstr = env_get(name))) return 0;
+  if (!scan_ulong(fdstr,&fd)) return 0;
+
+  return (int)fd;
+}
+
+static int notify_control_socket() {
+  unsigned int fd = get_fd_for("SSLCTLFD");
+
+  if (!fd) return 0;
+  if (write(fd, "Y", 1) < 1) return 0;
+
+  return 1;
+}
+
+static int adjust_read_fd() {
+  unsigned int fd = get_fd_for("SSLREADFD");
+
+  if (!fd) return 0;
+  if (fd_move(0,fd) == -1) return 0;
+
+  return 1;
+}
+
+static int adjust_write_fd() {
+  unsigned int fd = get_fd_for("SSLWRITEFD");
+
+  if (!fd) return 0;
+  if (fd_move(1,fd) == -1) return 0;
+
+  return 1;
+}
+
+int tls_init(void) {
+  return notify_control_socket() && adjust_read_fd() && adjust_write_fd();
+}
+
+int tls_info(void (*die_nomem)(const char *caller,const char *alloc_fn)) {
+  unsigned long fd;
+  char envbuf[8192];
+  char *x;
+  int j;
+
+  stralloc ssl_env   = {0};
+  stralloc ssl_parm  = {0};
+  stralloc ssl_value = {0};
+
+  fd = get_fd_for("SSLCTLFD");
+  if (!fd) return 0;
+
+  while ((j=read(fd,envbuf,8192)) > 0 ) {
+    catb(&ssl_env,envbuf,j);
+      if (ssl_env.len >= 2 && ssl_env.s[ssl_env.len-2]==0 && ssl_env.s[ssl_env.len-1]==0)
+        break;
+  }
+  if (j <= 0)
+    die_nomem(__func__,"read");
+
+  x = ssl_env.s;
+
+  for (j=0;j < ssl_env.len-1;++j) {
+    if ( *x != '=' ) {
+      catb(&ssl_parm,x,1);
+      x++;
+    } else {
+      append0(&ssl_parm);
+      x++;
+
+      for (;j < ssl_env.len-j-1;++j) {
+        if ( *x != '\0' ) {
+          catb(&ssl_value,x,1);
+          x++;
+        } else {
+          append0(&ssl_value);
+          x++;
+          if (!env_put2(ssl_parm.s,ssl_value.s))
+            die_nomem(__func__,"env_put2");
+          ssl_parm.len = 0;
+          ssl_value.len = 0;
+          break;
+        }
+      }
+    }
+  }
+  return j;
+}
diff --git acceptutils_ucspitls.h acceptutils_ucspitls.h
new file mode 100644
index 0000000..e9f2179
--- /dev/null
+++ acceptutils_ucspitls.h
@@ -0,0 +1,7 @@
+#define UCSPITLS_UNAVAILABLE 0
+#define UCSPITLS_AVAILABLE   1
+#define UCSPITLS_REQUIRED    2
+
+int ucspitls_level(void);
+int tls_init(void);
+int tls_info(void (*)(const char *,const char *));
diff --git acceptutils_unistd.c acceptutils_unistd.c
new file mode 100644
index 0000000..fad749e
--- /dev/null
+++ acceptutils_unistd.c
@@ -0,0 +1,21 @@
+#include <unistd.h>
+
+int unistd_chdir(const char *path) { return chdir(path); }
+
+int unistd_close(int fildes) { return close(fildes); }
+
+int unistd_execvp(const char *file, char *const argv[]) {
+  return execvp(file, argv);
+}
+
+void unistd_exit(int status) { return _exit(status); }
+
+int unistd_fork(void) { return fork(); }
+
+int unistd_getpid(void) { return getpid(); }
+
+int unistd_getuid(void) { return getuid(); }
+
+int unistd_pipe(int fildes[2]) { return pipe(fildes); }
+
+int unistd_sleep(int seconds) { return sleep(seconds); }
diff --git acceptutils_unistd.h acceptutils_unistd.h
new file mode 100644
index 0000000..72d0d46
--- /dev/null
+++ acceptutils_unistd.h
@@ -0,0 +1,9 @@
+int unistd_chdir(const char *);
+int unistd_close(int);
+int unistd_execvp(const char *,char *const[]);
+void unistd_exit(int);
+int unistd_fork(void);
+int unistd_getpid(void);
+int unistd_getuid(void);
+int unistd_pipe(int [2]);
+int unistd_sleep(int);
diff --git authup.8 authup.8
new file mode 100644
index 0000000..8158e9b
--- /dev/null
+++ authup.8
@@ -0,0 +1,163 @@
+.TH AUTHUP 8 2018-11-06
+.SH NAME
+authup \- offer SMTP or POP3 authentication
+.SH SYNOPSIS
+.B authup
+.I protocol
+.I checkpassword
+.I prog
+.SH DESCRIPTION
+.B authup
+speaks just enough SMTP (AUTH LOGIN or PLAIN)
+or POP3 (USER-PASS)
+to parse a username and password.
+It then runs
+.I checkpassword
+via the
+.B checkpassword
+interface.
+
+.B authup
+is most commonly invoked from
+.B sslserver
+as root, so that
+.B checkpassword
+can run
+.B ofmipd
+(or
+.BR qmail-smtpd )
+or
+.B qmail-pop3d
+under the UID of the authenticated user.
+
+.B authup
+waits for
+.I checkpassword
+and
+.I prog
+to finish.
+It prints an error message if
+.I checkpassword
+or
+.IR prog
+crashes or exits nonzero.
+If authentication failed, and
+.I protocol
+is SMTP, it waits 5 seconds
+before printing the message.
+
+When run under
+.BR "sslserver -n" ,
+.B authup
+can be configured to offer TLS and/or refuse to authenticate without it.
+.SH "ENVIRONMENT VARIABLES"
+If
+.B UCSPITLS
+is set to "!",
+.B authup
+will require the client to negotiate TLS before attempting to authenticate.
+
+If
+.B DISABLETLS
+is set,
+.B UCSPITLS
+will not be honored.
+
+If
+.B AUTHUP_SASL_BROKEN_CLIENTS
+is set,
+.B authup
+will additionally advertise AUTH support in a second, non-standard way
+to interoperate with Outlook Express 4, Exchange 5,
+and other SMTP clients that implement an obsolete version of the AUTH command.
+
+If authentication succeeds,
+.IR prog 's
+environment will contain the username in
+.BR AUTHUP_USER .
+
+Since
+.I prog
+will run with the privileges of the authenticated user, so will
+any
+.B qmail-queue
+wrapper configured as
+.BR QMAILQUEUE .
+This can be useful for user-controlled filtering.
+.SH "CONTROL FILES"
+.TP 5
+.I smtpgreeting / pop3greeting
+SMTP and POP3 greeting messages.
+Default:
+.IR me ,
+if that is supplied;
+otherwise
+.B authup
+will refuse to run.
+The first word of the greeting
+should be the current host's name.
+.TP 5
+.I smtpcapabilities / pop3capabilities
+SMTP and POP3 capabilities (one per line) to advertise in
+.I EHLO
+and
+.IR CAPA ,
+respectively.
+Default:
+.IR none .
+Without the needed file,
+.B authup
+will refuse to run.
+
+Typical POP3 capabilities: TOP, UIDL. (Omit STLS and USER.)
+
+Typical SMTP capabilities: PIPELINING, 8BITMIME. (Omit STARTTLS and AUTH.)
+
+Correct values for your system depend on what
+.I prog
+offers.
+For instance, to inspect your installed
+.BR qmail-smtpd :
+
+$ echo EHLO | qmail-smtpd \\
+     | sed -e '1,2d' -e 's|^....||' | egrep -v '^(STARTTLS|AUTH)'
+
+.B authup
+will automatically advertise TLS support when run under
+.B sslserver
+with the
+.B -n
+flag.
+.TP 5
+.I timeoutsmtpd / timeoutpop3d
+Number of seconds
+.B authup
+will wait for each new buffer of data from the remote SMTP or POP3 client.
+Default: 1200.
+.SH "COMPATIBILITY"
+While
+.B CRAM-MD5
+is available in most SMTP AUTH patches, and
+.B APOP
+is available in
+.BR qmail-popup ,
+neither is currently supported by
+.BR authup .
+Nor is authenticating with a client certificate.
+If you rely on any of these, please share your use case with the author.
+.SH "EXAMPLES"
+See
+.IR https://schmonz.com/qmail/acceptutils .
+.SH "AUTHOR"
+.B Amitai Schleier <schmonz-web-acceptutils@schmonz.com>
+.SH "SEE ALSO"
+reup(8),
+checknotroot(8),
+fixsmtpio(8),
+sslserver(1),
+ucspi-tls(2),
+qmail-qfilter-ofmipd-queue(8),
+ofmipd(8),
+qmail-smtpd(8),
+qmail-pop3d(8),
+qmail-popup(8).
diff --git authup.c authup.c
new file mode 100644
index 0000000..2a72e78
--- /dev/null
+++ authup.c
@@ -0,0 +1,583 @@
+#include "auto_qmail.h"
+#include "commands.h"
+#include "fd.h"
+#include "sig.h"
+#include "substdio.h"
+#include "alloc.h"
+#include "wait.h"
+#include "str.h"
+#include "byte.h"
+#include "now.h"
+#include "fmt.h"
+#include "scan.h"
+#include "exit.h"
+#include "readwrite.h"
+#include "timeoutread.h"
+#include "timeoutwrite.h"
+#include "case.h"
+#include "env.h"
+#include "control.h"
+#include "error.h"
+#include "open.h"
+
+#include "acceptutils_base64.h"
+#include "acceptutils_stralloc.h"
+#include "acceptutils_ucspitls.h"
+#include "acceptutils_unistd.h"
+
+#define HOMEPAGE "https://schmonz.com/qmail/acceptutils"
+#define PROGNAME "authup"
+
+#define EXITCODE_CHECKPASSWORD_UNACCEPTABLE   1
+#define EXITCODE_CHECKPASSWORD_MISUSED        2
+#define EXITCODE_CHECKPASSWORD_TEMPFAIL     111
+/* sync with fixsmtpio.h */
+#define EXITCODE_FIXSMTPIO_TIMEOUT           16
+#define EXITCODE_FIXSMTPIO_PARSEFAIL         18
+
+static int timeout = 1200;
+int tls_level = UCSPITLS_UNAVAILABLE;
+int in_tls = 0;
+
+void die()         { _exit( 1); }
+void die_noretry() { _exit(12); }
+
+int safewrite(int fd,char *buf,int len) {
+  int r;
+  r = timeoutwrite(timeout,fd,buf,len);
+  if (r <= 0) die();
+  return r;
+}
+
+char ssoutbuf[SUBSTDIO_OUTSIZE];
+substdio ssout = SUBSTDIO_FDBUF(safewrite,1,ssoutbuf,sizeof ssoutbuf);
+
+void out(char *s) { substdio_puts(&ssout,s); }
+void flush() { substdio_flush(&ssout); }
+
+void pop3_err(char *s) { out("-ERR "); out(s); out("\r\n"); flush(); }
+void smtp_out(char *s) {               out(s); out("\r\n"); flush(); }
+
+struct authup_error {
+  char *name;
+  char *message;
+  char *smtpcode;
+  char *smtperror;
+  int sleep;
+  void (*die)();
+};
+
+struct authup_error e[] = {
+  { "control", "unable to read controls",      "421", "4.3.0", 0, die_noretry }
+, { "nomem",   "out of memory",                "451", "4.3.0", 0, die_noretry }
+, { "alarm",   "timeout",                      "451", "4.4.2", 0, die_noretry }
+, { "pipe",    "unable to open pipe",          "454", "4.3.0", 0, die_noretry }
+, { "read",    "unable to read pipe",          "454", "4.3.0", 0, die_noretry }
+, { "write",   "unable to write pipe",         "454", "4.3.0", 0, die_noretry }
+, { "fork",    "unable to fork",               "454", "4.3.0", 0, die_noretry }
+, { "wait",    "unable to wait for child",     "454", "4.3.0", 0, die_noretry }
+, { "crash",   "aack, child crashed",          "454", "4.3.0", 0, die_noretry }
+, { "badauth", "authorization failed",         "535", "5.7.0", 5, die         }
+, { "noauth",  "auth type unimplemented",      "504", "5.5.1", 5, die         }
+, { "input",   "malformed auth input",         "501", "5.5.4", 5, die         }
+, { "authabrt","auth exchange cancelled",      "501", "5.0.0", 5, die         }
+, { "protocol","protocol exchange ended",      "501", "5.0.0", 0, die_noretry }
+, { "starttls","TLS temporarily not available","454", "5.7.3", 0, die         }
+, { "needtls", "Must start TLS first",         "530", "5.7.0", 0, die         }
+, { 0,         "unknown or unspecified error", "421", "4.3.0", 0, die_noretry }
+};
+
+void pop3_auth_error(struct authup_error ae) {
+  out("-ERR");
+  out(" " PROGNAME " ");
+  out(ae.message);
+}
+
+void smtp_auth_error(struct authup_error ae) {
+  out(ae.smtpcode);
+  out(" " PROGNAME " ");
+  out(ae.message);
+  out(" (#");
+  out(ae.smtperror);
+  out(")");
+}
+
+void (*protocol_error)();
+
+void pop3_sleep(int s) { return; }
+void smtp_sleep(int s) { unistd_sleep(s); }
+void (*protocol_sleep)();
+
+char sserrbuf[SUBSTDIO_OUTSIZE];
+substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf);
+
+void errflush(char *s) {
+  substdio_puts(&sserr,PROGNAME ": ");
+  substdio_puts(&sserr,s);
+  substdio_putsflush(&sserr,"\n");
+}
+
+void authup_die(const char *name) {
+  int i;
+  for (i = 0;e[i].name;++i) if (case_equals(e[i].name,name)) break;
+  protocol_sleep(e[i].sleep);
+  protocol_error(e[i]);
+  out("\r\n");
+  flush();
+  e[i].die();
+}
+
+void die_nomem(const char *caller,const char *alloc_fn) {
+  authup_die("nomem");
+}
+
+void die_usage() { errflush("usage: " PROGNAME " <smtp|pop3> prog"); die(); }
+
+void smtp_err_authoriz() { smtp_out("530 " PROGNAME " authentication required (#5.7.1)"); }
+void pop3_err_authoriz() { pop3_err(PROGNAME " authorization first"); }
+
+void pop3_err_syntax()   { pop3_err(PROGNAME " syntax error"); }
+void pop3_err_wantuser() { pop3_err(PROGNAME " USER first"); }
+
+int saferead(int fd,char *buf,int len) {
+  int r;
+  r = timeoutread(timeout,fd,buf,len);
+  if (r == -1) if (errno == error_timeout) authup_die("alarm");
+  if (r <= 0) authup_die("read");
+  return r;
+}
+
+char ssinbuf[SUBSTDIO_INSIZE];
+substdio ssin = SUBSTDIO_FDBUF(saferead,0,ssinbuf,sizeof ssinbuf);
+
+stralloc greeting = {0};
+stralloc capabilities = {0};
+char **childargs;
+
+void pop3_okay() { out("+OK \r\n"); flush(); }
+void pop3_quit() { pop3_okay(); _exit(0); }
+void smtp_quit() { out("221 "); smtp_out(greeting.s); _exit(0); }
+
+stralloc username = {0};
+stralloc password = {0};
+stralloc timestamp = {0};
+
+void exit_according_to_child_exit(int exitcode) {
+  switch (exitcode) {
+    case EXITCODE_CHECKPASSWORD_UNACCEPTABLE:
+    case EXITCODE_CHECKPASSWORD_MISUSED:
+    case EXITCODE_CHECKPASSWORD_TEMPFAIL:
+      authup_die("badauth");
+    case EXITCODE_FIXSMTPIO_TIMEOUT:
+      authup_die("alarm");
+    case EXITCODE_FIXSMTPIO_PARSEFAIL:
+      authup_die("control");
+    default:
+      _exit(0);
+  }
+}
+
+void logtry(char *username) {
+  substdio_puts(&sserr,PROGNAME ": login attempt by ");
+  substdio_puts(&sserr,username);
+  substdio_putsflush(&sserr,"\n");
+}
+
+void checkpassword(stralloc *username,stralloc *password,stralloc *timestamp) {
+  int child;
+  int wstat;
+  int pi[2];
+  char upbuf[SUBSTDIO_OUTSIZE];
+  substdio ssup;
+
+  unistd_close(3);
+  if (unistd_pipe(pi) == -1) authup_die("pipe");
+  if (pi[0] != 3) authup_die("pipe");
+  switch((child = unistd_fork())) {
+    case -1:
+      authup_die("fork");
+    case 0:
+      unistd_close(pi[1]);
+      sig_pipedefault();
+      append0(username);
+      logtry(username->s);
+      if (!env_put2("AUTHUP_USER",username->s)) authup_die("nomem");
+      unistd_execvp(*childargs,childargs);
+      authup_die("fork");
+  }
+  unistd_close(pi[0]);
+  substdio_fdbuf(&ssup,write,pi[1],upbuf,sizeof upbuf);
+
+  append0(username);
+  if (substdio_put(&ssup,username->s,username->len) == -1) authup_die("write");
+  byte_zero(username->s,username->len);
+
+  append0(password);
+  if (substdio_put(&ssup,password->s,password->len) == -1) authup_die("write");
+  byte_zero(password->s,password->len);
+
+  append0(timestamp);
+  if (substdio_put(&ssup,timestamp->s,timestamp->len) == -1) authup_die("write");
+  byte_zero(timestamp->s,timestamp->len);
+
+  if (substdio_flush(&ssup) == -1) authup_die("write");
+  unistd_close(pi[1]);
+  byte_zero(upbuf,sizeof upbuf);
+
+  if (wait_pid(&wstat,child) == -1) authup_die("wait");
+  if (wait_crashed(wstat)) authup_die("crash");
+
+  exit_according_to_child_exit(wait_exitcode(wstat));
+}
+
+static char unique[FMT_ULONG + FMT_ULONG + 3];
+
+void pop3_greet() {
+  char *s;
+  s = unique;
+  s += fmt_uint(s,unistd_getpid());
+  *s++ = '.';
+  s += fmt_ulong(s,(unsigned long) now());
+  *s++ = '@';
+  *s++ = 0;
+  out("+OK <");
+  out(unique);
+  out(greeting.s);
+  out(">\r\n");
+  flush();
+}
+
+void pop3_format_capa(stralloc *multiline) {
+  cats(multiline,".\r\n");
+}
+
+void pop3_capa(char *arg) {
+  out("+OK capability list follows\r\n");
+  if (tls_level >= UCSPITLS_AVAILABLE && !in_tls) out("STLS\r\n");
+  out("USER\r\n");
+  out(capabilities.s);
+  flush();
+}
+
+static int seenuser = 0;
+
+void pop3_stls(char *arg) {
+  if (tls_level < UCSPITLS_AVAILABLE || in_tls) return pop3_err("STLS not available");
+  out("+OK starting TLS negotiation\r\n");
+  flush();
+
+  if (!tls_init() || !tls_info(die_nomem)) authup_die("starttls");
+  /* reset state */
+  seenuser = 0;
+
+  in_tls = 1;
+}
+
+void pop3_user(char *arg) {
+  if (tls_level >= UCSPITLS_REQUIRED && !in_tls) authup_die("needtls");
+  if (!*arg) { pop3_err_syntax(); return; }
+  pop3_okay();
+  seenuser = 1;
+  copys(&username,arg);
+}
+
+void pop3_pass(char *arg) {
+  if (!seenuser) { pop3_err_wantuser(); return; }
+  if (!*arg) { pop3_err_syntax(); return; }
+
+  copys(&password,arg);
+  byte_zero(arg,str_len(arg));
+
+  copys(&timestamp,"<");
+  cats(&timestamp,unique);
+  cats(&timestamp,greeting.s);
+  cats(&timestamp,">");
+
+  checkpassword(&username,&password,&timestamp);
+}
+
+void smtp_greet() {
+  out("220 ");
+  out(greeting.s);
+  out(" ESMTP\r\n");
+  flush();
+}
+
+void smtp_helo(char *arg) {
+  out("250 ");
+  smtp_out(greeting.s);
+}
+
+// copy from fixsmtpio_munge.c:change_last_line_fourth_char_to_space()
+void smtp_format_ehlo(stralloc *multiline) {
+  int pos = 0;
+  int i;
+  for (i = multiline->len - 2; i >= 0; i--) {
+    if (multiline->s[i] == '\n') {
+      pos = i + 1;
+      break;
+    }
+  }
+  capabilities.s[pos+3] = ' ';
+}
+
+void smtp_ehlo(char *arg) {
+  char *x;
+  out("250-"); out(greeting.s); out("\r\n");
+  if (tls_level >= UCSPITLS_AVAILABLE && !in_tls) out("250-STARTTLS\r\n");
+  out("250-AUTH LOGIN PLAIN\r\n");
+  if ((x = env_get("AUTHUP_SASL_BROKEN_CLIENTS")))
+    out("250-AUTH=LOGIN PLAIN\r\n");
+  out(capabilities.s);
+  flush();
+}
+
+void smtp_starttls() {
+  if (tls_level < UCSPITLS_AVAILABLE || in_tls) return smtp_out("502 unimplemented (#5.5.1)");
+  smtp_out("220 Ready to start TLS (#5.7.0)");
+
+  if (!tls_init() || !tls_info(die_nomem)) authup_die("starttls");
+  /* reset state */
+  ssin.p = 0;
+
+  in_tls = 1;
+}
+
+static stralloc authin = {0};
+
+void smtp_authgetl() {
+  int i;
+
+  blank(&authin);
+
+  for (;;) {
+    if (!stralloc_readyplus(&authin,1)) authup_die("nomem"); /* XXX */
+    i = substdio_get(&ssin,authin.s + authin.len,1);
+    if (i != 1) authup_die("read");
+    if (authin.s[authin.len] == '\n') break;
+    ++authin.len;
+  }
+
+  if (authin.len > 0) if (authin.s[authin.len - 1] == '\r') --authin.len;
+  authin.s[authin.len] = 0;
+
+  if (*authin.s == '*' && *(authin.s + 1) == 0) authup_die("authabrt");
+  if (authin.len == 0) authup_die("input");
+}
+
+static int b64decode2(char *c,int i,stralloc *sa) {
+  return b64decode((const unsigned char *)c,i,sa);
+}
+
+void auth_login(char *arg) {
+  int r;
+
+  if (*arg) {
+    if ((r = b64decode2(arg,str_len(arg),&username)) == 1) authup_die("input");
+  }
+  else {
+    smtp_out("334 VXNlcm5hbWU6"); /* Username: */
+    smtp_authgetl();
+    if ((r = b64decode2(authin.s,authin.len,&username)) == 1) authup_die("input");
+  }
+  if (r == -1) authup_die("nomem");
+
+  smtp_out("334 UGFzc3dvcmQ6"); /* Password: */
+
+  smtp_authgetl();
+  if ((r = b64decode2(authin.s,authin.len,&password)) == 1) authup_die("input");
+  if (r == -1) authup_die("nomem");
+
+  if (!username.len || !password.len) authup_die("input");
+  checkpassword(&username,&password,&timestamp);
+}
+
+static stralloc resp = {0};
+
+void auth_plain(char *arg) {
+  int r, id = 0;
+
+  if (*arg) {
+    if ((r = b64decode2(arg,str_len(arg),&resp)) == 1) authup_die("input");
+  }
+  else {
+    smtp_out("334 ");
+    smtp_authgetl();
+    if ((r = b64decode2(authin.s,authin.len,&resp)) == 1) authup_die("input");
+  }
+  if (r == -1) authup_die("nomem");
+  append0(&resp);
+  while (resp.s[id]) id++; /* ignore authorize-id */
+
+  if (resp.len > id + 1)
+    copys(&username,resp.s + id + 1);
+  if (resp.len > id + username.len + 2)
+    copys(&password,resp.s + id + username.len + 2);
+
+  if (!username.len || !password.len) authup_die("input");
+  checkpassword(&username,&password,&timestamp);
+}
+
+void smtp_auth(char *arg) {
+  int i;
+  char *cmd = arg;
+
+  if (tls_level >= UCSPITLS_REQUIRED && !in_tls) authup_die("needtls");
+
+  i = str_chr(cmd,' ');
+  arg = cmd + i;
+  while (*arg == ' ') ++arg;
+  cmd[i] = 0;
+
+  if (case_equals("login",cmd)) auth_login(arg);
+  if (case_equals("plain",cmd)) auth_plain(arg);
+  authup_die("noauth");
+}
+
+void smtp_help() {
+  smtp_out("214 " PROGNAME " home page: " HOMEPAGE);
+}
+
+void smtp_noop() {
+  smtp_out("250 ok");
+}
+
+struct commands pop3commands[] = {
+  { "stls", pop3_stls, 0 }
+, { "user", pop3_user, 0 }
+, { "pass", pop3_pass, 0 }
+, { "quit", pop3_quit, 0 }
+, { "capa", pop3_capa, 0 }
+, { "noop", pop3_okay, 0 }
+, { 0, pop3_err_authoriz, 0 }
+};
+
+struct commands smtpcommands[] = {
+  { "starttls", smtp_starttls, 0 }
+, { "auth", smtp_auth, flush }
+, { "quit", smtp_quit, 0 }
+, { "helo", smtp_helo, 0 }
+, { "ehlo", smtp_ehlo, 0 }
+, { "help", smtp_help, 0 }
+, { "noop", smtp_noop, 0 }
+, { 0, smtp_err_authoriz, 0 }
+};
+
+struct protocol {
+  char *name;
+  char *cap_prefix;
+  void (*cap_format_response)();
+  void (*error)();
+  void (*sleep)();
+  void (*greet)();
+  struct commands *c;
+};
+
+struct protocol p[] = {
+  { "pop3", "",     pop3_format_capa, pop3_auth_error, pop3_sleep, pop3_greet, pop3commands }
+, { "smtp", "250-", smtp_format_ehlo, smtp_auth_error, smtp_sleep, smtp_greet, smtpcommands }
+, { 0,      "",     0,                die_usage,       0,     die_usage,  0            }
+};
+
+int control_readgreeting(char *p) {
+  stralloc file = {0};
+  int retval;
+
+  copys(&file,"control/");
+  cats(&file,p);
+  cats(&file,"greeting");
+  append0(&file);
+
+  retval = control_rldef(&greeting,file.s,1,(char *) 0);
+  if (retval != 1) retval = -1;
+
+  append0(&greeting);
+
+  return retval;
+}
+
+int control_readtimeout(char *p) {
+  stralloc file = {0};
+
+  copys(&file,"control/timeout");
+  cats(&file,p);
+  cats(&file,"d");
+  append0(&file);
+
+  return control_readint(&timeout,file.s);
+}
+
+int control_readcapabilities(struct protocol p) {
+  stralloc file = {0};
+  stralloc lines = {0};
+  int linestart;
+  int pos;
+
+  copys(&file,"control/");
+  cats(&file,p.name);
+  cats(&file,"capabilities");
+  append0(&file);
+
+  if (control_readfile(&lines,file.s,0) != 1) return -1;
+
+  blank(&capabilities);
+  for (linestart = 0, pos = 0; pos < lines.len; pos++) {
+    if (lines.s[pos] == '\0') {
+      cats(&capabilities,p.cap_prefix);
+      cats(&capabilities,lines.s+linestart);
+      cats(&capabilities,"\r\n");
+      linestart = pos + 1;
+    }
+  }
+  p.cap_format_response(&capabilities);
+  append0(&capabilities);
+
+  return 1;
+}
+
+int should_greet() {
+  char *x;
+  int r;
+
+  if (!(x = env_get("REUP"))) return 1;
+  if (!scan_ulong(x,&r)) return 1;
+  if (r > 1) return 0;
+  return 1;
+}
+
+void doprotocol(struct protocol p) {
+  protocol_error = p.error;
+  protocol_sleep = p.sleep;
+
+  if (unistd_chdir(auto_qmail) == -1) authup_die("control");
+  if (control_init() == -1) authup_die("control");
+  if (control_readgreeting(p.name) == -1) authup_die("control");
+  if (control_readtimeout(p.name) == -1) authup_die("control");
+  if (control_readcapabilities(p) == -1) authup_die("control");
+  if (should_greet()) p.greet();
+  commands(&ssin,p.c);
+  authup_die("protocol");
+}
+
+int main(int argc,char **argv) {
+  char *protocol;
+  int i;
+
+  sig_alarmcatch(die);
+  sig_pipeignore();
+
+  stralloc_set_die(die_nomem);
+
+  protocol = argv[1];
+  if (!protocol) die_usage();
+
+  childargs = argv + 2;
+  if (!*childargs) die_usage();
+
+  tls_level = ucspitls_level();
+
+  for (i = 0; p[i].name; ++i)
+    if (case_equals(p[i].name,protocol))
+      doprotocol(p[i]);
+  die_usage();
+}
diff --git checknotroot.8 checknotroot.8
new file mode 100644
index 0000000..3be2f45
--- /dev/null
+++ checknotroot.8
@@ -0,0 +1,32 @@
+.TH CHECKNOTROOT 8 2018-11-06
+.SH NAME
+checknotroot \- refuse to run as UID 0
+.SH SYNOPSIS
+.B checknotroot
+.I prog
+.SH DESCRIPTION
+.B checknotroot
+ensures that another command can never be
+run as UID 0.
+
+.B checknotroot
+is most commonly placed after
+.B checkpassword
+and before its remaining arguments to ensure that if someone guesses the
+root password, they won't know they did.
+
+.SH "EXIT CODES"
+If run by UID 0,
+.B checknotroot
+exits 1.
+Otherwise it exits with the same code as
+.IR prog .
+.SH "EXAMPLES"
+See
+.IR https://schmonz.com/qmail/acceptutils .
+.SH "AUTHOR"
+.B Amitai Schleier <schmonz-web-acceptutils@schmonz.com>
+.SH "SEE ALSO"
+checkpassword(8),
+authup(8),
+fixsmtpio(8).
diff --git checknotroot.c checknotroot.c
new file mode 100644
index 0000000..0f6d887
--- /dev/null
+++ checknotroot.c
@@ -0,0 +1,31 @@
+#include "exit.h"
+#include "readwrite.h"
+#include "substdio.h"
+
+#include "acceptutils_unistd.h"
+
+char sserrbuf[SUBSTDIO_OUTSIZE];
+substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf);
+
+void errflush(char *s) {
+  substdio_puts(&sserr,"checknotroot: ");
+  substdio_puts(&sserr,s);
+  substdio_puts(&sserr,"\n");
+  substdio_flush(&sserr);
+}
+
+void die() { _exit(1); }
+void die_usage() { errflush("usage: checknotroot prog"); die(); }
+void die_root() { errflush("WAS RUNNING AS ROOT, TERMINATING"); die(); }
+
+int main(int argc,char **argv) {
+  char **childargs;
+ 
+  childargs = argv + 1;
+  if (!*childargs) die_usage();
+
+  if (unistd_getuid() == 0) die_root();
+ 
+  unistd_execvp(*childargs,childargs);
+  die();
+}
diff --git conf-check conf-check
new file mode 100644
index 0000000..566cfbc
--- /dev/null
+++ conf-check
@@ -0,0 +1,3 @@
+/opt/pkg
+
+This is the prefix where libcheck is installed.
diff --git fixsmtpio.8 fixsmtpio.8
new file mode 100644
index 0000000..26ac898
--- /dev/null
+++ fixsmtpio.8
@@ -0,0 +1,253 @@
+.TH FIXSMTPIO 8 2018-11-06
+.SH NAME
+fixsmtpio \- filter SMTP I/O and exit status
+.SH SYNOPSIS
+.B fixsmtpio
+.I prog
+.SH DESCRIPTION
+.B fixsmtpio
+is a proxy for changing how an SMTP service behaves without changing its code.
+By default, with no configuration, it changes almost no observable behavior.
+In a typical configuration, it makes
+.B ofmipd
+or
+.B qmail-smtpd
+behave as though they were designed to run under
+.B authup
+by adjusting certain
+client requests,
+server responses,
+and exit codes.
+The
+.B "EXIT CODES"
+section is important enough to be all the way up here.
+.SH "EXIT CODES"
+.B authup
+expects
+.B fixsmtpio
+to exit 18
+when
+.I control/fixsmtpio
+exists and fails to parse.
+.B fixsmtpio
+does so.
+
+.B authup
+expects distinct exit codes for distinct conditions.
+.B checkpassword
+exits 1 when a password is unacceptable.
+.B ofmipd
+and
+.B qmail-smtpd
+exit 1 when any error occurs.
+.B fixsmtpio
+can and should be configured to exit
+.TP 3
+14
+when the server greeting code starts with 4 (temporary failure),
+.TP 3
+15
+when the server greeting code starts with 5 (permanent failure),
+.TP 3
+16
+when the server times out,
+and
+.TP 3
+0
+when the server receives EOF from the client.
+.SH "ENVIRONMENT VARIABLES"
+.B authup
+sets
+.B AUTHUP_USER
+in the environment.
+Many typical filter rules apply if and only if this variable is present.
+
+If
+.B UCSPITLS
+is set while running under
+.BR "sslserver -n" ,
+.B fixsmtpio
+supports STARTTLS.
+The special value
+.B !
+requires peers to negotiate TLS before doing much of anything else.
+The empty string is a more sensible choice for public SMTP service.
+.B fixsmtpio
+sets
+.B FIXSMTPIOTLS
+in the environment when TLS has been negotiated.
+
+If
+.B DISABLETLS
+is set,
+.B UCSPITLS
+will not be honored.
+
+If
+.B NOFIXSMTPIO
+is set,
+.B fixsmtpio
+will simply replace itself with
+.IR prog .
+
+If
+.B FIXSMTPIODEBUG
+is set,
+.B fixsmtpio
+will show its work.
+Lines are prefixed like so:
+.TP 3
+1: Request received from client
+.TP 3
+2: Request sent to server
+.TP 3
+3: Response received from server
+.TP 3
+4: Response sent to client
+.TP 3
+D: DATA received from client and sent to server as is
+
+.SH "FILTER RULES"
+Filter rules follow the format:
+
+.I [env]:event:[request-prepend]:response-line-glob:[exitcode]:[response]
+
+(Values other than
+.I response
+may not contain ":", the field separator.)
+.TP 5
+.I env
+.br
+(optional)
+Environment variable which must be present for the rule to apply.
+If empty, none is required.
+.TP 5
+.I event
+.br
+(required)
+SMTP verb (or "clienteof", "greeting", or "timeout") to which the rule applies.
+This field is matched case-insensitively.
+.TP 5
+.I request-prepend
+.br
+(optional)
+String to prepend to the request before passing it to the server.
+"NOOP " (with trailing space) causes the server to trivially accept a request,
+performing no action.
+If empty, the request is sent as is.
+.TP 5
+.I response-line-glob
+.br
+(required)
+.BR fnmatch(3) -style
+glob pattern, with no special options set.
+Each line of the response is compared against this glob.
+.TP 5
+.I exitcode
+.br
+(optional)
+Numeric code with which
+.B fixsmtpio
+is to exit immediately.
+If empty, it will exit whenever it normally would.
+If there are no rules specifying a value for this field,
+.B fixsmtpio
+will always exit with the same code as its child
+.IR prog .
+.TP 5
+.I response
+.br
+(optional)
+String that replaces every matching line of the server response.
+
+If empty, matching lines are removed.
+
+Strings starting with
+"&fixsmtpio"
+are reserved for special handling.
+
+"&fixsmtpio_noop"
+causes the response to be sent as is.
+
+"&fixsmtpio_fixup"
+computes the replacement string via an event-specific internal routine.
+(If no corresponding routine exists,
+.B fixsmtpio
+will refuse to run.)
+Routines exist for
+.TP 10
+.BR HELP :
+.br
+Prepend this program's home page,
+in an attempt to direct support requests to
+.BR AUTHOR .
+Please enable it!
+.TP 10
+.BR "greeting",
+.BR EHLO ,
+.BR HELO ,
+and
+.BR QUIT :
+.br
+Include
+.I smtpgreeting
+in the response.
+This is harmless with
+.BR qmail-smtpd ,
+which already does so,
+and brings
+.BR ofmipd 's
+behavior up to par.
+.TP 0
+Rules are applied in the order written. For instance, if two rules
+match, the later rule modifies the response returned by the
+earlier rule.
+If multiple matching rules for an event set
+.BR exitcode ,
+the last one wins.
+
+Not all rules (alone or in combination) make practical sense.
+An earlier rule can cause a later one to start (or stop) matching.
+Keep your configuration as simple as possible, and test it well.
+.SH "CONTROL FILES"
+.TP 5
+.I fixsmtpio
+Filter rules as described in
+.BR "FILTER RULES" .
+Default: none.
+.TP 5
+.I smtpgreeting
+SMTP greeting message.
+Default:
+.IR me ,
+if that is supplied;
+otherwise
+.B fixsmtpio
+will refuse to run.
+The first word of
+.I smtpgreeting
+should be the current host's name.
+.SH "COMPATIBILITY"
+.B fixsmtpio
+handles STARTTLS internally, terminating TLS in order to continue
+observing requests and responses and do its job.
+If you rely on STARTTLS support directly in
+.B qmail-smtpd
+via a patch,
+please share your use case with the author.
+.SH "EXAMPLES"
+See
+.IR https://schmonz.com/qmail/acceptutils .
+.SH "AUTHOR"
+.B Amitai Schleier <schmonz-web-acceptutils@schmonz.com>
+.SH "SEE ALSO"
+authup(8),
+sslserver(1),
+ucspi-tls(2),
+checkpassword(8),
+checknotroot(8),
+qmail-smtpd(8),
+ofmipd(8),
+fnmatch(3),
+fixcrio(1),
+spamdyke.
diff --git fixsmtpio.c fixsmtpio.c
new file mode 100644
index 0000000..a9b589e
--- /dev/null
+++ fixsmtpio.c
@@ -0,0 +1,33 @@
+#include "fixsmtpio.h"
+#include "fixsmtpio_die.h"
+#include "fixsmtpio_filter.h"
+#include "fixsmtpio_proxy.h"
+
+#include "acceptutils_stralloc.h"
+#include "acceptutils_unistd.h"
+
+static void load_smtp_greeting(stralloc *greeting,char *configfile) {
+  if (control_init() == -1) die_control();
+  if (control_rldef(greeting,configfile,1,(char *) 0) != 1) die_control();
+}
+
+static void cd_var_qmail() {
+  if (unistd_chdir(auto_qmail) == -1) die_control();
+}
+
+int main(int argc,char **argv) {
+  stralloc greeting = {0};
+  filter_rule *rules;
+
+  argv++; if (!*argv) die_usage();
+
+  if (env_get("NOFIXSMTPIO")) unistd_execvp(*argv,argv);
+
+  stralloc_set_die(die_nomem);
+
+  cd_var_qmail();
+  load_smtp_greeting(&greeting,"control/smtpgreeting");
+  rules = load_filter_rules();
+
+  start_proxy(&greeting,rules,argv);
+}
diff --git fixsmtpio.h fixsmtpio.h
new file mode 100644
index 0000000..77bd0d6
--- /dev/null
+++ fixsmtpio.h
@@ -0,0 +1,31 @@
+#include "alloc.h"
+#include "auto_qmail.h"
+#include "case.h"
+#include "control.h"
+#include "env.h"
+#include "fd.h"
+#include "scan.h"
+#include "str.h"
+#include "stralloc.h"
+#include "substdio.h"
+#include "wait.h"
+
+#define HOMEPAGE                 "https://schmonz.com/qmail/acceptutils"
+#define PROGNAME                 "fixsmtpio"
+
+#define EVENT_GREETING           "greeting"
+#define EVENT_TIMEOUT            "timeout"
+#define EVENT_CLIENTEOF          "clienteof"
+#define MUNGE_INTERNALLY         "&" PROGNAME "_fixup"
+#define REQUEST_PASSTHRU         ""
+#define REQUEST_NOOP             "NOOP "
+
+#define RESPONSELINE_NOCHANGE    "&" PROGNAME "_noop"
+
+#define EXIT_LATER_NORMALLY      -1
+#define EXIT_NOW_SUCCESS         0
+#define EXIT_NOW_TEMPFAIL        14
+#define EXIT_NOW_PERMFAIL        15
+/* sync with authup.c */
+#define EXIT_NOW_TIMEOUT         16
+#define EXIT_NOW_PARSEFAIL       18
diff --git fixsmtpio_control.c fixsmtpio_control.c
new file mode 100644
index 0000000..3ec1b6c
--- /dev/null
+++ fixsmtpio_control.c
@@ -0,0 +1,73 @@
+#include "fixsmtpio_control.h"
+#include "fixsmtpio_munge.h"
+
+#include "acceptutils_stralloc.h"
+
+static void parse_field(int *fields_seen, stralloc *value, filter_rule *rule) {
+  char *s;
+
+  (*fields_seen)++;
+
+  if (!value->len) return;
+
+  append0(value);
+  s = (char *)alloc(value->len);
+  str_copy(s, value->s);
+  blank(value);
+
+  switch (*fields_seen) {
+    case 1: rule->env                = s; break;
+    case 2: rule->event              = s; break;
+    case 3: rule->request_prepend    = s; break;
+    case 4: rule->response_line_glob = s; break;
+    case 5:
+      if (!scan_ulong(s,&rule->exitcode))
+        rule->exitcode = 777;
+                                          break;
+    case 6: rule->response           = s; break;
+  }
+}
+
+filter_rule *parse_control_line(char *line) {
+  filter_rule *rule = (filter_rule *)alloc(sizeof(filter_rule));
+  int line_length = str_len(line);
+  stralloc value = {0};
+  int fields_seen = 0;
+  int i;
+
+  rule->next                = 0;
+
+  rule->env                 = 0;
+  rule->event               = 0;
+  rule->request_prepend     = 0;
+  rule->response_line_glob  = 0;
+  rule->exitcode            = EXIT_LATER_NORMALLY;
+  rule->response            = 0;
+
+  for (i = 0; i < line_length; i++) {
+    char c = line[i];
+    if (':' == c && fields_seen < 5) parse_field(&fields_seen, &value, rule);
+    else append(&value, &c);
+  }
+  parse_field(&fields_seen, &value, rule);
+
+  if (fields_seen < 6)            return 0;
+  if (!rule->event)               return 0;
+  if (!rule->response_line_glob)  return 0;
+  if ( rule->exitcode > 255)      return 0;
+  if ( rule->response) {
+    if (!case_diffs(rule->event,"clienteof"))
+                                  return 0;
+    if (want_munge_internally(rule->response)
+        && !munge_line_fn(rule->event))
+                                  return 0;
+    if (str_start(rule->response,"&fixsmtpio")
+        && !want_munge_internally(rule->response)
+        && !want_leave_line_as_is(rule->response))
+                                  return 0;
+  } else {
+    rule->response = "";
+  }
+
+  return rule;
+}
diff --git fixsmtpio_control.h fixsmtpio_control.h
new file mode 100644
index 0000000..ea821a7
--- /dev/null
+++ fixsmtpio_control.h
@@ -0,0 +1,3 @@
+#include "fixsmtpio_filter.h"
+
+filter_rule *parse_control_line(char *);
diff --git fixsmtpio_die.c fixsmtpio_die.c
new file mode 100644
index 0000000..c628c5e
--- /dev/null
+++ fixsmtpio_die.c
@@ -0,0 +1,63 @@
+#include "fixsmtpio.h"
+#include "fixsmtpio_die.h"
+#include "readwrite.h"
+
+#include "acceptutils_stralloc.h"
+#include "acceptutils_unistd.h"
+
+static void die() { unistd_exit(1); }
+
+static char sserrbuf[SUBSTDIO_OUTSIZE];
+substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf);
+
+static void errflush3(const char *caller,const char *alloc_fn,char *s) {
+  substdio_puts(&sserr,PROGNAME ":");
+  if (caller) {
+    substdio_puts(&sserr,caller);
+    substdio_puts(&sserr,":");
+  }
+  if (alloc_fn) {
+    substdio_puts(&sserr,alloc_fn);
+    substdio_puts(&sserr,":");
+  }
+  substdio_puts(&sserr," ");
+  substdio_puts(&sserr,s);
+  substdio_putsflush(&sserr,"\n");
+}
+
+static void errflush(char *s) {
+  errflush3(0,0,s);
+}
+
+static void dieerrflush(char *s) {
+  errflush(s);
+  die();
+}
+
+void die_usage() { dieerrflush("usage: " PROGNAME " prog [ arg ... ]"); }
+void die_control(){dieerrflush("unable to read controls"); }
+void die_pipe()  { dieerrflush("unable to open pipe"); }
+void die_fork()  { dieerrflush("unable to fork"); }
+void die_exec()  { dieerrflush("unable to exec"); }
+void die_wait()  { dieerrflush("unable to wait for child"); }
+void die_kill()  { dieerrflush("unable to kill child"); }
+void die_crash() { dieerrflush("aack, child crashed"); }
+void die_read()  { dieerrflush("unable to read"); }
+void die_write() { dieerrflush("unable to write"); }
+void die_nomem(const char *caller,const char *alloc_fn) {
+  errflush3(caller,alloc_fn,"out of memory");
+  die();
+}
+void die_tls()   { dieerrflush("TLS temporarily not available"); }
+void die_parse() {    errflush("unable to parse control/fixsmtpio");
+                      unistd_exit(EXIT_NOW_PARSEFAIL); }
+
+void logit(stralloc *logstamp,char logprefix,stralloc *sa) {
+  if (!env_get("FIXSMTPIODEBUG")) return;
+
+  substdio_put (&sserr,logstamp->s,logstamp->len);
+  substdio_put (&sserr,&logprefix,1); substdio_puts(&sserr,": ");
+  substdio_put (&sserr,sa->s,sa->len);
+  if (!ends_with_newline(sa)) substdio_puts(&sserr,"\n");
+  substdio_flush(&sserr);
+}
diff --git fixsmtpio_die.h fixsmtpio_die.h
new file mode 100644
index 0000000..6874ba7
--- /dev/null
+++ fixsmtpio_die.h
@@ -0,0 +1,16 @@
+#include "stralloc.h"
+
+void die_usage(void);
+void die_control(void);
+void die_pipe(void);
+void die_fork(void);
+void die_exec(void);
+void die_wait(void);
+void die_kill(void);
+void die_crash(void);
+void die_read(void);
+void die_write(void);
+void die_nomem(const char *,const char *);
+void die_tls(void);
+void die_parse(void);
+void logit(stralloc *,char,stralloc *);
diff --git fixsmtpio_eventq.c fixsmtpio_eventq.c
new file mode 100644
index 0000000..97ed628
--- /dev/null
+++ fixsmtpio_eventq.c
@@ -0,0 +1,61 @@
+#include "alloc.h"
+#include "str.h"
+#include "fixsmtpio.h"
+#include "fixsmtpio_die.h"
+
+#include "fixsmtpio_eventq.h"
+
+#define NULL 0
+#include <sys/queue.h>
+
+typedef struct node {
+  const char *event;
+  TAILQ_ENTRY(node) nodes;
+} node_t;
+
+typedef TAILQ_HEAD(head_s, node) head_t;
+
+static head_t head;
+static int eventq_inited = 0;
+
+static void eventq_init() {
+  if (eventq_inited) return;
+  TAILQ_INIT(&head);
+  eventq_inited++;
+}
+
+static node_t *eventq_alloc_node() {
+  node_t *e = (node_t *)alloc(sizeof(node_t));
+  if (!e) die_nomem(__func__,"alloc");
+  return e;
+}
+
+static char *eventq_alloc_event(const char *event) {
+  char *s = (char *)alloc(sizeof(char) * (1 + str_len(event)));
+  if (!s) die_nomem(__func__,"alloc");
+  return s;
+}
+
+void eventq_put(const char *event) {
+  node_t *e;
+  eventq_init();
+  e = eventq_alloc_node();
+  e->event = event;
+  TAILQ_INSERT_TAIL(&head, e, nodes);
+}
+
+char *eventq_get() {
+  char *event;
+  node_t *e;
+  if (TAILQ_EMPTY(&head)) {
+    event = EVENT_TIMEOUT;
+  } else {
+    e = TAILQ_FIRST(&head);
+    event = eventq_alloc_event(e->event);
+    str_copy(event,e->event);
+    TAILQ_REMOVE(&head, e, nodes);
+    alloc_free(e);
+  }
+
+  return event;
+}
diff --git fixsmtpio_eventq.h fixsmtpio_eventq.h
new file mode 100644
index 0000000..7597fbc
--- /dev/null
+++ fixsmtpio_eventq.h
@@ -0,0 +1,2 @@
+void eventq_put(const char *);
+char *eventq_get(void);
diff --git fixsmtpio_filter.c fixsmtpio_filter.c
new file mode 100644
index 0000000..47591a2
--- /dev/null
+++ fixsmtpio_filter.c
@@ -0,0 +1,113 @@
+#include "fixsmtpio.h"
+#include "fixsmtpio_control.h"
+#include "fixsmtpio_filter.h"
+#include "fixsmtpio_die.h"
+#include "fixsmtpio_munge.h"
+#include "fixsmtpio_glob.h"
+
+#include "acceptutils_stralloc.h"
+
+int want_munge_internally(char *response) {
+  return !str_diff(MUNGE_INTERNALLY,response);
+}
+
+int want_leave_line_as_is(char *response) {
+  return !str_diff(RESPONSELINE_NOCHANGE,response);
+}
+
+int envvar_exists_if_needed(char *envvar) {
+  if (envvar) {
+    if (!str_diff("",envvar)) return 1;
+    if (env_get(envvar)) return 1;
+    return 0;
+  }
+  return 1;
+}
+
+int filter_rule_applies(filter_rule *rule,const char *event) {
+  return (event_matches(rule->event,event) && envvar_exists_if_needed(rule->env));
+}
+
+void munge_response_line(int lineno,
+                         stralloc *line,int *exitcode,
+                         stralloc *greeting,filter_rule *rules,const char *event,
+                         int tls_level,int in_tls) {
+  filter_rule *rule;
+  stralloc line0 = {0};
+
+  copy(&line0,line);
+  append0(&line0);
+
+  for (rule = rules; rule; rule = rule->next) {
+    if (!filter_rule_applies(rule,event)) continue;
+    if (!string_matches_glob(rule->response_line_glob,line0.s)) continue;
+    munge_exitcode(exitcode,rule);
+    if (want_munge_internally(rule->response))
+      munge_line_internally(line,lineno,greeting,event,tls_level,in_tls);
+    else if (!want_leave_line_as_is(rule->response))
+      copys(line,rule->response);
+  }
+  if (line->len) if (!ends_with_newline(line)) cats(line,"\r\n");
+}
+
+void munge_response(stralloc *response,int *exitcode,
+                    stralloc *greeting,filter_rule *rules,const char *event,
+                    int tls_level,int in_tls) {
+  stralloc munged = {0};
+  stralloc line = {0};
+  int lineno = 0;
+  int i;
+
+  for (i = 0; i < response->len; i++) {
+    append(&line,i + response->s);
+    if (response->s[i] == '\n' || i == response->len - 1) {
+      munge_response_line(lineno++,&line,exitcode,greeting,rules,event,tls_level,in_tls);
+      cat(&munged,&line);
+      blank(&line);
+    }
+  }
+
+  if (munged.len) reformat_multiline_response(&munged);
+  copy(response,&munged);
+}
+
+filter_rule *prepend_rule(filter_rule *next, filter_rule *rule) {
+  rule->next = next;
+  next = rule;
+  return next;
+}
+
+filter_rule *reverse_rules(filter_rule *rules) {
+  filter_rule *reversed_rules = 0;
+  filter_rule *temp;
+
+  while (rules) {
+    temp = rules;
+    rules = rules->next;
+    temp->next = reversed_rules;
+    reversed_rules = temp;
+  }
+
+  return reversed_rules;
+}
+
+filter_rule *load_filter_rules(void) {
+  filter_rule *backwards_rules = 0;
+
+  stralloc lines = {0};
+  int linestart;
+  int pos;
+
+  if (control_readfile(&lines,"control/fixsmtpio",0) == -1) die_control();
+
+  for (linestart = 0, pos = 0; pos < lines.len; pos++) {
+    if (lines.s[pos] == '\0') {
+      filter_rule *rule = parse_control_line(lines.s + linestart);
+      if (0 == rule) die_parse();
+      backwards_rules = prepend_rule(backwards_rules, rule);
+      linestart = pos + 1;
+    }
+  }
+
+  return reverse_rules(backwards_rules);
+}
diff --git fixsmtpio_filter.h fixsmtpio_filter.h
new file mode 100644
index 0000000..7a0b20b
--- /dev/null
+++ fixsmtpio_filter.h
@@ -0,0 +1,29 @@
+#ifndef _FIXSMTPIO_FILTER_H_
+#define _FIXSMTPIO_FILTER_H_
+
+#include "fixsmtpio.h"
+
+typedef struct filter_rule {
+  struct filter_rule *next;
+  char *env;
+  char *event;
+  char *request_prepend;
+  char *response_line_glob;
+  int   exitcode;
+  char *response;
+} filter_rule;
+
+int want_munge_internally(char *);
+int want_leave_line_as_is(char *);
+int envvar_exists_if_needed(char *);
+
+filter_rule * load_filter_rules(void);
+int filter_rule_applies(filter_rule *,const char *);
+
+int event_matches(char *,const char *);
+
+void munge_response(stralloc *,int *,stralloc *,filter_rule *,const char *,int,int);
+void munge_response_line(int,stralloc *,int *,stralloc *,filter_rule *,const char *,int,int);
+filter_rule *prepend_rule(filter_rule *,filter_rule *);
+
+#endif
diff --git fixsmtpio_glob.c fixsmtpio_glob.c
new file mode 100644
index 0000000..5b2c370
--- /dev/null
+++ fixsmtpio_glob.c
@@ -0,0 +1,8 @@
+#include "fixsmtpio_glob.h"
+
+/*
+ maybe this should be regex instead of glob?
+ */
+int string_matches_glob(char *glob,char *string) {
+  return 0 == fnmatch(glob,string,0);
+}
diff --git fixsmtpio_glob.h fixsmtpio_glob.h
new file mode 100644
index 0000000..98ed86b
--- /dev/null
+++ fixsmtpio_glob.h
@@ -0,0 +1,3 @@
+#include <fnmatch.h>
+
+int string_matches_glob(char *,char *);
diff --git fixsmtpio_munge.c fixsmtpio_munge.c
new file mode 100644
index 0000000..94ad0ce
--- /dev/null
+++ fixsmtpio_munge.c
@@ -0,0 +1,116 @@
+#include "fixsmtpio_munge.h"
+
+#include "acceptutils_stralloc.h"
+#include "acceptutils_ucspitls.h"
+
+void munge_exitcode(int *exitcode,filter_rule *rule) {
+  if (rule->exitcode != EXIT_LATER_NORMALLY) *exitcode = rule->exitcode;
+}
+
+void munge_greeting(stralloc *response,int lineno,stralloc *greeting,
+                    int tls_level,int in_tls) {
+  copys(response,"220 "); cat(response,greeting); cats(response," ESMTP");
+}
+
+void munge_helo(stralloc *response,int lineno,stralloc *greeting,
+                int tls_level,int in_tls) {
+  copys(response,"250 "); cat(response,greeting);
+}
+
+static int is_starttls_line(stralloc *response) {
+  return starts(response,"250-STARTTLS\r\n")
+      || starts(response,"250 STARTTLS\r\n");
+}
+
+void munge_ehlo(stralloc *response,int lineno,stralloc *greeting,
+                int tls_level,int in_tls) {
+  switch (lineno) {
+    case 0:
+      munge_helo(response,lineno,greeting,tls_level,in_tls);
+      break;
+    case 1:
+      if (is_starttls_line(response)) blank(response);
+      if (tls_level >= UCSPITLS_AVAILABLE && !in_tls && !env_get("AUTHUP_USER"))
+        prepends(response,"250-STARTTLS\r\n");
+      break;
+    default:
+      if (is_starttls_line(response)) blank(response);
+      break;
+  }
+}
+
+void munge_help(stralloc *response,int lineno,stralloc *greeting,
+                int tls_level,int in_tls) {
+  stralloc munged = {0};
+  copys(&munged,"214 " PROGNAME " home page: " HOMEPAGE "\r\n");
+  cat(&munged,response);
+  copy(response,&munged);
+}
+
+void munge_quit(stralloc *response,int lineno,stralloc *greeting,
+                int tls_level,int in_tls) {
+  copys(response,"221 "); cat(response,greeting);
+}
+
+void change_every_line_fourth_char_to_dash(stralloc *multiline) {
+  int pos = 0;
+  int i;
+  for (i = 0; i < multiline->len; i++) {
+    if (multiline->s[i] == '\n') pos = -1;
+    if (pos == 3) multiline->s[i] = '-';
+    pos++;
+  }
+}
+
+// copy to with authup.c:smtp_ehlo_format()
+void change_last_line_fourth_char_to_space(stralloc *multiline) {
+  int pos = 0;
+  int i;
+  for (i = multiline->len - 2; i >= 0; i--) {
+    if (multiline->s[i] == '\n') {
+      pos = i + 1;
+      break;
+    }
+  }
+  multiline->s[pos+3] = ' ';
+}
+
+void reformat_multiline_response(stralloc *response) {
+  change_every_line_fourth_char_to_dash(response);
+  change_last_line_fourth_char_to_space(response);
+}
+
+int event_matches(char *s,const char *s2) {
+  if (!s || !s2) return 0;
+  if (!str_len(s) || !str_len(s2)) return 0;
+  return !case_diffs(s,s2);
+}
+
+struct munge_command {
+  char *event;
+  void (*munger)();
+};
+
+struct munge_command m[] = {
+  { EVENT_GREETING, munge_greeting }
+, { "ehlo", munge_ehlo }
+, { "helo", munge_helo }
+, { "help", munge_help }
+, { "quit", munge_quit }
+, { 0, 0 }
+};
+
+void *munge_line_fn(const char *event) {
+  int i;
+  for (i = 0; m[i].event; ++i)
+    if (event_matches(m[i].event,event))
+      return m[i].munger;
+  return 0;
+}
+
+void munge_line_internally(stralloc *line,int lineno,
+                           stralloc *greeting,const char *event,
+                           int tls_level,int in_tls) {
+  void (*munger)() = munge_line_fn(event);
+  if (munger) munger(line,lineno,greeting,tls_level,in_tls);
+}
diff --git fixsmtpio_munge.h fixsmtpio_munge.h
new file mode 100644
index 0000000..511e080
--- /dev/null
+++ fixsmtpio_munge.h
@@ -0,0 +1,14 @@
+#include "fixsmtpio_filter.h"
+
+void munge_exitcode(int *,filter_rule *);
+void munge_greeting(stralloc *,int,stralloc *,int,int);
+void munge_helo(stralloc *,int,stralloc *,int,int);
+void munge_ehlo(stralloc *,int,stralloc *,int,int);
+void munge_help(stralloc *,int,stralloc *,int,int);
+void munge_quit(stralloc *,int,stralloc *,int,int);
+void reformat_multiline_response(stralloc *);
+int event_matches(char *,const char *);
+void *munge_line_fn(const char *);
+void munge_line_internally(stralloc *,int,stralloc *,const char *,int,int);
+void change_every_line_fourth_char_to_dash(stralloc *);
+void change_last_line_fourth_char_to_space(stralloc *);
diff --git fixsmtpio_proxy.c fixsmtpio_proxy.c
new file mode 100644
index 0000000..e910ec5
--- /dev/null
+++ fixsmtpio_proxy.c
@@ -0,0 +1,423 @@
+#include <libgen.h>
+#include <signal.h>
+
+#include "fmt.h"
+
+#include "fixsmtpio_proxy.h"
+#include "fixsmtpio_readwrite.h"
+#include "fixsmtpio_die.h"
+#include "fixsmtpio_eventq.h"
+#include "fixsmtpio_filter.h"
+
+#include "acceptutils_stralloc.h"
+#include "acceptutils_ucspitls.h"
+#include "acceptutils_unistd.h"
+
+int ends_data(stralloc *r) {
+  int len = r->len;
+
+  if (!len                      ) return 0;
+  if (       r->s[--len] != '\n') return 0;
+  if (len && r->s[--len] != '\r') ++len;
+  if (!len                      ) return 0;
+  if (       r->s[--len] !=  '.') return 0;
+  if (len && r->s[--len] != '\n') return 0;
+
+  return 1;
+}
+
+static int find_first_space(stralloc *request) {
+  int i;
+  for (i = 0; i < request->len; i++) if (request->s[i] == ' ') return i;
+  return -1;
+}
+
+void strip_last_eol(stralloc *sa) {
+  if (sa->len > 0 && sa->s[sa->len-1] == '\n') sa->len--;
+  if (sa->len > 0 && sa->s[sa->len-1] == '\r') sa->len--;
+}
+
+static void all_verb_no_arg(stralloc *verb,stralloc *arg,stralloc *request) {
+  copy(verb,request);
+  strip_last_eol(verb);
+  blank(arg);
+}
+
+static void verb_and_arg(stralloc *verb,stralloc *arg,int pos,stralloc *request) {
+  copyb(verb,request->s,pos-1);
+  copyb(arg,request->s+pos,request->len-pos);
+  strip_last_eol(arg);
+}
+
+void parse_client_request(stralloc *verb,stralloc *arg,stralloc *request) {
+  int pos;
+  pos = find_first_space(request);
+  if (pos == -1)
+    all_verb_no_arg(verb,arg,request);
+  else
+    verb_and_arg(verb,arg,++pos,request);
+}
+
+static int need_starttls_first(int tls_level,int in_tls,char *event) {
+  return tls_level >= UCSPITLS_REQUIRED
+    && !in_tls
+    && !event_matches(EVENT_GREETING,event)
+    && !event_matches(EVENT_TIMEOUT,event)
+    && !event_matches(EVENT_CLIENTEOF,event)
+    && !event_matches("noop",event)
+    && !event_matches("ehlo",event)
+    && !event_matches("starttls",event)
+    && !event_matches("quit",event);
+}
+
+void construct_proxy_request(stralloc *proxy_request,
+                             filter_rule *rules,
+                             char *event,stralloc *arg,
+                             stralloc *client_request,
+                             int tls_level,
+                             int *want_tls,int in_tls,
+                             int *want_data) {
+  filter_rule *rule;
+
+  for (rule = rules; rule; rule = rule->next)
+    if (rule->request_prepend && filter_rule_applies(rule,event))
+      prepends(proxy_request,rule->request_prepend);
+  if (need_starttls_first(tls_level,in_tls,event))
+    prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS FIRST ");
+  else if (event_matches("starttls",event)) {
+    *want_tls = 1;
+    if (tls_level >= UCSPITLS_AVAILABLE) {
+      if (in_tls)
+        prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS AGAIN ");
+      else
+        prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS BEGIN ");
+    } else {
+      prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS BLOCK ");
+    }
+  }
+  else if (event_matches("data",event))
+    *want_data = 1;
+  cat(proxy_request,client_request);
+}
+
+static int accepted_data(stralloc *response) { return starts(response,"354 "); }
+
+void construct_proxy_response(stralloc *proxy_response,
+                              stralloc *greeting,
+                              filter_rule *rules,
+                              char *event,
+                              stralloc *server_response,
+                              int *proxy_exitcode,
+                              int tls_level,
+                              int want_tls,int in_tls,
+                              int *want_data,int *in_data) {
+  if (event_matches("data",event) && *want_data) {
+    *want_data = 0;
+    if (accepted_data(server_response)) {
+      eventq_put("in_data");
+      *in_data = 1;
+    }
+  }
+
+  if (event_matches("starttls",event) && want_tls) {
+    if (tls_level < UCSPITLS_AVAILABLE || in_tls)
+      copys(proxy_response,"502 unimplemented (#5.5.1)\r\n");
+    else
+      copys(proxy_response,"220 Ready to start TLS (#5.7.0)\r\n");
+    return;
+  }
+  if (need_starttls_first(tls_level,in_tls,event))
+    copys(proxy_response,"530 Must start TLS first (#5.7.0)\r\n");
+  else
+    copy(proxy_response,server_response);
+    munge_response(proxy_response,proxy_exitcode,greeting,rules,event,tls_level,in_tls);
+}
+
+int get_one(const char *caller,stralloc *one,stralloc *pile,int (*fn)(stralloc *)) {
+  stralloc caller_sa = {0};
+  int got_one = 0;
+  stralloc next_pile = {0};
+  int pos = 0;
+  int i;
+
+  for (i = pos; i < pile->len; i++) {
+    if (pile->s[i] == '\n') {
+      stralloc line = {0};
+
+      catb(&line,pile->s+pos,i+1-pos);
+      pos = i+1;
+      cat(one,&line);
+
+      if (!fn || fn(&line)) {
+        got_one = 1;
+        break;
+      }
+    }
+  }
+
+  if (got_one) {
+    copys(&caller_sa,(char *)caller);
+    cats(&caller_sa,":");
+    cats(&caller_sa,(char *)__func__);
+    append0(&caller_sa);
+
+    contextlogging_copyb(caller_sa.s,&next_pile,pile->s+pos,pile->len-pos);
+    contextlogging_copy(caller_sa.s,pile,&next_pile);
+    blank(&next_pile);
+
+    blank(&caller_sa);
+  } else {
+    blank(one);
+  }
+
+  return got_one;
+}
+
+int get_one_request(stralloc *one,stralloc *pile) {
+  return get_one(__func__,one,pile,0);
+}
+
+int is_last_line_of_response(stralloc *line) {
+  return line->len >= 4 && line->s[3] == ' ';
+}
+
+int get_one_response(stralloc *one,stralloc *pile) {
+  return get_one(__func__,one,pile,&is_last_line_of_response);
+}
+
+static void handle_data_specially(stralloc *data,int *in_data,stralloc *logstamp) {
+  logit(logstamp,'D',data);
+  if (ends_data(data))
+    *in_data = 0;
+}
+
+static void handle_request(stralloc *proxy_request,stralloc *request,int tls_level,int *want_tls,int in_tls,int *want_data,filter_rule *rules,stralloc *logstamp) {
+  stralloc event = {0}, verb = {0}, arg = {0};
+
+  logit(logstamp,'1',request);
+  parse_client_request(&verb,&arg,request);
+  copy(&event,&verb);
+  append0(&event);
+  eventq_put(event.s);
+  construct_proxy_request(proxy_request,rules,
+                          event.s,&arg,
+                          request,
+                          tls_level,
+                          want_tls,in_tls,
+                          want_data);
+  blank(request);
+  logit(logstamp,'2',proxy_request);
+}
+
+static void handle_response(stralloc *proxy_response,int *exitcode,stralloc *response,int tls_level,int want_tls,int in_tls,int *want_data,int *in_data,filter_rule *rules,stralloc *greeting,stralloc *logstamp) {
+  char *event;
+  logit(logstamp,'3',response);
+  event = eventq_get();
+  construct_proxy_response(proxy_response,
+                           greeting,rules,event,
+                           response,
+                           exitcode,
+                           tls_level,
+                           want_tls,in_tls,
+                           want_data,in_data);
+  logit(logstamp,'4',proxy_response);
+  alloc_free(event);
+  blank(response);
+}
+
+static void use_as_stdin(int fd)  { if (fd_move(0,fd) == -1) die_pipe(); }
+static void use_as_stdout(int fd) { if (fd_move(1,fd) == -1) die_pipe(); }
+
+static void make_pipe(int *from,int *to) {
+  int pi[2];
+  if (unistd_pipe(pi) == -1) die_pipe();
+  *from = pi[0];
+  *to = pi[1];
+}
+
+static void be_proxied(int from_proxy,int to_proxy,
+                       int from_proxied,int to_proxied,
+                       char **argv) {
+  unistd_close(from_proxied);
+  unistd_close(to_proxied);
+  use_as_stdin(from_proxy);
+  use_as_stdout(to_proxy);
+  unistd_execvp(*argv,argv);
+  die_exec();
+}
+
+static void teardown_and_exit(int exitcode,int child,filter_rule *rules,
+                              int from_server,int to_server) {
+  int wstat;
+
+  unistd_close(from_server);
+  unistd_close(to_server);
+
+  if (wait_pid(&wstat,child) == -1) die_wait();
+  if (wait_crashed(wstat)) die_crash();
+
+  if (exitcode == EXIT_LATER_NORMALLY) unistd_exit(wait_exitcode(wstat));
+  else unistd_exit(exitcode);
+}
+
+static char *format_pid(unsigned int pid) {
+  char pidbuf[FMT_ULONG];
+  stralloc sa = {0};
+  if (!sa.len) {
+    int len = fmt_ulong(pidbuf,pid);
+    if (len) copyb(&sa,pidbuf,len);
+    append0(&sa);
+  }
+  return sa.s;
+}
+
+static void prepare_logstamp(stralloc *sa,int kid_pid,char *kid_name) {
+  copys(sa,PROGNAME " ");
+  cats(sa,format_pid(unistd_getpid())); cats(sa," ");
+  cats(sa,kid_name);                    cats(sa," ");
+  cats(sa,format_pid(kid_pid));         cats(sa," ");
+}
+
+static void adjust_proxy_for_new_kid(int from_proxy,int to_proxy,
+                                     stralloc *logstamp,
+                                     int kid_pid,char *kid_name) {
+  unistd_close(from_proxy);
+  unistd_close(to_proxy);
+  prepare_logstamp(logstamp,kid_pid,kid_name);
+  eventq_put(EVENT_GREETING);
+}
+
+static void be_proxy(int from_client,int to_client,
+                     int from_proxy,int to_proxy,
+                     int from_server,int to_server,
+                     stralloc *greeting,filter_rule *rules,
+                     int kid_pid,char **argv) {
+  int exitcode;
+  stralloc logstamp = {0};
+
+  adjust_proxy_for_new_kid(from_proxy,to_proxy,
+                           &logstamp,
+                           kid_pid,basename(argv[0]));
+
+  exitcode = read_and_process_until_either_end_closes(from_client,to_server,
+                                                      from_server,to_client,
+                                                      greeting,rules,
+                                                      &logstamp,
+                                                      &kid_pid,argv);
+  teardown_and_exit(exitcode,kid_pid,rules,from_server,to_server);
+}
+
+static void stop_kid(int kid_pid,int from_server,int to_server) {
+  unistd_close(from_server);
+  unistd_close(to_server);
+  if (-1 == kill(kid_pid,SIGTERM)) die_kill();
+}
+
+static void start_kid(int *kid_pid,int *from_server,int *to_server,
+                      stralloc *logstamp,char **argv) {
+  int from_proxy, to_proxy;
+
+  make_pipe(&from_proxy,to_server);
+  make_pipe(from_server,&to_proxy);
+
+  if ((*kid_pid = unistd_fork()))
+    adjust_proxy_for_new_kid(from_proxy,to_proxy,
+                             logstamp,
+                             *kid_pid,basename(argv[0]));
+  else if (*kid_pid == 0)
+    be_proxied(from_proxy,to_proxy,
+               *from_server,*to_server,
+               argv);
+  else
+    die_fork();
+}
+
+void start_proxy(stralloc *greeting,filter_rule *rules,char **argv) {
+  int from_client = 0;
+  int from_proxy, to_server;
+  int from_server, to_proxy;
+  int to_client = 1;
+  int kid_pid;
+
+  make_pipe(&from_proxy,&to_server);
+  make_pipe(&from_server,&to_proxy);
+
+  if ((kid_pid = unistd_fork()))
+    be_proxy(from_client,to_client,
+             from_proxy,to_proxy,
+             from_server,to_server,
+             greeting,rules,
+             kid_pid,argv);
+  else if (kid_pid == 0)
+    be_proxied(from_proxy,to_proxy,
+               from_server,to_server,
+               argv);
+  else
+    die_fork();
+}
+
+int read_and_process_until_either_end_closes(int from_client,int to_server,
+                                             int from_server,int to_client,
+                                             stralloc *greeting,
+                                             filter_rule *rules,
+                                             stralloc *logstamp,
+                                             int *kid_pid,char **argv) {
+  char buf[SUBSTDIO_INSIZE];
+
+  int      exitcode         = EXIT_LATER_NORMALLY;
+
+  int      tls_level        = ucspitls_level(),
+           want_tls         =  0,
+           in_tls           =  0,
+           want_data        =  0,
+           in_data          =  0;
+
+  stralloc client_requests  = {0},
+           one_request      = {0},
+           proxy_request    = {0},
+           server_responses = {0},
+           one_response     = {0},
+           proxy_response   = {0};
+
+  for (;;) {
+    if (!block_efficiently_until_can_read_either(from_client,from_server)) break;
+
+    if (can_read(from_client)) {
+      if (!safeappend(&client_requests,from_client,buf,sizeof buf)) {
+        munge_response_line(0,&client_requests,&exitcode,greeting,rules,EVENT_CLIENTEOF,tls_level,in_tls);
+        break;
+      }
+      while (client_requests.len) {
+        if (in_data) {
+          handle_data_specially(&client_requests,&in_data,logstamp);
+          safewrite(to_server,&client_requests);
+        } else if (get_one_request(&one_request,&client_requests)) {
+          handle_request(&proxy_request,&one_request,tls_level,&want_tls,in_tls,&want_data,rules,logstamp);
+          safewrite(to_server,&proxy_request);
+        }
+      }
+    }
+
+    if (can_read(from_server)) {
+      if (!safeappend(&server_responses,from_server,buf,sizeof buf)) break;
+      while (server_responses.len && exitcode == EXIT_LATER_NORMALLY && get_one_response(&one_response,&server_responses)) {
+        handle_response(&proxy_response,&exitcode,&one_response,tls_level,want_tls,in_tls,&want_data,&in_data,rules,greeting,logstamp);
+        safewrite(to_client,&proxy_response);
+        if (want_tls) {
+          want_tls = 0;
+          if (tls_level >= UCSPITLS_AVAILABLE && !in_tls) {
+            if (!tls_init() || !tls_info(die_nomem)) die_tls();
+            if (!env_put("FIXSMTPIOTLS=1")) die_nomem(__func__,"env_put");
+            stop_kid(*kid_pid,from_server,to_server);
+            start_kid(kid_pid,&from_server,&to_server,logstamp,argv);
+            in_tls = 1;
+          }
+        }
+      }
+    }
+
+    if (exitcode != EXIT_LATER_NORMALLY) break;
+  }
+
+  return exitcode;
+}
diff --git fixsmtpio_proxy.h fixsmtpio_proxy.h
new file mode 100644
index 0000000..9a7d6a1
--- /dev/null
+++ fixsmtpio_proxy.h
@@ -0,0 +1,11 @@
+#include "fixsmtpio.h"
+#include "fixsmtpio_filter.h"
+
+void strip_last_eol(stralloc *);
+int ends_data(stralloc *);
+int is_last_line_of_response(stralloc *);
+void parse_client_request(stralloc *,stralloc *,stralloc *);
+int get_one_response(stralloc *,stralloc *);
+int read_and_process_until_either_end_closes(int,int,int,int,stralloc *,filter_rule *,stralloc *,int *,char **);
+void construct_proxy_request(stralloc *,filter_rule *,char *,stralloc *,stralloc *,int,int *,int,int *);
+void start_proxy(stralloc *,filter_rule *,char **);
diff --git fixsmtpio_readwrite.c fixsmtpio_readwrite.c
new file mode 100644
index 0000000..42ffa9f
--- /dev/null
+++ fixsmtpio_readwrite.c
@@ -0,0 +1,46 @@
+#include "fixsmtpio_readwrite.h"
+#include "fixsmtpio_die.h"
+#include "error.h"
+#include "readwrite.h"
+#include "select.h"
+
+#include "acceptutils_stralloc.h"
+
+fd_set fds;
+
+static void want_to_read(int fd1,int fd2) {
+  FD_ZERO(&fds);
+  FD_SET(fd1,&fds);
+  FD_SET(fd2,&fds);
+}
+
+int can_read(int fd) { return FD_ISSET(fd,&fds); }
+
+static int max(int a,int b) { return a > b ? a : b; }
+
+int block_efficiently_until_can_read_either(int fd1,int fd2) {
+  int ready;
+  want_to_read(fd1,fd2);
+  ready = select(1+max(fd1,fd2),&fds,(fd_set *)0,(fd_set *)0,(struct timeval *) 0);
+  if (ready == -1 && errno != error_intr) die_read();
+  return ready;
+}
+
+static int saferead(int fd,char *buf,int len) {
+  int r;
+  r = read(fd,buf,len);
+  if (r == -1) if (errno != error_intr) die_read();
+  return r;
+}
+
+int safeappend(stralloc *sa,int fd,char *buf,int len) {
+  int r;
+  r = saferead(fd,buf,len);
+  catb(sa,buf,r);
+  return r;
+}
+
+void safewrite(int fd,stralloc *sa) {
+  if (write(fd,sa->s,sa->len) == -1) die_write();
+  blank(sa);
+}
diff --git fixsmtpio_readwrite.h fixsmtpio_readwrite.h
new file mode 100644
index 0000000..b1d77b9
--- /dev/null
+++ fixsmtpio_readwrite.h
@@ -0,0 +1,6 @@
+#include "stralloc.h"
+
+int can_read(int);
+int block_efficiently_until_can_read_either(int,int);
+int safeappend(stralloc *,int,char *,int);
+void safewrite(int,stralloc *);
diff --git make-compile99.sh make-compile99.sh
new file mode 100644
index 0000000..5699fcf
--- /dev/null
+++ make-compile99.sh
@@ -0,0 +1 @@
+echo exec "$CC" -std=c99 -c '${1+"$@"}'
diff --git reup.8 reup.8
new file mode 100644
index 0000000..8176c3c
--- /dev/null
+++ reup.8
@@ -0,0 +1,58 @@
+.TH REUP 8 2018-11-06
+.SH NAME
+reup \- run a program repeatedly
+.SH SYNOPSIS
+.B reup
+[
+.B \-t \fItries
+]
+.I prog
+.SH DESCRIPTION
+.B reup
+is typically used to invoke
+.B authup
+so that a user is given more than one authentication attempt before being disconnected.
+
+.B reup
+prints an error message if
+.I prog
+crashes or can't be started.
+.SH "EXIT CODES"
+.B reup
+stops retrying as soon as
+
+-
+.I prog
+exits 0 (success), or
+
+-
+.I prog
+exits 12 (permanent failure), or
+
+-
+.B reup
+reaches its retry limit.
+
+.B reup
+then exits with the same code as
+.IR prog .
+.SH "ENVIRONMENT VARIABLES"
+Child processes can see how many tries have been made in
+.BR REUP .
+.SH "OPTIONS"
+.TP
+.B \-t \fItries
+Run
+.B prog
+no more than
+.I tries
+times.
+Default: 0 (unlimited).
+.SH "EXAMPLES"
+See
+.IR https://schmonz.com/qmail/acceptutils .
+.SH "AUTHOR"
+.B Amitai Schleier <schmonz-web-acceptutils@schmonz.com>
+.SH "SEE ALSO"
+authup(8),
+fixsmtpio(8).
diff --git reup.c reup.c
new file mode 100644
index 0000000..cc0dc6a
--- /dev/null
+++ reup.c
@@ -0,0 +1,112 @@
+#include "env.h"
+#include "fmt.h"
+#include "readwrite.h"
+#include "scan.h"
+#include "sgetopt.h"
+#include "str.h"
+#include "substdio.h"
+#include "wait.h"
+
+#include "acceptutils_unistd.h"
+
+#define PROGNAME "reup"
+
+void die() { unistd_exit(1); }
+
+char sserrbuf[SUBSTDIO_OUTSIZE];
+substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf);
+
+void errflush(char *s) {
+  substdio_puts(&sserr,PROGNAME ": ");
+  substdio_puts(&sserr,s);
+  substdio_putsflush(&sserr,"\n");
+}
+
+void dieerrflush(char *s) {
+  errflush(s);
+  die();
+}
+
+void die_usage() { dieerrflush("usage: " PROGNAME " [ -t tries ] prog"); }
+void die_fork()  { dieerrflush("unable to fork"); }
+void die_nomem() { dieerrflush("out of memory"); }
+
+void logtry(char *try,char *progname) {
+  substdio_puts(&sserr,PROGNAME ": try ");
+  substdio_puts(&sserr,try);
+  substdio_puts(&sserr,": ");
+  substdio_puts(&sserr,progname);
+  substdio_puts(&sserr,"\n");
+  substdio_flush(&sserr);
+}
+
+int try(int attempt,char **childargs) {
+  int child;
+  int wstat;
+  char reup[FMT_ULONG];
+
+  switch ((child = unistd_fork())) {
+    case -1:
+      die_fork();
+    case 0:
+      str_copy(reup + fmt_ulong(reup,attempt),"");
+      if (!env_put2("REUP",reup)) die_nomem();
+      logtry(reup,*childargs);
+      unistd_execvp(*childargs,childargs);
+      die(); // log something I guess
+  }
+
+  if (wait_pid(&wstat,child) == -1) die(); //die why? log something
+  if (wait_crashed(wstat)) die(); //die why? log something
+
+  return wait_exitcode(wstat);
+}
+
+int keep_trying(int attempt,int max) {
+  if (max == 0) return 1;
+  if (attempt <= max) return 1;
+  return 0;
+}
+
+int stop_trying(int exitcode) {
+  switch (exitcode) {
+    case 0:
+      errflush("success");
+      return 1;
+    case 12:
+      errflush("permanent failure");
+      return 1;
+    default:
+      return 0;
+  }
+}
+
+int main(int argc,char **argv) {
+  int exitcode;
+  int opt;
+  int tries;
+  int i;
+
+  tries = 0;
+  while ((opt = getopt(argc,argv,"t:")) != opteof) {
+    switch (opt) {
+      case 't':
+        if (!scan_ulong(optarg,&tries)) die_usage();
+        break;
+      default:
+        die_usage();
+    }
+  }
+  argc -= optind;
+  argv += optind;
+
+  if (!*argv) die_usage();
+
+  for (i = 1; keep_trying(i,tries); i++) {
+    exitcode = try(i,argv);
+    if (stop_trying(exitcode)) unistd_exit(exitcode);
+  }
+
+  errflush("no more tries");
+  unistd_exit(exitcode);
+}
diff --git test_acceptutils_stralloc.c test_acceptutils_stralloc.c
new file mode 100644
index 0000000..6f1f8d9
--- /dev/null
+++ test_acceptutils_stralloc.c
@@ -0,0 +1,31 @@
+#include "check.h"
+
+#include "acceptutils_stralloc.h"
+
+void assert_prepends(const char *input, char *prepend, const char *expected_output) {
+  stralloc sa = {0}; stralloc_copys(&sa, input);
+
+  prepends(&sa, prepend);
+
+  stralloc_0(&sa);
+  ck_assert_str_eq(sa.s, expected_output);
+}
+
+START_TEST (test_prepends)
+{
+  assert_prepends("", "", "");
+  assert_prepends("", "foo", "foo");
+  assert_prepends("bar", "", "bar");
+  assert_prepends("baz", "foo bar", "foo barbaz");
+  assert_prepends("baz quux", "foo bar ", "foo bar baz quux");
+  assert_prepends(" baz quux", "foo bar", "foo bar baz quux");
+}
+END_TEST
+
+TCase *tc_stralloc(void) {
+  TCase *tc = tcase_create("");
+
+  tcase_add_test(tc, test_prepends);
+
+  return tc;
+}
diff --git test_fixsmtpio.c test_fixsmtpio.c
new file mode 100644
index 0000000..68bea62
--- /dev/null
+++ test_fixsmtpio.c
@@ -0,0 +1,41 @@
+#include "check.h"
+#include <stdlib.h>
+
+extern TCase *tc_stralloc(void);
+extern TCase *tc_control(void);
+extern TCase *tc_eventq(void);
+extern TCase *tc_filter(void);
+extern TCase *tc_glob(void);
+extern TCase *tc_munge(void);
+extern TCase *tc_proxy(void);
+
+Suite * fixsmtpio_suite(void)
+{
+  Suite *s = suite_create("fixsmtpio");
+
+  suite_add_tcase(s, tc_stralloc());
+  suite_add_tcase(s, tc_control());
+  suite_add_tcase(s, tc_eventq());
+  suite_add_tcase(s, tc_filter());
+  suite_add_tcase(s, tc_glob());
+  suite_add_tcase(s, tc_munge());
+  suite_add_tcase(s, tc_proxy());
+
+  return s;
+}
+
+int main(void)
+{
+  int number_failed;
+  Suite *s;
+  SRunner *sr;
+
+  s = fixsmtpio_suite();
+  sr = srunner_create(s);
+
+  srunner_set_tap(sr, "-");
+  srunner_run_all(sr, CK_SILENT);
+  number_failed = srunner_ntests_failed(sr);
+  srunner_free(sr);
+  return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git test_fixsmtpio_control.c test_fixsmtpio_control.c
new file mode 100644
index 0000000..2e4f8e1
--- /dev/null
+++ test_fixsmtpio_control.c
@@ -0,0 +1,200 @@
+#include "check.h"
+
+#include "fixsmtpio_control.h"
+
+#define assert_str_null_or_eq(s1,s2) \
+  if (s1 == NULL) \
+    ck_assert_ptr_null(s2); \
+  else \
+    ck_assert_str_eq(s1, s2);
+
+void assert_parsed_line(char *input,
+                        char *env,char *event,char *request_prepend,
+                        char *response_line_glob,int exitcode,char *response) {
+  filter_rule *rule = parse_control_line(input);
+
+  ck_assert_ptr_nonnull(rule);
+  ck_assert_ptr_null(rule->next);
+  assert_str_null_or_eq(env, rule->env);
+  assert_str_null_or_eq(event, rule->event);
+  assert_str_null_or_eq(request_prepend, rule->request_prepend);
+  assert_str_null_or_eq(response_line_glob, rule->response_line_glob);
+  ck_assert_int_eq(exitcode, rule->exitcode);
+  assert_str_null_or_eq(response, rule->response);
+}
+
+void assert_non_parsed_line(char *input) {
+  ck_assert_ptr_null(parse_control_line(input));
+}
+
+START_TEST (test_reject_blank_line) {
+  assert_non_parsed_line(
+    ""
+  );
+} END_TEST
+
+START_TEST (test_reject_just_a_comma) {
+  assert_non_parsed_line(
+    ","
+  );
+} END_TEST
+
+START_TEST (test_reject_just_a_colon) {
+  assert_non_parsed_line(
+    ":"
+  );
+} END_TEST
+
+START_TEST (test_accept_empty_env) {
+  assert_parsed_line(
+    ":event:prepend:glob:55:response",
+    NULL,"event","prepend","glob",55,"response"
+  );
+} END_TEST
+
+START_TEST (test_reject_empty_event) {
+  assert_non_parsed_line(
+    "env::prepend:glob:55:response"
+  );
+} END_TEST
+
+START_TEST (test_accept_invented_event) {
+  assert_parsed_line(
+    "env:flibbertigibbet:prepend:glob:55:response",
+    "env","flibbertigibbet","prepend","glob",55,"response"
+  );
+} END_TEST
+
+START_TEST (test_accept_empty_request_prepend) {
+  assert_parsed_line(
+    "env:event::glob:55:response",
+    "env","event",NULL,"glob",55,"response"
+  );
+} END_TEST
+
+START_TEST (test_reject_empty_response_line_glob) {
+  assert_non_parsed_line(
+    "env:event:prepend::55:response"
+  );
+} END_TEST
+
+START_TEST (test_accept_empty_exitcode) {
+  assert_parsed_line(
+    "env:event:prepend:glob::response",
+    "env","event","prepend","glob",EXIT_LATER_NORMALLY,"response"
+  );
+} END_TEST
+
+START_TEST (test_reject_exitcode_non_numeric) {
+  assert_non_parsed_line(
+    "env:event:prepend:glob:exitcode:response"
+  );
+} END_TEST
+
+START_TEST (test_reject_exitcode_too_large) {
+  assert_non_parsed_line(
+    "env:event:prepend:glob:500:response"
+  );
+} END_TEST
+
+START_TEST (test_accept_valid_exitcode) {
+  assert_parsed_line(
+    "env:event:prepend:glob:5:response",
+    "env","event","prepend","glob",5,"response"
+  );
+} END_TEST
+
+START_TEST (test_accept_empty_response) {
+  assert_parsed_line(
+    "env:event:prepend:glob:55:",
+    "env","event","prepend","glob",55,""
+  );
+} END_TEST
+
+START_TEST (test_reject_response_not_specified) {
+  assert_non_parsed_line(
+    "env:event:prepend:glob:55"
+  );
+} END_TEST
+
+START_TEST (test_accept_response_containing_space) {
+  assert_parsed_line(
+    "env:event:prepend:glob:55:response 250 ok",
+    "env","event","prepend","glob",55,"response 250 ok"
+  );
+} END_TEST
+
+START_TEST (test_accept_response_containing_colon) {
+  assert_parsed_line(
+    "env:event:prepend:glob:55:response: 250 ok",
+    "env","event","prepend","glob",55,"response: 250 ok"
+  );
+} END_TEST
+
+START_TEST (test_accept_realistic_line) {
+  assert_parsed_line(
+    ":word:NOOP :*::250 indeed",
+    NULL,"word","NOOP ","*",EXIT_LATER_NORMALLY,"250 indeed"
+  );
+} END_TEST
+
+START_TEST (test_reject_clienteof_with_custom_response) {
+  assert_non_parsed_line(
+    "env:clienteof:prepend:glob:55:custom response"
+  );
+} END_TEST
+
+START_TEST (test_accept_fixup_for_applicable_event) {
+  assert_parsed_line(
+    "env:HELO:prepend:glob:55:&fixsmtpio_fixup",
+    "env","HELO","prepend","glob",55,"&fixsmtpio_fixup"
+  );
+} END_TEST
+
+START_TEST (test_reject_fixup_for_inapplicable_event) {
+  assert_non_parsed_line(
+    "env:event:prepend:glob:55:&fixsmtpio_fixup"
+  );
+} END_TEST
+
+START_TEST (test_reject_unknown_response_thingy) {
+  assert_non_parsed_line(
+    "env:event:prepend:glob:55:&fixsmtpio_unknown_thingy"
+  );
+} END_TEST
+
+START_TEST (test_accept_other_ampersand_response) {
+  assert_parsed_line(
+    "env:event:prepend:glob:55:&other_ampersand_response",
+    "env","event","prepend","glob",55,"&other_ampersand_response"
+  );
+} END_TEST
+
+TCase *tc_control(void) {
+  TCase *tc = tcase_create("");
+
+  tcase_add_test(tc, test_reject_blank_line);
+  tcase_add_test(tc, test_reject_just_a_comma);
+  tcase_add_test(tc, test_reject_just_a_colon);
+  tcase_add_test(tc, test_accept_empty_env);
+  tcase_add_test(tc, test_reject_empty_event);
+  tcase_add_test(tc, test_accept_invented_event);
+  tcase_add_test(tc, test_accept_empty_request_prepend);
+  tcase_add_test(tc, test_reject_empty_response_line_glob);
+  tcase_add_test(tc, test_accept_empty_exitcode);
+  tcase_add_test(tc, test_reject_exitcode_non_numeric);
+  tcase_add_test(tc, test_reject_exitcode_too_large);
+  tcase_add_test(tc, test_accept_valid_exitcode);
+  tcase_add_test(tc, test_accept_empty_response);
+  tcase_add_test(tc, test_reject_response_not_specified);
+  tcase_add_test(tc, test_accept_response_containing_space);
+  tcase_add_test(tc, test_accept_response_containing_colon);
+  tcase_add_test(tc, test_accept_realistic_line);
+  tcase_add_test(tc, test_reject_clienteof_with_custom_response);
+  tcase_add_test(tc, test_accept_fixup_for_applicable_event);
+  tcase_add_test(tc, test_reject_fixup_for_inapplicable_event);
+  tcase_add_test(tc, test_reject_unknown_response_thingy);
+  tcase_add_test(tc, test_accept_other_ampersand_response);
+
+  return tc;
+}
diff --git test_fixsmtpio_eventq.c test_fixsmtpio_eventq.c
new file mode 100644
index 0000000..517c5b0
--- /dev/null
+++ test_fixsmtpio_eventq.c
@@ -0,0 +1,28 @@
+#include "check.h"
+
+#include "fixsmtpio_eventq.h"
+
+START_TEST (test_eventq_put_and_get)
+{
+  ck_assert_str_eq(eventq_get(), "timeout");
+
+  eventq_put("foo");
+  ck_assert_str_eq(eventq_get(), "foo");
+
+  eventq_put("bar");
+  eventq_put("baz");
+  eventq_put("quux");
+  ck_assert_str_eq(eventq_get(), "bar");
+  ck_assert_str_eq(eventq_get(), "baz");
+  ck_assert_str_eq(eventq_get(), "quux");
+  ck_assert_str_eq(eventq_get(), "timeout");
+}
+END_TEST
+
+TCase *tc_eventq(void) {
+  TCase *tc = tcase_create("");
+
+  tcase_add_test(tc, test_eventq_put_and_get);
+
+  return tc;
+}
diff --git test_fixsmtpio_filter.c test_fixsmtpio_filter.c
new file mode 100644
index 0000000..6cf4ba4
--- /dev/null
+++ test_fixsmtpio_filter.c
@@ -0,0 +1,120 @@
+#include "check.h"
+
+#include "fixsmtpio_filter.h"
+
+void assert_filter_rule(filter_rule *filter_rule, const char *event, int expected) {
+  ck_assert_int_eq(filter_rule_applies(filter_rule, event), expected);
+}
+
+START_TEST (test_filter_rule_applies)
+{
+  filter_rule rule = {
+    0,
+    0, "caliente",
+    REQUEST_PASSTHRU, "*",
+    EXIT_LATER_NORMALLY, "",
+  };
+  assert_filter_rule(&rule, "clienteof", 0);
+
+  rule.event = "clienteof";
+  assert_filter_rule(&rule, "clienteof", 1);
+}
+END_TEST
+
+START_TEST (test_want_munge_internally) {
+  ck_assert_int_eq(1, want_munge_internally("&fixsmtpio_fixup"));
+  ck_assert_int_eq(0, want_munge_internally("&fixsmtpio_noop"));
+  ck_assert_int_eq(0, want_munge_internally(""));
+  //ck_assert_int_eq(0, want_munge_internally(NULL));
+  ck_assert_int_eq(0, want_munge_internally("random other text\r\n"));
+}
+END_TEST
+
+START_TEST (test_want_leave_line_as_is) {
+  ck_assert_int_eq(1, want_leave_line_as_is("&fixsmtpio_noop"));
+  ck_assert_int_eq(0, want_leave_line_as_is("&fixsmtpio_fixup"));
+  ck_assert_int_eq(0, want_leave_line_as_is(""));
+  //ck_assert_int_eq(0, want_leave_line_as_is(NULL));
+  ck_assert_int_eq(0, want_leave_line_as_is("random other text\r\n"));
+}
+END_TEST
+
+START_TEST (test_envvar_exists_if_needed) {
+  ck_assert_int_eq(0, envvar_exists_if_needed("VERY_UNLIKELY_TO_BE_SET"));
+  ck_assert_int_eq(1, envvar_exists_if_needed(""));
+  ck_assert_int_eq(1, envvar_exists_if_needed(NULL));
+}
+END_TEST
+
+void assert_munge_response_line(char *expected_output, int lineno, char *line, int exitcode, char *greeting, filter_rule *rules, char *event) {
+  stralloc line_sa = {0}; stralloc_copys(&line_sa, line);
+  stralloc greeting_sa = {0}; stralloc_copys(&greeting_sa, greeting);
+
+  munge_response_line(lineno, &line_sa, &exitcode, &greeting_sa, rules, event, 0, 0);
+
+  stralloc_0(&line_sa);
+
+  ck_assert_str_eq(line_sa.s, expected_output);
+}
+
+START_TEST (test_munge_response_line) {
+  filter_rule *rules = 0;
+  filter_rule helo = {
+    0,
+    0, "helo",
+    REQUEST_PASSTHRU, "2*",
+    EXIT_LATER_NORMALLY, MUNGE_INTERNALLY,
+  };
+  filter_rule ehlo = {
+    0,
+    0, "ehlo",
+    REQUEST_PASSTHRU, "2*",
+    EXIT_LATER_NORMALLY, MUNGE_INTERNALLY,
+  };
+
+  assert_munge_response_line("222 sup duuuude\r\n", 0, "222 sup duuuude", 0, "yo.sup.local", rules, "ehlo");
+  assert_munge_response_line("222 OUTSTANDING\r\n", 1, "222 OUTSTANDING", 0, "yo.sup.local", rules, "ehlo");
+
+  rules = prepend_rule(rules, &helo);
+  assert_munge_response_line("222 sup duuuude\r\n", 0, "222 sup duuuude", 0, "yo.sup.local", rules, "ehlo");
+  assert_munge_response_line("222 OUTSTANDING\r\n", 1, "222 OUTSTANDING", 0, "yo.sup.local", rules, "ehlo");
+
+  rules = prepend_rule(rules, &ehlo);
+  assert_munge_response_line("250 yo.sup.local\r\n", 0, "222 sup duuuude", 0, "yo.sup.local", rules, "ehlo");
+  assert_munge_response_line("222 OUTSTANDING\r\n", 1, "222 OUTSTANDING", 0, "yo.sup.local", rules, "ehlo");
+}
+END_TEST
+
+void assert_munge_response(char *expected_output, char *response, int exitcode, char *greeting, filter_rule *rules, char *event) {
+  stralloc response_sa = {0}; stralloc_copys(&response_sa, response);
+  stralloc greeting_sa = {0}; stralloc_copys(&greeting_sa, greeting);
+
+  munge_response(&response_sa, &exitcode, &greeting_sa, rules, event, 0, 0);
+
+  stralloc_0(&response_sa);
+
+  ck_assert_str_eq(response_sa.s, expected_output);
+}
+
+START_TEST (test_munge_response) {
+  filter_rule *rules = 0;
+
+  // annoying to test NULL, unlikely to be bug
+  assert_munge_response("", "", EXIT_LATER_NORMALLY, "yo.sup.local", rules, "ehlo");
+  assert_munge_response("512 grump\r\n", "512 grump", EXIT_LATER_NORMALLY, "yo.sup.local", rules, "ehlo");
+  assert_munge_response("512-grump\r\n256 mump\r\n", "512 grump\r\n256 mump", EXIT_LATER_NORMALLY, "yo.sup.local", rules, "ehlo");
+}
+END_TEST
+
+TCase *tc_filter(void) {
+  TCase *tc = tcase_create("");
+
+  tcase_add_test(tc, test_filter_rule_applies);
+  tcase_add_test(tc, test_want_munge_internally);
+  tcase_add_test(tc, test_want_leave_line_as_is);
+  tcase_add_test(tc, test_envvar_exists_if_needed);
+  tcase_add_test(tc, test_munge_response_line);
+  tcase_add_test(tc, test_munge_response);
+
+  return tc;
+}
diff --git test_fixsmtpio_glob.c test_fixsmtpio_glob.c
new file mode 100644
index 0000000..8e8287c
--- /dev/null
+++ test_fixsmtpio_glob.c
@@ -0,0 +1,35 @@
+#include "check.h"
+
+#include "fixsmtpio_glob.h"
+
+START_TEST (test_string_matches_glob) {
+  ck_assert(string_matches_glob("*", ""));
+  ck_assert(string_matches_glob("*", "foob;;ar"));
+  ck_assert(string_matches_glob("4*", "450 tempfail"));
+  ck_assert(string_matches_glob("4*", "4"));
+  ck_assert(string_matches_glob("250?STARTTLS", "250?STARTTLS"));
+  ck_assert(string_matches_glob("250?AUTH*", "250 AUTH "));
+  ck_assert(string_matches_glob("250?AUTH*", "250 AUTH"));
+  ck_assert(string_matches_glob("250?auth*", "250 auth"));
+  ck_assert(string_matches_glob("2*", "250-auth login"));
+  ck_assert(string_matches_glob("250?auth*", "250-auth login"));
+  ck_assert(string_matches_glob("", ""));
+ 
+ 
+  ck_assert(!string_matches_glob("250 auth", "the anthology contains works by 250 authors"));
+  ck_assert(!string_matches_glob("250?AUTH*", "250  AUTH"));
+  ck_assert(!string_matches_glob("250?AUTH*", " 250  AUTH"));
+  ck_assert(!string_matches_glob("250?AUTH*", " 250 AUTH"));
+  ck_assert(!string_matches_glob("4*", "foob;;ar"));
+  ck_assert(!string_matches_glob("", " 250 AUTH"));
+  ck_assert(!string_matches_glob("4*", "I have eaten 450 french fries"));
+}
+END_TEST
+
+TCase *tc_glob(void) {
+  TCase *tc = tcase_create("");
+
+  tcase_add_test(tc, test_string_matches_glob);
+
+  return tc;
+}
diff --git test_fixsmtpio_munge.c test_fixsmtpio_munge.c
new file mode 100644
index 0000000..8f57a5b
--- /dev/null
+++ test_fixsmtpio_munge.c
@@ -0,0 +1,152 @@
+#include "check.h"
+
+#include "fixsmtpio_munge.h"
+
+void assert_change_every_line_fourth_char_to_dash( char *input, char *expected_response) {
+  stralloc response_sa = {0}; stralloc_copys(&response_sa, input);
+  change_every_line_fourth_char_to_dash(&response_sa);
+  stralloc_0(&response_sa);
+  ck_assert_str_eq(response_sa.s, expected_response);
+
+}
+
+START_TEST (test_change_every_line_fourth_char_to_dash) {
+  // annoying to test, currently don't believe I have this bug:
+  // assert_change_every_line_fourth_char_to_dash(NULL, "");
+
+  assert_change_every_line_fourth_char_to_dash("", "");
+
+  assert_change_every_line_fourth_char_to_dash("ab", "ab");
+
+  assert_change_every_line_fourth_char_to_dash("abc", "abc");
+
+  assert_change_every_line_fourth_char_to_dash("abcd", "abc-");
+
+  assert_change_every_line_fourth_char_to_dash("abcd efgh\n", "abc- efgh\n");
+
+  assert_change_every_line_fourth_char_to_dash(
+      "abcd efgh\nijk\n", "abc- efgh\nijk\n");
+
+  assert_change_every_line_fourth_char_to_dash(
+      "ijk\n"
+      "abcd efgh\n",
+
+      "ijk\n"
+      "abc- efgh\n");
+
+  assert_change_every_line_fourth_char_to_dash(
+      "abcd efgh\n"
+      "ijk\n"
+      "bcde fghi\n",
+
+      "abc- efgh\n"
+      "ijk\n"
+      "bcd- fghi\n");
+}
+END_TEST
+
+void assert_change_last_line_fourth_char_to_space(char *input, char *expected_response) {
+  stralloc response_sa = {0}; stralloc_copys(&response_sa, input);
+  change_last_line_fourth_char_to_space(&response_sa);
+  stralloc_0(&response_sa);
+  ck_assert_str_eq(response_sa.s, expected_response);
+}
+
+START_TEST (test_change_last_line_fourth_char_to_space) {
+  // annoying to test, currently don't believe I have this bug:
+  // assert_change_last_line_fourth_char_to_space(NULL, "");
+
+  assert_change_last_line_fourth_char_to_space("", "");
+
+  assert_change_last_line_fourth_char_to_space("ab", "ab");
+
+  assert_change_last_line_fourth_char_to_space("abc", "abc");
+
+  assert_change_last_line_fourth_char_to_space("abcd", "abc ");
+
+  assert_change_last_line_fourth_char_to_space("abcd efgh\n", "abc  efgh\n");
+
+  assert_change_last_line_fourth_char_to_space(
+      "abcd efgh\nij\n", "abcd efgh\nij\n");
+
+  assert_change_last_line_fourth_char_to_space(
+      "abcd efgh\nijk\n", "abcd efgh\nijk ");
+
+  assert_change_last_line_fourth_char_to_space(
+      "ijk\n"
+      "abcd efgh\n",
+
+      "ijk\n"
+      "abc  efgh\n");
+
+  assert_change_last_line_fourth_char_to_space(
+      "abcd efgh\n"
+      "ijk\n"
+      "bcde fghi\n",
+
+      "abcd efgh\n"
+      "ijk\n"
+      "bcd  fghi\n");
+}
+END_TEST
+
+START_TEST (test_event_matches) {
+  /*
+  char empty_unterminated[] = {                   };
+  char       unterminated[] = {'f', 'o', 'o'      };
+  char         terminated[] = {'f', 'o', 'o', '\0'};
+
+  ck_assert(!event_matches(empty_unterminated, ""));
+  ck_assert(!event_matches("", empty_unterminated));
+  ck_assert( event_matches(empty_unterminated, empty_unterminated));
+
+  ck_assert(!event_matches(unterminated, ""));
+  ck_assert(!event_matches("", unterminated));
+  ck_assert( event_matches(unterminated, unterminated));
+
+  ck_assert(!event_matches(unterminated, NULL));
+  ck_assert(!event_matches(NULL, unterminated));
+
+  ck_assert( event_matches(unterminated, terminated));
+  */
+
+  ck_assert(!event_matches(NULL, ""));
+  ck_assert(!event_matches("", NULL));
+  ck_assert(!event_matches(NULL, NULL));
+  ck_assert(!event_matches("", ""));
+
+  ck_assert(!event_matches("foo", "bar"));
+  ck_assert( event_matches("baz", "baz"));
+  ck_assert( event_matches("Quux", "quuX"));
+}
+END_TEST
+
+void assert_munge_line_internally(char *input, int lineno, char *greeting, char *event, char *expected_output) {
+  stralloc input_sa = {0}; stralloc_copys(&input_sa, input);
+  stralloc greeting_sa = {0}; stralloc_copys(&greeting_sa, greeting);
+  munge_line_internally(&input_sa,lineno,&greeting_sa,event,0,0);
+  stralloc_0(&input_sa);
+  ck_assert_str_eq(input_sa.s, expected_output);
+}
+
+START_TEST (test_munge_line_internally) {
+  assert_munge_line_internally("250 word up, kids", 0, "yo.sup.local", "word", "250 word up, kids");
+  assert_munge_line_internally("250-applesauce", 0, "yo.sup.local", "ehlo", "250 yo.sup.local");
+  assert_munge_line_internally("250-STARTSOMETHING", 1, "yo.sup.local", "ehlo", "250-STARTSOMETHING");
+  assert_munge_line_internally("250 ENDSOMETHING", 2, "yo.sup.local", "ehlo", "250 ENDSOMETHING");
+  assert_munge_line_internally("250 applesauce", 0, "yo.sup.local", "helo", "250 yo.sup.local");
+  assert_munge_line_internally("214 ask your grandmother\r\n", 0, "yo.sup.local", "help", "214 fixsmtpio home page: https://schmonz.com/qmail/acceptutils\r\n214 ask your grandmother\r\n");
+  assert_munge_line_internally("221 get outta here", 0, "yo.sup.local", "quit", "221 yo.sup.local");
+}
+END_TEST
+
+TCase *tc_munge(void) {
+  TCase *tc = tcase_create("");
+
+  tcase_add_test(tc, test_change_every_line_fourth_char_to_dash);
+  tcase_add_test(tc, test_change_last_line_fourth_char_to_space);
+  tcase_add_test(tc, test_event_matches);
+  tcase_add_test(tc, test_munge_line_internally);
+
+  return tc;
+}
diff --git test_fixsmtpio_proxy.c test_fixsmtpio_proxy.c
new file mode 100644
index 0000000..76c766c
--- /dev/null
+++ test_fixsmtpio_proxy.c
@@ -0,0 +1,216 @@
+#include "check.h"
+#include "acceptutils_stralloc.h"
+
+#include "fixsmtpio_proxy.h"
+
+void assert_strip_last_eol(const char *input, const char *expected_output) {
+  stralloc sa = {0}; stralloc_copys(&sa, input);
+
+  strip_last_eol(&sa);
+
+  ck_assert_int_eq(sa.len, strlen(expected_output));
+  stralloc_0(&sa);
+  ck_assert_str_eq(sa.s, expected_output);
+}
+
+START_TEST (test_strip_last_eol)
+{
+  assert_strip_last_eol("", "");
+  assert_strip_last_eol("\n", "");
+  assert_strip_last_eol("\r", "");
+  assert_strip_last_eol("\r\n", "");
+  assert_strip_last_eol("\n\r", "\n");
+  assert_strip_last_eol("\r\r", "\r");
+  assert_strip_last_eol("\n\n", "\n");
+  assert_strip_last_eol("yo geeps", "yo geeps");
+  assert_strip_last_eol("yo geeps\r\n", "yo geeps");
+  assert_strip_last_eol("yo geeps\r\nhow you doin?\r\n", "yo geeps\r\nhow you doin?");
+  assert_strip_last_eol("yo geeps\r\nhow you doin?", "yo geeps\r\nhow you doin?");
+}
+END_TEST
+
+void assert_ends_with_newline(char *input, int expected) {
+  stralloc sa = {0}; stralloc_copys(&sa, input);
+
+  int actual = ends_with_newline(&sa);
+
+  ck_assert_int_eq(actual, expected);
+}
+
+START_TEST (test_ends_with_newline)
+{
+  // annoying to test, currently don't believe I have this bug:
+  // assert_ends_with_newline(NULL, 0);
+  assert_ends_with_newline("", 0);
+  assert_ends_with_newline("123", 0);
+  assert_ends_with_newline("123\n", 1);
+  assert_ends_with_newline("1\n23\n", 1);
+}
+END_TEST
+
+void assert_is_last_line_of_response(const char *input, int expected)
+{
+  stralloc sa = {0}; stralloc_copys(&sa, input);
+  int actual = is_last_line_of_response(&sa);
+  ck_assert_int_eq(actual, expected);
+}
+
+START_TEST (test_is_last_line_of_response)
+{
+  //assert_is_last_line_of_response(NULL, 0);
+  assert_is_last_line_of_response("", 0);
+  assert_is_last_line_of_response("123", 0);
+  assert_is_last_line_of_response("1234", 0);
+  assert_is_last_line_of_response("123 this is a final line", 1);
+  assert_is_last_line_of_response("123-this is NOT a final line", 0);
+  assert_is_last_line_of_response("777-is not\r\n", 0);
+  assert_is_last_line_of_response("777 is\r\n", 1);
+  
+
+  // two surprises, but maybe fine for this function's job:
+  // - "\r\n" can be un-present and it's fine
+  // - it can have nothing after the space and it's fine
+  assert_is_last_line_of_response("123 ", 1);
+  assert_is_last_line_of_response("123\n", 0);
+}
+END_TEST
+
+void assert_parse_client_request(const char *request, const char *verb, const char *arg)
+{
+  stralloc sa_request = {0}; stralloc_copys(&sa_request, request);
+  stralloc sa_request_copy = {0}; stralloc_copy(&sa_request_copy, &sa_request);
+  stralloc sa_verb = {0};
+  stralloc sa_arg = {0};
+
+  parse_client_request(&sa_verb, &sa_arg, &sa_request);
+
+  ck_assert_int_eq(sa_request_copy.len, sa_request.len);
+  stralloc_0(&sa_verb);
+  ck_assert_str_eq(sa_verb.s, verb);
+  stralloc_0(&sa_arg);
+  ck_assert_str_eq(sa_arg.s, arg);
+}
+
+START_TEST (test_parse_client_request)
+{
+  //assert_parse_client_request(NULL, "", "");
+  assert_parse_client_request("", "", "");
+  assert_parse_client_request("MAIL FROM:<schmonz@schmonz.com>\r\n", "MAIL", "FROM:<schmonz@schmonz.com>");
+  assert_parse_client_request("RCPT TO:<geepawhill@geepawhill.org>\r\n", "RCPT", "TO:<geepawhill@geepawhill.org>");
+  assert_parse_client_request("GENIUSPROGRAMMER\r\n", "GENIUSPROGRAMMER", "");
+  assert_parse_client_request(" NEATO\r\n", "", "NEATO");
+  assert_parse_client_request("SWELL \r\n", "SWELL", "");
+  assert_parse_client_request(" \r\n", "", "");
+  assert_parse_client_request("   \r\n", "", "  ");
+  assert_parse_client_request("SUPER WEIRD STUFF\r\n", "SUPER", "WEIRD STUFF");
+  assert_parse_client_request("R WEIRD STUFF\r\n", "R", "WEIRD STUFF");
+  assert_parse_client_request("MAIL FROM:<schmonz@schmonz.com>\r\nRCPT TO:<geepawhill@geepawhill.org>\r\n", "MAIL", "FROM:<schmonz@schmonz.com>\r\nRCPT TO:<geepawhill@geepawhill.org>");
+}
+END_TEST
+
+static void assert_get_one_response(char *input, const char *expected_result, const char *expected_remaining, int expected_return) {
+  stralloc actual_one = {0}, actual_many = {0};
+  int return_value;
+  copys(&actual_many,input);
+
+  return_value = get_one_response(&actual_one,&actual_many);
+
+  ck_assert_int_eq(return_value, expected_return);
+
+  stralloc_0(&actual_one);
+  ck_assert_str_eq(actual_one.s, expected_result);
+
+  stralloc_0(&actual_many);
+  ck_assert_str_eq(actual_many.s, expected_remaining);
+}
+
+START_TEST (test_get_one_response)
+{
+  assert_get_one_response("777 oneline\r\n", "777 oneline\r\n", "", 1);
+  assert_get_one_response("777 separate\r\n888 responses\r\n", "777 separate\r\n", "888 responses\r\n", 1);
+  assert_get_one_response("777-two\r\n777 lines\r\n888 three\r\n", "777-two\r\n777 lines\r\n", "888 three\r\n", 1);
+  assert_get_one_response("777-two\r\n777 lines\r\n888 three\r\n999 four\r\n", "777-two\r\n777 lines\r\n", "888 three\r\n999 four\r\n", 1);
+  assert_get_one_response("777-two\r\n", "", "777-two\r\n", 0);
+}
+END_TEST
+
+static void assert_ends_data(const char *input, const int expected) {
+  stralloc input_sa = {0}; stralloc_copys(&input_sa, input);
+
+  int actual = ends_data(&input_sa);
+
+  ck_assert_int_eq(actual, expected);
+}
+
+START_TEST (test_ends_data)
+{
+  // annoying to test, currently don't believe I have this bug:
+  // assert_is_last_line_of_data(NULL, 0);
+  assert_ends_data("", 0);
+  assert_ends_data("\r\n", 0);
+  assert_ends_data(" \r\n", 0);
+  assert_ends_data(".\r\n", 1);
+  assert_ends_data(" .\r\n", 0);
+  assert_ends_data("\n.\r\n", 1);
+  assert_ends_data("\r\n.\r\n", 1);
+  assert_ends_data("snorf.\r\n", 0);
+  assert_ends_data("snorf.\r\n.\r\n", 1);
+}
+END_TEST
+
+START_TEST (test_construct_proxy_request)
+{
+  stralloc proxy_request = {0},
+           arg = {0},
+           client_request = {0};
+  int want_data = 0;
+
+  filter_rule test_rule = {
+    0,
+    "SPECIFIC_ENV_VAR",   "specific_verb",
+    "prepend me: ",       "*",
+    EXIT_LATER_NORMALLY,  "337 hello friend",
+  };
+  filter_rule *test_rules = prepend_rule(0, &test_rule);
+
+  blank(&proxy_request); copys(&client_request, "SPECIFIC_VERB somearg\r\n");
+  construct_proxy_request(&proxy_request,test_rules,"SPECIFIC_VERB",&arg,&client_request,0,(void *)0,0,&want_data);
+  stralloc_0(&proxy_request); stralloc_0(&client_request);
+  ck_assert_str_eq(proxy_request.s, client_request.s);
+
+  env_put2("SPECIFIC_ENV_VAR","");
+
+  blank(&proxy_request); copys(&client_request, "SPECIFIC_VERB somearg\r\n");
+  construct_proxy_request(&proxy_request,test_rules,"SPECIFIC_VERB",&arg,&client_request,0,(void *)0,0,&want_data);
+  stralloc_0(&proxy_request); stralloc_0(&client_request);
+  ck_assert_str_ne(proxy_request.s, client_request.s);
+
+  env_unset("SPECIFIC_ENV_VAR");
+
+  // XXX not in_data, a rule says to prepend: proxy request is equal to prepended + client_request
+  // XXX not in_data, two rules prepend: proxy request is equal to p1 + p2 + client_request (WILL FAIL)
+}
+END_TEST
+
+START_TEST (test_construct_proxy_response)
+{
+  ck_assert(1);
+  // not sure whether to test:
+  // not want_data, not in_data, no request_received, no verb: it's a timeout
+}
+END_TEST
+
+TCase *tc_proxy(void) {
+  TCase *tc = tcase_create("");
+
+  tcase_add_test(tc, test_strip_last_eol);
+  tcase_add_test(tc, test_ends_with_newline);
+  tcase_add_test(tc, test_is_last_line_of_response);
+  tcase_add_test(tc, test_parse_client_request);
+  tcase_add_test(tc, test_get_one_response);
+  tcase_add_test(tc, test_ends_data);
+  tcase_add_test(tc, test_construct_proxy_request);
+  tcase_add_test(tc, test_construct_proxy_response);
+
+  return tc;
+}
