about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile31
-rw-r--r--README.745
-rw-r--r--compat.c89
-rw-r--r--compat.h94
-rwxr-xr-xconfigure50
-rw-r--r--getservinfo.c122
-rw-r--r--git-notemap.sh90
-rw-r--r--imap.c169
-rw-r--r--imap.h34
-rw-r--r--notemap.1157
-rw-r--r--notemap.c368
12 files changed, 763 insertions, 487 deletions
diff --git a/.gitignore b/.gitignore
index 7dbdb7e..9e9cfb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 *.o
 config.mk
+git-notemap
 notemap
diff --git a/Makefile b/Makefile
index caaa9ac..5475b97 100644
--- a/Makefile
+++ b/Makefile
@@ -1,29 +1,34 @@
-PREFIX = /usr/local
-MANDIR = ${PREFIX}/share/man
-LIBRESSL_PREFIX = /usr/local
+PREFIX ?= /usr/local
+MANDIR ?= ${PREFIX}/share/man
 
 CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
-CFLAGS += ${LIBRESSL_PREFIX:%=-I%/include}
-LDFLAGS += ${LIBRESSL_PREFIX:%=-L%/lib}
-LDLIBS = -lcrypto -ltls
+LDLIBS = -ltls
 
 -include config.mk
 
+BINS = notemap git-notemap
+
+OBJS += getservinfo.o
 OBJS += imap.o
 OBJS += notemap.o
 
+all: ${BINS}
+
 notemap: ${OBJS}
 	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
 
-${OBJS}: compat.h imap.h
+${OBJS}: imap.h
 
 clean:
-	rm -f notemap ${OBJS}
+	rm -f ${BINS} ${OBJS}
 
-install: notemap notemap.1
-	install -d ${PREFIX}/bin ${MANDIR}/man1
-	install notemap ${PREFIX}/bin
-	gzip -c notemap.1 > ${MANDIR}/man1/notemap.1.gz
+install: ${BINS} notemap.1
+	install -d ${DESTDIR}${PREFIX}/bin ${DESTDIR}${MANDIR}/man1
+	install ${BINS} ${DESTDIR}${PREFIX}/bin
+	install -m 644 notemap.1 ${DESTDIR}${MANDIR}/man1
+	ln -fs notemap.1 ${DESTDIR}${MANDIR}/man1/git-notemap.1
 
 uninstall:
-	rm -f ${PREFIX}/bin/notemap ${MANDIR}/man1/notemap.1.gz
+	rm -f ${BINS:%=${DESTDIR}${PREFIX}/bin/%}
+	rm -f ${DESTDIR}${MANDIR}/man1/notemap.1
+	rm -f ${DESTDIR}${MANDIR}/man1/git-notemap.1
diff --git a/README.7 b/README.7
index e995e57..ae50df3 100644
--- a/README.7
+++ b/README.7
@@ -1,4 +1,4 @@
-.Dd May  5, 2020
+.Dd December 16, 2020
 .Dt README 7
 .Os "Causal Agency"
 .
@@ -16,39 +16,18 @@ easily accessible from the phone.
 .
 .Pp
 .Nm
-requires LibreSSL
-.Pq Fl ltls
-and either
-.Xr dig 1
-or
-.Xr drill 1
-for automatic IMAP server configuration.
+requires
+.Sy libtls ,
+provided by either
+.Lk https://git.causal.agency/libretls/about LibreTLS
+(for OpenSSL)
+or by LibreSSL.
 .
-.Pp
-Build configuration can be written to
-.Pa config.mk .
-The install prefix of LibreSSL is set by
-.Va LIBRESSL_PREFIX .
-The path of
-.Xr dig 1
-or
-.Xr drill 1
-can be set by defining the C preprocessor macro
-.Va DIG_PATH
-or
-.Va DRILL_PATH ,
-respectively.
-The default is to set
-.Va DRILL_PATH
-to
-.Pa /usr/bin/drill
-on
-.Fx
-and set
-.Va DIG_PATH
-to
-.Pa dig
-everywhere else.
+.Bd -literal -offset indent
+\&./configure
+make
+sudo make install
+.Ed
 .
 .Sh CONTRIBUTING
 The upstream URL of this project is
diff --git a/compat.c b/compat.c
new file mode 100644
index 0000000..f220499
--- /dev/null
+++ b/compat.c
@@ -0,0 +1,89 @@
+/* Copyright (C) 2019  June McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with LibreSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of LibreSSL used as well as that of the
+ * covered work.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+char *readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags) {
+	if (flags) {
+		if (!fgets(buf, bufsiz, stdin)) return NULL;
+		return strsep(&buf, "\n");
+	}
+	return getpass(prompt);
+}
+
+typedef int Read(void *, char *, int);
+typedef int Write(void *, const char *, int);
+typedef fpos_t Seek(void *, fpos_t, int);
+typedef int Close(void *);
+
+struct Funopen {
+	void *cookie;
+	Read *readfn;
+	Write *writefn;
+	Close *closefn;
+};
+
+static ssize_t cookieRead(void *cookie, char *buf, size_t size) {
+	struct Funopen *funopen = cookie;
+	return funopen->readfn(funopen->cookie, buf, size);
+}
+
+static ssize_t cookieWrite(void *cookie, const char *buf, size_t size) {
+	struct Funopen *funopen = cookie;
+	return funopen->writefn(funopen->cookie, buf, size);
+}
+
+static int cookieClose(void *cookie) {
+	struct Funopen *funopen = cookie;
+	int ret = 0;
+	if (funopen->closefn) ret = funopen->closefn(funopen->cookie);
+	free(cookie);
+	return ret;
+}
+
+FILE *funopen(
+	const void *cookie,
+	Read *readfn, Write *writefn, Seek *seekfn, Close *closefn
+) {
+	struct Funopen *funopen = malloc(sizeof(*funopen));
+	if (!funopen) return NULL;
+	funopen->cookie = (void *)cookie;
+	funopen->readfn = readfn;
+	funopen->writefn = writefn;
+	assert(!seekfn);
+	funopen->closefn = closefn;
+	cookie_io_functions_t fns = {
+		.read = (readfn ? cookieRead : NULL),
+		.write = (writefn ? cookieWrite : NULL),
+		.close = cookieClose,
+	};
+	return fopencookie(funopen, (!readfn ? "w" : !writefn ? "r" : "r+"), fns);
+}
diff --git a/compat.h b/compat.h
deleted file mode 100644
index ac7cc3f..0000000
--- a/compat.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-#ifdef __linux__
-
-#define _GNU_SOURCE
-
-#include <assert.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-// Defined in libcrypto:
-void arc4random_buf(void *buf, size_t nbytes);
-
-#define NO_READPASSPHRASE_H
-#define RPP_STDIN 1
-static inline char *
-readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags) {
-	if (flags) {
-		if (!fgets(buf, bufsiz, stdin)) return NULL;
-		return strsep(&buf, "\n");
-	}
-	return getpass(prompt);
-}
-
-typedef int _readfn(void *, char *, int);
-typedef int _writefn(void *, const char *, int);
-typedef fpos_t _seekfn(void *, fpos_t, int);
-typedef int _closefn(void *);
-
-struct _funopen {
-	void *cookie;
-	_readfn *readfn;
-	_writefn *writefn;
-	_closefn *closefn;
-};
-
-static inline ssize_t
-_cookie_read(void *cookie, char *buf, size_t size) {
-	struct _funopen *funopen = cookie;
-	assert((size_t)(int)size == size);
-	return funopen->readfn(funopen->cookie, buf, size);
-}
-
-static inline ssize_t
-_cookie_write(void *cookie, const char *buf, size_t size) {
-	struct _funopen *funopen = cookie;
-	assert((size_t)(int)size == size);
-	return funopen->writefn(funopen->cookie, buf, size);
-}
-
-static inline int
-_cookie_close(void *cookie) {
-	struct _funopen *funopen = cookie;
-	int ret = (funopen->closefn ? funopen->closefn(funopen->cookie) : 0);
-	free(cookie);
-	return ret;
-}
-
-static const cookie_io_functions_t _cookie_fns = {
-	.read = _cookie_read,
-	.write = _cookie_write,
-	.close = _cookie_close,
-};
-
-static inline FILE *
-funopen(
-	const void *cookie,
-	_readfn *readfn, _writefn *writefn, _seekfn *seekfn, _closefn *closefn
-) {
-	struct _funopen *funopen = malloc(sizeof(*funopen));
-	if (!funopen) return NULL;
-	assert(!seekfn);
-	*funopen = (struct _funopen) { (void *)cookie, readfn, writefn, closefn };
-	const char *mode = (!readfn ? "w" : !writefn ? "r" : "r+");
-	return fopencookie(funopen, mode, _cookie_fns);
-}
-
-#endif /* __linux__ */
diff --git a/configure b/configure
new file mode 100755
index 0000000..57d878a
--- /dev/null
+++ b/configure
@@ -0,0 +1,50 @@
+#!/bin/sh
+set -eu
+
+cflags() {
+	echo "CFLAGS += $*"
+}
+ldlibs() {
+	echo "LDLIBS ${o:-}= $*"
+	o=+
+}
+config() {
+	pkg-config --print-errors "$@"
+	cflags $(pkg-config --cflags "$@")
+	ldlibs $(pkg-config --libs "$@")
+}
+defstr() {
+	cflags "-D'$1=\"$2\"'"
+}
+defvar() {
+	defstr "$1" "$(pkg-config --variable=$3 $2)${4:-}"
+}
+
+exec >config.mk
+
+for opt; do
+	case "${opt}" in
+		(--prefix=*) echo "PREFIX = ${opt#*=}" ;;
+		(--mandir=*) echo "MANDIR = ${opt#*=}" ;;
+		(*) echo "warning: unsupported option ${opt}" >&2 ;;
+	esac
+done
+
+case "$(uname)" in
+	(OpenBSD)
+		ldlibs -ltls
+		;;
+	(Linux)
+		cflags -D_GNU_SOURCE -DDECLARE_RPP
+		ldlibs -lresolv
+		config libtls
+		echo 'OBJS += compat.o'
+		;;
+	(Darwin)
+		ldlibs -lresolv
+		config libtls
+		;;
+	(*)
+		config libtls
+		;;
+esac
diff --git a/getservinfo.c b/getservinfo.c
new file mode 100644
index 0000000..4bc4b34
--- /dev/null
+++ b/getservinfo.c
@@ -0,0 +1,122 @@
+/* Copyright (C) 2020  June McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with OpenSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of OpenSSL used as well as that of the
+ * covered work.
+ */
+
+#include <netdb.h>
+#include <netinet/in.h>
+#include <resolv.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef EAI_BADHINTS
+#define EAI_BADHINTS EAI_BADFLAGS
+#endif
+
+#ifndef EAI_PROTOCOL
+#define EAI_PROTOCOL EAI_BADFLAGS
+#endif
+
+/* A wrapper around getaddrinfo(3) which first performs SRV record (RFC 2782)
+ * lookup. hints must be provided and hints->ai_protocol must be set. SRV
+ * lookup is skipped if servname is numerical. If SRV lookup is successful, the
+ * ai_canonname field of the first addrinfo structure returned is set to the
+ * target name. Only the first SRV record is used. Priority and weight are
+ * ignored.
+ */
+int getservinfo(
+	const char *hostname, const char *servname,
+	const struct addrinfo *hints, struct addrinfo **res
+) {
+	if (!hints) return EAI_BADHINTS;
+	if (!hints->ai_protocol) return EAI_PROTOCOL;
+
+	char *rest;
+	strtoul(servname, &rest, 10);
+	if (!*rest || hints->ai_flags & (AI_NUMERICHOST | AI_NUMERICSERV)) {
+		return getaddrinfo(hostname, servname, hints, res);
+	}
+
+	struct protoent *proto = getprotobynumber(hints->ai_protocol);
+	if (!proto) return EAI_PROTOCOL;
+
+	char dname[256];
+	int len = snprintf(
+		dname, sizeof(dname), "_%s._%s.%s",
+		servname, proto->p_name, hostname
+	);
+	if ((size_t)len >= sizeof(dname)) return EAI_OVERFLOW;
+
+	uint8_t msg[512];
+	len = res_query(dname, 1 /* IN */, 33 /* SRV */, msg, sizeof(msg));
+	if (len < 12) return getaddrinfo(hostname, servname, hints, res);
+	uint8_t *ptr = &msg[12];
+
+#define NAME_SKIP \
+	for (uint8_t n; ptr < &msg[len] && (n = *ptr++); ptr += n) { \
+		if (n & 0xC0) { \
+			ptr++; \
+			break; \
+		} \
+	}
+
+	uint16_t qdcount = msg[4] << 8 | msg[5];
+	for (uint16_t q = 0; ptr < &msg[len] && q < qdcount; ++q) {
+		NAME_SKIP; // QNAME
+		ptr += 4; // QTYPE, QCLASS
+	}
+
+	NAME_SKIP; // NAME
+	ptr += 14; // TYPE, CLASS, TTL, RDLENGTH, Priority, Weight
+	if (&msg[len] < ptr + 3) {
+		return getaddrinfo(hostname, servname, hints, res);
+	}
+
+	char port[sizeof("65535")];
+	snprintf(port, sizeof(port), "%d", ptr[0] << 8 | ptr[1]);
+	ptr += 2;
+
+	// Name compression is not used for Target.
+	if (!ptr[0]) return EAI_NONAME;
+	hostname = (const char *)&ptr[1];
+	for (uint8_t n; ptr < &msg[len] && (n = *ptr); ptr += n) {
+		*ptr++ = '.';
+	}
+
+	struct addrinfo myHints = *hints;
+	myHints.ai_flags |= AI_NUMERICSERV;
+	myHints.ai_flags &= ~AI_CANONNAME;
+	int error = getaddrinfo(hostname, port, &myHints, res);
+	if (error) return error;
+
+	(*res)->ai_canonname = strdup(hostname);
+	if (!(*res)->ai_canonname) {
+		freeaddrinfo(*res);
+		return EAI_MEMORY;
+	}
+	return 0;
+}
diff --git a/git-notemap.sh b/git-notemap.sh
new file mode 100644
index 0000000..5fd4d7c
--- /dev/null
+++ b/git-notemap.sh
@@ -0,0 +1,90 @@
+#!/bin/sh
+# Copyright (C) 2020  June McEnroe <june@causal.agency>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+set -u
+
+add=
+force=
+verbose=
+host=$(git config notemap.imapServer)
+port=$(git config notemap.imapServerPort)
+user=$(git config notemap.imapUser)
+pass=$(git config notemap.imapPass)
+mailbox=$(git config notemap.imapMailbox)
+path=$(git config notemap.mapFile)
+
+OPTS_SPEC="\
+git notemap [<options>] [<args>...]
+--
+M,mailbox=! mirror notes to mailbox
+a,add! add new notes to map file
+f,force! overwite modified notes in mailbox
+h,host=! connect to IMAP on host
+m,map-file=! set path to map file
+p,port=! connect to IMAP on port
+u,user=! log in to IMAP as user
+v,verbose! log IMAP protocol to standard error
+"
+eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
+
+while [ $# -gt 0 ]; do
+	opt=$1
+	shift
+	case "${opt}" in
+		(-M) mailbox=$1; shift;;
+		(-a) add=yes;;
+		(-f) force=yes;;
+		(-h) host=$1; shift;;
+		(-m) path=$1; shift;;
+		(-p) port=$1; shift;;
+		(-u) user=$1; shift;;
+		(-v) verbose=yes;;
+		(--) break;;
+	esac
+done
+if [ -z "${user}" ]; then
+	echo "${0}: username required" >&2
+	exit 1
+fi
+
+description() {
+	cat <<-EOF
+		protocol=imaps
+		host=${host:-${user#*@}}
+		username=${user%@*}
+		${pass:+password=${pass}}
+	EOF
+}
+
+if [ -z "${pass}" ]; then
+	pass=$(description | git credential fill | grep '^password=')
+	pass=${pass#*=}
+fi
+
+printf '%s' "${pass}" | notemap -w \
+	${add:+-a} ${force:+-f} ${verbose:+-v} \
+	${path:+-m "${path}"} \
+	${host:+-h "${host}"} \
+	${port:+-p "${port}"} \
+	${mailbox:+-M "${mailbox}"} \
+	"${user}" "$@"
+status=$?
+if [ $status -ne 78 ]; then
+	description | git credential approve
+else
+	description | git credential reject
+fi
+exit $status
diff --git a/imap.c b/imap.c
index 7984e0e..c24fd42 100644
--- a/imap.c
+++ b/imap.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June McEnroe <june@causal.agency>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -12,20 +12,46 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with OpenSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of OpenSSL used as well as that of the
+ * covered work.
  */
 
-#include "compat.h"
-
 #include <err.h>
+#include <netdb.h>
+#include <netinet/in.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/socket.h>
 #include <sysexits.h>
 #include <tls.h>
+#include <unistd.h>
+
+FILE *funopen(
+	const void *cookie,
+	int (*readfn)(void *, char *, int),
+	int (*writefn)(void *, const char *, int),
+	fpos_t (*seekfn)(void *, fpos_t, int),
+	int (*closefn)(void *)
+);
 
 #include "imap.h"
 
+int getservinfo(
+	const char *hostname, const char *servname,
+	const struct addrinfo *hints, struct addrinfo **res
+);
+
 const char *Atoms[AtomCap] = {
 #define X(id, str) [id] = str,
 	ENUM_ATOM
@@ -64,7 +90,7 @@ static int imapClose(void *_tls) {
 	return error;
 }
 
-void imapOpen(FILE **read, FILE **write, const char *host, const char *port) {
+struct IMAP imapOpen(const char *host, const char *port) {
 	struct tls *client = tls_client();
 	if (!client) errx(EX_SOFTWARE, "tls_client");
 
@@ -75,99 +101,128 @@ void imapOpen(FILE **read, FILE **write, const char *host, const char *port) {
 	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
 	tls_config_free(config);
 
-	error = tls_connect(client, host, port);
-	if (error) errx(EX_NOHOST, "tls_connect: %s", tls_error(client));
+	struct addrinfo *head;
+	struct addrinfo hints = {
+		.ai_family = PF_UNSPEC,
+		.ai_socktype = SOCK_STREAM,
+		.ai_protocol = IPPROTO_TCP,
+	};
+	error = getservinfo(host, port, &hints, &head);
+	if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error));
 
-	*read = funopen(client, imapRead, NULL, NULL, NULL);
-	*write = funopen(client, NULL, imapWrite, NULL, imapClose);
-	if (!*read || !*write) err(EX_SOFTWARE, "funopen");
+	int sock = -1;
+	for (struct addrinfo *ai = head; ai; ai = ai->ai_next) {
+		sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+		if (sock < 0) err(EX_OSERR, "socket");
 
-	setlinebuf(*write);
-}
+		error = connect(sock, ai->ai_addr, ai->ai_addrlen);
+		if (!error) break;
 
-static size_t cap;
-static char *buf;
-static char *ptr;
+		close(sock);
+		sock = -1;
+	}
+	if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port);
 
-static void imapLine(FILE *imap) {
-	ssize_t len = getline(&buf, &cap, imap);
+	error = tls_connect_socket(
+		client, sock, (head->ai_canonname ? head->ai_canonname : host)
+	);
+	if (error) errx(EX_SOFTWARE, "tls_connect_socket: %s", tls_error(client));
+	freeaddrinfo(head);
+
+	struct IMAP imap = {
+		.sock = sock,
+		.r = funopen(client, imapRead, NULL, NULL, NULL),
+		.w = funopen(client, NULL, imapWrite, NULL, imapClose),
+	};
+	if (!imap.r || !imap.w) err(EX_OSERR, "funopen");
+
+	setlinebuf(imap.w);
+	return imap;
+}
+
+static void imapLine(struct IMAP *imap) {
+	ssize_t len = getline(&imap->buf, &imap->cap, imap->r);
 	if (len < 0) errx(EX_PROTOCOL, "unexpected eof");
-	if (len < 1 || buf[len - 1] != '\n') errx(EX_PROTOCOL, "missing LF");
-	if (len < 2 || buf[len - 2] != '\r') errx(EX_PROTOCOL, "missing CR");
-	buf[len - 2] = '\0';
-	ptr = buf;
+	if (len < 1 || imap->buf[len - 1] != '\n') errx(EX_PROTOCOL, "missing LF");
+	if (len < 2 || imap->buf[len - 2] != '\r') errx(EX_PROTOCOL, "missing CR");
+	imap->buf[len - 2] = '\0';
+	imap->ptr = imap->buf;
 }
 
-static struct Data parseAtom(void) {
-	size_t len = (*ptr == '.' ? 1 : strcspn(ptr, " .()[]{\""));
+static struct Data parseAtom(struct IMAP *imap) {
+	size_t len = (*imap->ptr == '.' ? 1 : strcspn(imap->ptr, " .()[]{\""));
 	struct Data data = {
 		.type = Atom,
-		.atom = atomn(ptr, len),
+		.atom = atomn(imap->ptr, len),
 	};
-	ptr += len;
+	imap->ptr += len;
 	return data;
 }
 
-static struct Data parseNumber(void) {
+static struct Data parseNumber(struct IMAP *imap) {
 	return (struct Data) {
 		.type = Number,
-		.number = strtoull(ptr, &ptr, 10),
+		.number = strtoull(imap->ptr, &imap->ptr, 10),
 	};
 }
 
-static struct Data parseQuoted(void) {
-	ptr++;
-	size_t len = strcspn(ptr, "\"");
-	if (ptr[len] != '"') errx(EX_PROTOCOL, "missing quoted string delimiter");
+static struct Data parseQuoted(struct IMAP *imap) {
+	imap->ptr++;
+	size_t len = strcspn(imap->ptr, "\"");
+	if (imap->ptr[len] != '"') {
+		errx(EX_PROTOCOL, "missing quoted string delimiter");
+	}
 	struct Data data = {
 		.type = String,
-		.string = strndup(ptr, len),
+		.string = strndup(imap->ptr, len),
 	};
 	if (!data.string) err(EX_OSERR, "strndup");
-	ptr += len + 1;
+	imap->ptr += len + 1;
 	return data;
 }
 
-static struct Data parseLiteral(FILE *imap) {
-	ptr++;
-	size_t len = strtoull(ptr, &ptr, 10);
-	if (*ptr != '}') errx(EX_PROTOCOL, "missing literal prefix delimiter");
+static struct Data parseLiteral(struct IMAP *imap) {
+	imap->ptr++;
+	size_t len = strtoull(imap->ptr, &imap->ptr, 10);
+	if (*imap->ptr != '}') {
+		errx(EX_PROTOCOL, "missing literal prefix delimiter");
+	}
 	struct Data data = {
 		.type = String,
 		.string = malloc(len + 1),
 	};
 	if (!data.string) err(EX_OSERR, "malloc");
-	size_t n = fread(data.string, len, 1, imap);
+	size_t n = fread(data.string, len, 1, imap->r);
 	if (!n) errx(EX_PROTOCOL, "truncated literal");
 	imapLine(imap);
 	data.string[len] = '\0';
 	return data;
 }
 
-static struct Data parseData(FILE *imap);
+static struct Data parseData(struct IMAP *imap);
 
-static struct Data parseList(FILE *imap, char close) {
-	if (*ptr) ptr++;
+static struct Data parseList(struct IMAP *imap, char close) {
+	if (*imap->ptr) imap->ptr++;
 	struct Data data = { .type = List };
-	while (*ptr != close) {
+	while (*imap->ptr != close) {
 		listPush(&data.list, parseData(imap));
 	}
-	if (*ptr) ptr++;
+	if (*imap->ptr) imap->ptr++;
 	return data;
 }
 
-static struct Data parseData(FILE *imap) {
-	if (*ptr == ' ') ptr++;
-	if (*ptr == '"') return parseQuoted();
-	if (*ptr == '{') return parseLiteral(imap);
-	if (*ptr == '(') return parseList(imap, ')');
-	if (*ptr == '[') return parseList(imap, ']');
-	if (*ptr >= '0' && *ptr <= '9') return parseNumber();
-	if (*ptr) return parseAtom();
+static struct Data parseData(struct IMAP *imap) {
+	if (*imap->ptr == ' ') imap->ptr++;
+	if (*imap->ptr == '"') return parseQuoted(imap);
+	if (*imap->ptr == '{') return parseLiteral(imap);
+	if (*imap->ptr == '(') return parseList(imap, ')');
+	if (*imap->ptr == '[') return parseList(imap, ']');
+	if (*imap->ptr >= '0' && *imap->ptr <= '9') return parseNumber(imap);
+	if (*imap->ptr) return parseAtom(imap);
 	errx(EX_PROTOCOL, "unexpected eof");
 }
 
-struct Resp imapResp(FILE *imap) {
+struct Resp imapResp(struct IMAP *imap) {
 	struct Data data;
 	struct Resp resp = {0};
 	imapLine(imap);
@@ -176,8 +231,8 @@ struct Resp imapResp(FILE *imap) {
 	if (data.type != Atom) errx(EX_PROTOCOL, "expected tag atom");
 	resp.tag = data.atom;
 	if (resp.tag == AtomContinue) {
-		if (*ptr == ' ') ptr++;
-		resp.text = ptr;
+		if (*imap->ptr == ' ') imap->ptr++;
+		resp.text = imap->ptr;
 		return resp;
 	}
 
@@ -196,13 +251,13 @@ struct Resp imapResp(FILE *imap) {
 		resp.resp == AtomPreauth ||
 		resp.resp == AtomBye
 	) {
-		if (*ptr == ' ') ptr++;
-		if (*ptr == '[') {
+		if (*imap->ptr == ' ') imap->ptr++;
+		if (*imap->ptr == '[') {
 			data = parseList(imap, ']');
 			resp.code = data.list;
 		}
-		if (*ptr == ' ') ptr++;
-		resp.text = ptr;
+		if (*imap->ptr == ' ') imap->ptr++;
+		resp.text = imap->ptr;
 	} else {
 		data = parseList(imap, '\0');
 		resp.data = data.list;
diff --git a/imap.h b/imap.h
index 8b6b717..3b3bfc7 100644
--- a/imap.h
+++ b/imap.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June McEnroe <june@causal.agency>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -12,6 +12,17 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with OpenSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of OpenSSL used as well as that of the
+ * covered work.
  */
 
 #ifndef IMAP_H
@@ -143,13 +154,30 @@ struct Resp {
 	const char *text;
 };
 
+static inline struct Resp respOk(struct Resp resp) {
+	if (resp.resp == AtomNo || resp.resp == AtomBad || resp.resp == AtomBye) {
+		errx(EX_CONFIG, "%s: %s", Atoms[resp.tag], resp.text);
+	}
+	return resp;
+}
+
 static inline void respFree(struct Resp resp) {
 	listFree(resp.code);
 	listFree(resp.data);
 }
 
 extern bool imapVerbose;
-void imapOpen(FILE **read, FILE **write, const char *host, const char *port);
-struct Resp imapResp(FILE *imapRead);
+
+struct IMAP {
+	int sock;
+	FILE *r;
+	FILE *w;
+	size_t cap;
+	char *buf;
+	char *ptr;
+};
+
+struct IMAP imapOpen(const char *host, const char *port);
+struct Resp imapResp(struct IMAP *imap);
 
 #endif /* IMAP_H */
diff --git a/notemap.1 b/notemap.1
index 5f28ca5..0ba9adc 100644
--- a/notemap.1
+++ b/notemap.1
@@ -1,9 +1,10 @@
-.Dd May  5, 2020
+.Dd December 16, 2020
 .Dt NOTEMAP 1
 .Os
 .
 .Sh NAME
-.Nm notemap
+.Nm notemap ,
+.Nm git-notemap
 .Nd mirror notes to IMAP
 .
 .Sh SYNOPSIS
@@ -13,6 +14,16 @@
 .Op Fl h Ar host
 .Op Fl m Ar file
 .Op Fl p Ar port
+.Ar user
+.Op Ar
+.
+.Nm git
+.Cm notemap
+.Op Fl afv
+.Op Fl M Ar mailbox
+.Op Fl h Ar host
+.Op Fl m Ar file
+.Op Fl p Ar port
 .Op Fl u Ar user
 .Op Ar
 .
@@ -21,14 +32,27 @@ The
 .Nm
 utility
 mirrors text notes
-to an IMAP mailbox.
+to an IMAP mailbox
+appropriate for display
+by the Apple Notes application.
 Files are mapped to IMAP messages
-using a map file.
-If no files are given as arguments,
+using a map file,
+by default
+.Pa .notemap
+in the current directory.
+If no files are specified,
 all mapped files are mirrored.
+New files are added
+to the map file using
+.Fl a .
 .
 .Pp
-IMAP over TLS without STARTTLS is assumed.
+IMAP over TLS is assumed.
+The IMAP host and port
+are automatically discovered
+through SRV record lookup
+on the domain portion of
+.Ar user .
 The password is read from
 .Pa /dev/tty ,
 or standard input if
@@ -36,6 +60,15 @@ or standard input if
 is used.
 .
 .Pp
+The
+.Nm git-notemap
+wrapper uses
+.Xr git-config 1
+and
+.Xr gitcredentials 7
+for defaults and authentication.
+.
+.Pp
 The arguments are as follows:
 .Bl -tag -width Ds
 .It Fl M Ar mailbox
@@ -43,86 +76,85 @@ Mirror notes to
 .Ar mailbox .
 The default is
 .Sy Notes .
-.
 .It Fl a
 Add new notes to the map file.
-.
 .It Fl f
-Overwrite notes which have changed in the mailbox.
-.
+Overwrite modified notes in the mailbox.
 .It Fl h Ar host
 Connect to IMAP on
 .Ar host .
-The default host is determined
-by SRV record lookup on the domain of
-.Ar user ,
-or simply the domain name
-if no SRV record exists.
-Lookup requires
-.Xr dig 1 .
-.
 .It Fl m Ar file
-Set the location of the map file.
+Set the path of the map file.
 The default is
 .Pa .notemap .
-.
 .It Fl p Ar port
 Connect to IMAP on
 .Ar port .
-If the
-.Fl h
-option is used,
-the default port is
-.Sy imaps
-(993).
-Otherwise,
-the port is determined
-in the same fashion as the host.
-.
-.It Fl u Ar user
-Log in to IMAP as
-.Ar user .
-The IMAP connection information
-is inferred from the username unless
-.Fl h
-is used.
-.
 .It Fl v
 Log IMAP protocol to standard error.
-.
 .It Fl w
 Read the password from standard input.
 .El
 .
+.Pp
+The
+.Nm git-notemap
+wrapper loads defaults
+from the following
+.Xr git-config 1
+options:
+.Cm notemap.imapServer ,
+.Cm notemap.imapServerPort ,
+.Cm notemap.imapUser ,
+.Cm notemap.imapPass ,
+.Cm notemap.imapMailbox ,
+.Cm notemap.mapFile .
+If
+.Cm notemap.imapPass
+is unset,
+the password is obtained through
+.Xr gitcredentials 7 .
+.
 .Sh EXAMPLES
 .Bd -literal
-notemap -a -u june@causal.agency note.txt
+notemap -a june@causal.agency note.txt
+git config notemap.imapUser june@causal.agency
+git notemap note.txt
 .Ed
 .
 .Sh STANDARDS
 .Bl -item
 .It
 .Rs
+.%A N. Borenstein
+.%A N. Freed
+.%T MIME Part One: Format of Internet Message Bodies
+.%I IETF
+.%R RFC 2045
+.%U https://tools.ietf.org/html/rfc2045
+.%D November 1996
+.Re
+.It
+.Rs
 .%A M. Crispin
-.%Q University of Washington
 .%T INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
 .%I IETF
-.%N RFC 3501
-.%D March 2003
+.%R RFC 3501
 .%U https://tools.ietf.org/html/rfc3501
+.%Q University of Washington
+.%D March 2003
 .Re
-.
 .It
 .Rs
-.%A N. Freed
-.%A N. Borenstein
-.%T MIME Part One: Format of Internet Message Bodies
+.%A L. Esibov
+.%A A. Gulbrandsen
+.%A P. Vixie
+.%T A DNS RR for specifying the location of services (DNS SRV)
 .%I IETF
-.%N RFC 2045
-.%D November 1996
-.%U https://tools.ietf.org/html/rfc2045
+.%R RFC 2782
+.%U https://tools.ietf.org/html/rfc2782
+.%D February 2000
 .Re
-.
 .It
 .Rs
 .%A P. Leach
@@ -130,25 +162,32 @@ notemap -a -u june@causal.agency note.txt
 .%A R. Salz
 .%T A Universally Unique IDentifier (UUID) URN Namespace
 .%I IETF
-.%N RFC 4122
-.%D July 2005
+.%R RFC 4122
 .%U https://tools.ietf.org/html/rfc4122
+.%D July 2005
+.Re
+.It
+.Rs
+.%A P. Mockapetris
+.%T DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
+.%I IETF
+.%R RFC 1035
+.%U https://tools.ietf.org/html/rfc1035
+.%D November 1987
 .Re
-.
 .It
 .Rs
 .%A P. Resnick, Ed.
-.%Q Qualcomm Incorporated
 .%T Internet Message Format
 .%I IETF
-.%N RFC 5322
-.%D October 2008
+.%R RFC 5322
 .%U https://tools.ietf.org/html/rfc5322
+.%D October 2008
 .Re
 .El
 .
 .Sh AUTHORS
-.An June Bug Aq Mt june@causal.agency
+.An June McEnroe Aq Mt june@causal.agency
 .
 .Sh CAVEATS
 Notes are assumed to be plain UTF-8 text
@@ -160,4 +199,4 @@ Send mail to
 or join
 .Li #ascii.town
 on
-.Li chat.freenode.net .
+.Li irc.tilde.chat .
diff --git a/notemap.c b/notemap.c
index 61112e5..a7e79c2 100644
--- a/notemap.c
+++ b/notemap.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June McEnroe <june@causal.agency>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -12,10 +12,19 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with OpenSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of OpenSSL used as well as that of the
+ * covered work.
  */
 
-#include "compat.h"
-
 #include <err.h>
 #include <inttypes.h>
 #include <stdbool.h>
@@ -29,91 +38,26 @@
 #include <time.h>
 #include <unistd.h>
 
-#ifndef NO_READPASSPHRASE_H
-#include <readpassphrase.h>
-#endif
-
-#include "imap.h"
-
-#if !defined(DIG_PATH) && !defined(DRILL_PATH)
-#	ifdef __FreeBSD__
-#		define DRILL_PATH "/usr/bin/drill"
-#	else
-#		define DIG_PATH "dig"
-#	endif
+#ifdef __APPLE__
+#include <sys/random.h>
 #endif
 
-typedef unsigned char byte;
-
-static void lookup(const char **host, const char **port, const char *domain) {
-	static char buf[1024];
-	snprintf(buf, sizeof(buf), "_imaps._tcp.%s", domain);
-
-	int rw[2];
-	int error = pipe(rw);
-	if (error) err(EX_OSERR, "pipe");
-
-	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
-
-	if (!pid) {
-		close(rw[0]);
-		dup2(rw[1], STDOUT_FILENO);
-		dup2(rw[1], STDERR_FILENO);
-		close(rw[1]);
-#ifdef DRILL_PATH
-		execlp(DRILL_PATH, DRILL_PATH, buf, "SRV", NULL);
-		err(EX_CONFIG, "%s", DRILL_PATH);
+#ifdef DECLARE_RPP
+#define RPP_STDIN 1
+char * readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags);
 #else
-		execlp(DIG_PATH, DIG_PATH, "-t", "SRV", "-q", buf, "+short", NULL);
-		err(EX_CONFIG, "%s", DIG_PATH);
+#include <readpassphrase.h>
 #endif
-	}
-
-	int status;
-	pid = wait(&status);
-	if (pid < 0) err(EX_OSERR, "wait");
-
-	close(rw[1]);
-	FILE *pipe = fdopen(rw[0], "r");
-	if (!pipe) err(EX_IOERR, "fdopen");
 
-	fgets(buf, sizeof(buf), pipe);
-	if (ferror(pipe)) err(EX_IOERR, "fgets");
-
-	if (!WIFEXITED(status) || WEXITSTATUS(status)) {
-		fprintf(stderr, "%s", buf);
-		exit(WEXITSTATUS(status));
-	}
+#include "imap.h"
 
-	char *ptr = buf;
-#ifdef DRILL_PATH
-	for (;;) {
-		char *line = fgets(buf, sizeof(buf), pipe);
-		if (!line || !strcmp(line, ";; ANSWER SECTION:\n")) break;
-	}
-	fgets(buf, sizeof(buf), pipe);
-	if (ferror(pipe)) err(EX_IOERR, "fgets");
-	ptr = strrchr(buf, '\t');
-	ptr = (ptr ? ptr + 1 : buf);
-#endif
-	fclose(pipe);
-
-	char *dot = strrchr(ptr, '.');
-	if (dot) *dot = '\0';
-	strsep(&ptr, " \n"); // priority
-	strsep(&ptr, " \n"); // weight
-	*port = strsep(&ptr, " \n");
-	*host = strsep(&ptr, " \n");
-	if (!*host) {
-		*host = domain;
-		*port = "imaps";
-	}
-}
+typedef unsigned char byte;
 
 static const char *uuidGen(void) {
 	byte uuid[16];
-	arc4random_buf(uuid, sizeof(uuid));
+	int error = getentropy(uuid, sizeof(uuid));
+	if (error) err(EX_OSERR, "getentropy");
+
 	uuid[6] &= 0x0F;
 	uuid[6] |= 0x40;
 	uuid[8] &= 0x3F;
@@ -153,33 +97,24 @@ static char *format(const char *from, const char *uuid, const char *path) {
 		localtime(&status.st_mtime)
 	);
 
-#define HEADERS \
-	"From: <%s>\r\n" \
-	"Date: %s\r\n" \
-	"X-Universally-Unique-Identifier: %s\r\n" \
-	"X-Uniform-Type-Identifier: com.apple.mail-note\r\n" \
-	"X-Mailer: notemap\r\n" \
-	"MIME-Version: 1.0\r\n" \
-	"Content-Type: text/plain; charset=\"utf-8\"\r\n" \
-	"Content-Transfer-Encoding: quoted-printable\r\n" \
-	"Subject: =?utf-8?Q?"
-#define HEADERS_END "?=\r\n\r\n"
-
-	size_t max = sizeof(HEADERS)
-		+ strlen(from)
-		+ strlen(date)
-		+ strlen(uuid)
-		+ 3 * strlen(path)
-		+ sizeof(HEADERS_END)
-		+ 3 * status.st_size
-		+ 3 * status.st_size / 76;
-	char *buf = malloc(max);
-	if (!buf) err(EX_OSERR, "malloc");
-
-	FILE *msg = fmemopen(buf, max, "w");
-	if (!msg) err(EX_OSERR, "fmemopen");
-	fprintf(msg, HEADERS, from, date, uuid);
-
+	char *buf;
+	size_t buflen;
+	FILE *msg = open_memstream(&buf, &buflen);
+	if (!msg) err(EX_OSERR, "open_memstream");
+
+	fprintf(
+		msg,
+		"From: <%s>\r\n"
+		"Date: %s\r\n"
+		"X-Universally-Unique-Identifier: %s\r\n"
+		"X-Uniform-Type-Identifier: com.apple.mail-note\r\n"
+		"X-Mailer: notemap\r\n"
+		"MIME-Version: 1.0\r\n"
+		"Content-Type: text/plain; charset=\"utf-8\"\r\n"
+		"Content-Transfer-Encoding: quoted-printable\r\n"
+		"Subject: =?utf-8?Q?",
+		from, date, uuid
+	);
 	for (const char *ch = path; *ch; ++ch) {
 		if ((uint8_t)*ch & 0x80) {
 			fprintf(msg, "=%02hhX", (uint8_t)*ch);
@@ -189,7 +124,7 @@ static char *format(const char *from, const char *uuid, const char *path) {
 			fprintf(msg, "%c", *ch);
 		}
 	}
-	fprintf(msg, HEADERS_END);
+	fprintf(msg, "?=\r\n\r\n");
 
 	int ch;
 	int len = 0;
@@ -213,9 +148,10 @@ static char *format(const char *from, const char *uuid, const char *path) {
 	}
 	if (ferror(note)) err(EX_IOERR, "%s", path);
 	fclose(note);
-	fclose(msg);
 
-	buf[max - 1] = '\0';
+	error = fclose(msg);
+	if (error) err(EX_IOERR, "fclose");
+
 	return buf;
 }
 
@@ -231,7 +167,7 @@ int main(int argc, char *argv[]) {
 	int rppFlags = 0;
 
 	int opt;
-	while (0 < (opt = getopt(argc, argv, "M:afh:m:p:u:vw"))) {
+	while (0 < (opt = getopt(argc, argv, "M:afh:m:p:vw"))) {
 		switch (opt) {
 			break; case 'M': mailbox = optarg;
 			break; case 'a': add = true;
@@ -245,14 +181,15 @@ int main(int argc, char *argv[]) {
 			break; default:  return EX_USAGE;
 		}
 	}
-	if (!user) errx(EX_USAGE, "username required");
+	if (optind < argc) user = argv[optind++];
 	argv += optind;
 	argc -= optind;
 	
+	if (!user) errx(EX_USAGE, "username required");
 	if (!host) {
-		const char *domain = strchr(user, '@');
-		if (!domain) errx(EX_USAGE, "no domain in username");
-		lookup(&host, &port, &domain[1]);
+		host = strchr(user, '@');
+		if (!host) errx(EX_USAGE, "no domain in username");
+		host++;
 	}
 
 	char buf[1024];
@@ -277,153 +214,128 @@ int main(int argc, char *argv[]) {
 	FILE *map = fopen(path, "r");
 	if (!map) err(EX_NOINPUT, "%s", path);
 
-	size_t cap = 0;
-	char *entry = NULL;
-	char *uuid = NULL;
-	char *note = NULL;
-	uint32_t seq = 0;
-	char *message = NULL;
-
-	enum Atom login = 0;
-	enum Atom next = atom("next");
-	enum Atom create = atom("create");
-	enum Atom replace = atom("replace");
-
-	FILE *imapRead, *imap;
-	imapOpen(&imapRead, &imap, host, port);
-	for (
-		struct Resp resp;
-		resp = imapResp(imapRead), resp.resp != AtomBye;
-		respFree(resp)
-	) {
-		if (resp.resp == AtomNo || resp.resp == AtomBad) {
-			errx(EX_CONFIG, "%s: %s", Atoms[resp.resp], resp.text);
-		}
+	struct Resp resp;
+	struct IMAP imap = imapOpen(host, port);
+	respFree(respOk(imapResp(&imap)));
 
-		if (!login) {
-			login = atom("login");
-			fprintf(
-				imap, "%s LOGIN \"%s\" \"%s\"\r\n",
-				Atoms[login], user, pass
-			);
-		}
+	enum Atom login = atom("login");
+	fprintf(imap.w, "%s LOGIN \"%s\" \"%s\"\r\n", Atoms[login], user, pass);
+	for (; (resp = respOk(imapResp(&imap))).tag != login; respFree(resp));
+	respFree(resp);
 
-		if (resp.tag == login) {
-			fprintf(imap, "%s SELECT \"%s\"\r\n", Atoms[next], mailbox);
-		}
+	enum Atom select = atom("select");
+	fprintf(imap.w, "%s SELECT \"%s\"\r\n", Atoms[select], mailbox);
+	for (; (resp = respOk(imapResp(&imap))).tag != select; respFree(resp));
+	respFree(resp);
 
-		ssize_t len;
-		if (resp.tag == next) {
-next:
-			len = getline(&entry, &cap, map);
-			if (ferror(map)) err(EX_IOERR, "%s", path);
-			if (len < 1) {
-				fprintf(imap, "ayy LOGOUT\r\n");
-				continue;
-			}
-			if (entry[len - 1] == '\n') entry[len - 1] = '\0';
+	size_t cap = 0;
+	char *entry = NULL;
+	for (ssize_t len; 0 < (len = getline(&entry, &cap, map));) {
+		if (entry[len - 1] == '\n') entry[len - 1] = '\0';
 
-			note = entry;
-			uuid = strsep(&note, " ");
-			if (!note || !uuid || !uuidCheck(uuid)) {
-				errx(EX_CONFIG, "invalid map entry: %s", entry);
-			}
+		char *note = entry;
+		char *uuid = strsep(&note, " ");
+		if (!note || !uuid || !uuidCheck(uuid)) {
+			errx(EX_CONFIG, "invalid map entry: %s", entry);
+		}
 
-			if (argc) {
-				int i;
-				for (i = 0; i < argc; ++i) {
-					if (!argv[i]) continue;
-					if (strcmp(argv[i], note)) continue;
-					argv[i] = NULL;
-					break;
-				}
-				if (i == argc) goto next;
+		if (argc) {
+			int i;
+			for (i = 0; i < argc; ++i) {
+				if (!argv[i]) continue;
+				if (strcmp(argv[i], note)) continue;
+				argv[i] = NULL;
+				break;
 			}
-
-			fprintf(
-				imap,
-				"%s SEARCH HEADER X-Universally-Unique-Identifier \"%s\"\r\n",
-				Atoms[AtomSearch], uuid
-			);
-			continue;
+			if (i == argc) continue;
 		}
 
-		if (resp.resp == AtomSearch) {
+		uint32_t seq = 0;
+		enum Atom search = atom("search");
+		fprintf(
+			imap.w,
+			"%s SEARCH HEADER X-Universally-Unique-Identifier \"%s\"\r\n",
+			Atoms[search], uuid
+		);
+		for (; (resp = respOk(imapResp(&imap))).tag != search; respFree(resp)) {
+			if (resp.resp != AtomSearch) continue;
 			if (resp.data.len > 1) {
 				errx(EX_CONFIG, "multiple messages matching %s", uuid);
 			}
 			if (resp.data.len) {
 				seq = dataCheck(resp.data.ptr[0], Number).number;
-				fprintf(
-					imap, "%s FETCH %" PRIu32 " ENVELOPE\r\n",
-					Atoms[AtomFetch], seq
-				);
-			} else {
-				message = format(user, uuid, note);
-				fprintf(
-					imap, "%s APPEND %s (\\Seen) {%zu}\r\n",
-					Atoms[create], mailbox, strlen(message)
-				);
 			}
 		}
-		
-		if (resp.resp == AtomFetch) {
+		respFree(resp);
+
+		if (!seq) goto append;
+
+		struct tm date = {0};
+		enum Atom fetch = atom("fetch");
+		fprintf(
+			imap.w, "%s FETCH %" PRIu32 " ENVELOPE\r\n",
+			Atoms[fetch], seq
+		);
+		for (; (resp = respOk(imapResp(&imap))).tag != fetch; respFree(resp)) {
+			if (resp.resp != AtomFetch) continue;
 			if (!resp.data.len) errx(EX_PROTOCOL, "missing fetch data");
 			struct List items = dataCheck(resp.data.ptr[0], List).list;
 			if (items.len < 2) errx(EX_PROTOCOL, "missing fetch data items");
 			enum Atom item = dataCheck(items.ptr[0], Atom).atom;
 			if (item != AtomEnvelope) continue;
-
 			struct List envelope = dataCheck(items.ptr[1], List).list;
-			if (envelope.len < 1) errx(EX_PROTOCOL, "missing envelope date");
-
-			struct tm date = {0};
-			char *rest = strptime(
+			if (!envelope.len) errx(EX_PROTOCOL, "missing envelope date");
+			const char *rest = strptime(
 				dataCheck(envelope.ptr[0], String).string, DATE_FORMAT, &date
 			);
 			if (!rest) errx(EX_PROTOCOL, "invalid envelope date format");
-
-			struct stat status;
-			int error = stat(note, &status);
-			if (error) err(EX_NOINPUT, "%s", note);
-
-			if (!force && status.st_mtime < mktime(&date)) {
-				errx(
-					EX_TEMPFAIL,
-					"%s: note modified in mailbox; use -f to overwrite",
-					note
-				);
-			} else if (status.st_mtime == mktime(&date)) {
-				goto next;
-			}
-
-			message = format(user, uuid, note);
-			fprintf(
-				imap, "%s APPEND %s (\\Seen) {%zu}\r\n",
-				Atoms[replace], mailbox, strlen(message)
+		}
+		respFree(resp);
+
+		struct stat status;
+		int error = stat(note, &status);
+		if (error) err(EX_NOINPUT, "%s", note);
+		if (status.st_mtime < mktime(&date) && !force) {
+			errx(
+				EX_TEMPFAIL,
+				"%s: note modified in mailbox; use -f to overwrite", note
 			);
+		} else if (status.st_mtime == mktime(&date)) {
+			continue;
 		}
 
-		if (resp.tag == AtomContinue) {
-			fprintf(imap, "%s\r\n", message);
-			free(message);
+append:;
+		char *message = format(user, uuid, note);
+		enum Atom append = atom("append");
+		fprintf(
+			imap.w, "%s APPEND %s (\\Seen) {%zu}\r\n",
+			Atoms[append], mailbox, strlen(message)
+		);
+		for (; (resp = respOk(imapResp(&imap))).tag != append; respFree(resp)) {
+			if (resp.tag == AtomContinue) fprintf(imap.w, "%s\r\n", message);
 		}
+		respFree(resp);
+		free(message);
 
-		if (resp.tag == create) {
+		if (!seq) {
 			printf("+ %s\n", note);
-			goto next;
+			continue;
 		}
 
-		if (resp.tag == replace) {
-			printf("~ %s\n", note);
-			fprintf(
-				imap, "%s STORE %" PRIu32 " +FLAGS (\\Deleted)\r\n",
-				Atoms[next], seq
-			);
-		}
+		enum Atom delete = atom("delete");
+		fprintf(
+			imap.w, "%s STORE %" PRIu32 " +FLAGS (\\Deleted)\r\n",
+			Atoms[delete], seq
+		);
+		for (; (resp = respOk(imapResp(&imap))).tag != delete; respFree(resp));
+		respFree(resp);
+		printf("~ %s\n", note);
 	}
-	fclose(imapRead);
-	fclose(imap);
+	if (ferror(map)) err(EX_IOERR, "%s", path);
+
+	fprintf(imap.w, "ayy LOGOUT\r\n");
+	fclose(imap.r);
+	fclose(imap.w);
 
 	int ret = EX_OK;
 	for (int i = 0; i < argc; ++i) {