<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">diff --git Makefile Makefile
index 0f0e31a..245a3fc 100644
--- Makefile
+++ Makefile
@@ -4,6 +4,251 @@ SHELL=/bin/sh
 
 default: it
 
+ACCEPTUTILS_BIN=authup   checknotroot \
+		fixsmtpio   qmail-qfilter-addtlsheader
+ACCEPTUTILS_MAN=authup.8 checknotroot.8 \
+		fixsmtpio.8 qmail-qfilter-addtlsheader.8
+
+acceptutils: \
+${ACCEPTUTILS_BIN} ${ACCEPTUTILS_MAN}
+
+acceptutils_base64.o: \
+compile99 acceptutils_base64.c acceptutils_base64.h
+	./compile99 acceptutils_base64.c
+
+acceptutils_pfilter.o: \
+compile99 acceptutils_pfilter.c acceptutils_pfilter.h hasblacklist.h
+	./compile99 acceptutils_pfilter.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 substdio.h
+	./compile99 acceptutils_ucspitls.c
+
+acceptutils_unistd.o: \
+compile99 acceptutils_unistd.c acceptutils_unistd.h
+	./compile99 acceptutils_unistd.c
+
+acceptutils-install: \
+acceptutils
+	cp ${ACCEPTUTILS_BIN} `head -1 conf-qmail`/bin &amp;&amp; \
+	cp ${ACCEPTUTILS_MAN} `head -1 conf-qmail`/man/man8
+
+acceptutils-memcheck: \
+test_fixsmtpio
+	valgrind --track-origins=yes --leak-check=full --error-exitcode=99 ./test_fixsmtpio &gt;/dev/null
+	#valgrind --track-origins=yes --leak-check=full --error-exitcode=88 ./fixsmtpio echo hi &gt;/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 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 \
+getopt.a substdio.a error.a str.a fs.a \
+acceptutils_base64.o acceptutils_pfilter.o acceptutils_stralloc.o \
+acceptutils_unistd.o acceptutils_ucspitls.o \
+socket.lib blacklist.lib
+	./load authup auto_qmail.o acceptutils_base64.o acceptutils_pfilter.o \
+	acceptutils_stralloc.o acceptutils_unistd.o acceptutils_ucspitls.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 \
+	getopt.a substdio.a error.a str.a fs.a \
+	`cat socket.lib` `cat blacklist.lib`
+
+authup.o: \
+compile99 authup.c auto_qmail.h commands.h sig.h substdio.h wait.h \
+str.h byte.h now.h fmt.h scan.h readwrite.h timeoutread.h timeoutwrite.h \
+case.h env.h control.h error.h sgetopt.h \
+acceptutils_base64.h acceptutils_pfilter.h acceptutils_stralloc.h \
+acceptutils_ucspitls.h acceptutils_unistd.h 
+	./compile99 authup.c
+
+blacklist.lib: \
+tryblist.c compile load
+	( ( ./compile tryblist.c &amp;&amp; ./load tryblist -lblacklist ) &gt;/dev/null \
+	2&gt;&amp;1 \
+	&amp;&amp; echo || exit 0 ) &gt; blacklist.lib
+	rm -f tryblist.o tryblist
+
+check.h: \
+conf-check check_stdint.h
+	cat `head -1 conf-check`/include/check.h \
+	| sed 's}&lt;check_stdint\.h&gt;}"check_stdint.h"}g' \
+	&gt; 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 .
+
+qmail-qfilter-addtlsheader: \
+load qmail-qfilter-addtlsheader.o date822fmt.o now.o \
+datetime.a substdio.a env.a error.a str.a fs.a
+	./load qmail-qfilter-addtlsheader date822fmt.o now.o \
+	datetime.a substdio.a env.a error.a str.a fs.a
+
+qmail-qfilter-addtlsheader.o: \
+compile99 qmail-qfilter-addtlsheader.c datetime.h date822fmt.h env.h \
+now.h readwrite.h substdio.h
+	./compile99 qmail-qfilter-addtlsheader.c
+
+rt.lib: \
+compile load
+	( ( echo 'main() { ; }' &gt; tryrt.c &amp;&amp; ./compile tryrt.c &amp;&amp; \
+	./load tryrt -lrt ) &gt;/dev/null 2&gt;&amp;1 \
+	&amp;&amp; echo -lrt || exit 0 ) &gt; rt.lib
+	rm -f tryrt.c tryrt.o tryrt
+
 addresses.0: \
 addresses.5
 	nroff -man addresses.5 &gt; addresses.0
@@ -303,6 +548,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 +565,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`" ) &gt; \
+	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
@@ -633,6 +885,13 @@ gfrom.o: \
 compile gfrom.c str.h gfrom.h
 	./compile gfrom.c
 
+hasblacklist.h: \
+tryblist.c compile load
+	( ( ./compile tryblist.c &amp;&amp; ./load tryblist -lblacklist ) &gt;/dev/null \
+	2&gt;&amp;1 \
+	&amp;&amp; echo || exit 0 ) &gt; hasblacklist.h
+	rm -f tryblist.o tryblist
+
 hasflock.h: \
 tryflock.c compile load
 	( ( ./compile tryflock.c &amp;&amp; ./load tryflock ) &gt;/dev/null \
@@ -908,6 +1167,11 @@ make-compile.sh auto-ccld.sh
 	cat auto-ccld.sh make-compile.sh &gt; make-compile
 	chmod 755 make-compile
 
+make-compile99: \
+make-compile99.sh auto-ccld.sh
+	cat auto-ccld.sh make-compile99.sh &gt; make-compile99
+	chmod 755 make-compile99
+
 make-load: \
 make-load.sh auto-ccld.sh
 	cat auto-ccld.sh make-load.sh &gt; make-load
diff --git TARGETS TARGETS
index facdad7..8e50f83 100644
--- TARGETS
+++ TARGETS
@@ -385,3 +385,41 @@ forgeries.0
 man
 setup
 check
+make-compile99
+compile99
+acceptutils_base64.o
+acceptutils_pfilter.o
+acceptutils_stralloc.o
+acceptutils_ucspitls.o
+acceptutils_unistd.o
+authup
+authup.o
+checknotroot
+checknotroot.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
+hasblacklist.h
+blacklist.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
+qmail-qfilter-addtlsheader
+qmail-qfilter-addtlsheader.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 =&gt;  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-&gt;len = i;
+  s = out-&gt;s;
+
+  for (i = 0; i &lt; n - 1; i++) {
+    x = 0;
+    for (j = 0; j &lt; 4; j++) {
+      if (in[j] &gt;= 'A' &amp;&amp; in[j] &lt;= 'Z')
+        x = (x &lt;&lt; 6) + (unsigned int)(in[j] - 'A' + 0);
+      else if (in[j] &gt;= 'a' &amp;&amp; in[j] &lt;= 'z')
+        x = (x &lt;&lt; 6) + (unsigned int)(in[j] - 'a' + 26);
+      else if (in[j] &gt;= '0' &amp;&amp; in[j] &lt;= '9')
+        x = (x &lt;&lt; 6) + (unsigned int)(in[j] - '0' + 52);
+      else if (in[j] == '+')
+        x = (x &lt;&lt; 6) + 62;
+      else if (in[j] == '/')
+        x = (x &lt;&lt; 6) + 63;
+      else if (in[j] == '=')
+        x = (x &lt;&lt; 6);
+    }
+
+    s[2] = (unsigned char)(x &amp; 255); x &gt;&gt;= 8;
+    s[1] = (unsigned char)(x &amp; 255); x &gt;&gt;= 8;
+    s[0] = (unsigned char)(x &amp; 255); x &gt;&gt;= 8;
+    s += 3; in += 4;
+  }
+
+  x = 0;
+  for (j = 0; j &lt; 4; j++) {
+    if (in[j] &gt;= 'A' &amp;&amp; in[j] &lt;= 'Z')
+      x = (x &lt;&lt; 6) + (unsigned int)(in[j] - 'A' + 0);
+    else if (in[j] &gt;= 'a' &amp;&amp; in[j] &lt;= 'z')
+      x = (x &lt;&lt; 6) + (unsigned int)(in[j] - 'a' + 26);
+    else if (in[j] &gt;= '0' &amp;&amp; in[j] &lt;= '9')
+      x = (x &lt;&lt; 6) + (unsigned int)(in[j] - '0' + 52);
+    else if (in[j] == '+')
+      x = (x &lt;&lt; 6) + 62;
+    else if (in[j] == '/')
+      x = (x &lt;&lt; 6) + 63;
+    else if (in[j] == '=')
+      x = (x &lt;&lt; 6);
+  }
+
+  b[2] = (unsigned char)(x &amp; 255); x &gt;&gt;= 8;
+  b[1] = (unsigned char)(x &amp; 255); x &gt;&gt;= 8;
+  b[0] = (unsigned char)(x &amp; 255); x &gt;&gt;= 8;
+
+  for (i = 0; i &lt; 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-&gt;len == 0)
+  {
+    if (!stralloc_copys(out,"")) return -1;
+    return 0;
+  }
+
+  i = in-&gt;len / 3 * 4 + 4;
+  if (!stralloc_ready(out,i)) return -1;
+  s = out-&gt;s;
+
+  for (i = 0; i &lt; in-&gt;len; i += 3) {
+    a = in-&gt;s[i];
+    b = i + 1 &lt; in-&gt;len ? in-&gt;s[i + 1] : 0;
+    c = i + 2 &lt; in-&gt;len ? in-&gt;s[i + 2] : 0;
+
+    *s++ = b64alpha[a &gt;&gt; 2];
+    *s++ = b64alpha[((a &amp; 3 ) &lt;&lt; 4) | (b &gt;&gt; 4)];
+
+    if (i + 1 &gt;= in-&gt;len) *s++ = B64PAD;
+    else *s++ = b64alpha[((b &amp; 15) &lt;&lt; 2) | (c &gt;&gt; 6)];
+
+    if (i + 2 &gt;= in-&gt;len) *s++ = B64PAD;
+    else *s++ = b64alpha[c &amp; 63];
+  }
+  out-&gt;len = s - out-&gt;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_pfilter.c acceptutils_pfilter.c
new file mode 100644
index 0000000..9ffab18
--- /dev/null
+++ acceptutils_pfilter.c
@@ -0,0 +1,63 @@
+#include "hasblacklist.h"
+
+#if HASBLACKLIST
+
+#include &lt;blacklist.h&gt;
+#include &lt;errno.h&gt;
+#include &lt;stdio.h&gt;
+#include &lt;stdlib.h&gt;
+#include &lt;string.h&gt;
+#include &lt;arpa/inet.h&gt;
+
+#define GEN_FILL_SOCKADDR(funcname,family,structname,familyfield,portfield,addrfield) \
+static int funcname(const struct sockaddr_storage *ss,socklen_t *slen,char *ip,char *port) { \
+  struct structname *sock = (struct structname *)ss; \
+  sock-&gt;familyfield = family; \
+  sock-&gt;portfield = htons(atoi(port)); \
+  *slen = sizeof(*sock); \
+  return inet_pton(sock-&gt;familyfield, ip, &amp;sock-&gt;addrfield); \
+}
+
+GEN_FILL_SOCKADDR(ip6,AF_INET6,sockaddr_in6,sin6_family,sin6_port,sin6_addr);
+GEN_FILL_SOCKADDR(ip4,AF_INET,sockaddr_in,sin_family,sin_port,sin_addr);
+
+static void fill_sockaddr_ip6(const struct sockaddr_storage *ss,socklen_t *slen) {
+  char *ip = getenv("TCP6REMOTEIP"); char *port = getenv("TCP6LOCALPORT");
+  if (!ip || !port || 0 == ip6(ss,slen,ip,port))
+    (void)ip6(ss,slen,getenv("TCPREMOTEIP"),getenv("TCPLOCALPORT"));
+}
+
+static void fill_sockaddr_maybe_ip4(const struct sockaddr_storage *ss,socklen_t *slen) {
+  char *ip = getenv("TCPREMOTEIP"); char *port = getenv("TCPLOCALPORT");
+  if (!ip || !port) return;
+  if (0 == ip6(ss,slen,ip,port))
+    (void)ip4(ss,slen,ip,port);
+}
+
+static void fill_sockaddr_info(const struct sockaddr_storage *ss,socklen_t *slen) {
+  char *proto = getenv("PROTO");
+  memset((void *)ss, 0, *slen);
+  if (proto &amp;&amp; 0 == strcmp(proto,"TCP6"))
+    fill_sockaddr_ip6(ss,slen);
+  else
+    fill_sockaddr_maybe_ip4(ss,slen);
+}
+
+void pfilter_notify(int action,int fd,const char *msg,const char *pidstr) {
+  const struct sockaddr_storage ss;
+  socklen_t slen = sizeof(ss);
+
+  fill_sockaddr_info(&amp;ss,&amp;slen);
+  if (0 == blacklist_sa(action, fd, (void *)&amp;ss, slen, msg))
+    fprintf(stderr,"%s %s blacklist_sa(%d, %d...)\n", msg, pidstr, action, fd);
+  else
+    fprintf(stderr,"%s %s blacklist_sa(%d, %d...) failed with errno %d\n", msg, pidstr, action, fd, errno);
+}
+
+#else
+
+void pfilter_notify(int action,int fd,const char *msg) {
+  ;
+}
+
+#endif
diff --git acceptutils_pfilter.h acceptutils_pfilter.h
new file mode 100644
index 0000000..ba12c8d
--- /dev/null
+++ acceptutils_pfilter.h
@@ -0,0 +1 @@
+void pfilter_notify(int,int,const char *,const char *);
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(&amp;tmp,to);
+  copys(to,(char *)from);
+  cat(to,&amp;tmp);
+}
+int starts(stralloc *haystack,char *needle) { return stralloc_starts(haystack,needle); }
+int ends_with_newline(stralloc *sa) {
+  return sa-&gt;len &gt; 0 &amp;&amp; sa-&gt;s[sa-&gt;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..1551143
--- /dev/null
+++ acceptutils_ucspitls.c
@@ -0,0 +1,111 @@
+#include "case.h"
+#include "env.h"
+#include "fd.h"
+#include "readwrite.h"
+#include "scan.h"
+#include "substdio.h"
+
+#include "acceptutils_stralloc.h"
+#include "acceptutils_ucspitls.h"
+
+int ucspitls_level_configured(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,&amp;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) &lt; 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() &amp;&amp; adjust_read_fd() &amp;&amp; adjust_write_fd();
+}
+
+int tls_info(void (*die_nomem)(const char *caller,const char *alloc_fn)) {
+  unsigned long fd;
+  char envbuf[SUBSTDIO_INSIZE];
+  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,SUBSTDIO_INSIZE)) &gt; 0) {
+    catb(&amp;ssl_env,envbuf,j);
+    if (ssl_env.len &gt;= 2 &amp;&amp; ssl_env.s[ssl_env.len-2] == 0 &amp;&amp; ssl_env.s[ssl_env.len-1] == 0)
+      break;
+  }
+  if (j &lt; 0) die_nomem(__func__,"read");
+  if (ssl_env.len == 0) return 0;
+
+  x = ssl_env.s;
+
+  for (j=0;j &lt; ssl_env.len-1;++j) {
+    if ( *x != '=' ) {
+      catb(&amp;ssl_parm,x,1);
+      x++;
+    } else {
+      append0(&amp;ssl_parm);
+      x++;
+
+      for (;j &lt; ssl_env.len-j-1;++j) {
+        if ( *x != '\0' ) {
+          catb(&amp;ssl_value,x,1);
+          x++;
+        } else {
+          append0(&amp;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..e7384f1
--- /dev/null
+++ acceptutils_ucspitls.h
@@ -0,0 +1,7 @@
+#define UCSPITLS_UNAVAILABLE 0
+#define UCSPITLS_AVAILABLE   1
+#define UCSPITLS_REQUIRED    2
+
+int ucspitls_level_configured(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..40315b0
--- /dev/null
+++ acceptutils_unistd.c
@@ -0,0 +1,19 @@
+#include &lt;unistd.h&gt;
+
+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); }
diff --git acceptutils_unistd.h acceptutils_unistd.h
new file mode 100644
index 0000000..067b964
--- /dev/null
+++ acceptutils_unistd.h
@@ -0,0 +1,8 @@
+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]);
diff --git authup.8 authup.8
new file mode 100644
index 0000000..042b237
--- /dev/null
+++ authup.8
@@ -0,0 +1,168 @@
+.TH AUTHUP 8 2020-12-11
+.SH NAME
+authup \- offer SMTP or POP3 authentication
+.SH SYNOPSIS
+.B authup
+[
+.B \-t \fItries
+]
+.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 and pass them to
+.IR checkpassword .
+.B authup
+is most commonly invoked as root so
+.I checkpassword
+can change to the UID of the authenticated user before running
+.IR prog .
+.P
+Common combinations of 
+.I protocol
+and
+.IR prog :
+.TP 5
+.B smtp
+and
+.B "checknotroot fixsmtpio ofmipd"
+.TP 5
+.B pop3
+and
+.B "checknotroot qmail-pop3d"
+.P
+.B authup
+waits for
+.I checkpassword
+and
+.I prog
+to finish, and prints an error message if either of them crashes or exits nonzero.
+.SH "OPTIONS"
+.TP
+.B \-t \fItries
+Disconnect after
+.I tries
+failed authentication attempts.
+Default: 5 for SMTP, 1 for POP3.
+.SH "ENVIRONMENT VARIABLES"
+When running under
+.B "sslserver -n"
+or
+.BR "s6-ucspitlsd" ,
+.B authup
+can offer TLS.
+Set
+.B UCSPITLS
+to the special value
+.B !
+to require that clients negotiate TLS before authenticating.
+.P
+If
+.B DISABLETLS
+is set, the presence and value of
+.B UCSPITLS
+will be ignored.
+.P
+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.
+.P
+If authentication succeeds,
+.IR prog 's
+environment will contain the username in
+.BR AUTHUP_USER .
+.P
+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 SMTP capabilities: PIPELINING, 8BITMIME. (Omit STARTTLS and AUTH.)
+
+Typical POP3 capabilities: TOP, UIDL. (Omit STLS and USER.)
+
+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
+.B UCSPITLS
+has been enabled in the
+.B "sslserver -n"
+or
+.B "s6-ucspitlsd"
+environment.
+.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 &lt;schmonz-web-acceptutils@schmonz.com&gt;
+.SH "SEE ALSO"
+checknotroot(8),
+fixsmtpio(8),
+sslserver(1),
+s6-ucspitlsd,
+ucspi-tls(2),
+qmail-qfilter-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..9688f6e
--- /dev/null
+++ authup.c
@@ -0,0 +1,674 @@
+#include "auto_qmail.h"
+#include "commands.h"
+#include "sig.h"
+#include "substdio.h"
+#include "wait.h"
+#include "str.h"
+#include "byte.h"
+#include "now.h"
+#include "fmt.h"
+#include "scan.h"
+#include "readwrite.h"
+#include "timeoutread.h"
+#include "timeoutwrite.h"
+#include "case.h"
+#include "env.h"
+#include "control.h"
+#include "error.h"
+#include "sgetopt.h"
+
+#include "acceptutils_base64.h"
+#include "acceptutils_pfilter.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 auth_tries_remaining = 0;
+
+static int timeout = 1200;
+static int tls_level = UCSPITLS_UNAVAILABLE;
+static int in_tls = 0;
+
+static void die()         { unistd_exit( 1); }
+
+static int safewrite(int fd,char *buf,int len) {
+  int r;
+  r = timeoutwrite(timeout,fd,buf,len);
+  if (r &lt;= 0) die();
+  return r;
+}
+
+static char ssoutbuf[SUBSTDIO_OUTSIZE];
+static substdio ssout = SUBSTDIO_FDBUF(safewrite,1,ssoutbuf,sizeof ssoutbuf);
+
+static void out(const char *s) { substdio_puts(&amp;ssout,s); }
+static void flush() { substdio_flush(&amp;ssout); }
+
+static void pop3_err(const char *s) { out("-ERR "); out(s); out("\r\n"); flush(); }
+static void smtp_out(const char *s) {               out(s); out("\r\n"); flush(); }
+
+struct authup_error {
+  const char *name;
+  const char *message;
+  const char *smtpcode;
+  const char *smtperror;
+};
+
+static const struct authup_error fatals[] = {
+  { "control", "unable to read controls",      "421", "4.3.0" }
+, { "nomem",   "out of memory",                "451", "4.3.0" }
+, { "alarm",   "timeout",                      "451", "4.4.2" }
+, { "pipe",    "unable to open pipe",          "454", "4.3.0" }
+, { "read",    "unable to read pipe",          "454", "4.3.0" }
+, { "write",   "unable to write pipe",         "454", "4.3.0" }
+, { "fork",    "unable to fork",               "454", "4.3.0" }
+, { "wait",    "unable to wait for child",     "454", "4.3.0" }
+, { "crash",   "aack, child crashed",          "454", "4.3.0" }
+, { "badauth", "authorization failed",         "535", "5.7.0" }
+, { "protocol","protocol exchange ended",      "501", "5.0.0" }
+, { 0,         "unknown or unspecified error", "421", "4.3.0" }
+};
+
+static const struct authup_error errors[] = {
+  { "badauth", "authorization failed",         "535", "5.7.0" }
+, { "noauth",  "auth type unimplemented",      "504", "5.5.1" }
+, { "input",   "malformed auth input",         "501", "5.5.4" }
+, { "authabrt","auth exchange cancelled",      "501", "5.0.0" }
+, { "starttls","TLS temporarily not available","454", "5.7.3" }
+, { "needtls", "Must start TLS first",         "530", "5.7.0" }
+, { 0,         "unknown or unspecified error", "421", "4.3.0" }
+};
+
+static void pop3_auth_error(struct authup_error ae) {
+  out("-ERR " PROGNAME " ");
+  out(ae.message);
+}
+
+static void smtp_auth_error(struct authup_error ae) {
+  out(ae.smtpcode);
+  out(" " PROGNAME " ");
+  out(ae.message);
+  out(" (#");
+  out(ae.smtperror);
+  out(")");
+}
+
+static void (*protocol_error)();
+
+static char sserrbuf[SUBSTDIO_OUTSIZE];
+static substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf);
+
+static void authup_die(const char *name) {
+  int i;
+  for (i = 0;fatals[i].name;++i) if (case_equals(fatals[i].name,name)) break;
+  protocol_error(fatals[i]);
+  out("\r\n");
+  flush();
+  die();
+}
+
+static void authup_err(const char *name) {
+  int i;
+  for (i = 0;errors[i].name;++i) if (case_equals(errors[i].name,name)) break;
+  protocol_error(errors[i]);
+  out("\r\n");
+  flush();
+}
+
+static void die_nomem(const char *caller,const char *alloc_fn) {
+  substdio_puts(&amp;sserr,PROGNAME ": die_nomem: ");
+  if (caller) {
+    substdio_puts(&amp;sserr,caller);
+    substdio_puts(&amp;sserr,": ");
+  }
+  if (alloc_fn) {
+    substdio_puts(&amp;sserr,alloc_fn);
+  }
+  substdio_putsflush(&amp;sserr,"\n");
+  authup_die("nomem");
+}
+
+static void die_usage() {
+  substdio_puts(&amp;sserr,PROGNAME ": ");
+  substdio_puts(&amp;sserr,"usage: " PROGNAME " [ -t tries ] &lt;smtp|pop3&gt; prog");
+  substdio_putsflush(&amp;sserr,"\n");
+  die();
+}
+
+static void smtp_err_authoriz() { smtp_out("530 " PROGNAME " authentication required (#5.7.1)"); }
+static void pop3_err_authoriz() { pop3_err(PROGNAME " authorization first"); }
+
+static void pop3_err_syntax()   { pop3_err(PROGNAME " syntax error"); }
+static void pop3_err_wantuser() { pop3_err(PROGNAME " USER first"); }
+
+static 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 &lt;= 0) authup_die("read");
+  return r;
+}
+
+static char ssinbuf[SUBSTDIO_INSIZE];
+static substdio ssin = SUBSTDIO_FDBUF(saferead,0,ssinbuf,sizeof ssinbuf);
+
+static stralloc greeting = {0};
+static stralloc capabilities = {0};
+static char **childargs;
+
+static void pop3_okay() { out("+OK \r\n"); flush(); }
+static void pop3_quit() { pop3_okay(); unistd_exit(0); }
+static void smtp_quit() { out("221 "); smtp_out(greeting.s); unistd_exit(0); }
+
+static stralloc username = {0};
+static stralloc logname = {0};
+static stralloc password = {0};
+static stralloc timestamp = {0};
+
+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(&amp;sa,pidbuf,len);
+    append0(&amp;sa);
+  }
+  return sa.s;
+}
+
+static char *authup_pid;
+
+static void logpass(int checkpassword_pid) {
+  substdio_puts(&amp;sserr,PROGNAME);
+  substdio_puts(&amp;sserr," ");
+  substdio_puts(&amp;sserr,authup_pid);
+  substdio_puts(&amp;sserr," checkpassword ");
+  substdio_puts(&amp;sserr,format_pid(checkpassword_pid));
+  substdio_puts(&amp;sserr," succeeded, child completed");
+  substdio_putsflush(&amp;sserr,"\n");
+}
+
+static void logfail(int checkpassword_pid) {
+  substdio_puts(&amp;sserr,PROGNAME);
+  substdio_puts(&amp;sserr," ");
+  substdio_puts(&amp;sserr,authup_pid);
+  substdio_puts(&amp;sserr," checkpassword ");
+  substdio_puts(&amp;sserr,format_pid(checkpassword_pid));
+  substdio_puts(&amp;sserr," failed, ");
+  substdio_puts(&amp;sserr,format_pid(auth_tries_remaining));
+  substdio_puts(&amp;sserr," remaining");
+  substdio_putsflush(&amp;sserr,"\n");
+}
+
+static void logstart(char *protocol) {
+  substdio_puts(&amp;sserr,PROGNAME);
+  substdio_puts(&amp;sserr," ");
+  substdio_puts(&amp;sserr,authup_pid);
+  substdio_puts(&amp;sserr," protocol ");
+  substdio_puts(&amp;sserr,protocol);
+  substdio_putsflush(&amp;sserr,"\n");
+}
+
+static void logtls() {
+  substdio_puts(&amp;sserr,PROGNAME);
+  substdio_puts(&amp;sserr," ");
+  substdio_puts(&amp;sserr,authup_pid);
+  substdio_puts(&amp;sserr," negotiated TLS");
+  substdio_putsflush(&amp;sserr,"\n");
+}
+
+static void exit_according_to_child_exit(int exitcode,int child) {
+  switch (exitcode) {
+    case EXITCODE_CHECKPASSWORD_UNACCEPTABLE:
+    case EXITCODE_CHECKPASSWORD_MISUSED:
+    case EXITCODE_CHECKPASSWORD_TEMPFAIL:
+      logfail(child);
+      pfilter_notify(1, 0, PROGNAME, authup_pid);
+      if (auth_tries_remaining) return authup_err("badauth");
+      authup_die("badauth");
+    case EXITCODE_FIXSMTPIO_TIMEOUT:
+      authup_die("alarm");
+    case EXITCODE_FIXSMTPIO_PARSEFAIL:
+      authup_die("control");
+  }
+
+  logpass(child);
+  pfilter_notify(0, 0, PROGNAME, authup_pid);
+  unistd_exit(exitcode);
+}
+
+static void logtry(int checkpassword_pid) {
+  substdio_puts(&amp;sserr,PROGNAME);
+  substdio_puts(&amp;sserr," ");
+  substdio_puts(&amp;sserr,authup_pid);
+  substdio_puts(&amp;sserr," checkpassword ");
+  substdio_puts(&amp;sserr,format_pid(checkpassword_pid));
+  substdio_puts(&amp;sserr," attempt for ");
+  substdio_puts(&amp;sserr,logname.s);
+  substdio_putsflush(&amp;sserr,"\n");
+}
+
+static void checkpassword(stralloc *username,stralloc *password,stralloc *timestamp) {
+  int child;
+  int wstat;
+  int pi[2];
+  char upbuf[SUBSTDIO_OUTSIZE];
+  substdio ssup;
+
+  copy(&amp;logname,username); append0(&amp;logname);
+
+  auth_tries_remaining--;
+
+  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(unistd_getpid());
+      if (!env_put2("AUTHUP_USER",username-&gt;s)) authup_die("nomem");
+      unistd_execvp(*childargs,childargs);
+      authup_die("fork");
+  }
+  unistd_close(pi[0]);
+  substdio_fdbuf(&amp;ssup,write,pi[1],upbuf,sizeof upbuf);
+
+  append0(username);
+  if (substdio_put(&amp;ssup,username-&gt;s,username-&gt;len) == -1) authup_die("write");
+  byte_zero(username-&gt;s,username-&gt;len);
+
+  append0(password);
+  if (substdio_put(&amp;ssup,password-&gt;s,password-&gt;len) == -1) authup_die("write");
+  byte_zero(password-&gt;s,password-&gt;len);
+
+  append0(timestamp);
+  if (substdio_put(&amp;ssup,timestamp-&gt;s,timestamp-&gt;len) == -1) authup_die("write");
+  byte_zero(timestamp-&gt;s,timestamp-&gt;len);
+
+  if (substdio_flush(&amp;ssup) == -1) authup_die("write");
+  unistd_close(pi[1]);
+  byte_zero(upbuf,sizeof upbuf);
+
+  if (wait_pid(&amp;wstat,child) == -1) authup_die("wait");
+  if (wait_crashed(wstat)) authup_die("crash");
+
+  exit_according_to_child_exit(wait_exitcode(wstat),child);
+}
+
+static char unique[FMT_ULONG + FMT_ULONG + 3];
+
+static 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 &lt;");
+  out(unique);
+  out(greeting.s);
+  out("&gt;\r\n");
+  flush();
+}
+
+static void pop3_format_capa(stralloc *multiline) {
+  cats(multiline,".\r\n");
+}
+
+static void pop3_capa(char *arg) {
+  out("+OK capability list follows\r\n");
+  if (tls_level &gt;= UCSPITLS_AVAILABLE &amp;&amp; !in_tls) out("STLS\r\n");
+  out("USER\r\n");
+  out(capabilities.s);
+  flush();
+}
+
+static int seenuser = 0;
+
+static void pop3_stls(char *arg) {
+  if (tls_level &lt; 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)) return authup_err("starttls");
+  /* reset state */
+  seenuser = 0;
+
+  in_tls = 1;
+  logtls();
+}
+
+static void pop3_user(char *arg) {
+  if (tls_level &gt;= UCSPITLS_REQUIRED &amp;&amp; !in_tls) return authup_err("needtls");
+  if (!*arg) { pop3_err_syntax(); return; }
+  pop3_okay();
+  seenuser = 1;
+  copys(&amp;username,arg);
+}
+
+static void pop3_pass(char *arg) {
+  if (!seenuser) { pop3_err_wantuser(); return; }
+  if (!*arg) { pop3_err_syntax(); return; }
+
+  copys(&amp;password,arg);
+  byte_zero(arg,str_len(arg));
+
+  copys(&amp;timestamp,"&lt;");
+  cats(&amp;timestamp,unique);
+  cats(&amp;timestamp,greeting.s);
+  cats(&amp;timestamp,"&gt;");
+
+  checkpassword(&amp;username,&amp;password,&amp;timestamp);
+}
+
+static void smtp_greet() {
+  out("220 ");
+  out(greeting.s);
+  out(" ESMTP\r\n");
+  flush();
+}
+
+static void smtp_helo(char *arg) {
+  out("250 ");
+  smtp_out(greeting.s);
+}
+
+// copy from fixsmtpio_munge.c:change_last_line_fourth_char_to_space()
+static void smtp_format_ehlo(stralloc *multiline) {
+  int pos = 0;
+  int i;
+  for (i = multiline-&gt;len - 2; i &gt;= 0; i--) {
+    if (multiline-&gt;s[i] == '\n') {
+      pos = i + 1;
+      break;
+    }
+  }
+  capabilities.s[pos+3] = ' ';
+}
+
+static void smtp_ehlo(char *arg) {
+  char *x;
+  out("250-"); out(greeting.s); out("\r\n");
+  if (tls_level &gt;= UCSPITLS_AVAILABLE &amp;&amp; !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();
+}
+
+static void smtp_starttls() {
+  if (tls_level &lt; 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)) return authup_err("starttls");
+  /* reset state */
+  ssin.p = 0;
+
+  in_tls = 1;
+  logtls();
+}
+
+static stralloc authin = {0};
+
+static void smtp_authgetl() {
+  int i;
+
+  blank(&amp;authin);
+
+  for (;;) {
+    if (!stralloc_readyplus(&amp;authin,1)) authup_die("nomem"); /* XXX */
+    i = substdio_get(&amp;ssin,authin.s + authin.len,1);
+    if (i != 1) authup_die("read");
+    if (authin.s[authin.len] == '\n') break;
+    ++authin.len;
+  }
+
+  if (authin.len &gt; 0) if (authin.s[authin.len - 1] == '\r') --authin.len;
+  authin.s[authin.len] = 0;
+
+  if (*authin.s == '*' &amp;&amp; *(authin.s + 1) == 0) return authup_err("authabrt");
+  if (authin.len == 0) return authup_err("input");
+}
+
+static int b64decode2(char *c,int i,stralloc *sa) {
+  return b64decode((const unsigned char *)c,i,sa);
+}
+
+static void auth_login(char *arg) {
+  int r;
+
+  if (*arg) {
+    if ((r = b64decode2(arg,str_len(arg),&amp;username)) == 1) return authup_err("input");
+  }
+  else {
+    smtp_out("334 VXNlcm5hbWU6"); /* Username: */
+    smtp_authgetl();
+    if ((r = b64decode2(authin.s,authin.len,&amp;username)) == 1) return authup_err("input");
+  }
+  if (r == -1) authup_die("nomem");
+
+  smtp_out("334 UGFzc3dvcmQ6"); /* Password: */
+
+  smtp_authgetl();
+  if ((r = b64decode2(authin.s,authin.len,&amp;password)) == 1) return authup_err("input");
+  if (r == -1) authup_die("nomem");
+
+  if (!username.len || !password.len) return authup_err("input");
+  checkpassword(&amp;username,&amp;password,&amp;timestamp);
+}
+
+static stralloc resp = {0};
+
+static void auth_plain(char *arg) {
+  int r, id = 0;
+
+  if (*arg) {
+    if ((r = b64decode2(arg,str_len(arg),&amp;resp)) == 1) return authup_err("input");
+  }
+  else {
+    smtp_out("334 ");
+    smtp_authgetl();
+    if ((r = b64decode2(authin.s,authin.len,&amp;resp)) == 1) return authup_err("input");
+  }
+  if (r == -1) authup_die("nomem");
+  append0(&amp;resp);
+  while (resp.s[id]) id++; /* ignore authorize-id */
+
+  if (resp.len &gt; id + 1)
+    copys(&amp;username,resp.s + id + 1);
+  if (resp.len &gt; id + username.len + 2)
+    copys(&amp;password,resp.s + id + username.len + 2);
+
+  if (!username.len || !password.len) return authup_err("input");
+  checkpassword(&amp;username,&amp;password,&amp;timestamp);
+}
+
+static void smtp_auth(char *arg) {
+  int i;
+  char *cmd = arg;
+
+  if (tls_level &gt;= UCSPITLS_REQUIRED &amp;&amp; !in_tls) return authup_err("needtls");
+
+  i = str_chr(cmd,' ');
+  arg = cmd + i;
+  while (*arg == ' ') ++arg;
+  cmd[i] = 0;
+
+  if (case_equals("login",cmd)) return auth_login(arg);
+  if (case_equals("plain",cmd)) return auth_plain(arg);
+  return authup_err("noauth");
+}
+
+static void smtp_help() {
+  smtp_out("214 " PROGNAME " home page: " HOMEPAGE);
+}
+
+static void smtp_noop() {
+  smtp_out("250 ok");
+}
+
+static 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 }
+};
+
+static 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 (*greet)();
+  int auth_tries_remaining;
+  struct commands *c;
+};
+
+static struct protocol p[] = {
+  { "pop3", "",     pop3_format_capa, pop3_auth_error, pop3_greet, 1, pop3commands }
+, { "smtp", "250-", smtp_format_ehlo, smtp_auth_error, smtp_greet, 5, smtpcommands }
+, { 0,      "",     0,                die_usage,       die_usage,  0, 0            }
+};
+
+static int control_readgreeting(char *p) {
+  stralloc file = {0};
+  int retval;
+
+  copys(&amp;file,"control/");
+  cats(&amp;file,p);
+  cats(&amp;file,"greeting");
+  append0(&amp;file);
+
+  retval = control_rldef(&amp;greeting,file.s,1,(char *) 0);
+  if (retval != 1) retval = -1;
+
+  append0(&amp;greeting);
+
+  return retval;
+}
+
+static int control_readtimeout(char *p) {
+  stralloc file = {0};
+
+  copys(&amp;file,"control/timeout");
+  cats(&amp;file,p);
+  cats(&amp;file,"d");
+  append0(&amp;file);
+
+  return control_readint(&amp;timeout,file.s);
+}
+
+static int control_readcapabilities(struct protocol p) {
+  stralloc file = {0};
+  stralloc lines = {0};
+  int linestart;
+  int pos;
+
+  copys(&amp;file,"control/");
+  cats(&amp;file,p.name);
+  cats(&amp;file,"capabilities");
+  append0(&amp;file);
+
+  if (control_readfile(&amp;lines,file.s,0) != 1) return -1;
+
+  blank(&amp;capabilities);
+  for (linestart = 0, pos = 0; pos &lt; lines.len; pos++) {
+    if (lines.s[pos] == '\0') {
+      cats(&amp;capabilities,p.cap_prefix);
+      cats(&amp;capabilities,lines.s+linestart);
+      cats(&amp;capabilities,"\r\n");
+      linestart = pos + 1;
+    }
+  }
+  p.cap_format_response(&amp;capabilities);
+  append0(&amp;capabilities);
+
+  return 1;
+}
+
+static void doprotocol(struct protocol p) {
+  protocol_error = p.error;
+
+  logstart(p.name);
+
+  if (auth_tries_remaining == 0) auth_tries_remaining = p.auth_tries_remaining;
+  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");
+  p.greet();
+  commands(&amp;ssin,p.c);
+  authup_die("protocol");
+}
+
+int main(int argc,char **argv) {
+  char *protocol;
+  int opt;
+  int i;
+
+  sig_alarmcatch(die);
+  sig_pipeignore();
+
+  stralloc_set_die(die_nomem);
+
+  auth_tries_remaining = 0;
+  while ((opt = getopt(argc,argv,"t:")) != opteof) {
+    switch (opt) {
+      case 't':
+        if (!scan_ulong(optarg,&amp;auth_tries_remaining)) die_usage();
+        break;
+      default:
+        die_usage();
+    }
+  }
+  argc -= optind;
+  argv += optind;
+
+  if (!*argv) die_usage();
+
+  protocol = argv[0];
+  if (!protocol) die_usage();
+
+  childargs = argv + 1;
+  if (!*childargs) die_usage();
+
+  tls_level = ucspitls_level_configured();
+
+  authup_pid = format_pid(unistd_getpid());
+
+  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..2c016a6
--- /dev/null
+++ checknotroot.8
@@ -0,0 +1,32 @@
+.TH CHECKNOTROOT 8 2018-12-01
+.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 &lt;schmonz-web-acceptutils@schmonz.com&gt;
+.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(&amp;sserr,"checknotroot: ");
+  substdio_puts(&amp;sserr,s);
+  substdio_puts(&amp;sserr,"\n");
+  substdio_flush(&amp;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..2ea1960
--- /dev/null
+++ fixsmtpio.8
@@ -0,0 +1,260 @@
+.TH FIXSMTPIO 8 2020-12-11
+.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.
+.P
+In a typical configuration,
+.B fixsmtpio
+enables TLS for
+.BR qmail-smtpd ,
+modernizes responses from
+.BR ofmipd ,
+and makes both programs behave as though they were designed to run under
+.BR authup(8) .
+.B fixsmtpio
+accomplishes this 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 fixsmtpio
+exits 18 when
+.I control/fixsmtpio
+exists and fails to parse.
+.P
+Part of
+.BR fixsmtpio 's
+job is to return distinct exit codes for distinct conditions.
+For instance,
+.B authup
+needs to distinguish
+.B checkpassword
+exiting 1 (to reject a password)
+from
+.B ofmipd
+or
+.B qmail-smtpd
+exiting 1 (on any error).
+When running under
+.BR authup ,
+.B fixsmtpio
+can and should be configured to exit uniquely for each SMTP condition:
+.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"
+When running under
+.B "sslserver -n"
+or
+.BR "s6-ucspitlsd" ,
+.B fixsmtpio
+can offer TLS.
+Set
+.B UCSPITLS
+to the empty string to permit peers to negotiate TLS before transferring messages.
+If the peer has negotiated TLS,
+.B FIXSMTPIOTLS
+will be set for use by filter rules.
+.P
+When running under
+.BR authup ,
+.B AUTHUP_USER
+will be set for use by filter rules
+(such as those recommended in
+.BR "EXIT CODES" ).
+.P
+If
+.B DISABLETLS
+is set, the presence and value of
+.B UCSPITLS
+will be ignored.
+.P
+If
+.B NOFIXSMTPIO
+is set,
+.B fixsmtpio
+will simply replace itself with
+.IR prog .
+.P
+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:
+.P
+.I [env]:event:[request-prepend]:response-line-glob:[exitcode]:[response]
+.P
+(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
+"&amp;fixsmtpio"
+are reserved for special handling.
+
+"&amp;fixsmtpio_noop"
+causes the response to be sent as is.
+
+"&amp;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
+.RS 10
+.TP 6
+.BR HELP :
+.br
+Prepend this program's home page,
+in an attempt to direct support requests to
+.BR AUTHOR .
+Please enable it!
+.TP 6
+.BR EHLO ,
+.BR HELO ,
+.BR QUIT ,
+and "greeting":
+.br
+Include
+.I smtpgreeting
+in the response,
+matching what
+.B qmail-smtpd
+already does.
+This brings
+.B ofmipd
+up to par.
+.RE
+.TP 0
+.P
+Rules are applied in the order written. For instance, if two rules
+match, the later rule re-modifies the response returned by the
+earlier rule.
+If multiple matching rules for an event set
+.BR exitcode ,
+the last one wins.
+.P
+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
+must terminate TLS in order to observe and modify requests and responses.
+If you rely on
+.B qmail-smtpd
+being patched to support STARTTLS directly,
+please share your use case with the author.
+.SH "EXAMPLES"
+See
+.IR https://schmonz.com/qmail/acceptutils .
+.SH "AUTHOR"
+.B Amitai Schleier &lt;schmonz-web-acceptutils@schmonz.com&gt;
+.SH "SEE ALSO"
+authup(8),
+sslserver(1),
+s6-ucspitlsd,
+ucspi-tls(2),
+checkpassword(8),
+checknotroot(8),
+qmail-smtpd(8),
+ofmipd(8),
+qmail-qfilter-queue(8),
+fnmatch(3),
+fixcrio(1),
+spamdyke.
diff --git fixsmtpio.c fixsmtpio.c
new file mode 100644
index 0000000..f05f785
--- /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(&amp;greeting,"control/smtpgreeting");
+  rules = load_filter_rules();
+
+  be_proxy(&amp;greeting,rules,argv);
+}
diff --git fixsmtpio.h fixsmtpio.h
new file mode 100644
index 0000000..fd5be60
--- /dev/null
+++ fixsmtpio.h
@@ -0,0 +1,32 @@
+#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         "&amp;" PROGNAME "_fixup"
+#define REQUEST_PASSTHRU         ""
+#define REQUEST_NOOP             "NOOP "
+
+#define RESPONSELINE_NOCHANGE    "&amp;" PROGNAME "_noop"
+
+#define BEGIN_STARTTLS_NOW       -2
+#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-&gt;len) return;
+
+  append0(value);
+  s = (char *)alloc(value-&gt;len);
+  str_copy(s, value-&gt;s);
+  blank(value);
+
+  switch (*fields_seen) {
+    case 1: rule-&gt;env                = s; break;
+    case 2: rule-&gt;event              = s; break;
+    case 3: rule-&gt;request_prepend    = s; break;
+    case 4: rule-&gt;response_line_glob = s; break;
+    case 5:
+      if (!scan_ulong(s,&amp;rule-&gt;exitcode))
+        rule-&gt;exitcode = 777;
+                                          break;
+    case 6: rule-&gt;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-&gt;next                = 0;
+
+  rule-&gt;env                 = 0;
+  rule-&gt;event               = 0;
+  rule-&gt;request_prepend     = 0;
+  rule-&gt;response_line_glob  = 0;
+  rule-&gt;exitcode            = EXIT_LATER_NORMALLY;
+  rule-&gt;response            = 0;
+
+  for (i = 0; i &lt; line_length; i++) {
+    char c = line[i];
+    if (':' == c &amp;&amp; fields_seen &lt; 5) parse_field(&amp;fields_seen, &amp;value, rule);
+    else append(&amp;value, &amp;c);
+  }
+  parse_field(&amp;fields_seen, &amp;value, rule);
+
+  if (fields_seen &lt; 6)            return 0;
+  if (!rule-&gt;event)               return 0;
+  if (!rule-&gt;response_line_glob)  return 0;
+  if ( rule-&gt;exitcode &gt; 255)      return 0;
+  if ( rule-&gt;response) {
+    if (!case_diffs(rule-&gt;event,"clienteof"))
+                                  return 0;
+    if (want_munge_internally(rule-&gt;response)
+        &amp;&amp; !munge_line_fn(rule-&gt;event))
+                                  return 0;
+    if (str_start(rule-&gt;response,"&amp;fixsmtpio")
+        &amp;&amp; !want_munge_internally(rule-&gt;response)
+        &amp;&amp; !want_leave_line_as_is(rule-&gt;response))
+                                  return 0;
+  } else {
+    rule-&gt;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(&amp;sserr,PROGNAME ":");
+  if (caller) {
+    substdio_puts(&amp;sserr,caller);
+    substdio_puts(&amp;sserr,":");
+  }
+  if (alloc_fn) {
+    substdio_puts(&amp;sserr,alloc_fn);
+    substdio_puts(&amp;sserr,":");
+  }
+  substdio_puts(&amp;sserr," ");
+  substdio_puts(&amp;sserr,s);
+  substdio_putsflush(&amp;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 (&amp;sserr,logstamp-&gt;s,logstamp-&gt;len);
+  substdio_put (&amp;sserr,&amp;logprefix,1); substdio_puts(&amp;sserr,": ");
+  substdio_put (&amp;sserr,sa-&gt;s,sa-&gt;len);
+  if (!ends_with_newline(sa)) substdio_puts(&amp;sserr,"\n");
+  substdio_flush(&amp;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..5163ac6
--- /dev/null
+++ fixsmtpio_eventq.c
@@ -0,0 +1,59 @@
+#include "alloc.h"
+#include "str.h"
+#include "fixsmtpio.h"
+#include "fixsmtpio_die.h"
+
+#include "fixsmtpio_eventq.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(&amp;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-&gt;event = eventq_alloc_event(event);
+  str_copy(e-&gt;event,event);
+  TAILQ_INSERT_TAIL(&amp;head, e, nodes);
+}
+
+const char *eventq_get() {
+  const char *event;
+  node_t *e;
+  if (TAILQ_EMPTY(&amp;head)) {
+    event = eventq_alloc_event(EVENT_TIMEOUT);
+    str_copy(event,EVENT_TIMEOUT);
+  } else {
+    e = TAILQ_FIRST(&amp;head);
+    event = e-&gt;event;
+    TAILQ_REMOVE(&amp;head, e, nodes);
+    alloc_free(e);
+  }
+
+  return event;
+}
diff --git fixsmtpio_eventq.h fixsmtpio_eventq.h
new file mode 100644
index 0000000..d193505
--- /dev/null
+++ fixsmtpio_eventq.h
@@ -0,0 +1,858 @@
+void eventq_put(const char *);
+const char *eventq_get(void);
+
+#define NULL 0
+/*	$NetBSD: queue.h,v 1.74 2019/03/23 12:01:18 maxv Exp $	*/
+
+/*
+ * Copyright (c) 1991, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)queue.h	8.5 (Berkeley) 8/20/94
+ */
+
+#ifndef	_SYS_QUEUE_H_
+#define	_SYS_QUEUE_H_
+
+/*
+ * This file defines five types of data structures: singly-linked lists,
+ * lists, simple queues, tail queues, and circular queues.
+ *
+ * A singly-linked list is headed by a single forward pointer. The
+ * elements are singly linked for minimum space and pointer manipulation
+ * overhead at the expense of O(n) removal for arbitrary elements. New
+ * elements can be added to the list after an existing element or at the
+ * head of the list.  Elements being removed from the head of the list
+ * should use the explicit macro for this purpose for optimum
+ * efficiency. A singly-linked list may only be traversed in the forward
+ * direction.  Singly-linked lists are ideal for applications with large
+ * datasets and few or no removals or for implementing a LIFO queue.
+ *
+ * A list is headed by a single forward pointer (or an array of forward
+ * pointers for a hash table header). The elements are doubly linked
+ * so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before
+ * or after an existing element or at the head of the list. A list
+ * may only be traversed in the forward direction.
+ *
+ * A simple queue is headed by a pair of pointers, one the head of the
+ * list and the other to the tail of the list. The elements are singly
+ * linked to save space, so elements can only be removed from the
+ * head of the list. New elements can be added to the list after
+ * an existing element, at the head of the list, or at the end of the
+ * list. A simple queue may only be traversed in the forward direction.
+ *
+ * A tail queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or
+ * after an existing element, at the head of the list, or at the end of
+ * the list. A tail queue may be traversed in either direction.
+ *
+ * A circle queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or after
+ * an existing element, at the head of the list, or at the end of the list.
+ * A circle queue may be traversed in either direction, but has a more
+ * complex end of list detection.
+ *
+ * For details on the use of these macros, see the queue(3) manual page.
+ */
+
+/*
+ * Include the definition of NULL only on NetBSD because sys/null.h
+ * is not available elsewhere.  This conditional makes the header
+ * portable and it can simply be dropped verbatim into any system.
+ * The caveat is that on other systems some other header
+ * must provide NULL before the macros can be used.
+ */
+#ifdef __NetBSD__
+#include &lt;sys/null.h&gt;
+#endif
+
+#if defined(_KERNEL) &amp;&amp; defined(_KERNEL_OPT)
+#include "opt_diagnostic.h"
+#ifdef DIAGNOSTIC
+#define QUEUEDEBUG	1
+#endif
+#endif
+
+#if defined(QUEUEDEBUG)
+# if defined(_KERNEL)
+#  define QUEUEDEBUG_ABORT(...) panic(__VA_ARGS__)
+# else
+#  include &lt;err.h&gt;
+#  define QUEUEDEBUG_ABORT(...) err(1, __VA_ARGS__)
+# endif
+#endif
+
+/*
+ * Singly-linked List definitions.
+ */
+#define	SLIST_HEAD(name, type)						\
+struct name {								\
+	struct type *slh_first;	/* first element */			\
+}
+
+#define	SLIST_HEAD_INITIALIZER(head)					\
+	{ NULL }
+
+#define	SLIST_ENTRY(type)						\
+struct {								\
+	struct type *sle_next;	/* next element */			\
+}
+
+/*
+ * Singly-linked List access methods.
+ */
+#define	SLIST_FIRST(head)	((head)-&gt;slh_first)
+#define	SLIST_END(head)		NULL
+#define	SLIST_EMPTY(head)	((head)-&gt;slh_first == NULL)
+#define	SLIST_NEXT(elm, field)	((elm)-&gt;field.sle_next)
+
+#define	SLIST_FOREACH(var, head, field)					\
+	for((var) = (head)-&gt;slh_first;					\
+	    (var) != SLIST_END(head);					\
+	    (var) = (var)-&gt;field.sle_next)
+
+#define	SLIST_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = SLIST_FIRST((head));				\
+	    (var) != SLIST_END(head) &amp;&amp;					\
+	    ((tvar) = SLIST_NEXT((var), field), 1);			\
+	    (var) = (tvar))
+
+/*
+ * Singly-linked List functions.
+ */
+#define	SLIST_INIT(head) do {						\
+	(head)-&gt;slh_first = SLIST_END(head);				\
+} while (/*CONSTCOND*/0)
+
+#define	SLIST_INSERT_AFTER(slistelm, elm, field) do {			\
+	(elm)-&gt;field.sle_next = (slistelm)-&gt;field.sle_next;		\
+	(slistelm)-&gt;field.sle_next = (elm);				\
+} while (/*CONSTCOND*/0)
+
+#define	SLIST_INSERT_HEAD(head, elm, field) do {			\
+	(elm)-&gt;field.sle_next = (head)-&gt;slh_first;			\
+	(head)-&gt;slh_first = (elm);					\
+} while (/*CONSTCOND*/0)
+
+#define	SLIST_REMOVE_AFTER(slistelm, field) do {			\
+	(slistelm)-&gt;field.sle_next =					\
+	    SLIST_NEXT(SLIST_NEXT((slistelm), field), field);		\
+} while (/*CONSTCOND*/0)
+
+#define	SLIST_REMOVE_HEAD(head, field) do {				\
+	(head)-&gt;slh_first = (head)-&gt;slh_first-&gt;field.sle_next;		\
+} while (/*CONSTCOND*/0)
+
+#define	SLIST_REMOVE(head, elm, type, field) do {			\
+	if ((head)-&gt;slh_first == (elm)) {				\
+		SLIST_REMOVE_HEAD((head), field);			\
+	}								\
+	else {								\
+		struct type *curelm = (head)-&gt;slh_first;		\
+		while(curelm-&gt;field.sle_next != (elm))			\
+			curelm = curelm-&gt;field.sle_next;		\
+		curelm-&gt;field.sle_next =				\
+		    curelm-&gt;field.sle_next-&gt;field.sle_next;		\
+	}								\
+} while (/*CONSTCOND*/0)
+
+
+/*
+ * List definitions.
+ */
+#define	LIST_HEAD(name, type)						\
+struct name {								\
+	struct type *lh_first;	/* first element */			\
+}
+
+#define	LIST_HEAD_INITIALIZER(head)					\
+	{ NULL }
+
+#define	LIST_ENTRY(type)						\
+struct {								\
+	struct type *le_next;	/* next element */			\
+	struct type **le_prev;	/* address of previous next element */	\
+}
+
+/*
+ * List access methods.
+ */
+#define	LIST_FIRST(head)		((head)-&gt;lh_first)
+#define	LIST_END(head)			NULL
+#define	LIST_EMPTY(head)		((head)-&gt;lh_first == LIST_END(head))
+#define	LIST_NEXT(elm, field)		((elm)-&gt;field.le_next)
+
+#define	LIST_FOREACH(var, head, field)					\
+	for ((var) = ((head)-&gt;lh_first);				\
+	    (var) != LIST_END(head);					\
+	    (var) = ((var)-&gt;field.le_next))
+
+#define	LIST_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = LIST_FIRST((head));				\
+	    (var) != LIST_END(head) &amp;&amp;					\
+	    ((tvar) = LIST_NEXT((var), field), 1);			\
+	    (var) = (tvar))
+
+#define	LIST_MOVE(head1, head2, field) do {				\
+	LIST_INIT((head2));						\
+	if (!LIST_EMPTY((head1))) {					\
+		(head2)-&gt;lh_first = (head1)-&gt;lh_first;			\
+		(head2)-&gt;lh_first-&gt;field.le_prev = &amp;(head2)-&gt;lh_first;	\
+		LIST_INIT((head1));					\
+	}								\
+} while (/*CONSTCOND*/0)
+
+/*
+ * List functions.
+ */
+#if defined(QUEUEDEBUG)
+#define	QUEUEDEBUG_LIST_INSERT_HEAD(head, elm, field)			\
+	if ((head)-&gt;lh_first &amp;&amp;						\
+	    (head)-&gt;lh_first-&gt;field.le_prev != &amp;(head)-&gt;lh_first)	\
+		QUEUEDEBUG_ABORT("LIST_INSERT_HEAD %p %s:%d", (head),	\
+		    __FILE__, __LINE__);
+#define	QUEUEDEBUG_LIST_OP(elm, field)					\
+	if ((elm)-&gt;field.le_next &amp;&amp;					\
+	    (elm)-&gt;field.le_next-&gt;field.le_prev !=			\
+	    &amp;(elm)-&gt;field.le_next)					\
+		QUEUEDEBUG_ABORT("LIST_* forw %p %s:%d", (elm),		\
+		    __FILE__, __LINE__);				\
+	if (*(elm)-&gt;field.le_prev != (elm))				\
+		QUEUEDEBUG_ABORT("LIST_* back %p %s:%d", (elm),		\
+		    __FILE__, __LINE__);
+#define	QUEUEDEBUG_LIST_POSTREMOVE(elm, field)				\
+	(elm)-&gt;field.le_next = (void *)1L;				\
+	(elm)-&gt;field.le_prev = (void *)1L;
+#else
+#define	QUEUEDEBUG_LIST_INSERT_HEAD(head, elm, field)
+#define	QUEUEDEBUG_LIST_OP(elm, field)
+#define	QUEUEDEBUG_LIST_POSTREMOVE(elm, field)
+#endif
+
+#define	LIST_INIT(head) do {						\
+	(head)-&gt;lh_first = LIST_END(head);				\
+} while (/*CONSTCOND*/0)
+
+#define	LIST_INSERT_AFTER(listelm, elm, field) do {			\
+	QUEUEDEBUG_LIST_OP((listelm), field)				\
+	if (((elm)-&gt;field.le_next = (listelm)-&gt;field.le_next) != 	\
+	    LIST_END(head))						\
+		(listelm)-&gt;field.le_next-&gt;field.le_prev =		\
+		    &amp;(elm)-&gt;field.le_next;				\
+	(listelm)-&gt;field.le_next = (elm);				\
+	(elm)-&gt;field.le_prev = &amp;(listelm)-&gt;field.le_next;		\
+} while (/*CONSTCOND*/0)
+
+#define	LIST_INSERT_BEFORE(listelm, elm, field) do {			\
+	QUEUEDEBUG_LIST_OP((listelm), field)				\
+	(elm)-&gt;field.le_prev = (listelm)-&gt;field.le_prev;		\
+	(elm)-&gt;field.le_next = (listelm);				\
+	*(listelm)-&gt;field.le_prev = (elm);				\
+	(listelm)-&gt;field.le_prev = &amp;(elm)-&gt;field.le_next;		\
+} while (/*CONSTCOND*/0)
+
+#define	LIST_INSERT_HEAD(head, elm, field) do {				\
+	QUEUEDEBUG_LIST_INSERT_HEAD((head), (elm), field)		\
+	if (((elm)-&gt;field.le_next = (head)-&gt;lh_first) != LIST_END(head))\
+		(head)-&gt;lh_first-&gt;field.le_prev = &amp;(elm)-&gt;field.le_next;\
+	(head)-&gt;lh_first = (elm);					\
+	(elm)-&gt;field.le_prev = &amp;(head)-&gt;lh_first;			\
+} while (/*CONSTCOND*/0)
+
+#define	LIST_REMOVE(elm, field) do {					\
+	QUEUEDEBUG_LIST_OP((elm), field)				\
+	if ((elm)-&gt;field.le_next != NULL)				\
+		(elm)-&gt;field.le_next-&gt;field.le_prev = 			\
+		    (elm)-&gt;field.le_prev;				\
+	*(elm)-&gt;field.le_prev = (elm)-&gt;field.le_next;			\
+	QUEUEDEBUG_LIST_POSTREMOVE((elm), field)			\
+} while (/*CONSTCOND*/0)
+
+#define LIST_REPLACE(elm, elm2, field) do {				\
+	if (((elm2)-&gt;field.le_next = (elm)-&gt;field.le_next) != NULL)	\
+		(elm2)-&gt;field.le_next-&gt;field.le_prev =			\
+		    &amp;(elm2)-&gt;field.le_next;				\
+	(elm2)-&gt;field.le_prev = (elm)-&gt;field.le_prev;			\
+	*(elm2)-&gt;field.le_prev = (elm2);				\
+	QUEUEDEBUG_LIST_POSTREMOVE((elm), field)			\
+} while (/*CONSTCOND*/0)
+
+/*
+ * Simple queue definitions.
+ */
+#define	SIMPLEQ_HEAD(name, type)					\
+struct name {								\
+	struct type *sqh_first;	/* first element */			\
+	struct type **sqh_last;	/* addr of last next element */		\
+}
+
+#define	SIMPLEQ_HEAD_INITIALIZER(head)					\
+	{ NULL, &amp;(head).sqh_first }
+
+#define	SIMPLEQ_ENTRY(type)						\
+struct {								\
+	struct type *sqe_next;	/* next element */			\
+}
+
+/*
+ * Simple queue access methods.
+ */
+#define	SIMPLEQ_FIRST(head)		((head)-&gt;sqh_first)
+#define	SIMPLEQ_END(head)		NULL
+#define	SIMPLEQ_EMPTY(head)		((head)-&gt;sqh_first == SIMPLEQ_END(head))
+#define	SIMPLEQ_NEXT(elm, field)	((elm)-&gt;field.sqe_next)
+
+#define	SIMPLEQ_FOREACH(var, head, field)				\
+	for ((var) = ((head)-&gt;sqh_first);				\
+	    (var) != SIMPLEQ_END(head);					\
+	    (var) = ((var)-&gt;field.sqe_next))
+
+#define	SIMPLEQ_FOREACH_SAFE(var, head, field, next)			\
+	for ((var) = ((head)-&gt;sqh_first);				\
+	    (var) != SIMPLEQ_END(head) &amp;&amp;				\
+	    ((next = ((var)-&gt;field.sqe_next)), 1);			\
+	    (var) = (next))
+
+/*
+ * Simple queue functions.
+ */
+#define	SIMPLEQ_INIT(head) do {						\
+	(head)-&gt;sqh_first = NULL;					\
+	(head)-&gt;sqh_last = &amp;(head)-&gt;sqh_first;				\
+} while (/*CONSTCOND*/0)
+
+#define	SIMPLEQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)-&gt;field.sqe_next = (head)-&gt;sqh_first) == NULL)	\
+		(head)-&gt;sqh_last = &amp;(elm)-&gt;field.sqe_next;		\
+	(head)-&gt;sqh_first = (elm);					\
+} while (/*CONSTCOND*/0)
+
+#define	SIMPLEQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)-&gt;field.sqe_next = NULL;					\
+	*(head)-&gt;sqh_last = (elm);					\
+	(head)-&gt;sqh_last = &amp;(elm)-&gt;field.sqe_next;			\
+} while (/*CONSTCOND*/0)
+
+#define	SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)-&gt;field.sqe_next = (listelm)-&gt;field.sqe_next) == NULL)\
+		(head)-&gt;sqh_last = &amp;(elm)-&gt;field.sqe_next;		\
+	(listelm)-&gt;field.sqe_next = (elm);				\
+} while (/*CONSTCOND*/0)
+
+#define	SIMPLEQ_REMOVE_HEAD(head, field) do {				\
+	if (((head)-&gt;sqh_first = (head)-&gt;sqh_first-&gt;field.sqe_next) == NULL) \
+		(head)-&gt;sqh_last = &amp;(head)-&gt;sqh_first;			\
+} while (/*CONSTCOND*/0)
+
+#define SIMPLEQ_REMOVE_AFTER(head, elm, field) do {			\
+	if (((elm)-&gt;field.sqe_next = (elm)-&gt;field.sqe_next-&gt;field.sqe_next) \
+	    == NULL)							\
+		(head)-&gt;sqh_last = &amp;(elm)-&gt;field.sqe_next;		\
+} while (/*CONSTCOND*/0)
+
+#define	SIMPLEQ_REMOVE(head, elm, type, field) do {			\
+	if ((head)-&gt;sqh_first == (elm)) {				\
+		SIMPLEQ_REMOVE_HEAD((head), field);			\
+	} else {							\
+		struct type *curelm = (head)-&gt;sqh_first;		\
+		while (curelm-&gt;field.sqe_next != (elm))			\
+			curelm = curelm-&gt;field.sqe_next;		\
+		if ((curelm-&gt;field.sqe_next =				\
+			curelm-&gt;field.sqe_next-&gt;field.sqe_next) == NULL) \
+			    (head)-&gt;sqh_last = &amp;(curelm)-&gt;field.sqe_next; \
+	}								\
+} while (/*CONSTCOND*/0)
+
+#define	SIMPLEQ_CONCAT(head1, head2) do {				\
+	if (!SIMPLEQ_EMPTY((head2))) {					\
+		*(head1)-&gt;sqh_last = (head2)-&gt;sqh_first;		\
+		(head1)-&gt;sqh_last = (head2)-&gt;sqh_last;		\
+		SIMPLEQ_INIT((head2));					\
+	}								\
+} while (/*CONSTCOND*/0)
+
+#define	SIMPLEQ_LAST(head, type, field)					\
+	(SIMPLEQ_EMPTY((head)) ?						\
+		NULL :							\
+	        ((struct type *)(void *)				\
+		((char *)((head)-&gt;sqh_last) - offsetof(struct type, field))))
+
+/*
+ * Tail queue definitions.
+ */
+#define	_TAILQ_HEAD(name, type, qual)					\
+struct name {								\
+	qual type *tqh_first;		/* first element */		\
+	qual type *qual *tqh_last;	/* addr of last next element */	\
+}
+#define TAILQ_HEAD(name, type)	_TAILQ_HEAD(name, struct type,)
+
+#define	TAILQ_HEAD_INITIALIZER(head)					\
+	{ TAILQ_END(head), &amp;(head).tqh_first }
+
+#define	_TAILQ_ENTRY(type, qual)					\
+struct {								\
+	qual type *tqe_next;		/* next element */		\
+	qual type *qual *tqe_prev;	/* address of previous next element */\
+}
+#define TAILQ_ENTRY(type)	_TAILQ_ENTRY(struct type,)
+
+/*
+ * Tail queue access methods.
+ */
+#define	TAILQ_FIRST(head)		((head)-&gt;tqh_first)
+#define	TAILQ_END(head)			(NULL)
+#define	TAILQ_NEXT(elm, field)		((elm)-&gt;field.tqe_next)
+#define	TAILQ_LAST(head, headname) \
+	(*(((struct headname *)(void *)((head)-&gt;tqh_last))-&gt;tqh_last))
+#define	TAILQ_PREV(elm, headname, field) \
+	(*(((struct headname *)(void *)((elm)-&gt;field.tqe_prev))-&gt;tqh_last))
+#define	TAILQ_EMPTY(head)		(TAILQ_FIRST(head) == TAILQ_END(head))
+
+
+#define	TAILQ_FOREACH(var, head, field)					\
+	for ((var) = ((head)-&gt;tqh_first);				\
+	    (var) != TAILQ_END(head);					\
+	    (var) = ((var)-&gt;field.tqe_next))
+
+#define	TAILQ_FOREACH_SAFE(var, head, field, next)			\
+	for ((var) = ((head)-&gt;tqh_first);				\
+	    (var) != TAILQ_END(head) &amp;&amp;					\
+	    ((next) = TAILQ_NEXT(var, field), 1); (var) = (next))
+
+#define	TAILQ_FOREACH_REVERSE(var, head, headname, field)		\
+	for ((var) = TAILQ_LAST((head), headname);			\
+	    (var) != TAILQ_END(head);					\
+	    (var) = TAILQ_PREV((var), headname, field))
+
+#define	TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, prev)	\
+	for ((var) = TAILQ_LAST((head), headname);			\
+	    (var) != TAILQ_END(head) &amp;&amp; 				\
+	    ((prev) = TAILQ_PREV((var), headname, field), 1); (var) = (prev))
+
+/*
+ * Tail queue functions.
+ */
+#if defined(QUEUEDEBUG)
+#define	QUEUEDEBUG_TAILQ_INSERT_HEAD(head, elm, field)			\
+	if ((head)-&gt;tqh_first &amp;&amp;					\
+	    (head)-&gt;tqh_first-&gt;field.tqe_prev != &amp;(head)-&gt;tqh_first)	\
+		QUEUEDEBUG_ABORT("TAILQ_INSERT_HEAD %p %s:%d", (head),	\
+		    __FILE__, __LINE__);
+#define	QUEUEDEBUG_TAILQ_INSERT_TAIL(head, elm, field)			\
+	if (*(head)-&gt;tqh_last != NULL)					\
+		QUEUEDEBUG_ABORT("TAILQ_INSERT_TAIL %p %s:%d", (head),	\
+		    __FILE__, __LINE__);
+#define	QUEUEDEBUG_TAILQ_OP(elm, field)					\
+	if ((elm)-&gt;field.tqe_next &amp;&amp;					\
+	    (elm)-&gt;field.tqe_next-&gt;field.tqe_prev !=			\
+	    &amp;(elm)-&gt;field.tqe_next)					\
+		QUEUEDEBUG_ABORT("TAILQ_* forw %p %s:%d", (elm),	\
+		    __FILE__, __LINE__);				\
+	if (*(elm)-&gt;field.tqe_prev != (elm))				\
+		QUEUEDEBUG_ABORT("TAILQ_* back %p %s:%d", (elm),	\
+		    __FILE__, __LINE__);
+#define	QUEUEDEBUG_TAILQ_PREREMOVE(head, elm, field)			\
+	if ((elm)-&gt;field.tqe_next == NULL &amp;&amp;				\
+	    (head)-&gt;tqh_last != &amp;(elm)-&gt;field.tqe_next)			\
+		QUEUEDEBUG_ABORT("TAILQ_PREREMOVE head %p elm %p %s:%d",\
+		    (head), (elm), __FILE__, __LINE__);
+#define	QUEUEDEBUG_TAILQ_POSTREMOVE(elm, field)				\
+	(elm)-&gt;field.tqe_next = (void *)1L;				\
+	(elm)-&gt;field.tqe_prev = (void *)1L;
+#else
+#define	QUEUEDEBUG_TAILQ_INSERT_HEAD(head, elm, field)
+#define	QUEUEDEBUG_TAILQ_INSERT_TAIL(head, elm, field)
+#define	QUEUEDEBUG_TAILQ_OP(elm, field)
+#define	QUEUEDEBUG_TAILQ_PREREMOVE(head, elm, field)
+#define	QUEUEDEBUG_TAILQ_POSTREMOVE(elm, field)
+#endif
+
+#define	TAILQ_INIT(head) do {						\
+	(head)-&gt;tqh_first = TAILQ_END(head);				\
+	(head)-&gt;tqh_last = &amp;(head)-&gt;tqh_first;				\
+} while (/*CONSTCOND*/0)
+
+#define	TAILQ_INSERT_HEAD(head, elm, field) do {			\
+	QUEUEDEBUG_TAILQ_INSERT_HEAD((head), (elm), field)		\
+	if (((elm)-&gt;field.tqe_next = (head)-&gt;tqh_first) != TAILQ_END(head))\
+		(head)-&gt;tqh_first-&gt;field.tqe_prev =			\
+		    &amp;(elm)-&gt;field.tqe_next;				\
+	else								\
+		(head)-&gt;tqh_last = &amp;(elm)-&gt;field.tqe_next;		\
+	(head)-&gt;tqh_first = (elm);					\
+	(elm)-&gt;field.tqe_prev = &amp;(head)-&gt;tqh_first;			\
+} while (/*CONSTCOND*/0)
+
+#define	TAILQ_INSERT_TAIL(head, elm, field) do {			\
+	QUEUEDEBUG_TAILQ_INSERT_TAIL((head), (elm), field)		\
+	(elm)-&gt;field.tqe_next = TAILQ_END(head);			\
+	(elm)-&gt;field.tqe_prev = (head)-&gt;tqh_last;			\
+	*(head)-&gt;tqh_last = (elm);					\
+	(head)-&gt;tqh_last = &amp;(elm)-&gt;field.tqe_next;			\
+} while (/*CONSTCOND*/0)
+
+#define	TAILQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	QUEUEDEBUG_TAILQ_OP((listelm), field)				\
+	if (((elm)-&gt;field.tqe_next = (listelm)-&gt;field.tqe_next) != 	\
+	    TAILQ_END(head))						\
+		(elm)-&gt;field.tqe_next-&gt;field.tqe_prev = 		\
+		    &amp;(elm)-&gt;field.tqe_next;				\
+	else								\
+		(head)-&gt;tqh_last = &amp;(elm)-&gt;field.tqe_next;		\
+	(listelm)-&gt;field.tqe_next = (elm);				\
+	(elm)-&gt;field.tqe_prev = &amp;(listelm)-&gt;field.tqe_next;		\
+} while (/*CONSTCOND*/0)
+
+#define	TAILQ_INSERT_BEFORE(listelm, elm, field) do {			\
+	QUEUEDEBUG_TAILQ_OP((listelm), field)				\
+	(elm)-&gt;field.tqe_prev = (listelm)-&gt;field.tqe_prev;		\
+	(elm)-&gt;field.tqe_next = (listelm);				\
+	*(listelm)-&gt;field.tqe_prev = (elm);				\
+	(listelm)-&gt;field.tqe_prev = &amp;(elm)-&gt;field.tqe_next;		\
+} while (/*CONSTCOND*/0)
+
+#define	TAILQ_REMOVE(head, elm, field) do {				\
+	QUEUEDEBUG_TAILQ_PREREMOVE((head), (elm), field)		\
+	QUEUEDEBUG_TAILQ_OP((elm), field)				\
+	if (((elm)-&gt;field.tqe_next) != TAILQ_END(head))			\
+		(elm)-&gt;field.tqe_next-&gt;field.tqe_prev = 		\
+		    (elm)-&gt;field.tqe_prev;				\
+	else								\
+		(head)-&gt;tqh_last = (elm)-&gt;field.tqe_prev;		\
+	*(elm)-&gt;field.tqe_prev = (elm)-&gt;field.tqe_next;			\
+	QUEUEDEBUG_TAILQ_POSTREMOVE((elm), field);			\
+} while (/*CONSTCOND*/0)
+
+#define TAILQ_REPLACE(head, elm, elm2, field) do {			\
+        if (((elm2)-&gt;field.tqe_next = (elm)-&gt;field.tqe_next) != 	\
+	    TAILQ_END(head))   						\
+                (elm2)-&gt;field.tqe_next-&gt;field.tqe_prev =		\
+                    &amp;(elm2)-&gt;field.tqe_next;				\
+        else								\
+                (head)-&gt;tqh_last = &amp;(elm2)-&gt;field.tqe_next;		\
+        (elm2)-&gt;field.tqe_prev = (elm)-&gt;field.tqe_prev;			\
+        *(elm2)-&gt;field.tqe_prev = (elm2);				\
+	QUEUEDEBUG_TAILQ_POSTREMOVE((elm), field);			\
+} while (/*CONSTCOND*/0)
+
+#define	TAILQ_CONCAT(head1, head2, field) do {				\
+	if (!TAILQ_EMPTY(head2)) {					\
+		*(head1)-&gt;tqh_last = (head2)-&gt;tqh_first;		\
+		(head2)-&gt;tqh_first-&gt;field.tqe_prev = (head1)-&gt;tqh_last;	\
+		(head1)-&gt;tqh_last = (head2)-&gt;tqh_last;			\
+		TAILQ_INIT((head2));					\
+	}								\
+} while (/*CONSTCOND*/0)
+
+/*
+ * Singly-linked Tail queue declarations.
+ */
+#define	STAILQ_HEAD(name, type)						\
+struct name {								\
+	struct type *stqh_first;	/* first element */		\
+	struct type **stqh_last;	/* addr of last next element */	\
+}
+
+#define	STAILQ_HEAD_INITIALIZER(head)					\
+	{ NULL, &amp;(head).stqh_first }
+
+#define	STAILQ_ENTRY(type)						\
+struct {								\
+	struct type *stqe_next;	/* next element */			\
+}
+
+/*
+ * Singly-linked Tail queue access methods.
+ */
+#define	STAILQ_FIRST(head)	((head)-&gt;stqh_first)
+#define	STAILQ_END(head)	NULL
+#define	STAILQ_NEXT(elm, field)	((elm)-&gt;field.stqe_next)
+#define	STAILQ_EMPTY(head)	(STAILQ_FIRST(head) == STAILQ_END(head))
+
+/*
+ * Singly-linked Tail queue functions.
+ */
+#define	STAILQ_INIT(head) do {						\
+	(head)-&gt;stqh_first = NULL;					\
+	(head)-&gt;stqh_last = &amp;(head)-&gt;stqh_first;				\
+} while (/*CONSTCOND*/0)
+
+#define	STAILQ_INSERT_HEAD(head, elm, field) do {			\
+	if (((elm)-&gt;field.stqe_next = (head)-&gt;stqh_first) == NULL)	\
+		(head)-&gt;stqh_last = &amp;(elm)-&gt;field.stqe_next;		\
+	(head)-&gt;stqh_first = (elm);					\
+} while (/*CONSTCOND*/0)
+
+#define	STAILQ_INSERT_TAIL(head, elm, field) do {			\
+	(elm)-&gt;field.stqe_next = NULL;					\
+	*(head)-&gt;stqh_last = (elm);					\
+	(head)-&gt;stqh_last = &amp;(elm)-&gt;field.stqe_next;			\
+} while (/*CONSTCOND*/0)
+
+#define	STAILQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	if (((elm)-&gt;field.stqe_next = (listelm)-&gt;field.stqe_next) == NULL)\
+		(head)-&gt;stqh_last = &amp;(elm)-&gt;field.stqe_next;		\
+	(listelm)-&gt;field.stqe_next = (elm);				\
+} while (/*CONSTCOND*/0)
+
+#define	STAILQ_REMOVE_HEAD(head, field) do {				\
+	if (((head)-&gt;stqh_first = (head)-&gt;stqh_first-&gt;field.stqe_next) == NULL) \
+		(head)-&gt;stqh_last = &amp;(head)-&gt;stqh_first;			\
+} while (/*CONSTCOND*/0)
+
+#define	STAILQ_REMOVE(head, elm, type, field) do {			\
+	if ((head)-&gt;stqh_first == (elm)) {				\
+		STAILQ_REMOVE_HEAD((head), field);			\
+	} else {							\
+		struct type *curelm = (head)-&gt;stqh_first;		\
+		while (curelm-&gt;field.stqe_next != (elm))			\
+			curelm = curelm-&gt;field.stqe_next;		\
+		if ((curelm-&gt;field.stqe_next =				\
+			curelm-&gt;field.stqe_next-&gt;field.stqe_next) == NULL) \
+			    (head)-&gt;stqh_last = &amp;(curelm)-&gt;field.stqe_next; \
+	}								\
+} while (/*CONSTCOND*/0)
+
+#define	STAILQ_FOREACH(var, head, field)				\
+	for ((var) = ((head)-&gt;stqh_first);				\
+		(var);							\
+		(var) = ((var)-&gt;field.stqe_next))
+
+#define	STAILQ_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = STAILQ_FIRST((head));				\
+	    (var) &amp;&amp; ((tvar) = STAILQ_NEXT((var), field), 1);		\
+	    (var) = (tvar))
+
+#define	STAILQ_CONCAT(head1, head2) do {				\
+	if (!STAILQ_EMPTY((head2))) {					\
+		*(head1)-&gt;stqh_last = (head2)-&gt;stqh_first;		\
+		(head1)-&gt;stqh_last = (head2)-&gt;stqh_last;		\
+		STAILQ_INIT((head2));					\
+	}								\
+} while (/*CONSTCOND*/0)
+
+#define	STAILQ_LAST(head, type, field)					\
+	(STAILQ_EMPTY((head)) ?						\
+		NULL :							\
+	        ((struct type *)(void *)				\
+		((char *)((head)-&gt;stqh_last) - offsetof(struct type, field))))
+
+
+#ifndef _KERNEL
+/*
+ * Circular queue definitions. Do not use. We still keep the macros
+ * for compatibility but because of pointer aliasing issues their use
+ * is discouraged!
+ */
+
+/*
+ * __launder_type():  We use this ugly hack to work around the compiler
+ * noticing that two types may not alias each other and elide tests in code.
+ * We hit this in the CIRCLEQ macros when comparing 'struct name *' and
+ * 'struct type *' (see CIRCLEQ_HEAD()).  Modern compilers (such as GCC
+ * 4.8) declare these comparisons as always false, causing the code to
+ * not run as designed.
+ *
+ * This hack is only to be used for comparisons and thus can be fully const.
+ * Do not use for assignment.
+ *
+ * If we ever choose to change the ABI of the CIRCLEQ macros, we could fix
+ * this by changing the head/tail sentinal values, but see the note above
+ * this one.
+ */
+static __inline const void * __launder_type(const void *);
+static __inline const void *
+__launder_type(const void *__x)
+{
+	__asm __volatile("" : "+r" (__x));
+	return __x;
+}
+
+#if defined(QUEUEDEBUG)
+#define QUEUEDEBUG_CIRCLEQ_HEAD(head, field)				\
+	if ((head)-&gt;cqh_first != CIRCLEQ_ENDC(head) &amp;&amp;			\
+	    (head)-&gt;cqh_first-&gt;field.cqe_prev != CIRCLEQ_ENDC(head))	\
+		QUEUEDEBUG_ABORT("CIRCLEQ head forw %p %s:%d", (head),	\
+		      __FILE__, __LINE__);				\
+	if ((head)-&gt;cqh_last != CIRCLEQ_ENDC(head) &amp;&amp;			\
+	    (head)-&gt;cqh_last-&gt;field.cqe_next != CIRCLEQ_ENDC(head))	\
+		QUEUEDEBUG_ABORT("CIRCLEQ head back %p %s:%d", (head),	\
+		      __FILE__, __LINE__);
+#define QUEUEDEBUG_CIRCLEQ_ELM(head, elm, field)			\
+	if ((elm)-&gt;field.cqe_next == CIRCLEQ_ENDC(head)) {		\
+		if ((head)-&gt;cqh_last != (elm))				\
+			QUEUEDEBUG_ABORT("CIRCLEQ elm last %p %s:%d",	\
+			    (elm), __FILE__, __LINE__);			\
+	} else {							\
+		if ((elm)-&gt;field.cqe_next-&gt;field.cqe_prev != (elm))	\
+			QUEUEDEBUG_ABORT("CIRCLEQ elm forw %p %s:%d",	\
+			    (elm), __FILE__, __LINE__);			\
+	}								\
+	if ((elm)-&gt;field.cqe_prev == CIRCLEQ_ENDC(head)) {		\
+		if ((head)-&gt;cqh_first != (elm))				\
+			QUEUEDEBUG_ABORT("CIRCLEQ elm first %p %s:%d",	\
+			    (elm), __FILE__, __LINE__);			\
+	} else {							\
+		if ((elm)-&gt;field.cqe_prev-&gt;field.cqe_next != (elm))	\
+			QUEUEDEBUG_ABORT("CIRCLEQ elm prev %p %s:%d",	\
+			    (elm), __FILE__, __LINE__);			\
+	}
+#define QUEUEDEBUG_CIRCLEQ_POSTREMOVE(elm, field)			\
+	(elm)-&gt;field.cqe_next = (void *)1L;				\
+	(elm)-&gt;field.cqe_prev = (void *)1L;
+#else
+#define QUEUEDEBUG_CIRCLEQ_HEAD(head, field)
+#define QUEUEDEBUG_CIRCLEQ_ELM(head, elm, field)
+#define QUEUEDEBUG_CIRCLEQ_POSTREMOVE(elm, field)
+#endif
+
+#define	CIRCLEQ_HEAD(name, type)					\
+struct name {								\
+	struct type *cqh_first;		/* first element */		\
+	struct type *cqh_last;		/* last element */		\
+}
+
+#define	CIRCLEQ_HEAD_INITIALIZER(head)					\
+	{ CIRCLEQ_END(&amp;head), CIRCLEQ_END(&amp;head) }
+
+#define	CIRCLEQ_ENTRY(type)						\
+struct {								\
+	struct type *cqe_next;		/* next element */		\
+	struct type *cqe_prev;		/* previous element */		\
+}
+
+/*
+ * Circular queue functions.
+ */
+#define	CIRCLEQ_INIT(head) do {						\
+	(head)-&gt;cqh_first = CIRCLEQ_END(head);				\
+	(head)-&gt;cqh_last = CIRCLEQ_END(head);				\
+} while (/*CONSTCOND*/0)
+
+#define	CIRCLEQ_INSERT_AFTER(head, listelm, elm, field) do {		\
+	QUEUEDEBUG_CIRCLEQ_HEAD((head), field)				\
+	QUEUEDEBUG_CIRCLEQ_ELM((head), (listelm), field)		\
+	(elm)-&gt;field.cqe_next = (listelm)-&gt;field.cqe_next;		\
+	(elm)-&gt;field.cqe_prev = (listelm);				\
+	if ((listelm)-&gt;field.cqe_next == CIRCLEQ_ENDC(head))		\
+		(head)-&gt;cqh_last = (elm);				\
+	else								\
+		(listelm)-&gt;field.cqe_next-&gt;field.cqe_prev = (elm);	\
+	(listelm)-&gt;field.cqe_next = (elm);				\
+} while (/*CONSTCOND*/0)
+
+#define	CIRCLEQ_INSERT_BEFORE(head, listelm, elm, field) do {		\
+	QUEUEDEBUG_CIRCLEQ_HEAD((head), field)				\
+	QUEUEDEBUG_CIRCLEQ_ELM((head), (listelm), field)		\
+	(elm)-&gt;field.cqe_next = (listelm);				\
+	(elm)-&gt;field.cqe_prev = (listelm)-&gt;field.cqe_prev;		\
+	if ((listelm)-&gt;field.cqe_prev == CIRCLEQ_ENDC(head))		\
+		(head)-&gt;cqh_first = (elm);				\
+	else								\
+		(listelm)-&gt;field.cqe_prev-&gt;field.cqe_next = (elm);	\
+	(listelm)-&gt;field.cqe_prev = (elm);				\
+} while (/*CONSTCOND*/0)
+
+#define	CIRCLEQ_INSERT_HEAD(head, elm, field) do {			\
+	QUEUEDEBUG_CIRCLEQ_HEAD((head), field)				\
+	(elm)-&gt;field.cqe_next = (head)-&gt;cqh_first;			\
+	(elm)-&gt;field.cqe_prev = CIRCLEQ_END(head);			\
+	if ((head)-&gt;cqh_last == CIRCLEQ_ENDC(head))			\
+		(head)-&gt;cqh_last = (elm);				\
+	else								\
+		(head)-&gt;cqh_first-&gt;field.cqe_prev = (elm);		\
+	(head)-&gt;cqh_first = (elm);					\
+} while (/*CONSTCOND*/0)
+
+#define	CIRCLEQ_INSERT_TAIL(head, elm, field) do {			\
+	QUEUEDEBUG_CIRCLEQ_HEAD((head), field)				\
+	(elm)-&gt;field.cqe_next = CIRCLEQ_END(head);			\
+	(elm)-&gt;field.cqe_prev = (head)-&gt;cqh_last;			\
+	if ((head)-&gt;cqh_first == CIRCLEQ_ENDC(head))			\
+		(head)-&gt;cqh_first = (elm);				\
+	else								\
+		(head)-&gt;cqh_last-&gt;field.cqe_next = (elm);		\
+	(head)-&gt;cqh_last = (elm);					\
+} while (/*CONSTCOND*/0)
+
+#define	CIRCLEQ_REMOVE(head, elm, field) do {				\
+	QUEUEDEBUG_CIRCLEQ_HEAD((head), field)				\
+	QUEUEDEBUG_CIRCLEQ_ELM((head), (elm), field)			\
+	if ((elm)-&gt;field.cqe_next == CIRCLEQ_ENDC(head))		\
+		(head)-&gt;cqh_last = (elm)-&gt;field.cqe_prev;		\
+	else								\
+		(elm)-&gt;field.cqe_next-&gt;field.cqe_prev =			\
+		    (elm)-&gt;field.cqe_prev;				\
+	if ((elm)-&gt;field.cqe_prev == CIRCLEQ_ENDC(head))		\
+		(head)-&gt;cqh_first = (elm)-&gt;field.cqe_next;		\
+	else								\
+		(elm)-&gt;field.cqe_prev-&gt;field.cqe_next =			\
+		    (elm)-&gt;field.cqe_next;				\
+	QUEUEDEBUG_CIRCLEQ_POSTREMOVE((elm), field)			\
+} while (/*CONSTCOND*/0)
+
+#define	CIRCLEQ_FOREACH(var, head, field)				\
+	for ((var) = ((head)-&gt;cqh_first);				\
+		(var) != CIRCLEQ_ENDC(head);				\
+		(var) = ((var)-&gt;field.cqe_next))
+
+#define	CIRCLEQ_FOREACH_REVERSE(var, head, field)			\
+	for ((var) = ((head)-&gt;cqh_last);				\
+		(var) != CIRCLEQ_ENDC(head);				\
+		(var) = ((var)-&gt;field.cqe_prev))
+
+/*
+ * Circular queue access methods.
+ */
+#define	CIRCLEQ_FIRST(head)		((head)-&gt;cqh_first)
+#define	CIRCLEQ_LAST(head)		((head)-&gt;cqh_last)
+/* For comparisons */
+#define	CIRCLEQ_ENDC(head)		(__launder_type(head))
+/* For assignments */
+#define	CIRCLEQ_END(head)		((void *)(head))
+#define	CIRCLEQ_NEXT(elm, field)	((elm)-&gt;field.cqe_next)
+#define	CIRCLEQ_PREV(elm, field)	((elm)-&gt;field.cqe_prev)
+#define	CIRCLEQ_EMPTY(head)						\
+    (CIRCLEQ_FIRST(head) == CIRCLEQ_ENDC(head))
+
+#define CIRCLEQ_LOOP_NEXT(head, elm, field)				\
+	(((elm)-&gt;field.cqe_next == CIRCLEQ_ENDC(head))			\
+	    ? ((head)-&gt;cqh_first)					\
+	    : (elm-&gt;field.cqe_next))
+#define CIRCLEQ_LOOP_PREV(head, elm, field)				\
+	(((elm)-&gt;field.cqe_prev == CIRCLEQ_ENDC(head))			\
+	    ? ((head)-&gt;cqh_last)					\
+	    : (elm-&gt;field.cqe_prev))
+#endif /* !_KERNEL */
+
+#endif	/* !_SYS_QUEUE_H_ */
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-&gt;event,event) &amp;&amp; envvar_exists_if_needed(rule-&gt;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(&amp;line0,line);
+  append0(&amp;line0);
+
+  for (rule = rules; rule; rule = rule-&gt;next) {
+    if (!filter_rule_applies(rule,event)) continue;
+    if (!string_matches_glob(rule-&gt;response_line_glob,line0.s)) continue;
+    munge_exitcode(exitcode,rule);
+    if (want_munge_internally(rule-&gt;response))
+      munge_line_internally(line,lineno,greeting,event,tls_level,in_tls);
+    else if (!want_leave_line_as_is(rule-&gt;response))
+      copys(line,rule-&gt;response);
+  }
+  if (line-&gt;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 &lt; response-&gt;len; i++) {
+    append(&amp;line,i + response-&gt;s);
+    if (response-&gt;s[i] == '\n' || i == response-&gt;len - 1) {
+      munge_response_line(lineno++,&amp;line,exitcode,greeting,rules,event,tls_level,in_tls);
+      cat(&amp;munged,&amp;line);
+      blank(&amp;line);
+    }
+  }
+
+  if (munged.len) reformat_multiline_response(&amp;munged);
+  copy(response,&amp;munged);
+}
+
+filter_rule *prepend_rule(filter_rule *next, filter_rule *rule) {
+  rule-&gt;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-&gt;next;
+    temp-&gt;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(&amp;lines,"control/fixsmtpio",0) == -1) die_control();
+
+  for (linestart = 0, pos = 0; pos &lt; 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 &lt;fnmatch.h&gt;
+
+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-&gt;exitcode != EXIT_LATER_NORMALLY) *exitcode = rule-&gt;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 &gt;= UCSPITLS_AVAILABLE &amp;&amp; !in_tls &amp;&amp; !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(&amp;munged,"214 " PROGNAME " home page: " HOMEPAGE "\r\n");
+  cat(&amp;munged,response);
+  copy(response,&amp;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 &lt; multiline-&gt;len; i++) {
+    if (multiline-&gt;s[i] == '\n') pos = -1;
+    if (pos == 3) multiline-&gt;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-&gt;len - 2; i &gt;= 0; i--) {
+    if (multiline-&gt;s[i] == '\n') {
+      pos = i + 1;
+      break;
+    }
+  }
+  multiline-&gt;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..fd8b8cf
--- /dev/null
+++ fixsmtpio_proxy.c
@@ -0,0 +1,413 @@
+#include &lt;libgen.h&gt;
+#include &lt;signal.h&gt;
+
+#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-&gt;len;
+
+  if (!len                      ) return 0;
+  if (       r-&gt;s[--len] != '\n') return 0;
+  if (len &amp;&amp; r-&gt;s[--len] != '\r') ++len;
+  if (!len                      ) return 0;
+  if (       r-&gt;s[--len] !=  '.') return 0;
+  if (len &amp;&amp; r-&gt;s[--len] != '\n') return 0;
+
+  return 1;
+}
+
+static int find_first_space(stralloc *request) {
+  int i;
+  for (i = 0; i &lt; request-&gt;len; i++) if (request-&gt;s[i] == ' ') return i;
+  return -1;
+}
+
+void strip_last_eol(stralloc *sa) {
+  if (sa-&gt;len &gt; 0 &amp;&amp; sa-&gt;s[sa-&gt;len-1] == '\n') sa-&gt;len--;
+  if (sa-&gt;len &gt; 0 &amp;&amp; sa-&gt;s[sa-&gt;len-1] == '\r') sa-&gt;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-&gt;s,pos-1);
+  copyb(arg,request-&gt;s+pos,request-&gt;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,const char *event) {
+  return tls_level &gt;= UCSPITLS_REQUIRED
+    &amp;&amp; !in_tls
+    &amp;&amp; !event_matches(EVENT_GREETING,event)
+    &amp;&amp; !event_matches(EVENT_TIMEOUT,event)
+    &amp;&amp; !event_matches(EVENT_CLIENTEOF,event)
+    &amp;&amp; !event_matches("noop",event)
+    &amp;&amp; !event_matches("ehlo",event)
+    &amp;&amp; !event_matches("starttls",event)
+    &amp;&amp; !event_matches("quit",event);
+}
+
+void construct_proxy_request(stralloc *proxy_request,
+                             filter_rule *rules,
+                             const 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-&gt;next)
+    if (rule-&gt;request_prepend &amp;&amp; filter_rule_applies(rule,event))
+      prepends(proxy_request,rule-&gt;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 &gt;= 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,
+                              const 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) &amp;&amp; *want_data) {
+    *want_data = 0;
+    if (accepted_data(server_response)) {
+      eventq_put("in_data");
+      *in_data = 1;
+    }
+  }
+
+  if (event_matches("starttls",event) &amp;&amp; want_tls) {
+    if (tls_level &lt; 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 &lt; pile-&gt;len; i++) {
+    if (pile-&gt;s[i] == '\n') {
+      stralloc line = {0};
+
+      catb(&amp;line,pile-&gt;s+pos,i+1-pos);
+      pos = i+1;
+      cat(one,&amp;line);
+
+      if (!fn || fn(&amp;line)) {
+        got_one = 1;
+        break;
+      }
+    }
+  }
+
+  if (got_one) {
+    copys(&amp;caller_sa,(char *)caller);
+    cats(&amp;caller_sa,":");
+    cats(&amp;caller_sa,(char *)__func__);
+    append0(&amp;caller_sa);
+
+    contextlogging_copyb(caller_sa.s,&amp;next_pile,pile-&gt;s+pos,pile-&gt;len-pos);
+    contextlogging_copy(caller_sa.s,pile,&amp;next_pile);
+    blank(&amp;next_pile);
+
+    blank(&amp;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-&gt;len &gt;= 4 &amp;&amp; line-&gt;s[3] == ' ';
+}
+
+int get_one_response(stralloc *one,stralloc *pile) {
+  return get_one(__func__,one,pile,&amp;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(&amp;verb,&amp;arg,request);
+  copy(&amp;event,&amp;verb);
+  append0(&amp;event);
+  eventq_put(event.s);
+  construct_proxy_request(proxy_request,rules,
+                          event.s,&amp;arg,
+                          request,
+                          tls_level,
+                          want_tls,in_tls,
+                          want_data);
+  blank(request);
+  logit(logstamp,'2',proxy_request);
+}
+
+static void handle_response(int *exitcode,
+                            stralloc *proxy_response,stralloc *response,
+                            int tls_level,int want_tls,int in_tls,
+                            int *want_data,int *in_data,
+                            filter_rule *rules,stralloc *greeting,
+                            stralloc *logstamp) {
+  const 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_server,int to_server,
+                       char **argv) {
+  unistd_close(from_server);
+  unistd_close(to_server);
+  use_as_stdin(from_proxy);
+  use_as_stdout(to_proxy);
+  unistd_execvp(*argv,argv);
+  die_exec();
+}
+
+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(&amp;sa,pidbuf,len);
+    append0(&amp;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 stop_kid_and_maybe_myself(int exitcode,int kid_pid,
+                                      int from_server,int to_server) {
+  int wstat;
+  int startingtls = (exitcode == BEGIN_STARTTLS_NOW);
+
+  unistd_close(from_server);
+  unistd_close(to_server);
+
+  if (startingtls &amp;&amp; -1 == kill(kid_pid,SIGTERM)) die_kill();
+
+  if (wait_pid(&amp;wstat,kid_pid) == -1) die_wait();
+
+  if (startingtls) return;
+
+  if (wait_crashed(wstat)) die_crash();
+
+  if (exitcode == EXIT_LATER_NORMALLY)
+    unistd_exit(wait_exitcode(wstat));
+  else
+    unistd_exit(exitcode);
+}
+
+static void run_new_kid_in_read_loop(int from_client,int to_proxy,
+                                    int from_proxy,int to_server,
+                                    int from_server,int to_client,
+                                    stralloc *logstamp,stralloc *greeting,
+                                    filter_rule *rules,char **argv,
+                                    int in_tls) {
+  int kid_pid = unistd_fork();
+
+  if (kid_pid) {
+    unistd_close(from_proxy);
+    unistd_close(to_proxy);
+    prepare_logstamp(logstamp,kid_pid,basename(argv[0]));
+    eventq_put(EVENT_GREETING);
+    stop_kid_and_maybe_myself(
+        read_and_process_until_either_end_closes(from_client,to_server,
+                                                 from_server,to_client,
+                                                 greeting,rules,
+                                                 logstamp,in_tls),
+        kid_pid,from_server,to_server);
+  } else if (0 == kid_pid) {
+    be_proxied(from_proxy,to_proxy,
+               from_server,to_server,
+               argv);
+  } else {
+    die_fork();
+  }
+}
+
+void be_proxy(stralloc *greeting,filter_rule *rules,char **argv) {
+  int from_client = 0, to_proxy;
+  int from_proxy, to_server;
+  int from_server, to_client = 1;
+  stralloc logstamp = {0};
+
+  for (int in_tls = 0; in_tls &lt;= 1; in_tls++) {
+    make_pipe(&amp;from_proxy,&amp;to_server);
+    make_pipe(&amp;from_server,&amp;to_proxy);
+    run_new_kid_in_read_loop(from_client,to_proxy,
+                             from_proxy,to_server,
+                             from_server,to_client,
+                             &amp;logstamp,greeting,
+                             rules,argv,
+                             in_tls);
+  }
+}
+
+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 in_tls) {
+  char     buf               [SUBSTDIO_INSIZE];
+  int      exitcode         = EXIT_LATER_NORMALLY;
+  int      tls_level        = ucspitls_level_configured(),
+           want_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(&amp;client_requests,from_client,buf,sizeof buf)) {
+        // XXX maybe telnet and Ctrl-C gets here??
+        munge_response_line(0,
+                            &amp;client_requests,&amp;exitcode,
+                            greeting,rules,EVENT_CLIENTEOF,
+                            tls_level,in_tls);
+        break;
+      }
+      while (client_requests.len) {
+        if (in_data) {
+          handle_data_specially(&amp;client_requests,&amp;in_data,logstamp);
+          safewrite(to_server,&amp;client_requests);
+        } else if (get_one_request(&amp;one_request,&amp;client_requests)) {
+          handle_request(&amp;proxy_request,&amp;one_request,
+                         tls_level,&amp;want_tls,in_tls,
+                         &amp;want_data,
+                         rules,logstamp);
+          safewrite(to_server,&amp;proxy_request);
+        //} else {
+        //  /* not in_data and no full request received yet */
+        //  break;
+        }
+      }
+    }
+
+    if (can_read(from_server)) {
+      if (!safeappend(&amp;server_responses,from_server,buf,sizeof buf)) break;
+      while (server_responses.len
+             &amp;&amp; exitcode == EXIT_LATER_NORMALLY
+             &amp;&amp; get_one_response(&amp;one_response,&amp;server_responses)) {
+        handle_response(&amp;exitcode,
+                        &amp;proxy_response,&amp;one_response,
+                        tls_level,want_tls,in_tls,
+                        &amp;want_data,&amp;in_data,
+                        rules,greeting,
+                        logstamp);
+        safewrite(to_client,&amp;proxy_response);
+        if (want_tls) {
+          want_tls = 0;
+          if (tls_level &gt;= UCSPITLS_AVAILABLE &amp;&amp; !in_tls) {
+            if (!tls_init() || !tls_info(die_nomem)) die_tls();
+            if (!env_put("FIXSMTPIOTLS=1")) die_nomem(__func__,"env_put");
+            exitcode = BEGIN_STARTTLS_NOW;
+          }
+        }
+      }
+    }
+
+    if (exitcode != EXIT_LATER_NORMALLY) break;
+  }
+
+  return exitcode;
+}
diff --git fixsmtpio_proxy.h fixsmtpio_proxy.h
new file mode 100644
index 0000000..112a1b3
--- /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);
+void construct_proxy_request(stralloc *,filter_rule *,const char *,stralloc *,stralloc *,int,int *,int,int *);
+void be_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(&amp;fds);
+  FD_SET(fd1,&amp;fds);
+  FD_SET(fd2,&amp;fds);
+}
+
+int can_read(int fd) { return FD_ISSET(fd,&amp;fds); }
+
+static int max(int a,int b) { return a &gt; 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),&amp;fds,(fd_set *)0,(fd_set *)0,(struct timeval *) 0);
+  if (ready == -1 &amp;&amp; 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-&gt;s,sa-&gt;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 qmail-qfilter-addtlsheader.8 qmail-qfilter-addtlsheader.8
new file mode 100644
index 0000000..d4714de
--- /dev/null
+++ qmail-qfilter-addtlsheader.8
@@ -0,0 +1,53 @@
+.TH QMAIL-QFILTER-ADDTLSHEADER 8 2018-12-01
+.SH NAME
+qmail-qfilter-addtlsheader \- Add Received: header with TLS parameters
+.SH SYNOPSIS
+.B qmail-qfilter-addtlsheader
+.SH DESCRIPTION
+.B qmail-qfilter-addtlsheader
+takes a message (headers and body) on standard input
+and prints it to standard output.
+If
+.B UCSPITLS
+connection details are available, it prepends a
+.B Received:
+header.
+.PP
+.B qmail-qfilter-addtlsheader
+is intended to be invoked by
+.BR qmail-qfilter .
+It is typically used under
+.B authup
+or
+.B fixsmtpio
+(via
+.BR qmail-qfilter-queue ).
+.SH "ENVIRONMENT VARIABLES"
+If both
+.B SSL_CIPHER
+and
+.B SSL_PROTOCOL
+are set,
+.B qmail-qfilter-addtlsheader
+will add its header.
+.PP
+If either
+.B FIXSMTPIOTLS
+or
+.B AUTHUP_USER
+is set, the header will mention
+.BR fixsmtpio (8)
+or
+.BR authup (8),
+respectively.
+.SH "EXAMPLES"
+See
+.IR https://schmonz.com/qmail/acceptutils .
+.SH "AUTHOR"
+.B Amitai Schleier &lt;schmonz-web-acceptutils@schmonz.com&gt;
+.SH "SEE ALSO"
+.BR ucspi-tls(2),
+.BR qmail-qfilter(1),
+.BR authup(8),
+.BR fixsmtpio(8),
+.BR qmail-qfilter-queue(8).
diff --git qmail-qfilter-addtlsheader.c qmail-qfilter-addtlsheader.c
new file mode 100755
index 0000000..12e8b0d
--- /dev/null
+++ qmail-qfilter-addtlsheader.c
@@ -0,0 +1,54 @@
+#include "datetime.h"
+#include "date822fmt.h"
+#include "env.h"
+#include "now.h"
+#include "readwrite.h"
+#include "substdio.h"
+
+static char inbuf[SUBSTDIO_INSIZE];
+static substdio ssin = SUBSTDIO_FDBUF(read,0,inbuf,sizeof(inbuf));
+static char outbuf[SUBSTDIO_OUTSIZE];
+static substdio ssout = SUBSTDIO_FDBUF(write,1,outbuf,sizeof(outbuf));
+
+static void out(char *s) { substdio_puts(&amp;ssout,s); }
+
+static char datebuf[DATE822FMT];
+
+static void set_now(char *datebuf) {
+  struct datetime dt;
+  datetime_tai(&amp;dt,now());
+  date822fmt(datebuf,&amp;dt);
+}
+
+static void perhaps_write_tls_header() {
+  char *ssl_cipher, *ssl_protocol, *authup_user;
+
+  if ((ssl_cipher = env_get("SSL_CIPHER"))
+    &amp;&amp; (ssl_protocol = env_get("SSL_PROTOCOL"))) {
+    out("Received: (ucspitls");
+    if (env_get("FIXSMTPIOTLS")) {
+      out(" acceptutils fixsmtpio");
+    } else if ((authup_user = env_get("AUTHUP_USER"))) {
+      out(" acceptutils authup ");
+      out(authup_user);
+    }
+    out(  " "); out(ssl_protocol);
+    out(  " "); out(ssl_cipher);
+    set_now(datebuf);
+    out("); "); out(datebuf);
+    substdio_flush(&amp;ssout);
+  }
+}
+
+static void copy_stdin_to_stdout() {
+  int i;
+
+  while ((i = substdio_get(&amp;ssin,inbuf,sizeof(inbuf))) &gt; 0)
+    substdio_putflush(&amp;ssout,inbuf,i);
+}
+
+int main(void) {
+  perhaps_write_tls_header();
+  copy_stdin_to_stdout();
+  return 0;
+}
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(&amp;sa, input);
+
+  prepends(&amp;sa, prepend);
+
+  stralloc_0(&amp;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 &lt;stdlib.h&gt;
+
+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-&gt;next);
+  assert_str_null_or_eq(env, rule-&gt;env);
+  assert_str_null_or_eq(event, rule-&gt;event);
+  assert_str_null_or_eq(request_prepend, rule-&gt;request_prepend);
+  assert_str_null_or_eq(response_line_glob, rule-&gt;response_line_glob);
+  ck_assert_int_eq(exitcode, rule-&gt;exitcode);
+  assert_str_null_or_eq(response, rule-&gt;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:&amp;fixsmtpio_fixup",
+    "env","HELO","prepend","glob",55,"&amp;fixsmtpio_fixup"
+  );
+} END_TEST
+
+START_TEST (test_reject_fixup_for_inapplicable_event) {
+  assert_non_parsed_line(
+    "env:event:prepend:glob:55:&amp;fixsmtpio_fixup"
+  );
+} END_TEST
+
+START_TEST (test_reject_unknown_response_thingy) {
+  assert_non_parsed_line(
+    "env:event:prepend:glob:55:&amp;fixsmtpio_unknown_thingy"
+  );
+} END_TEST
+
+START_TEST (test_accept_other_ampersand_response) {
+  assert_parsed_line(
+    "env:event:prepend:glob:55:&amp;other_ampersand_response",
+    "env","event","prepend","glob",55,"&amp;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(&amp;rule, "clienteof", 0);
+
+  rule.event = "clienteof";
+  assert_filter_rule(&amp;rule, "clienteof", 1);
+}
+END_TEST
+
+START_TEST (test_want_munge_internally) {
+  ck_assert_int_eq(1, want_munge_internally("&amp;fixsmtpio_fixup"));
+  ck_assert_int_eq(0, want_munge_internally("&amp;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("&amp;fixsmtpio_noop"));
+  ck_assert_int_eq(0, want_leave_line_as_is("&amp;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(&amp;line_sa, line);
+  stralloc greeting_sa = {0}; stralloc_copys(&amp;greeting_sa, greeting);
+
+  munge_response_line(lineno, &amp;line_sa, &amp;exitcode, &amp;greeting_sa, rules, event, 0, 0);
+
+  stralloc_0(&amp;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, &amp;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, &amp;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(&amp;response_sa, response);
+  stralloc greeting_sa = {0}; stralloc_copys(&amp;greeting_sa, greeting);
+
+  munge_response(&amp;response_sa, &amp;exitcode, &amp;greeting_sa, rules, event, 0, 0);
+
+  stralloc_0(&amp;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(&amp;response_sa, input);
+  change_every_line_fourth_char_to_dash(&amp;response_sa);
+  stralloc_0(&amp;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(&amp;response_sa, input);
+  change_last_line_fourth_char_to_space(&amp;response_sa);
+  stralloc_0(&amp;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(&amp;input_sa, input);
+  stralloc greeting_sa = {0}; stralloc_copys(&amp;greeting_sa, greeting);
+  munge_line_internally(&amp;input_sa,lineno,&amp;greeting_sa,event,0,0);
+  stralloc_0(&amp;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(&amp;sa, input);
+
+  strip_last_eol(&amp;sa);
+
+  ck_assert_int_eq(sa.len, strlen(expected_output));
+  stralloc_0(&amp;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(&amp;sa, input);
+
+  int actual = ends_with_newline(&amp;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(&amp;sa, input);
+  int actual = is_last_line_of_response(&amp;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(&amp;sa_request, request);
+  stralloc sa_request_copy = {0}; stralloc_copy(&amp;sa_request_copy, &amp;sa_request);
+  stralloc sa_verb = {0};
+  stralloc sa_arg = {0};
+
+  parse_client_request(&amp;sa_verb, &amp;sa_arg, &amp;sa_request);
+
+  ck_assert_int_eq(sa_request_copy.len, sa_request.len);
+  stralloc_0(&amp;sa_verb);
+  ck_assert_str_eq(sa_verb.s, verb);
+  stralloc_0(&amp;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:&lt;schmonz@schmonz.com&gt;\r\n", "MAIL", "FROM:&lt;schmonz@schmonz.com&gt;");
+  assert_parse_client_request("RCPT TO:&lt;geepawhill@geepawhill.org&gt;\r\n", "RCPT", "TO:&lt;geepawhill@geepawhill.org&gt;");
+  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:&lt;schmonz@schmonz.com&gt;\r\nRCPT TO:&lt;geepawhill@geepawhill.org&gt;\r\n", "MAIL", "FROM:&lt;schmonz@schmonz.com&gt;\r\nRCPT TO:&lt;geepawhill@geepawhill.org&gt;");
+}
+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(&amp;actual_many,input);
+
+  return_value = get_one_response(&amp;actual_one,&amp;actual_many);
+
+  ck_assert_int_eq(return_value, expected_return);
+
+  stralloc_0(&amp;actual_one);
+  ck_assert_str_eq(actual_one.s, expected_result);
+
+  stralloc_0(&amp;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(&amp;input_sa, input);
+
+  int actual = ends_data(&amp;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, &amp;test_rule);
+
+  blank(&amp;proxy_request); copys(&amp;client_request, "SPECIFIC_VERB somearg\r\n");
+  construct_proxy_request(&amp;proxy_request,test_rules,"SPECIFIC_VERB",&amp;arg,&amp;client_request,0,(void *)0,0,&amp;want_data);
+  stralloc_0(&amp;proxy_request); stralloc_0(&amp;client_request);
+  ck_assert_str_eq(proxy_request.s, client_request.s);
+
+  env_put2("SPECIFIC_ENV_VAR","");
+
+  blank(&amp;proxy_request); copys(&amp;client_request, "SPECIFIC_VERB somearg\r\n");
+  construct_proxy_request(&amp;proxy_request,test_rules,"SPECIFIC_VERB",&amp;arg,&amp;client_request,0,(void *)0,0,&amp;want_data);
+  stralloc_0(&amp;proxy_request); stralloc_0(&amp;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;
+}
diff --git tryblist.c tryblist.c
new file mode 100644
index 0000000..b48fd34
--- /dev/null
+++ tryblist.c
@@ -0,0 +1,6 @@
+#include &lt;blacklist.h&gt;
+
+void main()
+{
+  struct blacklist *blstate = blacklist_open();
+}
</pre></body></html>