Commit Diff


commit - 58a006b393da74d6143bdf2a5378f87f87bd70be
commit + 81c42ca212c93182ef77d20cbb4e58ac4b0de3c7
blob - 152b3f5b275cc7d4db1e7535377d06795b9e40fa
blob + 8c49ea0424b08f1a24a575117dad815777540219
--- README.md
+++ README.md
@@ -2,7 +2,7 @@
 
 A from-scratch IMAP4rev2 ([RFC 9051](https://www.rfc-editor.org/rfc/rfc9051)) server for OpenBSD, written in C in the privilege-separated tradition of `smtpd(8)`, `httpd(8)`, and `ntpd(8)`. No third-party IMAP library.
 
-**Status:** 0.2.2 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
+**Status:** 0.2.3 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
 
 ## What it is
 
@@ -88,7 +88,7 @@ doas rcctl start imapd
 
 `SIGHUP` reloads `spool`, `append max`, `attachment max`, `account sessions`, `connections max`, `idle poll`, `lock timeout`, `login grace`, `startups`, and the TLS certificate/key without dropping connected sessions, `listen on` and `credentials` changes still require a restart. See `imapd.conf(5)`.
 
-IPv6 is supported (`listen on ::` or `listen on *` for dual-stack) but not the default, see the `listen on` directive in `imapd.conf(5)`.
+imapd listens on both IPv4 and IPv6 by default (`listen on *`); use `listen on 0.0.0.0` for IPv4 only or `listen on ::` for IPv6 only. See the `listen on` directive in `imapd.conf(5)`.
 
 ## Getting the source
 
@@ -110,7 +110,7 @@ Report security issues to security@openimapd.dev. Gene
 
 ## License
 
-ISC. See the copyright header in each source file.
+ISC. See the copyright header in each source file, and `CREDITS` for what each third-party copyright in them is for.
 
 ## More
 
blob - /dev/null
blob + 7e624d172740c7fcc358be72fe06639b4ba8ea66 (mode 644)
--- /dev/null
+++ CREDITS
@@ -0,0 +1,121 @@
+OpenIMAPD: credits and lineage
+
+Each file under src/ carries its copyright lines and the ISC licence text.
+Where a file carries a copyright other than David Williams', the notes
+below say what was borrowed from whom and why that copyright is carried.
+They were the files' header comments until 2026-10-05, and are kept here
+so the headers can take the form the rest of the tree uses; the
+attribution they record is unchanged.
+
+log.c
+-----
+This file's log_init()/log_procinit()/log_setverbose()/log_getverbose()/
+vlog()/logit()/log_warn()/log_warnx()/log_info()/log_debug()/fatal()/
+fatalx() API is the same daemon-logging idiom Henning Brauer wrote and
+that is carried, nearly verbatim, across essentially every privsep
+daemon in the OpenBSD base system (smtpd, bgpd, and many others all
+credit him in their own log.c). This file's implementation details
+(log_procname storage, log_init()'s parameters) are this project's own,
+but the API shape and name are that same shared idiom, so his copyright
+is carried forward here too, same rationale as parse.y's copyright
+chain.
+
+log.h
+-----
+The declarations here, log_init(), log_procinit(), log_setverbose(),
+log_getverbose(), log_warn(), log_warnx(), log_info(), log_debug(),
+logit(), vlog(), fatal() and fatalx(), have the names, argument types
+and return types of those in smtpd's log.h (usr.sbin/smtpd/log.h:26-55).
+That is the daemon-logging idiom Henning Brauer wrote and that smtpd,
+bgpd and most other privsep daemons in the OpenBSD base system carry
+his copyright for. smtpd's _r variants are not declared here. His
+copyright is carried forward for the same reason as log.c's.
+
+imsgev.c
+--------
+This file's name and its "struct imsgev" wrapper-around-imsgbuf+
+event(3) concept match Eric Faurot's imsgev.c in OpenBSD's ldapd
+(usr.sbin/ldapd/imsgev.c), not a generic/obvious name, so his copyright
+is carried forward here even though this file's actual function
+signatures and dispatch design (a caller-supplied raw libevent handler,
+vs. ldapd's callback+needfd model) were written independently and differ
+from his implementation.
+
+imsgev_add() below is the exception: it is a verbatim copy (up to
+whitespace and the choice of iev vs. iev->data as event_set()'s last
+argument) of the imsg_event_add() idiom shared across OpenBSD privsep
+daemons, traceable to usr.sbin/ospfd/ospfd.c:525-534 (Claudio Jeker
+2005, Esben Norby 2004, Henning Brauer 2003-2004) and copied with only
+that one-argument variation into dvmrpd.c, npppd.c, and rad.c. Their
+copyright is carried forward for that function specifically, same
+rationale as log.c's and parse.y's shared-idiom copyright chains.
+
+parent.c
+--------
+This file's boot-time peer-wiring handshake, setup_peer_send() and
+setup_done_send(), and the IMSG_SETUP_PEER/IMSG_SETUP_DONE message names
+they and imapd.h use, follows smtpd's setup_peers()/ setup_done()
+(usr.sbin/smtpd/smtpd.c:861-903) and its identically-named
+IMSG_SETUP_PEER/IMSG_SETUP_DONE enumerators (smtpd.h:212-213), which no
+other daemon in the OpenBSD base system defines under those names. The
+functions here are reworked onto imapd's own struct imsgev and multi-
+child bookkeeping rather than copied verbatim, but the message protocol
+and handshake shape are smtpd's; see the inline citations at this file's
+setup_peer_send() and setup_done_send().
+
+send_keymgr_init()'s TLS key permission check, fstat(2), then st_uid
+!= 0 and st_mode & (S_IRWXU|S_IRWXG|S_IRWXO) & ~0740, reuses smtpd's
+ssl_load_key() check (usr.sbin/smtpd/ssl.c:132-145) verbatim for that
+mask and rejection order; only the fixed 0740 bound and the imsg
+delivery around it are this file's own.
+
+listener.c
+----------
+The RSA_METHOD/EC_KEY_METHOD engine override below (keymgr_engine_
+init() and everything it installs) adapts smtpd's ca.c
+(rsa_engine_init()/ecdsa_engine_init()/rsae_priv_enc()/rsae_priv_
+dec()/ecdsae_do_sign(), ca.c:329-508) to imapd's own imsg conventions:
+the OpenSSL API shape leaves little room for independent structure, and
+this project's own licensing precedent (log.c, imsgev.c) already treats
+a borrow this close as needing the original author's copyright even
+where the implementation differs. keymgr_use_fake_private_key()'s two-
+line call shape is lifted from smtpd's smtp.c:187-194 (Gilles Chehade,
+Pierre-Yves Ritschard, Jacek Masiulaniec); see that function's own
+comment.
+
+keymgr.c
+--------
+This file's overall architecture, a dedicated process holding the real
+TLS private key, a "fake private key" installed in the TLS-terminating
+process instead, and a process-wide OpenSSL RSA_METHOD/EC_KEY_METHOD
+engine override that forwards every private-key operation here as a
+synchronous imsg round-trip, is smtpd's ca.c (Reyk Floeter, Gilles
+Chehade), ported to imapd's own imsg/privsep conventions rather than
+copied verbatim. The RSA_METHOD/ EC_KEY_METHOD engine-override code
+itself (the code this file's request/reply pair answers) lives in
+listener.c, not here; see the listener.c entry above for the matching
+attribution.
+
+keymgr_pubkey_hash() below additionally replicates, byte-for-byte,
+smtpd's ssl.c hash_x509()/ssl_pubkey_hash() algorithm (Pierre-Yves
+Ritschard, Reyk Floeter, Gilles Chehade): SHA256 of the certificate's
+DER-encoded SubjectPublicKeyInfo, formatted "SHA256:" plus lowercase
+hex. That exact format is what libtls's own (unexported)
+tls_cert_pubkey_hash() tags onto the fake key's OpenSSL ex_data slot 0
+at TLS-config time (lib/libtls/tls.c); this process's key lookup only
+works if it derives the identical string from the same certificate.
+
+parse.y
+-------
+The parser skeleton below (lgetc()/lungetc()/findeol(), the hand-
+written yylex() built on top of them, pushfile()/popfile(), and
+symset()/symget()) follows the structure common to ripd's, smtpd's, and
+httpd's own parse.y in the OpenBSD base system, all of which carry this
+same four-name copyright chain at their root. This file's grammar and
+domain-specific rules are original; the above four names are carried
+forward for the shared parser-skeleton lineage only.
+
+mbox_manage.c
+-------------
+Carries Copyright (c) 2011 Gilles Chehade <gilles@poolp.org> in its
+header.
blob - 28d70578cab1d0af11363ec0d4b50101db9f8375
blob + f38b0c9a173bb56afa6dc4a4b3e3ce0e9cce1bea
--- src/Makefile
+++ src/Makefile
@@ -39,9 +39,9 @@ DPADD+=		${LIBTLS} ${LIBSSL} ${LIBCRYPTO}
 
 MAN=		imapd.8 imapd.conf.5
 
-CFLAGS+=	-Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes
+CFLAGS+=	-Wall -Wstrict-prototypes -Wmissing-prototypes
 CFLAGS+=	-Wmissing-declarations -Wshadow -Wpointer-arith
-CFLAGS+=	-Wsign-compare -Wcast-qual -Wcast-align
+CFLAGS+=	-Wsign-compare -Wcast-align
 
 # Explicit -g: bsd.prog.mk's DEBUG?=-g default apparently isn't reaching
 # the actual compile line in this tree (the crash-diagnosis gdb session
blob - 596c4dfcef0195bd1299f70fbea1d32586cee463
blob + 1bab52693fc3f4c5eb7ed1bcee3f795343e99c8b
--- src/append_cmd.c
+++ src/append_cmd.c
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/socket.h>
@@ -27,6 +26,7 @@
 #include <errno.h>
 #include <event.h>
 #include <imsg.h>
+#include <limits.h>
 #include <resolv.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -41,7 +41,7 @@
 #include "listener.h"
 #include "mboxname.h"
 
-/* RFC 9051 SS9 date-time */
+/* RFC 9051 section 9 date-time */
 int
 parse_date_time(const char *s, int64_t *out)
 {
@@ -102,11 +102,17 @@ struct append_parsed {
 	int		litnonsync;
 };
 
-/* RFC 9051 SS6.3.12 APPEND */
+/* RFC 9051 section 6.3.12 APPEND */
 int
 parse_append_args(char *args, struct append_parsed *out, const char **errmsg)
 {
-	char	*p = args;
+	char		*p = args, *start, *end, *digits_stop;
+	const char	*sub_err, *errstr;
+	char		 datebuf[64];
+	char		 digitsbuf[24];
+	size_t		 dlen, digits_len;
+	char		 saved;
+	int		 rc;
 
 	memset(out, 0, sizeof(*out));
 	*errmsg = NULL;
@@ -127,13 +133,8 @@ parse_append_args(char *args, struct append_parsed *ou
 	while (*p == ' ')
 		p++;
 	if (*p == '(') {
-		char		*start = p;
-		char		*end = strchr(p, ')');
-		char		 saved;
-		int		 rc;
-		const char	*sub_err;
-
-		if (end == NULL) {
+		start = p;
+		if ((end = strchr(p, ')')) == NULL) {
 			*errmsg = "unterminated flag list";
 			return (-1);
 		}
@@ -153,12 +154,8 @@ parse_append_args(char *args, struct append_parsed *ou
 	}
 
 	if (*p == '"') {
-		const char	*start = p + 1;
-		char		*end = strchr(start, '"');
-		size_t		 dlen;
-		char		 datebuf[64];
-
-		if (end == NULL) {
+		start = p + 1;
+		if ((end = strchr(start, '"')) == NULL) {
 			*errmsg = "unterminated date-time string";
 			return (-1);
 		}
@@ -183,48 +180,36 @@ parse_append_args(char *args, struct append_parsed *ou
 		*errmsg = "expected a message literal";
 		return (-1);
 	}
-	{
-		const char	*start = p + 1;
-		char		*end = strchr(start, '}');
-		char		*digits_end;
-		const char	*digits_stop;
-		char		 digitsbuf[24];
-		size_t		 digits_len;
-		unsigned long long litlen;
+	start = p + 1;
+	if ((end = strchr(start, '}')) == NULL) {
+		*errmsg = "malformed literal announcement";
+		return (-1);
+	}
 
-		if (end == NULL) {
-			*errmsg = "malformed literal announcement";
-			return (-1);
-		}
+	out->litnonsync = (end > start && end[-1] == '+');
+	digits_stop = out->litnonsync ? end - 1 : end;
+	digits_len = (size_t)(digits_stop - start);
 
-		out->litnonsync = (end > start && end[-1] == '+');
-		digits_stop = out->litnonsync ? end - 1 : end;
-		digits_len = (size_t)(digits_stop - start);
+	if (digits_len == 0 || digits_len >= sizeof(digitsbuf)) {
+		*errmsg = "malformed literal octet count";
+		return (-1);
+	}
+	memcpy(digitsbuf, start, digits_len);
+	digitsbuf[digits_len] = '\0';
 
-		if (digits_len == 0 || digits_len >= sizeof(digitsbuf)) {
-			*errmsg = "malformed literal octet count";
-			return (-1);
-		}
-		memcpy(digitsbuf, start, digits_len);
-		digitsbuf[digits_len] = '\0';
+	if (digitsbuf[0] < '0' || digitsbuf[0] > '9') {
+		*errmsg = "malformed literal octet count";
+		return (-1);
+	}
+	out->litlen = strtonum(digitsbuf, 0, LLONG_MAX, &errstr);
+	if (errstr != NULL) {
+		*errmsg = "malformed literal octet count";
+		return (-1);
+	}
 
-		/* strtoull(3) accepts a sign */
-		if (digitsbuf[0] < '0' || digitsbuf[0] > '9') {
-			*errmsg = "malformed literal octet count";
-			return (-1);
-		}
-		errno = 0;
-		litlen = strtoull(digitsbuf, &digits_end, 10);
-		if (*digits_end != '\0' || errno == ERANGE) {
-			*errmsg = "malformed literal octet count";
-			return (-1);
-		}
-		out->litlen = (uint64_t)litlen;
-
-		if (end[1] != '\0') {
-			*errmsg = "literal must be the final argument";
-			return (-1);
-		}
+	if (end[1] != '\0') {
+		*errmsg = "literal must be the final argument";
+		return (-1);
 	}
 
 	return (0);
@@ -249,7 +234,7 @@ cmd_append(struct session *s, const char *tag, char *a
 	if (parsed.litlen > listener_append_max) {
 		char	text[96];
 
-		/* RFC 5530 LIMIT, stating the number as RFC 9051 SS7.1 does */
+		/* RFC 5530 LIMIT; RFC 9051 section 7.1 states the number */
 		(void)snprintf(text, sizeof(text), "[LIMIT] message exceeds "
 		    "this server's %llu octet limit",
 		    (unsigned long long)listener_append_max);
@@ -298,7 +283,7 @@ cmd_append(struct session *s, const char *tag, char *a
 	s->literal_remaining = parsed.litlen;
 	s->literal_pending = 1;
 
-	/* RFC 9051 SS4.3: "+" only for synchronizing literals */
+	/* RFC 9051 section 4.3: "+" only for synchronizing literals */
 	if (!parsed.litnonsync) {
 		static const char cont[] = "+ Ready for literal data\r\n";
 
@@ -339,12 +324,14 @@ void
 session_handle_mbox_appended(struct session *s,
     const struct imsg_mbox_appended *res)
 {
+	char	buf[96];
+
 	s->state = s->append_prev_state;
 
 	if (res->error != MBOX_OP_OK) {
 		if (res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX)
 			session_reply(s, s->pending_tag, "NO",
-			    /* RFC 9051 SS6.3.12 */
+			    /* RFC 9051 section 6.3.12 */
 			    "[TRYCREATE] no such mailbox");
 		else if (res->error == MBOX_OP_ERR_BUSY)
 			session_reply(s, s->pending_tag, "NO",
@@ -358,12 +345,7 @@ session_handle_mbox_appended(struct session *s,
 		return;
 	}
 
-	{
-		char	buf[96];
-
-		snprintf(buf, sizeof(buf),
-		    "[APPENDUID %u %u] APPEND completed", res->uidvalidity,
-		    res->uid);
-		session_reply(s, s->pending_tag, "OK", buf);
-	}
+	snprintf(buf, sizeof(buf), "[APPENDUID %u %u] APPEND completed",
+	    res->uidvalidity, res->uid);
+	session_reply(s, s->pending_tag, "OK", buf);
 }
blob - 21da97eeae47457e4c695fb195895f59f99d0393
blob + d5a6e53fece5ca9abd5fc56e604051fabac867ed
--- src/auth.c
+++ src/auth.c
@@ -25,6 +25,7 @@
 #include <grp.h>
 #include <imsg.h>
 #include <pwd.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -97,6 +98,7 @@ auth_main(void)
 	int			 peer_fd;
 	char			*slash;
 	char			 chrootdir[1024];
+	char			 unveil_path[512];
 	ssize_t			 n;
 
 	imsgev_ibuf_init(&ibuf3, 3);
@@ -161,22 +163,16 @@ auth_main(void)
 
 	imsgev_init_from_ibuf(&iev_parent, &ibuf3, auth_dispatch_parent, NULL);
 
-	{
-		char unveil_path[512];
+	(void)snprintf(unveil_path, sizeof(unveil_path), "/%s",
+	    cred_file_basename);
+	if (unveil(unveil_path, "r") == -1)
+		fatal("unveil %s", unveil_path);
+	if (unveil(NULL, NULL) == -1)
+		fatal("unveil lock");
 
-		(void)snprintf(unveil_path, sizeof(unveil_path), "/%s",
-		    cred_file_basename);
-		if (unveil(unveil_path, "r") == -1)
-			fatal("unveil %s", unveil_path);
-		if (unveil(NULL, NULL) == -1)
-			fatal("unveil lock");
-	}
-
 	/* no recvfd or sendfd: the one peer fd came before this */
-#ifdef __OpenBSD__
 	if (pledge("stdio rpath", NULL) == -1)
 		fatal("pledge");
-#endif
 
 	event_dispatch();
 	fatalx("auth: exited event loop");
@@ -225,7 +221,6 @@ auth_dispatch(int fd, short event, void *arg)
 				log_warnx("bad IMSG_AUTH_REQUEST");
 				break;
 			}
-			/* imsg_get_data() does not NUL-terminate */
 			req.username[sizeof(req.username) - 1] = '\0';
 			req.password[sizeof(req.password) - 1] = '\0';
 
@@ -233,9 +228,8 @@ auth_dispatch(int fd, short event, void *arg)
 			setproctitle("session %u auth", req.session_id);
 
 			if (auth_session_already_resolved(req.session_id)) {
-				log_warnx("session %u: IMSG_AUTH_REQUEST for a "
-				    "session already successfully "
-				    "authenticated, refusing", req.session_id);
+				log_warnx("session %u: IMSG_AUTH_REQUEST: "
+				    "already authenticated", req.session_id);
 				explicit_bzero(req.password,
 				    sizeof(req.password));
 				break;
@@ -278,7 +272,6 @@ auth_dispatch(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 static void
@@ -322,7 +315,6 @@ auth_dispatch_parent(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 /* network usernames reach syslog only through here */
@@ -354,9 +346,8 @@ auth_verify(struct imsg_auth_request *req, struct imsg
 
 		res->ok = 0;
 		auth_safe_name(req->username, overname, sizeof(overname));
-		log_info("session %u: authentication failed for \"%s\" "
-		    "(over AUTH_MAX_TRIES, not checked)", req->session_id,
-		    overname);
+		log_info("session %u: authentication failed for \"%s\" (over "
+		    "AUTH_MAX_TRIES)", req->session_id, overname);
 		return;
 	}
 
@@ -422,8 +413,10 @@ cred_lookup(const char *path, const char *username, st
     int *maxcost)
 {
 	FILE			*fp;
-	char			 line[1024];
+	struct stat		 st;
 	struct cred_entry	 ce;
+	char			*line = NULL;
+	size_t			 linesize = 0;
 	int			 found = 0, cost;
 
 	*maxcost = 0;
@@ -431,45 +424,26 @@ cred_lookup(const char *path, const char *username, st
 		log_warn("fopen %s", path);
 		return (-1);
 	}
-
-	{
-		struct stat	 st;
-
-		if (fstat(fileno(fp), &st) == -1) {
-			log_warn("fstat %s", path);
-			fclose(fp);
-			return (-1);
-		}
-		if (!cred_file_secure(&st)) {
-			log_warnx("%s: insecure (mode %04o, owner uid %u), "
-			    "must be owned by root or the current user and "
-			    "not group-writable, group-executable, or "
-			    "world-accessible; refusing all authentication "
-			    "until this is fixed",
-			    path, (unsigned int)(st.st_mode & 07777),
-			    (unsigned int)st.st_uid);
-			fclose(fp);
-			return (-1);
-		}
+	if (fstat(fileno(fp), &st) == -1) {
+		log_warn("fstat %s", path);
+		fclose(fp);
+		return (-1);
 	}
+	if (!cred_file_secure(&st)) {
+		log_warnx("%s: insecure mode %04o, owner uid %u; "
+		    "authentication refused",
+		    path, (unsigned int)(st.st_mode & 07777),
+		    (unsigned int)st.st_uid);
+		fclose(fp);
+		return (-1);
+	}
 
-	while (fgets(line, sizeof(line), fp) != NULL) {
+	while (getline(&line, &linesize, fp) != -1) {
 		char		*p = line;
 		char		*fields[5];
+		const char	*errstr;
 		int		 i;
-		char		*ep;
-		unsigned long	 ulval;
 
-		/* fgets(3) splits an over-long line */
-		if (strchr(line, '\n') == NULL &&
-		    strlen(line) == sizeof(line) - 1) {
-			log_warnx("%s: over-long line, refusing to parse the "
-			    "credential file", path);
-			explicit_bzero(line, sizeof(line));
-			explicit_bzero(&ce, sizeof(ce));
-			fclose(fp);
-			return (-1);
-		}
 		line[strcspn(line, "\n")] = '\0';
 		if (line[0] == '\0' || line[0] == '#')
 			continue;
@@ -493,22 +467,16 @@ cred_lookup(const char *path, const char *username, st
 		/* crypt_checkpass(3) passes an empty hash and password */
 		if (fields[1][0] != '$' || fields[1][1] != '2')
 			continue;
-		/* strtoul(3) accepts "-1", and uid 0 is root */
+		/* strtonum(3) takes "-1"; 0 is root, (uid_t)-1 is not an id */
 		if (fields[2][0] < '0' || fields[2][0] > '9' ||
 		    fields[3][0] < '0' || fields[3][0] > '9')
 			continue;
-		errno = 0;
-		ulval = strtoul(fields[2], &ep, 10);
-		if (*ep != '\0' || errno != 0 || ulval == 0 ||
-		    ulval >= (unsigned long)(uid_t)-1)
+		ce.uid = strtonum(fields[2], 1, UINT32_MAX - 1, &errstr);
+		if (errstr != NULL)
 			continue;
-		ce.uid = (uid_t)ulval;
-		errno = 0;
-		ulval = strtoul(fields[3], &ep, 10);
-		if (*ep != '\0' || errno != 0 || ulval == 0 ||
-		    ulval >= (unsigned long)(gid_t)-1)
+		ce.gid = strtonum(fields[3], 1, UINT32_MAX - 1, &errstr);
+		if (errstr != NULL)
 			continue;
-		ce.gid = (gid_t)ulval;
 		if (strlcpy(ce.maildir, fields[4], sizeof(ce.maildir)) >=
 		    sizeof(ce.maildir))
 			continue;
@@ -521,8 +489,7 @@ cred_lookup(const char *path, const char *username, st
 		}
 	}
 
-	/* line[] and ce held credential records */
-	explicit_bzero(line, sizeof(line));
+	freezero(line, linesize);
 	explicit_bzero(&ce, sizeof(ce));
 	fclose(fp);
 	return (found ? 0 : -1);
blob - 295dbfe792a76217279f47edda320d56449332aa
blob + 2c7a5f12af243cb6db3e03d426ed7bf90cbc842f
--- src/auth_cmd.c
+++ src/auth_cmd.c
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/socket.h>
@@ -40,18 +39,15 @@
 #include "log.h"
 #include "listener.h"
 
-/* RFC 9051 SS6.1.1 capability strings, selected by session->tls_active. */
+/* RFC 9051 section 6.1.1 capability strings, by session->tls_active */
 #define CAPABILITY_PRE_TLS	"IMAP4rev2 STARTTLS LOGINDISABLED ID " \
 				"CONDSTORE QRESYNC"
 #define CAPABILITY_POST_TLS	"IMAP4rev2 AUTH=PLAIN LOGINDISABLED ID " \
 				"CONDSTORE QRESYNC"
 
-
 int
 cmd_capability(struct session *s, const char *tag, char *args)
 {
-	(void)args;
-
 	session_untagged(s, s->tls_active ?
 	    "CAPABILITY " CAPABILITY_POST_TLS :
 	    "CAPABILITY " CAPABILITY_PRE_TLS);
@@ -59,13 +55,10 @@ cmd_capability(struct session *s, const char *tag, cha
 	return (1);
 }
 
-
 int
 cmd_noop(struct session *s, const char *tag, char *args)
 {
-	(void)args;
-
-	/* RFC 9051 SS6.1.2: the poll; the reply waits for the refresh */
+	/* RFC 9051 section 6.1.2: the poll; the reply waits for the refresh */
 	if (s->state != SESSION_SELECTED ||
 	    strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
 	    sizeof(s->pending_tag) || session_request_idle_refresh(s) == -1) {
@@ -77,20 +70,16 @@ cmd_noop(struct session *s, const char *tag, char *arg
 	return (1);
 }
 
-
 int
 cmd_logout(struct session *s, const char *tag, char *args)
 {
-	(void)args;
-
-	/* Exact example text from RFC 9051 SS6.1.3. */
+	/* Exact example text from RFC 9051 section 6.1.3. */
 	session_untagged(s, "BYE IMAP4rev2 Server logging out");
 	session_reply(s, tag, "OK", "LOGOUT completed");
 	session_teardown(s, "logout");
 	return (0);
 }
 
-
 int
 cmd_id(struct session *s, const char *tag, char *args)
 {
@@ -99,7 +88,7 @@ cmd_id(struct session *s, const char *tag, char *args)
 	const char	*errmsg;
 	int		 inquote = 0;
 
-	/* RFC 2971 SS3.1: only literals are read; the reply is NIL */
+	/* RFC 2971 section 3.1: only literals are read; the reply is NIL */
 	while (p != NULL && *p != '\0') {
 		if (!inquote && *p == '{') {
 			if (read_literal(&p, &lit, &len, &errmsg) == -1)
@@ -119,18 +108,14 @@ cmd_id(struct session *s, const char *tag, char *args)
 	return (1);
 }
 
-
 int
 cmd_login(struct session *s, const char *tag, char *args)
 {
-	(void)args;
-
 	session_reply(s, tag, "NO",
 	    "LOGIN not supported, use AUTHENTICATE PLAIN");
 	return (1);
 }
 
-
 int
 cmd_starttls(struct session *s, const char *tag, char *args)
 {
@@ -139,7 +124,7 @@ cmd_starttls(struct session *s, const char *tag, char 
 		return (1);
 	}
 	if (s->tls_active) {
-		/* RFC 9051 SS6.2.1: BAD if STARTTLS comes after negotiation */
+		/* RFC 9051 section 6.2.1: STARTTLS after negotiation is BAD */
 		session_reply(s, tag, "BAD", "TLS already active");
 		return (1);
 	}
@@ -160,7 +145,7 @@ cmd_starttls(struct session *s, const char *tag, char 
 	return (1);
 }
 
-/* RFC 4616 SS2: authzid/authcid/passwd up to 255 octets + 2 NULs = 767 */
+/* RFC 4616 section 2: authzid/authcid/passwd up to 255 octets + 2 NULs = 767 */
 #define SASL_PLAIN_MAX	768
 
 /* as auth.c's auth_safe_name(): '?' for anything unprintable */
@@ -223,7 +208,7 @@ sasl_plain_finish(struct session *s, const char *tag, 
 	passwd = nul + 1;
 	passwdlen = (size_t)rawlen - off - authcidlen - 1;
 
-	/* RFC 4616 SS2: empty prep result fails verification, NO not BAD */
+	/* RFC 4616 section 2: an empty prep result is NO, not BAD */
 	if (authcidlen == 0 || passwdlen == 0) {
 		session_reply(s, tag, "NO",
 		    "[AUTHENTICATIONFAILED] authentication failed");
@@ -273,14 +258,14 @@ session_handle_auth_continuation(struct session *s, co
 {
 	s->auth_cont = 0;
 
-	/* RFC 9051 SS6.2.2: lone "*" cancels exchange */
+	/* RFC 9051 section 6.2.2: lone "*" cancels exchange */
 	if (strcmp(line, "*") == 0) {
 		session_reply(s, s->pending_tag, "BAD",
 		    "AUTHENTICATE cancelled");
 		return (1);
 	}
 
-	return sasl_plain_finish(s, s->pending_tag, line, 0);
+	return (sasl_plain_finish(s, s->pending_tag, line, 0));
 }
 
 int
@@ -295,7 +280,7 @@ session_handle_idle_continuation(struct session *s, co
 	done = strcasecmp(line, "DONE") == 0;
 	s->refresh_status = done ? "OK" : "BAD";
 	s->refresh_text = done ? "IDLE terminated" : "expected DONE";
-	/* RFC 9051 SS7.5.1: an EXPUNGE in flight goes before the reply */
+	/* RFC 9051 section 7.5.1: an EXPUNGE in flight goes before the reply */
 	if (!s->idle_refresh_pending) {
 		session_reply(s, s->pending_tag, s->refresh_status,
 		    s->refresh_text);
@@ -304,7 +289,6 @@ session_handle_idle_continuation(struct session *s, co
 	return (1);
 }
 
-
 int
 cmd_authenticate(struct session *s, const char *tag, char *args)
 {
@@ -327,7 +311,7 @@ cmd_authenticate(struct session *s, const char *tag, c
 		initial = (*p != '\0') ? p : NULL;
 	}
 
-	/* RFC 9051 SS6.2.2: MUST NOT permit plaintext mechanisms pre-TLS */
+	/* RFC 9051 section 6.2.2: no plaintext mechanisms before TLS */
 	if (!s->tls_active) {
 		/* RFC 5530 PRIVACYREQUIRED: retry after STARTTLS */
 		session_reply(s, tag, "NO",
@@ -343,7 +327,7 @@ cmd_authenticate(struct session *s, const char *tag, c
 
 	if (initial != NULL) {
 		s->scrub_inbuf = 1;
-		return sasl_plain_finish(s, tag, initial, 1);
+		return (sasl_plain_finish(s, tag, initial, 1));
 	}
 
 	if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
@@ -359,13 +343,12 @@ cmd_authenticate(struct session *s, const char *tag, c
 int
 stub_not_implemented(struct session *s, const char *tag, const char *cmdname)
 {
-	log_debug("session %u: %s not implemented (store.c's IMSG_MBOX_* "
-	    "wire protocol isn't designed yet)", s->id, cmdname);
+	log_debug("session %u: %s not implemented", s->id, cmdname);
 	session_reply(s, tag, "NO", "not implemented");
 	return (1);
 }
 
-/* RFC 9051 SS6.3.1 ENABLE */
+/* RFC 9051 section 6.3.1 ENABLE */
 int
 cmd_enable(struct session *s, const char *tag, char *args)
 {
blob - 8e8b857481d199891feabf48f917a2758c8c1a62
blob + a5e1441eb3b2b803cc7daebfc68f9faa3ddca484
--- src/envelope.c
+++ src/envelope.c
@@ -16,7 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
+#include <sys/param.h>
 #include <sys/types.h>
 #include <sys/file.h>
 #include <sys/stat.h>
@@ -58,7 +58,6 @@ envbuf_append(char *buf, size_t bufsize, size_t *outle
 	return (0);
 }
 
-
 int
 envbuf_append_str(char *buf, size_t bufsize, size_t *outlen, const char *s)
 {
@@ -81,7 +80,7 @@ envbuf_append_nstring(char *buf, size_t bufsize, size_
 	for (i = 0; i < vallen; i++) {
 		char	 c = val[i];
 
-		/* RFC 9051 SS4.3: substituted, not dropped */
+		/* RFC 9051 section 4.3: substituted, not dropped */
 		if (c == '\0' || c == '\r' || c == '\n')
 			c = ' ';
 		if ((c == '"' || c == '\\') &&
@@ -99,7 +98,7 @@ fail:
 	return (-1);
 }
 
-/* RFC 5322 SS3.2.4: the index just past the quoted string at i */
+/* RFC 5322 section 3.2.4: the index just past the quoted string at i */
 static size_t
 quoted_end(const char *s, size_t len, size_t i)
 {
@@ -126,7 +125,7 @@ unquoted_find(const char *s, size_t len, size_t i, con
 	return (len);
 }
 
-/* RFC 5322 SS3.2.2: each comment becomes one space; returns the length */
+/* RFC 5322 section 3.2.2: each comment becomes one space; returns the length */
 size_t
 address_uncomment(char *s, size_t len)
 {
@@ -157,7 +156,7 @@ address_uncomment(char *s, size_t len)
 	return (n);
 }
 
-/* unquoted in place (RFC 5322 SS3.2.4); a phrase keeps one space per run */
+/* RFC 5322 section 3.2.4, unquoted in place; one space per run in a phrase */
 size_t
 address_cook(char *s, size_t len, int phrase)
 {
@@ -187,7 +186,7 @@ address_cook(char *s, size_t len, int phrase)
 	return (n);
 }
 
-/* RFC 5322 SS3.4 mailbox, split and cooked in place; -1 if it is none */
+/* RFC 5322 section 3.4 mailbox, split and cooked in place; -1 if it is none */
 int
 address_split(char *tok, size_t toklen, struct address *a)
 {
@@ -210,7 +209,7 @@ address_split(char *tok, size_t toklen, struct address
 		speclen--;
 	}
 
-	/* RFC 5322 SS4.4 obs-route, for RFC 9051's at-domain-list */
+	/* RFC 5322 section 4.4 obs-route, for RFC 9051's at-domain-list */
 	if (speclen > 0 && spec[0] == '@') {
 		colon = unquoted_find(spec, speclen, 0, ":");
 		if (colon == speclen)
@@ -237,7 +236,7 @@ address_split(char *tok, size_t toklen, struct address
 	return (0);
 }
 
-/* RFC 9051 SS7.5.2 address structure; a group marker has no host */
+/* RFC 9051 section 7.5.2 address structure; a group marker has no host */
 int
 envbuf_append_one_address(char *buf, size_t bufsize, size_t *outlen,
     const struct address *a)
@@ -258,7 +257,7 @@ envbuf_append_one_address(char *buf, size_t bufsize, s
 	return (envbuf_append(buf, bufsize, outlen, ")", 1));
 }
 
-/* RFC 5322 SS3.4 address-list: the next mailbox, group start or end */
+/* RFC 5322 section 3.4 address-list: the next mailbox, group start or end */
 int
 address_list_next(char *val, size_t vallen, size_t *pos, int *ingroup,
     char **tok_out, size_t *toklen_out)
@@ -324,17 +323,18 @@ address_list_add(char *buf, size_t bufsize, size_t *ou
 	return (0);
 }
 
-/* RFC 5322 SS4.5.3: every occurrence of the field as one list, or NIL */
+/* RFC 5322 section 4.5.3: every occurrence of the field as one list, or NIL */
 static int
 envbuf_append_address_fields(char *buf, size_t bufsize, size_t *outlen,
     const char *hdr, size_t hdrlen, const char *field, int *seen)
 {
 	const char	*name, *val;
 	char		*unf;
-	size_t		 flen = strlen(field), off = 0, namelen, vallen;
+	size_t		 flen, off = 0, namelen, vallen;
 	size_t		 ulen, i;
 	int		 any = 0, rc;
 
+	flen = strlen(field);
 	*seen = 0;
 	while (header_next_field(hdr, hdrlen, &off, &name, &namelen, &val,
 	    &vallen) == 1) {
@@ -376,7 +376,7 @@ append_field_nstring(char *out, size_t outsize, size_t
 	return (rc);
 }
 
-/* RFC 9051 SS7.5.2 ENVELOPE; Sender and Reply-To default to From */
+/* RFC 9051 section 7.5.2 ENVELOPE; Sender and Reply-To default to From */
 static int
 envelope_from_header(const char *hdr, size_t hdrlen, char *out,
     size_t outsize, size_t *outlen)
@@ -411,7 +411,7 @@ envelope_from_header(const char *hdr, size_t hdrlen, c
 	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
 		return (-1);
 
-	for (fi = 0; fi < 2; fi++) {
+	for (fi = 0; fi < nitems(fallback_fields); fi++) {
 		save = *outlen;
 		if (envbuf_append_address_fields(out, outsize, outlen, hdr,
 		    hdrlen, fallback_fields[fi], &seen) == -1)
@@ -426,7 +426,7 @@ envelope_from_header(const char *hdr, size_t hdrlen, c
 			return (-1);
 	}
 
-	for (fi = 0; fi < 3; fi++) {
+	for (fi = 0; fi < nitems(addr_fields); fi++) {
 		if (envbuf_append_address_fields(out, outsize, outlen, hdr,
 		    hdrlen, addr_fields[fi], &seen) == -1)
 			return (-1);
@@ -462,8 +462,8 @@ build_envelope(int fd, const char *basename, char **bu
 		return (-1);
 	if (envelope_from_header(hdrbuf, hdrlen, out, sizeof(out),
 	    &outlen) == -1) {
-		log_warnx("session %u: message %s: formatted ENVELOPE exceeds "
-		    "ENVELOPE_MAX, ENVELOPE skipped", session_id, basename);
+		log_warnx("session %u: message %s: ENVELOPE over "
+		    "ENVELOPE_MAX, skipped", session_id, basename);
 		free(hdrbuf);
 		return (-1);
 	}
@@ -479,36 +479,36 @@ build_envelope(int fd, const char *basename, char **bu
 	return (0);
 }
 
-/* RFC 9051 SS7.5.2 BODYSTRUCTURE; no extension data */
+/* RFC 9051 section 7.5.2 BODYSTRUCTURE; no extension data */
 int
 build_body_structure(int depth, int *nparts_used, const char *hdr,
     size_t hdrlen, const char *body, size_t bodylen, char *out,
     size_t outsize, size_t *outlen)
 {
-	char	type[64], subtype[64];
-	char	params_fmt[600];
-	/* RFC 2046 SS5.1.1 caps boundary at 70 chars, +1 NUL */
-	char	boundary[70 + 1];
-	int	has_boundary;
+	static const char *const cte_known[] = { "7BIT", "8BIT", "BINARY",
+	    "BASE64", "QUOTED-PRINTABLE" };
+	struct content_type	 ct;
+	char			 encstr[40], numbuf[32];
+	char			*idval = NULL, *descval = NULL, *encval = NULL;
+	const char		*enc;
+	size_t			 idlen = 0, desclen = 0, enclen = 0, ihdr, ki;
+	int			 is_text, is_msg, rc;
 
 	if (depth > MIME_MAX_DEPTH)
 		return (-1);
 	if (++*nparts_used > MIME_MAX_PARTS)
 		return (-1);
 
-	params_fmt[0] = '\0';
-	if (parse_content_type(hdr, hdrlen, type, sizeof(type), subtype,
-	    sizeof(subtype), params_fmt, sizeof(params_fmt), boundary,
-	    sizeof(boundary), &has_boundary) == -1)
+	if (parse_content_type(hdr, hdrlen, &ct) == -1)
 		return (-1);
 
-	if (strcasecmp(type, "MULTIPART") == 0) {
+	if (strcasecmp(ct.type, "MULTIPART") == 0) {
 		size_t	 part_starts[MIME_MAX_PARTS], part_ends[MIME_MAX_PARTS];
 		int	 n, i;
 
-		if (!has_boundary)
+		if (!ct.has_boundary)
 			return (-1);
-		if (split_multipart(body, bodylen, boundary, part_starts,
+		if (split_multipart(body, bodylen, ct.boundary, part_starts,
 		    part_ends, &n, MIME_MAX_PARTS) == -1)
 			return (-1);
 
@@ -519,7 +519,7 @@ build_body_structure(int depth, int *nparts_used, cons
 			size_t		 plen = part_ends[i] - part_starts[i];
 			size_t		 phdrend;
 
-			/* RFC 2046 SS5.1.1: an empty part is legal */
+			/* RFC 2046 section 5.1.1: an empty part is legal */
 			if (plen == 0)
 				phdrend = 0;
 			else if (find_header_body_split(pbuf, plen,
@@ -532,132 +532,104 @@ build_body_structure(int depth, int *nparts_used, cons
 		}
 		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
 			return (-1);
-		if (envbuf_append_nstring(out, outsize, outlen, subtype,
-		    strlen(subtype)) == -1)
+		if (envbuf_append_nstring(out, outsize, outlen, ct.subtype,
+		    strlen(ct.subtype)) == -1)
 			return (-1);
 		return (envbuf_append(out, outsize, outlen, ")", 1));
 	}
 
-	{
-		char	*idval = NULL, *descval = NULL, *encval = NULL;
-		size_t	 idlen = 0, desclen = 0, enclen = 0, ihdr;
-		char	 encstr[40];
-		int	 is_text = (strcasecmp(type, "TEXT") == 0);
-		int	 is_msg = strcasecmp(type, "MESSAGE") == 0 &&
-		    (strcasecmp(subtype, "RFC822") == 0 ||
-		    strcasecmp(subtype, "GLOBAL") == 0);
-		int	 rc = 0;
+	is_text = strcasecmp(ct.type, "TEXT") == 0;
+	is_msg = strcasecmp(ct.type, "MESSAGE") == 0 &&
+	    (strcasecmp(ct.subtype, "RFC822") == 0 ||
+	    strcasecmp(ct.subtype, "GLOBAL") == 0);
 
-		if (envbuf_append(out, outsize, outlen, "(", 1) == -1)
-			return (-1);
-		if (envbuf_append_nstring(out, outsize, outlen, type,
-		    strlen(type)) == -1)
-			return (-1);
-		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
-			return (-1);
-		if (envbuf_append_nstring(out, outsize, outlen, subtype,
-		    strlen(subtype)) == -1)
-			return (-1);
-		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
-			return (-1);
-		if (envbuf_append(out, outsize, outlen, params_fmt,
-		    strlen(params_fmt)) == -1)
-			return (-1);
-		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
-			return (-1);
+	if (envbuf_append(out, outsize, outlen, "(", 1) == -1)
+		return (-1);
+	if (envbuf_append_nstring(out, outsize, outlen, ct.type,
+	    strlen(ct.type)) == -1)
+		return (-1);
+	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+		return (-1);
+	if (envbuf_append_nstring(out, outsize, outlen, ct.subtype,
+	    strlen(ct.subtype)) == -1)
+		return (-1);
+	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+		return (-1);
+	if (envbuf_append(out, outsize, outlen, ct.params_fmt,
+	    strlen(ct.params_fmt)) == -1)
+		return (-1);
+	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+		return (-1);
 
-		if (extract_header_field(hdr, hdrlen, "Content-Id", &idval,
-		    &idlen) == 0)
-			rc = envbuf_append_nstring(out, outsize, outlen, idval,
-			    idlen);
-		else
-			rc = envbuf_append_nstring(out, outsize, outlen,
-			    NULL, 0);
-		free(idval);
-		if (rc == -1)
-			return (-1);
-		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
-			return (-1);
+	if (extract_header_field(hdr, hdrlen, "Content-Id", &idval,
+	    &idlen) == 0)
+		rc = envbuf_append_nstring(out, outsize, outlen, idval, idlen);
+	else
+		rc = envbuf_append_nstring(out, outsize, outlen, NULL, 0);
+	free(idval);
+	if (rc == -1)
+		return (-1);
+	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+		return (-1);
 
-		if (extract_header_field(hdr, hdrlen, "Content-Description",
-		    &descval, &desclen) == 0)
-			rc = envbuf_append_nstring(out, outsize, outlen,
-			    descval, desclen);
-		else
-			rc = envbuf_append_nstring(out, outsize, outlen,
-			    NULL, 0);
-		free(descval);
-		if (rc == -1)
-			return (-1);
-		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
-			return (-1);
+	if (extract_header_field(hdr, hdrlen, "Content-Description",
+	    &descval, &desclen) == 0)
+		rc = envbuf_append_nstring(out, outsize, outlen, descval,
+		    desclen);
+	else
+		rc = envbuf_append_nstring(out, outsize, outlen, NULL, 0);
+	free(descval);
+	if (rc == -1)
+		return (-1);
+	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+		return (-1);
 
-		if (extract_header_field(hdr, hdrlen,
-		    "Content-Transfer-Encoding", &encval, &enclen) == 0 &&
-		    enclen > 0 && enclen < sizeof(encstr)) {
-			memcpy(encstr, encval, enclen);
-			encstr[enclen] = '\0';
-		} else if (strlcpy(encstr, "7BIT", sizeof(encstr)) >=
-		    sizeof(encstr)) {
-			return (-1);
+	if (extract_header_field(hdr, hdrlen, "Content-Transfer-Encoding",
+	    &encval, &enclen) == 0 && enclen > 0 && enclen < sizeof(encstr)) {
+		memcpy(encstr, encval, enclen);
+		encstr[enclen] = '\0';
+	} else
+		strlcpy(encstr, "7BIT", sizeof(encstr));
+	free(encval);
+	enc = encstr;
+	for (ki = 0; ki < nitems(cte_known); ki++) {
+		if (strcasecmp(encstr, cte_known[ki]) == 0) {
+			enc = cte_known[ki];
+			break;
 		}
-		free(encval);
-		{
-			static const char *const known[] = { "7BIT", "8BIT",
-			    "BINARY", "BASE64", "QUOTED-PRINTABLE" };
-			size_t	 ki;
+	}
+	if (envbuf_append_nstring(out, outsize, outlen, enc, strlen(enc)) == -1)
+		return (-1);
+	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+		return (-1);
 
-			for (ki = 0; ki < 5; ki++) {
-				if (strcasecmp(encstr, known[ki]) == 0) {
-					if (strlcpy(encstr, known[ki],
-					    sizeof(encstr)) >= sizeof(encstr))
-						return (-1);
-					break;
-				}
-			}
-		}
-		if (envbuf_append_nstring(out, outsize, outlen, encstr,
-		    strlen(encstr)) == -1)
-			return (-1);
-		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
-			return (-1);
+	snprintf(numbuf, sizeof(numbuf), "%zu", bodylen);
+	if (envbuf_append_str(out, outsize, outlen, numbuf) == -1)
+		return (-1);
 
-		{
-			char	numbuf[32];
+	/* RFC 9051 section 7.5.2 body-type-msg: envelope, body, lines */
+	if (is_msg && (bodylen == 0 ||
+	    find_header_body_split(body, bodylen, &ihdr) == -1 ||
+	    envbuf_append(out, outsize, outlen, " ", 1) == -1 ||
+	    envelope_from_header(body, ihdr, out, outsize, outlen) == -1 ||
+	    envbuf_append(out, outsize, outlen, " ", 1) == -1 ||
+	    build_body_structure(depth + 1, nparts_used, body, ihdr,
+	    body + ihdr, bodylen - ihdr, out, outsize, outlen) == -1))
+		return (-1);
 
-			snprintf(numbuf, sizeof(numbuf), "%zu", bodylen);
-			if (envbuf_append_str(out, outsize, outlen,
-			    numbuf) == -1)
-				return (-1);
-		}
+	if (is_text || is_msg) {
+		size_t	lines = 0, li;
 
-		/* RFC 9051 SS7.5.2 body-type-msg: envelope, body, lines */
-		if (is_msg && (bodylen == 0 ||
-		    find_header_body_split(body, bodylen, &ihdr) == -1 ||
-		    envbuf_append(out, outsize, outlen, " ", 1) == -1 ||
-		    envelope_from_header(body, ihdr, out, outsize,
-		    outlen) == -1 ||
-		    envbuf_append(out, outsize, outlen, " ", 1) == -1 ||
-		    build_body_structure(depth + 1, nparts_used, body, ihdr,
-		    body + ihdr, bodylen - ihdr, out, outsize, outlen) == -1))
-			return (-1);
-
-		if (is_text || is_msg) {
-			size_t	lines = 0, li;
-			char	numbuf2[32];
-
-			for (li = 0; li < bodylen; li++) {
-				if (body[li] == '\n')
-					lines++;
-			}
-			snprintf(numbuf2, sizeof(numbuf2), " %zu", lines);
-			if (envbuf_append_str(out, outsize, outlen,
-			    numbuf2) == -1)
-				return (-1);
+		for (li = 0; li < bodylen; li++) {
+			if (body[li] == '\n')
+				lines++;
 		}
-
-		return (envbuf_append(out, outsize, outlen, ")", 1));
+		snprintf(numbuf, sizeof(numbuf), " %zu", lines);
+		if (envbuf_append_str(out, outsize, outlen, numbuf) == -1)
+			return (-1);
 	}
+
+	return (envbuf_append(out, outsize, outlen, ")", 1));
 }
 
 int
blob - e04d21fe95064d78b9b7c40c63a0569221f683f7
blob + da4556a01b07ad01066be08de5e5a4b7ec0b1a8f
--- src/fetch_cmd.c
+++ src/fetch_cmd.c
@@ -39,18 +39,18 @@
 #include "log.h"
 #include "listener.h"
 
+/* RFC 9051 section 9 nz-number: digit-nz first, so no sign or leading zero */
 int
 parse_nz_number(const char *str, uint32_t *out)
 {
-	unsigned long	 v;
-	char		*end;
+	const char	*errstr;
+	long long	 v;
 
-	if (str == NULL || *str == '\0' || *str == '0')
+	if (str == NULL || *str < '1' || *str > '9')
 		return (-1);
 
-	errno = 0;
-	v = strtoul(str, &end, 10);
-	if (*end != '\0' || errno == ERANGE || v == 0 || v > UINT32_MAX)
+	v = strtonum(str, 1, UINT32_MAX, &errstr);
+	if (errstr != NULL)
 		return (-1);
 
 	*out = (uint32_t)v;
@@ -101,7 +101,7 @@ parse_one_seq_range(const char *tok, struct seq_range 
 	return (0);
 }
 
-/* RFC 9051 SS9 sequence-set */
+/* RFC 9051 section 9 sequence-set */
 int
 parse_sequence_set(const char *text, struct seq_range ranges[SEQSET_MAX_RANGES],
     uint32_t *nranges, const char **errmsg)
@@ -146,7 +146,7 @@ parse_sequence_set(const char *text, struct seq_range 
 
 		if (*p == '\0')
 			break;
-		p++;	/* skip ',' */
+		p++;
 	}
 
 	*nranges = n;
@@ -193,7 +193,7 @@ fetch_att_tok(char *str, char **savep)
 	return (start);
 }
 
-/* RFC 9051 SS9 HEADER.FIELDS[.NOT]; -1 is BAD */
+/* RFC 9051 section 9 HEADER.FIELDS[.NOT]; -1 is BAD */
 int
 parse_header_fields_att(const char *inner, int *not_out, char *fields_out,
     size_t fields_outsize)
@@ -257,7 +257,7 @@ parse_header_fields_att(const char *inner, int *not_ou
 	return (0);
 }
 
-/* RFC 9051 SS6.4.5.1 section-part */
+/* RFC 9051 section 6.4.5.1 section-part */
 int
 section_part_valid(const char *s)
 {
@@ -280,12 +280,12 @@ section_part_valid(const char *s)
 			return (0);
 		s++;
 		if (*s == '\0')
-			return (0);	/* trailing dot */
+			return (0);
 	}
 	return (1);
 }
 
-/* RFC 9051 SS6.4.5 <start.count> */
+/* RFC 9051 section 6.4.5 <start.count> */
 int
 parse_partial_suffix(const char *s, int *has_partial_out,
     uint32_t *start_out, uint32_t *count_out)
@@ -326,11 +326,23 @@ parse_partial_suffix(const char *s, int *has_partial_o
 	return (0);
 }
 
+/* RFC 9051 section 6.4.5 fetch-att list, parsed */
+struct fetch_atts {
+	uint32_t	 attrs;
+	int		 degraded;
+	int		 bodystructure_full;
+	int		 header_fields_not;
+	int		 has_partial;
+	uint32_t	 partial_start;
+	uint32_t	 partial_count;
+	char		 header_fields[HEADER_FIELDS_MAX];
+	char		 header_fields_label[HEADER_FIELDS_LABEL_MAX];
+	char		 section_part[SECTION_PART_MAX];
+};
+
 static int
-parse_body_peek_section_tok(const char *bracket_start, uint32_t *attrs_inout,
-    char *section_part_out, size_t section_part_outsize,
-    int *has_partial_out, uint32_t *partial_start_out,
-    uint32_t *partial_count_out, int *degraded_out, const char **errmsg)
+parse_body_peek_section_tok(const char *bracket_start, struct fetch_atts *fa,
+    int *degraded_out, const char **errmsg)
 {
 	char		*close;
 	char		 inner[SECTION_PART_MAX];
@@ -350,21 +362,21 @@ parse_body_peek_section_tok(const char *bracket_start,
 	suffix = close + 1;
 
 	if (suffix[0] != '\0' &&
-	    parse_partial_suffix(suffix, has_partial_out, partial_start_out,
-	    partial_count_out) == -1) {
+	    parse_partial_suffix(suffix, &fa->has_partial, &fa->partial_start,
+	    &fa->partial_count) == -1) {
 		*errmsg = "malformed <partial> range";
 		return (-1);
 	}
 
 	if (inner[0] == '\0') {
-		*attrs_inout |= MBOX_FETCH_BODY_WHOLE;
+		fa->attrs |= MBOX_FETCH_BODY_WHOLE;
 	} else if (strcasecmp(inner, "TEXT") == 0) {
-		*attrs_inout |= MBOX_FETCH_BODY_TEXT;
+		fa->attrs |= MBOX_FETCH_BODY_TEXT;
 	} else if (section_part_valid(inner)) {
-		*attrs_inout |= MBOX_FETCH_BODY_PART;
-		if (strlcpy(section_part_out, inner, section_part_outsize) >=
-		    section_part_outsize) {
-			*attrs_inout &= ~MBOX_FETCH_BODY_PART;
+		fa->attrs |= MBOX_FETCH_BODY_PART;
+		if (strlcpy(fa->section_part, inner,
+		    sizeof(fa->section_part)) >= sizeof(fa->section_part)) {
+			fa->attrs &= ~MBOX_FETCH_BODY_PART;
 			*degraded_out = 1;
 			return (0);
 		}
@@ -378,19 +390,18 @@ parse_body_peek_section_tok(const char *bracket_start,
 }
 
 static int
-parse_body_peek_header_fields_tok(const char *sec, uint32_t *attrs_inout,
-    int *header_fields_not_out, char *header_fields_out,
-    size_t header_fields_outsize, char *header_fields_label_out,
-    size_t header_fields_label_outsize, const char **errmsg)
+parse_body_peek_header_fields_tok(const char *sec, struct fetch_atts *fa,
+    const char **errmsg)
 {
-	size_t	 len = strlen(sec);
+	size_t	 len;
 	char	 inner[HEADER_FIELDS_LABEL_MAX];
 
+	len = strlen(sec);
 	if (len < 1 || sec[len - 1] != ']') {
 		*errmsg = "malformed HEADER.FIELDS section";
 		return (-1);
 	}
-	if (*attrs_inout & MBOX_FETCH_HEADER_FIELDS)
+	if (fa->attrs & MBOX_FETCH_HEADER_FIELDS)
 		/* already captured one, ignore any further duplicates */
 		return (0);
 
@@ -401,17 +412,18 @@ parse_body_peek_header_fields_tok(const char *sec, uin
 	memcpy(inner, sec, len - 1);
 	inner[len - 1] = '\0';
 
-	if (parse_header_fields_att(inner, header_fields_not_out,
-	    header_fields_out, header_fields_outsize) == -1) {
+	if (parse_header_fields_att(inner, &fa->header_fields_not,
+	    fa->header_fields, sizeof(fa->header_fields)) == -1) {
 		*errmsg = "malformed HEADER.FIELDS section";
 		return (-1);
 	}
-	if (strlcpy(header_fields_label_out, inner,
-	    header_fields_label_outsize) >= header_fields_label_outsize) {
+	if (strlcpy(fa->header_fields_label, inner,
+	    sizeof(fa->header_fields_label)) >=
+	    sizeof(fa->header_fields_label)) {
 		*errmsg = "HEADER.FIELDS section too long";
 		return (-1);
 	}
-	*attrs_inout |= MBOX_FETCH_HEADER_FIELDS;
+	fa->attrs |= MBOX_FETCH_HEADER_FIELDS;
 	return (1);
 }
 
@@ -426,35 +438,17 @@ body_section(const char *tok, int *plain)
 	return (NULL);
 }
 
-/* RFC 9051 SS6.4.5 fetch-att; unsupported items are skipped */
+/* RFC 9051 section 6.4.5 fetch-att; unsupported items are skipped */
 int
-parse_fetch_atts(char *spec, uint32_t *attrs_out, int *degraded_out,
-    int *header_fields_not_out, char *header_fields_out,
-    size_t header_fields_outsize, char *header_fields_label_out,
-    size_t header_fields_label_outsize, int *bodystructure_full_out,
-    char *section_part_out, size_t section_part_outsize,
-    int *has_partial_out, uint32_t *partial_start_out,
-    uint32_t *partial_count_out, const char **errmsg)
+parse_fetch_atts(char *spec, struct fetch_atts *fa, const char **errmsg)
 {
 	char		*p, *tok, *save;
 	const char	*sec;
 	size_t		 len;
-	uint32_t	 attrs = 0;
-	int		 degraded = 0, plain, d;
-	int		 has_partial = 0;
-	uint32_t	 partial_start = 0, partial_count = 0;
+	int		 plain, d;
 
 	*errmsg = NULL;
-	*attrs_out = 0;
-	*degraded_out = 0;
-	*header_fields_not_out = 0;
-	header_fields_out[0] = '\0';
-	header_fields_label_out[0] = '\0';
-	*bodystructure_full_out = 0;
-	section_part_out[0] = '\0';
-	*has_partial_out = 0;
-	*partial_start_out = 0;
-	*partial_count_out = 0;
+	memset(fa, 0, sizeof(*fa));
 
 	if (spec == NULL || *spec == '\0') {
 		*errmsg = "missing message data item(s)";
@@ -475,87 +469,75 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int 
 	for (tok = fetch_att_tok(p, &save); tok != NULL;
 	    tok = fetch_att_tok(NULL, &save)) {
 		if (strcasecmp(tok, "FAST") == 0) {
-			/* RFC 9051 SS6.4.5 FAST macro */
-			attrs |= MBOX_FETCH_FLAGS | MBOX_FETCH_INTERNALDATE |
-			    MBOX_FETCH_RFC822_SIZE;
+			fa->attrs |= MBOX_FETCH_FLAGS |
+			    MBOX_FETCH_INTERNALDATE | MBOX_FETCH_RFC822_SIZE;
 		} else if (strcasecmp(tok, "ALL") == 0) {
 			/* FAST + ENVELOPE */
-			attrs |= MBOX_FETCH_FLAGS | MBOX_FETCH_INTERNALDATE |
-			    MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_ENVELOPE;
+			fa->attrs |= MBOX_FETCH_FLAGS |
+			    MBOX_FETCH_INTERNALDATE | MBOX_FETCH_RFC822_SIZE |
+			    MBOX_FETCH_ENVELOPE;
 		} else if (strcasecmp(tok, "FULL") == 0) {
 			/* ALL + bare BODY */
-			attrs |= MBOX_FETCH_FLAGS | MBOX_FETCH_INTERNALDATE |
-			    MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_ENVELOPE |
-			    MBOX_FETCH_BODYSTRUCTURE;
+			fa->attrs |= MBOX_FETCH_FLAGS |
+			    MBOX_FETCH_INTERNALDATE | MBOX_FETCH_RFC822_SIZE |
+			    MBOX_FETCH_ENVELOPE | MBOX_FETCH_BODYSTRUCTURE;
 		} else if (strcasecmp(tok, "FLAGS") == 0) {
-			attrs |= MBOX_FETCH_FLAGS;
+			fa->attrs |= MBOX_FETCH_FLAGS;
 		} else if (strcasecmp(tok, "UID") == 0) {
-			attrs |= MBOX_FETCH_UID;
+			fa->attrs |= MBOX_FETCH_UID;
 		} else if (strcasecmp(tok, "INTERNALDATE") == 0) {
-			attrs |= MBOX_FETCH_INTERNALDATE;
+			fa->attrs |= MBOX_FETCH_INTERNALDATE;
 		} else if (strcasecmp(tok, "RFC822.SIZE") == 0) {
-			attrs |= MBOX_FETCH_RFC822_SIZE;
+			fa->attrs |= MBOX_FETCH_RFC822_SIZE;
 		} else if (strcasecmp(tok, "MODSEQ") == 0) {
-			/* RFC 7162 SS3.1.4.2 */
-			attrs |= MBOX_FETCH_MODSEQ;
+			fa->attrs |= MBOX_FETCH_MODSEQ;
 		} else if ((sec = body_section(tok, &plain)) != NULL) {
 			d = 0;
 			if (strcasecmp(sec, "HEADER]") == 0)
-				attrs |= MBOX_FETCH_BODY_HEADER;
+				fa->attrs |= MBOX_FETCH_BODY_HEADER;
 			else if (strncasecmp(sec, "HEADER.FIELDS",
 			    strlen("HEADER.FIELDS")) == 0) {
-				if (parse_body_peek_header_fields_tok(sec,
-				    &attrs, header_fields_not_out,
-				    header_fields_out, header_fields_outsize,
-				    header_fields_label_out,
-				    header_fields_label_outsize,
+				if (parse_body_peek_header_fields_tok(sec, fa,
 				    errmsg) == -1)
 					return (-1);
-			} else if (parse_body_peek_section_tok(sec, &attrs,
-			    section_part_out, section_part_outsize,
-			    &has_partial, &partial_start, &partial_count,
-			    &d, errmsg) == -1)
+			} else if (parse_body_peek_section_tok(sec, fa, &d,
+			    errmsg) == -1)
 				return (-1);
 			if (plain && !d)
-				attrs |= MBOX_FETCH_SET_SEEN;
-			degraded |= d;
+				fa->attrs |= MBOX_FETCH_SET_SEEN;
+			fa->degraded |= d;
 		} else if (strcasecmp(tok, "ENVELOPE") == 0) {
-			/* SS7.5.2; no .PEEK, no \Seen effect */
-			attrs |= MBOX_FETCH_ENVELOPE;
+			/* section 7.5.2; no .PEEK, no \Seen effect */
+			fa->attrs |= MBOX_FETCH_ENVELOPE;
 		} else if (strcasecmp(tok, "BODY") == 0 ||
 		    strcasecmp(tok, "BODYSTRUCTURE") == 0) {
-			attrs |= MBOX_FETCH_BODYSTRUCTURE;
-			*bodystructure_full_out =
+			fa->attrs |= MBOX_FETCH_BODYSTRUCTURE;
+			fa->bodystructure_full =
 			    (strcasecmp(tok, "BODYSTRUCTURE") == 0);
 		} else if (strncasecmp(tok, "BODY", 4) == 0 ||
 		    strcasecmp(tok, "RFC822") == 0 ||
 		    strcasecmp(tok, "RFC822.HEADER") == 0 ||
 		    strcasecmp(tok, "RFC822.TEXT") == 0) {
 			/* other BODY forms and RFC822 items: unimplemented */
-			degraded = 1;
+			fa->degraded = 1;
 		} else {
 			*errmsg = "unknown message data item";
 			return (-1);
 		}
 	}
 
-	if (attrs == 0) {
+	if (fa->attrs == 0) {
 		*errmsg = "cannot fetch that message content yet, "
 		    "supported: FLAGS/UID/INTERNALDATE/RFC822.SIZE/MODSEQ/"
 		    "ENVELOPE/(BODY|BODYSTRUCTURE)/BODY[...]/BODY.PEEK[...]";
 		return (-2);
 	}
 
-	/* RFC 9051 SS6.4.5: HEADER wins over HEADER.FIELDS */
-	if ((attrs & MBOX_FETCH_BODY_HEADER) &&
-	    (attrs & MBOX_FETCH_HEADER_FIELDS))
-		attrs &= ~MBOX_FETCH_HEADER_FIELDS;
+	/* RFC 9051 section 6.4.5: HEADER wins over HEADER.FIELDS */
+	if ((fa->attrs & MBOX_FETCH_BODY_HEADER) &&
+	    (fa->attrs & MBOX_FETCH_HEADER_FIELDS))
+		fa->attrs &= ~MBOX_FETCH_HEADER_FIELDS;
 
-	*attrs_out = attrs;
-	*degraded_out = degraded;
-	*has_partial_out = has_partial;
-	*partial_start_out = partial_start;
-	*partial_count_out = partial_count;
 	return (0);
 }
 
@@ -564,7 +546,7 @@ const char *fetch_month_names[12] = {
 	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
 };
 
-/* RFC 9051 SS9 date-time, always "+0000": no tzdata in the chroot */
+/* RFC 9051 section 9 date-time, always "+0000": no tzdata in the chroot */
 void
 format_internaldate(int64_t ts, char *out, size_t outsize)
 {
@@ -572,10 +554,7 @@ format_internaldate(int64_t ts, char *out, size_t outs
 	time_t		 t = (time_t)ts;
 
 	if (gmtime_r(&t, &tm) == NULL) {
-		if (strlcpy(out, "01-Jan-1970 00:00:00 +0000", outsize) >=
-		    outsize)
-			log_warnx("format_internaldate: fallback string "
-			    "truncated, caller's buffer too small");
+		strlcpy(out, "01-Jan-1970 00:00:00 +0000", outsize);
 		return;
 	}
 
@@ -636,7 +615,6 @@ session_write_file_range(struct session *s, int fd, ui
 	}
 }
 
-/* RFC 9051 SS7.5.2 */
 void
 session_send_fetch_response(struct session *s,
     struct imsg_mbox_fetch_meta *meta)
@@ -659,7 +637,7 @@ session_send_fetch_response(struct session *s,
 
 	fetch_append(buf, sizeof(buf), &len, "%u FETCH (", meta->seqno);
 
-	/* RFC 9051 SS6.4.5, RFC 7162 SS3.2.4: report a \Seen this set */
+	/* RFC 9051 section 6.4.5, RFC 7162 section 3.2.4: FLAGS if \Seen set */
 	if ((s->fetch_attrs & MBOX_FETCH_FLAGS) || meta->seen_set) {
 		fetch_append(buf, sizeof(buf), &len, "FLAGS (%s)", meta->flags);
 		need_sp = 1;
@@ -681,7 +659,7 @@ session_send_fetch_response(struct session *s,
 		need_sp = 1;
 	}
 	if ((s->fetch_attrs & MBOX_FETCH_MODSEQ) || cs) {
-		/* RFC 7162 SS3.1.4.2 fetch-mod-resp */
+		/* RFC 7162 section 3.1.4.2 fetch-mod-resp */
 		fetch_append(buf, sizeof(buf), &len, "%sMODSEQ (%llu)",
 		    need_sp ? " " : "", (unsigned long long)meta->modseq);
 		need_sp = 1;
@@ -801,7 +779,7 @@ session_send_fetch_response(struct session *s,
 	}
 }
 
-/* RFC 9051 SS6.4.6 STORE echo */
+/* RFC 9051 section 6.4.6 STORE echo */
 void
 session_send_store_fetch_response(struct session *s,
     const struct imsg_mbox_fetch_meta *meta)
@@ -809,7 +787,7 @@ session_send_store_fetch_response(struct session *s,
 	char	buf[MBOX_FLAGS_MAX + 96];
 	size_t	len;
 
-	/* RFC 9051 SS6.4.9: UID STORE echoes UID after FLAGS */
+	/* RFC 9051 section 6.4.9: UID STORE echoes UID after FLAGS */
 	len = (size_t)snprintf(buf, sizeof(buf), "%u FETCH (FLAGS (%s)",
 	    meta->seqno, meta->flags);
 	if (s->cmd_by_uid && len < sizeof(buf))
@@ -824,7 +802,7 @@ session_send_store_fetch_response(struct session *s,
 	session_untagged(s, buf);
 }
 
-/* RFC 4466, RFC 7162 SS3.1.4.1 and SS3.2.6 modifiers */
+/* RFC 4466, RFC 7162 section 3.1.4.1 and section 3.2.6 modifiers */
 int
 parse_fetch_modifiers(char *modspec, struct imsg_mbox_fetch *req,
     const struct session *s, int by_uid, int *want_vanished,
@@ -846,24 +824,21 @@ parse_fetch_modifiers(char *modspec, struct imsg_mbox_
 	for (tok = strtok_r(p, " ", &save); tok != NULL;
 	    tok = strtok_r(NULL, " ", &save)) {
 		if (strcasecmp(tok, "CHANGEDSINCE") == 0) {
-			const char	*valtok = strtok_r(NULL, " ", &save);
-			char		*ep;
+			const char	*valtok, *errstr;
 
+			valtok = strtok_r(NULL, " ", &save);
 			if (valtok == NULL) {
 				*errmsg = "CHANGEDSINCE requires a "
 				    "mod-sequence value";
 				return (-1);
 			}
-			/* strtoull(3) accepts a sign */
 			if (*valtok < '0' || *valtok > '9') {
 				*errmsg = "invalid CHANGEDSINCE mod-sequence";
 				return (-1);
 			}
-			errno = 0;
-			req->changedsince = strtoull(valtok, &ep, 10);
-			if (*ep != '\0' || errno != 0 ||
-			    req->changedsince == 0 ||
-			    req->changedsince > MODSEQ_MAX) {
+			req->changedsince = strtonum(valtok, 1, MODSEQ_MAX,
+			    &errstr);
+			if (errstr != NULL) {
 				*errmsg = "invalid CHANGEDSINCE mod-sequence";
 				return (-1);
 			}
@@ -871,14 +846,14 @@ parse_fetch_modifiers(char *modspec, struct imsg_mbox_
 			req->attrs |= MBOX_FETCH_MODSEQ;
 		} else if (strcasecmp(tok, "VANISHED") == 0) {
 			if (!by_uid) {
-				/* RFC 7162 SS3.2.6: BAD */
+				/* RFC 7162 section 3.2.6: BAD */
 				*errmsg = "VANISHED is only valid as a UID "
-				    "FETCH modifier (RFC 7162 SS3.2.6)";
+				    "FETCH modifier";
 				return (-1);
 			}
 			if (!s->qresync_enabled) {
 				*errmsg = "VANISHED requires ENABLE QRESYNC "
-				    "first (RFC 7162 SS3.2.6)";
+				    "first";
 				return (-1);
 			}
 			*want_vanished = 1;
@@ -891,7 +866,7 @@ parse_fetch_modifiers(char *modspec, struct imsg_mbox_
 	return (0);
 }
 
-/* RFC 9051 SS4.3: FETCH's literals, header-fld-names, as quoted strings */
+/* RFC 9051 section 4.3: FETCH's literals (header-fld-names) as quoted */
 static int
 fetch_unliteral(char *spec, char *out, size_t outsize, const char **errmsg)
 {
@@ -937,29 +912,22 @@ fetch_unliteral(char *spec, char *out, size_t outsize,
 int
 cmd_fetch(struct session *s, const char *tag, char *args)
 {
-	return fetch_dispatch(s, tag, args, 0);
+	return (fetch_dispatch(s, tag, args, 0));
 }
 
-/* RFC 9051 SS6.4.9: UID FETCH forces UID */
+/* RFC 9051 section 6.4.9: UID FETCH forces UID */
 int
 fetch_dispatch(struct session *s, const char *tag, char *args, int by_uid)
 {
 	struct imsg_mbox_fetch	 req;
-	const char		*seqtok;
-	char			*attspec, *modspec;
+	struct fetch_atts	 fa;
 	struct seq_range	 ranges[SEQSET_MAX_RANGES];
-	uint32_t		 nranges, attrs;
-	int			 rc, want_vanished = 0, degraded;
-	int			 header_fields_not = 0;
-	char			 header_fields[HEADER_FIELDS_MAX];
-	char			 header_fields_label[HEADER_FIELDS_LABEL_MAX];
-	char			 unlit[SESSION_INBUF_MAX];
-	int			 bodystructure_full = 0;
-	char			 section_part[SECTION_PART_MAX];
-	int			 has_partial = 0;
-	uint32_t		 partial_start = 0, partial_count = 0;
-	const char		*errmsg;
+	const char		*seqtok, *errmsg;
 	const char		*cmdname = by_uid ? "UID FETCH" : "FETCH";
+	char			*attspec, *modspec;
+	char			 unlit[SESSION_INBUF_MAX];
+	uint32_t		 nranges;
+	int			 rc, want_vanished = 0;
 
 	if (args == NULL) {
 		session_reply(s, tag, "BAD",
@@ -994,11 +962,7 @@ fetch_dispatch(struct session *s, const char *tag, cha
 	if (*modspec == '\0')
 		modspec = NULL;
 
-	rc = parse_fetch_atts(attspec, &attrs, &degraded, &header_fields_not,
-	    header_fields, sizeof(header_fields), header_fields_label,
-	    sizeof(header_fields_label), &bodystructure_full, section_part,
-	    sizeof(section_part), &has_partial, &partial_start,
-	    &partial_count, &errmsg);
+	rc = parse_fetch_atts(attspec, &fa, &errmsg);
 	if (rc == -1) {
 		session_reply(s, tag, "BAD", errmsg);
 		return (1);
@@ -1007,81 +971,45 @@ fetch_dispatch(struct session *s, const char *tag, cha
 		session_reply(s, tag, "NO", errmsg);
 		return (1);
 	}
-	/* RFC 9051 SS6.3.3: EXAMINE permits no change, \Seen included */
+	/* RFC 9051 section 6.3.3: EXAMINE permits no change, \Seen included */
 	if (s->mbox_readonly)
-		attrs &= ~MBOX_FETCH_SET_SEEN;
-	if (degraded)
-		log_debug("session %u: %s: one or more unsupported message "
-		    "data items not returned (a part number before "
-		    "HEADER/TEXT/MIME, RFC822[.HEADER/.TEXT]), "
-		    "answering the rest, then NO", s->id, cmdname);
+		fa.attrs &= ~MBOX_FETCH_SET_SEEN;
+	if (fa.degraded)
+		log_debug("session %u: %s: unsupported item skipped", s->id,
+		    cmdname);
 
 	memset(&req, 0, sizeof(req));
-	req.attrs = attrs;
+	req.attrs = fa.attrs;
 	req.by_uid = by_uid;
-	req.header_fields_not = header_fields_not;
+	req.header_fields_not = fa.header_fields_not;
+	req.has_partial = fa.has_partial;
+	req.partial_start = fa.partial_start;
+	req.partial_count = fa.partial_count;
+	strlcpy(req.header_fields, fa.header_fields, sizeof(req.header_fields));
+	strlcpy(req.section_part, fa.section_part, sizeof(req.section_part));
 
-	if (strlcpy(req.header_fields, header_fields,
-	    sizeof(req.header_fields)) >= sizeof(req.header_fields) ||
-	    strlcpy(req.section_part, section_part, sizeof(req.section_part))
-	    >= sizeof(req.section_part)) {
-		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
-		return (1);
-	}
-	req.has_partial = has_partial;
-	req.partial_start = partial_start;
-	req.partial_count = partial_count;
+	if (fa.attrs & MBOX_FETCH_BODY_HEADER)
+		strlcpy(s->pending_header_label, "HEADER",
+		    sizeof(s->pending_header_label));
+	else if (fa.attrs & MBOX_FETCH_HEADER_FIELDS)
+		strlcpy(s->pending_header_label, fa.header_fields_label,
+		    sizeof(s->pending_header_label));
 
-	if (attrs & MBOX_FETCH_BODY_HEADER) {
-		if (strlcpy(s->pending_header_label, "HEADER",
-		    sizeof(s->pending_header_label)) >=
-		    sizeof(s->pending_header_label)) {
-			session_reply(s, tag, "NO",
-			    "[SERVERBUG] internal error");
-			return (1);
-		}
-	} else if (attrs & MBOX_FETCH_HEADER_FIELDS) {
-		if (strlcpy(s->pending_header_label, header_fields_label,
-		    sizeof(s->pending_header_label)) >=
-		    sizeof(s->pending_header_label)) {
-			session_reply(s, tag, "NO",
-			    "[SERVERBUG] internal error");
-			return (1);
-		}
-	}
-
-	if (attrs & MBOX_FETCH_BODY_WHOLE) {
+	if (fa.attrs & MBOX_FETCH_BODY_WHOLE)
 		s->pending_body_label[0] = '\0';
-	} else if (attrs & MBOX_FETCH_BODY_TEXT) {
-		if (strlcpy(s->pending_body_label, "TEXT",
-		    sizeof(s->pending_body_label)) >=
-		    sizeof(s->pending_body_label)) {
-			session_reply(s, tag, "NO",
-			    "[SERVERBUG] internal error");
-			return (1);
-		}
-	} else if (attrs & MBOX_FETCH_BODY_PART) {
-		if (strlcpy(s->pending_body_label, section_part,
-		    sizeof(s->pending_body_label)) >=
-		    sizeof(s->pending_body_label)) {
-			session_reply(s, tag, "NO",
-			    "[SERVERBUG] internal error");
-			return (1);
-		}
-	}
-	s->pending_body_has_partial = has_partial;
-	s->pending_body_partial_origin = partial_start;
+	else if (fa.attrs & MBOX_FETCH_BODY_TEXT)
+		strlcpy(s->pending_body_label, "TEXT",
+		    sizeof(s->pending_body_label));
+	else if (fa.attrs & MBOX_FETCH_BODY_PART)
+		strlcpy(s->pending_body_label, fa.section_part,
+		    sizeof(s->pending_body_label));
+	s->pending_body_has_partial = fa.has_partial;
+	s->pending_body_partial_origin = fa.partial_start;
 
-	if (attrs & MBOX_FETCH_BODYSTRUCTURE) {
-		if (strlcpy(s->pending_bodystructure_label,
-		    bodystructure_full ? "BODYSTRUCTURE" : "BODY",
-		    sizeof(s->pending_bodystructure_label)) >=
-		    sizeof(s->pending_bodystructure_label)) {
-			session_reply(s, tag, "NO",
-			    "[SERVERBUG] internal error");
-			return (1);
-		}
-	}
+	if (fa.attrs & MBOX_FETCH_BODYSTRUCTURE)
+		strlcpy(s->pending_bodystructure_label,
+		    fa.bodystructure_full ? "BODYSTRUCTURE" : "BODY",
+		    sizeof(s->pending_bodystructure_label));
 
 	if (modspec != NULL) {
 		if (parse_fetch_modifiers(modspec, &req, s, by_uid,
@@ -1092,10 +1020,9 @@ fetch_dispatch(struct session *s, const char *tag, cha
 	}
 
 	if (want_vanished && !req.has_changedsince) {
-		/* RFC 7162 SS3.2.6: VANISHED needs CHANGEDSINCE */
+		/* RFC 7162 section 3.2.6: VANISHED needs CHANGEDSINCE */
 		session_reply(s, tag, "BAD",
-		    "VANISHED requires CHANGEDSINCE also be specified "
-		    "(RFC 7162 SS3.2.6)");
+		    "VANISHED requires CHANGEDSINCE");
 		return (1);
 	}
 	req.want_vanished = want_vanished;
@@ -1112,7 +1039,7 @@ fetch_dispatch(struct session *s, const char *tag, cha
 
 	req.nranges = nranges;
 
-	/* RFC 7162 SS3.1: both enable CONDSTORE */
+	/* RFC 7162 section 3.1: both enable CONDSTORE */
 	if (req.attrs & MBOX_FETCH_MODSEQ)
 		session_condstore_enable(s);
 
@@ -1123,12 +1050,12 @@ fetch_dispatch(struct session *s, const char *tag, cha
 	}
 	s->fetch_attrs = req.attrs;
 	s->cmd_by_uid = by_uid;
-	/* RFC 9051 SS6.4.5: "NO - fetch error: can't fetch that data" */
-	s->fetch_incomplete = degraded;
+	/* RFC 9051 section 6.4.5: "NO - fetch error: can't fetch that data" */
+	s->fetch_incomplete = fa.degraded;
 	s->state = SESSION_FETCHING;
 
-	if (!send_mbox_request(s, IMSG_MBOX_FETCH, cmdname, "IMSG_MBOX_FETCH",
-	    &req, sizeof(req), ranges, nranges, sizeof(struct seq_range))) {
+	if (send_mbox_request(s, IMSG_MBOX_FETCH, "IMSG_MBOX_FETCH", &req,
+	    sizeof(req), ranges, nranges, sizeof(struct seq_range)) == -1) {
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		s->state = SESSION_SELECTED;
 		return (1);
blob - 4e44803fb3a69f2c0741294274aac08ab9941e6a
blob + 778057317fadc0ccefd39083d8134d5be49d653c
--- src/imapd.8
+++ src/imapd.8
@@ -3,7 +3,7 @@
 .\" Written for the OpenIMAPD project. Public domain / no rights reserved,
 .\" matching the project's ports-oriented, OpenBSD-base-inclusion goal.
 .\"
-.Dd $Mdocdate: October 3 2026 $
+.Dd $Mdocdate: October 5 2026 $
 .Dt IMAPD 8
 .Os
 .Sh NAME
@@ -11,7 +11,7 @@
 .Nd Internet Message Access Protocol (IMAP) daemon
 .Sh SYNOPSIS
 .Nm
-.Op Fl dVv
+.Op Fl dnVv
 .Op Fl D Ar macro Ns = Ns Ar value
 .Op Fl f Ar file
 .Sh DESCRIPTION
@@ -27,6 +27,24 @@ as described in
 and rewrites each new message with the CRLF line endings RFC 5322
 requires before any client sees it.
 .Pp
+Message files that other software places in a mailbox's
+.Pa cur
+or
+.Pa new
+directory, such as a maildir brought from another server, are given
+UIDs the next time the mailbox is selected or its status is read,
+keeping the flags in their names.
+A mailbox whose
+.Pa imapd.index
+is lost is rebuilt the same way, with a new
+.Li UIDVALIDITY .
+A file found in
+.Pa cur
+with the deleted flag
+.Pq Sq T
+is left out unless the mailbox has no index, since it may be a
+message that was expunged but could not be removed.
+.Pp
 By default,
 .Nm
 listens on port 143, where clients may upgrade to TLS with
@@ -79,10 +97,16 @@ Use
 .Ar file
 as the configuration file, instead of the default
 .Pa /etc/imapd.conf .
+.It Fl n
+Configtest mode.
+Only check the configuration file for validity.
 .It Fl V
 Print the version and exit.
 .It Fl v
 Produce more verbose logging.
+Multiple
+.Fl v
+options increase the verbosity.
 .El
 .Sh FILES
 .Bl -tag -width "/etc/ssl/private/imapd.keyXX" -compact
@@ -113,6 +137,8 @@ While it is absent, every mailbox is subscribed.
 Highest
 .Li UIDVALIDITY
 issued, at the root of each maildir.
+If it cannot be read or written, a value already issued may be issued
+again.
 .El
 .Sh SEE ALSO
 .Xr imapd.conf 5 ,
@@ -156,6 +182,11 @@ or
 polls the selected mailbox, so new mail is reported within one
 .Ic idle poll
 interval.
+Files placed directly in
+.Pa cur
+are not noticed during
+.Li IDLE
+until something else changes the mailbox.
 .Pp
 One process serves all sessions of an account, so a
 .Li STORE ,
blob - a4b13bb83eaccee953142bf9ebf1432fe2958254
blob + 3ddc28bc90525e93fd6ccfd5d0d24f281d248de7
--- src/imapd.conf.5
+++ src/imapd.conf.5
@@ -3,7 +3,7 @@
 .\" Written for the OpenIMAPD project. Public domain / no rights reserved,
 .\" matching the project's ports-oriented, OpenBSD-base-inclusion goal.
 .\"
-.Dd $Mdocdate: October 4 2026 $
+.Dd $Mdocdate: October 6 2026 $
 .Dt IMAPD.CONF 5
 .Os
 .Sh NAME
@@ -79,7 +79,9 @@ A login past the limit is answered
 .Li NO
 with the
 .Li LIMIT
-response code and the connection is closed.
+response code, after an untagged
+.Li BYE
+with the same code, and the connection is closed.
 A client that vanishes without closing its connection holds its
 session until TCP keepalive notices, which by default is 2 hours 10
 minutes; see
@@ -220,7 +222,11 @@ If no
 .Ic listen on
 is given,
 .Xr imapd 8
-listens on 0.0.0.0 port 143 and 0.0.0.0 tls port 993.
+listens on
+.Sq *
+port 143 and
+.Sq *
+tls port 993.
 If any is given, only the listeners named are bound.
 .It Ic lock timeout Ar seconds
 How long a command waits for a mailbox lock held by another process
@@ -362,6 +368,18 @@ or
 .Pa ..
 component.
 A line of 1023 or more characters causes every login to be refused.
+A login is also refused if the account's mailbox process is not running
+in the
+.Ar maildir
+within 10 seconds, as when the directory does not exist or its
+filesystem is not mounted.
+The client is sent an untagged
+.Li BYE
+and then
+.Li NO ,
+both with the
+.Li UNAVAILABLE
+response code, and the cause is logged.
 .Xr imapduser 8
 adds and removes entries.
 .Pp
blob - ba320d62cfd97a54d1625488669effc1319157c3
blob + 5e7347935a2ef25064d6c7a8e50def90a4decbe3
--- src/imapd.conf.example
+++ src/imapd.conf.example
@@ -21,19 +21,18 @@
 # && doas chmod 600 /etc/imapd.conf)
 #
 
-# Listen on all IPv4 interfaces: cleartext/STARTTLS on port 143, implicit
-# TLS (RFC 8314) on port 993. Both listeners must share the same address.
-# These are also the defaults if "listen on" is omitted entirely.
-listen on 0.0.0.0 port 143
-listen on 0.0.0.0 tls port 993
+# Listen on all IPv4 and IPv6 interfaces: cleartext/STARTTLS on port 143,
+# implicit TLS (RFC 8314) on port 993. Both listeners must share the same
+# address. These are also the defaults if "listen on" is omitted entirely.
+listen on * port 143
+listen on * tls port 993
 
-# The address may also be "::" (all IPv6 interfaces) or "*" (both IPv4 and
-# IPv6, binds two sockets per listener, one of each family, matching
-# httpd.conf(5)'s own "*" convention), or a single literal IPv4/IPv6
-# address. Hostnames are not accepted. For example, to listen on both
-# address families:
-#listen on * port 143
-#listen on * tls port 993
+# The address may also be "0.0.0.0" (all IPv4 interfaces), "::" (all IPv6
+# interfaces), or a single literal IPv4/IPv6 address. Hostnames are not
+# accepted. "*" binds two sockets per listener, one of each family, as
+# httpd.conf(5)'s "*" does. For example, IPv4 only:
+#listen on 0.0.0.0 port 143
+#listen on 0.0.0.0 tls port 993
 
 # Mail spool root, chrooted into by the per-session store child.
 # Defaults to /var/mail/imapd.
blob - ad6ba2b57e341a039b4ee83e8985f20671d4446f
blob + 585519020ecf17b7299167f9eaaf557d89299509
--- src/imapd.h
+++ src/imapd.h
@@ -28,7 +28,7 @@
 #include <imsg.h>
 #include <stdint.h>
 
-#define IMAPD_VERSION	"0.2.2"
+#define IMAPD_VERSION	"0.2.3"
 
 #define IMAPD_USER	"_imapd"
 #define IMAPD_AUTH_USER	"_imapauth"
@@ -125,17 +125,17 @@ enum imsg_type {
 	IMSG_MBOX_SELECT_VANISHED,
 	IMSG_MBOX_STORE_MODIFIED,
 
-	/* RFC 9051 SS6.3.13 (IDLE); the VIEW ones go with any command */
+	/* RFC 9051 section 6.3.13 (IDLE); the VIEW ones go with any command */
 	IMSG_MBOX_IDLE_REFRESH,
 	IMSG_MBOX_VIEW_EXPUNGE,
 	IMSG_MBOX_VIEW_FETCH,
 	IMSG_MBOX_VIEW_EXISTS,
 	IMSG_MBOX_IDLE_REFRESHED,
 
-	/* RFC 9051 SS6.3.9 (LIST) */
+	/* RFC 9051 section 6.3.9 (LIST) */
 	IMSG_MBOX_LIST_ITEM,
 
-	/* RFC 9051 SS6.3.7/SS6.3.8 (SUBSCRIBE/UNSUBSCRIBE) */
+	/* RFC 9051 section 6.3.7/section 6.3.8 (SUBSCRIBE/UNSUBSCRIBE) */
 	IMSG_MBOX_SUBSCRIBE,
 	IMSG_MBOX_UNSUBSCRIBE,
 	IMSG_MBOX_SAVED_CLEAR
@@ -258,7 +258,7 @@ struct imsg_store_init {
 
 #define MBOX_NAME_MAX	256
 
-/* RFC 5530 SS3 response codes, and RFC 9051 NOTSAVED */
+/* RFC 5530 section 3 response codes, and RFC 9051 NOTSAVED */
 enum mbox_op_error {
 	MBOX_ERR_UNSET = 0,
 	MBOX_OP_OK,
@@ -272,7 +272,7 @@ enum mbox_op_error {
 	MBOX_OP_ERR_OVERQUOTA,
 };
 
-/* QRESYNC select-param (RFC 7162 SS3.2.5). */
+/* QRESYNC select-param (RFC 7162 section 3.2.5). */
 struct imsg_mbox_select {
 	char		mailbox[MBOX_NAME_MAX];
 	int		readonly;	/* 1 = EXAMINE, 0 = SELECT */
@@ -291,10 +291,10 @@ struct imsg_mbox_selected {
 	uint32_t	uidnext;
 	uint64_t	highestmodseq;
 
-	/* preceded by VANISHED, then FETCH_META (RFC 7162 SS3.2.6) */
+	/* preceded by VANISHED, then FETCH_META (RFC 7162 section 3.2.6) */
 };
 
-/* RFC 9051 SS6.3.11, plus RFC 7162 SS3.1.7 HIGHESTMODSEQ */
+/* RFC 9051 section 6.3.11, plus RFC 7162 section 3.1.7 HIGHESTMODSEQ */
 #define STATUS_ATT_MESSAGES		(1U << 0)
 #define STATUS_ATT_UIDNEXT		(1U << 1)
 #define STATUS_ATT_UIDVALIDITY		(1U << 2)
@@ -304,7 +304,7 @@ struct imsg_mbox_selected {
 #define STATUS_ATT_HIGHESTMODSEQ	(1U << 6)
 #define STATUS_ATT_RECENT		(1U << 7)	/* answered "0" */
 
-/* RFC 9051 SS6.3.11 STATUS; the selection is left alone */
+/* RFC 9051 section 6.3.11 STATUS; the selection is left alone */
 struct imsg_mbox_status {
 	char		mailbox[MBOX_NAME_MAX];
 	uint32_t	attrs;
@@ -321,7 +321,7 @@ struct imsg_mbox_status_result {
 	uint64_t	size;
 };
 
-/* one gap [uid_lo, uid_hi]; RFC 7162 SS5.1 minimal state */
+/* one gap [uid_lo, uid_hi]; RFC 7162 section 5.1 minimal state */
 struct imsg_mbox_select_vanished {
 	uint32_t	uid_lo;
 	uint32_t	uid_hi;
@@ -333,7 +333,7 @@ struct imsg_mbox_select_vanished {
 #define MBOX_FETCH_UID			(1U << 1)
 #define MBOX_FETCH_INTERNALDATE	(1U << 2)
 #define MBOX_FETCH_RFC822_SIZE		(1U << 3)
-#define MBOX_FETCH_MODSEQ		(1U << 4) /* RFC 7162 SS3.1.4.2 */
+#define MBOX_FETCH_MODSEQ		(1U << 4) /* RFC 7162 section 3.1.4.2 */
 #define MBOX_FETCH_BODY_HEADER		(1U << 5)
 #define MBOX_FETCH_BODY_WHOLE		(1U << 6)
 #define MBOX_FETCH_BODY_TEXT		(1U << 7)
@@ -384,11 +384,11 @@ struct imsg_mbox_select_vanished {
 #define IDLE_POLL_DEFAULT	5
 #define IDLE_POLL_MAX		300	/* seconds; 0 disables polling */
 
-/* RFC 9051 SS5.4 permits a short pre-authentication timer */
+/* RFC 9051 section 5.4 permits a short pre-authentication timer */
 #define LOGIN_GRACE_DEFAULT	60
 #define LOGIN_GRACE_MAX		3600	/* seconds; 0 disables the timer */
 
-/* lock wait, then NO [INUSE] (RFC 9051 SS7.1) */
+/* lock wait, then NO [INUSE] (RFC 9051 section 7.1) */
 #define LOCK_TIMEOUT_DEFAULT	120
 #define LOCK_TIMEOUT_MAX	3600	/* seconds; 0 disables the bound */
 
@@ -416,7 +416,7 @@ struct imsg_parser_req {
 	uint32_t	poollen;
 };
 
-/* RFC 9051 SS6.4.4 content key; strings are offsets into the pool */
+/* RFC 9051 section 6.4.4 content key; strings are offsets into the pool */
 struct imsg_parser_leaf {
 	int32_t		op;
 	uint32_t	str_off;
@@ -434,7 +434,7 @@ struct imsg_parser_rep {
 	uint32_t	retiring;
 };
 
-/* RFC 9051 SS9 sequence-set range; the store resolves "*" */
+/* RFC 9051 section 9 sequence-set range; the store resolves "*" */
 struct seq_range {
 	uint32_t	lo;
 	uint32_t	hi;
@@ -442,7 +442,7 @@ struct seq_range {
 	int		hi_is_star;
 };
 
-/* RFC 9051 SS9 seq-last-command, "$": the all-zero range */
+/* RFC 9051 section 9 seq-last-command, "$": the all-zero range */
 #define SEQ_RANGE_SAVED(r)	((r)->lo == 0 && !(r)->lo_is_star)
 
 /* keeps the trailing array under MAX_IMSGSIZE */
@@ -452,11 +452,11 @@ struct imsg_mbox_fetch {
 	uint32_t	nranges;
 	uint32_t	attrs;
 
-	/* RFC 7162 SS3.1.4.1 CHANGEDSINCE */
+	/* RFC 7162 section 3.1.4.1 CHANGEDSINCE */
 	int		has_changedsince;
 	uint64_t	changedsince;
 
-	/* RFC 9051 SS6.4.9 UID FETCH; RFC 7162 SS3.2.6 VANISHED */
+	/* RFC 9051 section 6.4.9 UID FETCH; RFC 7162 section 3.2.6 VANISHED */
 	int		by_uid;
 	int		want_vanished;
 
@@ -517,7 +517,7 @@ struct imsg_mbox_result {
 	uint32_t	count;
 	uint64_t	highestmodseq;
 
-	/* RFC 9051 SS7.1 COPYUID; COPY and MOVE only */
+	/* RFC 9051 section 7.1 COPYUID; COPY and MOVE only */
 	uint32_t	uidvalidity;
 };
 
@@ -539,20 +539,20 @@ struct imsg_mbox_store {
 	uint32_t	sysflags;
 	char		keywords[MBOX_FLAGS_MAX];
 
-	/* RFC 7162 SS3.1.3 UNCHANGEDSINCE */
+	/* RFC 7162 section 3.1.3 UNCHANGEDSINCE */
 	int		has_unchangedsince;
 	uint64_t	unchangedsince;
 
 	int		by_uid;
 };
 
-/* RFC 7162 SS3.1.3 MODIFIED */
+/* RFC 7162 section 3.1.3 MODIFIED */
 struct imsg_mbox_store_modified {
 	uint32_t	seqno;
 	uint32_t	uid;
 };
 
-/* RFC 9051 SS6.4.3; lowest-numbered first (SS7.5.1) */
+/* RFC 9051 section 6.4.3; lowest-numbered first (section 7.5.1) */
 struct imsg_mbox_expunge {
 	int		silent;	/* 1 for CLOSE, 0 for a real EXPUNGE command */
 
@@ -562,18 +562,18 @@ struct imsg_mbox_expunge {
 };
 
 struct imsg_mbox_expunged {
-	uint32_t	seqno;	/* after earlier EXPUNGEs (RFC 9051 SS7.5.1) */
-	uint32_t	uid;	/* for VANISHED (RFC 7162 SS3.2.10.2) */
+	uint32_t	seqno;	/* RFC 9051 section 7.5.1: after EXPUNGEs */
+	uint32_t	uid;	/* for VANISHED (RFC 7162 section 3.2.10.2) */
 };
 
-/* RFC 9051 SS6.4.7 COPY and SS6.4.8 MOVE */
+/* RFC 9051 section 6.4.7 COPY and section 6.4.8 MOVE */
 struct imsg_mbox_copy {
 	int		by_uid;
 	uint32_t	nranges;
 	char		destname[MBOX_NAME_MAX];
 };
 
-/* COPYUID pairs, sent before EXPUNGE for MOVE (RFC 9051 SS6.4.8) */
+/* COPYUID pairs, sent before EXPUNGE for MOVE (RFC 9051 section 6.4.8) */
 struct imsg_mbox_copy_mapping {
 	uint32_t	src_uid;
 	uint32_t	dest_uid;
@@ -595,10 +595,10 @@ struct imsg_mbox_appended {
 	uint32_t	uid;		/* APPENDUID, with uidvalidity */
 };
 
-/* RFC 9051 SS6.4.4 search-program, as a postfix array of nodes */
+/* RFC 9051 section 6.4.4 search-program, as a postfix array of nodes */
 #define SEARCH_PROGRAM_MAX_NODES	100
 #define SEARCH_KEYWORD_MAX		64	/* one keyword, not a list */
-/* operand pool; RFC 9051 SS4.3's non-synchronizing literal cap */
+/* operand pool; RFC 9051 section 4.3's non-synchronizing literal cap */
 #define SEARCH_OPERANDS_MAX		4096
 
 #define SEARCH_OP_ALL		0
@@ -624,8 +624,8 @@ struct imsg_mbox_appended {
 #define SEARCH_OP_AND		20
 #define SEARCH_OP_OR		21
 #define SEARCH_OP_NOT		22
-#define SEARCH_OP_MODSEQ	23	/* RFC 7162 SS3.1.5 */
-/* RFC 9051 SS6.4.4 content keys, answered by the parser-worker */
+#define SEARCH_OP_MODSEQ	23	/* RFC 7162 section 3.1.5 */
+/* RFC 9051 section 6.4.4 content keys, answered by the parser-worker */
 #define SEARCH_OP_SUBJECT	24
 #define SEARCH_OP_HEADER	25
 #define SEARCH_OP_SENTBEFORE	26
@@ -653,7 +653,7 @@ struct search_node {
 	uint32_t	name_len;
 };
 
-/* RFC 9051 SS6.4.4 search-return-opt */
+/* RFC 9051 section 6.4.4 search-return-opt */
 #define SEARCH_RETURN_MIN	(1U << 0)
 #define SEARCH_RETURN_MAX	(1U << 1)
 #define SEARCH_RETURN_ALL	(1U << 2)
@@ -683,10 +683,10 @@ struct search_parse_result {
 struct imsg_mbox_search_match {
 	uint32_t	seqno;
 	uint32_t	uid;
-	uint64_t	modseq;		/* RFC 7162 SS3.1.6 */
+	uint64_t	modseq;		/* RFC 7162 section 3.1.6 */
 };
 
-/* RFC 9051 SS6.3.13 IDLE, SS6.1.2 NOOP; EXPUNGE is imsg_mbox_expunged */
+/* RFC 9051 sections 6.3.13 IDLE, 6.1.2 NOOP; EXPUNGE is imsg_mbox_expunged */
 struct imsg_mbox_idle_refreshed {
 	int		ok;
 	int		unchanged;	/* the probe saw no change */
@@ -697,7 +697,7 @@ struct imsg_mbox_view_exists {
 	uint32_t	exists;
 };
 
-/* RFC 9051 SS6.3.4/SS6.3.5; store.c re-validates the name */
+/* RFC 9051 section 6.3.4/section 6.3.5; store.c re-validates the name */
 struct imsg_mbox_create {
 	char		mailbox[MBOX_NAME_MAX];
 };
@@ -706,24 +706,24 @@ struct imsg_mbox_delete {
 	char		mailbox[MBOX_NAME_MAX];
 };
 
-/* RFC 9051 SS6.3.6 RENAME */
+/* RFC 9051 section 6.3.6 RENAME */
 struct imsg_mbox_rename {
 	char		oldname[MBOX_NAME_MAX];
 	char		newname[MBOX_NAME_MAX];
 };
 
-/* RFC 9051 SS6.3.9 LIST; unordered, INBOX excluded */
+/* RFC 9051 section 6.3.9 LIST; unordered, INBOX excluded */
 struct imsg_mbox_list {
 	int		subscribed_only;
 };
 
-/* exists 0: subscribed, no mailbox (RFC 9051 SS6.3.9.6) */
+/* exists 0: subscribed, no mailbox (RFC 9051 section 6.3.9.6) */
 struct imsg_mbox_list_item {
 	char		mailbox[MBOX_NAME_MAX];
 	int		exists;
 };
 
-/* RFC 9051 SS6.3.7 SUBSCRIBE and SS6.3.8 UNSUBSCRIBE */
+/* RFC 9051 section 6.3.7 SUBSCRIBE and section 6.3.8 UNSUBSCRIBE */
 struct imsg_mbox_subscribe {
 	char		mailbox[MBOX_NAME_MAX];
 };
@@ -756,6 +756,8 @@ void		 imsgev_add(struct imsgev *);
 
 /* a dispatch handler must call this before returning */
 void		 imsgev_rearm_read(struct imsgev *);
+int		 recv_fixed(struct imsg *, void *, size_t, const char *);
+int		 recv_header(struct imsg *, void *, size_t, const char *);
 
 int		 setup_recv_one_peer(struct imsgbuf *);
 int		 setup_recv_one_peer_id(struct imsgbuf *, uint32_t *);
blob - 4abc10cf7e54c09814b8651cf7a107c6d0ec0c43
blob + 2ef6ced46ea7c66ecc9add39ef9bc3cf9c0faff9
--- src/imsgev.c
+++ src/imsgev.c
@@ -7,23 +7,6 @@
  * Copyright (c) 2004 Esben Norby <norby@openbsd.org>
  * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
  *
- * This file's name and its "struct imsgev" wrapper-around-imsgbuf+
- * event(3) concept match Eric Faurot's imsgev.c in OpenBSD's ldapd
- * (usr.sbin/ldapd/imsgev.c), not a generic/obvious name, so his
- * copyright is carried forward here even though this file's actual
- * function signatures and dispatch design (a caller-supplied raw
- * libevent handler, vs. ldapd's callback+needfd model) were written
- * independently and differ from his implementation.
- *
- * imsgev_add() below is the exception: it is a verbatim copy (up to
- * whitespace and the choice of iev vs. iev->data as event_set()'s last
- * argument) of the imsg_event_add() idiom shared across OpenBSD privsep
- * daemons, traceable to usr.sbin/ospfd/ospfd.c:525-534 (Claudio Jeker
- * 2005, Esben Norby 2004, Henning Brauer 2003-2004) and copied with
- * only that one-argument variation into dvmrpd.c, npppd.c, and rad.c.
- * Their copyright is carried forward for that function specifically,
- * same rationale as log.c's and parse.y's shared-idiom copyright chains.
- *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
@@ -61,8 +44,6 @@ imsgev_on_compose(struct imsgbuf *ibuf, void *arg)
 {
 	struct imsgev	*iev = arg;
 
-	(void)ibuf;
-
 	if (event_pending(&iev->ev, EV_WRITE, NULL))
 		return;
 	imsgev_add(iev);
@@ -119,6 +100,27 @@ imsgev_add(struct imsgev *iev)
 }
 
 /* EV_READ lacks EV_PERSIST: every handler must re-arm it */
+/* a fixed-size message, or 0 after a log line */
+int
+recv_fixed(struct imsg *imsg, void *data, size_t len, const char *what)
+{
+	if (imsg_get_data(imsg, data, len) == -1) {
+		log_warnx("bad %s", what);
+		return (0);
+	}
+	return (1);
+}
+
+int
+recv_header(struct imsg *imsg, void *data, size_t len, const char *what)
+{
+	if (imsg_get_buf(imsg, data, len) == -1) {
+		log_warnx("bad %s (header)", what);
+		return (0);
+	}
+	return (1);
+}
+
 void
 imsgev_rearm_read(struct imsgev *iev)
 {
blob - 68126e28fffe04e0f26c21b5e2882ed2b8e61d41
blob + 14c1f133989ed4d5fd815ac332d079572515d5f9
--- src/index.c
+++ src/index.c
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
 #include <sys/types.h>
 #include <sys/file.h>
 #include <sys/stat.h>
@@ -37,8 +36,7 @@
 #include "log.h"
 #include "store_internal.h"
 
-/* no ':', CR or LF in any index field */
-/* RFC 7162 SS7 */
+/* RFC 7162 section 7 */
 #define INDEX_MODSEQ_MAX	INT64_MAX
 
 #define CRLF_BLOCK		65536
@@ -91,7 +89,9 @@ int
 index_load(int fd, struct mbox_index *idx)
 {
 	FILE	*fp;
-	char	 line[STORE_INDEX_LINE_MAX];
+	char	*line = NULL;
+	size_t	 linesize = 0;
+	ssize_t	 linelen;
 	int	 dupfd;
 	int	 first = 1;
 
@@ -111,26 +111,19 @@ index_load(int fd, struct mbox_index *idx)
 		return (-1);
 	}
 
-	while (fgets(line, sizeof(line), fp) != NULL) {
-		/* fgets(3) splits an over-long line */
-		if (strchr(line, '\n') == NULL &&
-		    strlen(line) == sizeof(line) - 1) {
-			int	c = fgetc(fp);
-
-			if (c != EOF && c != '\n') {
-				log_warnx("session %u: over-long line in %s, "
-				    "refusing to parse the index", session_id,
-				    STORE_INDEX_NAME);
-				goto fail;
-			}
+	while ((linelen = getline(&line, &linesize, fp)) != -1) {
+		if (linelen >= STORE_INDEX_LINE_MAX) {
+			log_warnx("session %u: %s: line too long, index not "
+			    "parsed", session_id, STORE_INDEX_NAME);
+			goto fail;
 		}
 		line[strcspn(line, "\n")] = '\0';
 		if (line[0] == '\0')
 			continue;
 
 		if (first) {
-			char		*colon, *colon2, *ep;
-			unsigned long	 parsed;
+			char		*colon, *colon2;
+			const char	*errstr;
 
 			first = 0;
 			if ((colon = strchr(line, ':')) == NULL) {
@@ -139,21 +132,18 @@ index_load(int fd, struct mbox_index *idx)
 				goto fail;
 			}
 			*colon = '\0';
-			/* strtoul(3) accepts leading space and a sign */
 			if (line[0] < '0' || line[0] > '9') {
 				log_warnx("session %u: malformed "
 				    "UIDVALIDITY: %s", session_id, line);
 				goto fail;
 			}
-			errno = 0;
-			parsed = strtoul(line, &ep, 10);
-			if (*ep != '\0' || errno != 0 ||
-			    parsed > UINT32_MAX) {
+			idx->uidvalidity = strtonum(line, 0, UINT32_MAX,
+			    &errstr);
+			if (errstr != NULL) {
 				log_warnx("session %u: malformed "
 				    "UIDVALIDITY: %s", session_id, line);
 				goto fail;
 			}
-			idx->uidvalidity = (uint32_t)parsed;
 
 			/* RFC 7162: optional HIGHESTMODSEQ; absent means 1 */
 			if ((colon2 = strchr(colon + 1, ':')) != NULL)
@@ -164,15 +154,13 @@ index_load(int fd, struct mbox_index *idx)
 				    session_id, colon + 1);
 				goto fail;
 			}
-			errno = 0;
-			parsed = strtoul(colon + 1, &ep, 10);
-			if (*ep != '\0' || errno != 0 ||
-			    parsed > UINT32_MAX) {
+			idx->uidnext = strtonum(colon + 1, 0, UINT32_MAX,
+			    &errstr);
+			if (errstr != NULL) {
 				log_warnx("session %u: malformed UIDNEXT: %s",
 				    session_id, colon + 1);
 				goto fail;
 			}
-			idx->uidnext = (uint32_t)parsed;
 
 			if (colon2 != NULL) {
 				if (colon2[1] < '0' || colon2[1] > '9') {
@@ -181,11 +169,9 @@ index_load(int fd, struct mbox_index *idx)
 					    colon2 + 1);
 					goto fail;
 				}
-				errno = 0;
-				idx->highestmodseq = strtoull(colon2 + 1, &ep,
-				    10);
-				if (*ep != '\0' || errno != 0 ||
-				    idx->highestmodseq > INDEX_MODSEQ_MAX) {
+				idx->highestmodseq = strtonum(colon2 + 1, 0,
+				    INDEX_MODSEQ_MAX, &errstr);
+				if (errstr != NULL) {
 					log_warnx("session %u: malformed "
 					    "HIGHESTMODSEQ: %s", session_id,
 					    colon2 + 1);
@@ -205,9 +191,11 @@ index_load(int fd, struct mbox_index *idx)
 		idx->nlines++;
 	}
 	if (ferror(fp)) {
-		log_warn("session %u: fgets %s", session_id, STORE_INDEX_NAME);
+		log_warn("session %u: getline %s", session_id,
+		    STORE_INDEX_NAME);
 		goto fail;
 	}
+	free(line);
 	fclose(fp);
 
 	if (first) {
@@ -220,6 +208,7 @@ index_load(int fd, struct mbox_index *idx)
 	return (0);
 
 fail:
+	free(line);
 	index_free(idx);
 	fclose(fp);
 	return (-1);
@@ -228,7 +217,7 @@ fail:
 int
 index_parse_line(const char *line, struct index_rec *rec)
 {
-	const char	*p, *q, *r;
+	const char	*p, *q, *r, *errstr;
 	char		*ep;
 	unsigned long	 parsed;
 
@@ -261,8 +250,8 @@ index_parse_line(const char *line, struct index_rec *r
 	memcpy(rec->basename, p, (size_t)(q - p));
 	rec->basename[q - p] = '\0';
 	if (!index_basename_valid(rec->basename)) {
-		log_warnx("session %u: refusing index line with unsafe "
-		    "basename (UID %u)", session_id, rec->uid);
+		log_warnx("session %u: index line uid %u: unsafe basename",
+		    session_id, rec->uid);
 		return (-1);
 	}
 
@@ -282,10 +271,8 @@ index_parse_line(const char *line, struct index_rec *r
 			    "in index line: %s", session_id, line);
 			return (-1);
 		}
-		errno = 0;
-		rec->modseq = strtoull(r + 1, &ep, 10);
-		if (*ep != '\0' || errno != 0 ||
-		    rec->modseq > INDEX_MODSEQ_MAX) {
+		rec->modseq = strtonum(r + 1, 0, INDEX_MODSEQ_MAX, &errstr);
+		if (errstr != NULL) {
 			log_warnx("session %u: malformed per-message MODSEQ "
 			    "in index line: %s", session_id, line);
 			return (-1);
@@ -370,7 +357,7 @@ seqset_max_hi(const struct seq_range *resolved, uint32
 	return (max);
 }
 
-/* RFC 9051 SS6.4.9: a UID command's set is in UID space */
+/* RFC 9051 section 6.4.9: a UID command's set is in UID space */
 enum seqset_pos
 seqset_position(const struct seq_range *resolved, uint32_t nresolved,
     uint32_t max_hi, int by_uid, uint32_t uid, uint32_t seqno)
@@ -384,7 +371,7 @@ seqset_position(const struct seq_range *resolved, uint
 	return (SEQSET_MATCH);
 }
 
-/* RFC 7162 SS3.2.6 */
+/* RFC 7162 section 3.2.6 */
 void
 send_vanished_range(const struct mbox_index *idx, uint32_t lo, uint32_t hi,
     struct imsgev *iev)
@@ -465,16 +452,16 @@ index_append(struct mbox_index *idx, uint32_t uid, con
 	char	line[STORE_INDEX_LINE_MAX];
 	int	len;
 
-	/* RFC 9051 SS9 forbids UID 0; refuse rather than wrap */
+	/* RFC 9051 section 9 forbids UID 0; refuse rather than wrap */
 	if (uid == 0) {
-		log_warnx("session %u: refusing index entry with UID 0 "
-		    "(uidnext exhausted or index header corrupt)", session_id);
+		log_warnx("session %u: index entry with UID 0, refused",
+		    session_id);
 		return (-1);
 	}
 
 	if (strpbrk(basename, ":\r\n") != NULL) {
-		log_warnx("session %u: refusing index entry with unsafe "
-		    "basename: %s", session_id, basename);
+		log_warnx("session %u: index entry %s: unsafe basename",
+		    session_id, basename);
 		return (-1);
 	}
 
@@ -576,7 +563,6 @@ fail:
 	return (-1);
 }
 
-
 void
 index_free(struct mbox_index *idx)
 {
@@ -588,7 +574,7 @@ index_free(struct mbox_index *idx)
 	memset(idx, 0, sizeof(*idx));
 }
 
-/* RFC 7162 SS3.2.5.1 QRESYNC resync; "$" is the one before SELECT */
+/* RFC 7162 section 3.2.5.1 QRESYNC resync; "$" is the one before SELECT */
 void
 qresync_send_resync(const struct imsg_mbox_select *req,
     const struct seq_range *ranges, uint32_t nranges,
@@ -608,7 +594,7 @@ qresync_send_resync(const struct imsg_mbox_select *req
 			nresolved += ss->nsaved;
 		}
 	} else {
-		/* RFC 7162 SS3.2.5.1: no known-uids means 1:uidnext-1 */
+		/* RFC 7162 section 3.2.5.1: no known-uids means 1:uidnext-1 */
 		if (idx->uidnext <= 1)
 			return;
 		resolved[0].lo = 1;
@@ -706,16 +692,14 @@ index_lock_release(struct index_lock *il)
 	}
 }
 
-/* RFC 9051 SS2.3.1.1: it must increase, so time is only a floor */
+/* RFC 9051 section 2.3.1.1: it must increase, so time is only a floor */
 uint32_t
 uidvalidity_next(void)
 {
-	const char	*path;
+	const char	*path, *errstr;
 	char		 buf[32];
 	struct stat	 st;
 	time_t		 now;
-	char		*ep;
-	unsigned long	 parsed;
 	uint32_t	 floor = 0, val;
 	ssize_t		 n;
 	int		 fd, writeback = 1;
@@ -727,13 +711,13 @@ uidvalidity_next(void)
 
 	if ((fd = openat(maildir_root_fd, path, O_RDWR | O_CREAT,
 	    0600)) == -1) {
-		log_warn("session %u: open %s (UIDVALIDITY floor); falling "
-		    "back to a bare timestamp", session_id, path);
+		log_warn("session %u: open %s, using a bare timestamp",
+		    session_id, path);
 		return (val != 0 ? val : 1);
 	}
 	if (flock(fd, LOCK_EX) == -1) {
-		log_warn("session %u: flock %s (UIDVALIDITY floor); falling "
-		    "back to a bare timestamp", session_id, path);
+		log_warn("session %u: flock %s, using a bare timestamp",
+		    session_id, path);
 		close(fd);
 		return (val != 0 ? val : 1);
 	}
@@ -746,17 +730,16 @@ uidvalidity_next(void)
 		} else {
 			buf[n] = '\0';
 			buf[strcspn(buf, "\r\n")] = '\0';
-			errno = 0;
-			parsed = strtoul(buf, &ep, 10);
-			if (buf[0] < '0' || buf[0] > '9' || *ep != '\0' ||
-			    errno != 0 || parsed > UINT32_MAX) {
-				log_warnx("session %u: %s holds no usable "
-				    "UIDVALIDITY floor (%s); leaving it alone "
-				    "and falling back to a bare timestamp",
-				    session_id, path, buf);
+			errstr = "not a decimal number";
+			if (buf[0] >= '0' && buf[0] <= '9')
+				floor = strtonum(buf, 0, UINT32_MAX, &errstr);
+			if (errstr != NULL) {
+				log_warnx("session %u: %s: no usable "
+				    "UIDVALIDITY floor (%s), using a bare "
+				    "timestamp", session_id, path, buf);
 				writeback = 0;
-			} else
-				floor = (uint32_t)parsed;
+				floor = 0;
+			}
 		}
 	}
 
@@ -780,22 +763,20 @@ uidvalidity_next(void)
 		    lseek(fd, 0, SEEK_SET) == -1 ||
 		    ftruncate(fd, 0) == -1 ||
 		    write(fd, buf, (size_t)n) != n)
-			log_warn("session %u: could not record the "
-			    "UIDVALIDITY floor in %s; the value issued now "
-			    "may be issued again", session_id, path);
+			log_warn("session %u: %s: UIDVALIDITY floor not "
+			    "recorded", session_id, path);
 	}
 
-	log_debug("session %u: UIDVALIDITY: floor %u in %s -> issued %u%s",
-	    session_id, floor, path, val,
-	    writeback ? "" : " (floor NOT updated)");
+	log_debug("session %u: UIDVALIDITY floor %u in %s, issued %u%s",
+	    session_id, floor, path, val, writeback ? "" : " (floor kept)");
 
 	close(fd);			/* releases the flock(2) */
 	return (val);
 }
 
-/* RFC 5322 SS2.3: CR and LF only as CRLF; mail.maildir(8) writes LF alone */
+/* RFC 5322 section 2.3 wants CRLF only; mail.maildir(8) writes LF alone */
 static int
-new_to_crlf(int dfd, const char *name)
+to_crlf(int dfd, const char *dir, const char *name)
 {
 	struct stat	 st;
 	char		 src[PATH_MAX], tmp[PATH_MAX];
@@ -805,11 +786,12 @@ new_to_crlf(int dfd, const char *name)
 	int		 ifd = -1, ofd = -1, made = 0, cr = 0, changed = 0;
 	int		 rc = -1;
 
-	if (snprintf(src, sizeof(src), "new/%s", name) >= (int)sizeof(src) ||
+	if (snprintf(src, sizeof(src), "%s/%s", dir, name) >=
+	    (int)sizeof(src) ||
 	    snprintf(tmp, sizeof(tmp), "tmp/%s.crlf", name) >=
 	    (int)sizeof(tmp)) {
-		log_warnx("session %u: new/%s: name too long for CRLF, "
-		    "indexed as it is", session_id, name);
+		log_warnx("session %u: %s/%s: name too long for CRLF, "
+		    "indexed as it is", session_id, dir, name);
 		return (-1);
 	}
 	ifd = openat(dfd, src, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
@@ -881,22 +863,37 @@ done:
 	return (rc);
 }
 
+static void
+new_to_cur(int dfd, const char *name)
+{
+	struct stat	st;
+	char		from[PATH_MAX], to[PATH_MAX];
+
+	if (snprintf(from, sizeof(from), "new/%s", name) >=
+	    (int)sizeof(from) ||
+	    snprintf(to, sizeof(to), "cur/%s", name) >= (int)sizeof(to)) {
+		log_warnx("session %u: new/%s: name too long to move to cur/",
+		    session_id, name);
+		return;
+	}
+	if (fstatat(dfd, to, &st, AT_SYMLINK_NOFOLLOW) == 0) {
+		log_warnx("session %u: %s: not moved, %s already exists",
+		    session_id, from, to);
+		return;
+	}
+	if (errno != ENOENT || renameat(dfd, from, dfd, to) == -1)
+		log_warn("session %u: moving %s to cur/", session_id, from);
+}
+
 static int
 index_scan_new(int dfd, struct mbox_index *idx, int mutate)
 {
 	DIR		*dp;
 	struct dirent	*de;
+	const char	*info;
 	int		 added = 0, converted = 0;
 
-	{
-		int	newfd;
-
-		dp = NULL;
-		newfd = openat(dfd, "new", O_RDONLY | O_DIRECTORY);
-		if (newfd != -1 && (dp = fdopendir(newfd)) == NULL)
-			close(newfd);
-	}
-	if (dp == NULL) {
+	if ((dp = opendirat(dfd, "new")) == NULL) {
 		if (errno == ENOENT)
 			/* no new/ yet on a never-used mailbox, not an error */
 			return (0);
@@ -909,12 +906,22 @@ index_scan_new(int dfd, struct mbox_index *idx, int mu
 		if (de->d_name[0] == '.')
 			/* ".", "..", and dotfiles */
 			continue;
+		/* maildir info ":2,": the cur/ scan indexes it there */
+		if (strpbrk(de->d_name, "\r\n") == NULL &&
+		    (info = strchr(de->d_name, ':')) != NULL &&
+		    strncmp(info, ":2,", 3) == 0) {
+			if (!mutate) {
+				closedir(dp);
+				return (1);
+			}
+			new_to_cur(dfd, de->d_name);
+			continue;
+		}
 		/* ':' or a newline would corrupt the index line */
 		if (strpbrk(de->d_name, ":\r\n") != NULL) {
 			if (mutate)
-				log_warnx("session %u: skipping new/ file "
-				    "with unsafe name (contains ':' or "
-				    "newline): %s", session_id, de->d_name);
+				log_warnx("session %u: new/%s: unsafe name, "
+				    "skipped", session_id, de->d_name);
 			continue;
 		}
 		if (index_has_basename(idx, de->d_name))
@@ -923,8 +930,8 @@ index_scan_new(int dfd, struct mbox_index *idx, int mu
 			closedir(dp);
 			return (1);
 		}
-		/* RFC 9051 SS2.3.1.1: convert before the UID is given */
-		if (new_to_crlf(dfd, de->d_name) == 1)
+		/* RFC 9051 section 2.3.1.1: convert before the UID is given */
+		if (to_crlf(dfd, "new", de->d_name) == 1)
 			converted = 1;
 		if (index_append(idx, idx->uidnext, de->d_name) == -1) {
 			closedir(dp);
@@ -940,6 +947,195 @@ index_scan_new(int dfd, struct mbox_index *idx, int mu
 	return (added);
 }
 
+struct base_set {
+	const char	**name;
+	size_t		 *len;
+	size_t		  mask;
+};
+
+static size_t
+base_slot(const struct base_set *bs, const char *s, size_t len)
+{
+	uint32_t	h = 2166136261U;	/* FNV-1a */
+	size_t		i, k;
+
+	for (i = 0; i < len; i++)
+		h = (h ^ (unsigned char)s[i]) * 16777619U;
+	for (k = h & bs->mask; bs->name[k] != NULL; k = (k + 1) & bs->mask)
+		if (bs->len[k] == len && memcmp(bs->name[k], s, len) == 0)
+			break;
+	return (k);
+}
+
+static int
+name_cmp(const void *a, const void *b)
+{
+	return (strcmp(*(char * const *)a, *(char * const *)b));
+}
+
+static int
+index_scan_cur(int dfd, struct mbox_index *idx, int mutate)
+{
+	struct base_set	 bs = { NULL, NULL, 0 };
+	struct stat	 st;
+	DIR		*dp = NULL;
+	struct dirent	*de;
+	char		**found = NULL, **grown, *info, *name;
+	char		 base[NAME_MAX + 1], from[PATH_MAX], to[PATH_MAX];
+	const char	*p, *prev = NULL;
+	size_t		 nfound = 0, cap = 0, size = 1, i, k, len;
+	size_t		 prevlen = 0, added = 0;
+	int		 synced = 0, rv = -1;
+
+	while (size < 2 * idx->nlines + 1)
+		size <<= 1;
+	bs.mask = size - 1;
+	if ((bs.name = calloc(size, sizeof(*bs.name))) == NULL ||
+	    (bs.len = calloc(size, sizeof(*bs.len))) == NULL) {
+		log_warn("session %u: scanning cur", session_id);
+		goto done;
+	}
+	for (i = 0; i < idx->nlines; i++) {
+		if ((p = strchr(idx->lines[i], ':')) == NULL)
+			continue;
+		len = strcspn(++p, ":");
+		k = base_slot(&bs, p, len);
+		bs.name[k] = p;
+		bs.len[k] = len;
+	}
+
+	if ((dp = opendirat(dfd, "cur")) == NULL) {
+		if (errno == ENOENT)
+			rv = 0;
+		else
+			log_warn("session %u: opendir cur", session_id);
+		goto done;
+	}
+	while ((de = readdir(dp)) != NULL) {
+		if (de->d_name[0] == '.' ||
+		    strpbrk(de->d_name, "\r\n") != NULL)
+			continue;
+		len = strcspn(de->d_name, ":");
+		if (bs.name[base_slot(&bs, de->d_name, len)] != NULL)
+			continue;
+		info = de->d_name[len] == ':' ? de->d_name + len : NULL;
+		if (len >= sizeof(base) ||
+		    (info != NULL && strncmp(info, ":2,", 3) != 0)) {
+			if (mutate)
+				log_warnx("session %u: cur/%s: not indexed, "
+				    "not a maildir name", session_id,
+				    de->d_name);
+			continue;
+		}
+		/* RFC 9051 section 6.4.3: likely a failed EXPUNGE unlink */
+		if (info != NULL && !idx->fresh &&
+		    strchr(info + 3, 'T') != NULL) {
+			if (mutate)
+				log_warnx("session %u: cur/%s: not indexed, "
+				    "flagged deleted", session_id, de->d_name);
+			continue;
+		}
+		memcpy(base, de->d_name, len);
+		base[len] = '\0';
+		if (!index_basename_valid(base))
+			continue;
+		if (!mutate) {
+			rv = 1;
+			goto done;
+		}
+		if (nfound == cap) {
+			cap = cap != 0 ? cap * 2 : 64;
+			if ((grown = reallocarray(found, cap,
+			    sizeof(*found))) == NULL) {
+				log_warn("session %u: scanning cur",
+				    session_id);
+				goto done;
+			}
+			found = grown;
+		}
+		if ((found[nfound] = strdup(de->d_name)) == NULL) {
+			log_warn("session %u: scanning cur", session_id);
+			goto done;
+		}
+		nfound++;
+	}
+	if (nfound > 1)
+		qsort(found, nfound, sizeof(*found), name_cmp);
+
+	for (i = 0; i < nfound; i++) {
+		name = found[i];
+		len = strcspn(name, ":");
+		if (prev != NULL && len == prevlen &&
+		    memcmp(prev, name, len) == 0) {
+			log_warnx("session %u: cur/%s: not indexed, another "
+			    "file has its base name", session_id, name);
+			continue;
+		}
+		if (name[len] == '\0') {
+			if (snprintf(from, sizeof(from), "cur/%s", name) >=
+			    (int)sizeof(from) ||
+			    snprintf(to, sizeof(to), "cur/%s:2,", name) >=
+			    (int)sizeof(to) ||
+			    fstatat(dfd, to, &st, AT_SYMLINK_NOFOLLOW) == 0) {
+				log_warnx("session %u: cur/%s: not indexed, "
+				    "cannot add \":2,\"", session_id, name);
+				continue;
+			}
+			if (errno != ENOENT ||
+			    renameat(dfd, from, dfd, to) == -1) {
+				log_warn("session %u: cur/%s: not indexed",
+				    session_id, name);
+				continue;
+			}
+			synced = 1;
+			name = to + strlen("cur/");
+		}
+		prev = found[i];
+		prevlen = len;
+		/* RFC 9051 section 2.3.1.1: convert before the UID is given */
+		if (to_crlf(dfd, "cur", name) == 1)
+			synced = 1;
+		memcpy(base, name, len);
+		base[len] = '\0';
+		if (index_append(idx, idx->uidnext, base) == -1)
+			goto done;
+		idx->uidnext++;
+		added++;
+	}
+	if (synced && fsync(dirfd(dp)) == -1)
+		log_warn("session %u: fsync cur", session_id);
+	if (added > 0)
+		log_info("session %u: indexed %zu file(s) found in cur/",
+		    session_id, added);
+	rv = added > 0;
+
+done:
+	if (dp != NULL)
+		closedir(dp);
+	for (i = 0; i < nfound; i++)
+		free(found[i]);
+	free(found);
+	free(bs.name);
+	free(bs.len);
+	if (rv == -1 && mutate)
+		index_free(idx);
+	return (rv);
+}
+
+static int
+index_scan(int dfd, struct mbox_index *idx, int mutate)
+{
+	int	in_new, in_cur;
+
+	if ((in_new = index_scan_new(dfd, idx, mutate)) == -1)
+		return (-1);
+	if (in_new && !mutate)
+		return (1);
+	if ((in_cur = index_scan_cur(dfd, idx, mutate)) == -1)
+		return (-1);
+	return (in_new || in_cur);
+}
+
 int
 refresh_index(int dfd, struct mbox_index *idx, int fd)
 {
@@ -948,8 +1144,8 @@ refresh_index(int dfd, struct mbox_index *idx, int fd)
 	if (index_load(fd, idx) == -1)
 		return (-1);
 
-	if ((added = index_scan_new(dfd, idx, 1)) == -1)
-		return (-1);	/* index_scan_new() has already freed idx */
+	if ((added = index_scan(dfd, idx, 1)) == -1)
+		return (-1);	/* index_scan() has already freed idx */
 
 	/* a fresh UIDVALIDITY must still be written down */
 	if (!added && !idx->fresh)
@@ -998,7 +1194,7 @@ uid_find(const uint32_t *list, size_t n, uint32_t uid)
 	return ((lo < n && list[lo] == uid) ? lo : n);
 }
 
-/* RFC 9051 SS7.5.1: once told, never again, so drop it from the baseline */
+/* RFC 9051 section 7.5.1: told once, never again; drop it from the baseline */
 int
 send_expunged(struct store_session *ss, uint32_t seqno, uint32_t uid)
 {
@@ -1097,7 +1293,7 @@ saved_resolve(const struct store_session *ss, const st
 	return (0);
 }
 
-/* RFC 2180 SS4.1.2: does the set name a message expunged, not yet told? */
+/* RFC 2180 section 4.1.2: does the set name an expunge not yet told? */
 int
 view_names_ghost(const struct store_session *ss, const struct mbox_index *idx,
     const struct seq_range *uids, uint32_t nuids)
@@ -1122,7 +1318,7 @@ view_names_ghost(const struct store_session *ss, const
 	return (0);
 }
 
-/* RFC 9051 SS7.5.1: each EXPUNGE renumbers those above it */
+/* RFC 9051 section 7.5.1: each EXPUNGE renumbers those above it */
 static size_t
 view_merge(const uint32_t *old, size_t oldn, const uint32_t *cur,
     size_t curn, int expunge_ok, uint32_t *view, size_t *viewn,
@@ -1158,7 +1354,7 @@ view_merge(const uint32_t *old, size_t oldn, const uin
 	return (gone);
 }
 
-/* RFC 9051 SS6.3.13: flag changes, as FETCH with UID */
+/* RFC 9051 section 6.3.13: flag changes, as FETCH with UID */
 static size_t
 view_send_flag_fetches(const struct mbox_index *idx, const uint32_t *old,
     size_t oldn, uint64_t since, const uint32_t *view, size_t viewn,
@@ -1183,7 +1379,7 @@ view_send_flag_fetches(const struct mbox_index *idx, c
 		if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
 		    rec.basename, &size, suffix, sizeof(suffix)) == -1) {
 			log_warnx("session %u: message %s (uid %u) indexed "
-			    "but missing on disk, no IDLE flag push",
+			    "but missing on disk, not pushed",
 			    session_id, rec.basename, rec.uid);
 			continue;
 		}
@@ -1286,7 +1482,7 @@ idle_baseline_seed(struct store_session *ss, const str
 	base->valid = 1;
 }
 
-/* RFC 9051 SS5.2, SS7.5.1: what changed; EXPUNGE only if expunge_ok */
+/* RFC 9051 sections 5.2, 7.5.1: what changed; EXPUNGE only if expunge_ok */
 int
 view_sync(struct store_session *ss, const struct mbox_index *idx,
     int expunge_ok)
@@ -1316,7 +1512,7 @@ view_sync(struct store_session *ss, const struct mbox_
 	    &viewn, &ss->iev);
 	(void)view_send_flag_fetches(idx, base->uids, base->n, base->modseq,
 	    view, viewn, ss);
-	/* after EXPUNGEs too, as RFC 9051 SS6.3.13's example does */
+	/* after EXPUNGEs too, as RFC 9051 section 6.3.13's example does */
 	if (gone > 0 || viewn != base->n) {
 		memset(&ve, 0, sizeof(ve));
 		ve.exists = (uint32_t)viewn;
@@ -1379,7 +1575,7 @@ handle_mbox_idle_refresh(struct store_session *ss)
 	struct store_idle_baseline	*base = &ss->idle_baseline;
 	struct imsgev			*iev = &ss->iev;
 	struct mbox_index		 idx;
-	struct imsg_mbox_idle_refreshed reply;
+	struct imsg_mbox_idle_refreshed	 reply;
 	struct index_lock		 il = INDEX_LOCK_INIT;
 	int				 pending = 0, seeded, locked;
 
@@ -1406,7 +1602,7 @@ handle_mbox_idle_refresh(struct store_session *ss)
 		index_lock_release(&il);
 		goto send;
 	}
-	if ((pending = index_scan_new(ss->mailbox_dir_fd, &idx, 0)) == -1) {
+	if ((pending = index_scan(ss->mailbox_dir_fd, &idx, 0)) == -1) {
 		index_free(&idx);
 		index_lock_release(&il);
 		goto send;
@@ -1441,7 +1637,7 @@ handle_mbox_idle_refresh(struct store_session *ss)
 	goto send;
 
 busy:
-	/* RFC 9051 SS7.5.1: a seed cannot skip, or EXPUNGEs repeat */
+	/* RFC 9051 section 7.5.1: a seed cannot skip, or EXPUNGEs repeat */
 	reply.busy = 1;
 	if (seeded && idle_seed_unlocked(ss) == 0) {
 		reply.ok = 1;
blob - d1f56a65c1e211d40c9520058aff64700ffd3ce8
blob + ccbf8986e244971ab893be8b94ac99c6eaa7fcc9
--- src/keymgr.c
+++ src/keymgr.c
@@ -7,28 +7,6 @@
  * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
  * Copyright (c) 2008 Reyk Floeter <reyk@openbsd.org>
  *
- * This file's overall architecture -- a dedicated process holding the
- * real TLS private key, a "fake private key" installed in the
- * TLS-terminating process instead, and a process-wide OpenSSL
- * RSA_METHOD/EC_KEY_METHOD engine override that forwards every
- * private-key operation here as a synchronous imsg round-trip -- is
- * smtpd's ca.c (Reyk Floeter, Gilles Chehade), ported to imapd's own
- * imsg/privsep conventions rather than copied verbatim. The RSA_METHOD/
- * EC_KEY_METHOD engine-override code itself (the code this file's
- * request/reply pair answers) lives in listener.c, not here -- see that
- * file's header comment for the matching attribution.
- *
- * keymgr_pubkey_hash() below additionally replicates, byte-for-byte,
- * smtpd's ssl.c hash_x509()/ssl_pubkey_hash() algorithm (Pierre-Yves
- * Ritschard, Reyk Floeter, Gilles Chehade): SHA256 of the certificate's
- * DER-encoded SubjectPublicKeyInfo, formatted "SHA256:" plus lowercase
- * hex. That exact format is what libtls's own (unexported)
- * tls_cert_pubkey_hash() tags onto the fake key's OpenSSL ex_data slot 0
- * at TLS-config time (lib/libtls/tls.c, confirmed by direct reading, not
- * assumed) -- this process's key lookup only works if it derives the
- * identical string from the same certificate, so the exact byte shape
- * of hash_x509() is load-bearing here, not just a convenient precedent.
- *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
@@ -168,15 +146,12 @@ keymgr_main(void)
 		imsg_free(&imsg);
 	}
 
-	/* a bad key is not fatal: its operations fail instead */
 	if (cert_len == 0 || key_len == 0)
-		log_warnx("no usable TLS cert/key at boot, TLS "
-		    "private-key operations will fail until the next "
-		    "SIGHUP reload supplies valid material");
+		log_warnx("no usable TLS cert/key, handshakes fail until a "
+		    "reload");
 	else if (keymgr_load(cert_buf, cert_len, key_buf, key_len) == -1)
-		log_warnx("failed to load TLS cert/key at boot "
-		    "(see above), TLS private-key operations will fail "
-		    "until the next SIGHUP reload supplies valid material");
+		log_warnx("TLS cert/key not loaded, handshakes fail until a "
+		    "reload");
 	explicit_bzero(key_buf, sizeof(key_buf));
 	keymgr_got_init = 1;
 
@@ -203,17 +178,15 @@ keymgr_main(void)
 	imsgev_init_from_ibuf(&iev_parent, &ibuf3, keymgr_dispatch_parent,
 	    NULL);
 
-#ifdef __OpenBSD__
 	/* recvfd only: peers arrive, nothing is sent */
 	if (pledge("stdio recvfd", NULL) == -1)
 		fatal("pledge");
-#endif
 
 	event_dispatch();
 	fatalx("exited event loop");
 }
 
-/* as smtpd's ssl.c hash_x509() */
+/* as smtpd's ssl.c hash_x509(); libtls tags the key with this exact string */
 static int
 keymgr_pubkey_hash(X509 *cert, char *hash, size_t hashlen)
 {
@@ -254,6 +227,7 @@ keymgr_load(const char *cert_buf, size_t cert_len, con
 	X509		*cert = NULL;
 	EVP_PKEY	*pkey = NULL;
 	char		 hash[KEYMGR_HASH_MAX];
+	int		 rc = -1;
 
 	if (cert_len == 0 || key_len == 0) {
 		log_warnx("empty cert or key, not (re)loading");
@@ -298,22 +272,14 @@ keymgr_load(const char *cert_buf, size_t cert_len, con
 	(void)strlcpy(keymgr_hash, hash, sizeof(keymgr_hash));
 
 	log_info("TLS key loaded (%s)", keymgr_hash);
+	rc = 0;
 
+fail:
 	BIO_free(cert_bio);
 	BIO_free(key_bio);
 	X509_free(cert);
-	return (0);
-
-fail:
-	if (cert_bio != NULL)
-		BIO_free(cert_bio);
-	if (key_bio != NULL)
-		BIO_free(key_bio);
-	if (cert != NULL)
-		X509_free(cert);
-	if (pkey != NULL)
-		EVP_PKEY_free(pkey);
-	return (-1);
+	EVP_PKEY_free(pkey);
+	return (rc);
 }
 
 static void
@@ -324,7 +290,7 @@ keymgr_try_reload(void)
 
 	if (keymgr_load(reload_cert_buf, reload_cert_len, reload_key_buf,
 	    reload_key_len) == -1)
-		log_warnx("SIGHUP reload: keeping previous key, see above");
+		log_warnx("SIGHUP: keeping the previous key");
 	explicit_bzero(reload_key_buf, sizeof(reload_key_buf));
 	reload_got_cert = reload_got_key = 0;
 }
@@ -364,11 +330,13 @@ keymgr_dispatch_parent(int fd, short event, void *arg)
 			keymgr_key_free();
 			exit(0);
 		case IMSG_SETUP_PEER: {
-			uint32_t		 sess_id = imsg_get_id(&imsg);
-			int			 peer_fd = imsg_get_fd(&imsg);
+			uint32_t		 sess_id;
+			int			 peer_fd;
 			struct keymgr_peer	*kp;
 			int			 flags;
 
+			sess_id = imsg_get_id(&imsg);
+			peer_fd = imsg_get_fd(&imsg);
 			if (peer_fd == -1) {
 				log_warnx("IMSG_SETUP_PEER carried no fd");
 				break;
@@ -394,8 +362,9 @@ keymgr_dispatch_parent(int fd, short event, void *arg)
 			break;
 		}
 		case IMSG_TLS_CERT: {
-			size_t	 len = imsg_get_len(&imsg);
+			size_t	 len;
 
+			len = imsg_get_len(&imsg);
 			if (len > sizeof(reload_cert_buf)) {
 				log_warnx("SIGHUP reload: TLS cert "
 				    "too large (%zu > %zu)", len,
@@ -413,8 +382,9 @@ keymgr_dispatch_parent(int fd, short event, void *arg)
 			break;
 		}
 		case IMSG_KEYMGR_INIT: {
-			size_t	 len = imsg_get_len(&imsg);
+			size_t	 len;
 
+			len = imsg_get_len(&imsg);
 			if (len > sizeof(reload_key_buf)) {
 				log_warnx("SIGHUP reload: TLS key "
 				    "too large (%zu > %zu)", len,
@@ -439,7 +409,6 @@ keymgr_dispatch_parent(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 static void
@@ -497,7 +466,6 @@ keymgr_dispatch_listener(int fd, short event, void *ar
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 static void
@@ -514,21 +482,21 @@ static int
 keymgr_recv_trailing(struct imsg *imsg, uint32_t len, unsigned char *buf,
     size_t bufsize)
 {
-	size_t	 bodylen = imsg_get_len(imsg);
+	size_t	 bodylen;
 
+	bodylen = imsg_get_len(imsg);
 	if (len > bufsize || bodylen != (size_t)len) {
-		log_warnx("trailing data length mismatch (header "
-		    "says %u, imsg has %zu bytes, buffer holds %zu)", len,
-		    bodylen, bufsize);
-		return (0);
+		log_warnx("trailing data length mismatch (header %u, imsg "
+		    "%zu, buffer %zu)", len, bodylen, bufsize);
+		return (-1);
 	}
 	if (bodylen == 0)
-		return (1);
+		return (0);
 	if (imsg_get_buf(imsg, buf, bodylen) == -1) {
 		log_warnx("bad trailing data");
-		return (0);
+		return (-1);
 	}
-	return (1);
+	return (0);
 }
 
 /* the reply reuses the request's type, correlated by id */
@@ -541,9 +509,8 @@ keymgr_reply(struct imsgev *iev, uint32_t type, uint32
 	    KEYMGR_DATA_MAX];
 
 	if (ok && tolen > KEYMGR_DATA_MAX) {
-		log_warnx("keymgr_reply: %zu-byte result exceeds "
-		    "KEYMGR_DATA_MAX (%d), refusing", tolen,
-		    KEYMGR_DATA_MAX);
+		log_warnx("keymgr_reply: %zu bytes over KEYMGR_DATA_MAX %d",
+		    tolen, KEYMGR_DATA_MAX);
 		ok = 0;
 	}
 
@@ -584,20 +551,18 @@ keymgr_handle_rsa(struct imsgev *iev, struct imsg *ims
 	/* imsg_get_buf() does not NUL-terminate */
 	req.hash[sizeof(req.hash) - 1] = '\0';
 
-	if (!keymgr_recv_trailing(imsg, req.fromlen, from, sizeof(from))) {
+	if (keymgr_recv_trailing(imsg, req.fromlen, from, sizeof(from)) == -1) {
 		keymgr_reply(iev, type, id, 0, NULL, 0);
 		return;
 	}
 
 	if (!keymgr_got_init) {
-		log_warnx("%s request before IMSG_KEYMGR_INIT "
-		    "completed, refusing", opname);
+		log_warnx("%s before IMSG_KEYMGR_INIT", opname);
 		keymgr_reply(iev, type, id, 0, NULL, 0);
 		return;
 	}
 	if (keymgr_pkey == NULL || strcmp(req.hash, keymgr_hash) != 0) {
-		log_warnx("%s request for unrecognized key hash, "
-		    "refusing", opname);
+		log_warnx("%s: unknown key hash", opname);
 		keymgr_reply(iev, type, id, 0, NULL, 0);
 		return;
 	}
@@ -634,7 +599,6 @@ keymgr_handle_rsa(struct imsgev *iev, struct imsg *ims
 	explicit_bzero(to, sizeof(to));
 }
 
-/* as smtpd's ca.c ca_imsg() */
 static void
 keymgr_handle_ecdsa(struct imsgev *iev, struct imsg *imsg, uint32_t id)
 {
@@ -652,20 +616,18 @@ keymgr_handle_ecdsa(struct imsgev *iev, struct imsg *i
 	}
 	req.hash[sizeof(req.hash) - 1] = '\0';
 
-	if (!keymgr_recv_trailing(imsg, req.fromlen, dgst, sizeof(dgst))) {
+	if (keymgr_recv_trailing(imsg, req.fromlen, dgst, sizeof(dgst)) == -1) {
 		keymgr_reply(iev, IMSG_KEYMGR_ECDSA_SIGN, id, 0, NULL, 0);
 		return;
 	}
 
 	if (!keymgr_got_init) {
-		log_warnx("ECDSA_SIGN request before "
-		    "IMSG_KEYMGR_INIT completed, refusing");
+		log_warnx("ECDSA_SIGN before IMSG_KEYMGR_INIT");
 		keymgr_reply(iev, IMSG_KEYMGR_ECDSA_SIGN, id, 0, NULL, 0);
 		return;
 	}
 	if (keymgr_pkey == NULL || strcmp(req.hash, keymgr_hash) != 0) {
-		log_warnx("ECDSA_SIGN request for unrecognized key "
-		    "hash, refusing");
+		log_warnx("ECDSA_SIGN: unknown key hash");
 		keymgr_reply(iev, IMSG_KEYMGR_ECDSA_SIGN, id, 0, NULL, 0);
 		return;
 	}
blob - 72cd56b418ec77a0a000346f1b2af0a4aa3ff452
blob + 5a6b4b20eaf510e140a2221587e287c2228b1633
--- src/listener.c
+++ src/listener.c
@@ -5,19 +5,6 @@
  * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
  * Copyright (c) 2012 Gilles Chehade <gilles@poolp.org>
  *
- * The RSA_METHOD/EC_KEY_METHOD engine override below (keymgr_engine_
- * init() and everything it installs) adapts smtpd's ca.c
- * (rsa_engine_init()/ecdsa_engine_init()/rsae_priv_enc()/rsae_priv_
- * dec()/ecdsae_do_sign(), ca.c:289-558) to imapd's own imsg
- * conventions: the OpenSSL API shape leaves little room for
- * independent structure, and this project's own licensing
- * precedent (log.c, imsgev.c) already treats a borrow this close as
- * needing the original author's copyright even where the
- * implementation differs. keymgr_use_fake_private_key()'s two-line call
- * shape is lifted from smtpd's smtp.c:187-193 (Gilles Chehade,
- * Pierre-Yves Ritschard, Jacek Masiulaniec); see that function's own
- * comment.
- *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
@@ -31,10 +18,10 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
+#include <sys/param.h>
 #include <sys/types.h>
-#include <sys/queue.h>
 #include <sys/socket.h>
+#include <sys/uio.h>
 
 #include <netinet/in.h>
 
@@ -45,6 +32,7 @@
 #include <fcntl.h>
 #include <grp.h>
 #include <imsg.h>
+#include <limits.h>
 #include <netdb.h>
 #include <poll.h>
 #include <pwd.h>
@@ -67,7 +55,7 @@
 #include "log.h"
 #include "listener.h"
 
-struct session_list	 sessions = TAILQ_HEAD_INITIALIZER(sessions);
+static struct session	 sess;	/* one process, one session */
 
 struct imsgev	 iev_auth;
 struct imsgev	 iev_parent;
@@ -108,7 +96,7 @@ struct imap_cmd_entry {
 	(ST_NOTAUTH | (1U << SESSION_AUTHENTICATING) | \
 	 (1U << SESSION_STORE_PENDING))
 
-/* RFC 9051 SS9; excludes transient states, which have their own pending_tag. */
+/* RFC 9051 section 9; the transient states have their own pending_tag */
 #define ST_AUTH \
 	((1U << SESSION_AUTHENTICATED) | (1U << SESSION_SELECTED))
 #define ST_SELECTED	(1U << SESSION_SELECTED)
@@ -122,7 +110,7 @@ static const struct imap_cmd_entry imap_cmds[] = {
 	{ "STARTTLS",	ST_NOTAUTH,	cmd_starttls },
 	{ "AUTHENTICATE", ST_NOTAUTH,	cmd_authenticate },
 
-	/* command-auth, RFC 9051 SS6.3 order */
+	/* command-auth, RFC 9051 section 6.3 order */
 	{ "ENABLE",	ST_AUTH,	cmd_enable },
 	{ "SELECT",	ST_AUTH,	cmd_select },
 	{ "EXAMINE",	ST_AUTH,	cmd_examine },
@@ -138,7 +126,7 @@ static const struct imap_cmd_entry imap_cmds[] = {
 	{ "APPEND",	ST_AUTH,	cmd_append },
 	{ "IDLE",	ST_AUTH,	cmd_idle },
 
-	/* command-select, RFC 9051 SS6.4 order */
+	/* command-select, RFC 9051 section 6.4 order */
 	{ "CLOSE",	ST_SELECTED,	cmd_close },
 	{ "UNSELECT",	ST_SELECTED,	cmd_unselect },
 	{ "EXPUNGE",	ST_SELECTED,	cmd_expunge },
@@ -149,7 +137,6 @@ static const struct imap_cmd_entry imap_cmds[] = {
 	{ "MOVE",	ST_SELECTED,	cmd_move },
 	{ "UID",	ST_SELECTED,	cmd_uid },
 };
-#define NUM_IMAP_CMDS	(sizeof(imap_cmds) / sizeof(imap_cmds[0]))
 
 /* internal to libtls, declared as smtpd/smtp.c does */
 void	tls_config_use_fake_private_key(struct tls_config *);
@@ -159,8 +146,8 @@ keymgr_set_fake_keypair(struct tls_config *config, con
     size_t cert_len)
 {
 	tls_config_use_fake_private_key(config);
-	return tls_config_set_keypair_mem(config, (const uint8_t *)cert_buf,
-	    cert_len, NULL, 0);
+	return (tls_config_set_keypair_mem(config,
+	    (const uint8_t *)cert_buf, cert_len, NULL, 0));
 }
 
 /* private-key operations go to keymgr, after smtpd's ca.c */
@@ -348,29 +335,27 @@ keymgr_assert_fake_key(const char *hash, const BIGNUM 
 
 	/* check A: the placeholder key has no private part */
 	if (priv != NULL)
-		fatalx("%s: key object carries a private component -- "
-		    "libtls is no longer using a placeholder key, and this "
-		    "process is not separated from the TLS private key", op);
+		fatalx("%s: key object carries a private component", op);
 
 	/* check B: one keypair, so a missing tag is a regression */
 	if (hash == NULL)
-		fatalx("%s: no pubkey-hash tag on the key object -- libtls's "
-		    "ex_data slot 0 tagging has changed", op);
+		fatalx("%s: no pubkey-hash tag on the key object", op);
 
 	/* check C1: NUL-terminated within KEYMGR_HASH_MAX */
 	for (i = 0; i < KEYMGR_HASH_MAX; i++)
 		if (hash[i] == '\0')
 			return;
-	fatalx("%s: pubkey-hash tag is not a NUL-terminated string within "
-	    "%d bytes -- refusing to read it", op, KEYMGR_HASH_MAX);
+	fatalx("%s: pubkey-hash tag not NUL-terminated within %d bytes",
+	    op, KEYMGR_HASH_MAX);
 }
 
 static int
 keymgr_rsa_priv_enc(int flen, const unsigned char *from, unsigned char *to,
     RSA *rsa, int padding)
 {
-	const char	*hash = RSA_get_ex_data(rsa, 0);
+	const char	*hash;
 
+	hash = RSA_get_ex_data(rsa, 0);
 	keymgr_assert_fake_key(hash, RSA_get0_d(rsa), "RSA_PRIVENC");
 	return (keymgr_forward_rsa(IMSG_KEYMGR_RSA_PRIVENC, hash,
 	    from, flen, to, (size_t)RSA_size(rsa), padding));
@@ -380,8 +365,9 @@ static int
 keymgr_rsa_priv_dec(int flen, const unsigned char *from, unsigned char *to,
     RSA *rsa, int padding)
 {
-	const char	*hash = RSA_get_ex_data(rsa, 0);
+	const char	*hash;
 
+	hash = RSA_get_ex_data(rsa, 0);
 	keymgr_assert_fake_key(hash, RSA_get0_d(rsa), "RSA_PRIVDEC");
 	return (keymgr_forward_rsa(IMSG_KEYMGR_RSA_PRIVDEC, hash,
 	    from, flen, to, (size_t)RSA_size(rsa), padding));
@@ -391,11 +377,9 @@ static ECDSA_SIG *
 keymgr_ecdsa_do_sign(const unsigned char *dgst, int dgst_len,
     const BIGNUM *inv, const BIGNUM *rp, EC_KEY *eckey)
 {
-	const char	*hash = EC_KEY_get_ex_data(eckey, 0);
+	const char	*hash;
 
-	(void)inv;
-	(void)rp;
-
+	hash = EC_KEY_get_ex_data(eckey, 0);
 	keymgr_assert_fake_key(hash, EC_KEY_get0_private_key(eckey),
 	    "ECDSA_SIGN");
 	return (keymgr_forward_ecdsa(hash, dgst, dgst_len));
@@ -486,9 +470,11 @@ listener_main(void)
 		}
 		switch (imsg_get_type(&imsg)) {
 		case IMSG_SETUP_PEER: {
-			uint32_t	 id = imsg_get_id(&imsg);
-			int		 peer_fd = imsg_get_fd(&imsg);
+			uint32_t	 id;
+			int		 peer_fd;
 
+			id = imsg_get_id(&imsg);
+			peer_fd = imsg_get_fd(&imsg);
 			if (peer_fd == -1) {
 				log_warnx("listener: IMSG_SETUP_PEER "
 				    "carried no fd");
@@ -617,9 +603,8 @@ listener_main(void)
 		    NULL);
 	else {
 		iev_auth.ibuf.fd = -1;
-		log_warnx("session %u: no auth-worker was spawned for this "
-		    "connection, AUTHENTICATE/LOGIN will fail until a new "
-		    "connection gets one", sinit.session_id);
+		log_warnx("session %u: no auth-worker for this connection",
+		    sinit.session_id);
 	}
 
 	if (imsgbuf_init(&keymgr_ibuf, keymgr_peer_fd) == -1)
@@ -638,10 +623,8 @@ listener_main(void)
 	    sinit.implicit_tls, &sinit.remote_ss, sinit.remote_sslen);
 
 	/* no "inet": getnameinfo(3) only formats numeric bytes */
-#ifdef __OpenBSD__
 	if (pledge("stdio recvfd", NULL) == -1)
 		fatal("pledge");
-#endif
 
 	event_dispatch();
 	fatalx("listener: exited event loop");
@@ -652,9 +635,6 @@ session_login_grace_expired(int fd, short event, void 
 {
 	struct session	*s = arg;
 
-	(void)fd;
-	(void)event;
-
 	log_info("session %u: closing peer=%.200s, no authentication within "
 	    "%u seconds", s->id, s->remote_addr, listener_login_grace_secs);
 	session_teardown(s, "login-grace");
@@ -672,9 +652,7 @@ session_login_grace_init(struct session *s)
 	tv.tv_sec = (time_t)listener_login_grace_secs;
 	tv.tv_usec = 0;
 	if (evtimer_add(&s->grace_ev, &tv) == -1)
-		log_warnx("session %u: evtimer_add (login grace); this "
-		    "session will not be closed if it never authenticates",
-		    s->id);
+		log_warnx("session %u: evtimer_add (login grace)", s->id);
 }
 
 void
@@ -687,15 +665,10 @@ static void
 listener_start_session(uint32_t session_id, int client_fd, int implicit_tls,
     const struct sockaddr_storage *ss, socklen_t sslen)
 {
-	struct session	*s;
+	struct session	*s = &sess;
+	char		 hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
 
-	s = calloc(1, sizeof(*s));
-	if (s == NULL) {
-		log_warn("calloc");
-		close(client_fd);
-		exit(1);
-	}
-	s->pending_body_fd = -1;	/* calloc(3)'s 0 is a real descriptor */
+	s->pending_body_fd = -1;	/* 0 is a real descriptor */
 	session_idle_poll_init(s);	/* before anything can tear s down */
 	s->id = session_id;
 	s->client_fd = client_fd;
@@ -704,22 +677,16 @@ listener_start_session(uint32_t session_id, int client
 	session_login_grace_init(s);
 	if (clock_gettime(CLOCK_MONOTONIC, &s->connected_at) == -1)
 		log_warn("session %u: clock_gettime", s->id);
-	TAILQ_INSERT_TAIL(&sessions, s, entry);
 
-	{
-		char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
+	/* numeric: no resolver or network I/O */
+	if (getnameinfo((const struct sockaddr *)ss, sslen, hbuf, sizeof(hbuf),
+	    sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV) == 0)
+		snprintf(s->remote_addr, sizeof(s->remote_addr),
+		    ss->ss_family == AF_INET6 ? "[%s]:%s" : "%s:%s", hbuf,
+		    sbuf);
+	else
+		strlcpy(s->remote_addr, "?", sizeof(s->remote_addr));
 
-		/* numeric: no resolver or network I/O */
-		if (getnameinfo((const struct sockaddr *)ss, sslen, hbuf,
-		    sizeof(hbuf), sbuf, sizeof(sbuf),
-		    NI_NUMERICHOST | NI_NUMERICSERV) == 0)
-			snprintf(s->remote_addr, sizeof(s->remote_addr),
-			    ss->ss_family == AF_INET6 ? "[%s]:%s" : "%s:%s",
-			    hbuf, sbuf);
-		else
-			strlcpy(s->remote_addr, "?", sizeof(s->remote_addr));
-	}
-
 	/* %.200s bounds untrusted text, the way sshd's auth.c does */
 	/* tls= is how it was accepted; the close line has the final state */
 	log_info("session %u: connected peer=%.200s tls=%s", s->id,
@@ -781,9 +748,6 @@ session_tls_handshake(int fd, short event, void *arg)
 	struct session	*s = arg;
 	int		 ret;
 
-	(void)fd;
-	(void)event;
-
 	ret = tls_handshake(s->tls_ctx);
 	if (ret == 0) {
 		s->tls_active = 1;
@@ -817,7 +781,7 @@ session_tls_handshake(int fd, short event, void *arg)
 	session_teardown(s, "tls-error");
 }
 
-/* RFC 9051 SS7.1.1's example OK-response text, used verbatim. */
+/* RFC 9051 section 7.1.1's example OK-response text, used verbatim. */
 void
 session_send_greeting(struct session *s)
 {
@@ -829,16 +793,17 @@ session_send_greeting(struct session *s)
 static int	session_is_busy(const struct session *);
 static int	session_enqueue_cmd(struct session *, const char *);
 
-/* RFC 9051 SS4.3 hard cap on a non-synchronizing literal. */
+/* RFC 9051 section 4.3 hard cap on a non-synchronizing literal. */
 #define IMAP_NONSYNC_LITERAL_MAX	4096
 
-/* RFC 9051 SS4.3 "{n}" or "{n+}" at p; *endp is past its "}" */
+/* RFC 9051 section 4.3 "{n}" or "{n+}" at p; *endp is past its "}" */
 static int
 literal_count(const char *p, uint64_t *lenp, int *nonsyncp, const char **endp)
 {
-	char			 digits[24];
-	size_t			 dlen;
-	unsigned long long	 v;
+	char		 digits[24];
+	const char	*errstr;
+	size_t		 dlen;
+	long long	 v;
 
 	if (*p++ != '{')
 		return (0);
@@ -847,9 +812,8 @@ literal_count(const char *p, uint64_t *lenp, int *nons
 		return (0);
 	memcpy(digits, p, dlen);
 	digits[dlen] = '\0';
-	errno = 0;
-	v = strtoull(digits, NULL, 10);
-	if (errno == ERANGE)
+	v = strtonum(digits, 0, LLONG_MAX, &errstr);
+	if (errstr != NULL)
 		return (0);
 	p += dlen;
 	*nonsyncp = (*p == '+');
@@ -962,7 +926,7 @@ session_gather(struct session *s, uint64_t n)
 	return (1);
 }
 
-/* RFC 9051 SS4.3: run a command, re-parsed as each literal arrives */
+/* RFC 9051 section 4.3: run a command, re-parsed as each literal arrives */
 static int
 session_run_cmd(struct session *s, char *cmd)
 {
@@ -1001,7 +965,7 @@ session_queue(struct session *s, const char *cmd)
 	return (0);
 }
 
-/* RFC 9051 SS9: a tag may not contain "+" */
+/* RFC 9051 section 9: a tag may not contain "+" */
 static int
 tag_is_valid(const char *tag)
 {
@@ -1018,24 +982,16 @@ tag_is_valid(const char *tag)
 	return (1);
 }
 
-void
-session_dispatch_client(int fd, short event, void *arg)
+/* octets read into inbuf, or -1: retry later, or the session is gone */
+static ssize_t
+session_read_client(struct session *s, int fd, size_t *tls_wantp)
 {
-	struct session	*s = arg;
-	ssize_t		 n;
-	char		*crlf;
-	size_t		 tls_want = 0;
+	ssize_t	n;
 
-	(void)event;
-
-	if (s->write_failed) {
-		session_teardown(s, "io-error");
-		return;
-	}
-
+	*tls_wantp = 0;
 	if (s->tls_active) {
-		tls_want = sizeof(s->inbuf) - s->inbuflen;
-		n = tls_read(s->tls_ctx, s->inbuf + s->inbuflen, tls_want);
+		*tls_wantp = sizeof(s->inbuf) - s->inbuflen;
+		n = tls_read(s->tls_ctx, s->inbuf + s->inbuflen, *tls_wantp);
 		if (n == TLS_WANT_POLLIN || n == TLS_WANT_POLLOUT) {
 			/* tls_read() may want to write */
 			event_del(&s->client_ev);
@@ -1043,13 +999,13 @@ session_dispatch_client(int fd, short event, void *arg
 			    (n == TLS_WANT_POLLIN) ? EV_READ : EV_WRITE,
 			    session_dispatch_client, s);
 			event_add(&s->client_ev, NULL);
-			return;
+			return (-1);
 		}
 		if (n == -1) {
 			log_warnx("session %u: tls_read: %s", s->id,
 			    tls_error(s->tls_ctx));
 			session_teardown(s, "tls-error");
-			return;
+			return (-1);
 		}
 		session_arm_client_read(s);
 	} else {
@@ -1058,219 +1014,255 @@ session_dispatch_client(int fd, short event, void *arg
 		if (n == -1) {
 			if (errno == EINTR || errno == EAGAIN ||
 			    errno == EWOULDBLOCK)
-				return;
+				return (-1);
 			log_warn("session %u: read", s->id);
 			session_teardown(s, "io-error");
-			return;
+			return (-1);
 		}
 	}
-
 	if (n == 0) {
 		log_debug("session %u: client closed connection", s->id);
 		session_teardown(s, "client-closed");
-		return;
+		return (-1);
 	}
-	s->inbuflen += (size_t)n;
+	return (n);
+}
 
-	for (;;) {
-		uint64_t	nonsync_len;
-		size_t		consumed, linelen;
-		int		alive, resume;
+/* consumption steps: 1 consumed, 0 wants more input, -1 session gone */
+static int
+session_discard_literal(struct session *s)
+{
+	uint64_t	take;
 
-		if (s->literal_discard > 0) {
-			uint64_t	take;
+	take = (uint64_t)s->inbuflen < s->literal_discard ?
+	    (uint64_t)s->inbuflen : s->literal_discard;
+	if (take > 0) {
+		memmove(s->inbuf, s->inbuf + take, s->inbuflen - (size_t)take);
+		s->inbuflen -= (size_t)take;
+		s->literal_discard -= take;
+	}
+	if (s->literal_discard > 0)
+		return (0);
+	s->literal_skipline = 1;
+	return (1);
+}
 
-			take = (uint64_t)s->inbuflen < s->literal_discard ?
-			    (uint64_t)s->inbuflen : s->literal_discard;
-			if (take > 0) {
-				memmove(s->inbuf, s->inbuf + take,
-				    s->inbuflen - (size_t)take);
-				s->inbuflen -= (size_t)take;
-				s->literal_discard -= take;
-			}
-			if (s->literal_discard > 0)
-				break;
-			s->literal_skipline = 1;
-			continue;
-		}
+/* RFC 9051 section 4.3 literal; may contain a CRLF */
+static int
+session_stream_append(struct session *s)
+{
+	uint64_t	want, take;
 
-		/* RFC 9051 SS4.3 literal; may contain a CRLF */
-		if (s->literal_pending) {
-			uint64_t	want, take;
-
-			want = s->literal_remaining;
-			take = (uint64_t)s->inbuflen < want ?
-			    (uint64_t)s->inbuflen : want;
-
-			if (take > 0) {
-				if (imsg_compose(&s->store_iev->ibuf,
-				    IMSG_MBOX_APPEND_DATA, 0, 0, -1, s->inbuf,
-				    (size_t)take) == -1) {
-					log_warn("session %u: imsg_compose "
-					    "IMSG_MBOX_APPEND_DATA", s->id);
-					session_teardown(s, "io-error");
-					return;
-				}
-				s->literal_remaining -= take;
-				memmove(s->inbuf, s->inbuf + take,
-				    s->inbuflen - (size_t)take);
-				s->inbuflen -= (size_t)take;
-			}
-
-			if (s->literal_remaining > 0)
-				break;
-
-			if (s->inbuflen < 2)
-				break;
-			if (s->inbuf[0] != '\r' || s->inbuf[1] != '\n') {
-				/* no reliable resync point, give up */
-				log_warnx("session %u: expected CRLF after "
-				    "literal data, closing", s->id);
-				session_teardown(s, "protocol-error");
-				return;
-			}
-			memmove(s->inbuf, s->inbuf + 2, s->inbuflen - 2);
-			s->inbuflen -= 2;
-
-			s->literal_pending = 0;
-			alive = session_finish_append(s);
-			if (!alive)
-				return;
-			continue;
+	want = s->literal_remaining;
+	take = (uint64_t)s->inbuflen < want ? (uint64_t)s->inbuflen : want;
+	if (take > 0) {
+		if (imsg_compose(&s->store_iev->ibuf, IMSG_MBOX_APPEND_DATA,
+		    0, 0, -1, s->inbuf, (size_t)take) == -1) {
+			log_warn("session %u: imsg_compose "
+			    "IMSG_MBOX_APPEND_DATA", s->id);
+			session_teardown(s, "io-error");
+			return (-1);
 		}
+		s->literal_remaining -= take;
+		memmove(s->inbuf, s->inbuf + take, s->inbuflen - (size_t)take);
+		s->inbuflen -= (size_t)take;
+	}
+	if (s->literal_remaining > 0)
+		return (0);
 
-		if (s->cmd_gather && s->cmd_octets > 0) {
-			size_t	take;
+	if (s->inbuflen < 2)
+		return (0);
+	if (s->inbuf[0] != '\r' || s->inbuf[1] != '\n') {
+		/* no reliable resync point, give up */
+		log_warnx("session %u: expected CRLF after literal data, "
+		    "closing", s->id);
+		session_teardown(s, "protocol-error");
+		return (-1);
+	}
+	memmove(s->inbuf, s->inbuf + 2, s->inbuflen - 2);
+	s->inbuflen -= 2;
 
-			take = s->inbuflen < s->cmd_octets ?
-			    s->inbuflen : s->cmd_octets;
-			if (take == 0)
-				break;
-			if (memchr(s->inbuf, '\0', take) != NULL) {
-				/* RFC 9051 SS9: CHAR8 excludes NUL */
-				session_cmd_reply(s, "BAD", "NUL in a literal");
-				s->literal_discard = s->cmd_octets;
-				s->cmd_gather = 0;
-				s->cmd_queued = 0;
-				s->cmd_octets = 0;
-				continue;
-			}
-			memcpy(s->cmdbuf + s->cmdlen, s->inbuf, take);
-			s->cmdlen += take;
-			s->cmd_octets -= take;
-			memmove(s->inbuf, s->inbuf + take, s->inbuflen - take);
-			s->inbuflen -= take;
-			continue;
-		}
+	s->literal_pending = 0;
+	if (!session_finish_append(s))
+		return (-1);
+	return (1);
+}
 
-		crlf = memmem(s->inbuf, s->inbuflen, "\r\n", 2);
-		if (crlf == NULL)
-			break;
+static int
+session_gather_octets(struct session *s)
+{
+	size_t	take;
 
-		linelen = (size_t)(crlf - s->inbuf);
-		consumed = linelen + 2;
+	take = s->inbuflen < s->cmd_octets ? s->inbuflen : s->cmd_octets;
+	if (take == 0)
+		return (0);
+	if (memchr(s->inbuf, '\0', take) != NULL) {
+		/* RFC 9051 section 9: CHAR8 excludes NUL */
+		session_cmd_reply(s, "BAD", "NUL in a literal");
+		s->literal_discard = s->cmd_octets;
+		s->cmd_gather = 0;
+		s->cmd_queued = 0;
+		s->cmd_octets = 0;
+		return (1);
+	}
+	memcpy(s->cmdbuf + s->cmdlen, s->inbuf, take);
+	s->cmdlen += take;
+	s->cmd_octets -= take;
+	memmove(s->inbuf, s->inbuf + take, s->inbuflen - take);
+	s->inbuflen -= take;
+	return (1);
+}
 
-		/* RFC 9051 SS2.2/SS9: no CR, LF or NUL inside a line */
-		if (memchr(s->inbuf, '\r', linelen) != NULL ||
-		    memchr(s->inbuf, '\n', linelen) != NULL ||
-		    memchr(s->inbuf, '\0', linelen) != NULL) {
-			static const char bad[] = "* BAD bare CR, LF or NUL "
-			    "in command line, closing connection\r\n";
+static int
+session_route_line(struct session *s, size_t linelen, uint64_t nonsync_len)
+{
+	int	alive, resume;
 
-			log_warnx("session %u: bare CR/LF/NUL in command "
-			    "line, closing", s->id);
-			session_write(s, bad, sizeof(bad) - 1);
-			session_teardown(s, "protocol-error");
-			return;
-		}
-
-		*crlf = '\0';
-
-		nonsync_len = 0;
-		if (line_nonsync_literal(s->inbuf, &nonsync_len) &&
-		    nonsync_len > IMAP_NONSYNC_LITERAL_MAX) {
-			/* RFC 7888 SS4, SS5: BYE, with RFC 4469's TOOBIG */
-			static const char bye[] = "* BYE [TOOBIG] "
-			    "non-synchronizing literal exceeds 4096 octets\r\n";
-
-			log_warnx("session %u: oversized non-synchronizing "
-			    "literal (%llu), closing", s->id,
-			    (unsigned long long)nonsync_len);
-			session_write(s, bye, sizeof(bye) - 1);
-			session_teardown(s, "limit-exceeded");
-			return;
-		}
-
-		resume = 0;
-		if (s->literal_skipline) {
-			s->literal_skipline = 0;
-			alive = 1;
-			resume = 1;
-		} else if (s->cmd_gather) {
-			s->cmd_gather = 0;
-			alive = 1;
-			if (linelen >= sizeof(s->cmdbuf) - s->cmdlen) {
-				session_cmd_reply(s, "NO", cmd_too_long);
-				s->cmd_queued = 0;
-			} else {
-				memcpy(s->cmdbuf + s->cmdlen, s->inbuf,
-				    linelen + 1);
-				if (!s->cmd_queued)
-					alive = session_run_cmd(s, s->cmdbuf);
-				else if (nonsync_len > 0) {
-					s->cmdlen += linelen;
-					(void)session_gather(s, nonsync_len);
-				} else {
-					s->cmd_queued = 0;
-					alive = session_queue(s, s->cmdbuf);
-				}
-			}
-			resume = 1;
-		} else if (s->auth_cont) {
-			/* the line is the user's base64 password, see below */
-			s->scrub_inbuf = 1;
-			alive = session_handle_auth_continuation(s, s->inbuf);
-		} else if (s->idling) {
-			alive = session_handle_idle_continuation(s, s->inbuf);
-		} else if (session_is_busy(s)) {
-			/* RFC 9051 SS5.5: queue while one is in flight */
-			alive = 1;
-			if (nonsync_len > 0) {
-				(void)strlcpy(s->cmdbuf, s->inbuf,
-				    sizeof(s->cmdbuf));
-				s->cmdlen = strlen(s->cmdbuf);
-				s->cmd_queued = 1;
-				(void)session_gather(s, nonsync_len);
-			} else
-				alive = session_queue(s, s->inbuf);
+	resume = 0;
+	if (s->literal_skipline) {
+		s->literal_skipline = 0;
+		alive = 1;
+		resume = 1;
+	} else if (s->cmd_gather) {
+		s->cmd_gather = 0;
+		alive = 1;
+		if (linelen >= sizeof(s->cmdbuf) - s->cmdlen) {
+			session_cmd_reply(s, "NO", cmd_too_long);
+			s->cmd_queued = 0;
 		} else {
-			alive = session_run_cmd(s, s->inbuf);
+			memcpy(s->cmdbuf + s->cmdlen, s->inbuf, linelen + 1);
+			if (!s->cmd_queued)
+				alive = session_run_cmd(s, s->cmdbuf);
+			else if (nonsync_len > 0) {
+				s->cmdlen += linelen;
+				(void)session_gather(s, nonsync_len);
+			} else {
+				s->cmd_queued = 0;
+				alive = session_queue(s, s->cmdbuf);
+			}
 		}
-		if (alive && resume && nonsync_len == 0)
-			alive = session_dequeue_next(s);
-		if (alive == 0)
-			return;	/* s was torn down (LOGOUT), do not touch */
+		resume = 1;
+	} else if (s->auth_cont) {
+		/* the line is the user's base64 password, scrubbed after use */
+		s->scrub_inbuf = 1;
+		alive = session_handle_auth_continuation(s, s->inbuf);
+	} else if (s->idling) {
+		alive = session_handle_idle_continuation(s, s->inbuf);
+	} else if (session_is_busy(s)) {
+		/* RFC 9051 section 5.5: queue while one is in flight */
+		alive = 1;
+		if (nonsync_len > 0) {
+			(void)strlcpy(s->cmdbuf, s->inbuf, sizeof(s->cmdbuf));
+			s->cmdlen = strlen(s->cmdbuf);
+			s->cmd_queued = 1;
+			(void)session_gather(s, nonsync_len);
+		} else
+			alive = session_queue(s, s->inbuf);
+	} else {
+		alive = session_run_cmd(s, s->inbuf);
+	}
+	if (alive && resume && nonsync_len == 0)
+		alive = session_dequeue_next(s);
+	return (alive);
+}
 
-		if (nonsync_len > 0 && !s->literal_pending &&
-		    !s->cmd_gather)
-			s->literal_discard = nonsync_len;
+static int
+session_consume_line(struct session *s)
+{
+	uint64_t	 nonsync_len;
+	size_t		 consumed, linelen;
+	char		*crlf;
 
-		/* cmd_starttls() zeroes inbuflen */
-		if (consumed > s->inbuflen)
-			consumed = s->inbuflen;
+	crlf = memmem(s->inbuf, s->inbuflen, "\r\n", 2);
+	if (crlf == NULL)
+		return (0);
+	linelen = (size_t)(crlf - s->inbuf);
+	consumed = linelen + 2;
 
-		/* don't leave a base64 password in inbuf */
-		if (s->scrub_inbuf) {
-			explicit_bzero(s->inbuf, consumed);
-			s->scrub_inbuf = 0;
-		}
+	/* RFC 9051 section 2.2/section 9: no CR, LF or NUL inside a line */
+	if (memchr(s->inbuf, '\r', linelen) != NULL ||
+	    memchr(s->inbuf, '\n', linelen) != NULL ||
+	    memchr(s->inbuf, '\0', linelen) != NULL) {
+		static const char bad[] = "* BAD bare CR, LF or NUL "
+		    "in command line, closing connection\r\n";
 
-		memmove(s->inbuf, s->inbuf + consumed, s->inbuflen - consumed);
-		s->inbuflen -= consumed;
+		log_warnx("session %u: bare CR/LF/NUL in command "
+		    "line, closing", s->id);
+		session_write(s, bad, sizeof(bad) - 1);
+		session_teardown(s, "protocol-error");
+		return (-1);
 	}
+	*crlf = '\0';
 
+	nonsync_len = 0;
+	if (line_nonsync_literal(s->inbuf, &nonsync_len) &&
+	    nonsync_len > IMAP_NONSYNC_LITERAL_MAX) {
+		/* RFC 7888 section 4, section 5: BYE, with RFC 4469's TOOBIG */
+		static const char bye[] = "* BYE [TOOBIG] "
+		    "non-synchronizing literal exceeds 4096 octets\r\n";
+
+		log_warnx("session %u: oversized non-synchronizing "
+		    "literal (%llu), closing", s->id,
+		    (unsigned long long)nonsync_len);
+		session_write(s, bye, sizeof(bye) - 1);
+		session_teardown(s, "limit-exceeded");
+		return (-1);
+	}
+
+	if (!session_route_line(s, linelen, nonsync_len))
+		return (-1);	/* s was torn down (LOGOUT), do not touch */
+
+	if (nonsync_len > 0 && !s->literal_pending && !s->cmd_gather)
+		s->literal_discard = nonsync_len;
+
+	/* cmd_starttls() zeroes inbuflen */
+	if (consumed > s->inbuflen)
+		consumed = s->inbuflen;
+
+	/* don't leave a base64 password in inbuf */
+	if (s->scrub_inbuf) {
+		explicit_bzero(s->inbuf, consumed);
+		s->scrub_inbuf = 0;
+	}
+
+	memmove(s->inbuf, s->inbuf + consumed, s->inbuflen - consumed);
+	s->inbuflen -= consumed;
+	return (1);
+}
+
+void
+session_dispatch_client(int fd, short event, void *arg)
+{
+	struct session	*s = arg;
+	ssize_t		 n;
+	size_t		 tls_want;
+	int		 rc;
+
+	if (s->write_failed) {
+		session_teardown(s, "io-error");
+		return;
+	}
+	if ((n = session_read_client(s, fd, &tls_want)) == -1)
+		return;
+	s->inbuflen += (size_t)n;
+
+	for (;;) {
+		if (s->literal_discard > 0)
+			rc = session_discard_literal(s);
+		else if (s->literal_pending)
+			rc = session_stream_append(s);
+		else if (s->cmd_gather && s->cmd_octets > 0)
+			rc = session_gather_octets(s);
+		else
+			rc = session_consume_line(s);
+		if (rc == -1)
+			return;
+		if (rc == 0)
+			break;
+	}
+
 	if (s->inbuflen == sizeof(s->inbuf)) {
-		/* RFC 9051 SS7.1.3's example text */
+		/* RFC 9051 section 7.1.3's example text */
 		static const char bad[] = "* BAD command line too long\r\n";
 
 		log_warnx("session %u: command line too long, closing",
@@ -1281,12 +1273,12 @@ session_dispatch_client(int fd, short event, void *arg
 	}
 
 	/* libtls may hold bytes that EV_READ will not report */
-	if (s->tls_active && n > 0 && (size_t)n == tls_want &&
+	if (s->tls_active && (size_t)n == tls_want &&
 	    s->inbuflen < sizeof(s->inbuf))
 		event_active(&s->client_ev, EV_READ, 1);
 }
 
-/* RFC 9051 SS5.4: a short timer before login, 30 minutes after */
+/* RFC 9051 section 5.4: a short timer before login, 30 minutes after */
 #define SESSION_WRITE_PREAUTH_MS	5000
 #define SESSION_WRITE_AUTH_MS		(1800 * 1000)
 
@@ -1380,7 +1372,6 @@ fail:
 	event_active(&s->client_ev, EV_READ, 1);
 }
 
-
 /* on overflow the line still ends in CRLF */
 static void	 session_writef(struct session *, const char *, ...)
 		    __attribute__((__format__ (printf, 2, 3)));
@@ -1413,7 +1404,6 @@ session_reply(struct session *s, const char *tag, cons
 	session_writef(s, "%s %s %s\r\n", tag, status, text);
 }
 
-
 void
 session_untagged(struct session *s, const char *text)
 {
@@ -1421,38 +1411,22 @@ session_untagged(struct session *s, const char *text)
 }
 
 int
-send_mbox_request(struct session *s, int imsg_type, const char *what,
-    const char *imsgname, const void *req, size_t reqlen, const void *elems,
-    uint32_t nelems, size_t elemsize)
+send_mbox_request(struct session *s, int imsg_type, const char *imsgname,
+    const void *req, size_t reqlen, const void *elems, uint32_t nelems,
+    size_t elemsize)
 {
-	size_t	 bodylen = (size_t)nelems * elemsize;
-	char	*combined;
+	struct iovec	 iov[2];
 
-	if (bodylen == 0) {
-		if (imsg_compose(&s->store_iev->ibuf, imsg_type, 0, 0, -1,
-		    req, reqlen) == -1) {
-			log_warn("session %u: imsg_compose %s", s->id,
-			    imsgname);
-			return (0);
-		}
-		return (1);
+	iov[0].iov_base = (void *)req;
+	iov[0].iov_len = reqlen;
+	iov[1].iov_base = (void *)elems;
+	iov[1].iov_len = (size_t)nelems * elemsize;
+	if (imsg_composev(&s->store_iev->ibuf, imsg_type, 0, 0, -1, iov,
+	    iov[1].iov_len > 0 ? 2 : 1) == -1) {
+		log_warn("session %u: imsg_composev %s", s->id, imsgname);
+		return (-1);
 	}
-
-	if ((combined = malloc(reqlen + bodylen)) == NULL) {
-		log_warn("session %u: malloc %s imsg buffer", s->id, what);
-		return (0);
-	}
-	memcpy(combined, req, reqlen);
-	memcpy(combined + reqlen, elems, bodylen);
-
-	if (imsg_compose(&s->store_iev->ibuf, imsg_type, 0, 0, -1, combined,
-	    reqlen + bodylen) == -1) {
-		log_warn("session %u: imsg_compose %s", s->id, imsgname);
-		free(combined);
-		return (0);
-	}
-	free(combined);
-	return (1);
+	return (0);
 }
 
 void
@@ -1589,7 +1563,7 @@ session_handle_line(struct session *s, char *line)
 		return (1);
 	}
 
-	/* RFC 4616 SS2: the initial response holds the password */
+	/* RFC 4616 section 2: the initial response holds the password */
 	if (name != NULL && (strcasecmp(name, "AUTHENTICATE") == 0 ||
 	    strcasecmp(name, "LOGIN") == 0))
 		log_debug("session %u: <<< %s %s <redacted>", s->id, tag,
@@ -1613,16 +1587,16 @@ session_handle_line(struct session *s, char *line)
 		return (1);
 	}
 
-	for (i = 0; i < NUM_IMAP_CMDS; i++) {
+	for (i = 0; i < nitems(imap_cmds); i++) {
 		if (strcasecmp(name, imap_cmds[i].name) == 0)
 			break;
 	}
-	if (i == NUM_IMAP_CMDS) {
+	if (i == nitems(imap_cmds)) {
 		session_reply(s, tag, "BAD", "Unknown command");
 		return (1);
 	}
 	if (!(imap_cmds[i].states & (1U << s->state))) {
-		/* RFC 9051 SS3 allows BAD or NO */
+		/* RFC 9051 section 3 allows BAD or NO */
 		session_reply(s, tag, "BAD",
 		    "Command not permitted in this state");
 		return (1);
@@ -1630,7 +1604,6 @@ session_handle_line(struct session *s, char *line)
 	return (imap_cmds[i].handler(s, tag, args));
 }
 
-
 void
 listener_dispatch_auth(int fd, short event, void *arg)
 {
@@ -1669,15 +1642,15 @@ listener_dispatch_auth(int fd, short event, void *arg)
 		switch (imsg_get_type(&imsg)) {
 		case IMSG_AUTH_RESULT: {
 			struct imsg_auth_result	 res;
-			struct session		*s;
+			struct session		*s = &sess;
 
 			if (imsg_get_data(&imsg, &res, sizeof(res)) == -1) {
 				log_warnx("bad IMSG_AUTH_RESULT");
 				break;
 			}
-			if ((s = session_find(res.session_id)) == NULL) {
-				log_debug("IMSG_AUTH_RESULT for unknown "
-				    "session %u", res.session_id);
+			if (res.session_id != s->id) {
+				log_debug("IMSG_AUTH_RESULT for session %u, "
+				    "not ours", res.session_id);
 				break;
 			}
 			if (!res.ok) {
@@ -1708,7 +1681,6 @@ listener_dispatch_auth(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 void
@@ -1727,14 +1699,10 @@ listener_dispatch_parent(int fd, short event, void *ar
 		if ((n = imsgbuf_read(&iev->ibuf)) == -1)
 			fatal("imsgbuf_read");
 		if (n == 0) {
-			struct session	*s = TAILQ_FIRST(&sessions);
-
 			/* the parent is stopping us: end the session */
 			log_debug("listener-worker: parent closed "
 			    "channel, shutting down");
-			if (s != NULL)
-				session_teardown(s, "shutdown");
-			exit(0);
+			session_teardown(&sess, "shutdown");
 		}
 	}
 
@@ -1747,40 +1715,47 @@ listener_dispatch_parent(int fd, short event, void *ar
 		switch (imsg_get_type(&imsg)) {
 		case IMSG_STORE_FORK: {
 			struct imsg_store_fork	 fail;
-			struct session		*s;
+			struct session		*s = &sess;
 
 			if (imsg_get_data(&imsg, &fail, sizeof(fail)) == -1) {
 				log_warnx("bad IMSG_STORE_FORK reply");
 				break;
 			}
-			if ((s = session_find(fail.session_id)) == NULL)
+			if (fail.session_id != s->id)
 				break;
+			/* RFC 9051 section 3.4: a BYE before the close */
 			if (fail.limit) {
+				session_untagged(s, "BYE [LIMIT] too many "
+				    "sessions for this account");
 				session_reply(s, s->pending_tag, "NO",
 				    "[LIMIT] too many sessions for this "
 				    "account");
 				session_teardown(s, "limit-exceeded");
 			} else {
+				session_untagged(s, "BYE [UNAVAILABLE] "
+				    "mailbox store unavailable");
 				session_reply(s, s->pending_tag, "NO",
-				    "authentication succeeded but mailbox "
-				    "store unavailable");
+				    "[UNAVAILABLE] authentication "
+				    "succeeded but mailbox store unavailable");
 				session_teardown(s, "io-error");
 			}
 			break;
 		}
 		case IMSG_SETUP_PEER: {
-			uint32_t		 sess_id = imsg_get_id(&imsg);
-			int			 store_fd = imsg_get_fd(&imsg);
-			struct session		*s;
+			struct session		*s = &sess;
+			uint32_t		 sess_id;
+			int			 store_fd;
 
+			sess_id = imsg_get_id(&imsg);
+			store_fd = imsg_get_fd(&imsg);
 			if (store_fd == -1) {
 				log_warnx("IMSG_SETUP_PEER (store) carried "
 				    "no fd");
 				break;
 			}
-			if ((s = session_find(sess_id)) == NULL) {
+			if (sess_id != s->id) {
 				log_warnx("IMSG_SETUP_PEER (store) for "
-				    "unknown session %u", sess_id);
+				    "session %u, not ours", sess_id);
 				close(store_fd);
 				break;
 			}
@@ -1795,7 +1770,7 @@ listener_dispatch_parent(int fd, short event, void *ar
 			s->state = SESSION_AUTHENTICATED;
 			session_login_grace_disarm(s);
 			log_debug("session %u: store peer wired", sess_id);
-			/* RFC 9051 SS6.2.2's PLAIN example text, verbatim. */
+			/* RFC 9051 section 6.2.2's PLAIN example, verbatim */
 			session_reply(s, s->pending_tag, "OK",
 			    "Success (tls protection)");
 			break;
@@ -1808,25 +1783,17 @@ listener_dispatch_parent(int fd, short event, void *ar
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
-struct session *
-session_find(uint32_t id)
-{
-	struct session *s;
 
-	TAILQ_FOREACH(s, &sessions, entry) {
-		if (s->id == id)
-			return (s);
-	}
-	return (NULL);
-}
-
 /* reason: one of eight fixed tokens, never NULL, never attacker text */
 void
 session_teardown(struct session *s, const char *reason)
 {
+	struct timespec	 now;
+	const char	*who = "-";
+	long long	 dur = -1;
+
 	if (s->store_iev != NULL) {
 		if (imsg_compose(&s->store_iev->ibuf, IMSG_STORE_SHUTDOWN,
 		    0, 0, -1, NULL, 0) == -1)
@@ -1847,8 +1814,9 @@ session_teardown(struct session *s, const char *reason
 		event_del(&s->client_ev);
 
 	if (s->tls_ctx != NULL) {
-		int	ret = tls_close(s->tls_ctx);
+		int	ret;
 
+		ret = tls_close(s->tls_ctx);
 		if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT)
 			close(s->client_fd);
 		/* debug: a missing close_notify is routine; httpd ignores it */
@@ -1873,29 +1841,19 @@ session_teardown(struct session *s, const char *reason
 	while (s->cmd_queue_n > 0)
 		free(s->cmd_queue[--s->cmd_queue_n]);
 
-	TAILQ_REMOVE(&sessions, s, entry);
-	{
-		struct timespec	 now;
-		const char	*who = "-";
-		long long	 dur = -1;
+	if (clock_gettime(CLOCK_MONOTONIC, &now) != -1 &&
+	    s->connected_at.tv_sec != 0)
+		dur = (long long)(now.tv_sec - s->connected_at.tv_sec);
+	/* user= stays "-" until past authenticating; see listener.h */
+	if (s->user[0] != '\0' && s->state != SESSION_AUTHENTICATING)
+		who = s->user;
+	/* tls= is the final state, unlike the connect line's */
+	log_info("session %u: closed peer=%.200s user=%.64s reason=%s tls=%s "
+	    "duration=%lld", s->id, s->remote_addr, who, reason,
+	    s->tls_active ? "yes" : "no", dur);
 
-		if (clock_gettime(CLOCK_MONOTONIC, &now) != -1 &&
-		    s->connected_at.tv_sec != 0)
-			dur = (long long)(now.tv_sec -
-			    s->connected_at.tv_sec);
-		/* user= stays "-" until past authenticating; see listener.h */
-		if (s->user[0] != '\0' && s->state != SESSION_AUTHENTICATING)
-			who = s->user;
-		/* tls= is the final state, unlike the connect line's */
-		log_info("session %u: closed peer=%.200s user=%.64s "
-		    "reason=%s tls=%s duration=%lld", s->id,
-		    s->remote_addr, who, reason,
-		    s->tls_active ? "yes" : "no", dur);
-	}
-
 	/* inbuf may hold a base64 SASL response */
 	explicit_bzero(s, sizeof(*s));
-	free(s);
 
 	log_debug("listener-worker: session closed, exiting");
 	exit(0);
blob - 14c2f93c66bdbc2b25143b61ec596f8d7db838fd
blob + 6b3f0d914c33d18b53a07299df430b5a8a5a5cac
--- src/listener.h
+++ src/listener.h
@@ -20,13 +20,17 @@
 #define LISTENER_H
 
 #include <sys/types.h>
-#include <sys/queue.h>
 
 #include <event.h>
 #include <stdint.h>
 #include <time.h>
 #include <tls.h>
 
+/*
+ * Returns: command handlers 1 while the session lives, 0 once torn down;
+ * parsers 0, -1 (BAD) or -2 (NO) with *errmsg set; the rest 0/-1 or 1/0.
+ */
+
 enum session_state {
 	SESSION_NOT_AUTH,
 	SESSION_AUTHENTICATING,
@@ -42,7 +46,7 @@ enum session_state {
 	SESSION_STATUSING,
 	SESSION_COPYING,
 
-	/* RFC 9051 SS6.3.4-SS6.3.9; mbox_op_prev_state is restored after */
+	/* RFC 9051 sections 6.3.4-6.3.9; mbox_op_prev_state restored after */
 	SESSION_CREATING,
 	SESSION_DELETING,
 	SESSION_RENAMING,
@@ -54,10 +58,10 @@ enum session_state {
 /* RFC 9051 sets no limit; this bounds a client that never sends CRLF */
 #define SESSION_INBUF_MAX	8192
 
-/* RFC 9051 SS9 sets no limit; a longer tag is truncated */
+/* RFC 9051 section 9 sets no limit; a longer tag is truncated */
 #define IMAP_TAG_MAX	64
 
-/* pipelined lines (RFC 9051 SS5.5); past this the session is dropped */
+/* pipelined lines (RFC 9051 section 5.5); past this the session is dropped */
 #define SESSION_CMD_QUEUE_MAX	8
 
 #define HEADER_FIELDS_LABEL_MAX	288
@@ -65,7 +69,7 @@ enum session_state {
 /* two DQUOTEs, a NUL, and a backslash per byte */
 #define MBOX_QUOTED_MAX		((2 * MBOX_NAME_MAX) + 3)
 
-/* RFC 7162 SS7: a mod-sequence is a positive 63-bit integer */
+/* RFC 7162 section 7: a mod-sequence is a positive 63-bit integer */
 #define MODSEQ_MAX		INT64_MAX
 
 struct vanished_range {
@@ -96,9 +100,9 @@ struct session {
 	/* inbuf held SASL credentials; explicit_bzero(3) it */
 	int			 scrub_inbuf;
 	int			 auth_cont;
-	/* RFC 9051 SS6.3.13: the next line is DONE */
+	/* RFC 9051 section 6.3.13: the next line is DONE */
 	int			 idling;
-	/* pipelined lines (RFC 9051 SS5.5), malloc(3)'d */
+	/* pipelined lines (RFC 9051 section 5.5), malloc(3)'d */
 	char			*cmd_queue[SESSION_CMD_QUEUE_MAX];
 	uint32_t		 cmd_queue_n;
 	/* the tag points into inbuf, which the next read overwrites */
@@ -114,7 +118,7 @@ struct session {
 	uint64_t		 pending_body_len;
 	int			 pending_body_found;
 	char			 pending_body_label[SECTION_PART_MAX];
-	/* only the origin is echoed back (RFC 9051 SS6.4.5) */
+	/* only the origin is echoed back (RFC 9051 section 6.4.5) */
 	int			 pending_body_has_partial;
 	uint32_t		 pending_body_partial_origin;
 	char			*pending_envelope_buf;
@@ -125,19 +129,19 @@ struct session {
 	int			 pending_bodystructure_found;
 	char			 pending_bodystructure_label[16];
 
-	/* tagged NO rather than OK (RFC 9051 SS6.4.5) */
+	/* tagged NO rather than OK (RFC 9051 section 6.4.5) */
 	int			 fetch_incomplete;
 
 	int			 close_after_expunge;
 
-	/* RFC 9051 SS4.3 literal octets, checked before the CRLF search */
+	/* RFC 9051 section 4.3 literal octets, read before the CRLF search */
 	int			 literal_pending;
 	uint64_t		 literal_len;	/* announced size, "{n}" */
 	uint64_t		 literal_remaining;
-	/* RFC 7888 SS3: a refused literal's octets, then its command's line */
+	/* RFC 7888 section 3: a refused literal's octets, then its line */
 	uint64_t		 literal_discard;
 	int			 literal_skipline;
-	/* RFC 9051 SS4.3: a command gathered with its literals */
+	/* RFC 9051 section 4.3: a command gathered with its literals */
 	char			 cmdbuf[SESSION_INBUF_MAX];
 	size_t			 cmdlen;
 	size_t			 cmd_octets;
@@ -169,15 +173,15 @@ struct session {
 	int			 search_used_modseq;
 	uint64_t		 search_max_modseq;
 
-	/* RFC 7162 SS3.1/SS3.2.3: sticky; QRESYNC implies CONDSTORE */
+	/* RFC 7162 sections 3.1, 3.2.3: sticky; QRESYNC implies CONDSTORE */
 	int			 condstore_enabled;
 	int			 qresync_enabled;
 	uint64_t		 mbox_highestmodseq;
-	/* RFC 9051 SS6.3.3 EXAMINE */
+	/* RFC 9051 section 6.3.3 EXAMINE */
 	int			 mbox_readonly;
 	char			 selected_mailbox[MBOX_NAME_MAX];
 
-	/* RFC 9051 SS6.3.13 IDLE; the store child keeps the UID list */
+	/* RFC 9051 section 6.3.13 IDLE; the store child keeps the UID list */
 	int			 idle_refresh_pending;
 	int			 idle_refresh_again;
 	/* the tagged reply IDLE or NOOP owes once the refresh is in */
@@ -186,7 +190,7 @@ struct session {
 	struct event		 idle_ev;
 	struct event		 grace_ev;
 
-	/* held until IMSG_MBOX_SELECTED (RFC 7162 SS3.2.6 ordering) */
+	/* held until IMSG_MBOX_SELECTED (RFC 7162 section 3.2.6 ordering) */
 	struct vanished_range	*vanished_ranges;
 	uint32_t		 vanished_nranges;
 	uint32_t		 vanished_cap;
@@ -196,7 +200,7 @@ struct session {
 	/* a dropped entry fails the SELECT: HIGHESTMODSEQ would hide it */
 	int			 qresync_alloc_failed;
 
-	/* COPYUID precedes EXPUNGED (RFC 9051 SS6.4.8) */
+	/* COPYUID precedes EXPUNGED (RFC 9051 section 6.4.8) */
 	uint32_t		*copy_src_uids;
 	uint32_t		*copy_dest_uids;
 	uint32_t		 copy_n;
@@ -207,28 +211,22 @@ struct session {
 	uint32_t		 move_expunged_n;
 	uint32_t		 move_expunged_cap;
 
-	/* RFC 7162 SS3.1.3 MODIFIED */
+	/* RFC 7162 section 3.1.3 MODIFIED */
 	uint32_t		*store_modified;
 	uint32_t		 store_modified_n;
 	uint32_t		 store_modified_cap;
 	/* MODIFIED must list every failure, so an incomplete set is refused */
 	int			 store_modified_alloc_failed;
 
-	/* RFC 9051 SS6.4.9 UID <cmd> */
+	/* RFC 9051 section 6.4.9 UID <cmd> */
 	int			 cmd_by_uid;
-
-	TAILQ_ENTRY(session)	 entry;
 };
 
-/* named: declared and defined in different files */
-TAILQ_HEAD(session_list, session);
-
-extern struct session_list	 sessions;
 extern struct imsgev		 iev_auth;
 extern struct imsgev		 iev_parent;
 extern struct tls		*listener_tls_ctx;
 
-/* RFC 9051 SS7.1 INUSE, OVERQUOTA */
+/* RFC 9051 section 7.1 INUSE, OVERQUOTA */
 #define IMAP_BUSY_TEXT	"[INUSE] mailbox busy, try again"
 #define IMAP_OVERQUOTA_TEXT	"[OVERQUOTA] not enough free space"
 
@@ -265,9 +263,8 @@ int	 parse_nz_number(const char *, uint32_t *);
 int	 parse_sequence_set(const char *,
 		    struct seq_range[SEQSET_MAX_RANGES], uint32_t *,
 		    const char **);
-int	 parse_fetch_atts(char *, uint32_t *, int *, int *, char *,
-		    size_t, char *, size_t, int *, char *, size_t, int *,
-		    uint32_t *, uint32_t *, const char **);
+struct fetch_atts;
+int	 parse_fetch_atts(char *, struct fetch_atts *, const char **);
 int	 parse_header_fields_att(const char *, int *, char *, size_t);
 int	 section_part_valid(const char *);
 int	 parse_partial_suffix(const char *, int *, uint32_t *,
@@ -293,7 +290,6 @@ void	 session_finish_search(struct session *,
 		    struct imsg_mbox_result *);
 void	 session_send_greeting(struct session *);
 void	 session_teardown(struct session *, const char *);
-struct session	*session_find(uint32_t);
 
 /* RFC 7162 (CONDSTORE/QRESYNC) */
 void	 session_condstore_enable(struct session *);
@@ -331,7 +327,7 @@ int	 parse_fetch_modifiers(char *, struct imsg_mbox_fe
 int	 parse_store_modifiers(char *, struct imsg_mbox_store *,
 		    const char **);
 
-/* RFC 9051 SS6.4.9 (UID) */
+/* RFC 9051 section 6.4.9 (UID) */
 int	 fetch_dispatch(struct session *, const char *, char *, int);
 int	 store_do(struct session *, const char *, char *, int);
 int	 search_dispatch(struct session *, const char *, char *, int);
@@ -339,7 +335,7 @@ int	 uid_expunge_dispatch(struct session *, const char
 void	 session_handle_fetch_vanished(struct session *,
 		    const struct imsg_mbox_select_vanished *);
 
-/* RFC 9051 SS6.4.7/SS6.4.8 (COPY/MOVE) */
+/* RFC 9051 section 6.4.7/section 6.4.8 (COPY/MOVE) */
 int	 copy_move_dispatch(struct session *, const char *, char *,
 		    int, int);
 int	 listener_mailbox_name_valid(const char *);
@@ -369,8 +365,8 @@ void	 session_write(struct session *, const char *, si
 void	 session_reply(struct session *, const char *, const char *,
 		    const char *);
 void	 session_untagged(struct session *, const char *);
-int	 send_mbox_request(struct session *, int, const char *, const char *,
-		    const void *, size_t, const void *, uint32_t, size_t);
+int	 send_mbox_request(struct session *, int, const char *, const void *,
+		    size_t, const void *, uint32_t, size_t);
 int	 parse_command_line(char *, char **, char **, char **);
 int	 session_handle_line(struct session *, char *);
 int	 session_dequeue_next(struct session *);
@@ -386,7 +382,7 @@ int	 cmd_login(struct session *, const char *, char *)
 int	 cmd_starttls(struct session *, const char *, char *);
 int	 cmd_authenticate(struct session *, const char *, char *);
 
-/* command-auth (RFC 9051 SS6.3) */
+/* command-auth (RFC 9051 section 6.3) */
 int	 stub_not_implemented(struct session *, const char *,
 		    const char *);
 int	 cmd_enable(struct session *, const char *, char *);
@@ -404,7 +400,7 @@ int	 cmd_status(struct session *, const char *, char *
 int	 cmd_append(struct session *, const char *, char *);
 int	 cmd_idle(struct session *, const char *, char *);
 
-/* command-select (RFC 9051 SS6.4, valid only in Selected state). */
+/* command-select (RFC 9051 section 6.4, valid only in Selected state). */
 int	 cmd_close(struct session *, const char *, char *);
 int	 cmd_unselect(struct session *, const char *, char *);
 int	 cmd_expunge(struct session *, const char *, char *);
blob - b2dad4bfd9b2765481ed2214adabfe90e4b5da87
blob + efaf76ace0f31de26d972dafffccbaabf3f683a4
--- src/log.c
+++ src/log.c
@@ -4,17 +4,6 @@
  * Copyright (c) 2026 David Williams <dhw@openimapd.dev>
  * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
  *
- * This file's log_init()/log_procinit()/log_setverbose()/log_getverbose()/
- * vlog()/logit()/log_warn()/log_warnx()/log_info()/log_debug()/fatal()/
- * fatalx() API is the same daemon-logging idiom Henning Brauer wrote and
- * that is carried, nearly verbatim, across essentially every privsep
- * daemon in the OpenBSD base system (smtpd, bgpd, and many others all
- * credit him in their own log.c). This file's implementation details
- * (log_procname storage, log_init()'s parameters) are this project's own,
- * but the API shape and name are that same shared idiom, so his copyright
- * is carried forward here too, same rationale as parse.y's copyright
- * chain.
- *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
blob - f315b680566be25f414789461f75694af4c4c95e
blob + 5a9fbc87de9ea457a108331001578181b316af06
--- src/log.h
+++ src/log.h
@@ -4,13 +4,6 @@
  * Copyright (c) 2026 David Williams <dhw@openimapd.dev>
  * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
  *
- * This header's call signatures were deliberately matched to smtpd's own
- * log.h (see the comment below), that log_init()/fatal()/fatalx()/
- * log_warn()/log_debug() API is the same shared daemon-logging idiom
- * Henning Brauer wrote and that smtpd, bgpd, and most other privsep
- * daemons in OpenBSD base carry his copyright for. Same rationale as
- * log.c's copyright chain.
- *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
blob - 7e8374d788e2868027027f32d27f31f14949c84f
blob + 8046d4740bbff21de11286d3b7566844c593d26d
--- src/mailbox_cmd.c
+++ src/mailbox_cmd.c
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/socket.h>
@@ -47,11 +46,11 @@ parse_qresync_group(char *inner, struct imsg_mbox_sele
     struct seq_range ranges[SEQSET_MAX_RANGES], uint32_t *nranges,
     const char **errmsg)
 {
-	char		*p;
-	const char	*tok;
-	uint32_t	 uidvalidity;
+	char		*p, *end;
+	const char	*tok, *errstr;
+	uint32_t	 uidvalidity, i;
 	uint64_t	 modseq;
-	char		*ep;
+	int		 depth;
 
 	p = inner;
 	while (*p == ' ')
@@ -81,15 +80,12 @@ parse_qresync_group(char *inner, struct imsg_mbox_sele
 		*errmsg = "QRESYNC requires uidvalidity and mod-sequence";
 		return (-1);
 	}
-	/* RFC 7162: 1..2^63-1; strtoull(3) accepts a sign */
 	if (*tok < '0' || *tok > '9') {
 		*errmsg = "invalid QRESYNC mod-sequence";
 		return (-1);
 	}
-	errno = 0;
-	modseq = strtoull(tok, &ep, 10);
-	if (*ep != '\0' || errno != 0 || modseq == 0 ||
-	    modseq > MODSEQ_MAX) {
+	modseq = strtonum(tok, 1, MODSEQ_MAX, &errstr);
+	if (errstr != NULL) {
 		*errmsg = "invalid QRESYNC mod-sequence";
 		return (-1);
 	}
@@ -115,22 +111,18 @@ parse_qresync_group(char *inner, struct imsg_mbox_sele
 		*p = '\0';
 		p++;
 	}
-	{
-		uint32_t	i;
-
-		/* RFC 7162 SS3.2.5.1: "*" is not allowed */
-		if (parse_sequence_set(tok, ranges, nranges, errmsg) == -1)
+	/* RFC 7162 section 3.2.5.1: "*" is not allowed */
+	if (parse_sequence_set(tok, ranges, nranges, errmsg) == -1)
+		return (-1);
+	for (i = 0; i < *nranges; i++) {
+		if (ranges[i].lo_is_star || ranges[i].hi_is_star) {
+			*errmsg = "invalid known-uids, '*' is not "
+			    "allowed here";
 			return (-1);
-		for (i = 0; i < *nranges; i++) {
-			if (ranges[i].lo_is_star || ranges[i].hi_is_star) {
-				*errmsg = "invalid known-uids, '*' is not "
-				    "allowed here (RFC 7162 SS3.2.5.1)";
-				return (-1);
-			}
 		}
-		req->qresync_has_uids = 1;
-		req->qresync_nranges = *nranges;
 	}
+	req->qresync_has_uids = 1;
+	req->qresync_nranges = *nranges;
 
 	while (*p == ' ')
 		p++;
@@ -141,25 +133,19 @@ parse_qresync_group(char *inner, struct imsg_mbox_sele
 		*errmsg = "expected seq-match-data";
 		return (-1);
 	}
-	{
-		char	*end = p;
-		int	 depth = 0;
-
-		for (;;) {
-			if (*end == '(')
-				depth++;
-			else if (*end == ')') {
-				depth--;
-				if (depth == 0)
-					break;
-			} else if (*end == '\0') {
-				*errmsg = "unterminated seq-match-data";
-				return (-1);
-			}
-			end++;
+	depth = 0;
+	for (end = p;; end++) {
+		if (*end == '(')
+			depth++;
+		else if (*end == ')') {
+			if (--depth == 0)
+				break;
+		} else if (*end == '\0') {
+			*errmsg = "unterminated seq-match-data";
+			return (-1);
 		}
-		p = end + 1;
 	}
+	p = end + 1;
 
 	while (*p == ' ')
 		p++;
@@ -206,8 +192,8 @@ parse_select_params(char *p, struct imsg_mbox_select *
 				return (-1);
 			}
 			if (!s->qresync_enabled) {
-				*errmsg = "QRESYNC select-param requires "
-				    "ENABLE QRESYNC first (RFC 7162 SS3.2.5)";
+				*errmsg = "QRESYNC requires ENABLE QRESYNC "
+				    "first";
 				return (-1);
 			}
 
@@ -234,7 +220,7 @@ parse_select_params(char *p, struct imsg_mbox_select *
 				return (-1);
 
 			req->qresync = 1;
-			/* RFC 7162 SS3.2.3: QRESYNC implies CONDSTORE */
+			/* RFC 7162 section 3.2.3: QRESYNC implies CONDSTORE */
 			*want_condstore = 1;
 			p = end + 1;
 			continue;
@@ -247,7 +233,7 @@ parse_select_params(char *p, struct imsg_mbox_select *
 	return (0);
 }
 
-/* RFC 9051 SS6.3.2/SS6.3.3 */
+/* RFC 9051 section 6.3.2/section 6.3.3 */
 static int
 select_or_examine(struct session *s, const char *tag, char *args, int readonly)
 {
@@ -277,7 +263,7 @@ select_or_examine(struct session *s, const char *tag, 
 		p++;
 	params = (*p != '\0') ? p : NULL;
 
-	/* "" parses as RFC 9051 SS9 quoted, so this is NO, not BAD */
+	/* "" parses as RFC 9051 section 9 quoted, so this is NO, not BAD */
 	if (mailbox[0] == '\0') {
 		session_reply(s, tag, "NO", "[NONEXISTENT] no such mailbox");
 		return (1);
@@ -299,8 +285,9 @@ select_or_examine(struct session *s, const char *tag, 
 	req.readonly = readonly;
 
 	if (params != NULL) {
-		size_t	plen = strlen(params);
+		size_t	plen;
 
+		plen = strlen(params);
 		if (plen < 2 || params[0] != '(' || params[plen - 1] != ')') {
 			session_reply(s, tag, "BAD",
 			    "malformed select-param list");
@@ -315,11 +302,11 @@ select_or_examine(struct session *s, const char *tag, 
 		}
 	}
 
-	/* RFC 7162 SS3.1.8/SS3.2.3 */
+	/* RFC 7162 section 3.1.8/section 3.2.3 */
 	if (want_condstore)
 		s->condstore_enabled = 1;
 
-	/* RFC 9051 SS6.3.2: SELECT auto-deselects the current mailbox. */
+	/* RFC 9051 section 6.3.2: SELECT auto-deselects the current mailbox. */
 	if (s->state == SESSION_SELECTED)
 		session_untagged(s,
 		    "OK [CLOSED] Previous mailbox is now closed");
@@ -334,8 +321,8 @@ select_or_examine(struct session *s, const char *tag, 
 	s->state = SESSION_SELECTING;
 	s->mbox_readonly = readonly;
 
-	if (!send_mbox_request(s, IMSG_MBOX_SELECT, cmdname, "IMSG_MBOX_SELECT",
-	    &req, sizeof(req), ranges, nranges, sizeof(struct seq_range))) {
+	if (send_mbox_request(s, IMSG_MBOX_SELECT, "IMSG_MBOX_SELECT", &req,
+	    sizeof(req), ranges, nranges, sizeof(struct seq_range)) == -1) {
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		s->state = SESSION_AUTHENTICATED;
 		return (1);
@@ -344,18 +331,16 @@ select_or_examine(struct session *s, const char *tag, 
 	return (1);
 }
 
-
 int
 cmd_select(struct session *s, const char *tag, char *args)
 {
-	return select_or_examine(s, tag, args, 0);
+	return (select_or_examine(s, tag, args, 0));
 }
 
-
 int
 cmd_examine(struct session *s, const char *tag, char *args)
 {
-	return select_or_examine(s, tag, args, 1);
+	return (select_or_examine(s, tag, args, 1));
 }
 
 /* checked here to save a round trip; the store checks again */
@@ -371,14 +356,13 @@ listener_reject_bad_utf8(struct session *s, const char
 	if (utf8_mailbox_ok(name))
 		return (0);
 
-	log_debug("session %u: mailbox name is not valid UTF-8, refusing "
-	    "(RFC 9051 SS5.1)", s->id);
+	log_debug("session %u: mailbox name not valid UTF-8", s->id);
 	session_reply(s, tag, "NO",
 	    "[CANNOT] mailbox name is not valid UTF-8");
 	return (1);
 }
 
-/* RFC 9051 SS6.3.4 CREATE */
+/* RFC 9051 section 6.3.4 CREATE */
 int
 cmd_create(struct session *s, const char *tag, char *args)
 {
@@ -402,7 +386,7 @@ cmd_create(struct session *s, const char *tag, char *a
 	}
 	if (listener_reject_bad_utf8(s, tag, mailbox))
 		return (1);
-	/* a name the server refuses is RFC 5530 SS3 NO [CANNOT], not BAD */
+	/* a refused name is RFC 5530 section 3 NO [CANNOT], not BAD */
 	if (!listener_mailbox_name_valid(mailbox)) {
 		session_reply(s, tag, "NO", "[CANNOT] invalid mailbox name");
 		return (1);
@@ -438,8 +422,8 @@ cmd_create(struct session *s, const char *tag, char *a
 	s->state = SESSION_CREATING;
 
 	/* a failed compose must not leave the session busy */
-	if (!send_mbox_request(s, IMSG_MBOX_CREATE, "CREATE",
-	    "IMSG_MBOX_CREATE", &req, sizeof(req), NULL, 0, 0)) {
+	if (send_mbox_request(s, IMSG_MBOX_CREATE,
+	    "IMSG_MBOX_CREATE", &req, sizeof(req), NULL, 0, 0) == -1) {
 		s->state = s->mbox_op_prev_state;
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		return (1);
@@ -448,7 +432,7 @@ cmd_create(struct session *s, const char *tag, char *a
 	return (1);
 }
 
-/* RFC 9051 SS6.3.5 DELETE */
+/* RFC 9051 section 6.3.5 DELETE */
 int
 cmd_delete(struct session *s, const char *tag, char *args)
 {
@@ -476,7 +460,6 @@ cmd_delete(struct session *s, const char *tag, char *a
 		return (1);
 	}
 
-	/* no arguments may follow the name */
 	while (*p == ' ')
 		p++;
 	if (*p != '\0') {
@@ -505,8 +488,8 @@ cmd_delete(struct session *s, const char *tag, char *a
 	s->mbox_op_prev_state = s->state;
 	s->state = SESSION_DELETING;
 
-	if (!send_mbox_request(s, IMSG_MBOX_DELETE, "DELETE",
-	    "IMSG_MBOX_DELETE", &req, sizeof(req), NULL, 0, 0)) {
+	if (send_mbox_request(s, IMSG_MBOX_DELETE,
+	    "IMSG_MBOX_DELETE", &req, sizeof(req), NULL, 0, 0) == -1) {
 		s->state = s->mbox_op_prev_state;
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		return (1);
@@ -515,7 +498,7 @@ cmd_delete(struct session *s, const char *tag, char *a
 	return (1);
 }
 
-/* RFC 9051 SS6.3.6 RENAME */
+/* RFC 9051 section 6.3.6 RENAME */
 int
 cmd_rename(struct session *s, const char *tag, char *args)
 {
@@ -538,7 +521,7 @@ cmd_rename(struct session *s, const char *tag, char *a
 		return (session_arg_error(s, tag, errmsg));
 
 	if (mailbox_name_is_inbox(oldname)) {
-		/* RFC 9051 SS6.3.6 allows refusing this */
+		/* RFC 9051 section 6.3.6 allows refusing this */
 		session_reply(s, tag, "NO", "[CANNOT] cannot rename INBOX");
 		return (1);
 	}
@@ -548,14 +531,12 @@ cmd_rename(struct session *s, const char *tag, char *a
 	}
 	if (listener_reject_bad_utf8(s, tag, newname))
 		return (1);
-	/* a name the server refuses is RFC 5530 SS3 NO [CANNOT], not BAD */
 	if (mailbox_name_is_inbox(newname) ||
 	    !listener_mailbox_name_valid(newname)) {
 		session_reply(s, tag, "NO", "[CANNOT] invalid mailbox name");
 		return (1);
 	}
 
-	/* no arguments may follow the name */
 	while (*p == ' ')
 		p++;
 	if (*p != '\0') {
@@ -588,8 +569,8 @@ cmd_rename(struct session *s, const char *tag, char *a
 	s->mbox_op_prev_state = s->state;
 	s->state = SESSION_RENAMING;
 
-	if (!send_mbox_request(s, IMSG_MBOX_RENAME, "RENAME",
-	    "IMSG_MBOX_RENAME", &req, sizeof(req), NULL, 0, 0)) {
+	if (send_mbox_request(s, IMSG_MBOX_RENAME,
+	    "IMSG_MBOX_RENAME", &req, sizeof(req), NULL, 0, 0) == -1) {
 		s->state = s->mbox_op_prev_state;
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		return (1);
@@ -598,8 +579,7 @@ cmd_rename(struct session *s, const char *tag, char *a
 	return (1);
 }
 
-
-/* RFC 9051 SS6.3.7 SUBSCRIBE and SS6.3.8 UNSUBSCRIBE */
+/* RFC 9051 section 6.3.7 SUBSCRIBE and section 6.3.8 UNSUBSCRIBE */
 static int
 subscribe_dispatch(struct session *s, const char *tag, char *args,
     int is_unsub)
@@ -628,7 +608,7 @@ subscribe_dispatch(struct session *s, const char *tag,
 	if (listener_reject_bad_utf8(s, tag, mailbox))
 		return (1);
 
-	/* RFC 9051 SS5.1: INBOX is always subscribed */
+	/* RFC 9051 section 5.1: INBOX is always subscribed */
 	if (mailbox_name_is_inbox(mailbox)) {
 		if (is_unsub) {
 			session_reply(s, tag, "NO",
@@ -638,7 +618,6 @@ subscribe_dispatch(struct session *s, const char *tag,
 		session_reply(s, tag, "OK", "SUBSCRIBE completed");
 		return (1);
 	}
-	/* a name the server refuses is RFC 5530 SS3 NO [CANNOT], not BAD */
 	if (!listener_mailbox_name_valid(mailbox)) {
 		session_reply(s, tag, "NO", "[CANNOT] invalid mailbox name");
 		return (1);
@@ -673,9 +652,9 @@ subscribe_dispatch(struct session *s, const char *tag,
 	s->mbox_op_prev_state = s->state;
 	s->state = is_unsub ? SESSION_UNSUBSCRIBING : SESSION_SUBSCRIBING;
 
-	if (!send_mbox_request(s, is_unsub ? IMSG_MBOX_UNSUBSCRIBE :
-	    IMSG_MBOX_SUBSCRIBE, cmdname, imsgname, &req, sizeof(req), NULL,
-	    0, 0)) {
+	if (send_mbox_request(s, is_unsub ? IMSG_MBOX_UNSUBSCRIBE :
+	    IMSG_MBOX_SUBSCRIBE, imsgname, &req, sizeof(req), NULL,
+	    0, 0) == -1) {
 		s->state = s->mbox_op_prev_state;
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		return (1);
@@ -684,21 +663,19 @@ subscribe_dispatch(struct session *s, const char *tag,
 	return (1);
 }
 
-
 int
 cmd_subscribe(struct session *s, const char *tag, char *args)
 {
-	return subscribe_dispatch(s, tag, args, 0);
+	return (subscribe_dispatch(s, tag, args, 0));
 }
 
-
 int
 cmd_unsubscribe(struct session *s, const char *tag, char *args)
 {
-	return subscribe_dispatch(s, tag, args, 1);
+	return (subscribe_dispatch(s, tag, args, 1));
 }
 
-/* RFC 9051 SS6.3.9 wildcards; a flat namespace */
+/* RFC 9051 section 6.3.9 wildcards; a flat namespace */
 int
 list_pattern_match(const char *pat, const char *name, int ci)
 {
@@ -733,7 +710,7 @@ list_pattern_match(const char *pat, const char *name, 
 	return (*p == '\0');
 }
 
-/* RFC 9051 SS4.3 quoted string */
+/* RFC 9051 section 4.3 quoted string */
 int
 quote_mailbox(char *out, size_t outsize, const char *name)
 {
@@ -750,7 +727,7 @@ quote_mailbox(char *out, size_t outsize, const char *n
 		char	c = name[i];
 		size_t	need;
 
-		/* RFC 9051 SS4.3: no NUL, CR or LF */
+		/* RFC 9051 section 4.3: no NUL, CR or LF */
 		if (c == '\r' || c == '\n')
 			c = ' ';
 
@@ -782,7 +759,7 @@ atom_char_ok(unsigned char c, int wildcards)
 	return (1);
 }
 
-/* RFC 9051 SS9 astring, or list-mailbox with wildcards */
+/* RFC 9051 section 9 astring, or list-mailbox with wildcards */
 static int
 parse_mailbox_arg(char **pp, char *out, size_t outsize, int wildcards,
     const char **errmsg)
@@ -827,7 +804,7 @@ parse_mailbox_arg(char **pp, char *out, size_t outsize
 				break;
 			}
 			if (*p == '\\') {
-				/* RFC 9051 SS9 QUOTED-CHAR */
+				/* RFC 9051 section 9 QUOTED-CHAR */
 				p++;
 				if (*p == '\0') {
 					/* a trailing backslash */
@@ -881,20 +858,20 @@ parse_list_pattern(char **pp, char *out, size_t outsiz
 	return (parse_mailbox_arg(pp, out, outsize, 1, errmsg));
 }
 
-/* RFC 9051 SS6.3.9 basic syntax; LSUB varies only the output */
+/* RFC 9051 section 6.3.9 basic syntax; LSUB varies only the output */
 static int
 list_dispatch(struct session *s, const char *tag, char *args, int is_lsub)
 {
 	struct imsg_mbox_list	 req;
-	char		 reference[MBOX_NAME_MAX];
-	char		 pattern[MBOX_NAME_MAX];
-	char		 canon[2 * MBOX_NAME_MAX];
-	char		*p;
-	const char	*errmsg;
-	const char	*cmdname = is_lsub ? "LSUB" : "LIST";
-	const char	*kw = is_lsub ? "LSUB" : "LIST";
-	char		 text[64];
-	int		 subscribed_only = 0;
+	char			 reference[MBOX_NAME_MAX];
+	char			 pattern[MBOX_NAME_MAX];
+	char			 canon[2 * MBOX_NAME_MAX];
+	char			 text[64];
+	char			*p;
+	const char		*errmsg;
+	const char		*cmdname = is_lsub ? "LSUB" : "LIST";
+	size_t			 n;
+	int			 subscribed_only = 0;
 
 	if (args == NULL) {
 		snprintf(text, sizeof(text), "%s requires two arguments",
@@ -908,7 +885,7 @@ list_dispatch(struct session *s, const char *tag, char
 		p++;
 
 	if (*p == '(') {
-		/* RFC 9051 SS6.3.9 condition 1: list-select-opts */
+		/* RFC 9051 section 6.3.9 condition 1: list-select-opts */
 		if (is_lsub) {
 			session_reply(s, tag, "BAD",
 			    "LSUB takes no selection options");
@@ -944,7 +921,7 @@ list_dispatch(struct session *s, const char *tag, char
 		p++;
 
 	if (*p == '(') {
-		/* RFC 9051 SS6.3.9 condition 2 */
+		/* RFC 9051 section 6.3.9 condition 2 */
 		snprintf(text, sizeof(text),
 		    "extended %s mailbox-pattern lists not supported",
 		    cmdname);
@@ -958,39 +935,36 @@ list_dispatch(struct session *s, const char *tag, char
 	while (*p == ' ')
 		p++;
 	if (*p != '\0') {
-		/* RFC 9051 SS6.3.9 condition 3 */
+		/* RFC 9051 section 6.3.9 condition 3 */
 		snprintf(text, sizeof(text),
 		    "extended %s return options not supported", cmdname);
 		session_reply(s, tag, "NO", text);
 		return (1);
 	}
 
-	/* RFC 9051 SS6.3.9: an empty pattern asks for the delimiter */
+	/* RFC 9051 section 6.3.9: an empty pattern asks for the delimiter */
 	if (pattern[0] == '\0') {
-		snprintf(text, sizeof(text), "%s (\\Noselect) \"/\" \"\"", kw);
+		snprintf(text, sizeof(text), "%s (\\Noselect) \"/\" \"\"",
+		    cmdname);
 		session_untagged(s, text);
 		snprintf(text, sizeof(text), "%s completed", cmdname);
 		session_reply(s, tag, "OK", text);
 		return (1);
 	}
 
-	{
-		size_t	n;
-
-		n = strlcpy(canon, reference, sizeof(canon));
-		if (n < sizeof(canon))
-			n = strlcat(canon, pattern, sizeof(canon));
-		if (n >= sizeof(canon)) {
-			session_reply(s, tag, "BAD",
-			    "combined reference and pattern too long");
-			return (1);
-		}
+	n = strlcpy(canon, reference, sizeof(canon));
+	if (n < sizeof(canon))
+		n = strlcat(canon, pattern, sizeof(canon));
+	if (n >= sizeof(canon)) {
+		session_reply(s, tag, "BAD",
+		    "combined reference and pattern too long");
+		return (1);
 	}
 
-	/* RFC 9051 SS6.3.9: INBOX is answered here */
+	/* RFC 9051 section 6.3.9: INBOX is answered here */
 	if (list_pattern_match(canon, "INBOX", 1)) {
-		/* RFC 9051 SS7.3.1: attributes are optional */
-		snprintf(text, sizeof(text), "%s (%s) \"/\" \"INBOX\"", kw,
+		/* RFC 9051 section 7.3.1: attributes are optional */
+		snprintf(text, sizeof(text), "%s (%s) \"/\" \"INBOX\"", cmdname,
 		    subscribed_only ? "\\Subscribed" : "");
 		session_untagged(s, text);
 	}
@@ -1010,7 +984,7 @@ list_dispatch(struct session *s, const char *tag, char
 		return (1);
 	}
 	s->list_is_lsub = is_lsub;
-	/* LSUB lists subscribed names (RFC 3501 SS6.3.9) */
+	/* LSUB lists subscribed names (RFC 3501 section 6.3.9) */
 	s->list_subscribed_only = subscribed_only || is_lsub;
 	s->mbox_op_prev_state = s->state;
 	s->state = SESSION_LISTING;
@@ -1018,8 +992,8 @@ list_dispatch(struct session *s, const char *tag, char
 	memset(&req, 0, sizeof(req));
 	req.subscribed_only = s->list_subscribed_only;
 
-	if (!send_mbox_request(s, IMSG_MBOX_LIST, cmdname, "IMSG_MBOX_LIST",
-	    &req, sizeof(req), NULL, 0, 0)) {
+	if (send_mbox_request(s, IMSG_MBOX_LIST, "IMSG_MBOX_LIST",
+	    &req, sizeof(req), NULL, 0, 0) == -1) {
 		s->state = s->mbox_op_prev_state;
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		return (1);
@@ -1028,31 +1002,28 @@ list_dispatch(struct session *s, const char *tag, char
 	return (1);
 }
 
-
 int
 cmd_list(struct session *s, const char *tag, char *args)
 {
-	return list_dispatch(s, tag, args, 0);
+	return (list_dispatch(s, tag, args, 0));
 }
 
-
 int
 cmd_lsub(struct session *s, const char *tag, char *args)
 {
-	return list_dispatch(s, tag, args, 1);
+	return (list_dispatch(s, tag, args, 1));
 }
 
-/* RFC 9051 SS6.3.10 NAMESPACE */
+/* RFC 9051 section 6.3.10 NAMESPACE */
 int
 cmd_namespace(struct session *s, const char *tag, char *args)
 {
-	(void)args;
 	session_untagged(s, "NAMESPACE ((\"\" \"/\")) NIL NIL");
 	session_reply(s, tag, "OK", "NAMESPACE command completed");
 	return (1);
 }
 
-/* RFC 9051 SS6.3.11 STATUS */
+/* RFC 9051 section 6.3.11 STATUS */
 int
 cmd_status(struct session *s, const char *tag, char *args)
 {
@@ -1107,7 +1078,7 @@ cmd_status(struct session *s, const char *tag, char *a
 		else if (strcasecmp(tok, "HIGHESTMODSEQ") == 0)
 			attrs |= STATUS_ATT_HIGHESTMODSEQ;
 		else if (strcasecmp(tok, "RECENT") == 0)
-			/* RFC 9051 SS2.3.2 dropped it; clients ask */
+			/* RFC 9051 section 2.3.2 dropped it; clients ask */
 			attrs |= STATUS_ATT_RECENT;
 		else {
 			session_reply(s, tag, "BAD", "unknown status-att");
@@ -1115,7 +1086,7 @@ cmd_status(struct session *s, const char *tag, char *a
 		}
 	}
 
-	/* RFC 9051 SS9: at least one status-att */
+	/* RFC 9051 section 9: at least one status-att */
 	if (attrs == 0) {
 		session_reply(s, tag, "BAD",
 		    "STATUS requires at least one status-att");
@@ -1136,7 +1107,7 @@ cmd_status(struct session *s, const char *tag, char *a
 		return (1);
 	}
 
-	/* RFC 7162 SS3.1: STATUS HIGHESTMODSEQ enables CONDSTORE */
+	/* RFC 7162 section 3.1: STATUS HIGHESTMODSEQ enables CONDSTORE */
 	if (attrs & STATUS_ATT_HIGHESTMODSEQ)
 		session_condstore_enable(s);
 
@@ -1155,8 +1126,8 @@ cmd_status(struct session *s, const char *tag, char *a
 	s->status_prev_state = s->state;
 	s->state = SESSION_STATUSING;
 
-	if (!send_mbox_request(s, IMSG_MBOX_STATUS, "STATUS",
-	    "IMSG_MBOX_STATUS", &req, sizeof(req), NULL, 0, 0)) {
+	if (send_mbox_request(s, IMSG_MBOX_STATUS,
+	    "IMSG_MBOX_STATUS", &req, sizeof(req), NULL, 0, 0) == -1) {
 		s->state = s->status_prev_state;
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		return (1);
blob - 328b47f988740d0a5fe9f0f6576e7df5ddcd1803
blob + 5b09cc8f6e152a8199588a67814172051a4eb00c
--- src/main.c
+++ src/main.c
@@ -16,6 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <sys/param.h>
 #include <sys/types.h>
 
 #include <err.h>
@@ -29,7 +30,7 @@
 #include "imapd.h"
 #include "log.h"
 
-__dead static void	 usage(void);
+static __dead void	 usage(void);
 
 static const struct {
 	const char		*name;
@@ -63,11 +64,11 @@ log_procname(enum openimap_proc_type type)
 	}
 }
 
-__dead static void
+static __dead void
 usage(void)
 {
 	fprintf(stderr,
-	    "usage: %s [-dVv] [-D macro=value] [-f file]\n",
+	    "usage: %s [-dnVv] [-D macro=value] [-f file]\n",
 	    getprogname());
 	exit(1);
 }
@@ -76,7 +77,7 @@ int
 main(int argc, char *argv[])
 {
 	int			 ch;
-	int			 debug = 0, verbose = 0;
+	int			 debug = 0, noaction = 0, verbose = 0;
 	const char		*conffile = "/etc/imapd.conf";
 	const char		*rolearg = NULL;
 	enum openimap_proc_type  role = PROC_PARENT;
@@ -88,7 +89,7 @@ main(int argc, char *argv[])
 	/* before any fork(2), so every role inherits SIG_IGN */
 	signal(SIGPIPE, SIG_IGN);
 
-	while ((ch = getopt(argc, argv, "D:df:Vvx:")) != -1) {
+	while ((ch = getopt(argc, argv, "D:df:nVvx:")) != -1) {
 		switch (ch) {
 		case 'V':
 			printf("%s %s\n", getprogname(), IMAPD_VERSION);
@@ -104,8 +105,11 @@ main(int argc, char *argv[])
 		case 'f':
 			conffile = optarg;
 			break;
+		case 'n':
+			noaction = 1;
+			break;
 		case 'v':
-			verbose = 1;
+			verbose++;
 			break;
 		case 'x':
 			rolearg = optarg;
@@ -116,14 +120,13 @@ main(int argc, char *argv[])
 	}
 
 	if (rolearg != NULL) {
-		for (i = 0; i < sizeof(proc_table) / sizeof(proc_table[0]);
-		    i++) {
+		for (i = 0; i < nitems(proc_table); i++) {
 			if (strcmp(rolearg, proc_table[i].name) == 0) {
 				role = proc_table[i].type;
 				break;
 			}
 		}
-		if (i == sizeof(proc_table) / sizeof(proc_table[0]))
+		if (i == nitems(proc_table))
 			usage();
 	}
 
@@ -133,7 +136,12 @@ main(int argc, char *argv[])
 
 	switch (role) {
 	case PROC_PARENT:
-		/* before config_load(): imapd.conf is root-only */
+		if (config_load(conffile, &conf) == -1)
+			fatalx("config_load: %s", conffile);
+		if (noaction) {
+			fprintf(stderr, "configuration OK\n");
+			return (0);
+		}
 		if (geteuid() != 0)
 			fatalx("parent must start as root (running as "
 			    "uid %u)", (unsigned int)geteuid());
@@ -143,8 +151,6 @@ main(int argc, char *argv[])
 			fatalx("unknown user %s", IMAPD_AUTH_USER);
 		if (getpwnam(IMAPD_KEY_USER) == NULL)
 			fatalx("unknown user %s", IMAPD_KEY_USER);
-		if (config_load(conffile, &conf) == -1)
-			fatalx("config_load: %s", conffile);
 		parent_main(conffile, argc, argv, &conf);
 	case PROC_LISTENER:
 		listener_main();
blob - 00ffff1b63663f3599aa5c68bef8d0360370412d
blob + 3097d348370917895540dacc8eb070368430406e
--- src/mbox_copy.c
+++ src/mbox_copy.c
@@ -88,8 +88,7 @@ stage_copy_messages(struct store_session *ss, struct m
 		if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
 		    rec.basename, &size, suffix, sizeof(suffix)) == -1) {
 			log_warnx("session %u: COPY: message %s indexed but "
-			    "missing on disk, failing whole COPY (partial "
-			    "copy not permitted, RFC 9051 SS6.4.7)",
+			    "missing on disk, COPY failed",
 			    session_id, rec.basename);
 			goto fail;
 		}
@@ -121,8 +120,8 @@ stage_copy_messages(struct store_session *ss, struct m
 			goto fail;
 		}
 		if (!index_basename_valid(cs.basename)) {
-			log_warnx("session %u: COPY: generated basename is "
-			    "unsafe for the index, failing COPY", session_id);
+			log_warnx("session %u: COPY: unsafe generated basename",
+			    session_id);
 			goto fail;
 		}
 
@@ -143,9 +142,10 @@ stage_copy_messages(struct store_session *ss, struct m
 		if (nstaged == stagedcap) {
 			size_t		     newcap = (stagedcap == 0) ? 8 :
 			    stagedcap * 2;
-			struct copy_staged  *newstaged = reallocarray(staged,
-			    newcap, sizeof(*staged));
+			struct copy_staged  *newstaged;
 
+			newstaged = reallocarray(staged, newcap,
+			    sizeof(*staged));
 			if (newstaged == NULL) {
 				log_warn("session %u: COPY: reallocarray "
 				    "staged", session_id);
@@ -298,8 +298,7 @@ commit_copy_messages(struct store_session *ss, int dfd
 		if (!index_basename_valid(staged[i].basename) ||
 		    !index_field_valid(staged[i].keywords)) {
 			log_warnx("session %u: COPY: unsafe field in staged "
-			    "message, refusing to update the index",
-			    session_id);
+			    "message", session_id);
 			goto rollback;
 		}
 
@@ -349,7 +348,7 @@ commit_copy_messages(struct store_session *ss, int dfd
 	return (1);
 
 rollback:
-	/* RFC 9051 SS6.4.7: no partial copy */
+	/* RFC 9051 section 6.4.7: no partial copy */
 	while (ncommitted > 0) {
 		char	curpath[320];
 		char	letters[8];
@@ -368,181 +367,173 @@ rollback:
 	return (0);
 }
 
+/* a COPY or MOVE's two mailboxes, the selected one and the destination */
+struct copy_move {
+	struct mbox_index	 idx_a, idx_b;
+	struct mbox_index	*srcidx, *destidx;
+	struct index_lock	 il_a, il_b;
+	struct imsg_mbox_result	 result;
+	char			 desttarget[MBOX_NAME_MAX];
+	int			 destfd;
+	int			 cross_mailbox;
+};
+
+/* 1 with both indexes locked and loaded; 2 lock busy; 0 failed, result set */
 static int
-lock_copy_move_mailboxes(struct store_session *ss, const char *what,
-    const char *destname, int *destfd_out, struct mbox_index *idx_a,
-    struct mbox_index *idx_b, struct mbox_index **srcidx_out,
-    struct mbox_index **destidx_out, char *desttarget, size_t desttargetlen,
-    int *cross_mailbox_out, struct index_lock *il_a, struct index_lock *il_b,
-    struct imsg_mbox_result *result)
+copy_move_lock(struct store_session *ss, const char *what,
+    const char *destname, struct copy_move *cm)
 {
-	int	firstfd, secondfd, first_is_dest, locked;
+	static const struct index_lock	 il_init = INDEX_LOCK_INIT;
+	int				 firstfd, secondfd, first_is_dest;
+	int				 locked;
 
-	if (resolve_mailbox_target(destname, desttarget, desttargetlen) ==
-	    -1) {
+	memset(cm, 0, sizeof(*cm));
+	cm->il_a = il_init;
+	cm->il_b = il_init;
+	cm->destfd = -1;
+
+	if (resolve_mailbox_target(destname, cm->desttarget,
+	    sizeof(cm->desttarget)) == -1) {
 		log_debug("session %u: %s %s: invalid destination mailbox "
 		    "name", session_id, what, destname);
-		result->error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
+		cm->result.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
 		return (0);
 	}
-	*cross_mailbox_out = (strcmp(ss->selected_mailbox, desttarget) != 0);
+	cm->cross_mailbox = (strcmp(ss->selected_mailbox, cm->desttarget) != 0);
 
-	if (!*cross_mailbox_out) {
-		locked = index_lock_acquire(ss->mailbox_dir_fd, il_a,
+	if (!cm->cross_mailbox) {
+		locked = index_lock_acquire(ss->mailbox_dir_fd, &cm->il_a,
 		    LOCK_EX | LOCK_NB);
 		if (locked == 1)
 			return (2);
 		if (locked == -1)
 			return (0);
-		if (index_load(il_a->fd, idx_a) == -1)
+		if (index_load(cm->il_a.fd, &cm->idx_a) == -1)
 			return (0);
-		*srcidx_out = idx_a;
-		*destidx_out = idx_a;
+		cm->srcidx = &cm->idx_a;
+		cm->destidx = &cm->idx_a;
 		return (1);
 	}
 
-	if ((*destfd_out = mailbox_open_dir(desttarget)) == -1) {
+	if ((cm->destfd = mailbox_open_dir(cm->desttarget)) == -1) {
 		log_debug("session %u: %s: no such mailbox %s", session_id,
-		    what, desttarget);
-		result->error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
+		    what, cm->desttarget);
+		cm->result.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
 		return (0);
 	}
 
 	/* both locked at once: take them in name order, against AB-BA */
-	first_is_dest = strcmp(ss->selected_mailbox, desttarget) > 0;
-	firstfd = first_is_dest ? *destfd_out : ss->mailbox_dir_fd;
-	secondfd = first_is_dest ? ss->mailbox_dir_fd : *destfd_out;
+	first_is_dest = strcmp(ss->selected_mailbox, cm->desttarget) > 0;
+	firstfd = first_is_dest ? cm->destfd : ss->mailbox_dir_fd;
+	secondfd = first_is_dest ? ss->mailbox_dir_fd : cm->destfd;
 
-	locked = index_lock_acquire(firstfd, il_a, LOCK_EX | LOCK_NB);
+	locked = index_lock_acquire(firstfd, &cm->il_a, LOCK_EX | LOCK_NB);
 	if (locked != 0)
 		goto fail;
-	if (index_load(il_a->fd, idx_a) == -1)
+	if (index_load(cm->il_a.fd, &cm->idx_a) == -1)
 		goto fail;
-	locked = index_lock_acquire(secondfd, il_b, LOCK_EX | LOCK_NB);
+	locked = index_lock_acquire(secondfd, &cm->il_b, LOCK_EX | LOCK_NB);
 	if (locked != 0)
 		goto fail;
-	if (index_load(il_b->fd, idx_b) == -1)
+	if (index_load(cm->il_b.fd, &cm->idx_b) == -1)
 		goto fail;
 
-	*srcidx_out = first_is_dest ? idx_b : idx_a;
-	*destidx_out = first_is_dest ? idx_a : idx_b;
+	cm->srcidx = first_is_dest ? &cm->idx_b : &cm->idx_a;
+	cm->destidx = first_is_dest ? &cm->idx_a : &cm->idx_b;
 	return (1);
 
 fail:
-	close(*destfd_out);
-	*destfd_out = -1;
+	close(cm->destfd);
+	cm->destfd = -1;
 	if (locked == 1) {
 		/* busy: the first lock goes too, so a re-run starts clean */
-		index_lock_release(il_a);
-		index_free(idx_a);
-		memset(idx_a, 0, sizeof(*idx_a));
+		index_lock_release(&cm->il_a);
+		index_free(&cm->idx_a);
+		memset(&cm->idx_a, 0, sizeof(cm->idx_a));
 		return (2);
 	}
 	return (0);
 }
 
 static void
-finish_copy_move(struct mbox_index *idx_a, struct mbox_index *idx_b,
-    struct index_lock *il_a, struct index_lock *il_b, int ok,
-    struct imsg_mbox_result *result, struct imsgev *iev)
+copy_move_finish(struct copy_move *cm, int ok, struct imsgev *iev)
 {
-	index_lock_release(il_a);
-	index_lock_release(il_b);
+	if (cm->destfd != -1)
+		close(cm->destfd);
+	index_lock_release(&cm->il_a);
+	index_lock_release(&cm->il_b);
 
-	if (result->error != MBOX_OP_ERR_NO_SUCH_MAILBOX &&
-	    result->error != MBOX_OP_ERR_LIMIT &&
-	    result->error != MBOX_OP_ERR_OVERQUOTA)
-		result->error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
-	index_free(idx_a);
-	index_free(idx_b);
+	if (cm->result.error != MBOX_OP_ERR_NO_SUCH_MAILBOX &&
+	    cm->result.error != MBOX_OP_ERR_LIMIT &&
+	    cm->result.error != MBOX_OP_ERR_OVERQUOTA)
+		cm->result.error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
+	index_free(&cm->idx_a);
+	index_free(&cm->idx_b);
 
-	if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, result,
-	    sizeof(*result)) == -1)
+	if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &cm->result,
+	    sizeof(cm->result)) == -1)
 		log_warn("session %u: imsg_compose IMSG_MBOX_RESULT",
 		    session_id);
 }
 
-/* RFC 9051 SS6.4.7 COPY */
-
+/* RFC 9051 section 6.4.7 COPY */
 int
 handle_mbox_copy(struct imsg_mbox_copy *req, const struct seq_range *ranges,
     uint32_t nranges, struct store_session *ss)
 {
-	struct imsgev			*iev = &ss->iev;
-	struct mbox_index		 idx_a, idx_b;
-	struct mbox_index		*srcidx = NULL, *destidx = NULL;
-	struct imsg_mbox_result	 result;
-	struct copy_staged		*staged = NULL;
-	size_t				 nstaged = 0, i;
-	struct index_lock		 il_a = INDEX_LOCK_INIT;
-	struct index_lock		 il_b = INDEX_LOCK_INIT;
-	struct seq_range		 vr[SEQSET_MAX_RANGES];
-	uint32_t			 nvr;
-	int				 ok = 1;
-	char				 desttarget[MBOX_NAME_MAX];
-	int				 cross_mailbox;
-	int				 destfd = -1, dfd, got, used;
+	struct copy_move	 cm;
+	struct copy_staged	*staged = NULL;
+	struct seq_range	 vr[SEQSET_MAX_RANGES];
+	size_t			 nstaged = 0, i;
+	uint32_t		 nvr;
+	int			 ok = 1, dfd, got, used;
 
-	memset(&idx_a, 0, sizeof(idx_a));
-	memset(&idx_b, 0, sizeof(idx_b));
-	memset(&result, 0, sizeof(result));
-
-	got = lock_copy_move_mailboxes(ss, "COPY", req->destname, &destfd,
-	    &idx_a, &idx_b, &srcidx, &destidx, desttarget,
-	    sizeof(desttarget), &cross_mailbox, &il_a, &il_b, &result);
+	got = copy_move_lock(ss, "COPY", req->destname, &cm);
 	if (got == 2)
 		return (1);
 	if (got == 0) {
 		ok = 0;
 		goto done;
 	}
-	dfd = cross_mailbox ? destfd : ss->mailbox_dir_fd;
+	dfd = cm.cross_mailbox ? cm.destfd : ss->mailbox_dir_fd;
 	if (!store_space_ok(dfd)) {
-		result.error = MBOX_OP_ERR_OVERQUOTA;
+		cm.result.error = MBOX_OP_ERR_OVERQUOTA;
 		ok = 0;
-		goto close_dest;
+		goto done;
 	}
-	/* RFC 2180 SS4.4.2: the numbers are those before the EXPUNGEs */
+	/* RFC 2180 section 4.4.2: the numbers are those before the EXPUNGEs */
 	if ((got = saved_resolve(ss, ranges, nranges, vr, &nvr,
 	    &req->by_uid, &used)) != 0) {
 		if (got == -1)
-			result.error = MBOX_OP_ERR_LIMIT;
+			cm.result.error = MBOX_OP_ERR_LIMIT;
 		ok = 0;
-		goto close_dest;
+		goto done;
 	}
-	(void)view_sync(ss, srcidx, 1);
+	(void)view_sync(ss, cm.srcidx, 1);
 
-	if (!stage_copy_messages(ss, srcidx, req, vr, nvr, &staged,
+	if (!stage_copy_messages(ss, cm.srcidx, req, vr, nvr, &staged,
 	    &nstaged)) {
 		ok = 0;
-		goto close_dest;
+		goto done;
 	}
-
 	if (nstaged > 0) {
-		if (ensure_maildir_dirs(dfd, "") == -1) {
+		if (ensure_maildir_dirs(dfd, "") == -1)
 			ok = 0;
-			goto close_dest;
-		}
-		if (!commit_copy_messages(ss, dfd, destidx, staged, nstaged))
+		else if (!commit_copy_messages(ss, dfd, cm.destidx, staged,
+		    nstaged))
 			ok = 0;
 	}
-
 	if (ok) {
-		result.count = (uint32_t)nstaged;
-		result.uidvalidity = destidx->uidvalidity;
-		result.highestmodseq = destidx->highestmodseq;
+		cm.result.count = (uint32_t)nstaged;
+		cm.result.uidvalidity = cm.destidx->uidvalidity;
+		cm.result.highestmodseq = cm.destidx->highestmodseq;
 	}
 
-close_dest:
-	if (destfd != -1)
-		close(destfd);
-
 done:
 	for (i = 0; i < nstaged; i++)
 		free(staged[i].srcpath);
 	free(staged);
-
-	finish_copy_move(&idx_a, &idx_b, &il_a, &il_b, ok, &result, iev);
+	copy_move_finish(&cm, ok, &ss->iev);
 	return (0);
 }
 
@@ -557,8 +548,6 @@ compaction_bail(struct mbox_index *idx, size_t dropped
 	return (out);
 }
 
-/* test the read position, not the write index */
-
 static int
 move_same_mailbox(struct imsg_mbox_copy *req, const struct seq_range *ranges,
     uint32_t nranges, struct mbox_index *idx, uint32_t *nmoved_out,
@@ -610,9 +599,9 @@ move_same_mailbox(struct imsg_mbox_copy *req, const st
 		if (nmoved == movedcap) {
 			size_t		newcap = (movedcap == 0) ? 8 :
 			    movedcap * 2;
-			struct moved   *newmoved = reallocarray(moved, newcap,
-			    sizeof(*moved));
+			struct moved   *newmoved;
 
+			newmoved = reallocarray(moved, newcap, sizeof(*moved));
 			if (newmoved == NULL) {
 				log_warn("session %u: MOVE: reallocarray "
 				    "moved", session_id);
@@ -652,8 +641,7 @@ move_same_mailbox(struct imsg_mbox_copy *req, const st
 		if (!index_basename_valid(moved[i].basename) ||
 		    !index_field_valid(moved[i].keywords)) {
 			log_warnx("session %u: MOVE: unsafe field in moved "
-			    "message, refusing to update the index",
-			    session_id);
+			    "message", session_id);
 			ok = 0;
 			goto done;
 		}
@@ -694,7 +682,6 @@ move_same_mailbox(struct imsg_mbox_copy *req, const st
 		goto done;
 	}
 
-	/* COPYUID mapping first */
 	for (i = 0; i < nmoved; i++) {
 		struct imsg_mbox_copy_mapping	 mapping;
 
@@ -706,7 +693,7 @@ move_same_mailbox(struct imsg_mbox_copy *req, const st
 			log_warn("session %u: imsg_compose "
 			    "IMSG_MBOX_COPY_MAPPING", session_id);
 	}
-	/* then EXPUNGE (RFC 9051 SS6.4.8) */
+	/* RFC 9051 section 6.4.8: COPYUID before EXPUNGE */
 	for (i = 0; i < nmoved; i++) {
 		pcache_drop_uid(ss->selected_mailbox, moved[i].old_uid);
 		(void)send_expunged(ss, moved[i].old_seqno, moved[i].old_uid);
@@ -718,13 +705,13 @@ done:
 	return (ok);
 }
 
-/* RFC 9051 SS6.4.8: dest commits before removal */
-
+/* RFC 9051 section 6.4.8: dest commits before removal */
 static int
 move_cross_mailbox(struct imsg_mbox_copy *req, const struct seq_range *ranges,
-    uint32_t nranges, struct mbox_index *srcidx, struct mbox_index *destidx,
-    int destfd, uint32_t *nmoved_out, struct store_session *ss)
+    uint32_t nranges, struct copy_move *cm, uint32_t *nmoved_out,
+    struct store_session *ss)
 {
+	struct mbox_index	*srcidx = cm->srcidx;
 	struct copy_staged	*staged = NULL;
 	size_t			 nstaged = 0, i;
 	int			 ok = 1, any_removed = 0;
@@ -738,11 +725,12 @@ move_cross_mailbox(struct imsg_mbox_copy *req, const s
 	if (nstaged == 0)
 		return (1);
 
-	if (ensure_maildir_dirs(destfd, "") == -1) {
+	if (ensure_maildir_dirs(cm->destfd, "") == -1) {
 		ok = 0;
 		goto cleanup;
 	}
-	if (!commit_copy_messages(ss, destfd, destidx, staged, nstaged)) {
+	if (!commit_copy_messages(ss, cm->destfd, cm->destidx, staged,
+	    nstaged)) {
 		ok = 0;
 		goto cleanup;
 	}
@@ -785,14 +773,13 @@ move_cross_mailbox(struct imsg_mbox_copy *req, const s
 		(void)send_expunged(ss, old_seqno, staged[i].src_uid);
 	}
 
-	/* RFC 7162 SS3.1 */
+	/* RFC 7162 section 3.1 */
 	if (any_removed)
 		srcidx->highestmodseq++;
 
 	if (index_save(ss->mailbox_dir_fd, srcidx) == -1) {
-		log_warnx("session %u: MOVE: destination commit succeeded "
-		    "but saving the source's compacted index failed, "
-		    "message(s) may remain duplicated", session_id);
+		log_warnx("session %u: MOVE: source index not saved after "
+		    "commit, duplicates possible", session_id);
 		ok = 0;
 		goto cleanup;
 	}
@@ -806,75 +793,49 @@ cleanup:
 	return (ok);
 }
 
-/* RFC 9051 SS6.4.8 MOVE */
-
+/* RFC 9051 section 6.4.8 MOVE */
 int
 handle_mbox_move(struct imsg_mbox_copy *req, const struct seq_range *ranges,
     uint32_t nranges, struct store_session *ss)
 {
-	struct imsgev			*iev = &ss->iev;
-	struct mbox_index		 idx_a, idx_b;
-	struct mbox_index		*srcidx = NULL, *destidx = NULL;
-	struct imsg_mbox_result	 result;
-	uint32_t			 nmoved = 0;
-	struct index_lock		 il_a = INDEX_LOCK_INIT;
-	struct index_lock		 il_b = INDEX_LOCK_INIT;
-	int				 ok = 1;
-	char				 desttarget[MBOX_NAME_MAX];
-	int				 cross_mailbox;
-	int				 destfd = -1, got, used;
-	struct seq_range		 vr[SEQSET_MAX_RANGES];
-	uint32_t			 nvr;
+	struct copy_move	 cm;
+	struct seq_range	 vr[SEQSET_MAX_RANGES];
+	uint32_t		 nmoved = 0, nvr;
+	int			 ok = 1, got, used;
 
-	memset(&idx_a, 0, sizeof(idx_a));
-	memset(&idx_b, 0, sizeof(idx_b));
-	memset(&result, 0, sizeof(result));
-
-	got = lock_copy_move_mailboxes(ss, "MOVE", req->destname, &destfd,
-	    &idx_a, &idx_b, &srcidx, &destidx, desttarget,
-	    sizeof(desttarget), &cross_mailbox, &il_a, &il_b, &result);
+	got = copy_move_lock(ss, "MOVE", req->destname, &cm);
 	if (got == 2)
 		return (1);
 	if (got == 0) {
 		ok = 0;
 		goto done;
 	}
-	if (cross_mailbox && !store_space_ok(destfd)) {
-		result.error = MBOX_OP_ERR_OVERQUOTA;
+	if (cm.cross_mailbox && !store_space_ok(cm.destfd)) {
+		cm.result.error = MBOX_OP_ERR_OVERQUOTA;
 		ok = 0;
-		close(destfd);
 		goto done;
 	}
-
 	if ((got = saved_resolve(ss, ranges, nranges, vr, &nvr,
 	    &req->by_uid, &used)) != 0) {
 		if (got == -1)
-			result.error = MBOX_OP_ERR_LIMIT;
+			cm.result.error = MBOX_OP_ERR_LIMIT;
 		ok = 0;
-		if (destfd != -1)
-			close(destfd);
 		goto done;
 	}
-	(void)view_sync(ss, srcidx, 1);
-	if (!cross_mailbox) {
-		if (!move_same_mailbox(req, vr, nvr, srcidx, &nmoved, ss))
+	(void)view_sync(ss, cm.srcidx, 1);
+	if (!cm.cross_mailbox) {
+		if (!move_same_mailbox(req, vr, nvr, cm.srcidx, &nmoved, ss))
 			ok = 0;
-	} else {
-		if (!move_cross_mailbox(req, vr, nvr, srcidx, destidx,
-		    destfd, &nmoved, ss))
-			ok = 0;
-	}
+	} else if (!move_cross_mailbox(req, vr, nvr, &cm, &nmoved, ss))
+		ok = 0;
 
 	if (ok) {
-		result.count = nmoved;
-		result.uidvalidity = destidx->uidvalidity;
-		result.highestmodseq = destidx->highestmodseq;
+		cm.result.count = nmoved;
+		cm.result.uidvalidity = cm.destidx->uidvalidity;
+		cm.result.highestmodseq = cm.destidx->highestmodseq;
 	}
 
-	if (destfd != -1)
-		close(destfd);
-
 done:
-	finish_copy_move(&idx_a, &idx_b, &il_a, &il_b, ok, &result, iev);
+	copy_move_finish(&cm, ok, &ss->iev);
 	return (0);
 }
blob - 4a85168d6e91df742c5d7a3a265c65d69f6f2598
blob + b7b8bb19580947ffe0a98e1aef02f008ac413bdb
--- src/mbox_fetch.c
+++ src/mbox_fetch.c
@@ -35,10 +35,10 @@
 #include "log.h"
 #include "store_internal.h"
 
-static void	 fetch_walk_step(struct store_session *);
-static void	 fetch_walk_finish(struct store_session *, int);
-static const char *fetch_cached(struct store_session *,
-		    const struct index_rec *, int, int *, uint32_t *);
+static void		 fetch_walk_step(struct store_session *);
+static void		 fetch_walk_finish(struct store_session *, int);
+static const char	*fetch_cached(struct store_session *,
+			    const struct index_rec *, int, int *, uint32_t *);
 
 static void
 fetch_send_part(struct store_session *ss, int imsg_type, const char *what,
@@ -63,7 +63,7 @@ fetch_send_part(struct store_session *ss, int imsg_typ
 	free(combined);
 }
 
-/* RFC 9051 SS6.4.5: BODY[...] sets \Seen, with STORE's own code */
+/* RFC 9051 section 6.4.5: BODY[...] sets \Seen, with STORE's own code */
 static int
 fetch_set_seen(struct store_session *ss, int synced)
 {
@@ -112,8 +112,8 @@ handle_mbox_fetch(struct imsg_mbox_fetch *req, const s
 		return (1);
 
 	if (fw->active) {
-		log_warnx("session %u: FETCH arrived during a paused FETCH, "
-		    "abandoning the earlier walk", session_id);
+		log_warnx("session %u: FETCH during a paused FETCH, earlier "
+		    "walk abandoned", session_id);
 		index_free(&fw->idx);
 	}
 	memset(fw, 0, sizeof(*fw));
@@ -142,7 +142,7 @@ handle_mbox_fetch(struct imsg_mbox_fetch *req, const s
 		fetch_walk_finish(ss, 0);
 		return (0);
 	}
-	/* RFC 9051 SS6.4.9 */
+	/* RFC 9051 section 6.4.9 */
 	fw->nresolved = seqset_resolve(vr, nvr, fw->req.by_uid ?
 	    index_max_uid(idx) : (uint32_t)idx->nlines, !fw->req.by_uid,
 	    fw->resolved);
@@ -159,7 +159,7 @@ handle_mbox_fetch(struct imsg_mbox_fetch *req, const s
 		}
 	}
 
-	/* RFC 7162 SS3.2.6: VANISHED (EARLIER) first */
+	/* RFC 7162 section 3.2.6: VANISHED (EARLIER) first */
 	if (req->by_uid && req->want_vanished) {
 		for (i = 0; i < fw->nresolved; i++)
 			send_vanished_range(idx, fw->resolved[i].lo,
@@ -208,32 +208,261 @@ fetch_cached(struct store_session *ss, const struct in
 	return (text);
 }
 
+static int
+fetch_meta(struct store_session *ss, const struct index_rec *rec, uint32_t i,
+    struct imsg_mbox_fetch_meta *meta, int *have_file)
+{
+	struct store_fetch_walk	*fw = &ss->fetch;
+	struct imsg_mbox_fetch	*req = &fw->req;
+	off_t			 size = 0;
+	char			 suffix[64];
+
+	memset(meta, 0, sizeof(*meta));
+	meta->seqno = view_seqno(ss, rec->uid, i);
+	meta->uid = rec->uid;
+	meta->modseq = rec->modseq;
+	meta->seen_set = fw->seen_modseq != 0 && rec->modseq == fw->seen_modseq;
+
+	if (meta->seen_set ||
+	    (req->attrs & (MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_FLAGS))) {
+		if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+		    rec->basename, &size, suffix, sizeof(suffix)) == -1) {
+			log_warnx("session %u: message %s (uid %u) indexed but "
+			    "missing on disk, skipped", session_id,
+			    rec->basename, meta->uid);
+			return (-1);
+		}
+		*have_file = 1;
+	}
+
+	if (req->attrs & MBOX_FETCH_RFC822_SIZE)
+		meta->size = (uint64_t)size;
+	if ((req->attrs & MBOX_FETCH_FLAGS) || meta->seen_set)
+		build_flags_string(*have_file ? suffix : "", rec->keywords,
+		    meta->flags, sizeof(meta->flags));
+	if (req->attrs & MBOX_FETCH_INTERNALDATE)
+		meta->internaldate = parse_maildir_timestamp(rec->basename);
+	return (0);
+}
+
+static void
+fetch_send_header(struct store_session *ss, const struct index_rec *rec,
+    uint32_t seqno)
+{
+	struct imsg_mbox_fetch		*req = &ss->fetch.req;
+	struct imsg_mbox_fetch_header	 hdrmeta;
+	struct imsg_parser_req		 preq;
+	struct imsg_parser_rep		 prep;
+	char				*hdrbuf = NULL;
+	uint32_t			 hdrlen = 0;
+	int				 mfd, rc;
+
+	memset(&hdrmeta, 0, sizeof(hdrmeta));
+	hdrmeta.seqno = seqno;
+	hdrmeta.uid = rec->uid;
+	if (req->attrs & MBOX_FETCH_BODY_HEADER)
+		rc = read_message_header(&ss->cur_snap, ss->mailbox_dir_fd,
+		    rec->basename, &hdrbuf, &hdrlen);
+	else if ((mfd = open_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+	    rec->basename)) == -1)
+		rc = -1;
+	else {
+		memset(&preq, 0, sizeof(preq));
+		/* same size, and terminated on arrival */
+		(void)strlcpy(preq.fields, req->header_fields,
+		    sizeof(preq.fields));
+		preq.fields_not = req->header_fields_not;
+		rc = parser_request(IMSG_PARSER_HEADER_FIELDS, "HEADER.FIELDS",
+		    mfd, rec->basename, &preq, NULL, 0, FETCH_HEADER_MAX, &prep,
+		    &hdrbuf);
+		hdrlen = prep.len;
+		close(mfd);
+	}
+	if (rc == 0) {
+		hdrmeta.found = 1;
+		hdrmeta.hdrlen = hdrlen;
+	}
+
+	fetch_send_part(ss, IMSG_MBOX_FETCH_HEADER, "IMSG_MBOX_FETCH_HEADER",
+	    &hdrmeta, sizeof(hdrmeta), hdrmeta.found, hdrbuf, hdrlen);
+	free(hdrbuf);
+}
+
+/* WHOLE wins over TEXT and PART; -1 when out of descriptors */
+static int
+fetch_send_body(struct store_session *ss, const struct index_rec *rec,
+    uint32_t seqno)
+{
+	struct store_fetch_walk		*fw = &ss->fetch;
+	struct imsg_mbox_fetch		*req = &fw->req;
+	struct imsg_mbox_fetch_body	 bodymeta;
+	struct imsg_parser_req		 preq;
+	struct imsg_parser_rep		 prep;
+	uint64_t			 off = 0, len = 0;
+	int				 fd = -1, fdbusy = 0;
+	int				 want_whole, want_part, want_text;
+
+	want_whole = (req->attrs & MBOX_FETCH_BODY_WHOLE) != 0;
+	want_part = !want_whole && (req->attrs & MBOX_FETCH_BODY_PART) != 0 &&
+	    !(req->attrs & MBOX_FETCH_BODY_TEXT);
+	want_text = !want_whole && !want_part;
+
+	memset(&bodymeta, 0, sizeof(bodymeta));
+	bodymeta.seqno = seqno;
+	bodymeta.uid = rec->uid;
+
+	if (want_part) {
+		memset(&preq, 0, sizeof(preq));
+		preq.pathlen = parse_section_part(req->section_part, preq.path,
+		    MIME_MAX_DEPTH);
+		if (preq.pathlen != -1 &&
+		    (fd = open_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+		    rec->basename)) == -1)
+			fdbusy = errno == EMFILE || errno == ENFILE;
+		else if (preq.pathlen != -1 &&
+		    parser_request(IMSG_PARSER_PART, "BODY[<part>]", fd,
+		    rec->basename, &preq, NULL, 0, 0, &prep, NULL) == 0) {
+			bodymeta.found = 1;
+			off = prep.part_off;
+			len = prep.part_len;
+		}
+		/* the fd the extent was checked on */
+		if (fd != -1 && (!bodymeta.found || len == 0)) {
+			close(fd);
+			fd = -1;
+		}
+	} else {
+		fd = message_body_range(&ss->cur_snap, ss->mailbox_dir_fd,
+		    rec->basename, want_text, &off, &len, &fdbusy);
+		if (fd != -1)
+			bodymeta.found = 1;
+	}
+
+	/* out of descriptors: retry once sent */
+	if (fdbusy && fw->fds > 0)
+		return (-1);
+
+	if (bodymeta.found) {
+		partial_range(req->has_partial, req->partial_start,
+		    req->partial_count, &off, &len);
+		if (len == 0 && fd != -1) {
+			close(fd);
+			fd = -1;
+		}
+		bodymeta.offset = off;
+		bodymeta.length = len;
+	}
+
+	/* imsg_compose() owns fd only once it succeeds */
+	if (imsg_compose(&ss->iev.ibuf, IMSG_MBOX_FETCH_BODY, 0, 0, fd,
+	    &bodymeta, sizeof(bodymeta)) == -1) {
+		log_warn("session %u: imsg_compose IMSG_MBOX_FETCH_BODY",
+		    session_id);
+		if (fd != -1)
+			close(fd);
+	} else {
+		fw->composed += sizeof(bodymeta);
+		if (fd != -1)
+			fw->fds++;
+	}
+	return (0);
+}
+
+/* a cached ENVELOPE or BODYSTRUCTURE, else the parser's; NULL if neither */
+static const char *
+fetch_parsed_text(struct store_session *ss, const struct index_rec *rec,
+    int item, int *have_file, char **bufp, uint32_t *lenp)
+{
+	struct imsg_parser_rep	 prep;
+	const char		*text, *what;
+	uint32_t		 max;
+	int			 mfd, type;
+
+	*bufp = NULL;
+	if ((text = fetch_cached(ss, rec, item, have_file, lenp)) != NULL)
+		return (text);
+	if ((mfd = open_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+	    rec->basename)) == -1)
+		return (NULL);
+	if (item == PCACHE_ENVELOPE) {
+		type = IMSG_PARSER_ENVELOPE;
+		what = "ENVELOPE";
+		max = ENVELOPE_MAX;
+	} else {
+		type = IMSG_PARSER_BODYSTRUCTURE;
+		what = "BODYSTRUCTURE";
+		max = BODYSTRUCTURE_MAX;
+	}
+	if (parser_request(type, what, mfd, rec->basename, NULL, NULL, 0, max,
+	    &prep, bufp) == 0) {
+		*lenp = prep.len;
+		pcache_put(ss->selected_mailbox, ss->fetch.idx.uidvalidity,
+		    rec->uid, rec->basename, item, *bufp, prep.len);
+		text = *bufp;
+	}
+	close(mfd);
+	return (text);
+}
+
+static void
+fetch_send_envelope(struct store_session *ss, const struct index_rec *rec,
+    uint32_t seqno, int *have_file)
+{
+	struct imsg_mbox_fetch_envelope	 envmeta;
+	const char			*env;
+	char				*envbuf;
+
+	memset(&envmeta, 0, sizeof(envmeta));
+	envmeta.seqno = seqno;
+	envmeta.uid = rec->uid;
+	env = fetch_parsed_text(ss, rec, PCACHE_ENVELOPE, have_file, &envbuf,
+	    &envmeta.envlen);
+	envmeta.found = env != NULL;
+	fetch_send_part(ss, IMSG_MBOX_FETCH_ENVELOPE,
+	    "IMSG_MBOX_FETCH_ENVELOPE", &envmeta, sizeof(envmeta),
+	    envmeta.found, env, envmeta.envlen);
+	free(envbuf);
+}
+
+static void
+fetch_send_bodystructure(struct store_session *ss,
+    const struct index_rec *rec, uint32_t seqno, int *have_file)
+{
+	struct imsg_mbox_fetch_bodystructure	 bsmeta;
+	const char				*bs;
+	char					*bsbuf;
+
+	memset(&bsmeta, 0, sizeof(bsmeta));
+	bsmeta.seqno = seqno;
+	bsmeta.uid = rec->uid;
+	bs = fetch_parsed_text(ss, rec, PCACHE_BODYSTRUCTURE, have_file, &bsbuf,
+	    &bsmeta.bslen);
+	bsmeta.found = bs != NULL;
+	fetch_send_part(ss, IMSG_MBOX_FETCH_BODYSTRUCTURE,
+	    "IMSG_MBOX_FETCH_BODYSTRUCTURE", &bsmeta, sizeof(bsmeta),
+	    bsmeta.found, bs, bsmeta.bslen);
+	free(bsbuf);
+}
+
 /* returns, still active, once FETCH_BATCH_MAX is queued */
 static void
 fetch_walk_step(struct store_session *ss)
 {
-	struct store_fetch_walk	*fw = &ss->fetch;
-	struct mbox_index	*idx = &fw->idx;
-	struct imsg_mbox_fetch	*req = &fw->req;
-	struct imsgev		*iev = &ss->iev;
-	struct seq_range	*resolved = fw->resolved;
-	uint32_t		 nresolved = fw->nresolved;
-	uint32_t		 max_hi = fw->max_hi;
-	uint32_t		 i;
+	struct store_fetch_walk		*fw = &ss->fetch;
+	struct mbox_index		*idx = &fw->idx;
+	struct imsg_mbox_fetch		*req = &fw->req;
+	struct imsg_mbox_fetch_meta	 meta;
+	struct index_rec		 rec;
+	enum seqset_pos			 pos;
+	uint32_t			 i;
+	int				 have_file, ready;
 
 	for (i = fw->next; i <= (uint32_t)idx->nlines; i++) {
-		struct imsg_mbox_fetch_meta	 meta;
-		struct index_rec		 rec;
-		enum seqset_pos			 pos;
-		off_t				 size = 0;
-		char				 suffix[64];
-		int				 have_file = 0;
-
 		if (index_parse_line(idx->lines[i - 1], &rec) == -1)
 			continue;
 
-		pos = seqset_position(resolved, nresolved, max_hi, req->by_uid,
-		    rec.uid, i);
+		pos = seqset_position(fw->resolved, fw->nresolved, fw->max_hi,
+		    req->by_uid, rec.uid, i);
 		if (pos == SEQSET_PAST_END)
 			break;
 		if (pos == SEQSET_SKIP)
@@ -244,9 +473,7 @@ fetch_walk_step(struct store_session *ss)
 
 		if (!fw->no_parser && fetch_needs_parser(req,
 		    ss->selected_mailbox, idx->uidvalidity, &rec)) {
-			int	 ready = parser_ready();
-
-			if (ready == 0) {
+			if ((ready = parser_ready()) == 0) {
 				fw->next = i;
 				fw->wait_parser = 1;
 				return;
@@ -255,237 +482,29 @@ fetch_walk_step(struct store_session *ss)
 				fw->no_parser = 1;
 		}
 
-		memset(&meta, 0, sizeof(meta));
-		meta.seqno = view_seqno(ss, rec.uid, i);
-		meta.uid = rec.uid;
-		meta.modseq = rec.modseq;
-		meta.seen_set = fw->seen_modseq != 0 &&
-		    rec.modseq == fw->seen_modseq;
+		have_file = 0;
+		if (fetch_meta(ss, &rec, i, &meta, &have_file) == -1)
+			continue;
 
-		if (meta.seen_set || (req->attrs &
-		    (MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_FLAGS))) {
-			if (locate_message_file(&ss->cur_snap,
-			    ss->mailbox_dir_fd, rec.basename, &size, suffix,
-			    sizeof(suffix)) == -1) {
-				log_warnx("session %u: message %s (uid %u) "
-				    "indexed but missing on disk, skipped",
-				    session_id, rec.basename, meta.uid);
-				continue;
-			}
-			have_file = 1;
-		}
-
-		if (req->attrs & MBOX_FETCH_RFC822_SIZE)
-			meta.size = (uint64_t)size;
-		if ((req->attrs & MBOX_FETCH_FLAGS) || meta.seen_set)
-			build_flags_string(have_file ? suffix : "",
-			    rec.keywords, meta.flags, sizeof(meta.flags));
-		if (req->attrs & MBOX_FETCH_INTERNALDATE)
-			meta.internaldate = parse_maildir_timestamp(
-			    rec.basename);
-
 		/* FETCH_HEADER precedes FETCH_META */
 		if (req->attrs & (MBOX_FETCH_BODY_HEADER |
-		    MBOX_FETCH_HEADER_FIELDS)) {
-			struct imsg_mbox_fetch_header	 hdrmeta;
-			struct imsg_parser_req		 preq;
-			struct imsg_parser_rep		 prep;
-			char				*hdrbuf = NULL;
-			uint32_t			 hdrlen = 0;
-			int				 mfd, rc;
+		    MBOX_FETCH_HEADER_FIELDS))
+			fetch_send_header(ss, &rec, meta.seqno);
 
-			memset(&hdrmeta, 0, sizeof(hdrmeta));
-			hdrmeta.seqno = view_seqno(ss, rec.uid, i);
-			hdrmeta.uid = rec.uid;
-			if (req->attrs & MBOX_FETCH_BODY_HEADER)
-				rc = read_message_header(&ss->cur_snap,
-				    ss->mailbox_dir_fd, rec.basename, &hdrbuf,
-				    &hdrlen);
-			else if ((mfd = open_message_file(&ss->cur_snap,
-			    ss->mailbox_dir_fd, rec.basename)) == -1)
-				rc = -1;
-			else {
-				memset(&preq, 0, sizeof(preq));
-				/* same size, and terminated on arrival */
-				(void)strlcpy(preq.fields, req->header_fields,
-				    sizeof(preq.fields));
-				preq.fields_not = req->header_fields_not;
-				rc = parser_request(IMSG_PARSER_HEADER_FIELDS,
-				    "HEADER.FIELDS", mfd, rec.basename, &preq,
-				    NULL, 0, FETCH_HEADER_MAX, &prep, &hdrbuf);
-				hdrlen = prep.len;
-				close(mfd);
-			}
-			if (rc == 0) {
-				hdrmeta.found = 1;
-				hdrmeta.hdrlen = hdrlen;
-			}
-
-			fetch_send_part(ss, IMSG_MBOX_FETCH_HEADER,
-			    "IMSG_MBOX_FETCH_HEADER", &hdrmeta, sizeof(hdrmeta),
-			    hdrmeta.found, hdrbuf, hdrlen);
-			free(hdrbuf);
+		if ((req->attrs & (MBOX_FETCH_BODY_WHOLE |
+		    MBOX_FETCH_BODY_TEXT | MBOX_FETCH_BODY_PART)) &&
+		    fetch_send_body(ss, &rec, meta.seqno) == -1) {
+			fw->next = i;
+			return;
 		}
 
-		/* WHOLE wins over TEXT and PART */
-		if (req->attrs & (MBOX_FETCH_BODY_WHOLE | MBOX_FETCH_BODY_TEXT |
-		    MBOX_FETCH_BODY_PART)) {
-			struct imsg_mbox_fetch_body	 bodymeta;
-			uint64_t			 off = 0, len = 0;
-			int				 fd = -1, fdbusy = 0;
-			int				 want_whole =
-			    (req->attrs & MBOX_FETCH_BODY_WHOLE) != 0;
-			int				 want_part =
-			    !want_whole &&
-			    (req->attrs & MBOX_FETCH_BODY_PART) != 0 &&
-			    !(req->attrs & MBOX_FETCH_BODY_TEXT);
-			int				 want_text =
-			    !want_whole &&
-			    !want_part;
+		if (req->attrs & MBOX_FETCH_ENVELOPE)
+			fetch_send_envelope(ss, &rec, meta.seqno, &have_file);
+		if (req->attrs & MBOX_FETCH_BODYSTRUCTURE)
+			fetch_send_bodystructure(ss, &rec, meta.seqno,
+			    &have_file);
 
-			memset(&bodymeta, 0, sizeof(bodymeta));
-			bodymeta.seqno = view_seqno(ss, rec.uid, i);
-			bodymeta.uid = rec.uid;
-
-			if (want_part) {
-				struct imsg_parser_req	 preq;
-				struct imsg_parser_rep	 prep;
-
-				memset(&preq, 0, sizeof(preq));
-				preq.pathlen = parse_section_part(
-				    req->section_part, preq.path,
-				    MIME_MAX_DEPTH);
-				if (preq.pathlen != -1 &&
-				    (fd = open_message_file(&ss->cur_snap,
-				    ss->mailbox_dir_fd, rec.basename)) == -1)
-					fdbusy = errno == EMFILE ||
-					    errno == ENFILE;
-				else if (preq.pathlen != -1 &&
-				    parser_request(IMSG_PARSER_PART,
-				    "BODY[<part>]", fd, rec.basename, &preq,
-				    NULL, 0, 0, &prep, NULL) == 0) {
-					bodymeta.found = 1;
-					off = prep.part_off;
-					len = prep.part_len;
-				}
-				/* the fd the extent was checked on */
-				if (fd != -1 && (!bodymeta.found || len == 0)) {
-					close(fd);
-					fd = -1;
-				}
-			} else {
-				fd = message_body_range(&ss->cur_snap,
-				    ss->mailbox_dir_fd, rec.basename, want_text,
-				    &off, &len, &fdbusy);
-				if (fd != -1)
-					bodymeta.found = 1;
-			}
-
-			/* out of descriptors: retry once sent */
-			if (fdbusy && fw->fds > 0) {
-				fw->next = i;
-				return;
-			}
-
-			if (bodymeta.found) {
-				partial_range(req->has_partial,
-				    req->partial_start, req->partial_count,
-				    &off, &len);
-				if (len == 0 && fd != -1) {
-					close(fd);
-					fd = -1;
-				}
-				bodymeta.offset = off;
-				bodymeta.length = len;
-			}
-
-			/* imsg_compose() owns fd only once it succeeds */
-			if (imsg_compose(&iev->ibuf, IMSG_MBOX_FETCH_BODY, 0, 0,
-			    fd, &bodymeta, sizeof(bodymeta)) == -1) {
-				log_warn("session %u: imsg_compose "
-				    "IMSG_MBOX_FETCH_BODY", session_id);
-				if (fd != -1)
-					close(fd);
-			} else {
-				fw->composed += sizeof(bodymeta);
-				if (fd != -1)
-					fw->fds++;
-			}
-		}
-
-		if (req->attrs & MBOX_FETCH_ENVELOPE) {
-			struct imsg_mbox_fetch_envelope	 envmeta;
-			struct imsg_parser_rep		 prep;
-			const char				*env;
-			char					*envbuf = NULL;
-			int					 mfd;
-
-			memset(&envmeta, 0, sizeof(envmeta));
-			envmeta.seqno = view_seqno(ss, rec.uid, i);
-			envmeta.uid = rec.uid;
-			if ((env = fetch_cached(ss, &rec, PCACHE_ENVELOPE,
-			    &have_file, &envmeta.envlen)) != NULL)
-				envmeta.found = 1;
-			else if ((mfd = open_message_file(&ss->cur_snap,
-			    ss->mailbox_dir_fd, rec.basename)) != -1) {
-				if (parser_request(IMSG_PARSER_ENVELOPE,
-				    "ENVELOPE", mfd, rec.basename, NULL, NULL,
-				    0, ENVELOPE_MAX, &prep, &envbuf) == 0) {
-					envmeta.found = 1;
-					envmeta.envlen = prep.len;
-					env = envbuf;
-					pcache_put(ss->selected_mailbox,
-					    idx->uidvalidity, rec.uid,
-					    rec.basename, PCACHE_ENVELOPE,
-					    envbuf, prep.len);
-				}
-				close(mfd);
-			}
-
-			fetch_send_part(ss, IMSG_MBOX_FETCH_ENVELOPE,
-			    "IMSG_MBOX_FETCH_ENVELOPE", &envmeta,
-			    sizeof(envmeta),
-			    envmeta.found, env, envmeta.envlen);
-			free(envbuf);
-		}
-
-		if (req->attrs & MBOX_FETCH_BODYSTRUCTURE) {
-			struct imsg_mbox_fetch_bodystructure	 bsmeta;
-			struct imsg_parser_rep			 prep;
-			const char				*bs;
-			char					*bsbuf = NULL;
-			int					 mfd;
-
-			memset(&bsmeta, 0, sizeof(bsmeta));
-			bsmeta.seqno = view_seqno(ss, rec.uid, i);
-			bsmeta.uid = rec.uid;
-			if ((bs = fetch_cached(ss, &rec, PCACHE_BODYSTRUCTURE,
-			    &have_file, &bsmeta.bslen)) != NULL)
-				bsmeta.found = 1;
-			else if ((mfd = open_message_file(&ss->cur_snap,
-			    ss->mailbox_dir_fd, rec.basename)) != -1) {
-				if (parser_request(IMSG_PARSER_BODYSTRUCTURE,
-				    "BODYSTRUCTURE", mfd, rec.basename, NULL,
-				    NULL, 0, BODYSTRUCTURE_MAX, &prep,
-				    &bsbuf) == 0) {
-					bsmeta.found = 1;
-					bsmeta.bslen = prep.len;
-					bs = bsbuf;
-					pcache_put(ss->selected_mailbox,
-					    idx->uidvalidity, rec.uid,
-					    rec.basename, PCACHE_BODYSTRUCTURE,
-					    bsbuf, prep.len);
-				}
-				close(mfd);
-			}
-
-			fetch_send_part(ss, IMSG_MBOX_FETCH_BODYSTRUCTURE,
-			    "IMSG_MBOX_FETCH_BODYSTRUCTURE", &bsmeta,
-			    sizeof(bsmeta), bsmeta.found, bs, bsmeta.bslen);
-			free(bsbuf);
-		}
-
-		if (imsg_compose(&iev->ibuf, IMSG_MBOX_FETCH_META, 0, 0, -1,
+		if (imsg_compose(&ss->iev.ibuf, IMSG_MBOX_FETCH_META, 0, 0, -1,
 		    &meta, sizeof(meta)) == -1)
 			log_warn("session %u: imsg_compose "
 			    "IMSG_MBOX_FETCH_META", session_id);
@@ -607,7 +626,7 @@ sysflags_to_letters(uint32_t sysflags, char *out, size
 	out[i] = '\0';
 }
 
-/* RFC 9051 SS6.3.11 STATUS */
+/* RFC 9051 section 6.3.11 STATUS */
 int
 handle_mbox_status(struct imsg_mbox_status *req, struct store_session *ss)
 {
@@ -679,9 +698,8 @@ handle_mbox_status(struct imsg_mbox_status *req, struc
 			    rec.basename, &size, suffix,
 			    sizeof(suffix)) == -1) {
 				log_warnx("session %u: message %s indexed but "
-				    "missing on disk, skipped for STATUS "
-				    "UNSEEN/DELETED/SIZE", session_id,
-				    rec.basename);
+				    "missing on disk, skipped (STATUS)",
+				    session_id, rec.basename);
 				continue;
 			}
 
blob - 87c2870025ee974842c127c02c638b19577c9cda
blob + 191a3b187becea845e6322a2b81e4de3a08380f2
--- src/mbox_manage.c
+++ src/mbox_manage.c
@@ -17,7 +17,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
+#include <sys/param.h>
 #include <sys/types.h>
 #include <sys/file.h>
 #include <sys/mount.h>
@@ -43,12 +43,12 @@ int
 handle_mbox_select(struct imsg_mbox_select *req,
     const struct seq_range *ranges, uint32_t nranges, struct store_session *ss)
 {
-	struct imsgev		*iev = &ss->iev;
-	struct mbox_index	 idx;
-	struct imsg_mbox_selected reply;
-	struct index_lock	 il = INDEX_LOCK_INIT;
-	const char		*target;
-	int			 dfd, locked;
+	struct imsgev			*iev = &ss->iev;
+	struct mbox_index		 idx;
+	struct imsg_mbox_selected	 reply;
+	struct index_lock		 il = INDEX_LOCK_INIT;
+	const char			*target;
+	int				 dfd, locked;
 
 	memset(&reply, 0, sizeof(reply));
 
@@ -101,16 +101,7 @@ handle_mbox_select(struct imsg_mbox_select *req,
 		close(ss->mailbox_dir_fd);
 	ss->mailbox_dir_fd = dfd;
 
-	if (strlcpy(ss->selected_mailbox, target,
-	    sizeof(ss->selected_mailbox)) >= sizeof(ss->selected_mailbox)) {
-		log_warnx("session %u: SELECT %s: name too long to "
-		    "record, can't happen (validated above, same-size "
-		    "buffers)", session_id, req->mailbox);
-		reply.error = MBOX_OP_ERR_GENERIC;
-		index_free(&idx);
-		index_lock_release(&il);
-		goto send;
-	}
+	strlcpy(ss->selected_mailbox, target, sizeof(ss->selected_mailbox));
 
 	reply.error = MBOX_OP_OK;
 	ss->mailbox_selected = 1;
@@ -142,7 +133,7 @@ ensure_maildir_dirs(int dfd, const char *prefix)
 	char	 path[MBOX_NAME_MAX + 8];
 	size_t	 i;
 
-	for (i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) {
+	for (i = 0; i < nitems(dirs); i++) {
 		if (snprintf(path, sizeof(path), "%s%s", prefix, dirs[i]) >=
 		    (int)sizeof(path)) {
 			log_warnx("session %u: mailbox path too long",
@@ -157,7 +148,6 @@ ensure_maildir_dirs(int dfd, const char *prefix)
 	return (0);
 }
 
-
 #define STORE_MINSPACE		5
 #define STORE_MININODES		5
 
@@ -176,25 +166,22 @@ store_space_ok(int fd)
 	avail = sfs.f_bavail > 0 ? (uint64_t)sfs.f_bavail : 0;
 	if (sfs.f_blocks > 0 &&
 	    avail * 100 < (avail + used) * STORE_MINSPACE) {
-		log_warnx("session %u: less than %d%% of the mail store's "
-		    "space is free, refusing to add to it", session_id,
-		    STORE_MINSPACE);
+		log_warnx("session %u: under %d%% of the store's space free, "
+		    "refused", session_id, STORE_MINSPACE);
 		return (0);
 	}
 	used = sfs.f_files - sfs.f_ffree;
 	avail = sfs.f_favail > 0 ? (uint64_t)sfs.f_favail : 0;
 	if (sfs.f_files > 0 &&
 	    avail * 100 < (avail + used) * STORE_MININODES) {
-		log_warnx("session %u: less than %d%% of the mail store's "
-		    "inodes are free, refusing to add to it", session_id,
-		    STORE_MININODES);
+		log_warnx("session %u: under %d%% of the store's inodes free, "
+		    "refused", session_id, STORE_MININODES);
 		return (0);
 	}
 	return (1);
 }
 
-/* RFC 9051 SS6.3.4 CREATE */
-
+/* RFC 9051 section 6.3.4 CREATE */
 void
 handle_mbox_create(struct imsg_mbox_create *req, struct imsgev *iev)
 {
@@ -225,7 +212,7 @@ handle_mbox_create(struct imsg_mbox_create *req, struc
 		} else {
 			log_debug("session %u: CREATE %s: already exists",
 			    session_id, req->mailbox);
-			/* RFC 5530 SS3 ALREADYEXISTS */
+			/* RFC 5530 section 3 ALREADYEXISTS */
 			result.error = MBOX_OP_ERR_ALREADY_EXISTS;
 		}
 		goto send;
@@ -251,12 +238,18 @@ send:
 static int
 remove_maildir_subtree(int dfd, const char *prefix)
 {
-	static const char *dirs[] = { "tmp", "new", "cur" };
+	static const char	*dirs[] = { "tmp", "new", "cur" };
+	/* every file created here must go before rmdir(2) */
+	static const char	*files[] = {
+		STORE_INDEX_NAME,
+		STORE_INDEX_TMP_NAME,
+		STORE_INDEX_LOCK_NAME,
+	};
 	/* room for STORE_INDEX_NAME after the longest name */
-	char	path[MBOX_NAME_MAX + 32];
-	size_t	i;
+	char			 path[MBOX_NAME_MAX + 32];
+	size_t			 i;
 
-	for (i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) {
+	for (i = 0; i < nitems(dirs); i++) {
 		DIR			*dp;
 		const struct dirent	*de;
 		int			 ok = 1;
@@ -267,15 +260,7 @@ remove_maildir_subtree(int dfd, const char *prefix)
 			    session_id);
 			return (-1);
 		}
-		{
-			int	subfd;
-
-			dp = NULL;
-			subfd = openat(dfd, path, O_RDONLY | O_DIRECTORY);
-			if (subfd != -1 && (dp = fdopendir(subfd)) == NULL)
-				close(subfd);
-		}
-		if (dp == NULL) {
+		if ((dp = opendirat(dfd, path)) == NULL) {
 			if (errno == ENOENT)
 				/* tmp/ may never have been created */
 				continue;
@@ -305,9 +290,8 @@ remove_maildir_subtree(int dfd, const char *prefix)
 				continue;
 			}
 			if (!S_ISREG(st.st_mode)) {
-				log_warnx("session %u: DELETE: refusing, "
-				    "%s is not a regular file", session_id,
-				    entpath);
+				log_warnx("session %u: DELETE %s: not a "
+				    "regular file", session_id, entpath);
 				ok = 0;
 				continue;
 			}
@@ -328,35 +312,24 @@ remove_maildir_subtree(int dfd, const char *prefix)
 		}
 	}
 
-	/* every file created here must go before rmdir(2) */
-	{
-		static const char *files[] = {
-			STORE_INDEX_NAME,
-			STORE_INDEX_TMP_NAME,
-			STORE_INDEX_LOCK_NAME,
-		};
-		size_t	f;
-
-		for (f = 0; f < sizeof(files) / sizeof(files[0]); f++) {
-			if (snprintf(path, sizeof(path), "%s%s", prefix,
-			    files[f]) >= (int)sizeof(path)) {
-				log_warnx("session %u: DELETE: index path too "
-				    "long", session_id);
-				return (-1);
-			}
-			if (unlinkat(dfd, path, 0) == -1 && errno != ENOENT) {
-				log_warn("session %u: DELETE: unlink %s",
-				    session_id, path);
-				return (-1);
-			}
+	for (i = 0; i < nitems(files); i++) {
+		if (snprintf(path, sizeof(path), "%s%s", prefix, files[i]) >=
+		    (int)sizeof(path)) {
+			log_warnx("session %u: DELETE: index path too long",
+			    session_id);
+			return (-1);
 		}
+		if (unlinkat(dfd, path, 0) == -1 && errno != ENOENT) {
+			log_warn("session %u: DELETE: unlink %s", session_id,
+			    path);
+			return (-1);
+		}
 	}
 
 	return (0);
 }
 
-/* RFC 9051 SS6.3.5 DELETE */
-
+/* RFC 9051 section 6.3.5 DELETE */
 void
 handle_mbox_delete(struct imsg_mbox_delete *req, struct store_session *ss)
 {
@@ -375,12 +348,11 @@ handle_mbox_delete(struct imsg_mbox_delete *req, struc
 		goto send;
 	}
 
-
 	if (fstatat(maildir_root_fd, req->mailbox, &st,
 	    AT_SYMLINK_NOFOLLOW) == -1 || !S_ISDIR(st.st_mode)) {
 		log_debug("session %u: DELETE %s: no such mailbox",
 		    session_id, req->mailbox);
-		/* RFC 5530 SS3 NONEXISTENT */
+		/* RFC 5530 section 3 NONEXISTENT */
 		result.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
 		goto send;
 	}
@@ -418,8 +390,7 @@ send:
 		    session_id);
 }
 
-/* RFC 9051 SS6.3.6 RENAME; INBOX is the root and cannot move */
-
+/* RFC 9051 section 6.3.6 RENAME; INBOX is the root and cannot move */
 void
 handle_mbox_rename(struct imsg_mbox_rename *req, struct store_session *ss)
 {
@@ -439,22 +410,21 @@ handle_mbox_rename(struct imsg_mbox_rename *req, struc
 		goto send;
 	}
 
-
 	if (fstatat(maildir_root_fd, req->oldname, &st,
 	    AT_SYMLINK_NOFOLLOW) == -1 || !S_ISDIR(st.st_mode)) {
 		log_debug("session %u: RENAME %s: no such mailbox",
 		    session_id, req->oldname);
-		/* RFC 5530 SS3 NONEXISTENT */
+		/* RFC 5530 section 3 NONEXISTENT */
 		result.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
 		goto send;
 	}
 
-	/* RFC 9051 SS6.3.6: error to rename onto an existing name */
+	/* RFC 9051 section 6.3.6: error to rename onto an existing name */
 	if (fstatat(maildir_root_fd, req->newname, &st,
 	    AT_SYMLINK_NOFOLLOW) == 0 || errno != ENOENT) {
 		log_debug("session %u: RENAME %s -> %s: destination exists",
 		    session_id, req->oldname, req->newname);
-		/* RFC 5530 SS3 ALREADYEXISTS */
+		/* RFC 5530 section 3 ALREADYEXISTS */
 		result.error = MBOX_OP_ERR_ALREADY_EXISTS;
 		goto send;
 	}
@@ -473,14 +443,9 @@ send:
 	if (result.error == MBOX_OP_OK)
 		pcache_drop_mailbox(req->oldname);
 	if (result.error == MBOX_OP_OK &&
-	    strcmp(ss->selected_mailbox, req->oldname) == 0) {
-		if (strlcpy(ss->selected_mailbox, req->newname,
-		    sizeof(ss->selected_mailbox)) >=
-		    sizeof(ss->selected_mailbox))
-			log_warnx("session %u: RENAME %s -> %s: new name too "
-			    "long to record, can't happen (validated)",
-			    session_id, req->oldname, req->newname);
-	}
+	    strcmp(ss->selected_mailbox, req->oldname) == 0)
+		strlcpy(ss->selected_mailbox, req->newname,
+		    sizeof(ss->selected_mailbox));
 	if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result,
 	    sizeof(result)) == -1)
 		log_warn("session %u: imsg_compose IMSG_MBOX_RESULT",
@@ -593,19 +558,15 @@ sublist_remove(struct sublist *sl, const char *name)
 static int
 sublist_load(struct sublist *sl)
 {
-	FILE	*fp;
-	char	 line[MBOX_NAME_MAX + 1];
+	FILE	*fp = NULL;
+	char	*line = NULL;
+	size_t	 linesize = 0;
+	int	 fd;
 
 	sl->present = 0;
-	{
-		int	fd;
-
-		fp = NULL;
-		fd = openat(maildir_root_fd, STORE_SUBSCRIPTIONS_NAME,
-		    O_RDONLY);
-		if (fd != -1 && (fp = fdopen(fd, "r")) == NULL)
-			close(fd);
-	}
+	fd = openat(maildir_root_fd, STORE_SUBSCRIPTIONS_NAME, O_RDONLY);
+	if (fd != -1 && (fp = fdopen(fd, "r")) == NULL)
+		close(fd);
 	if (fp == NULL) {
 		if (errno == ENOENT)
 			return (0);
@@ -615,28 +576,15 @@ sublist_load(struct sublist *sl)
 	}
 	sl->present = 1;
 
-	while (fgets(line, sizeof(line), fp) != NULL) {
-		/* fgets(3) splits an over-long line */
-		if (strchr(line, '\n') == NULL &&
-		    strlen(line) == sizeof(line) - 1) {
-			int	c = fgetc(fp);
-
-			if (c != EOF && c != '\n') {
-				log_warnx("session %u: over-long line in %s, "
-				    "refusing to parse it", session_id,
-				    STORE_SUBSCRIPTIONS_NAME);
-				goto fail;
-			}
-		}
+	while (getline(&line, &linesize, fp) != -1) {
 		line[strcspn(line, "\n")] = '\0';
 		if (line[0] == '\0')
 			continue;
 
 		/* a name SUBSCRIBE would refuse: the file was hand-edited */
 		if (!mailbox_name_syntax_ok(line)) {
-			log_warnx("session %u: unusable name in %s, refusing "
-			    "to parse it", session_id,
-			    STORE_SUBSCRIPTIONS_NAME);
+			log_warnx("session %u: %s: unusable name, not parsed",
+			    session_id, STORE_SUBSCRIPTIONS_NAME);
 			goto fail;
 		}
 		if (sublist_has(sl, line))
@@ -649,10 +597,12 @@ sublist_load(struct sublist *sl)
 		    STORE_SUBSCRIPTIONS_NAME);
 		goto fail;
 	}
+	free(line);
 	fclose(fp);
 	return (0);
 
 fail:
+	free(line);
 	fclose(fp);
 	sublist_free(sl);
 	sl->present = 0;
@@ -763,16 +713,7 @@ subs_materialise(struct sublist *sl)
 	struct dirent	*de;
 	int		 badname;
 
-	{
-		int	rootfd;
-
-		dp = NULL;
-		rootfd = openat(maildir_root_fd, ".",
-		    O_RDONLY | O_DIRECTORY);
-		if (rootfd != -1 && (dp = fdopendir(rootfd)) == NULL)
-			close(rootfd);
-	}
-	if (dp == NULL) {
+	if ((dp = opendirat(maildir_root_fd, ".")) == NULL) {
 		log_warn("session %u: UNSUBSCRIBE: opendir .", session_id);
 		return (-1);
 	}
@@ -795,13 +736,7 @@ list_item_send(struct imsgev *iev, const char *name, i
 	struct imsg_mbox_list_item	item;
 
 	memset(&item, 0, sizeof(item));
-	if (strlcpy(item.mailbox, name, sizeof(item.mailbox)) >=
-	    sizeof(item.mailbox)) {
-		log_warnx("session %u: LIST: %s truncated, can't happen (the "
-		    "name rule already bounds it under MBOX_NAME_MAX)",
-		    session_id, name);
-		return (0);
-	}
+	strlcpy(item.mailbox, name, sizeof(item.mailbox));
 	item.exists = exists;
 	if (imsg_compose(&iev->ibuf, IMSG_MBOX_LIST_ITEM, 0, 0, -1, &item,
 	    sizeof(item)) == -1) {
@@ -812,8 +747,7 @@ list_item_send(struct imsgev *iev, const char *name, i
 	return (1);
 }
 
-/* RFC 9051 SS6.3.9 LIST; INBOX is not sent */
-
+/* RFC 9051 section 6.3.9 LIST; INBOX is not sent */
 void
 handle_mbox_list(struct imsg_mbox_list *req, struct imsgev *iev)
 {
@@ -827,20 +761,10 @@ handle_mbox_list(struct imsg_mbox_list *req, struct im
 
 	memset(&result, 0, sizeof(result));
 
-
 	if (req->subscribed_only && sublist_load(&sl) == -1)
 		sl.present = 0;
 
-	{
-		int	rootfd;
-
-		dp = NULL;
-		rootfd = openat(maildir_root_fd, ".",
-		    O_RDONLY | O_DIRECTORY);
-		if (rootfd != -1 && (dp = fdopendir(rootfd)) == NULL)
-			close(rootfd);
-	}
-	if (dp == NULL) {
+	if ((dp = opendirat(maildir_root_fd, ".")) == NULL) {
 		log_warn("session %u: LIST: opendir .", session_id);
 		result.error = MBOX_OP_ERR_GENERIC;
 		goto send;
@@ -851,12 +775,9 @@ handle_mbox_list(struct imsg_mbox_list *req, struct im
 			/* a rejected name hides real mail: say so */
 			if (badname && !skip_warned) {
 				skip_warned = 1;
-				log_warnx("session %u: LIST: skipping %s: "
-				    "not a valid mailbox name (RFC 9051 "
-				    "SS5.1); it will not be listed or "
-				    "selectable. Further skips this "
-				    "connection are not logged", session_id,
-				    de->d_name);
+				log_warnx("session %u: LIST: %s: invalid "
+				    "mailbox name, skipped",
+				    session_id, de->d_name);
 			}
 			continue;
 		}
@@ -867,7 +788,7 @@ handle_mbox_list(struct imsg_mbox_list *req, struct im
 	}
 	closedir(dp);
 
-	/* RFC 9051 SS6.3.9.6: subscribed names with no mailbox */
+	/* RFC 9051 section 6.3.9.6: subscribed names with no mailbox */
 	for (i = 0; i < sl.n; i++) {
 		struct stat	st;
 
@@ -887,8 +808,7 @@ send:
 		    session_id);
 }
 
-/* RFC 9051 SS6.3.7 SUBSCRIBE; idempotent */
-
+/* RFC 9051 section 6.3.7 SUBSCRIBE; idempotent */
 void
 handle_mbox_subscribe(struct imsg_mbox_subscribe *req, struct imsgev *iev)
 {
@@ -910,8 +830,7 @@ handle_mbox_subscribe(struct imsg_mbox_subscribe *req,
 		goto send;
 	}
 
-
-	/* RFC 9051 SS6.3.7 lets a server require the mailbox exist */
+	/* RFC 9051 section 6.3.7 lets a server require the mailbox exist */
 	if (fstatat(maildir_root_fd, req->mailbox, &st,
 	    AT_SYMLINK_NOFOLLOW) == -1 || !S_ISDIR(st.st_mode)) {
 		log_debug("session %u: SUBSCRIBE %s: no such mailbox",
@@ -945,8 +864,7 @@ send:
 		    session_id);
 }
 
-/* RFC 9051 SS6.3.8 UNSUBSCRIBE; idempotent */
-
+/* RFC 9051 section 6.3.8 UNSUBSCRIBE; idempotent */
 void
 handle_mbox_unsubscribe(struct imsg_mbox_subscribe *req, struct imsgev *iev)
 {
@@ -962,7 +880,7 @@ handle_mbox_unsubscribe(struct imsg_mbox_subscribe *re
 		result.error = MBOX_OP_ERR_GENERIC;
 		goto send;
 	}
-	/* RFC 9051 SS6.3.8: no existence test */
+	/* RFC 9051 section 6.3.8: no existence test */
 	if (!mailbox_name_valid(req->mailbox)) {
 		log_debug("session %u: UNSUBSCRIBE %s: invalid name",
 		    session_id, req->mailbox);
@@ -970,7 +888,6 @@ handle_mbox_unsubscribe(struct imsg_mbox_subscribe *re
 		goto send;
 	}
 
-
 	if (subs_lock_acquire(&lockfd) == -1 || sublist_load(&sl) == -1) {
 		result.error = MBOX_OP_ERR_GENERIC;
 		goto send;
@@ -982,7 +899,7 @@ handle_mbox_unsubscribe(struct imsg_mbox_subscribe *re
 		goto send;
 	}
 	if (!sublist_has(&sl, req->mailbox)) {
-		/* SS6.3.8: unsubscribing what isn't subscribed returns OK */
+		/* section 6.3.8: unsubscribing the unsubscribed is OK */
 		result.error = MBOX_OP_OK;
 		goto send;
 	}
@@ -1006,16 +923,13 @@ send:
 const char *
 append_hostname(void)
 {
-	static char	hostbuf[256];
+	static char	hostbuf[HOST_NAME_MAX + 1];
 	static int	resolved;
 
 	if (!resolved) {
 		if (gethostname(hostbuf, sizeof(hostbuf)) == -1) {
 			log_warn("session %u: gethostname", session_id);
-			if (strlcpy(hostbuf, "imapd", sizeof(hostbuf)) >=
-			    sizeof(hostbuf))
-				log_warnx("session %u: gethostname fallback "
-				    "truncated, can't happen", session_id);
+			strlcpy(hostbuf, "imapd", sizeof(hostbuf));
 		}
 		resolved = 1;
 	}
@@ -1052,8 +966,8 @@ handle_mbox_append_begin(struct store_session *ss,
     const struct imsg_mbox_append *req)
 {
 	struct store_append	*ap = &ss->append;
-	char		target[MBOX_NAME_MAX];
-	int64_t		delivery_ts;
+	char			 target[MBOX_NAME_MAX];
+	int64_t			 delivery_ts;
 
 	if (ap->active) {
 		log_warnx("session %u: APPEND begun with another in flight, "
@@ -1078,8 +992,8 @@ handle_mbox_append_begin(struct store_session *ss,
 		return;
 	}
 	if (!index_field_valid(req->keywords)) {
-		log_warnx("session %u: APPEND: refusing unsafe keywords "
-		    "field", session_id);
+		log_warnx("session %u: APPEND: unsafe keywords field",
+		    session_id);
 		return;
 	}
 
@@ -1136,7 +1050,7 @@ void
 handle_mbox_append_data(struct store_session *ss, const char *buf, size_t len)
 {
 	struct store_append	*ap = &ss->append;
-	size_t	written = 0;
+	size_t			 written = 0;
 
 	if (!ap->active) {
 		log_warnx("session %u: APPEND data with no APPEND in flight, "
blob - f27afc00ad6efce70e76eb235576e2801e307357
blob + dd0be27fb30c0b6486c528ce9d6f7b8ec30da378
--- src/mbox_search.c
+++ src/mbox_search.c
@@ -19,6 +19,7 @@
 #include <sys/types.h>
 #include <sys/file.h>
 #include <sys/stat.h>
+#include <sys/time.h>
 
 #include <dirent.h>
 #include <errno.h>
@@ -44,15 +45,13 @@ kw_list_contains(const char *list, const char *kw)
 	char		*save;
 
 	if (strlcpy(tmp, list, sizeof(tmp)) >= sizeof(tmp)) {
-		log_warnx("session %u: kw_list_contains: keyword list "
-		    "truncated, can't happen (both same-size "
-		    "MBOX_FLAGS_MAX buffers); treating as not-found",
+		log_warnx("session %u: keyword list too long, no match",
 		    session_id);
 		return (0);
 	}
 	for (tok = strtok_r(tmp, ",", &save); tok != NULL;
 	    tok = strtok_r(NULL, ",", &save)) {
-		/* RFC 9051 SS2.3.2: flags/keywords are case-insensitive */
+		/* RFC 9051 section 2.3.2: flags are case-insensitive */
 		if (strcasecmp(tok, kw) == 0)
 			return (1);
 	}
@@ -76,7 +75,7 @@ append_kw_token(char *out, size_t outsize, int *firstp
 	return (1);
 }
 
-/* RFC 9051 SS6.4.6: SET ignores the old keywords */
+/* RFC 9051 section 6.4.6: SET ignores the old keywords */
 int
 merge_keywords(int mode, const char *old_kws, const char *new_kws,
     char *out, size_t outsize)
@@ -90,10 +89,8 @@ merge_keywords(int mode, const char *old_kws, const ch
 
 	if (mode == MBOX_STORE_REMOVE) {
 		if (strlcpy(tmp, old_kws, sizeof(tmp)) >= sizeof(tmp)) {
-			log_warnx("session %u: merge_keywords: old_kws "
-			    "truncated, can't happen (same-size "
-			    "MBOX_FLAGS_MAX buffers); refusing to guess at "
-			    "the keyword set", session_id);
+			log_warnx("session %u: STORE: old keyword list too "
+			    "long", session_id);
 			return (-1);
 		}
 		for (tok = strtok_r(tmp, ",", &save); tok != NULL;
@@ -108,10 +105,8 @@ merge_keywords(int mode, const char *old_kws, const ch
 
 	if (mode == MBOX_STORE_ADD) {
 		if (strlcpy(tmp, old_kws, sizeof(tmp)) >= sizeof(tmp)) {
-			log_warnx("session %u: merge_keywords: old_kws "
-			    "truncated, can't happen (same-size "
-			    "MBOX_FLAGS_MAX buffers); refusing to guess at "
-			    "the keyword set", session_id);
+			log_warnx("session %u: STORE: old keyword list too "
+			    "long", session_id);
 			return (-1);
 		}
 		for (tok = strtok_r(tmp, ",", &save); tok != NULL;
@@ -122,9 +117,8 @@ merge_keywords(int mode, const char *old_kws, const ch
 	}
 
 	if (strlcpy(tmp, new_kws, sizeof(tmp)) >= sizeof(tmp)) {
-		log_warnx("session %u: merge_keywords: new_kws truncated, "
-		    "can't happen (same-size MBOX_FLAGS_MAX buffers); "
-		    "refusing to guess at the keyword set", session_id);
+		log_warnx("session %u: STORE: new keyword list too long",
+		    session_id);
 		return (-1);
 	}
 	for (tok = strtok_r(tmp, ",", &save); tok != NULL;
@@ -158,8 +152,8 @@ struct search_msg_ctx {
 	const char	*keywords;
 	int64_t		internaldate;
 	uint64_t	size;
-	uint64_t	modseq;		/* RFC 7162 SS3.1.5 MODSEQ search key */
-	const struct seq_range *saved;
+	uint64_t	modseq;		/* RFC 7162 section 3.1.5 MODSEQ key */
+	const struct seq_range	*saved;
 	uint32_t	nsaved;
 };
 
@@ -217,7 +211,7 @@ search_eval_leaf(const struct search_node *n, const st
 	}
 }
 
-/* 1 for the RFC 9051 SS6.4.4 keys the parser-worker answers */
+/* 1 for the RFC 9051 section 6.4.4 keys the parser-worker answers */
 int
 search_op_content(int op)
 {
@@ -296,8 +290,8 @@ handle_mbox_search(struct imsg_mbox_search *req, struc
 
 	if (sw->active) {
 		/* unreachable while the listener holds the next command */
-		log_warnx("session %u: SEARCH arrived during a SEARCH, "
-		    "abandoning the earlier walk", session_id);
+		log_warnx("session %u: SEARCH during a SEARCH, earlier walk "
+		    "abandoned", session_id);
 		search_walk_abort(ss);
 	}
 	memset(sw, 0, sizeof(*sw));
@@ -377,7 +371,7 @@ handle_mbox_search(struct imsg_mbox_search *req, struc
 	return (0);
 }
 
-/* answers 0 or 1, or -1 if it could not be checked */
+/* 0 or 1; -1 if it could not be checked; -2 while no parser is ready */
 static int
 search_walk_ask(struct store_session *ss, const struct search_msg_ctx *m,
     const char *basename)
@@ -399,8 +393,8 @@ search_walk_ask(struct store_session *ss, const struct
 	}
 	if ((mfd = open_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
 	    basename)) == -1) {
-		log_warnx("session %u: message %s cannot be opened, SEARCH "
-		    "cannot check it", session_id, basename);
+		log_warnx("session %u: message %s: open failed, SEARCH cannot "
+		    "check it", session_id, basename);
 		return (-1);
 	}
 	memset(&preq, 0, sizeof(preq));
@@ -439,8 +433,7 @@ search_walk_step(struct store_session *ss)
 		if (++sw->walked > SEARCH_YIELD_MESSAGES) {
 			sw->walked = 0;
 			sw->next = i;
-			tv.tv_sec = 0;
-			tv.tv_usec = 0;
+			timerclear(&tv);
 			evtimer_add(&sw->yield_ev, &tv);
 			return;
 		}
@@ -474,8 +467,7 @@ search_walk_step(struct store_session *ss)
 			if (sw->asked >= SEARCH_YIELD_REQUESTS) {
 				sw->asked = 0;
 				sw->next = i;
-				tv.tv_sec = 0;
-				tv.tv_usec = 0;
+				timerclear(&tv);
 				evtimer_add(&sw->yield_ev, &tv);
 				return;
 			}
@@ -530,7 +522,7 @@ search_walk_finish(struct store_session *ss, int ok)
 	sw->active = 0;
 }
 
-/* RFC 9051 SS6.4.4.1: matches on adjacent index lines share a range */
+/* RFC 9051 section 6.4.4.1: matches on adjacent index lines share a range */
 static void
 search_save_match(struct store_search_walk *sw, uint32_t line, uint32_t uid)
 {
@@ -548,7 +540,7 @@ search_save_match(struct store_search_walk *sw, uint32
 	sw->sv_line = line;
 }
 
-/* RFC 9051 SS6.4.4.1 and Table 4; 0 when refused, as NOTSAVED */
+/* RFC 9051 section 6.4.4.1 and Table 4; 0 when refused, as NOTSAVED */
 static int
 search_save_finish(struct store_session *ss, int ok)
 {
@@ -589,8 +581,6 @@ search_walk_yield(int fd, short event, void *arg)
 {
 	struct store_session	*ss = arg;
 
-	(void)fd;
-	(void)event;
 	session_id = ss->id;
 	if (ss->search.active)
 		search_walk_step(ss);
@@ -630,4 +620,3 @@ search_walk_parser(struct store_session *ss, int ok)
 	}
 	search_walk_step(ss);
 }
-
blob - 7bb5732cb223f7c13c61e2c4dcfe219cd96454ab
blob + 875bad33c23f506e33a31f3cf3038b986b00ae65
--- src/mbox_store.c
+++ src/mbox_store.c
@@ -82,12 +82,11 @@ store_plan(struct store_session *ss, const struct imsg
 		break;
 	}
 
-	/* RFC 9051 SS6.4.6: refuse rather than store a truncation */
+	/* RFC 9051 section 6.4.6: refuse rather than store a truncation */
 	if (merge_keywords(req->mode, rec->keywords, req->keywords,
 	    newkeywords, sizeof(newkeywords)) == -1) {
-		log_warnx("session %u: STORE: merged keyword set for uid %u "
-		    "exceeds %zu bytes, failing STORE", session_id, rec->uid,
-		    sizeof(newkeywords));
+		log_warnx("session %u: STORE: uid %u: merged keywords over "
+		    "%zu bytes", session_id, rec->uid, sizeof(newkeywords));
 		return (-1);
 	}
 	sysflags_to_letters(new_sysflags, st->letters, sizeof(st->letters));
@@ -150,7 +149,7 @@ store_undo(struct store_session *ss, const struct mbox
 	}
 }
 
-/* RFC 9051 SS6.4.6 STORE: plan every message, then change; LOCK_EX held */
+/* RFC 9051 section 6.4.6 STORE: plan all, then change; LOCK_EX held */
 int
 store_apply(struct store_session *ss, const struct imsg_mbox_store *req,
     struct mbox_index *idx, const struct seq_range *resolved,
@@ -278,7 +277,6 @@ handle_mbox_store(struct imsg_mbox_store *req, const s
     uint32_t nranges, struct store_session *ss)
 {
 	struct imsgev		*iev = &ss->iev;
-	int			 locked;
 	struct mbox_index	 idx;
 	struct imsg_mbox_result	 result;
 	struct index_lock	 il = INDEX_LOCK_INIT;
@@ -286,15 +284,15 @@ handle_mbox_store(struct imsg_mbox_store *req, const s
 	struct seq_range	 vr[SEQSET_MAX_RANGES];
 	struct store_step	*steps = NULL;
 	size_t			 nsteps = 0, k;
+	uint64_t		 reported = 0;
 	uint32_t		 nresolved, nvr, sent = 0;
-	uint64_t		 reported = 0;
 	int			 ok = 1, by_uid, synced, used, rc;
-	int			 ghost, limit = 0;
+	int			 ghost, limit = 0, locked;
 
 	memset(&idx, 0, sizeof(idx));
 
 	if (!index_field_valid(req->keywords)) {
-		log_warnx("session %u: STORE: refusing unsafe keywords field",
+		log_warnx("session %u: STORE: unsafe keywords field",
 		    session_id);
 		ok = 0;
 		goto done;
@@ -323,7 +321,7 @@ handle_mbox_store(struct imsg_mbox_store *req, const s
 
 	reported = idx.highestmodseq;
 
-	/* RFC 9051 SS6.4.9 */
+	/* RFC 9051 section 6.4.9 */
 	nresolved = seqset_resolve(vr, nvr, by_uid ?
 	    index_max_uid(&idx) : (uint32_t)idx.nlines, !by_uid, resolved);
 	ghost = !req->by_uid && !used && view_names_ghost(ss, &idx,
@@ -354,7 +352,7 @@ handle_mbox_store(struct imsg_mbox_store *req, const s
 			continue;
 		}
 
-		/* RFC 7162 SS3.1.3: UNCHANGEDSINCE echoes even under .SILENT */
+		/* RFC 7162 section 3.1.3: UNCHANGEDSINCE echoes with .SILENT */
 		if (!req->silent || req->has_unchangedsince) {
 			struct imsg_mbox_fetch_meta	 meta;
 			char				 newsuffix[16];
@@ -381,7 +379,7 @@ done:
 	free(steps);
 
 	memset(&result, 0, sizeof(result));
-	/* RFC 2180 SS4.2.1-SS4.2.3 */
+	/* RFC 2180 sections 4.2.1-4.2.3 */
 	result.error = !ok ? (limit ? MBOX_OP_ERR_LIMIT :
 	    MBOX_OP_ERR_GENERIC) : ghost && !req->silent ?
 	    MBOX_OP_ERR_EXPUNGEISSUED : MBOX_OP_OK;
@@ -450,9 +448,9 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req,
 	out = 0;
 	for (in = 0; in < idx.nlines; in++) {
 		struct index_rec	 rec;
-		const char	*lp;
-		uint32_t	 sysflags;
-		char		 suffix[64];
+		const char		*lp;
+		char			 suffix[64];
+		uint32_t		 sysflags;
 
 		if (index_parse_line(idx.lines[in], &rec) == -1) {
 			idx.lines[out++] = idx.lines[in];
@@ -461,8 +459,8 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req,
 		if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
 		    rec.basename, NULL, suffix, sizeof(suffix)) == -1) {
 			log_warnx("session %u: message %s indexed but missing "
-			    "on disk, kept in index, not counted as "
-			    "expunged", session_id, rec.basename);
+			    "on disk, kept, not expunged",
+			    session_id, rec.basename);
 			idx.lines[out++] = idx.lines[in];
 			continue;
 		}
@@ -472,7 +470,7 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req,
 			idx.lines[out++] = idx.lines[in];
 			continue;
 		}
-		/* RFC 9051 SS6.4.9: \Deleted outside the ranges is kept */
+		/* RFC 9051 section 6.4.9: \Deleted outside the set is kept */
 		if (req->by_uid &&
 		    !seqset_contains(resolved, nresolved, rec.uid)) {
 			idx.lines[out++] = idx.lines[in];
@@ -515,7 +513,7 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req,
 
 	for (k = 0; k < ngone; k++) {
 		struct index_rec	 rec;
-		char		 path[600];
+		char			 path[600];
 
 		if (index_parse_line(gone[k].line, &rec) == -1)
 			continue;
@@ -536,7 +534,7 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req,
 	}
 
 done:
-	/* RFC 9051 SS6.4.1: CLOSE deselects only once its expunge is saved. */
+	/* RFC 9051 section 6.4.1: CLOSE deselects once its expunge is saved */
 	if (req->silent && ok)
 		ss->mailbox_selected = 0;
 
blob - f0673170737641f25ea55077ef7a880eec3800ad
blob + 061da5af7ca779d4c9007d8673d3754560c9c766
--- src/mboxname.c
+++ src/mboxname.c
@@ -22,7 +22,7 @@
 #include "mboxname.h"
 #include "utf8.h"
 
-/* RFC 9051 SS5.1: INBOX is case-insensitive */
+/* RFC 9051 section 5.1: INBOX is case-insensitive */
 int
 mailbox_name_is_inbox(const char *name)
 {
@@ -44,10 +44,10 @@ mailbox_name_syntax_ok(const char *name)
 	for (i = 0; i < len; i++) {
 		unsigned char	c = (unsigned char)name[i];
 
-		/* RFC 9051 SS5.1.1: "/" is the hierarchy delimiter */
+		/* RFC 9051 section 5.1.1: "/" is the hierarchy delimiter */
 		if (c == '/')
 			return (0);
-		/* RFC 9051 SS5.1 lets a server refuse CTL names */
+		/* RFC 9051 section 5.1 lets a server refuse CTL names */
 		if (c < 0x20 || c == 0x7f)
 			return (0);
 	}
blob - 66731709f1033ef153c187bd122bfb804719994d
blob + 16a1c724e3db800c87639c65f736eebf05a1e92c
--- src/mboxname.h
+++ src/mboxname.h
@@ -40,7 +40,7 @@ int	 mailbox_name_syntax_ok(const char *);
 
 int	 mailbox_name_reserved(const char *);
 
-/* RFC 9051 SS5.1: INBOX is case-insensitive and always exists */
+/* RFC 9051 section 5.1: INBOX is case-insensitive and always exists */
 int	 mailbox_name_is_inbox(const char *);
 
 #endif /* IMAPD_MBOXNAME_H */
blob - b31e1c0eec8e826c1f842dc5fa52f2781378025e
blob + 139f080299a8866b11aabd0d6578e8ca39bcf134
--- src/mime.c
+++ src/mime.c
@@ -16,6 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <sys/param.h>
 #include <sys/types.h>
 #include <sys/file.h>
 #include <sys/stat.h>
@@ -59,18 +60,14 @@ cur_snapshot_discard(struct cur_snapshot *snap)
 static int
 cur_snapshot_build(struct cur_snapshot *snap, int dfd)
 {
-	DIR			*dp = NULL;
+	DIR			*dp;
 	const struct dirent	*de;
 	char			**names = NULL, **tmp;
 	size_t			  n = 0, cap = 0;
-	int			  curfd;
 
 	cur_snapshot_discard(snap);
 
-	curfd = openat(dfd, "cur", O_RDONLY | O_DIRECTORY);
-	if (curfd != -1 && (dp = fdopendir(curfd)) == NULL)
-		close(curfd);
-	if (dp == NULL) {
+	if ((dp = opendirat(dfd, "cur")) == NULL) {
 		snap->failed = 1;
 		return (-1);
 	}
@@ -142,8 +139,8 @@ cur_entry_take(const char *name, size_t baselen, char 
 		return (-1);
 	if (suffix_out != NULL && strlcpy(suffix_out, name + baselen,
 	    suffix_out_size) >= suffix_out_size) {
-		log_warnx("session %u: %s: flag suffix truncated, refusing "
-		    "to report a possibly-wrong flag set", session_id, name);
+		log_warnx("session %u: %s: flag suffix truncated",
+		    session_id, name);
 		return (-1);
 	}
 	return (0);
@@ -157,23 +154,16 @@ scan_cur_for_basename(struct cur_snapshot *snap, int d
 	DIR			*dp;
 	const struct dirent	*de;
 	const char		*hit;
-	size_t			 baselen = strlen(basename);
+	size_t			 baselen;
 	int			 rv = -1;
 
+	baselen = strlen(basename);
 	if ((hit = cur_snapshot_find(snap, dfd, basename, baselen)) != NULL)
 		return (cur_entry_take(hit, baselen, path, pathsize,
 		    suffix_out, suffix_out_size));
 
 	/* absent from the snapshot, so it may have arrived since: look */
-	{
-		int	curfd;
-
-		dp = NULL;
-		curfd = openat(dfd, "cur", O_RDONLY | O_DIRECTORY);
-		if (curfd != -1 && (dp = fdopendir(curfd)) == NULL)
-			close(curfd);
-	}
-	if (dp == NULL) {
+	if ((dp = opendirat(dfd, "cur")) == NULL) {
 		if (errno != ENOENT)
 			log_warn("session %u: opendir cur", session_id);
 		return (-1);
@@ -197,8 +187,9 @@ locate_message_file(struct cur_snapshot *snap, int dfd
 	struct stat	 st;
 	const char	*hit;
 	char		 path[600];
-	size_t		 baselen = strlen(basename);
+	size_t		 baselen;
 
+	baselen = strlen(basename);
 	suffix_out[0] = '\0';
 
 	if ((hit = cur_snapshot_find(snap, dfd, basename, baselen)) != NULL) {
@@ -323,16 +314,15 @@ read_header_from_fd(int fd, const char *basename, char
 	limit = found_sep ? sepindex : (size_t)total;
 	for (i = 0; i < limit && i + 1 < (size_t)total; i++) {
 		if (readbuf[i] == '\0') {
-			log_warnx("session %u: message %s has a NUL byte in "
-			    "its header, BODY.PEEK[HEADER] skipped",
-			    session_id, basename);
+			log_warnx("session %u: message %s: NUL in header, "
+			    "BODY.PEEK[HEADER] skipped", session_id, basename);
 			return (-1);
 		}
 	}
 
 	if (!found_sep || sepindex > FETCH_HEADER_MAX) {
-		log_warnx("session %u: message %s: no header/body separator "
-		    "found within %d bytes, BODY.PEEK[HEADER] skipped",
+		log_warnx("session %u: message %s: no header end within %d "
+		    "bytes, BODY.PEEK[HEADER] skipped",
 		    session_id, basename, FETCH_HEADER_MAX);
 		return (-1);
 	}
@@ -379,7 +369,6 @@ read_body_from_fd(int fd, const char *basename, int te
 		n = read(fd, readbuf + total, readbuf_size - total);
 		if (n == -1) {
 			if (errno == EINTR)
-				/* as mbox_copy.c's staging read does */
 				continue;
 			log_warn("session %u: read message body (%s)",
 			    session_id, basename);
@@ -437,7 +426,7 @@ toolarge:
 	return (-1);
 }
 
-/* RFC 9051 SS6.4.5.1: ASCII case-insensitive */
+/* RFC 9051 section 6.4.5.1: ASCII case-insensitive */
 int
 header_field_name_matches(const char *name, size_t namelen, const char *list)
 {
@@ -447,13 +436,7 @@ header_field_name_matches(const char *name, size_t nam
 	if (namelen == 0 || namelen >= sizeof(listcopy))
 		return (0);
 
-	if (strlcpy(listcopy, list, sizeof(listcopy)) >= sizeof(listcopy)) {
-		log_warnx("session %u: header_field_name_matches: list "
-		    "truncated, can't happen (list bounded by same-size "
-		    "HEADER_FIELDS_MAX buffer upstream); treating as "
-		    "not-found", session_id);
-		return (0);
-	}
+	strlcpy(listcopy, list, sizeof(listcopy));
 	for (tok = strtok_r(listcopy, " ", &save); tok != NULL;
 	    tok = strtok_r(NULL, " ", &save)) {
 		if (strlen(tok) == namelen &&
@@ -463,7 +446,7 @@ header_field_name_matches(const char *name, size_t nam
 	return (0);
 }
 
-/* one field plus its RFC 5322 SS2.2.3 obs-fold lines */
+/* one field plus its RFC 5322 section 2.2.3 obs-fold lines */
 struct hdr_field {
 	size_t	start;
 	size_t	name_end;
@@ -502,7 +485,7 @@ hdr_next_field(const char *hdr, size_t hdrlen, size_t 
 		if (hdr[f->colon] == ':')
 			break;
 	}
-	/* RFC 5322 SS4.5: white space may precede the colon */
+	/* RFC 5322 section 4.5: white space may precede the colon */
 	f->name_end = f->colon;
 	while (f->colon < f->line_end && f->name_end > f->start &&
 	    (hdr[f->name_end - 1] == ' ' || hdr[f->name_end - 1] == '\t'))
@@ -523,7 +506,7 @@ hdr_next_field(const char *hdr, size_t hdrlen, size_t 
 	return (1);
 }
 
-/* RFC 9051 SS6.4.5.1 HEADER.FIELDS[.NOT] */
+/* RFC 9051 section 6.4.5.1 HEADER.FIELDS[.NOT] */
 int
 filter_header_fields(const char *hdr, size_t hdrlen, const char *fields_spec,
     int want_not, char *out, size_t *outlen_out)
@@ -542,7 +525,7 @@ filter_header_fields(const char *hdr, size_t hdrlen, c
 			/* no terminating blank line */
 			return (-1);
 		if (r == 0) {
-			/* RFC 9051 SS6.4.5: the blank line is included */
+			/* RFC 9051 section 6.4.5: the blank line is included */
 			memcpy(out + outlen, hdr + f.start, f.end - f.start);
 			outlen += f.end - f.start;
 			*outlen_out = outlen;
@@ -597,14 +580,15 @@ read_message_header_fields(int fd, const char *basenam
 	return (0);
 }
 
-/* RFC 5322 SS2.2.3 unfolding; NIL vs "" per RFC 9051 SS7.5.2 */
+/* RFC 5322 section 2.2.3 unfolding; NIL vs "" per RFC 9051 section 7.5.2 */
 int
 extract_header_field(const char *hdr, size_t hdrlen, const char *name,
     char **val_out, size_t *vallen_out)
 {
-	size_t	 namelen = strlen(name);
+	size_t	 namelen;
 	size_t	 off = 0;
 
+	namelen = strlen(name);
 	*val_out = NULL;
 	*vallen_out = 0;
 
@@ -664,7 +648,7 @@ header_next_field(const char *hdr, size_t hdrlen, size
 	for (;;) {
 		if (hdr_next_field(hdr, hdrlen, off, &f) != 1)
 			return (0);
-		/* a line with no colon is not a field (RFC 5322 SS2.2) */
+		/* a line with no colon is not a field (RFC 5322 section 2.2) */
 		if (f.colon == f.line_end)
 			continue;
 		vstart = f.colon + 1;
@@ -708,7 +692,7 @@ find_header_body_split(const char *buf, size_t len, si
 	return (-1);
 }
 
-/* RFC 2045 SS5.1 tspecials; a `token` excludes SPACE, CTLs, and these */
+/* RFC 2045 section 5.1 tspecials; a `token` excludes SPACE, CTLs, and these */
 int
 mime_is_tspecial(char c)
 {
@@ -734,7 +718,7 @@ mime_read_token_or_qstring(const char *s, size_t len, 
 				(*pos)++;
 				c = s[*pos];
 			}
-			/* RFC 5322 SS4: keep CR and LF; a NUL would end out */
+			/* keep CR and LF (RFC 5322 section 4); NUL ends out */
 			if (c == '\0')
 				c = ' ';
 			if (outlen + 1 >= outsize)
@@ -772,12 +756,9 @@ mime_str_upper(char *s)
 	}
 }
 
-/* RFC 2045 SS5.1 Content-Type; SS5.2 default on failure */
+/* RFC 2045 section 5.1 Content-Type; section 5.2 default on failure */
 int
-parse_content_type(const char *hdr, size_t hdrlen, char *type_out,
-    size_t typesize, char *subtype_out, size_t subtypesize,
-    char *params_fmt_out, size_t params_fmt_outsize, char *boundary_out,
-    size_t boundary_outsize, int *has_boundary_out)
+parse_content_type(const char *hdr, size_t hdrlen, struct content_type *ct)
 {
 	char	*val = NULL;
 	size_t	 vallen = 0;
@@ -786,48 +767,38 @@ parse_content_type(const char *hdr, size_t hdrlen, cha
 	int	 nparams = 0;
 	int	 use_default = 0;
 	/* -1: envbuf_append*() never NUL-terminates, reserve a byte */
-	size_t	 pfsize = (params_fmt_outsize > 0) ? params_fmt_outsize - 1 : 0;
+	size_t	 pfsize = sizeof(ct->params_fmt) - 1;
 
-	*has_boundary_out = 0;
-	boundary_out[0] = '\0';
+	memset(ct, 0, sizeof(*ct));
 
-	if (pfsize == 0)
-		return (-1);
-
 	if (extract_header_field(hdr, hdrlen, "Content-Type", &val,
 	    &vallen) == -1 || vallen == 0)
 		use_default = 1;
 
 	if (!use_default && (mime_read_token_or_qstring(val, vallen, &pos,
-	    type_out, typesize) == -1 || pos >= vallen || val[pos] != '/'))
+	    ct->type, sizeof(ct->type)) == -1 || pos >= vallen ||
+	    val[pos] != '/'))
 		use_default = 1;
 	if (!use_default) {
 		pos++;
-		if (mime_read_token_or_qstring(val, vallen, &pos, subtype_out,
-		    subtypesize) == -1)
+		if (mime_read_token_or_qstring(val, vallen, &pos, ct->subtype,
+		    sizeof(ct->subtype)) == -1)
 			use_default = 1;
 	}
 
 	if (use_default) {
 		free(val);
-		if (strlcpy(type_out, "TEXT", typesize) >= typesize ||
-		    strlcpy(subtype_out, "PLAIN", subtypesize) >=
-		    subtypesize) {
-			log_warnx("session %u: parse_content_type: default "
-			    "type/subtype truncated, caller-supplied "
-			    "buffer too small for \"TEXT\"/\"PLAIN\"",
-			    session_id);
-			return (-1);
-		}
-		if (envbuf_append_str(params_fmt_out, pfsize, &plen,
+		strlcpy(ct->type, "TEXT", sizeof(ct->type));
+		strlcpy(ct->subtype, "PLAIN", sizeof(ct->subtype));
+		if (envbuf_append_str(ct->params_fmt, pfsize, &plen,
 		    "(\"CHARSET\" \"US-ASCII\")") == -1)
 			return (-1);
-		params_fmt_out[plen] = '\0';
+		ct->params_fmt[plen] = '\0';
 		return (0);
 	}
 
-	mime_str_upper(type_out);
-	mime_str_upper(subtype_out);
+	mime_str_upper(ct->type);
+	mime_str_upper(ct->subtype);
 
 	for (;;) {
 		char	attr[64], value[256];
@@ -854,47 +825,45 @@ parse_content_type(const char *hdr, size_t hdrlen, cha
 			break;
 
 		mime_str_upper(attr);
-		if (envbuf_append_str(params_fmt_out, pfsize,
+		if (envbuf_append_str(ct->params_fmt, pfsize,
 		    &plen, nparams == 0 ? "(" : " ") == -1 ||
-		    envbuf_append_nstring(params_fmt_out, pfsize,
+		    envbuf_append_nstring(ct->params_fmt, pfsize,
 		    &plen, attr, strlen(attr)) == -1 ||
-		    envbuf_append(params_fmt_out, pfsize, &plen,
+		    envbuf_append(ct->params_fmt, pfsize, &plen,
 		    " ", 1) == -1 ||
-		    envbuf_append_nstring(params_fmt_out, pfsize,
+		    envbuf_append_nstring(ct->params_fmt, pfsize,
 		    &plen, value, strlen(value)) == -1) {
 			free(val);
 			return (-1);
 		}
 		nparams++;
 
-		if (strcasecmp(attr, "BOUNDARY") == 0) {
-			/* RFC 2046 SS5.1.1: at most 70 characters */
-			if (strlcpy(boundary_out, value, boundary_outsize) <
-			    boundary_outsize)
-				*has_boundary_out = 1;
-		}
+		if (strcasecmp(attr, "BOUNDARY") == 0 &&
+		    strlcpy(ct->boundary, value, sizeof(ct->boundary)) <
+		    sizeof(ct->boundary))
+			ct->has_boundary = 1;
 	}
 
 	free(val);
 	if (nparams > 0) {
-		if (envbuf_append_str(params_fmt_out, pfsize, &plen,
+		if (envbuf_append_str(ct->params_fmt, pfsize, &plen,
 		    ")") == -1)
 			return (-1);
-		params_fmt_out[plen] = '\0';
+		ct->params_fmt[plen] = '\0';
 		return (0);
 	}
-	if (envbuf_append_str(params_fmt_out, pfsize, &plen, "NIL") == -1)
+	if (envbuf_append_str(ct->params_fmt, pfsize, &plen, "NIL") == -1)
 		return (-1);
-	params_fmt_out[plen] = '\0';
+	ct->params_fmt[plen] = '\0';
 	return (0);
 }
 
-/* RFC 2046 SS5.1.1 */
+/* RFC 2046 section 5.1.1 */
 int
 split_multipart(const char *body, size_t bodylen, const char *boundary,
     size_t *part_starts, size_t *part_ends, int *nparts_out, int maxparts)
 {
-	/* "--" + boundary; RFC 2046 SS5.1.1 caps boundary at 70 characters */
+	/* "--" + boundary; RFC 2046 section 5.1.1 caps it at 70 characters */
 	char	 needle[2 + 70 + 1];
 	size_t	 needlelen;
 	size_t	 pos = 0;
@@ -902,12 +871,7 @@ split_multipart(const char *body, size_t bodylen, cons
 	int	 n = 0;
 	int	 closed = 0;
 
-	if (strlcpy(needle, "--", sizeof(needle)) >= sizeof(needle)) {
-		log_warnx("session %u: split_multipart: \"--\" truncated, "
-		    "can't happen (2 bytes into a 73-byte buffer)",
-		    session_id);
-		return (-1);
-	}
+	strlcpy(needle, "--", sizeof(needle));
 	needlelen = strlcat(needle, boundary, sizeof(needle));
 	if (needlelen >= sizeof(needle))
 		return (-1);	/* boundary too long, non-conformant */
@@ -987,7 +951,7 @@ split_multipart(const char *body, size_t bodylen, cons
 	return (0);
 }
 
-/* RFC 9051 SS6.4.5 section-part */
+/* RFC 9051 section 6.4.5 section-part */
 int
 parse_section_part(const char *s, int *path, int maxpath)
 {
@@ -1018,7 +982,7 @@ parse_section_part(const char *s, int *path, int maxpa
 			return (-1);
 		s++;
 		if (*s == '\0')
-			return (-1);	/* trailing dot */
+			return (-1);
 	}
 	return (n);
 }
@@ -1028,27 +992,22 @@ find_mime_part(int depth, const char *hdr, size_t hdrl
     size_t bodylen, const int *path, int pathlen, const char **part_out,
     size_t *partlen_out)
 {
-	char	type[64], subtype[64];
-	char	params_fmt[600];
-	char	boundary[70 + 1];
-	int	has_boundary;
-	size_t	part_starts[MIME_MAX_PARTS], part_ends[MIME_MAX_PARTS];
-	int	n, want;
-	const char	*pbuf, *pbody;
-	size_t		 plen, phdrend, pbodylen, ih;
+	struct content_type	 ct;
+	const char		*pbuf, *pbody;
+	size_t			 part_starts[MIME_MAX_PARTS];
+	size_t			 part_ends[MIME_MAX_PARTS];
+	size_t			 plen, phdrend, pbodylen, ih;
+	int			 n, want;
 
 	if (depth > MIME_MAX_DEPTH)
 		return (-1);
 
-	params_fmt[0] = '\0';
-	if (parse_content_type(hdr, hdrlen, type, sizeof(type), subtype,
-	    sizeof(subtype), params_fmt, sizeof(params_fmt), boundary,
-	    sizeof(boundary), &has_boundary) == -1)
+	if (parse_content_type(hdr, hdrlen, &ct) == -1)
 		return (-1);
-	if (strcasecmp(type, "MULTIPART") != 0 || !has_boundary)
+	if (strcasecmp(ct.type, "MULTIPART") != 0 || !ct.has_boundary)
 		return (-1);
-	if (split_multipart(body, bodylen, boundary, part_starts, part_ends,
-	    &n, MIME_MAX_PARTS) == -1)
+	if (split_multipart(body, bodylen, ct.boundary, part_starts,
+	    part_ends, &n, MIME_MAX_PARTS) == -1)
 		return (-1);
 
 	want = path[0];
@@ -1062,27 +1021,24 @@ find_mime_part(int depth, const char *hdr, size_t hdrl
 	else if (find_header_body_split(pbuf, plen, &phdrend) == -1)
 		return (-1);
 
-	params_fmt[0] = '\0';
-	if (parse_content_type(pbuf, phdrend, type, sizeof(type), subtype,
-	    sizeof(subtype), params_fmt, sizeof(params_fmt), boundary,
-	    sizeof(boundary), &has_boundary) == -1)
+	if (parse_content_type(pbuf, phdrend, &ct) == -1)
 		return (-1);
 	pbody = pbuf + phdrend;
 	pbodylen = plen - phdrend;
 
 	if (pathlen == 1) {
 		/* a leaf or a message: no "combined children" of a multipart */
-		if (strcasecmp(type, "MULTIPART") == 0)
+		if (strcasecmp(ct.type, "MULTIPART") == 0)
 			return (-1);
 		*part_out = pbody;
 		*partlen_out = pbodylen;
 		return (0);
 	}
 
-	/* RFC 9051 SS6.4.5.1: a message's parts are numbered within it */
-	if (strcasecmp(type, "MESSAGE") == 0 &&
-	    (strcasecmp(subtype, "RFC822") == 0 ||
-	    strcasecmp(subtype, "GLOBAL") == 0)) {
+	/* RFC 9051 section 6.4.5.1: a message's parts are numbered within it */
+	if (strcasecmp(ct.type, "MESSAGE") == 0 &&
+	    (strcasecmp(ct.subtype, "RFC822") == 0 ||
+	    strcasecmp(ct.subtype, "GLOBAL") == 0)) {
 		if (pbodylen == 0 ||
 		    find_header_body_split(pbody, pbodylen, &ih) == -1)
 			return (-1);
@@ -1095,28 +1051,22 @@ find_mime_part(int depth, const char *hdr, size_t hdrl
 	    path + 1, pathlen - 1, part_out, partlen_out));
 }
 
-/* RFC 9051 SS6.4.5.1: a non-multipart message is its own part 1 */
+/* RFC 9051 section 6.4.5.1: a non-multipart message is its own part 1 */
 int
 locate_mime_part(int depth, const char *hdr, size_t hdrlen,
     const char *body, size_t bodylen, const int *path, int pathlen,
     const char **part_out, size_t *partlen_out)
 {
-	char	type[64], subtype[64];
-	char	params_fmt[600];
-	char	boundary[70 + 1];
-	int	has_boundary;
-	size_t	ih;
+	struct content_type	 ct;
+	size_t			 ih;
 
 	if (depth > MIME_MAX_DEPTH || pathlen < 1)
 		return (-1);
 
-	params_fmt[0] = '\0';
-	if (parse_content_type(hdr, hdrlen, type, sizeof(type), subtype,
-	    sizeof(subtype), params_fmt, sizeof(params_fmt), boundary,
-	    sizeof(boundary), &has_boundary) == -1)
+	if (parse_content_type(hdr, hdrlen, &ct) == -1)
 		return (-1);
 
-	if (strcasecmp(type, "MULTIPART") == 0)
+	if (strcasecmp(ct.type, "MULTIPART") == 0)
 		return (find_mime_part(depth + 1, hdr, hdrlen, body, bodylen,
 		    path, pathlen, part_out, partlen_out));
 
@@ -1127,16 +1077,16 @@ locate_mime_part(int depth, const char *hdr, size_t hd
 		*partlen_out = bodylen;
 		return (0);
 	}
-	if (strcasecmp(type, "MESSAGE") != 0 ||
-	    (strcasecmp(subtype, "RFC822") != 0 &&
-	    strcasecmp(subtype, "GLOBAL") != 0) || bodylen == 0 ||
+	if (strcasecmp(ct.type, "MESSAGE") != 0 ||
+	    (strcasecmp(ct.subtype, "RFC822") != 0 &&
+	    strcasecmp(ct.subtype, "GLOBAL") != 0) || bodylen == 0 ||
 	    find_header_body_split(body, bodylen, &ih) == -1)
 		return (-1);
 	return (locate_mime_part(depth + 1, body, ih, body + ih,
 	    bodylen - ih, path + 1, pathlen - 1, part_out, partlen_out));
 }
 
-/* RFC 9051 SS6.4.5 "<start.count>" applied to a range; past its end is empty */
+/* RFC 9051 section 6.4.5 <start.count> on a range; past the end is empty */
 void
 partial_range(int has_partial, uint32_t start, uint32_t count,
     uint64_t *off, uint64_t *len)
@@ -1176,7 +1126,7 @@ extract_mime_part(int fd, const char *basename, const 
 		return (-1);
 	}
 
-	/* RFC 9051 SS6.4.5: a missing part is an empty item */
+	/* RFC 9051 section 6.4.5: a missing part is an empty item */
 	if (locate_mime_part(0, wholebuf, hdrend, wholebuf + hdrend,
 	    wholelen - hdrend, path, pathlen, &part, &partlen) == 0) {
 		*off_out = (uint64_t)(part - wholebuf);
@@ -1240,8 +1190,8 @@ message_body_range(struct cur_snapshot *snap, int dfd,
 	}
 
 	if (!found) {
-		log_warnx("session %u: message %s: no header/body separator "
-		    "found, BODY[TEXT] skipped", session_id, basename);
+		log_warnx("session %u: message %s: no header end, BODY[TEXT] "
+		    "skipped", session_id, basename);
 		close(fd);
 		return (-1);
 	}
@@ -1250,6 +1200,18 @@ message_body_range(struct cur_snapshot *snap, int dfd,
 	return (fd);
 }
 
+static int
+flag_append(char *out, size_t outsize, int *first, const char *flag)
+{
+	if ((!*first && strlcat(out, " ", outsize) >= outsize) ||
+	    strlcat(out, flag, outsize) >= outsize) {
+		log_warnx("session %u: flag list truncated", session_id);
+		return (0);
+	}
+	*first = 0;
+	return (1);
+}
+
 /* flag letters as in Courier's maildir(5) */
 void
 build_flags_string(const char *maildir_suffix, const char *keywords,
@@ -1266,6 +1228,8 @@ build_flags_string(const char *maildir_suffix, const c
 		{ 'T', "\\Deleted" },
 	};
 	const char	*letters;
+	char		*kw, *save;
+	char		 kwbuf[512];
 	size_t		 i;
 	int		 first = 1;
 
@@ -1274,57 +1238,23 @@ build_flags_string(const char *maildir_suffix, const c
 	letters = strstr(maildir_suffix, "2,");
 	letters = (letters != NULL) ? letters + 2 : "";
 
-	for (i = 0; i < sizeof(stdflags) / sizeof(stdflags[0]); i++) {
-		if (strchr(letters, stdflags[i].letter) == NULL)
-			continue;
-		if (!first && strlcat(out, " ", outsize) >= outsize) {
-			log_warnx("session %u: build_flags_string: out "
-			    "truncated, caller-supplied buffer too small "
-			    "for the flag list; stopping early", session_id);
+	for (i = 0; i < nitems(stdflags); i++) {
+		if (strchr(letters, stdflags[i].letter) != NULL &&
+		    !flag_append(out, outsize, &first, stdflags[i].flag))
 			return;
-		}
-		if (strlcat(out, stdflags[i].flag, outsize) >= outsize) {
-			log_warnx("session %u: build_flags_string: out "
-			    "truncated, caller-supplied buffer too small "
-			    "for the flag list; stopping early", session_id);
-			return;
-		}
-		first = 0;
 	}
 
-	if (keywords != NULL && keywords[0] != '\0') {
-		char	 kwbuf[512];
-		char	*kw, *save;
-
-		if (strlcpy(kwbuf, keywords, sizeof(kwbuf)) >= sizeof(kwbuf)) {
-			log_warnx("session %u: build_flags_string: keywords "
-			    "too long for kwbuf, omitting keyword flags "
-			    "rather than guessing at a truncated list",
-			    session_id);
+	if (keywords == NULL || keywords[0] == '\0')
+		return;
+	strlcpy(kwbuf, keywords, sizeof(kwbuf));
+	for (kw = strtok_r(kwbuf, ",", &save); kw != NULL;
+	    kw = strtok_r(NULL, ",", &save)) {
+		if (!flag_append(out, outsize, &first, kw))
 			return;
-		}
-		for (kw = strtok_r(kwbuf, ",", &save); kw != NULL;
-		    kw = strtok_r(NULL, ",", &save)) {
-			if (!first && strlcat(out, " ", outsize) >= outsize) {
-				log_warnx("session %u: build_flags_string: "
-				    "out truncated, caller-supplied "
-				    "buffer too small for the flag list; "
-				    "stopping early", session_id);
-				return;
-			}
-			if (strlcat(out, kw, outsize) >= outsize) {
-				log_warnx("session %u: build_flags_string: "
-				    "out truncated, caller-supplied "
-				    "buffer too small for the flag list; "
-				    "stopping early", session_id);
-				return;
-			}
-			first = 0;
-		}
 	}
 }
 
-/* RFC 9051 SS2.3.3: from the basename, not mtime */
+/* RFC 9051 section 2.3.3: from the basename, not mtime */
 int64_t
 parse_maildir_timestamp(const char *basename)
 {
blob - e6ffc62566f5ef03659e82ca925bbb36182680d5
blob + bf61ced9817a15ffeeaaf4c578c6c3ad2bb296c4
--- src/parent.c
+++ src/parent.c
@@ -7,23 +7,6 @@
  * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
  * Copyright (c) 2008 Reyk Floeter <reyk@openbsd.org>
  *
- * This file's boot-time peer-wiring handshake -- setup_peer_send() and
- * setup_done_send(), and the IMSG_SETUP_PEER/IMSG_SETUP_DONE message
- * names they and imapd.h use -- follows smtpd's setup_peers()/
- * setup_done() (usr.sbin/smtpd/smtpd.c:861-902) and its identically-
- * named IMSG_SETUP_PEER/IMSG_SETUP_DONE enumerators (smtpd.h:211-213),
- * which no other daemon in the OpenBSD base system defines under those
- * names. The functions here are reworked onto imapd's own struct imsgev
- * and multi-child bookkeeping rather than copied verbatim, but the
- * message protocol and handshake shape are smtpd's; see the inline
- * citations at this file's setup_peer_send() and setup_done_send().
- *
- * send_keymgr_init()'s TLS key permission check -- fstat(2), then
- * st_uid != 0 and st_mode & (S_IRWXU|S_IRWXG|S_IRWXO) & ~0740 -- reuses
- * smtpd's ssl_load_key() check (usr.sbin/smtpd/ssl.c:112-140) verbatim
- * for that mask and rejection order; only the fixed 0740 bound and the
- * imsg delivery around it are this file's own.
- *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
@@ -37,6 +20,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <sys/param.h>
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/resource.h>
@@ -163,6 +147,7 @@ static void	 store_fork_failed(struct open_session *, 
 static int	 store_child_attach(struct store_child *,
 		    struct open_session *);
 static void	 store_child_session_gone(struct store_child *);
+static void	 store_child_wire(struct store_child *);
 static void	 store_child_parser(struct store_child *);
 static void	 store_child_dispatch(int, short, void *);
 static void	 store_child_timeout(int, short, void *);
@@ -191,8 +176,6 @@ parent_main(const char *conffile, int argc, char *argv
 	int	 i;
 	struct rlimit	 rl;
 
-	(void)argc;
-
 	if (geteuid() != 0)
 		fatalx("parent must start as root (running as uid %u)",
 		    (unsigned int)geteuid());
@@ -223,8 +206,7 @@ parent_main(const char *conffile, int argc, char *argv
 	    bind_listen_socket(conf->listen_addr, conf->port_implicit_tls,
 	    tls_fds);
 	if (n_cleartext == 0 && n_tls == 0)
-		fatalx("no listener configured: imapd.conf named no usable "
-		    "\"listen on\" address/port");
+		fatalx("no usable \"listen on\" address in imapd.conf");
 
 	event_init();
 
@@ -255,10 +237,8 @@ parent_main(const char *conffile, int argc, char *argv
 	/* no setproctitle() here: rc.d matches the parent's command line */
 
 	/* proc exec sendfd: every connection forks and fd-passes */
-#ifdef __OpenBSD__
 	if (pledge("stdio rpath inet proc exec sendfd", NULL) == -1)
 		fatal("pledge");
-#endif
 
 	event_dispatch();
 	fatalx("parent: exited event loop");
@@ -274,7 +254,7 @@ exec_self_as(const char *role)
 	nargv[n++] = progpath;
 	nargv[n++] = "-x";
 	/* execv(3) takes char *const argv[] but never writes through it */
-	nargv[n++] = (char *)(uintptr_t)role;
+	nargv[n++] = (char *)role;
 	for (i = 1; saved_argv[i] != NULL; i++) {
 		if (strcmp(saved_argv[i], "-x") == 0) {
 			/* a trailing -x would read past argv's NULL */
@@ -283,11 +263,10 @@ exec_self_as(const char *role)
 			i++;
 			continue;
 		}
-		if (n >= (int)(sizeof(nargv) / sizeof(nargv[0])) - 1) {
+		if (n >= (int)nitems(nargv) - 1) {
 			/* truncating could split an option from its value */
-			log_warnx("exec_self_as: argv too long to pass to the "
-			    "%s child, refusing to exec a truncated command "
-			    "line", role);
+			log_warnx("exec_self_as: argv too long for the %s "
+			    "child", role);
 			_exit(1);
 		}
 		nargv[n++] = saved_argv[i];
@@ -488,25 +467,20 @@ parent_dispatch_child(int fd, short event, void *arg)
 
 			/* check against sessions the parent knows */
 			if ((os = open_session_find(req.session_id)) == NULL) {
-				log_warnx("refusing IMSG_AUTH_CRED for session "
-				    "%u: not a session parent knows is open "
-				    "(never opened, already closed, or evicted "
-				    "by OPEN_SESSION_MAX)",
-				    req.session_id);
+				log_warnx("IMSG_AUTH_CRED for unknown session "
+				    "%u, refused", req.session_id);
 				break;
 			}
 			/* only this session's own auth-worker may grant it */
 			if (iev != os->auth_iev) {
-				log_warnx("refusing IMSG_AUTH_CRED for "
-				    "session %u: not from that session's own "
-				    "auth-worker channel",
+				log_warnx("IMSG_AUTH_CRED for session %u from "
+				    "the wrong channel, refused",
 				    req.session_id);
 				break;
 			}
 			if (os->authenticated) {
-				log_warnx("refusing IMSG_AUTH_CRED for session "
-				    "%u: already authenticated, refusing "
-				    "duplicate grant", req.session_id);
+				log_warnx("IMSG_AUTH_CRED for session %u: "
+				    "already authenticated", req.session_id);
 				/* reply: the listener waits with no timeout */
 				store_fork_failed(os, 0);
 				break;
@@ -530,7 +504,6 @@ parent_dispatch_child(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 /* reap_child() frees c once SIGCHLD reports the exit */
@@ -609,8 +582,6 @@ accept_resume(int fd, short event, void *arg)
 {
 	int	 i;
 
-	(void)fd; (void)event; (void)arg;
-
 	if (shutting_down)
 		return;
 	for (i = 0; i < n_cleartext; i++)
@@ -660,7 +631,7 @@ count_startups_from(const struct sockaddr_storage *ss)
 	return (n);
 }
 
-/* RFC 9051 SS7.1.5 BYE, on the cleartext port only */
+/* RFC 9051 section 7.1.5 BYE, on the cleartext port only */
 static void
 refuse_connection(int fd, int cleartext)
 {
@@ -680,8 +651,6 @@ parent_accept(int fd, short event, void *arg)
 	int				 client_fd, flags, implicit_tls, on = 1;
 	unsigned int			 nstartups, nopen, nsource;
 
-	(void)event;
-
 	if ((client_fd = accept(fd, (struct sockaddr *)&ss, &sslen)) == -1) {
 		/* else the listen event fires again at once, and spins */
 		if (errno == EMFILE || errno == ENFILE) {
@@ -711,8 +680,7 @@ parent_accept(int fd, short event, void *arg)
 		nsource = count_startups_from(&ss);
 		if (nsource >= gconf->max_startups_per_source) {
 			log_debug("startups per-source: refusing connection "
-			    "(%u unauthenticated already open from its "
-			    "address)", nsource);
+			    "(%u unauthenticated from its address)", nsource);
 			refuse_connection(client_fd, arg == (void *)0);
 			return;
 		}
@@ -763,8 +731,8 @@ spawn_connection(int client_fd, int implicit_tls,
 	TAILQ_FOREACH(it, &open_sessions, entry)
 		nopen++;
 	if (nopen >= OPEN_SESSION_MAX) {
-		log_warnx("refusing connection: %u open sessions already "
-		    "tracked (OPEN_SESSION_MAX)", nopen);
+		log_warnx("refusing connection: %u sessions open "
+		    "(OPEN_SESSION_MAX)", nopen);
 		close(client_fd);
 		return;
 	}
@@ -779,8 +747,8 @@ spawn_connection(int client_fd, int implicit_tls,
 
 	if ((listener_pid = fork_child_nonfatal(PROC_LISTENER,
 	    &new_listener_iev, parent_dispatch_child)) == -1) {
-		log_warnx("session %u: refusing connection: could not spawn "
-		    "a listener-worker", session_id);
+		log_warnx("session %u: refusing connection: no listener-worker",
+		    session_id);
 		free(os);
 		close(client_fd);
 		return;
@@ -792,10 +760,7 @@ spawn_connection(int client_fd, int implicit_tls,
 
 	if ((auth_pid = fork_child_nonfatal(PROC_AUTH, &new_auth_iev,
 	    parent_dispatch_child)) == -1) {
-		log_warnx("session %u: no auth-worker available, this "
-		    "session's first AUTHENTICATE will fail instead of the "
-		    "connection itself (listener-worker detects a missing "
-		    "auth peer the same way it detects one dying later)",
+		log_warnx("session %u: no auth-worker for this connection",
 		    session_id);
 	} else {
 		os->auth_pid = auth_pid;
@@ -821,8 +786,7 @@ spawn_connection(int client_fd, int implicit_tls,
 	if (imsg_compose(&new_listener_iev->ibuf, IMSG_LISTENER_SESSION_INIT,
 	    0, 0, client_fd, &init, sizeof(init)) == -1) {
 		log_warnx("session %u: imsg_compose "
-		    "IMSG_LISTENER_SESSION_INIT failed, connection lost",
-		    session_id);
+		    "IMSG_LISTENER_SESSION_INIT, connection lost", session_id);
 		close(client_fd);
 		goto fail_workers;
 	}
@@ -851,10 +815,11 @@ maildir_path_is_safe(const char *p)
 	if (p == NULL || p[0] == '\0' || p[0] == '/')
 		return (0);
 	for (;;) {
-		const char	*slash = strchr(seg, '/');
-		size_t		 len = slash ? (size_t)(slash - seg) :
-		    strlen(seg);
+		const char	*slash;
+		size_t		 len;
 
+		slash = strchr(seg, '/');
+		len = slash ? (size_t)(slash - seg) : strlen(seg);
 		if (len == 1 && seg[0] == '.')
 			return (0);
 		if (len == 2 && seg[0] == '.' && seg[1] == '.')
@@ -909,8 +874,8 @@ parent_handle_store_fork(struct open_session *os, uid_
 		    strcmp(it->maildir, maildir) == 0) {
 			if (it->nsessions >= gconf->account_sessions) {
 				log_info("session %u: refusing login: uid %u "
-				    "already has %u sessions (account "
-				    "sessions)", session_id, (unsigned int)uid,
+				    "has %u sessions (account sessions)",
+				    session_id, (unsigned int)uid,
 				    it->nsessions);
 				store_fork_failed(os, 1);
 				return;
@@ -969,17 +934,15 @@ parent_handle_store_fork(struct open_session *os, uid_
 	if (strlcpy(init_payload.spool_root, gconf->spool_root,
 	    sizeof(init_payload.spool_root)) >=
 	    sizeof(init_payload.spool_root)) {
-		log_warnx("session %u: spool_root truncated spawning store "
-		    "child, refusing to wire a session to a possibly "
-		    "wrong spool root", session_id);
+		log_warnx("session %u: spool_root truncated, store not spawned",
+		    session_id);
 		goto fail_kill;
 	}
 
 	if (strlcpy(init_payload.maildir, maildir,
 	    sizeof(init_payload.maildir)) >= sizeof(init_payload.maildir)) {
-		log_warnx("session %u: maildir truncated spawning store "
-		    "child, refusing to wire a session to a possibly "
-		    "wrong mailbox", session_id);
+		log_warnx("session %u: maildir truncated, store not spawned",
+		    session_id);
 		goto fail_kill;
 	}
 	init_payload.bodystructure_read_max = gconf->bodystructure_read_max;
@@ -1037,14 +1000,32 @@ store_fork_failed(struct open_session *os, int limit)
 static int
 store_child_attach(struct store_child *sc, struct open_session *os)
 {
-	if (setup_peer_send(&sc->iev, os->listener_iev, IMSG_SETUP_PEER,
-	    "IMSG_SETUP_PEER", os->session_id) == -1)
+	if (!sc->pending && setup_peer_send(&sc->iev, os->listener_iev,
+	    IMSG_SETUP_PEER, "IMSG_SETUP_PEER", os->session_id) == -1)
 		return (-1);
 	os->store = sc;
 	sc->nsessions++;
 	return (0);
 }
 
+/* a login is answered OK only once its store is in the maildir */
+static void
+store_child_wire(struct store_child *sc)
+{
+	struct open_session	*os;
+
+	TAILQ_FOREACH(os, &open_sessions, entry) {
+		if (os->store != sc || os->listener_iev == NULL)
+			continue;
+		if (setup_peer_send(&sc->iev, os->listener_iev,
+		    IMSG_SETUP_PEER, "IMSG_SETUP_PEER", os->session_id) == 0)
+			continue;
+		os->store = NULL;
+		store_child_session_gone(sc);
+		store_fork_failed(os, 0);
+	}
+}
+
 /* the parent, not the store child, decides when it is done */
 static void
 store_child_session_gone(struct store_child *sc)
@@ -1143,6 +1124,7 @@ store_child_dispatch(int fd, short event, void *arg)
 			evtimer_del(&sc->timeout_ev);
 			sc->pending = 0;
 			imsg_free(&imsg);
+			store_child_wire(sc);
 			continue;
 		}
 		if (imsg_get_type(&imsg) == IMSG_PARSER_WANT) {
@@ -1156,7 +1138,6 @@ store_child_dispatch(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(&sc->iev);
-	(void)fd;
 }
 
 static void
@@ -1164,8 +1145,6 @@ store_child_timeout(int fd, short event, void *arg)
 {
 	struct store_child *sc = arg;
 
-	(void)fd;
-	(void)event;
 	log_warnx("session %u: store setup timed out after %ds",
 	    sc->session_id, STORE_SETUP_TIMEOUT_SEC);
 	store_child_fail(sc);
@@ -1286,14 +1265,12 @@ read_file_capped(FILE *fp, char *buf, size_t bufsize, 
 	n = fread(buf, 1, bufsize, fp);
 	if (n == bufsize) {
 		if (fgetc(fp) != EOF) {
-			log_warnx("%s: larger than %zu bytes, refusing to "
-			    "send a truncated %s (TLS will be disabled)",
-			    path, bufsize, what);
+			log_warnx("%s: over %zu bytes, %s not sent, TLS "
+			    "disabled", path, bufsize, what);
 			return (-1);
 		}
 	} else if (!feof(fp)) {
-		log_warnx("%s: short read that wasn't a clean EOF, refusing "
-		    "to send a possibly-truncated %s (TLS will be disabled)",
+		log_warnx("%s: short read, %s not sent, TLS disabled",
 		    path, what);
 		return (-1);
 	}
@@ -1352,12 +1329,12 @@ send_keymgr_init(struct imsgev *iev, struct openimap_c
 		log_warn("fstat %s", conf->tls_key_file);
 		fclose(fp);
 	} else if (st.st_uid != 0) {
-		log_warnx("%s: not owned by uid 0, refusing to load "
-		    "(TLS will be disabled)", conf->tls_key_file);
+		log_warnx("%s: not owned by uid 0, TLS disabled",
+		    conf->tls_key_file);
 		fclose(fp);
 	} else if (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO) & ~0740) {
-		log_warnx("%s: insecure permissions, must be at most "
-		    "rwxr----- (TLS will be disabled)", conf->tls_key_file);
+		log_warnx("%s: insecure permissions, TLS disabled",
+		    conf->tls_key_file);
 		fclose(fp);
 	} else {
 		if (read_file_capped(fp, buf, sizeof(buf), &n,
@@ -1389,8 +1366,7 @@ send_auth_init(struct imsgev *iev, struct openimap_con
 	memset(&init, 0, sizeof(init));
 	if (strlcpy(init.cred_file, conf->cred_file, sizeof(init.cred_file))
 	    >= sizeof(init.cred_file)) {
-		log_warnx("cred_file truncated sending IMSG_AUTH_INIT, config "
-		    "value too long for the wire struct's field");
+		log_warnx("cred_file truncated, IMSG_AUTH_INIT not sent");
 		return (-1);
 	}
 
@@ -1412,8 +1388,6 @@ sighup_handler(int fd, short event, void *arg)
 {
 	struct openimap_config	 newconf;
 
-	(void)fd; (void)event; (void)arg;
-
 	log_info("SIGHUP: reloading %s", conf_path);
 
 	memset(&newconf, 0, sizeof(newconf));
@@ -1426,24 +1400,19 @@ sighup_handler(int fd, short event, void *arg)
 	if (strcmp(newconf.listen_addr, gconf->listen_addr) != 0 ||
 	    newconf.port_cleartext != gconf->port_cleartext ||
 	    newconf.port_implicit_tls != gconf->port_implicit_tls) {
-		log_warnx("SIGHUP: %s: \"listen on\" changed but listening "
-		    "sockets cannot be rebound without a restart, still "
-		    "serving %s:%u / %s:%u", conf_path, gconf->listen_addr,
-		    gconf->port_cleartext, gconf->listen_addr,
-		    gconf->port_implicit_tls);
+		log_warnx("SIGHUP: %s: \"listen on\" changed, takes effect on "
+		    "restart", conf_path);
 	}
 	if (strcmp(newconf.cred_file, gconf->cred_file) != 0) {
-		log_warnx("SIGHUP: %s: \"credentials\" changed but each "
-		    "auth-worker chroots to the old path's directory, a "
-		    "restart is required for this to take effect", conf_path);
+		log_warnx("SIGHUP: %s: \"credentials\" changed, takes effect "
+		    "on restart", conf_path);
 	}
 
 	if (strlen(newconf.spool_root) >= sizeof(gconf->spool_root) ||
 	    strlen(newconf.tls_cert_file) >= sizeof(gconf->tls_cert_file) ||
 	    strlen(newconf.tls_key_file) >= sizeof(gconf->tls_key_file)) {
-		log_warnx("SIGHUP: %s: reloaded value too long for gconf's "
-		    "field, keeping the already-running configuration",
-		    conf_path);
+		log_warnx("SIGHUP: %s: value too long, keeping the running "
+		    "configuration", conf_path);
 		return;
 	}
 
@@ -1470,9 +1439,8 @@ sighup_handler(int fd, short event, void *arg)
 	/* keymgr holds the key for every connection, so it needs a push */
 	if (iev_keymgr != NULL) {
 		if (send_keymgr_init(iev_keymgr, gconf) == -1)
-			log_warnx("SIGHUP: pushing the reloaded certificate "
-			    "and key to keymgr failed; it is still using the "
-			    "previously loaded material");
+			log_warnx("SIGHUP: keymgr did not take the new "
+			    "certificate and key, keeping the old");
 	} else
 		log_warnx("SIGHUP: keymgr is gone, TLS private key not "
 		    "reloaded");
@@ -1502,8 +1470,6 @@ sigterm_force(int fd, short event, void *arg)
 	struct store_child	*sc;
 	int			 n = 0;
 
-	(void)fd; (void)event; (void)arg;
-
 	TAILQ_FOREACH(c, &children, entry) {
 		kill(c->pid, SIGKILL);
 		n++;
@@ -1526,8 +1492,6 @@ sigterm_handler(int fd, short event, void *arg)
 	struct timeval	 tv;
 	int		 i;
 
-	(void)fd; (void)event; (void)arg;
-
 	/* a 2nd SIGTERM does not wait, as l2tpd.c:509-512 also does */
 	if (shutting_down) {
 		log_info("SIGTERM again: not waiting for children");
@@ -1574,7 +1538,6 @@ sigchld_handler(int fd, short event, void *arg)
 	pid_t	 pid;
 	int	 status;
 
-	(void)fd; (void)event; (void)arg;
 	while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
 		reap_child(pid, status);
 
@@ -1609,29 +1572,15 @@ reap_child(pid_t pid, int status)
 					    "shutdown", pid);
 				else if (WIFSIGNALED(status))
 					log_warnx("keymgr[%d] terminated by "
-					    "signal %d, new TLS handshakes "
-					    "will now fail (already-"
-					    "negotiated TLS sessions are "
-					    "unaffected); run \"rcctl "
-					    "restart imapd\" to recover",
-					    pid, WTERMSIG(status));
+					    "signal %d, TLS handshakes will "
+					    "fail", pid, WTERMSIG(status));
 				else if (WIFEXITED(status))
-					log_warnx("keymgr[%d] exited "
-					    "unexpectedly, status %d, new "
-					    "TLS handshakes will now fail "
-					    "(already-negotiated TLS "
-					    "sessions are unaffected); run "
-					    "\"rcctl restart imapd\" to "
-					    "recover", pid,
-					    WEXITSTATUS(status));
+					log_warnx("keymgr[%d] exited, status "
+					    "%d, TLS handshakes will fail",
+					    pid, WEXITSTATUS(status));
 				else
-					log_warnx("keymgr[%d] exited "
-					    "unexpectedly, new TLS "
-					    "handshakes will now fail "
-					    "(already-negotiated TLS "
-					    "sessions are unaffected); run "
-					    "\"rcctl restart imapd\" to "
-					    "recover", pid);
+					log_warnx("keymgr[%d] exited, TLS "
+					    "handshakes will fail", pid);
 				iev_keymgr = NULL;
 				free(c);
 				return;
@@ -1678,10 +1627,8 @@ reap_child(pid_t pid, int status)
 				free(os);
 			} else {
 				log_debug("session %u: auth-worker[%d] "
-				    "exited (status %d); this session's "
-				    "listener-worker will detect this on "
-				    "its own auth channel", os->session_id,
-				    pid, status);
+				    "exited, status %d",
+				    os->session_id, pid, status);
 				os->auth_iev = NULL;
 				os->auth_pid = 0;
 			}
blob - cfad15bf92d2bb0d9cfdf97b9ce1d6cd053156ae
blob + 53bc7f3ae111d129938cd551bfeb10c68a178044
--- src/parse.y
+++ src/parse.y
@@ -7,14 +7,6 @@
  * Copyright (c) 2001 Daniel Hartmeier.  All rights reserved.
  * Copyright (c) 2001 Theo de Raadt.  All rights reserved.
  *
- * The parser skeleton below (lgetc()/lungetc()/findeol(), the hand-
- * written yylex() built on top of them, pushfile()/popfile(), and
- * symset()/symget()) follows the structure common to ripd's, smtpd's,
- * and httpd's own parse.y in the OpenBSD base system, all of which
- * carry this same four-name copyright chain at their root. This file's
- * grammar and domain-specific rules are original; the above four names
- * are carried forward for the shared parser-skeleton lineage only.
- *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
@@ -772,7 +764,7 @@ config_load(const char *path, struct openimap_config *
 	conf = xconf;
 	memset(conf, 0, sizeof(*conf));
 
-	(void)strlcpy(conf->listen_addr, "0.0.0.0", sizeof(conf->listen_addr));
+	(void)strlcpy(conf->listen_addr, "*", sizeof(conf->listen_addr));
 	conf->port_cleartext = 143;
 	conf->port_implicit_tls = 993;
 	conf->idle_poll_secs = IDLE_POLL_DEFAULT;
blob - 89cadd6d7d8b8435bffef0913ac0d978bd4afb4e
blob + 1c15f07121604a242b854c6e07d35ec683493006
--- src/parser.c
+++ src/parser.c
@@ -122,10 +122,8 @@ parser_main(void)
 	imsgev_init(&iev_store, store_fd, parser_dispatch_store, NULL);
 
 	/* no "rpath": the only bytes arrive on passed descriptors */
-#ifdef __OpenBSD__
 	if (pledge("stdio recvfd", NULL) == -1)
 		fatal("pledge");
-#endif
 
 	event_dispatch();
 	fatalx("parser: exited event loop");
@@ -266,8 +264,6 @@ parser_dispatch_store(int fd, short event, void *arg)
 	struct imsg	 imsg;
 	ssize_t		 n;
 
-	(void)fd;
-
 	if (event & EV_WRITE) {
 		if (imsgbuf_write(&iev->ibuf) == -1)
 			fatal("imsgbuf_write");
blob - fa25214c72a54d8338fdec64ec0a4114adcfd77c
blob + 8b3ea7fcb79f6b83ec5eef538e392579ff15c0b7
--- src/search_cmd.c
+++ src/search_cmd.c
@@ -16,7 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
+#include <sys/param.h>
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/socket.h>
@@ -87,7 +87,6 @@ struct search_parse_ctx {
 	char			pool[SEARCH_OPERANDS_MAX];
 };
 
-
 static int
 search_push(struct search_parse_ctx *ctx, const struct search_node *node,
     const char **errmsg)
@@ -100,7 +99,7 @@ search_push(struct search_parse_ctx *ctx, const struct
 	return (0);
 }
 
-/* RFC 9051 SS9 sequence-set, pushed as ORed ranges */
+/* RFC 9051 section 9 sequence-set, pushed as ORed ranges */
 static int
 parse_search_seqset(char **pp, struct search_parse_ctx *ctx, int op,
     const char **errmsg)
@@ -157,7 +156,7 @@ parse_search_seqset(char **pp, struct search_parse_ctx
 	return (0);
 }
 
-/* RFC 9051 SS6.4.4 search-key: 0 ok, -1 BAD, -2 NO */
+/* RFC 9051 section 6.4.4 search-key: 0 ok, -1 BAD, -2 NO */
 int
 parse_search_key(char **pp, struct search_parse_ctx *ctx, const char **errmsg)
 {
@@ -174,7 +173,6 @@ parse_search_key(char **pp, struct search_parse_ctx *c
 	return (rc);
 }
 
-
 static int
 read_search_token(char **pp, char *buf, size_t bufsize, const char *errtext,
     const char **errmsg)
@@ -197,7 +195,7 @@ read_search_token(char **pp, char *buf, size_t bufsize
 	return (0);
 }
 
-/* RFC 9051 SS9 date, as the UTC midnight that begins it */
+/* RFC 9051 section 9 date, as the UTC midnight that begins it */
 static int
 read_search_date(char **pp, int64_t *out, const char **errmsg)
 {
@@ -208,9 +206,10 @@ read_search_date(char **pp, int64_t *out, const char *
 		p++;
 	if (*p == '"') {
 		const char	*start = p + 1;
-		char		*end = strchr(start, '"');
+		char		*end;
 		size_t		 len;
 
+		end = strchr(start, '"');
 		if (end == NULL) {
 			*errmsg = "unterminated date string";
 			return (-1);
@@ -245,7 +244,7 @@ search_pool_add(struct search_parse_ctx *ctx, char c)
 	ctx->operand_len++;
 }
 
-/* RFC 9051 SS9 astring operand */
+/* RFC 9051 section 9 astring operand */
 static int
 read_search_astring(char **pp, struct search_parse_ctx *ctx,
     uint32_t *offp, uint32_t *lenp, const char **errmsg)
@@ -269,7 +268,7 @@ read_search_astring(char **pp, struct search_parse_ctx
 				*errmsg = "unterminated quoted string";
 				return (-1);
 			}
-			/* RFC 9051 SS9: quoted-specials are the only escapes */
+			/* RFC 9051 section 9: only quoted-specials escape */
 			if (*p == '\\' && *++p != '"' && *p != '\\') {
 				*errmsg = "malformed quoted string";
 				return (-1);
@@ -278,7 +277,7 @@ read_search_astring(char **pp, struct search_parse_ctx
 		}
 		p++;
 	} else {
-		/* RFC 9051 SS9 ASTRING-CHAR */
+		/* RFC 9051 section 9 ASTRING-CHAR */
 		for (; *p != '\0' && *p != ' ' && *p != ')'; p++) {
 			if ((unsigned char)*p <= 0x1f ||
 			    (unsigned char)*p >= 0x7f ||
@@ -300,13 +299,136 @@ read_search_astring(char **pp, struct search_parse_ctx
 	return (0);
 }
 
+/* what follows a search key, before it is pushed as one node */
+enum search_arg {
+	SARG_NONE,
+	SARG_KEYWORD,
+	SARG_DATE,
+	SARG_NUMBER,
+	SARG_MODSEQ,
+	SARG_STRING,
+	SARG_HEADER,
+	SARG_UIDSET,
+	SARG_KEY,
+	SARG_KEY2
+};
+
+static const struct search_keyword {
+	const char	*name;
+	int		 op;
+	enum search_arg	 arg;
+} search_keywords[] = {
+	{ "ALL",	SEARCH_OP_ALL,		SARG_NONE },
+	{ "ANSWERED",	SEARCH_OP_ANSWERED,	SARG_NONE },
+	{ "UNANSWERED",	SEARCH_OP_UNANSWERED,	SARG_NONE },
+	{ "DELETED",	SEARCH_OP_DELETED,	SARG_NONE },
+	{ "UNDELETED",	SEARCH_OP_UNDELETED,	SARG_NONE },
+	{ "DRAFT",	SEARCH_OP_DRAFT,	SARG_NONE },
+	{ "UNDRAFT",	SEARCH_OP_UNDRAFT,	SARG_NONE },
+	{ "FLAGGED",	SEARCH_OP_FLAGGED,	SARG_NONE },
+	{ "UNFLAGGED",	SEARCH_OP_UNFLAGGED,	SARG_NONE },
+	{ "SEEN",	SEARCH_OP_SEEN,		SARG_NONE },
+	{ "UNSEEN",	SEARCH_OP_UNSEEN,	SARG_NONE },
+	{ "KEYWORD",	SEARCH_OP_KEYWORD,	SARG_KEYWORD },
+	{ "UNKEYWORD",	SEARCH_OP_UNKEYWORD,	SARG_KEYWORD },
+	{ "BEFORE",	SEARCH_OP_BEFORE,	SARG_DATE },
+	{ "ON",		SEARCH_OP_ON,		SARG_DATE },
+	{ "SINCE",	SEARCH_OP_SINCE,	SARG_DATE },
+	{ "LARGER",	SEARCH_OP_LARGER,	SARG_NUMBER },
+	{ "SMALLER",	SEARCH_OP_SMALLER,	SARG_NUMBER },
+	{ "UID",	SEARCH_OP_UIDSET,	SARG_UIDSET },
+	{ "MODSEQ",	SEARCH_OP_MODSEQ,	SARG_MODSEQ },
+	{ "NOT",	SEARCH_OP_NOT,		SARG_KEY },
+	{ "OR",		SEARCH_OP_OR,		SARG_KEY2 },
+	/* RFC 9051 section 6.4.4 content keys, answered from the message */
+	{ "SENTBEFORE",	SEARCH_OP_SENTBEFORE,	SARG_DATE },
+	{ "SENTON",	SEARCH_OP_SENTON,	SARG_DATE },
+	{ "SENTSINCE",	SEARCH_OP_SENTSINCE,	SARG_DATE },
+	{ "BCC",	SEARCH_OP_BCC,		SARG_STRING },
+	{ "BODY",	SEARCH_OP_BODY,		SARG_STRING },
+	{ "CC",		SEARCH_OP_CC,		SARG_STRING },
+	{ "FROM",	SEARCH_OP_FROM,		SARG_STRING },
+	{ "HEADER",	SEARCH_OP_HEADER,	SARG_HEADER },
+	{ "SUBJECT",	SEARCH_OP_SUBJECT,	SARG_STRING },
+	{ "TEXT",	SEARCH_OP_TEXT,		SARG_STRING },
+	{ "TO",		SEARCH_OP_TO,		SARG_STRING },
+};
+
+static int
+read_search_number(char **pp, int64_t *out, int modseq, const char **errmsg)
+{
+	const char	*errstr;
+	char		 numbuf[24];
+
+	if (read_search_token(pp, numbuf, sizeof(numbuf), modseq ?
+	    "missing or malformed MODSEQ value" :
+	    "missing or malformed octet count", errmsg) == -1)
+		return (-1);
+	errstr = "not a number";
+	if (numbuf[0] >= '0' && numbuf[0] <= '9')
+		*out = strtonum(numbuf, 0, INT64_MAX, &errstr);
+	if (errstr != NULL) {
+		*errmsg = modseq ? "malformed MODSEQ value" :
+		    "malformed octet count";
+		return (-1);
+	}
+	return (0);
+}
+
+/* RFC 7162 section 3.1.5: MODSEQ [entry-name type-req]; both are skipped */
+static int
+skip_modseq_entry(char **pp, const char **errmsg)
+{
+	char	*p = *pp, *end;
+
+	while (*p == ' ')
+		p++;
+	if (*p == '\0' || isdigit((unsigned char)*p)) {
+		*pp = p;
+		return (0);
+	}
+	if (*p == '"') {
+		if ((end = strchr(p + 1, '"')) == NULL) {
+			*errmsg = "unterminated MODSEQ entry-name";
+			return (-1);
+		}
+		p = end + 1;
+	} else {
+		while (*p != '\0' && *p != ' ' && *p != ')')
+			p++;
+	}
+	while (*p == ' ')
+		p++;
+
+	if (strncasecmp(p, "priv", 4) == 0 &&
+	    (p[4] == ' ' || p[4] == '\0' || p[4] == ')'))
+		p += 4;
+	else if (strncasecmp(p, "shared", 6) == 0 &&
+	    (p[6] == ' ' || p[6] == '\0' || p[6] == ')'))
+		p += 6;
+	else if (strncasecmp(p, "all", 3) == 0 &&
+	    (p[3] == ' ' || p[3] == '\0' || p[3] == ')'))
+		p += 3;
+	else {
+		*errmsg = "expected priv/shared/all after MODSEQ entry-name";
+		return (-1);
+	}
+	while (*p == ' ')
+		p++;
+	*pp = p;
+	return (0);
+}
+
 int
 parse_search_key_inner(char **pp, struct search_parse_ctx *ctx,
     const char **errmsg)
 {
-	char	*p = *pp;
-	char	 word[32];
-	size_t	 wlen;
+	const struct search_keyword	*kw;
+	struct search_node		 node;
+	char				*p = *pp, *start;
+	char				 word[32];
+	size_t				 wlen, i;
+	int				 rc;
 
 	while (*p == ' ')
 		p++;
@@ -317,11 +439,8 @@ parse_search_key_inner(char **pp, struct search_parse_
 	}
 
 	if (*p == '(') {
-		int	rc;
-
 		p++;
-		rc = parse_search_key_list(&p, ctx, errmsg, 1);
-		if (rc != 0)
+		if ((rc = parse_search_key_list(&p, ctx, errmsg, 1)) != 0)
 			return (rc);
 		while (*p == ' ')
 			p++;
@@ -342,326 +461,94 @@ parse_search_key_inner(char **pp, struct search_parse_
 		return (0);
 	}
 
-	{
-		char	*start = p;
+	start = p;
+	while (*p != '\0' && *p != ' ' && *p != ')')
+		p++;
+	wlen = (size_t)(p - start);
+	if (wlen == 0 || wlen >= sizeof(word)) {
+		*errmsg = "unknown search key";
+		return (-1);
+	}
+	memcpy(word, start, wlen);
+	word[wlen] = '\0';
 
-		while (*p != '\0' && *p != ' ' && *p != ')')
-			p++;
-		wlen = (size_t)(p - start);
-		if (wlen == 0 || wlen >= sizeof(word)) {
-			*errmsg = "unknown search key";
-			return (-1);
+	kw = NULL;
+	for (i = 0; i < nitems(search_keywords); i++) {
+		if (strcasecmp(word, search_keywords[i].name) == 0) {
+			kw = &search_keywords[i];
+			break;
 		}
-		memcpy(word, start, wlen);
-		word[wlen] = '\0';
 	}
-
-	if (strcasecmp(word, "ALL") == 0) {
-		struct search_node	node;
-
-		memset(&node, 0, sizeof(node));
-		node.op = SEARCH_OP_ALL;
-		if (search_push(ctx, &node, errmsg) == -1)
-			return (-1);
-		*pp = p;
-		return (0);
+	if (kw == NULL) {
+		*errmsg = "unknown search key";
+		return (-1);
 	}
 
-	{
-		static const struct {
-			const char	*name;
-			int		 op;
-		} boolkeys[] = {
-			{ "ANSWERED",	SEARCH_OP_ANSWERED },
-			{ "UNANSWERED",	SEARCH_OP_UNANSWERED },
-			{ "DELETED",	SEARCH_OP_DELETED },
-			{ "UNDELETED",	SEARCH_OP_UNDELETED },
-			{ "DRAFT",	SEARCH_OP_DRAFT },
-			{ "UNDRAFT",	SEARCH_OP_UNDRAFT },
-			{ "FLAGGED",	SEARCH_OP_FLAGGED },
-			{ "UNFLAGGED",	SEARCH_OP_UNFLAGGED },
-			{ "SEEN",	SEARCH_OP_SEEN },
-			{ "UNSEEN",	SEARCH_OP_UNSEEN },
-		};
-		size_t	i;
-
-		for (i = 0; i < sizeof(boolkeys) / sizeof(boolkeys[0]); i++) {
-			struct search_node	node;
-
-			if (strcasecmp(word, boolkeys[i].name) != 0)
-				continue;
-			memset(&node, 0, sizeof(node));
-			node.op = boolkeys[i].op;
-			if (search_push(ctx, &node, errmsg) == -1)
-				return (-1);
-			*pp = p;
-			return (0);
-		}
-	}
-
-	if (strcasecmp(word, "KEYWORD") == 0 ||
-	    strcasecmp(word, "UNKEYWORD") == 0) {
-		struct search_node	node;
-
-		memset(&node, 0, sizeof(node));
-		node.op = (strcasecmp(word, "KEYWORD") == 0) ?
-		    SEARCH_OP_KEYWORD : SEARCH_OP_UNKEYWORD;
-
+	memset(&node, 0, sizeof(node));
+	node.op = kw->op;
+	switch (kw->arg) {
+	case SARG_NONE:
+		break;
+	case SARG_KEYWORD:
 		if (read_search_token(&p, node.keyword, sizeof(node.keyword),
 		    "missing or too-long KEYWORD/UNKEYWORD argument",
 		    errmsg) == -1)
 			return (-1);
-
-		if (search_push(ctx, &node, errmsg) == -1)
-			return (-1);
-		*pp = p;
-		return (0);
-	}
-
-	if (strcasecmp(word, "BEFORE") == 0 || strcasecmp(word, "ON") == 0 ||
-	    strcasecmp(word, "SINCE") == 0) {
-		struct search_node	node;
-
-		memset(&node, 0, sizeof(node));
-		if (strcasecmp(word, "BEFORE") == 0)
-			node.op = SEARCH_OP_BEFORE;
-		else if (strcasecmp(word, "ON") == 0)
-			node.op = SEARCH_OP_ON;
-		else
-			node.op = SEARCH_OP_SINCE;
-
+		break;
+	case SARG_DATE:
 		if (read_search_date(&p, &node.num, errmsg) == -1)
 			return (-1);
-
-		if (search_push(ctx, &node, errmsg) == -1)
+		break;
+	case SARG_NUMBER:
+		if (read_search_number(&p, &node.num, 0, errmsg) == -1)
 			return (-1);
-		*pp = p;
-		return (0);
-	}
-
-	if (strcasecmp(word, "LARGER") == 0 ||
-	    strcasecmp(word, "SMALLER") == 0) {
-		struct search_node	node;
-		char			numbuf[24];
-		char			*numend;
-		unsigned long long	 v;
-
-		memset(&node, 0, sizeof(node));
-		node.op = (strcasecmp(word, "LARGER") == 0) ?
-		    SEARCH_OP_LARGER : SEARCH_OP_SMALLER;
-
-		if (read_search_token(&p, numbuf, sizeof(numbuf),
-		    "missing or malformed octet count", errmsg) == -1)
+		break;
+	case SARG_MODSEQ:
+		if (skip_modseq_entry(&p, errmsg) == -1 ||
+		    read_search_number(&p, &node.num, 1, errmsg) == -1)
 			return (-1);
-
-		errno = 0;
-		v = strtoull(numbuf, &numend, 10);
-		if (*numend != '\0' || errno == ERANGE) {
-			*errmsg = "malformed octet count";
-			return (-1);
-		}
-		/* node.num is signed: refuse above INT64_MAX */
-		if (v > (unsigned long long)INT64_MAX) {
-			*errmsg = "octet count out of range";
-			return (-1);
-		}
-		node.num = (int64_t)v;
-
-		if (search_push(ctx, &node, errmsg) == -1)
-			return (-1);
-		*pp = p;
-		return (0);
-	}
-
-	if (strcasecmp(word, "UID") == 0) {
-		while (*p == ' ')
-			p++;
-		if (parse_search_seqset(&p, ctx, SEARCH_OP_UIDSET, errmsg) ==
-		    -1)
-			return (-1);
-		*pp = p;
-		return (0);
-	}
-
-	if (strcasecmp(word, "MODSEQ") == 0) {
-		struct search_node	node;
-		char			numbuf[24];
-		char			*numend;
-		unsigned long long	 v;
-
-		memset(&node, 0, sizeof(node));
-		node.op = SEARCH_OP_MODSEQ;
-
-		while (*p == ' ')
-			p++;
-
-		/* RFC 7162 SS3.1.5 entry-name and type-req are skipped */
-		if (*p != '\0' && !isdigit((unsigned char)*p)) {
-			if (*p == '"') {
-				char	*end = strchr(p + 1, '"');
-
-				if (end == NULL) {
-					*errmsg = "unterminated MODSEQ "
-					    "entry-name";
-					return (-1);
-				}
-				p = end + 1;
-			} else {
-				while (*p != '\0' && *p != ' ' && *p != ')')
-					p++;
-			}
-			while (*p == ' ')
-				p++;
-
-			if (strncasecmp(p, "priv", 4) == 0 &&
-			    (p[4] == ' ' || p[4] == '\0' || p[4] == ')'))
-				p += 4;
-			else if (strncasecmp(p, "shared", 6) == 0 &&
-			    (p[6] == ' ' || p[6] == '\0' || p[6] == ')'))
-				p += 6;
-			else if (strncasecmp(p, "all", 3) == 0 &&
-			    (p[3] == ' ' || p[3] == '\0' || p[3] == ')'))
-				p += 3;
-			else {
-				*errmsg = "expected priv/shared/all after "
-				    "MODSEQ entry-name";
-				return (-1);
-			}
-			while (*p == ' ')
-				p++;
-		}
-
-		if (read_search_token(&p, numbuf, sizeof(numbuf),
-		    "missing or malformed MODSEQ value", errmsg) == -1)
-			return (-1);
-
-		errno = 0;
-		v = strtoull(numbuf, &numend, 10);
-		if (*numend != '\0' || errno == ERANGE) {
-			*errmsg = "malformed MODSEQ value";
-			return (-1);
-		}
-		/* same signed-cast trap as LARGER/SMALLER above */
-		if (v > (unsigned long long)INT64_MAX) {
-			*errmsg = "MODSEQ value out of range";
-			return (-1);
-		}
-		node.num = (int64_t)v;
-
-		if (search_push(ctx, &node, errmsg) == -1)
-			return (-1);
 		ctx->uses_modseq = 1;
-		*pp = p;
-		return (0);
-	}
-
-	if (strcasecmp(word, "NOT") == 0) {
-		struct search_node	node;
-		int			rc;
-
-		rc = parse_search_key(&p, ctx, errmsg);
-		if (rc != 0)
-			return (rc);
-
-		memset(&node, 0, sizeof(node));
-		node.op = SEARCH_OP_NOT;
-		if (search_push(ctx, &node, errmsg) == -1)
+		break;
+	case SARG_HEADER:
+	case SARG_STRING:
+		if (kw->arg == SARG_HEADER && read_search_astring(&p, ctx,
+		    &node.name_off, &node.name_len, errmsg) == -1)
 			return (-1);
+		if (read_search_astring(&p, ctx, &node.str_off, &node.str_len,
+		    errmsg) == -1)
+			return (-1);
+		break;
+	case SARG_UIDSET:
+		while (*p == ' ')
+			p++;
+		if (parse_search_seqset(&p, ctx, SEARCH_OP_UIDSET, errmsg) ==
+		    -1)
+			return (-1);
 		*pp = p;
-		return (0);
-	}
-
-	if (strcasecmp(word, "OR") == 0) {
-		struct search_node	node;
-		int			rc;
-
-		rc = parse_search_key(&p, ctx, errmsg);
-		if (rc != 0)
+		return (0);	/* the set is its own node */
+	case SARG_KEY2:
+		if ((rc = parse_search_key(&p, ctx, errmsg)) != 0)
 			return (rc);
-		rc = parse_search_key(&p, ctx, errmsg);
-		if (rc != 0)
+		/* FALLTHROUGH */
+	case SARG_KEY:
+		if ((rc = parse_search_key(&p, ctx, errmsg)) != 0)
 			return (rc);
-
-		memset(&node, 0, sizeof(node));
-		node.op = SEARCH_OP_OR;
-		if (search_push(ctx, &node, errmsg) == -1)
-			return (-1);
-		*pp = p;
-		return (0);
+		break;
 	}
 
-	/* RFC 9051 SS6.4.4 content keys, answered from the message */
-	if (strcasecmp(word, "SENTBEFORE") == 0 ||
-	    strcasecmp(word, "SENTON") == 0 ||
-	    strcasecmp(word, "SENTSINCE") == 0) {
-		struct search_node	node;
-
-		memset(&node, 0, sizeof(node));
-		if (strcasecmp(word, "SENTBEFORE") == 0)
-			node.op = SEARCH_OP_SENTBEFORE;
-		else if (strcasecmp(word, "SENTON") == 0)
-			node.op = SEARCH_OP_SENTON;
-		else
-			node.op = SEARCH_OP_SENTSINCE;
-
-		if (read_search_date(&p, &node.num, errmsg) == -1)
-			return (-1);
-
-		if (search_push(ctx, &node, errmsg) == -1)
-			return (-1);
-		*pp = p;
-		return (0);
-	}
-
-	{
-		static const struct {
-			const char	*name;
-			int		 op;
-		} string_keys[] = {
-			{ "BCC",	SEARCH_OP_BCC },
-			{ "BODY",	SEARCH_OP_BODY },
-			{ "CC",		SEARCH_OP_CC },
-			{ "FROM",	SEARCH_OP_FROM },
-			{ "HEADER",	SEARCH_OP_HEADER },
-			{ "SUBJECT",	SEARCH_OP_SUBJECT },
-			{ "TEXT",	SEARCH_OP_TEXT },
-			{ "TO",		SEARCH_OP_TO },
-		};
-		struct search_node	node;
-		size_t			i;
-
-		for (i = 0; i < sizeof(string_keys) / sizeof(string_keys[0]);
-		    i++) {
-			if (strcasecmp(word, string_keys[i].name) != 0)
-				continue;
-			memset(&node, 0, sizeof(node));
-			node.op = string_keys[i].op;
-			/* HEADER's field-name is an astring too */
-			if (node.op == SEARCH_OP_HEADER &&
-			    read_search_astring(&p, ctx, &node.name_off,
-			    &node.name_len, errmsg) == -1)
-				return (-1);
-			if (read_search_astring(&p, ctx, &node.str_off,
-			    &node.str_len, errmsg) == -1)
-				return (-1);
-
-			if (search_push(ctx, &node, errmsg) == -1)
-				return (-1);
-			*pp = p;
-			return (0);
-		}
-	}
-
-	*errmsg = "unknown search key";
-	return (-1);
+	if (search_push(ctx, &node, errmsg) == -1)
+		return (-1);
+	*pp = p;
+	return (0);
 }
 
-/* RFC 9051 SS6.4.4 search-key list, ANDed */
+/* RFC 9051 section 6.4.4 search-key list, ANDed */
 int
 parse_search_key_list(char **pp, struct search_parse_ctx *ctx,
     const char **errmsg, int in_parens)
 {
-	int	rc;
+	struct search_node	combine;
+	int			rc;
 
 	rc = parse_search_key(pp, ctx, errmsg);
 	if (rc != 0)
@@ -696,14 +583,10 @@ parse_search_key_list(char **pp, struct search_parse_c
 		if (rc != 0)
 			return (rc);
 
-		{
-			struct search_node	combine;
-
-			memset(&combine, 0, sizeof(combine));
-			combine.op = SEARCH_OP_AND;
-			if (search_push(ctx, &combine, errmsg) == -1)
-				return (-1);
-		}
+		memset(&combine, 0, sizeof(combine));
+		combine.op = SEARCH_OP_AND;
+		if (search_push(ctx, &combine, errmsg) == -1)
+			return (-1);
 	}
 }
 
@@ -719,7 +602,7 @@ search_program_parse(char *args, struct search_node *n
 
 	memset(&ctx, 0, sizeof(ctx));
 	rc = parse_search_key_list(&p, &ctx, &errmsg, 0);
-	/* RFC 9051 SS7.1: LIMIT, an implementation limit was reached */
+	/* RFC 9051 section 7.1: LIMIT, an implementation limit was reached */
 	if (rc == 0 && ctx.operand_len > SEARCH_OPERANDS_MAX) {
 		errmsg = "[LIMIT] search strings exceed 4096 octets in all";
 		rc = -2;
@@ -741,7 +624,7 @@ search_program_parse(char *args, struct search_node *n
 	return (0);
 }
 
-/* RFC 9051 SS6.4.4 search-return-opts */
+/* RFC 9051 section 6.4.4 search-return-opts */
 int
 parse_search_return_opts(char **pp, uint32_t *opts_out, const char **errmsg)
 {
@@ -800,7 +683,7 @@ parse_search_return_opts(char **pp, uint32_t *opts_out
 	}
 }
 
-/* RFC 9051 SS9 charset: atom / quoted */
+/* RFC 9051 section 9 charset: atom / quoted */
 static int
 read_search_charset(char **pp, char *out, size_t outsize)
 {
@@ -840,29 +723,31 @@ read_search_charset(char **pp, char *out, size_t outsi
 static void	 search_dispatch_finish(struct session *,
 		    const struct search_parse_result *, struct search_node *);
 
-/* RFC 9051 SS6.4.4.1: a SAVE answered NO here empties "$" too */
+/* RFC 9051 section 6.4.4.1: a SAVE answered NO here empties "$" too */
 static void
 search_saved_clear(struct session *s, uint32_t opts)
 {
 	if ((opts & SEARCH_RETURN_SAVE) && s->store_iev != NULL)
-		(void)send_mbox_request(s, IMSG_MBOX_SAVED_CLEAR, "SEARCH",
+		(void)send_mbox_request(s, IMSG_MBOX_SAVED_CLEAR,
 		    "IMSG_MBOX_SAVED_CLEAR", NULL, 0, NULL, 0, 0);
 }
 
-/* RFC 9051 SS6.4.4 SEARCH; US-ASCII and UTF-8 only */
+/* RFC 9051 section 6.4.4 SEARCH; US-ASCII and UTF-8 only */
 int
 cmd_search(struct session *s, const char *tag, char *args)
 {
-	return search_dispatch(s, tag, args, 0);
+	return (search_dispatch(s, tag, args, 0));
 }
 
 int
 search_dispatch(struct session *s, const char *tag, char *args, int by_uid)
 {
+	struct search_parse_result	 res;
+	struct search_node		 nodes[SEARCH_PROGRAM_MAX_NODES];
 	char				*p;
-	uint32_t			 return_opts = 0;
-	int				 rc;
 	const char			*errmsg;
+	uint32_t			 return_opts = 0;
+	int				 rc;
 
 	if (args == NULL) {
 		session_reply(s, tag, "BAD", "SEARCH requires search criteria");
@@ -893,7 +778,8 @@ search_dispatch(struct session *s, const char *tag, ch
 	}
 
 	if (return_opts == 0)
-		return_opts = SEARCH_RETURN_ALL;	/* RFC 9051 SS6.4.4 */
+		/* RFC 9051 section 6.4.4 */
+		return_opts = SEARCH_RETURN_ALL;
 
 	if (strncasecmp(p, "CHARSET", 7) == 0 &&
 	    (p[7] == ' ' || p[7] == '\0')) {
@@ -910,7 +796,7 @@ search_dispatch(struct session *s, const char *tag, ch
 		if (strcasecmp(charset, "US-ASCII") != 0 &&
 		    strcasecmp(charset, "UTF-8") != 0) {
 			search_saved_clear(s, return_opts);
-			/* RFC 9051 SS9 puts the charset list in parens */
+			/* RFC 9051 section 9 puts the charset list in parens */
 			session_reply(s, tag, "NO",
 			    "[BADCHARSET (US-ASCII UTF-8)] unsupported "
 			    "CHARSET");
@@ -933,35 +819,27 @@ search_dispatch(struct session *s, const char *tag, ch
 		return (1);
 	}
 
-	{
-		struct search_parse_result	 res;
-		struct search_node	 nodes[SEARCH_PROGRAM_MAX_NODES];
+	memset(&res, 0, sizeof(res));
+	res.rc = search_program_parse(p, nodes, &res.nnodes, &res.uses_modseq,
+	    res.pool, &res.poollen, res.errmsg, sizeof(res.errmsg));
+	if (res.rc == -1 && strcmp(res.errmsg, literal_wanted) == 0)
+		return (session_arg_error(s, tag, literal_wanted));
 
-		memset(&res, 0, sizeof(res));
-		res.rc = search_program_parse(p, nodes, &res.nnodes,
-		    &res.uses_modseq, res.pool, &res.poollen, res.errmsg,
-		    sizeof(res.errmsg));
-		if (res.rc == -1 && strcmp(res.errmsg, literal_wanted) == 0)
-			return (session_arg_error(s, tag, literal_wanted));
+	free(s->search_matches);
+	s->search_matches = NULL;
+	s->search_nmatches = 0;
+	s->search_matches_cap = 0;
+	s->search_alloc_failed = 0;
+	s->search_return_opts = return_opts;
+	s->cmd_by_uid = by_uid;
 
-		free(s->search_matches);
-		s->search_matches = NULL;
-		s->search_nmatches = 0;
-		s->search_matches_cap = 0;
-		s->search_alloc_failed = 0;
-		s->search_return_opts = return_opts;
-		s->cmd_by_uid = by_uid;
-
-		if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
-		    sizeof(s->pending_tag)) {
-			search_saved_clear(s, return_opts);
-			session_reply(s, tag, "NO",
-			    "[SERVERBUG] internal error");
-			return (1);
-		}
-		search_dispatch_finish(s, &res,
-		    res.nnodes > 0 ? nodes : NULL);
+	if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
+	    sizeof(s->pending_tag)) {
+		search_saved_clear(s, return_opts);
+		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
+		return (1);
 	}
+	search_dispatch_finish(s, &res, res.nnodes > 0 ? nodes : NULL);
 
 	return (1);
 }
@@ -970,6 +848,8 @@ static void
 search_dispatch_finish(struct session *s,
     const struct search_parse_result *res, struct search_node *nodes)
 {
+	struct imsg_mbox_search	 req;
+
 	if (res->rc == -1) {
 		session_reply(s, s->pending_tag, "BAD", res->errmsg);
 		s->state = SESSION_SELECTED;
@@ -985,30 +865,25 @@ search_dispatch_finish(struct session *s,
 	s->search_used_modseq = res->uses_modseq;
 	s->search_max_modseq = 0;
 
-	/* RFC 7162 SS3.1: MODSEQ enables CONDSTORE */
+	/* RFC 7162 section 3.1: MODSEQ enables CONDSTORE */
 	if (res->uses_modseq)
 		session_condstore_enable(s);
 
 	s->state = SESSION_SEARCHING;
 
-	{
-		struct imsg_mbox_search	 req;
+	memset(&req, 0, sizeof(req));
+	req.nnodes = res->nnodes;
+	req.poollen = res->poollen;
+	req.return_opts = s->search_return_opts;
+	memcpy(req.pool, res->pool, res->poollen);
 
-		memset(&req, 0, sizeof(req));
-		req.nnodes = res->nnodes;
-		req.poollen = res->poollen;
-		req.return_opts = s->search_return_opts;
-		memcpy(req.pool, res->pool, res->poollen);
-
-		if (!send_mbox_request(s, IMSG_MBOX_SEARCH, "SEARCH",
-		    "IMSG_MBOX_SEARCH", &req, sizeof(req), nodes, res->nnodes,
-		    sizeof(struct search_node))) {
-			search_saved_clear(s, s->search_return_opts);
-			session_reply(s, s->pending_tag, "NO",
-			    "[SERVERBUG] internal error");
-			s->state = SESSION_SELECTED;
-			return;
-		}
+	if (send_mbox_request(s, IMSG_MBOX_SEARCH,
+	    "IMSG_MBOX_SEARCH", &req, sizeof(req), nodes, res->nnodes,
+	    sizeof(struct search_node)) == -1) {
+		search_saved_clear(s, s->search_return_opts);
+		session_reply(s, s->pending_tag, "NO",
+		    "[SERVERBUG] internal error");
+		s->state = SESSION_SELECTED;
 	}
 }
 
@@ -1022,9 +897,9 @@ session_handle_mbox_search_match(struct session *s,
 	if (s->search_nmatches == s->search_matches_cap) {
 		uint32_t	 newcap = s->search_matches_cap ?
 		    s->search_matches_cap * 2 : 32;
-		uint32_t	*n = reallocarray(s->search_matches, newcap,
-		    sizeof(uint32_t));
+		uint32_t	*n;
 
+		n = reallocarray(s->search_matches, newcap, sizeof(uint32_t));
 		if (n == NULL) {
 			log_warn("session %u: realloc SEARCH match array",
 			    s->id);
@@ -1035,11 +910,11 @@ session_handle_mbox_search_match(struct session *s,
 		s->search_matches_cap = newcap;
 	}
 
-	/* RFC 9051 SS6.4.9: UID SEARCH reports UIDs */
+	/* RFC 9051 section 6.4.9: UID SEARCH reports UIDs */
 	s->search_matches[s->search_nmatches++] = s->cmd_by_uid ?
 	    m->uid : m->seqno;
 
-	/* RFC 7162 SS3.1.6 */
+	/* RFC 7162 section 3.1.6 */
 	if (m->modseq > s->search_max_modseq)
 		s->search_max_modseq = m->modseq;
 }
@@ -1047,7 +922,7 @@ session_handle_mbox_search_match(struct session *s,
 #define SEARCH_RESP_PREFIX_MAX	256
 #define SEARCH_ALL_PER_MATCH	11	/* "4294967295" + one separator */
 
-/* RFC 9051 SS7.3.4 ESEARCH response */
+/* RFC 9051 section 7.3.4 ESEARCH response */
 void
 session_finish_search(struct session *s, struct imsg_mbox_result *res)
 {
@@ -1072,7 +947,7 @@ session_finish_search(struct session *s, struct imsg_m
 	}
 	if (res->error != MBOX_OP_OK || s->search_alloc_failed)
 		goto fail;
-	/* RFC 9051 SS6.4.4: SAVE alone suppresses ESEARCH */
+	/* RFC 9051 section 6.4.4: SAVE alone suppresses ESEARCH */
 	if (s->search_return_opts == SEARCH_RETURN_SAVE)
 		goto done;
 
@@ -1091,7 +966,7 @@ session_finish_search(struct session *s, struct imsg_m
 	}
 	len = (size_t)n;
 
-	/* RFC 9051 SS9: "UID" after correlator, before MIN/MAX/ALL/MODSEQ */
+	/* RFC 9051 section 9: "UID" after the correlator, before the others */
 	if (s->cmd_by_uid && len < bufsize)
 		len += (size_t)snprintf(buf + len, bufsize - len, " UID");
 
@@ -1116,11 +991,9 @@ session_finish_search(struct session *s, struct imsg_m
 			    s->search_nmatches, &truncated);
 			len += listlen;
 			if (truncated)
-				log_warnx("session %u: SEARCH ALL response "
-				    "truncated at %zu bytes (%u total "
-				    "matches) -- can't happen, bufsize is "
-				    "sized for the worst case", s->id,
-				    bufsize, s->search_nmatches);
+				fatalx("session %u: SEARCH ALL truncated at "
+				    "%zu bytes (%u matches)", s->id, bufsize,
+				    s->search_nmatches);
 		}
 	}
 
@@ -1128,7 +1001,7 @@ session_finish_search(struct session *s, struct imsg_m
 		len += (size_t)snprintf(buf + len, bufsize - len,
 		    " COUNT %u", s->search_nmatches);
 
-	/* RFC 7162 SS3.1.10: MODSEQ in ESEARCH */
+	/* RFC 7162 section 3.1.10: MODSEQ in ESEARCH */
 	if (s->search_used_modseq && s->search_nmatches > 0 &&
 	    len < bufsize)
 		len += (size_t)snprintf(buf + len, bufsize - len,
@@ -1144,7 +1017,7 @@ session_finish_search(struct session *s, struct imsg_m
 	free(buf);
 
 done:
-	/* as RFC 9051 SS6.4.9's "UID <cmd> completed" */
+	/* as RFC 9051 section 6.4.9's "UID <cmd> completed" */
 	session_reply(s, s->pending_tag, "OK",
 	    s->cmd_by_uid ? "UID SEARCH completed" : "SEARCH completed");
 	goto cleanup;
blob - 39a322115575c507c1a25286fcf680d5f615b106
blob + b2237913a69fabeb9c2f650a4f8f514a76dbb15c
--- src/search_match.c
+++ src/search_match.c
@@ -97,7 +97,7 @@ static int	 match_body(const char *, size_t, size_t,
 		    const struct imsg_parser_leaf *, uint32_t, const char *,
 		    char *);
 
-/* RFC 9051 SS6.4.4 folds the ASCII range only, byte by byte */
+/* RFC 9051 section 6.4.4 folds the ASCII range only, byte by byte */
 static int
 ascii_lower(int c)
 {
@@ -154,7 +154,7 @@ hex_value(int c)
 	return (-1);
 }
 
-/* RFC 2047 SS2 encoded-word */
+/* RFC 2047 section 2 encoded-word */
 static size_t
 decode_word(const char *w, size_t len, char *out)
 {
@@ -225,7 +225,7 @@ encoded_word_len(const char *s, size_t len)
 	return (0);
 }
 
-/* RFC 5322 SS2.2.3 unfolding and RFC 2047 SS6.1 decoding */
+/* RFC 5322 section 2.2.3 unfolding and RFC 2047 section 6.1 decoding */
 static char *
 decode_value(const char *v, size_t vlen, size_t *outlen)
 {
@@ -273,14 +273,14 @@ decode_value(const char *v, size_t vlen, size_t *outle
 	return (out);
 }
 
-/* RFC 5322 SS3.3 allows FWS, folds included, between a date's tokens */
+/* RFC 5322 section 3.3 allows FWS, folds included, between a date's tokens */
 static int
 date_fws(int c)
 {
 	return (c == ' ' || c == '\t' || c == '\r' || c == '\n');
 }
 
-/* RFC 9051 SS6.4.4: the day only, ignoring time and zone */
+/* RFC 9051 section 6.4.4: the day only, ignoring time and zone */
 static int
 date_field_day(const char *v, size_t vlen, int64_t *out)
 {
@@ -291,7 +291,7 @@ date_field_day(const char *v, size_t vlen, int64_t *ou
 
 	while (p < end && date_fws((unsigned char)*p))
 		p++;
-	/* RFC 5322 SS3.3: day-of-week is optional */
+	/* RFC 5322 section 3.3: day-of-week is optional */
 	if (p < end && ascii_lower((unsigned char)*p) >= 'a' &&
 	    ascii_lower((unsigned char)*p) <= 'z') {
 		while (p < end && *p != ',')
@@ -402,7 +402,7 @@ toolarge:
 	return (-1);
 }
 
-/* RFC 9051 SS6.4.4: addresses as ENVELOPE shows them, not group names */
+/* RFC 9051 section 6.4.4: addresses as ENVELOPE shows them, not group names */
 static int
 match_addresses(const char *v, size_t vlen, const char *needle,
     size_t needlelen)
@@ -412,7 +412,7 @@ match_addresses(const char *v, size_t vlen, const char
 	size_t		 i, ulen = 0, pos = 0, toklen, declen;
 	int		 hit = 0, ingroup = 0, kind;
 
-	/* RFC 5322 SS2.2.3: unfolding drops CR and LF */
+	/* RFC 5322 section 2.2.3: unfolding drops CR and LF */
 	if ((unf = malloc(vlen + 1)) == NULL)
 		return (-1);
 	for (i = 0; i < vlen; i++) {
@@ -563,7 +563,7 @@ kmp_contains(const char *hay, size_t haylen, const cha
 	return (0);
 }
 
-/* RFC 2045 SS6.8: characters outside the alphabet are ignored */
+/* RFC 2045 section 6.8: characters outside the alphabet are ignored */
 static size_t
 decode_b64(const char *in, size_t len, char *out)
 {
@@ -583,7 +583,7 @@ decode_b64(const char *in, size_t len, char *out)
 	return (n);
 }
 
-/* RFC 2045 SS6.7; an "=" that is neither form is kept as it is */
+/* RFC 2045 section 6.7; an "=" that is neither form is kept as it is */
 static size_t
 decode_qp(const char *in, size_t len, char *out)
 {
@@ -635,7 +635,7 @@ scan_text(struct body_walk *w, const char *t, size_t l
 	}
 }
 
-/* RFC 9051 SS6.4.4: TEXT sees every header, MIME part headers too */
+/* RFC 9051 section 6.4.4: TEXT sees every header, MIME part headers too */
 static int
 scan_header(struct body_walk *w, const char *hdr, size_t hdrlen)
 {
@@ -689,7 +689,7 @@ scan_body(struct body_walk *w, const char *body, size_
 	return (0);
 }
 
-/* RFC 2045 SS5.2, SS6.4; RFC 2046 SS5.1.3, SS5.1.5 */
+/* RFC 2045 section 5.2, section 6.4; RFC 2046 section 5.1.3, section 5.1.5 */
 static int
 part_type(const char *hdr, size_t hdrlen, int digest, char *boundary,
     size_t boundarysize, int *enc, int *subdigest)
@@ -714,14 +714,14 @@ part_type(const char *hdr, size_t hdrlen, int digest, 
 		    (vallen == 4 && (strncasecmp(val, "7bit", 4) == 0 ||
 		    strncasecmp(val, "8bit", 4) == 0)) ||
 		    (vallen == 6 && strncasecmp(val, "binary", 6) == 0))) {
-			/* SS6.4: an unknown one makes it octet-stream */
+			/* section 6.4: an unknown one makes it octet-stream */
 			free(val);
 			return (PART_SKIP);
 		}
 		free(val);
 	}
 
-	/* SS5.2: none, or an invalid one, is text/plain */
+	/* section 5.2: none, or an invalid one, is text/plain */
 	if (extract_header_field(hdr, hdrlen, "Content-Type", &val,
 	    &vallen) == -1 || vallen == 0) {
 		free(val);
@@ -753,7 +753,7 @@ part_type(const char *hdr, size_t hdrlen, int digest, 
 		if (mime_read_token_or_qstring(val, vallen, &pos, value,
 		    sizeof(value)) == -1)
 			break;
-		/* RFC 2046 SS5.1.1: at most 70 characters */
+		/* RFC 2046 section 5.1.1: at most 70 characters */
 		if (strcasecmp(attr, "boundary") == 0 &&
 		    strlcpy(boundary, value, boundarysize) < boundarysize)
 			has_boundary = 1;
@@ -761,7 +761,7 @@ part_type(const char *hdr, size_t hdrlen, int digest, 
 	free(val);
 
 	if (strcasecmp(type, "multipart") == 0) {
-		/* RFC 2046 SS5.1.1 requires the boundary: invalid without */
+		/* RFC 2046 section 5.1.1: invalid without a boundary */
 		if (!has_boundary)
 			return (PART_TEXT);
 		*subdigest = strcasecmp(subtype, "digest") == 0;
@@ -772,13 +772,13 @@ part_type(const char *hdr, size_t hdrlen, int digest, 
 		    PART_TEXT;
 	else
 		kind = strcasecmp(type, "text") == 0 ? PART_TEXT : PART_SKIP;
-	/* RFC 2046 SS5.1.1, SS5.2.1: these are never encoded */
+	/* RFC 2046 section 5.1.1, section 5.2.1: these are never encoded */
 	if (kind != PART_TEXT)
 		*enc = ENC_IDENTITY;
 	return (kind);
 }
 
-/* RFC 2046 SS5.1.1: the next "--boundary" line at or after *pos */
+/* RFC 2046 section 5.1.1: the next "--boundary" line at or after *pos */
 static int
 next_delimiter(const char *body, size_t len, const char *delim,
     size_t dlen, size_t *pos, size_t *start, int *closing)
@@ -828,9 +828,10 @@ walk_multipart(struct body_walk *w, int depth, const c
 	if (dlen >= sizeof(delim))
 		return (0);
 	for (;;) {
-		int	 found = next_delimiter(body, len, delim, dlen, &pos,
-		    &dstart, &closing);
+		int	 found;
 
+		found = next_delimiter(body, len, delim, dlen, &pos, &dstart,
+		    &closing);
 		if (!found)
 			dstart = len;
 		if (inpart) {
@@ -893,7 +894,7 @@ walk_entity(struct body_walk *w, int depth, const char
 	}
 }
 
-/* RFC 9051 SS6.4.4 BODY and TEXT over TEXT and MESSAGE parts */
+/* RFC 9051 section 6.4.4 BODY and TEXT over TEXT and MESSAGE parts */
 static int
 match_body(const char *msg, size_t hdrlen, size_t len,
     const struct imsg_parser_leaf *leaves, uint32_t nleaves,
blob - 39c29d5483acbef9f5451c27c58821b90f75bc4e
blob + 510e38cf36986d52626ac5b038cee07ca77e0914
--- src/store.c
+++ src/store.c
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
 #include <sys/types.h>
 #include <sys/file.h>
 #include <sys/stat.h>
@@ -100,11 +99,11 @@ store_main(void)
 {
 	struct imsgbuf		 ibuf3;
 	struct imsg		 imsg;
-	ssize_t			 n;
 	struct imsg_store_init	 init;
+	char			 unveil_path[sizeof(init.maildir) + 1];
+	ssize_t			 n;
 	gid_t			 gid;
 
-
 	imsgev_ibuf_init(&ibuf3, 3);
 
 	for (;;) {
@@ -151,21 +150,15 @@ store_main(void)
 	setproctitle("store uid %u", (unsigned int)init.uid);
 
 	/* confine before the ack, so a failure can be reported */
+	if (snprintf(unveil_path, sizeof(unveil_path), "/%s",
+	    init.maildir) >= (int)sizeof(unveil_path))
+		fatalx("session %u: maildir path too long: %s",
+		    session_id, init.maildir);
+	if (unveil(unveil_path, "rwc") == -1)
+		fatal("session %u: unveil %s", session_id, unveil_path);
+	if (chdir(unveil_path) == -1)
+		fatal("session %u: chdir %s", session_id, unveil_path);
 
-	{
-		char	unveil_path[sizeof(init.maildir) + 1];
-
-		if (snprintf(unveil_path, sizeof(unveil_path), "/%s",
-		    init.maildir) >= (int)sizeof(unveil_path))
-			fatalx("session %u: maildir path too long: %s",
-			    session_id, init.maildir);
-		if (unveil(unveil_path, "rwc") == -1)
-			fatal("unveil %s", unveil_path);
-
-		if (chdir(unveil_path) == -1)
-			fatal("chdir %s", unveil_path);
-	}
-
 	/* unveil(2) covers lookups relative to these (sys/kern/vfs_lookup.c) */
 	if ((maildir_root_fd = open(".", O_RDONLY | O_DIRECTORY)) == -1)
 		fatal("open maildir root");
@@ -181,14 +174,12 @@ store_main(void)
 	imsgev_init_from_ibuf(&parent_iev, &ibuf3, store_parent_dispatch,
 	    NULL);
 
-	/* the handshake may already have read the first attach: take it */
+	/* the handshake may have read a later message too: take it */
 	store_parent_dispatch(parent_iev.ibuf.fd, 0, &parent_iev);
 
 	/* sendfd: FETCH hands the listener a read-only descriptor */
-#ifdef __OpenBSD__
 	if (pledge("stdio rpath wpath cpath flock recvfd sendfd", NULL) == -1)
 		fatal("pledge");
-#endif
 
 	log_debug("session %u: store ready (uid %u, gid %u, spool %s, "
 	    "maildir %s)", session_id, init.uid, init.gid, init.spool_root,
@@ -255,7 +246,6 @@ store_parent_dispatch(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 static void
@@ -280,8 +270,8 @@ store_attach(uint32_t id, int fd)
 			break;
 	}
 	if (id == 0 || ss != NULL) {
-		log_warnx("session %u: refusing a session that is 0 or "
-		    "already attached", id);
+		log_warnx("session %u: attach refused: id 0 or already "
+		    "attached", id);
 		close(fd);
 		return;
 	}
@@ -345,7 +335,7 @@ parser_reply_safe(const char *buf, size_t len)
 	return (!inquote && !escaped && depth == 0);
 }
 
-/* RFC 9051 SS9: a literal may not hold NUL */
+/* RFC 9051 section 9: a literal may not hold NUL */
 static int
 parser_literal_safe(const char *buf, size_t len)
 {
@@ -543,9 +533,9 @@ parser_request(uint32_t type, const char *label, int f
 				*rep_out = rep;
 				rc = 0;
 			} else
-				log_warnx("session %u: parser-worker returned "
-				    "a %s that cannot be sent, message %s "
-				    "skipped", session_id, label, basename);
+				log_warnx("session %u: parser %s unsendable, "
+				    "message %s skipped",
+				    session_id, label, basename);
 		} else if (rep.found && rep.len > 0 && rep.len <= maxlen &&
 		    imsg_get_len(&imsg) == rep.len) {
 			if ((*buf_out = malloc(rep.len)) == NULL)
@@ -559,9 +549,9 @@ parser_request(uint32_t type, const char *label, int f
 				*buf_out = NULL;
 			} else if (!parser_reply_valid(type, *buf_out,
 			    rep.len, maxlen)) {
-				log_warnx("session %u: parser-worker returned "
-				    "a %s that cannot be sent, message %s "
-				    "skipped", session_id, label, basename);
+				log_warnx("session %u: parser %s unsendable, "
+				    "message %s skipped",
+				    session_id, label, basename);
 				free(*buf_out);
 				*buf_out = NULL;
 			} else {
@@ -658,9 +648,8 @@ parser_fail(const char *basename)
 		parser_strikes[i].count = 0;
 	}
 	if (++parser_strikes[i].count == PARSER_STRIKES)
-		log_warnx("session %u: message %s has failed the "
-		    "parser-worker %d times, not sending it again",
-		    session_id, basename, PARSER_STRIKES);
+		log_warnx("session %u: message %s: parser failed %d times, "
+		    "not retried", session_id, basename, PARSER_STRIKES);
 }
 
 static int
@@ -686,7 +675,7 @@ parser_install(int fd)
 		return;
 	}
 	if (parser_up) {
-		log_warnx("store: a second parser-worker, refusing it");
+		log_warnx("store: second parser-worker refused");
 		close(fd);
 		return;
 	}
@@ -723,9 +712,6 @@ parser_walks(int ok)
 static void
 parser_idle(int fd, short event, void *arg)
 {
-	(void)fd;
-	(void)event;
-	(void)arg;
 	log_debug("store: parser-worker idle, letting it go");
 	parser_drop();
 }
@@ -733,9 +719,6 @@ parser_idle(int fd, short event, void *arg)
 static void
 parser_wait_expired(int fd, short event, void *arg)
 {
-	(void)fd;
-	(void)event;
-	(void)arg;
 	log_warnx("store: no parser-worker within %d seconds",
 	    PARSER_REPLY_TIMEOUT_SEC);
 	parser_give_up();
@@ -744,18 +727,15 @@ parser_wait_expired(int fd, short event, void *arg)
 int
 mailbox_name_valid(const char *name)
 {
-	/* not NUL-terminated by imsg_get_data() */
 	if (memchr(name, '\0', MBOX_NAME_MAX) == NULL) {
-		log_warnx("session %u: mailbox name field is not "
-		    "NUL-terminated, refusing", session_id);
+		log_warnx("session %u: mailbox name not NUL-terminated",
+		    session_id);
 		return (0);
 	}
 
 	return (mailbox_name_syntax_ok(name));
 }
 
-
-
 int
 mailbox_open_dir(const char *target)
 {
@@ -763,13 +743,29 @@ mailbox_open_dir(const char *target)
 	    O_RDONLY | O_DIRECTORY));
 }
 
+/* opendir(3) relative to dfd; NULL with errno set, nothing left open */
+DIR *
+opendirat(int dfd, const char *name)
+{
+	DIR	*dp;
+	int	 fd, saved_errno;
+
+	if ((fd = openat(dfd, name, O_RDONLY | O_DIRECTORY)) == -1)
+		return (NULL);
+	if ((dp = fdopendir(fd)) == NULL) {
+		saved_errno = errno;
+		close(fd);
+		errno = saved_errno;
+	}
+	return (dp);
+}
+
 static int
 imsg_field_valid(const char *field, size_t size, const char *what)
 {
 	if (memchr(field, '\0', size) != NULL)
 		return (1);
-	log_warnx("session %u: %s is not NUL-terminated, refusing the "
-	    "request", session_id, what);
+	log_warnx("session %u: %s not NUL-terminated", session_id, what);
 	return (0);
 }
 
@@ -783,18 +779,16 @@ search_nodes_valid(const struct search_node *nodes, ui
 	for (i = 0; i < nnodes; i++) {
 		n = &nodes[i];
 		if (memchr(n->keyword, '\0', sizeof(n->keyword)) == NULL) {
-			log_warnx("session %u: SEARCH node %u keyword is not "
-			    "NUL-terminated, refusing the request",
-			    session_id, i);
+			log_warnx("session %u: SEARCH node %u: keyword not "
+			    "NUL-terminated", session_id, i);
 			return (0);
 		}
 		if (search_op_content(n->op) && (n->str_off > poollen ||
 		    n->str_len > poollen - n->str_off ||
 		    n->name_off > poollen ||
 		    n->name_len > poollen - n->name_off)) {
-			log_warnx("session %u: SEARCH node %u string lies "
-			    "outside its pool, refusing the request",
-			    session_id, i);
+			log_warnx("session %u: SEARCH node %u: string outside "
+			    "its pool", session_id, i);
 			return (0);
 		}
 	}
@@ -817,9 +811,9 @@ recv_trailing_array(struct imsg *imsg, const char *wha
 	}
 	bodylen = imsg_get_len(imsg);
 	if (bodylen != (size_t)count * elemsize) {
-		log_warnx("session %u: %s length mismatch (header says %u "
-		    "elements, imsg has %zu bytes)", session_id, what, count,
-		    bodylen);
+		log_warnx("session %u: %s length mismatch (header %u "
+		    "elements, imsg %zu bytes)",
+		    session_id, what, count, bodylen);
 		return (NULL);
 	}
 	*ok_out = 1;
@@ -863,7 +857,7 @@ deferred_expired(struct store_session *ss)
 	return (now.tv_nsec >= d->give_up_at.tv_nsec);
 }
 
-/* RFC 9051 SS7.1 INUSE: answers one command, having changed nothing. */
+/* RFC 9051 section 7.1 INUSE: answers one command, having changed nothing. */
 static void
 deferred_answer_busy_for(uint32_t type, struct store_session *ss)
 {
@@ -876,7 +870,7 @@ deferred_answer_busy_for(uint32_t type, struct store_s
 	size_t				 len = 0;
 	uint32_t			 rtype = 0;
 
-	/* RFC 9051 SS6.4.4.1: a SAVE answered NO empties "$" */
+	/* RFC 9051 section 6.4.4.1: a SAVE answered NO empties "$" */
 	if (type == IMSG_MBOX_SEARCH &&
 	    (ss->deferred.req.search.return_opts & SEARCH_RETURN_SAVE))
 		ss->nsaved = 0;
@@ -917,10 +911,8 @@ deferred_answer_busy_for(uint32_t type, struct store_s
 		len = sizeof(appended);
 		break;
 	default:
-		log_warnx("session %u: no busy reply for imsg %u, can't "
-		    "happen; only deferrable types are deferred", session_id,
+		fatalx("session %u: no busy reply for imsg %u", session_id,
 		    type);
-		break;
 	}
 	if (data != NULL && imsg_compose(&iev->ibuf, rtype, 0, 0, -1, data,
 	    len) == -1)
@@ -949,9 +941,8 @@ deferred_arm(struct store_session *ss)
 	tv.tv_usec = (d->wait_ms % 1000) * 1000;
 	evtimer_set(&d->ev, deferred_retry, ss);
 	if (evtimer_add(&d->ev, &tv) == -1) {
-		log_warnx("session %u: no timer for the index lock retry; "
-		    "answering busy now rather than waiting for ever",
-		    session_id);
+		log_warnx("session %u: evtimer_add (lock retry), answering "
+		    "busy", session_id);
 		deferred_give_up(ss);
 		return;
 	}
@@ -966,8 +957,6 @@ deferred_retry(int fd, short event, void *arg)
 	struct store_deferred	*d = &ss->deferred;
 	int			 again;
 
-	(void)fd;
-	(void)event;
 	session_id = ss->id;
 
 	switch (d->type) {
@@ -1007,10 +996,7 @@ deferred_retry(int fd, short event, void *arg)
 		again = handle_mbox_append_end(ss);
 		break;
 	default:
-		log_warnx("session %u: cannot re-run imsg %u, can't happen",
-		    session_id, d->type);
-		deferred_give_up(ss);
-		return;
+		fatalx("session %u: deferred imsg %u", session_id, d->type);
 	}
 	if (!again) {
 		d->active = 0;
@@ -1034,19 +1020,15 @@ defer_command(uint32_t type, const void *req, size_t r
 	struct timespec		 now;
 
 	if (d->active) {
-		log_warnx("session %u: a second command to defer while one "
-		    "waits, refusing the new one", session_id);
+		log_warnx("session %u: a second deferred command, refused",
+		    session_id);
 		deferred_answer_busy_for(type, ss);
 		return;
 	}
 	if (reqlen > sizeof(d->req) || nelts > maxelts ||
-	    (size_t)nelts * eltlen > sizeof(d->elts)) {
-		/* the handler answered nothing, so something must */
-		log_warnx("session %u: imsg %u too large to defer, can't "
-		    "happen, it is checked on receipt", session_id, type);
-		deferred_answer_busy_for(type, ss);
-		return;
-	}
+	    (size_t)nelts * eltlen > sizeof(d->elts))
+		fatalx("session %u: imsg %u too large to defer", session_id,
+		    type);
 	if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
 		log_warn("session %u: clock_gettime", session_id);
 		now.tv_sec = 0;
@@ -1074,18 +1056,52 @@ require_mailbox_selected(struct store_session *ss, con
 {
 	if (ss->mailbox_selected)
 		return (1);
-	log_warnx("session %u: %s before a successful IMSG_MBOX_SELECT, "
-	    "refusing", session_id, what);
+	log_warnx("session %u: %s before IMSG_MBOX_SELECT, refused",
+	    session_id, what);
 	return (0);
 }
 
+static struct seq_range *
+recv_ranges(struct imsg *imsg, const char *what, uint32_t nranges, int *ok)
+{
+	return (recv_trailing_array(imsg, what, nranges, SEQSET_MAX_RANGES,
+	    sizeof(struct seq_range), ok));
+}
+
+static int
+ranges_required(uint32_t nranges, const char *what)
+{
+	if (nranges != 0)
+		return (1);
+	log_warnx("session %u: %s requires at least one range", session_id,
+	    what);
+	return (0);
+}
+
+/* rc is the handler's: 1 keeps the command for the index lock */
+static void
+ranged_done(struct store_session *ss, uint32_t type, const void *req,
+    size_t reqlen, struct seq_range *ranges, uint32_t nranges, int rc)
+{
+	if (rc == 1)
+		defer_command(type, req, reqlen, ranges, nranges,
+		    SEQSET_MAX_RANGES, sizeof(*ranges), ss);
+	free(ranges);
+}
+
 void
 store_dispatch(int fd, short event, void *arg)
 {
 	struct store_session	*ss = arg;
 	struct imsgev		*iev = &ss->iev;
-	struct imsg	 imsg;
-	ssize_t		 n;
+	struct imsg		 imsg;
+	union mbox_request	 u;
+	struct seq_range	*ranges;
+	struct search_node	*nodes;
+	char			 buf[MAX_IMSGSIZE];
+	size_t			 len;
+	ssize_t			 n;
+	int			 ok, rc;
 
 	/* the account's other sessions share this process: label the log */
 	session_id = ss->id;
@@ -1128,126 +1144,78 @@ store_dispatch(int fd, short event, void *arg)
 			imsg_free(&imsg);
 			store_end(ss);
 			return;
-		case IMSG_MBOX_SELECT: {
-			struct imsg_mbox_select		 req;
-			struct seq_range		*ranges;
-			int				 ok;
-
-			if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_SELECT (header)");
+		case IMSG_MBOX_SELECT:
+			if (!recv_header(&imsg, &u.select, sizeof(u.select),
+			    "IMSG_MBOX_SELECT"))
 				break;
-			}
-			ranges = recv_trailing_array(&imsg, "IMSG_MBOX_SELECT",
-			    req.qresync_nranges, SEQSET_MAX_RANGES,
-			    sizeof(struct seq_range), &ok);
+			ranges = recv_ranges(&imsg, "IMSG_MBOX_SELECT",
+			    u.select.qresync_nranges, &ok);
 			if (!ok)
 				break;
-			/* RFC 9051 SS6.3.2: failure deselects */
+			/* RFC 9051 section 6.3.2: failure deselects */
 			ss->mailbox_selected = 0;
-
-			if (handle_mbox_select(&req, ranges,
-			    req.qresync_nranges, ss) == 1)
-				defer_command(IMSG_MBOX_SELECT, &req,
-				    sizeof(req), ranges, req.qresync_nranges,
-				    SEQSET_MAX_RANGES, sizeof(*ranges), ss);
-			free(ranges);
+			rc = handle_mbox_select(&u.select, ranges,
+			    u.select.qresync_nranges, ss);
+			ranged_done(ss, IMSG_MBOX_SELECT, &u.select,
+			    sizeof(u.select), ranges, u.select.qresync_nranges,
+			    rc);
 			break;
-		}
-		case IMSG_MBOX_FETCH: {
-			struct imsg_mbox_fetch	 req;
-			struct seq_range	*ranges;
-			int			 ok;
-
-			if (!require_mailbox_selected(ss, "IMSG_MBOX_FETCH"))
-				break;
-
-			if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_FETCH (header)");
-				break;
-			}
-			if (!imsg_field_valid(req.header_fields,
-			    sizeof(req.header_fields),
+		case IMSG_MBOX_FETCH:
+			if (!require_mailbox_selected(ss, "IMSG_MBOX_FETCH") ||
+			    !recv_header(&imsg, &u.fetch, sizeof(u.fetch),
+			    "IMSG_MBOX_FETCH") ||
+			    !imsg_field_valid(u.fetch.header_fields,
+			    sizeof(u.fetch.header_fields),
 			    "IMSG_MBOX_FETCH.header_fields") ||
-			    !imsg_field_valid(req.section_part,
-			    sizeof(req.section_part),
-			    "IMSG_MBOX_FETCH.section_part"))
+			    !imsg_field_valid(u.fetch.section_part,
+			    sizeof(u.fetch.section_part),
+			    "IMSG_MBOX_FETCH.section_part") ||
+			    !ranges_required(u.fetch.nranges,
+			    "IMSG_MBOX_FETCH"))
 				break;
-			if (req.nranges == 0) {
-				log_warnx("session %u: IMSG_MBOX_FETCH "
-				    "requires at least one range", session_id);
-				break;
-			}
-			ranges = recv_trailing_array(&imsg, "IMSG_MBOX_FETCH",
-			    req.nranges, SEQSET_MAX_RANGES,
-			    sizeof(struct seq_range), &ok);
+			ranges = recv_ranges(&imsg, "IMSG_MBOX_FETCH",
+			    u.fetch.nranges, &ok);
 			if (!ok)
 				break;
-			if (handle_mbox_fetch(&req, ranges, req.nranges,
-			    ss) == 1)
-				defer_command(IMSG_MBOX_FETCH, &req,
-				    sizeof(req), ranges, req.nranges,
-				    SEQSET_MAX_RANGES, sizeof(*ranges), ss);
-			free(ranges);
+			rc = handle_mbox_fetch(&u.fetch, ranges,
+			    u.fetch.nranges, ss);
+			ranged_done(ss, IMSG_MBOX_FETCH, &u.fetch,
+			    sizeof(u.fetch), ranges, u.fetch.nranges, rc);
 			break;
-		}
-		case IMSG_MBOX_STORE: {
-			struct imsg_mbox_store	 req;
-			struct seq_range	*ranges;
-			int			 ok;
-
-			if (!require_mailbox_selected(ss, "IMSG_MBOX_STORE"))
+		case IMSG_MBOX_STORE:
+			if (!require_mailbox_selected(ss, "IMSG_MBOX_STORE") ||
+			    !recv_header(&imsg, &u.store, sizeof(u.store),
+			    "IMSG_MBOX_STORE") ||
+			    !imsg_field_valid(u.store.keywords,
+			    sizeof(u.store.keywords),
+			    "IMSG_MBOX_STORE.keywords") ||
+			    !ranges_required(u.store.nranges,
+			    "IMSG_MBOX_STORE"))
 				break;
-
-			if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_STORE (header)");
-				break;
-			}
-			if (!imsg_field_valid(req.keywords,
-			    sizeof(req.keywords), "IMSG_MBOX_STORE.keywords"))
-				break;
-			if (req.nranges == 0) {
-				log_warnx("session %u: IMSG_MBOX_STORE "
-				    "requires at least one range", session_id);
-				break;
-			}
-			ranges = recv_trailing_array(&imsg, "IMSG_MBOX_STORE",
-			    req.nranges, SEQSET_MAX_RANGES,
-			    sizeof(struct seq_range), &ok);
+			ranges = recv_ranges(&imsg, "IMSG_MBOX_STORE",
+			    u.store.nranges, &ok);
 			if (!ok)
 				break;
-			if (handle_mbox_store(&req, ranges, req.nranges,
-			    ss) == 1)
-				defer_command(IMSG_MBOX_STORE, &req,
-				    sizeof(req), ranges, req.nranges,
-				    SEQSET_MAX_RANGES, sizeof(*ranges), ss);
-			free(ranges);
+			rc = handle_mbox_store(&u.store, ranges,
+			    u.store.nranges, ss);
+			ranged_done(ss, IMSG_MBOX_STORE, &u.store,
+			    sizeof(u.store), ranges, u.store.nranges, rc);
 			break;
-		}
-		case IMSG_MBOX_EXPUNGE: {
-			struct imsg_mbox_expunge	 req;
-			struct seq_range		*ranges;
-			int				 ok;
-
-			if (!require_mailbox_selected(ss, "IMSG_MBOX_EXPUNGE"))
+		case IMSG_MBOX_EXPUNGE:
+			if (!require_mailbox_selected(ss,
+			    "IMSG_MBOX_EXPUNGE") ||
+			    !recv_header(&imsg, &u.expunge, sizeof(u.expunge),
+			    "IMSG_MBOX_EXPUNGE"))
 				break;
-
-			if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_EXPUNGE (header)");
-				break;
-			}
-			ranges = recv_trailing_array(&imsg,
-			    "IMSG_MBOX_EXPUNGE", req.nranges,
-			    SEQSET_MAX_RANGES, sizeof(struct seq_range), &ok);
+			ranges = recv_ranges(&imsg, "IMSG_MBOX_EXPUNGE",
+			    u.expunge.nranges, &ok);
 			if (!ok)
 				break;
-			if (handle_mbox_expunge(&req, ranges, req.nranges,
-			    ss) == 1)
-				defer_command(IMSG_MBOX_EXPUNGE, &req,
-				    sizeof(req), ranges, req.nranges,
-				    SEQSET_MAX_RANGES, sizeof(*ranges), ss);
-			free(ranges);
+			rc = handle_mbox_expunge(&u.expunge, ranges,
+			    u.expunge.nranges, ss);
+			ranged_done(ss, IMSG_MBOX_EXPUNGE, &u.expunge,
+			    sizeof(u.expunge), ranges, u.expunge.nranges, rc);
 			break;
-		}
 		case IMSG_MBOX_IDLE_REFRESH:
 			if (imsg_get_len(&imsg) != 0) {
 				log_warnx("bad IMSG_MBOX_IDLE_REFRESH");
@@ -1258,24 +1226,16 @@ store_dispatch(int fd, short event, void *arg)
 			    "IMSG_MBOX_IDLE_REFRESH");
 			handle_mbox_idle_refresh(ss);
 			break;
-		case IMSG_MBOX_APPEND: {
-			struct imsg_mbox_append	 req;
-
+		case IMSG_MBOX_APPEND:
 			/* no reply: the literal follows regardless */
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_APPEND");
-				break;
-			}
-			if (!imsg_field_valid(req.keywords,
-			    sizeof(req.keywords), "IMSG_MBOX_APPEND.keywords"))
-				break;
-			handle_mbox_append_begin(ss, &req);
+			if (recv_fixed(&imsg, &u.append, sizeof(u.append),
+			    "IMSG_MBOX_APPEND") &&
+			    imsg_field_valid(u.append.keywords,
+			    sizeof(u.append.keywords),
+			    "IMSG_MBOX_APPEND.keywords"))
+				handle_mbox_append_begin(ss, &u.append);
 			break;
-		}
-		case IMSG_MBOX_APPEND_DATA: {
-			char		buf[MAX_IMSGSIZE];
-			size_t		len;
-
+		case IMSG_MBOX_APPEND_DATA:
 			len = imsg_get_len(&imsg);
 			if (len == 0 || len > sizeof(buf) ||
 			    imsg_get_buf(&imsg, buf, len) == -1) {
@@ -1284,45 +1244,38 @@ store_dispatch(int fd, short event, void *arg)
 			}
 			handle_mbox_append_data(ss, buf, len);
 			break;
-		}
 		case IMSG_MBOX_APPEND_END:
 			if (handle_mbox_append_end(ss) == 1)
 				defer_command(IMSG_MBOX_APPEND_END, NULL, 0,
 				    NULL, 0, 0, 0, ss);
 			break;
-		case IMSG_MBOX_SEARCH: {
-			struct imsg_mbox_search	 req;
-			struct search_node	*nodes;
-			int			 ok;
-
-			if (!require_mailbox_selected(ss, "IMSG_MBOX_SEARCH"))
+		case IMSG_MBOX_SEARCH:
+			if (!require_mailbox_selected(ss, "IMSG_MBOX_SEARCH") ||
+			    !recv_header(&imsg, &u.search, sizeof(u.search),
+			    "IMSG_MBOX_SEARCH"))
 				break;
-
-			if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1 ||
-			    req.poollen > sizeof(req.pool)) {
+			if (u.search.poollen > sizeof(u.search.pool)) {
 				log_warnx("bad IMSG_MBOX_SEARCH (header)");
 				break;
 			}
 			nodes = recv_trailing_array(&imsg, "IMSG_MBOX_SEARCH",
-			    req.nnodes, SEARCH_PROGRAM_MAX_NODES,
+			    u.search.nnodes, SEARCH_PROGRAM_MAX_NODES,
 			    sizeof(struct search_node), &ok);
 			if (!ok)
 				break;
-			if (nodes != NULL &&
-			    !search_nodes_valid(nodes, req.nnodes,
-			    req.poollen)) {
+			if (nodes != NULL && !search_nodes_valid(nodes,
+			    u.search.nnodes, u.search.poollen)) {
 				free(nodes);
 				break;
 			}
-			if (handle_mbox_search(&req, nodes, req.nnodes,
-			    ss) == 1)
-				defer_command(IMSG_MBOX_SEARCH, &req,
-				    sizeof(req), nodes, req.nnodes,
+			if (handle_mbox_search(&u.search, nodes,
+			    u.search.nnodes, ss) == 1)
+				defer_command(IMSG_MBOX_SEARCH, &u.search,
+				    sizeof(u.search), nodes, u.search.nnodes,
 				    SEARCH_PROGRAM_MAX_NODES, sizeof(*nodes),
 				    ss);
 			free(nodes);
 			break;
-		}
 		case IMSG_MBOX_SAVED_CLEAR:
 			if (imsg_get_len(&imsg) != 0) {
 				log_warnx("bad IMSG_MBOX_SAVED_CLEAR");
@@ -1330,138 +1283,73 @@ store_dispatch(int fd, short event, void *arg)
 			}
 			ss->nsaved = 0;
 			break;
-		case IMSG_MBOX_STATUS: {
-			struct imsg_mbox_status	 req;
-
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_STATUS");
-				break;
-			}
-			if (handle_mbox_status(&req, ss) == 1)
-				defer_command(IMSG_MBOX_STATUS, &req,
-				    sizeof(req), NULL, 0, 0, 0, ss);
+		case IMSG_MBOX_STATUS:
+			if (recv_fixed(&imsg, &u.status, sizeof(u.status),
+			    "IMSG_MBOX_STATUS") &&
+			    handle_mbox_status(&u.status, ss) == 1)
+				defer_command(IMSG_MBOX_STATUS, &u.status,
+				    sizeof(u.status), NULL, 0, 0, 0, ss);
 			break;
-		}
-		case IMSG_MBOX_COPY: {
-			struct imsg_mbox_copy	 req;
-			struct seq_range	*ranges;
-			int			 ok;
-
-			if (!require_mailbox_selected(ss, "IMSG_MBOX_COPY"))
+		case IMSG_MBOX_COPY:
+			if (!require_mailbox_selected(ss, "IMSG_MBOX_COPY") ||
+			    !recv_header(&imsg, &u.copy, sizeof(u.copy),
+			    "IMSG_MBOX_COPY") ||
+			    !ranges_required(u.copy.nranges, "IMSG_MBOX_COPY"))
 				break;
-
-			if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_COPY (header)");
-				break;
-			}
-			if (req.nranges == 0) {
-				log_warnx("session %u: IMSG_MBOX_COPY "
-				    "requires at least one range", session_id);
-				break;
-			}
-			ranges = recv_trailing_array(&imsg, "IMSG_MBOX_COPY",
-			    req.nranges, SEQSET_MAX_RANGES,
-			    sizeof(struct seq_range), &ok);
+			ranges = recv_ranges(&imsg, "IMSG_MBOX_COPY",
+			    u.copy.nranges, &ok);
 			if (!ok)
 				break;
-			if (handle_mbox_copy(&req, ranges, req.nranges,
-			    ss) == 1)
-				defer_command(IMSG_MBOX_COPY, &req,
-				    sizeof(req), ranges, req.nranges,
-				    SEQSET_MAX_RANGES, sizeof(*ranges), ss);
-			free(ranges);
+			rc = handle_mbox_copy(&u.copy, ranges, u.copy.nranges,
+			    ss);
+			ranged_done(ss, IMSG_MBOX_COPY, &u.copy, sizeof(u.copy),
+			    ranges, u.copy.nranges, rc);
 			break;
-		}
-		case IMSG_MBOX_MOVE: {
-			struct imsg_mbox_copy	 req;
-			struct seq_range	*ranges;
-			int			 ok;
-
-			if (!require_mailbox_selected(ss, "IMSG_MBOX_MOVE"))
+		case IMSG_MBOX_MOVE:
+			if (!require_mailbox_selected(ss, "IMSG_MBOX_MOVE") ||
+			    !recv_header(&imsg, &u.copy, sizeof(u.copy),
+			    "IMSG_MBOX_MOVE") ||
+			    !ranges_required(u.copy.nranges, "IMSG_MBOX_MOVE"))
 				break;
-
-			if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_MOVE (header)");
-				break;
-			}
-			if (req.nranges == 0) {
-				log_warnx("session %u: IMSG_MBOX_MOVE "
-				    "requires at least one range", session_id);
-				break;
-			}
-			ranges = recv_trailing_array(&imsg, "IMSG_MBOX_MOVE",
-			    req.nranges, SEQSET_MAX_RANGES,
-			    sizeof(struct seq_range), &ok);
+			ranges = recv_ranges(&imsg, "IMSG_MBOX_MOVE",
+			    u.copy.nranges, &ok);
 			if (!ok)
 				break;
-			if (handle_mbox_move(&req, ranges, req.nranges,
-			    ss) == 1)
-				defer_command(IMSG_MBOX_MOVE, &req,
-				    sizeof(req), ranges, req.nranges,
-				    SEQSET_MAX_RANGES, sizeof(*ranges), ss);
-			free(ranges);
+			rc = handle_mbox_move(&u.copy, ranges, u.copy.nranges,
+			    ss);
+			ranged_done(ss, IMSG_MBOX_MOVE, &u.copy, sizeof(u.copy),
+			    ranges, u.copy.nranges, rc);
 			break;
-		}
-		case IMSG_MBOX_LIST: {
-			struct imsg_mbox_list	 req;
-
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_LIST");
-				break;
-			}
-			handle_mbox_list(&req, iev);
+		case IMSG_MBOX_LIST:
+			if (recv_fixed(&imsg, &u.list, sizeof(u.list),
+			    "IMSG_MBOX_LIST"))
+				handle_mbox_list(&u.list, iev);
 			break;
-		}
-		case IMSG_MBOX_CREATE: {
-			struct imsg_mbox_create	 req;
-
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_CREATE");
-				break;
-			}
-			handle_mbox_create(&req, iev);
+		case IMSG_MBOX_CREATE:
+			if (recv_fixed(&imsg, &u.create, sizeof(u.create),
+			    "IMSG_MBOX_CREATE"))
+				handle_mbox_create(&u.create, iev);
 			break;
-		}
-		case IMSG_MBOX_DELETE: {
-			struct imsg_mbox_delete	 req;
-
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_DELETE");
-				break;
-			}
-			handle_mbox_delete(&req, ss);
+		case IMSG_MBOX_DELETE:
+			if (recv_fixed(&imsg, &u.delete, sizeof(u.delete),
+			    "IMSG_MBOX_DELETE"))
+				handle_mbox_delete(&u.delete, ss);
 			break;
-		}
-		case IMSG_MBOX_RENAME: {
-			struct imsg_mbox_rename	 req;
-
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_RENAME");
-				break;
-			}
-			handle_mbox_rename(&req, ss);
+		case IMSG_MBOX_RENAME:
+			if (recv_fixed(&imsg, &u.rename, sizeof(u.rename),
+			    "IMSG_MBOX_RENAME"))
+				handle_mbox_rename(&u.rename, ss);
 			break;
-		}
-		case IMSG_MBOX_SUBSCRIBE: {
-			struct imsg_mbox_subscribe	 req;
-
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_SUBSCRIBE");
-				break;
-			}
-			handle_mbox_subscribe(&req, iev);
+		case IMSG_MBOX_SUBSCRIBE:
+			if (recv_fixed(&imsg, &u.subscribe, sizeof(u.subscribe),
+			    "IMSG_MBOX_SUBSCRIBE"))
+				handle_mbox_subscribe(&u.subscribe, iev);
 			break;
-		}
-		case IMSG_MBOX_UNSUBSCRIBE: {
-			struct imsg_mbox_subscribe	 req;
-
-			if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
-				log_warnx("bad IMSG_MBOX_UNSUBSCRIBE");
-				break;
-			}
-			handle_mbox_unsubscribe(&req, iev);
+		case IMSG_MBOX_UNSUBSCRIBE:
+			if (recv_fixed(&imsg, &u.subscribe, sizeof(u.subscribe),
+			    "IMSG_MBOX_UNSUBSCRIBE"))
+				handle_mbox_unsubscribe(&u.subscribe, iev);
 			break;
-		}
 		default:
 			log_debug("store_dispatch: unhandled %d (session %u)",
 			    imsg_get_type(&imsg), session_id);
@@ -1474,7 +1362,6 @@ store_dispatch(int fd, short event, void *arg)
 			cur_snapshot_discard(&ss->cur_snap);
 	}
 	imsgev_rearm_read(iev);
-	(void)fd;
 }
 
 static void
blob - 0694acc1ffb3e92c45054d2d96a511d9ccdbcd5d
blob + f82ed4aea344afcc854de6974ae2f4a236c22df2
--- src/store_cache.c
+++ src/store_cache.c
@@ -18,7 +18,7 @@
 
 /*
  * ENVELOPE and BODYSTRUCTURE text, as checked by parser_request(), kept
- * per account worker. The key is RFC 9051 SS2.3.1.1's mailbox name,
+ * per account worker. The key is RFC 9051 section 2.3.1.1's mailbox name,
  * UIDVALIDITY and UID; the index's file name is checked on every hit.
  * Evicted oldest first, as usr.sbin/smtpd/queue_backend.c evicts.
  */
@@ -53,10 +53,11 @@ struct pcache_entry {
 RB_HEAD(pcache_tree, pcache_entry);
 TAILQ_HEAD(pcache_lru, pcache_entry);
 
-static int	 pcache_cmp(struct pcache_entry *, struct pcache_entry *);
-static struct pcache_entry *pcache_find(const char *, uint32_t, uint32_t,
-		    const char *);
-static void	 pcache_free(struct pcache_entry *);
+static int			 pcache_cmp(struct pcache_entry *,
+				    struct pcache_entry *);
+static struct pcache_entry	*pcache_find(const char *, uint32_t, uint32_t,
+				    const char *);
+static void			 pcache_free(struct pcache_entry *);
 
 RB_PROTOTYPE_STATIC(pcache_tree, pcache_entry, node, pcache_cmp);
 RB_GENERATE_STATIC(pcache_tree, pcache_entry, node, pcache_cmp);
@@ -233,7 +234,7 @@ pcache_drop_mailbox(const char *mailbox)
 	}
 }
 
-/* a new UIDVALIDITY (RFC 9051 SS2.3.1.1) ends the old entries */
+/* a new UIDVALIDITY (RFC 9051 section 2.3.1.1) ends the old entries */
 void
 pcache_check_mailbox(const char *mailbox, uint32_t uidvalidity)
 {
blob - 35ffc566b73f498e61f59bf40776748d26b87b89
blob + 1a90d4829bde5cb8355514fef2b9a56494558e36
--- src/store_cmd.c
+++ src/store_cmd.c
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/socket.h>
@@ -44,9 +43,6 @@
 int
 cmd_idle(struct session *s, const char *tag, char *args)
 {
-	/* RFC 9051 SS6.3.13: IDLE takes no arguments */
-	(void)args;
-
 	if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
 	    sizeof(s->pending_tag)) {
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
@@ -63,31 +59,27 @@ cmd_idle(struct session *s, const char *tag, char *arg
 	return (1);
 }
 
-/* RFC 9051 SS6.4.1 CLOSE: a silent EXPUNGE */
+/* RFC 9051 section 6.4.1 CLOSE: a silent EXPUNGE */
 int
 cmd_close(struct session *s, const char *tag, char *args)
 {
-	(void)args;
-	return session_request_expunge(s, tag, 1, 0, NULL, 0);
+	return (session_request_expunge(s, tag, 1, 0, NULL, 0));
 }
 
-/* RFC 9051 SS6.4.2 UNSELECT */
+/* RFC 9051 section 6.4.2 UNSELECT */
 int
 cmd_unselect(struct session *s, const char *tag, char *args)
 {
-	(void)args;
-
 	s->state = SESSION_AUTHENTICATED;
 	session_reply(s, tag, "OK", "Unselect completed");
 	return (1);
 }
 
-/* RFC 9051 SS6.4.3 EXPUNGE */
+/* RFC 9051 section 6.4.3 EXPUNGE */
 int
 cmd_expunge(struct session *s, const char *tag, char *args)
 {
-	(void)args;
-	return session_request_expunge(s, tag, 0, 0, NULL, 0);
+	return (session_request_expunge(s, tag, 0, 0, NULL, 0));
 }
 
 int
@@ -132,9 +124,7 @@ parse_store_flags(char *flagspec, uint32_t *sysflags_o
 			else if (strcasecmp(tok, "\\Draft") == 0)
 				sysflags |= MBOX_FLAG_DRAFT;
 			else if (strcasecmp(tok, "\\Recent") == 0) {
-				*errmsg = "\\Recent cannot be set, RFC 9051 "
-				    "deprecates it and excludes it from the "
-				    "flag grammar entirely";
+				*errmsg = "\\Recent cannot be set";
 				return (-2);
 			} else {
 				*errmsg = "unsupported system flag"
@@ -171,7 +161,7 @@ parse_store_flags(char *flagspec, uint32_t *sysflags_o
 	return (0);
 }
 
-/* RFC 7162 SS3.1.3 UNCHANGEDSINCE; 0 is legal */
+/* RFC 7162 section 3.1.3 UNCHANGEDSINCE; 0 is legal */
 int
 parse_store_modifiers(char *modspec, struct imsg_mbox_store *req,
     const char **errmsg)
@@ -191,23 +181,21 @@ parse_store_modifiers(char *modspec, struct imsg_mbox_
 	for (tok = strtok_r(p, " ", &save); tok != NULL;
 	    tok = strtok_r(NULL, " ", &save)) {
 		if (strcasecmp(tok, "UNCHANGEDSINCE") == 0) {
-			const char	*valtok = strtok_r(NULL, " ", &save);
-			char		*ep;
+			const char	*valtok, *errstr;
 
+			valtok = strtok_r(NULL, " ", &save);
 			if (valtok == NULL) {
 				*errmsg = "UNCHANGEDSINCE requires a "
 				    "mod-sequence value";
 				return (-1);
 			}
-			/* strtoull(3) accepts a sign */
 			if (*valtok < '0' || *valtok > '9') {
 				*errmsg = "invalid UNCHANGEDSINCE mod-sequence";
 				return (-1);
 			}
-			errno = 0;
-			req->unchangedsince = strtoull(valtok, &ep, 10);
-			if (*ep != '\0' || errno != 0 ||
-			    req->unchangedsince > MODSEQ_MAX) {
+			req->unchangedsince = strtonum(valtok, 0, MODSEQ_MAX,
+			    &errstr);
+			if (errstr != NULL) {
 				*errmsg = "invalid UNCHANGEDSINCE mod-sequence";
 				return (-1);
 			}
@@ -221,11 +209,11 @@ parse_store_modifiers(char *modspec, struct imsg_mbox_
 	return (0);
 }
 
-/* RFC 9051 SS6.4.6: store-modifiers lead */
+/* RFC 9051 section 6.4.6: store-modifiers lead */
 int
 cmd_store_cmd(struct session *s, const char *tag, char *args)
 {
-	return store_do(s, tag, args, 0);
+	return (store_do(s, tag, args, 0));
 }
 
 int
@@ -352,7 +340,7 @@ store_do(struct session *s, const char *tag, char *arg
 	}
 
 	if (s->mbox_readonly) {
-		/* RFC 9051 SS6.3.3: EXAMINE is read-only */
+		/* RFC 9051 section 6.3.3: EXAMINE is read-only */
 		session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
 		    "(selected via EXAMINE)");
 		return (1);
@@ -378,7 +366,7 @@ store_do(struct session *s, const char *tag, char *arg
 		return (1);
 	}
 
-	/* RFC 7162 SS3.1: UNCHANGEDSINCE enables CONDSTORE */
+	/* RFC 7162 section 3.1: UNCHANGEDSINCE enables CONDSTORE */
 	if (req.has_unchangedsince)
 		session_condstore_enable(s);
 
@@ -391,8 +379,8 @@ store_do(struct session *s, const char *tag, char *arg
 	s->cmd_by_uid = by_uid;
 	s->state = SESSION_STORING;
 
-	if (!send_mbox_request(s, IMSG_MBOX_STORE, cmdname, "IMSG_MBOX_STORE",
-	    &req, sizeof(req), ranges, nranges, sizeof(struct seq_range))) {
+	if (send_mbox_request(s, IMSG_MBOX_STORE, "IMSG_MBOX_STORE", &req,
+	    sizeof(req), ranges, nranges, sizeof(struct seq_range)) == -1) {
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		s->state = SESSION_SELECTED;
 		return (1);
@@ -401,7 +389,7 @@ store_do(struct session *s, const char *tag, char *arg
 	return (1);
 }
 
-/* RFC 9051 SS6.4.7 COPY and SS6.4.8 MOVE */
+/* RFC 9051 section 6.4.7 COPY and section 6.4.8 MOVE */
 int
 copy_move_dispatch(struct session *s, const char *tag, char *args,
     int by_uid, int is_move)
@@ -455,7 +443,7 @@ copy_move_dispatch(struct session *s, const char *tag,
 
 	if (listener_reject_bad_utf8(s, tag, mailbox))
 		return (1);
-	/* a name the server refuses is RFC 5530 SS3 NO [CANNOT], not BAD */
+	/* a refused name is RFC 5530 section 3 NO [CANNOT], not BAD */
 	if (!mailbox_name_is_inbox(mailbox) &&
 	    !listener_mailbox_name_valid(mailbox)) {
 		session_reply(s, tag, "NO", "[CANNOT] invalid mailbox name");
@@ -463,7 +451,7 @@ copy_move_dispatch(struct session *s, const char *tag,
 	}
 
 	if (is_move && s->mbox_readonly) {
-		/* RFC 9051 SS6.3.3: MOVE removes messages; COPY may proceed */
+		/* RFC 9051 section 6.3.3: MOVE would remove; COPY is fine */
 		session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
 		    "(selected via EXAMINE)");
 		return (1);
@@ -490,9 +478,9 @@ copy_move_dispatch(struct session *s, const char *tag,
 	s->cmd_is_move = is_move;
 	s->state = SESSION_COPYING;
 
-	if (!send_mbox_request(s, is_move ? IMSG_MBOX_MOVE : IMSG_MBOX_COPY,
-	    cmdname, cmdname, &req, sizeof(req), ranges, nranges,
-	    sizeof(struct seq_range))) {
+	if (send_mbox_request(s, is_move ? IMSG_MBOX_MOVE : IMSG_MBOX_COPY,
+	    is_move ? "IMSG_MBOX_MOVE" : "IMSG_MBOX_COPY", &req, sizeof(req),
+	    ranges, nranges, sizeof(struct seq_range)) == -1) {
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		s->state = SESSION_SELECTED;
 		return (1);
@@ -501,21 +489,19 @@ copy_move_dispatch(struct session *s, const char *tag,
 	return (1);
 }
 
-
 int
 cmd_copy(struct session *s, const char *tag, char *args)
 {
-	return copy_move_dispatch(s, tag, args, 0, 0);
+	return (copy_move_dispatch(s, tag, args, 0, 0));
 }
 
-
 int
 cmd_move(struct session *s, const char *tag, char *args)
 {
-	return copy_move_dispatch(s, tag, args, 0, 1);
+	return (copy_move_dispatch(s, tag, args, 0, 1));
 }
 
-/* RFC 9051 SS6.4.9 UID */
+/* RFC 9051 section 6.4.9 UID */
 int
 cmd_uid(struct session *s, const char *tag, char *args)
 {
@@ -538,24 +524,24 @@ cmd_uid(struct session *s, const char *tag, char *args
 		subargs = NULL;
 
 	if (strcasecmp(sub, "FETCH") == 0)
-		return fetch_dispatch(s, tag, subargs, 1);
+		return (fetch_dispatch(s, tag, subargs, 1));
 	if (strcasecmp(sub, "STORE") == 0)
-		return store_do(s, tag, subargs, 1);
+		return (store_do(s, tag, subargs, 1));
 	if (strcasecmp(sub, "SEARCH") == 0)
-		return search_dispatch(s, tag, subargs, 1);
+		return (search_dispatch(s, tag, subargs, 1));
 	if (strcasecmp(sub, "EXPUNGE") == 0)
-		return uid_expunge_dispatch(s, tag, subargs);
+		return (uid_expunge_dispatch(s, tag, subargs));
 	if (strcasecmp(sub, "COPY") == 0)
-		return copy_move_dispatch(s, tag, subargs, 1, 0);
+		return (copy_move_dispatch(s, tag, subargs, 1, 0));
 	if (strcasecmp(sub, "MOVE") == 0)
-		return copy_move_dispatch(s, tag, subargs, 1, 1);
+		return (copy_move_dispatch(s, tag, subargs, 1, 1));
 
 	session_reply(s, tag, "BAD",
 	    "UID sub-command must be COPY, FETCH, MOVE, SEARCH, or STORE");
 	return (1);
 }
 
-/* RFC 9051 SS7.5.1 EXPUNGE, or RFC 7162 SS3.2.10.2 VANISHED */
+/* RFC 9051 section 7.5.1 EXPUNGE, or RFC 7162 section 3.2.10.2 VANISHED */
 void
 session_send_expunge_response(struct session *s,
     const struct imsg_mbox_expunged *exp)
@@ -569,7 +555,7 @@ session_send_expunge_response(struct session *s,
 	session_untagged(s, buf);
 }
 
-/* RFC 7162 SS3.2.6 VANISHED (EARLIER) */
+/* RFC 7162 section 3.2.6 VANISHED (EARLIER) */
 void
 session_handle_fetch_vanished(struct session *s,
     const struct imsg_mbox_select_vanished *v)
@@ -595,12 +581,12 @@ session_request_expunge(struct session *s, const char 
 
 	if (s->mbox_readonly) {
 		if (is_close) {
-			/* RFC 9051 SS6.4.1: read-only, so just deselect */
+			/* RFC 9051 section 6.4.1: read-only, just deselect */
 			s->state = SESSION_AUTHENTICATED;
 			session_reply(s, tag, "OK", "CLOSE completed");
 			return (1);
 		}
-		/* RFC 9051 SS6.3.3: no read-only exception for EXPUNGE */
+		/* RFC 9051 section 6.3.3: no read-only exception for EXPUNGE */
 		session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
 		    "(selected via EXAMINE)");
 		return (1);
@@ -627,9 +613,9 @@ session_request_expunge(struct session *s, const char 
 	s->cmd_by_uid = by_uid;
 	s->state = SESSION_EXPUNGING;
 
-	if (!send_mbox_request(s, IMSG_MBOX_EXPUNGE, cmdname,
+	if (send_mbox_request(s, IMSG_MBOX_EXPUNGE,
 	    "IMSG_MBOX_EXPUNGE", &req, sizeof(req), ranges, nranges,
-	    sizeof(struct seq_range))) {
+	    sizeof(struct seq_range)) == -1) {
 		session_reply(s, tag, "NO", "[SERVERBUG] internal error");
 		s->state = SESSION_SELECTED;
 		return (1);
@@ -655,10 +641,10 @@ uid_expunge_dispatch(struct session *s, const char *ta
 		return (1);
 	}
 
-	return session_request_expunge(s, tag, 0, 1, ranges, nranges);
+	return (session_request_expunge(s, tag, 0, 1, ranges, nranges));
 }
 
-/* RFC 7162 SS3.1.3 MODIFIED entry */
+/* RFC 7162 section 3.1.3 MODIFIED entry */
 void
 session_handle_store_modified(struct session *s,
     struct imsg_mbox_store_modified *m)
@@ -669,9 +655,9 @@ session_handle_store_modified(struct session *s,
 	if (s->store_modified_n == s->store_modified_cap) {
 		uint32_t	 newcap = s->store_modified_cap ?
 		    s->store_modified_cap * 2 : 16;
-		uint32_t	*n = reallocarray(s->store_modified, newcap,
-		    sizeof(*n));
+		uint32_t	*n;
 
+		n = reallocarray(s->store_modified, newcap, sizeof(*n));
 		if (n == NULL) {
 			log_warn("session %u: realloc STORE MODIFIED array",
 			    s->id);
@@ -687,8 +673,9 @@ session_handle_store_modified(struct session *s,
 }
 
 /* refuses rather than truncates when it would not fit */
-static size_t
-append_range_token(char *buf, size_t bufsize, size_t written, int *first,
+/* appends "lo" or "lo:hi" after a comma; -1 when it would not fit */
+static int
+append_range_token(char *buf, size_t bufsize, size_t *written, int *first,
     uint32_t lo, uint32_t hi, int *truncated)
 {
 	char	tok[24];
@@ -699,23 +686,23 @@ append_range_token(char *buf, size_t bufsize, size_t w
 	else
 		toklen = snprintf(tok, sizeof(tok), "%u:%u", lo, hi);
 	if (toklen < 0)
-		return ((size_t)-1);
+		return (-1);
 
-	if (written + (size_t)toklen + (*first ? 0 : 1) >= bufsize) {
+	if (*written + (size_t)toklen + (*first ? 0 : 1) >= bufsize) {
 		*truncated = 1;
-		return ((size_t)-1);
+		return (-1);
 	}
 
 	if (!*first)
-		buf[written++] = ',';
-	memcpy(buf + written, tok, (size_t)toklen);
-	written += (size_t)toklen;
-	buf[written] = '\0';
+		buf[(*written)++] = ',';
+	memcpy(buf + *written, tok, (size_t)toklen);
+	*written += (size_t)toklen;
+	buf[*written] = '\0';
 	*first = 0;
-	return (written);
+	return (0);
 }
 
-/* RFC 9051 SS7.3.4 sequence list; input sorted */
+/* RFC 9051 section 7.3.4 sequence list; input sorted */
 size_t
 format_seq_list(char *buf, size_t bufsize, const uint32_t *nums, uint32_t n,
     int *truncated)
@@ -731,18 +718,15 @@ format_seq_list(char *buf, size_t bufsize, const uint3
 		uint32_t	start = nums[i];
 		uint32_t	end = start;
 		uint32_t	j = i + 1;
-		size_t		w;
 
 		while (j < n && nums[j] == end + 1) {
 			end = nums[j];
 			j++;
 		}
 
-		w = append_range_token(buf, bufsize, written, &first, start,
-		    end, truncated);
-		if (w == (size_t)-1)
+		if (append_range_token(buf, bufsize, &written, &first, start,
+		    end, truncated) == -1)
 			break;
-		written = w;
 		i = j;
 	}
 
@@ -763,13 +747,9 @@ format_range_list(char *buf, size_t bufsize,
 	buf[0] = '\0';
 
 	for (i = 0; i < n; i++) {
-		size_t	w;
-
-		w = append_range_token(buf, bufsize, written, &first,
-		    ranges[i].lo, ranges[i].hi, truncated);
-		if (w == (size_t)-1)
+		if (append_range_token(buf, bufsize, &written, &first,
+		    ranges[i].lo, ranges[i].hi, truncated) == -1)
 			break;
-		written = w;
 	}
 
 	return (written);
@@ -778,7 +758,7 @@ format_range_list(char *buf, size_t bufsize,
 #define COPYUID_PER_ENTRY	11
 #define COPYUID_WRAPPER_MAX	160
 
-/* RFC 9051 SS6.4.7/SS6.4.8 terminal reply */
+/* RFC 9051 section 6.4.7/section 6.4.8 terminal reply */
 void
 session_finish_copy_or_move(struct session *s,
     const struct imsg_mbox_result *res)
@@ -793,22 +773,21 @@ session_finish_copy_or_move(struct session *s,
 	if (res->error != MBOX_OP_OK || s->copy_alloc_failed) {
 		char	text[48];
 
-		/* RFC 9051 SS6.4.7: TRYCREATE */
+		/* RFC 9051 section 6.4.7: TRYCREATE */
 		if (!s->copy_alloc_failed &&
 		    res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX)
 			snprintf(text, sizeof(text), "[TRYCREATE] no such "
 			    "mailbox");
 		else if (!s->copy_alloc_failed &&
 		    res->error == MBOX_OP_ERR_BUSY)
-			snprintf(text, sizeof(text), "%s", IMAP_BUSY_TEXT);
+			strlcpy(text, IMAP_BUSY_TEXT, sizeof(text));
 		else if (!s->copy_alloc_failed &&
 		    res->error == MBOX_OP_ERR_LIMIT)
 			snprintf(text, sizeof(text), "[LIMIT] %s failed",
 			    cmdname);
 		else if (!s->copy_alloc_failed &&
 		    res->error == MBOX_OP_ERR_OVERQUOTA)
-			snprintf(text, sizeof(text), "%s",
-			    IMAP_OVERQUOTA_TEXT);
+			strlcpy(text, IMAP_OVERQUOTA_TEXT, sizeof(text));
 		else
 			snprintf(text, sizeof(text), "%s failed", cmdname);
 		session_reply(s, s->pending_tag, "NO", text);
@@ -817,7 +796,6 @@ session_finish_copy_or_move(struct session *s,
 
 	s->mbox_highestmodseq = res->highestmodseq;
 
-
 	if (s->copy_n > 0) {
 		char	*srcbuf = NULL, *destbuf = NULL, *text = NULL;
 		size_t	 listsize, textsize;
@@ -871,7 +849,7 @@ session_finish_copy_or_move(struct session *s,
 
 			session_reply(s, s->pending_tag, "OK", "Done");
 		} else if (!sent) {
-			/* RFC 9051 SS6.4.7: a SHOULD; never truncated */
+			/* RFC 9051 section 6.4.7: a SHOULD; never truncated */
 			char	fallback[64];
 
 			snprintf(fallback, sizeof(fallback), "%s completed",
@@ -879,7 +857,7 @@ session_finish_copy_or_move(struct session *s,
 			session_reply(s, s->pending_tag, "OK", fallback);
 		}
 	} else {
-		/* RFC 9051 SS6.4.7's own worked example, verbatim. */
+		/* RFC 9051 section 6.4.7's own worked example, verbatim. */
 		session_reply(s, s->pending_tag, "OK",
 		    "No matching messages, so nothing copied");
 	}
blob - af036bd14d59347e1db3ab7d2c57f9914359fe22
blob + 2ee04d07aa22ba325d4b7df0403cafa80a16c44c
--- src/store_internal.h
+++ src/store_internal.h
@@ -22,10 +22,16 @@
 #include <sys/types.h>
 #include <sys/queue.h>
 
+#include <dirent.h>
 #include <stdint.h>
 
 #include "mboxname.h"
 
+/*
+ * Returns: handle_mbox_*() 0 once answered, 1 to defer on a busy index
+ * lock; the rest 0/-1 or 1/0, except where a definition says otherwise.
+ */
+
 struct mbox_index {
 	uint32_t	  uidvalidity;
 	uint32_t	  uidnext;
@@ -75,7 +81,7 @@ struct store_fetch_walk {
 	uint64_t		 seen_modseq;
 };
 
-/* RFC 9051 SS6.4.4 SEARCH, yielding to the account's other sessions */
+/* RFC 9051 section 6.4.4 SEARCH, yielding to the account's other sessions */
 struct store_search_walk {
 	int			 active;
 	int			 wait_parser;	/* paused until one comes */
@@ -121,7 +127,7 @@ struct store_idle_baseline {
 	int		 valid;
 };
 
-/* RFC 9051 SS6.3.12 APPEND; a failure is held until END */
+/* RFC 9051 section 6.3.12 APPEND; a failure is held until END */
 struct store_append {
 	int			  active;
 	int			  failed;
@@ -142,6 +148,22 @@ struct cur_snapshot {
 	size_t	  n;
 };
 
+union mbox_request {
+	struct imsg_mbox_select		 select;
+	struct imsg_mbox_fetch		 fetch;
+	struct imsg_mbox_store		 store;
+	struct imsg_mbox_expunge	 expunge;
+	struct imsg_mbox_search		 search;
+	struct imsg_mbox_status		 status;
+	struct imsg_mbox_copy		 copy;
+	struct imsg_mbox_append		 append;
+	struct imsg_mbox_list		 list;
+	struct imsg_mbox_create		 create;
+	struct imsg_mbox_delete		 delete;
+	struct imsg_mbox_rename		 rename;
+	struct imsg_mbox_subscribe	 subscribe;
+};
+
 /* one command waiting on the index lock; re-run from the top */
 struct store_deferred {
 	int			 active;
@@ -149,16 +171,8 @@ struct store_deferred {
 	struct event		 ev;
 	struct timespec		 give_up_at;
 	unsigned int		 wait_ms;
+	union mbox_request	 req;
 	union {
-		struct imsg_mbox_store		 store;
-		struct imsg_mbox_expunge	 expunge;
-		struct imsg_mbox_fetch		 fetch;
-		struct imsg_mbox_search		 search;
-		struct imsg_mbox_select		 select;
-		struct imsg_mbox_status		 status;
-		struct imsg_mbox_copy		 copy;
-	} req;
-	union {
 		struct seq_range	 ranges[SEQSET_MAX_RANGES];
 		struct search_node	 nodes[SEARCH_PROGRAM_MAX_NODES];
 	} elts;
@@ -177,6 +191,15 @@ struct address {
 	size_t	 hostlen;
 };
 
+/* RFC 2045 section 5.1 Content-Type; RFC 2046 section 5.1.1: boundary <= 70 */
+struct content_type {
+	char	type[64];
+	char	subtype[64];
+	char	params_fmt[600];
+	char	boundary[70 + 1];
+	int	has_boundary;
+};
+
 #define ADDR_MAILBOX	1
 #define ADDR_GROUP	2
 #define ADDR_GROUP_END	3
@@ -186,7 +209,7 @@ struct store_session {
 	TAILQ_ENTRY(store_session)	 entry;
 	struct imsgev			 iev;
 
-	/* a failed SELECT leaves nothing selected (RFC 9051 SS6.3.2) */
+	/* a failed SELECT leaves nothing selected (RFC 9051 section 6.3.2) */
 	int				 mailbox_selected;
 
 	/* "" for INBOX */
@@ -203,7 +226,7 @@ struct store_session {
 	struct store_idle_baseline	 idle_baseline;
 	struct store_append		 append;
 
-	/* RFC 9051 SS6.4.4.1 "$", as UID ranges */
+	/* RFC 9051 section 6.4.4.1 "$", as UID ranges */
 	struct seq_range		 saved[SEQSET_MAX_RANGES];
 	uint32_t			 nsaved;
 };
@@ -253,6 +276,7 @@ void	 handle_mbox_unsubscribe(struct imsg_mbox_subscri
 	    struct imsgev *);
 int	 mailbox_name_valid(const char *);
 int	 mailbox_open_dir(const char *);
+DIR	*opendirat(int, const char *);
 void	 cur_snapshot_discard(struct cur_snapshot *);
 int	 fetch_walk_paused(struct store_session *);
 void	 fetch_walk_resume(struct store_session *);
@@ -308,8 +332,7 @@ int	 mime_is_tspecial(char);
 int	 mime_read_token_or_qstring(const char *, size_t, size_t *,
 		    char *, size_t);
 void	 mime_str_upper(char *);
-int	 parse_content_type(const char *, size_t, char *, size_t,
-		    char *, size_t, char *, size_t, char *, size_t, int *);
+int	 parse_content_type(const char *, size_t, struct content_type *);
 int	 split_multipart(const char *, size_t, const char *,
 		    size_t *, size_t *, int *, int);
 int	 build_body_structure(int, int *, const char *, size_t,
@@ -361,13 +384,13 @@ void	 index_free(struct mbox_index *);
 int	 index_parse_line(const char *, struct index_rec *);
 int	 index_field_valid(const char *);	/* no ':', CR or LF */
 int	 index_basename_valid(const char *);	/* safe in "new/%s" */
-uint32_t	 index_max_uid(struct mbox_index *);
+uint32_t index_max_uid(struct mbox_index *);
 void	 send_vanished_range(const struct mbox_index *, uint32_t, uint32_t,
 		    struct imsgev *);
-uint32_t	 seqset_resolve(const struct seq_range *, uint32_t, uint32_t,
-		    int, struct seq_range[SEQSET_MAX_RANGES]);
+uint32_t seqset_resolve(const struct seq_range *, uint32_t, uint32_t, int,
+		    struct seq_range[SEQSET_MAX_RANGES]);
 int	 seqset_contains(const struct seq_range *, uint32_t, uint32_t);
-uint32_t	 seqset_max_hi(const struct seq_range *, uint32_t);
+uint32_t seqset_max_hi(const struct seq_range *, uint32_t);
 
 /* PAST_END holds only because the scans walk in ascending order */
 enum seqset_pos {
blob - fb6781900013f0b414aa6a72d370df8ce6e14ddc
blob + 16c3448a2f38c87298731a90db0f7964928411b7
--- src/store_ipc.c
+++ src/store_ipc.c
@@ -16,7 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/socket.h>
@@ -53,7 +52,6 @@ fetch_part_recv(struct session *s, struct imsg *imsg, 
 	*foundp = 0;
 
 	if (!found)
-		/* store.c found/built nothing for this message */
 		return;
 
 	wirelen = imsg_get_len(imsg);
@@ -81,12 +79,86 @@ fetch_part_recv(struct session *s, struct imsg *imsg, 
 	*foundp = 1;
 }
 
+union mbox_reply {
+	struct imsg_mbox_selected		 selected;
+	struct imsg_mbox_status_result		 status;
+	struct imsg_mbox_fetch_header		 header;
+	struct imsg_mbox_fetch_envelope		 envelope;
+	struct imsg_mbox_fetch_bodystructure	 bodystructure;
+	struct imsg_mbox_fetch_meta		 meta;
+	struct imsg_mbox_expunged		 expunged;
+	struct imsg_mbox_copy_mapping		 mapping;
+	struct imsg_mbox_search_match		 match;
+	struct imsg_mbox_select_vanished	 vanished;
+	struct imsg_mbox_store_modified		 modified;
+	struct imsg_mbox_view_exists		 exists;
+	struct imsg_mbox_idle_refreshed		 refreshed;
+	struct imsg_mbox_result			 result;
+	struct imsg_mbox_list_item		 item;
+	struct imsg_mbox_appended		 appended;
+};
+
+static void
+session_recv_fetch_body(struct session *s, struct imsg *imsg)
+{
+	struct imsg_mbox_fetch_body	 bodyhdr;
+	int				 bodyfd;
+
+	bodyfd = imsg_get_fd(imsg);
+	if (s->pending_body_fd != -1)
+		close(s->pending_body_fd);
+	s->pending_body_fd = -1;
+	s->pending_body_off = 0;
+	s->pending_body_len = 0;
+	s->pending_body_found = 0;
+	if (!recv_fixed(imsg, &bodyhdr, sizeof(bodyhdr),
+	    "IMSG_MBOX_FETCH_BODY")) {
+		if (bodyfd != -1)
+			close(bodyfd);
+		return;
+	}
+	if (!bodyhdr.found || (bodyhdr.length > 0 && bodyfd == -1)) {
+		if (bodyhdr.found)
+			log_warnx("session %u: IMSG_MBOX_FETCH_BODY without "
+			    "its descriptor", s->id);
+		if (bodyfd != -1)
+			close(bodyfd);
+		return;
+	}
+	s->pending_body_fd = bodyfd;
+	s->pending_body_off = bodyhdr.offset;
+	s->pending_body_len = bodyhdr.length;
+	s->pending_body_found = 1;
+}
+
+/* a MOVE's EXPUNGEs wait for COPYUID, RFC 9051 section 6.4.8 */
+static void
+session_hold_expunged(struct session *s, const struct imsg_mbox_expunged *exp)
+{
+	struct imsg_mbox_expunged	*enew;
+	uint32_t			 newcap;
+
+	if (s->move_expunged_n == s->move_expunged_cap) {
+		newcap = s->move_expunged_cap ? s->move_expunged_cap * 2 : 16;
+		enew = reallocarray(s->move_expunged, newcap, sizeof(*enew));
+		if (enew == NULL) {
+			log_warn("session %u: realloc move_expunged", s->id);
+			return;
+		}
+		s->move_expunged = enew;
+		s->move_expunged_cap = newcap;
+	}
+	s->move_expunged[s->move_expunged_n++] = *exp;
+}
+
 void
 session_store_dispatch(int fd, short event, void *arg)
 {
-	struct session	*s = arg;
-	struct imsg	 imsg;
-	ssize_t		 n;
+	struct session		*s = arg;
+	struct imsg		 imsg;
+	union mbox_reply	 u;
+	char			 buf[32];
+	ssize_t			 n;
 
 	if (event & EV_WRITE) {
 		if (imsgbuf_write(&s->store_iev->ibuf) == -1) {
@@ -119,274 +191,138 @@ session_store_dispatch(int fd, short event, void *arg)
 			break;
 
 		switch (imsg_get_type(&imsg)) {
-		case IMSG_MBOX_SELECTED: {
-			struct imsg_mbox_selected	 res;
-
-			if (imsg_get_data(&imsg, &res, sizeof(res)) == -1) {
-				log_warnx("bad IMSG_MBOX_SELECTED");
-				break;
-			}
-			session_handle_mbox_selected(s, &res);
+		case IMSG_MBOX_SELECTED:
+			if (recv_fixed(&imsg, &u.selected, sizeof(u.selected),
+			    "IMSG_MBOX_SELECTED"))
+				session_handle_mbox_selected(s, &u.selected);
 			break;
-		}
-		case IMSG_MBOX_STATUS_RESULT: {
-			struct imsg_mbox_status_result	 res;
-
-			if (imsg_get_data(&imsg, &res, sizeof(res)) == -1) {
-				log_warnx("bad IMSG_MBOX_STATUS_RESULT");
-				break;
-			}
-			session_handle_mbox_status_result(s, &res);
+		case IMSG_MBOX_STATUS_RESULT:
+			if (recv_fixed(&imsg, &u.status, sizeof(u.status),
+			    "IMSG_MBOX_STATUS_RESULT"))
+				session_handle_mbox_status_result(s, &u.status);
 			break;
-		}
-		case IMSG_MBOX_FETCH_HEADER: {
-			struct imsg_mbox_fetch_header	 hdr;
-
-			if (imsg_get_buf(&imsg, &hdr, sizeof(hdr)) == -1) {
-				log_warnx("bad IMSG_MBOX_FETCH_HEADER "
-				    "(header)");
-				break;
-			}
-			fetch_part_recv(s, &imsg, "IMSG_MBOX_FETCH_HEADER",
-			    hdr.found, hdr.hdrlen, &s->pending_header_buf,
-			    &s->pending_header_len, &s->pending_header_found);
+		case IMSG_MBOX_FETCH_HEADER:
+			if (recv_header(&imsg, &u.header, sizeof(u.header),
+			    "IMSG_MBOX_FETCH_HEADER"))
+				fetch_part_recv(s, &imsg,
+				    "IMSG_MBOX_FETCH_HEADER", u.header.found,
+				    u.header.hdrlen, &s->pending_header_buf,
+				    &s->pending_header_len,
+				    &s->pending_header_found);
 			break;
-		}
-		case IMSG_MBOX_FETCH_BODY: {
-			struct imsg_mbox_fetch_body	 bodyhdr;
-			int				 bodyfd;
-
-			bodyfd = imsg_get_fd(&imsg);
-			if (s->pending_body_fd != -1)
-				close(s->pending_body_fd);
-			s->pending_body_fd = -1;
-			s->pending_body_off = 0;
-			s->pending_body_len = 0;
-			s->pending_body_found = 0;
-			if (imsg_get_data(&imsg, &bodyhdr,
-			    sizeof(bodyhdr)) == -1) {
-				log_warnx("bad IMSG_MBOX_FETCH_BODY");
-				if (bodyfd != -1)
-					close(bodyfd);
-				break;
-			}
-			if (!bodyhdr.found ||
-			    (bodyhdr.length > 0 && bodyfd == -1)) {
-				if (bodyhdr.found)
-					log_warnx("session %u: "
-					    "IMSG_MBOX_FETCH_BODY without its "
-					    "descriptor", s->id);
-				if (bodyfd != -1)
-					close(bodyfd);
-				break;
-			}
-			s->pending_body_fd = bodyfd;
-			s->pending_body_off = bodyhdr.offset;
-			s->pending_body_len = bodyhdr.length;
-			s->pending_body_found = 1;
+		case IMSG_MBOX_FETCH_BODY:
+			session_recv_fetch_body(s, &imsg);
 			break;
-		}
-		case IMSG_MBOX_FETCH_ENVELOPE: {
-			struct imsg_mbox_fetch_envelope	 envhdr;
-
-			if (imsg_get_buf(&imsg, &envhdr, sizeof(envhdr)) ==
-			    -1) {
-				log_warnx("bad IMSG_MBOX_FETCH_ENVELOPE "
-				    "(header)");
-				break;
-			}
-			fetch_part_recv(s, &imsg, "IMSG_MBOX_FETCH_ENVELOPE",
-			    envhdr.found, envhdr.envlen,
-			    &s->pending_envelope_buf, &s->pending_envelope_len,
-			    &s->pending_envelope_found);
+		case IMSG_MBOX_FETCH_ENVELOPE:
+			if (recv_header(&imsg, &u.envelope, sizeof(u.envelope),
+			    "IMSG_MBOX_FETCH_ENVELOPE"))
+				fetch_part_recv(s, &imsg,
+				    "IMSG_MBOX_FETCH_ENVELOPE",
+				    u.envelope.found, u.envelope.envlen,
+				    &s->pending_envelope_buf,
+				    &s->pending_envelope_len,
+				    &s->pending_envelope_found);
 			break;
-		}
-		case IMSG_MBOX_FETCH_BODYSTRUCTURE: {
-			struct imsg_mbox_fetch_bodystructure	 bshdr;
-
-			if (imsg_get_buf(&imsg, &bshdr, sizeof(bshdr)) ==
-			    -1) {
-				log_warnx("bad IMSG_MBOX_FETCH_BODYSTRUCTURE "
-				    "(header)");
-				break;
-			}
-			fetch_part_recv(s, &imsg,
-			    "IMSG_MBOX_FETCH_BODYSTRUCTURE",
-			    bshdr.found, bshdr.bslen,
-			    &s->pending_bodystructure_buf,
-			    &s->pending_bodystructure_len,
-			    &s->pending_bodystructure_found);
+		case IMSG_MBOX_FETCH_BODYSTRUCTURE:
+			if (recv_header(&imsg, &u.bodystructure,
+			    sizeof(u.bodystructure),
+			    "IMSG_MBOX_FETCH_BODYSTRUCTURE"))
+				fetch_part_recv(s, &imsg,
+				    "IMSG_MBOX_FETCH_BODYSTRUCTURE",
+				    u.bodystructure.found,
+				    u.bodystructure.bslen,
+				    &s->pending_bodystructure_buf,
+				    &s->pending_bodystructure_len,
+				    &s->pending_bodystructure_found);
 			break;
-		}
-		case IMSG_MBOX_FETCH_META: {
-			struct imsg_mbox_fetch_meta	 meta;
-
-			if (imsg_get_data(&imsg, &meta, sizeof(meta)) == -1) {
-				log_warnx("bad IMSG_MBOX_FETCH_META");
+		case IMSG_MBOX_FETCH_META:
+			if (!recv_fixed(&imsg, &u.meta, sizeof(u.meta),
+			    "IMSG_MBOX_FETCH_META"))
 				break;
-			}
-			/* imsg_get_data() does not NUL-terminate */
-			meta.flags[sizeof(meta.flags) - 1] = '\0';
-			/* SELECTING buffers, for RFC 7162 SS3.2.6 order */
+			u.meta.flags[sizeof(u.meta.flags) - 1] = '\0';
+			/* SELECTING buffers: RFC 7162 section 3.2.6 order */
 			if (s->state == SESSION_SELECTING)
-				session_handle_select_fetch(s, &meta);
+				session_handle_select_fetch(s, &u.meta);
 			else if (s->state == SESSION_STORING)
-				session_send_store_fetch_response(s, &meta);
+				session_send_store_fetch_response(s, &u.meta);
 			else
-				session_send_fetch_response(s, &meta);
+				session_send_fetch_response(s, &u.meta);
 			break;
-		}
-		case IMSG_MBOX_EXPUNGED: {
-			struct imsg_mbox_expunged	 exp;
-
-			if (imsg_get_data(&imsg, &exp, sizeof(exp)) == -1) {
-				log_warnx("bad IMSG_MBOX_EXPUNGED");
+		case IMSG_MBOX_EXPUNGED:
+			if (!recv_fixed(&imsg, &u.expunged, sizeof(u.expunged),
+			    "IMSG_MBOX_EXPUNGED"))
 				break;
-			}
-			/* buffered until after COPYUID (RFC 9051 SS6.4.8) */
-			if (s->state == SESSION_COPYING) {
-				if (s->move_expunged_n ==
-				    s->move_expunged_cap) {
-					uint32_t	 newcap =
-					    s->move_expunged_cap ?
-					    s->move_expunged_cap * 2 : 16;
-					struct imsg_mbox_expunged *enew =
-					    reallocarray(s->move_expunged,
-					    newcap, sizeof(*enew));
-
-					if (enew == NULL) {
-						log_warn("session %u: "
-						    "realloc move_expunged",
-						    s->id);
-						break;
-					}
-					s->move_expunged = enew;
-					s->move_expunged_cap = newcap;
-				}
-				s->move_expunged[s->move_expunged_n++] = exp;
-			} else
-				session_send_expunge_response(s, &exp);
+			if (s->state == SESSION_COPYING)
+				session_hold_expunged(s, &u.expunged);
+			else
+				session_send_expunge_response(s, &u.expunged);
 			break;
-		}
-		case IMSG_MBOX_COPY_MAPPING: {
-			struct imsg_mbox_copy_mapping	 m;
-
-			if (imsg_get_data(&imsg, &m, sizeof(m)) == -1) {
-				log_warnx("bad IMSG_MBOX_COPY_MAPPING");
-				break;
-			}
-			session_handle_mbox_copy_mapping(s, &m);
+		case IMSG_MBOX_COPY_MAPPING:
+			if (recv_fixed(&imsg, &u.mapping, sizeof(u.mapping),
+			    "IMSG_MBOX_COPY_MAPPING"))
+				session_handle_mbox_copy_mapping(s, &u.mapping);
 			break;
-		}
-		case IMSG_MBOX_SEARCH_MATCH: {
-			struct imsg_mbox_search_match	 m;
-
-			if (imsg_get_data(&imsg, &m, sizeof(m)) == -1) {
-				log_warnx("bad IMSG_MBOX_SEARCH_MATCH");
-				break;
-			}
-			session_handle_mbox_search_match(s, &m);
+		case IMSG_MBOX_SEARCH_MATCH:
+			if (recv_fixed(&imsg, &u.match, sizeof(u.match),
+			    "IMSG_MBOX_SEARCH_MATCH"))
+				session_handle_mbox_search_match(s, &u.match);
 			break;
-		}
-		case IMSG_MBOX_SELECT_VANISHED: {
-			struct imsg_mbox_select_vanished	 v;
-
-			if (imsg_get_data(&imsg, &v, sizeof(v)) == -1) {
-				log_warnx("bad IMSG_MBOX_SELECT_VANISHED");
+		case IMSG_MBOX_SELECT_VANISHED:
+			if (!recv_fixed(&imsg, &u.vanished, sizeof(u.vanished),
+			    "IMSG_MBOX_SELECT_VANISHED"))
 				break;
-			}
 			if (s->state == SESSION_SELECTING)
-				session_handle_select_vanished(s, &v);
+				session_handle_select_vanished(s, &u.vanished);
 			else
-				session_handle_fetch_vanished(s, &v);
+				session_handle_fetch_vanished(s, &u.vanished);
 			break;
-		}
-		case IMSG_MBOX_STORE_MODIFIED: {
-			struct imsg_mbox_store_modified	 m;
-
-			if (imsg_get_data(&imsg, &m, sizeof(m)) == -1) {
-				log_warnx("bad IMSG_MBOX_STORE_MODIFIED");
-				break;
-			}
-			session_handle_store_modified(s, &m);
+		case IMSG_MBOX_STORE_MODIFIED:
+			if (recv_fixed(&imsg, &u.modified, sizeof(u.modified),
+			    "IMSG_MBOX_STORE_MODIFIED"))
+				session_handle_store_modified(s, &u.modified);
 			break;
-		}
-		case IMSG_MBOX_VIEW_EXPUNGE: {
-			struct imsg_mbox_expunged	 item;
-
-			if (imsg_get_data(&imsg, &item, sizeof(item)) == -1) {
-				log_warnx("bad IMSG_MBOX_VIEW_EXPUNGE");
-				break;
-			}
-			session_send_expunge_response(s, &item);
+		case IMSG_MBOX_VIEW_EXPUNGE:
+			if (recv_fixed(&imsg, &u.expunged, sizeof(u.expunged),
+			    "IMSG_MBOX_VIEW_EXPUNGE"))
+				session_send_expunge_response(s, &u.expunged);
 			break;
-		}
-		case IMSG_MBOX_VIEW_FETCH: {
-			struct imsg_mbox_fetch_meta	 meta;
-
-			if (imsg_get_data(&imsg, &meta, sizeof(meta)) == -1) {
-				log_warnx("bad IMSG_MBOX_VIEW_FETCH");
+		case IMSG_MBOX_VIEW_FETCH:
+			if (!recv_fixed(&imsg, &u.meta, sizeof(u.meta),
+			    "IMSG_MBOX_VIEW_FETCH"))
 				break;
-			}
-			/* same risk as IMSG_MBOX_FETCH_META's flags above */
-			meta.flags[sizeof(meta.flags) - 1] = '\0';
-			session_handle_view_fetch(s, &meta);
+			u.meta.flags[sizeof(u.meta.flags) - 1] = '\0';
+			session_handle_view_fetch(s, &u.meta);
 			break;
-		}
-		case IMSG_MBOX_VIEW_EXISTS: {
-			struct imsg_mbox_view_exists	 ve;
-			char				 buf[32];
-
-			if (imsg_get_data(&imsg, &ve, sizeof(ve)) == -1) {
-				log_warnx("bad IMSG_MBOX_VIEW_EXISTS");
+		case IMSG_MBOX_VIEW_EXISTS:
+			if (!recv_fixed(&imsg, &u.exists, sizeof(u.exists),
+			    "IMSG_MBOX_VIEW_EXISTS"))
 				break;
-			}
-			snprintf(buf, sizeof(buf), "%u EXISTS", ve.exists);
+			snprintf(buf, sizeof(buf), "%u EXISTS",
+			    u.exists.exists);
 			session_untagged(s, buf);
 			break;
-		}
-		case IMSG_MBOX_IDLE_REFRESHED: {
-			struct imsg_mbox_idle_refreshed	 res;
-
-			if (imsg_get_data(&imsg, &res, sizeof(res)) == -1) {
-				log_warnx("bad IMSG_MBOX_IDLE_REFRESHED");
-				break;
-			}
-			session_handle_idle_refreshed(s, &res);
+		case IMSG_MBOX_IDLE_REFRESHED:
+			if (recv_fixed(&imsg, &u.refreshed, sizeof(u.refreshed),
+			    "IMSG_MBOX_IDLE_REFRESHED"))
+				session_handle_idle_refreshed(s, &u.refreshed);
 			break;
-		}
-		case IMSG_MBOX_RESULT: {
-			struct imsg_mbox_result	 res;
-
-			if (imsg_get_data(&imsg, &res, sizeof(res)) == -1) {
-				log_warnx("bad IMSG_MBOX_RESULT");
-				break;
-			}
-			session_handle_mbox_result(s, &res);
+		case IMSG_MBOX_RESULT:
+			if (recv_fixed(&imsg, &u.result, sizeof(u.result),
+			    "IMSG_MBOX_RESULT"))
+				session_handle_mbox_result(s, &u.result);
 			break;
-		}
-		case IMSG_MBOX_LIST_ITEM: {
-			struct imsg_mbox_list_item	 item;
-
-			if (imsg_get_data(&imsg, &item, sizeof(item)) == -1) {
-				log_warnx("bad IMSG_MBOX_LIST_ITEM");
+		case IMSG_MBOX_LIST_ITEM:
+			if (!recv_fixed(&imsg, &u.item, sizeof(u.item),
+			    "IMSG_MBOX_LIST_ITEM"))
 				break;
-			}
-			/* not NUL-terminated by imsg_get_data() */
-			item.mailbox[sizeof(item.mailbox) - 1] = '\0';
-			session_handle_mbox_list_item(s, &item);
+			u.item.mailbox[sizeof(u.item.mailbox) - 1] = '\0';
+			session_handle_mbox_list_item(s, &u.item);
 			break;
-		}
-		case IMSG_MBOX_APPENDED: {
-			struct imsg_mbox_appended	 res;
-
-			if (imsg_get_data(&imsg, &res, sizeof(res)) == -1) {
-				log_warnx("bad IMSG_MBOX_APPENDED");
-				break;
-			}
-			session_handle_mbox_appended(s, &res);
+		case IMSG_MBOX_APPENDED:
+			if (recv_fixed(&imsg, &u.appended, sizeof(u.appended),
+			    "IMSG_MBOX_APPENDED"))
+				session_handle_mbox_appended(s, &u.appended);
 			break;
-		}
 		default:
 			log_debug("session %u: store channel: unhandled %d",
 			    s->id, imsg_get_type(&imsg));
@@ -395,18 +331,21 @@ session_store_dispatch(int fd, short event, void *arg)
 		imsg_free(&imsg);
 	}
 	imsgev_rearm_read(s->store_iev);
-	(void)fd;
 
 	if (!session_dequeue_next(s))
 		return;	/* s was torn down by a queued LOGOUT, do not touch */
 }
 
-/* RFC 9051 SS6.3.2; for QRESYNC: VANISHED, FETCH, tagged OK */
+/* RFC 9051 section 6.3.2; for QRESYNC: VANISHED, FETCH, tagged OK */
 void
 session_handle_mbox_selected(struct session *s,
     const struct imsg_mbox_selected *res)
 {
-	char	buf[128];
+	char		 buf[128];
+	char		 qname[MBOX_QUOTED_MAX];
+	char		 listbuf[MBOX_QUOTED_MAX + 64];
+	uint32_t	 i;
+	int		 qlen;
 
 	if (res->error != MBOX_OP_OK || s->qresync_alloc_failed) {
 		/* RFC 5530 NONEXISTENT only when there is no such mailbox */
@@ -452,7 +391,7 @@ session_handle_mbox_selected(struct session *s,
 		session_untagged(s, buf);
 	}
 
-	/* RFC 9051 SS6.3.3: EXAMINE gets no PERMANENTFLAGS */
+	/* RFC 9051 section 6.3.3: EXAMINE gets no PERMANENTFLAGS */
 	session_untagged(s,
 	    "FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)");
 	if (s->mbox_readonly)
@@ -463,24 +402,17 @@ session_handle_mbox_selected(struct session *s,
 		    "OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen "
 		    "\\Draft \\*)] System flags and keywords allowed");
 
-	{
-		char	qname[MBOX_QUOTED_MAX];
-		char	listbuf[MBOX_QUOTED_MAX + 64];
-		int	qlen;
+	if (quote_mailbox(qname, sizeof(qname), s->selected_mailbox) == -1)
+		log_warnx("session %u: SELECT LIST mailbox name truncated",
+		    s->id);
+	qlen = snprintf(listbuf, sizeof(listbuf), "* LIST () \"/\" %s\r\n",
+	    qname);
+	if (qlen > 0 && (size_t)qlen < sizeof(listbuf))
+		session_write(s, listbuf, (size_t)qlen);
+	else
+		log_warnx("session %u: SELECT LIST response did not fit",
+		    s->id);
 
-		if (quote_mailbox(qname, sizeof(qname),
-		    s->selected_mailbox) == -1)
-			log_warnx("session %u: SELECT LIST mailbox name "
-			    "truncated", s->id);
-		qlen = snprintf(listbuf, sizeof(listbuf),
-		    "* LIST () \"/\" %s\r\n", qname);
-		if (qlen > 0 && (size_t)qlen < sizeof(listbuf))
-			session_write(s, listbuf, (size_t)qlen);
-		else
-			log_warnx("session %u: SELECT LIST response did not "
-			    "fit", s->id);
-	}
-
 	if (s->vanished_nranges > 0) {
 		char	vbuf[8192];
 		char	full[8192 + 32];
@@ -506,13 +438,8 @@ session_handle_mbox_selected(struct session *s,
 	s->vanished_nranges = 0;
 	s->vanished_cap = 0;
 
-	{
-		uint32_t	i;
-
-		for (i = 0; i < s->qresync_nfetches; i++)
-			session_send_qresync_fetch_response(s,
-			    &s->qresync_fetches[i]);
-	}
+	for (i = 0; i < s->qresync_nfetches; i++)
+		session_send_qresync_fetch_response(s, &s->qresync_fetches[i]);
 	free(s->qresync_fetches);
 	s->qresync_fetches = NULL;
 	s->qresync_nfetches = 0;
@@ -527,13 +454,13 @@ session_handle_mbox_selected(struct session *s,
 		    "[READ-WRITE] SELECT completed");
 }
 
-/* RFC 9051 SS6.3.11; attributes in a fixed order */
+/* RFC 9051 section 6.3.11; attributes in a fixed order */
 void
 session_handle_mbox_status_result(struct session *s,
     const struct imsg_mbox_status_result *res)
 {
 	char	qname[MBOX_QUOTED_MAX];
-	/* RFC 9051 SS4.3-escaped name plus attributes */
+	/* RFC 9051 section 4.3-escaped name plus attributes */
 	char	buf[MBOX_QUOTED_MAX + 384];
 	size_t	len;
 	int	n, first = 1;
@@ -568,7 +495,7 @@ session_handle_mbox_status_result(struct session *s,
 	if (s->status_attrs & STATUS_ATT_MESSAGES)
 		STATUS_APPEND("MESSAGES %u", res->messages);
 	if (s->status_attrs & STATUS_ATT_RECENT)
-		/* RFC 9051 SS2.3.2 dropped \Recent; always 0 */
+		/* RFC 9051 section 2.3.2 dropped \Recent; always 0 */
 		STATUS_APPEND("RECENT %u", 0U);
 	if (s->status_attrs & STATUS_ATT_UIDNEXT)
 		STATUS_APPEND("UIDNEXT %u", res->uidnext);
@@ -624,12 +551,12 @@ session_finish_mbox_op(struct session *s, const struct
 	case MBOX_OP_OK:
 		break;
 	case MBOX_OP_ERR_NO_SUCH_MAILBOX:
-		/* RFC 5530 SS3 NONEXISTENT */
+		/* RFC 5530 section 3 NONEXISTENT */
 		session_reply(s, s->pending_tag, "NO",
 		    "[NONEXISTENT] no such mailbox");
 		return;
 	case MBOX_OP_ERR_ALREADY_EXISTS:
-		/* RFC 5530 SS3 ALREADYEXISTS */
+		/* RFC 5530 section 3 ALREADYEXISTS */
 		session_reply(s, s->pending_tag, "NO",
 		    "[ALREADYEXISTS] mailbox already exists");
 		return;
@@ -646,12 +573,8 @@ session_finish_mbox_op(struct session *s, const struct
 	if (s->state == SESSION_SELECTED &&
 	    strcmp(s->selected_mailbox, s->mbox_op_name) == 0) {
 		if (strcmp(cmdname, "RENAME") == 0) {
-			if (strlcpy(s->selected_mailbox, s->rename_newname,
-			    sizeof(s->selected_mailbox)) >=
-			    sizeof(s->selected_mailbox))
-				log_warnx("session %u: selected_mailbox "
-				    "truncated after RENAME, can't happen "
-				    "(both same size)", s->id);
+			strlcpy(s->selected_mailbox, s->rename_newname,
+			    sizeof(s->selected_mailbox));
 		} else if (strcmp(cmdname, "DELETE") == 0) {
 			/* the mailbox is gone: deselect, as CLOSE does */
 			s->state = SESSION_AUTHENTICATED;
@@ -676,7 +599,7 @@ session_handle_mbox_list_item(struct session *s,
 	if (!list_pattern_match(s->list_pattern, item->mailbox, 0))
 		return;
 
-	/* RFC 9051 SS7.3.1 attributes */
+	/* RFC 9051 section 7.3.1 attributes */
 	if (!s->list_subscribed_only)
 		attrs = "";
 	else if (s->list_is_lsub)
@@ -722,10 +645,11 @@ session_handle_mbox_copy_mapping(struct session *s,
 
 	if (s->copy_n == s->copy_cap) {
 		uint32_t	 newcap = s->copy_cap ? s->copy_cap * 2 : 16;
-		uint32_t	*newsrc = reallocarray(s->copy_src_uids, newcap,
-		    sizeof(uint32_t));
+		uint32_t	*newsrc;
 		uint32_t	*newdest;
 
+		newsrc = reallocarray(s->copy_src_uids, newcap,
+		    sizeof(uint32_t));
 		if (newsrc == NULL) {
 			log_warn("session %u: realloc COPY src array", s->id);
 			s->copy_alloc_failed = 1;
@@ -757,9 +681,9 @@ session_handle_select_vanished(struct session *s,
 	if (s->vanished_nranges == s->vanished_cap) {
 		uint32_t		 newcap = s->vanished_cap ?
 		    s->vanished_cap * 2 : 16;
-		struct vanished_range	*n = reallocarray(s->vanished_ranges,
-		    newcap, sizeof(*n));
+		struct vanished_range	*n;
 
+		n = reallocarray(s->vanished_ranges, newcap, sizeof(*n));
 		if (n == NULL) {
 			log_warn("session %u: realloc VANISHED range array",
 			    s->id);
@@ -783,9 +707,9 @@ session_handle_select_fetch(struct session *s,
 		uint32_t			 newcap =
 		    s->qresync_fetches_cap ?
 		    s->qresync_fetches_cap * 2 : 16;
-		struct imsg_mbox_fetch_meta	*n = reallocarray(
-		    s->qresync_fetches, newcap, sizeof(*n));
+		struct imsg_mbox_fetch_meta	*n;
 
+		n = reallocarray(s->qresync_fetches, newcap, sizeof(*n));
 		if (n == NULL) {
 			log_warn("session %u: realloc QRESYNC fetch array",
 			    s->id);
@@ -799,7 +723,7 @@ session_handle_select_fetch(struct session *s,
 	s->qresync_fetches[s->qresync_nfetches++] = *m;
 }
 
-/* RFC 9051 SS6.3.13: an unsolicited FETCH carries UID */
+/* RFC 9051 section 6.3.13: an unsolicited FETCH carries UID */
 void
 session_handle_view_fetch(struct session *s,
     const struct imsg_mbox_fetch_meta *meta)
@@ -864,13 +788,9 @@ session_idle_poll(int fd, short event, void *arg)
 {
 	struct session	*s = arg;
 
-	(void)fd;
-	(void)event;
-
 	if (!s->idling || s->state != SESSION_SELECTED) {
-		log_debug("session %u: idle poll fired while not idling "
-		    "(idling=%d state=%d), ignored", s->id, s->idling,
-		    (int)s->state);
+		log_debug("session %u: idle poll while not idling (idling=%d "
+		    "state=%d)", s->id, s->idling, (int)s->state);
 		return;
 	}
 
@@ -895,8 +815,7 @@ session_idle_poll_arm(struct session *s)
 	tv.tv_sec = (time_t)listener_idle_poll_secs;
 	tv.tv_usec = 0;
 	if (evtimer_add(&s->idle_ev, &tv) == -1)
-		log_warnx("session %u: evtimer_add (idle poll); this IDLE "
-		    "will not be woken until the client sends DONE", s->id);
+		log_warnx("session %u: evtimer_add (idle poll)", s->id);
 }
 
 void
@@ -905,7 +824,7 @@ session_idle_poll_disarm(struct session *s)
 	evtimer_del(&s->idle_ev);
 }
 
-/* RFC 7162 SS3.2.5.1: always UID, FLAGS and MODSEQ */
+/* RFC 7162 section 3.2.5.1: always UID, FLAGS and MODSEQ */
 void
 session_send_qresync_fetch_response(struct session *s,
     const struct imsg_mbox_fetch_meta *meta)
@@ -918,15 +837,16 @@ session_send_qresync_fetch_response(struct session *s,
 	session_untagged(s, buf);
 }
 
-/* RFC 7162 SS3.1.3 MODIFIED, worst case per entry */
+/* RFC 7162 section 3.1.3 MODIFIED, worst case per entry */
 #define MODIFIED_PER_ENTRY	11
 #define MODIFIED_WRAPPER_MAX	160
 
 void
 session_handle_mbox_result(struct session *s, struct imsg_mbox_result *res)
 {
+	const char	*cmdname;
+	char		 reply[96];
 	int		 was_close = 0, was_storing = 0, was_expunging = 0;
-	const char	*cmdname;
 
 	if (s->state == SESSION_SEARCHING) {
 		session_finish_search(s, res);
@@ -947,7 +867,7 @@ session_handle_mbox_result(struct session *s, struct i
 		return;
 	}
 
-	/* RFC 9051 SS6.4.9: "UID <CMD>" */
+	/* RFC 9051 section 6.4.9: "UID <CMD>" */
 	if (s->state == SESSION_STORING) {
 		cmdname = s->cmd_by_uid ? "UID STORE" : "STORE";
 		was_storing = 1;
@@ -963,13 +883,11 @@ session_handle_mbox_result(struct session *s, struct i
 	    s->id, cmdname, res->error == MBOX_OP_OK ? "OK" : "ERROR",
 	    res->count);
 
-	/* RFC 9051 SS6.4.1: only a successful CLOSE deselects */
+	/* RFC 9051 section 6.4.1: only a successful CLOSE deselects */
 	s->state = (was_close && res->error == MBOX_OP_OK) ?
 	    SESSION_AUTHENTICATED : SESSION_SELECTED;
 
 	if (res->error != MBOX_OP_OK) {
-		char	text[64];
-
 		free(s->store_modified);
 		s->store_modified = NULL;
 		s->store_modified_n = 0;
@@ -981,18 +899,18 @@ session_handle_mbox_result(struct session *s, struct i
 			    IMAP_BUSY_TEXT);
 			return;
 		}
-		snprintf(text, sizeof(text), "%s%s failed",
+		snprintf(reply, sizeof(reply), "%s%s failed",
 		    res->error == MBOX_OP_ERR_EXPUNGEISSUED ?
 		    "[EXPUNGEISSUED] " : res->error == MBOX_OP_ERR_LIMIT ?
 		    "[LIMIT] " : "", cmdname);
-		session_reply(s, s->pending_tag, "NO", text);
+		session_reply(s, s->pending_tag, "NO", reply);
 		return;
 	}
 
 	if (was_storing || was_expunging)
 		s->mbox_highestmodseq = res->highestmodseq;
 
-	/* RFC 7162 SS3.1.3 MODIFIED */
+	/* RFC 7162 section 3.1.3 MODIFIED */
 	if (was_storing && (s->store_modified_n > 0 ||
 	    s->store_modified_alloc_failed)) {
 		char	*rbuf = NULL, *text = NULL;
@@ -1038,7 +956,7 @@ session_handle_mbox_result(struct session *s, struct i
 		free(rbuf);
 		free(text);
 
-		/* RFC 7162 SS3.1.3: an incomplete MODIFIED is refused */
+		/* RFC 7162 section 3.1.3: an incomplete MODIFIED is refused */
 		if (incomplete) {
 			char	fail[64];
 
@@ -1060,35 +978,26 @@ session_handle_mbox_result(struct session *s, struct i
 	s->store_modified_cap = 0;
 	s->store_modified_alloc_failed = 0;
 
-
-	/* RFC 7162 SS3.2.7, but never on CLOSE (SS3.2.8) */
+	/* RFC 7162 section 3.2.7, but never on CLOSE (section 3.2.8) */
 	if (was_expunging && !was_close && s->condstore_enabled &&
 	    res->count > 0) {
-		char	text[64];
-
-		snprintf(text, sizeof(text), "[HIGHESTMODSEQ %llu] %s "
+		snprintf(reply, sizeof(reply), "[HIGHESTMODSEQ %llu] %s "
 		    "completed", (unsigned long long)res->highestmodseq,
 		    cmdname);
-		session_reply(s, s->pending_tag, "OK", text);
+		session_reply(s, s->pending_tag, "OK", reply);
 		return;
 	}
 
-	/* RFC 9051 SS6.4.5 */
+	/* RFC 9051 section 6.4.5 */
 	if (s->fetch_incomplete) {
-		char	text[96];
-
 		s->fetch_incomplete = 0;
-		snprintf(text, sizeof(text),
+		snprintf(reply, sizeof(reply),
 		    "%s failed, some requested items could not be returned",
 		    cmdname);
-		session_reply(s, s->pending_tag, "NO", text);
+		session_reply(s, s->pending_tag, "NO", reply);
 		return;
 	}
 
-	{
-		char	text[32];
-
-		snprintf(text, sizeof(text), "%s completed", cmdname);
-		session_reply(s, s->pending_tag, "OK", text);
-	}
+	snprintf(reply, sizeof(reply), "%s completed", cmdname);
+	session_reply(s, s->pending_tag, "OK", reply);
 }
blob - 591d448412ee231a8ee8bc46eda9c51afb1b6fdf
blob + 0e316e857ef102f15f9640ef5cc55c55eefb6c21
--- src/utf8.c
+++ src/utf8.c
@@ -16,7 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-/* RFC 5198 SS2: UTF-8, no C1 controls, no BOM */
+/* RFC 5198 section 2: UTF-8, no C1 controls, no BOM */
 
 #include "utf8.h"
 
@@ -33,7 +33,7 @@ utf8_mailbox_ok(const char *name)
 			continue;
 		}
 
-		/* RFC 3629 SS4's table, one branch per lead-byte class. */
+		/* RFC 3629 section 4's table, one branch per lead byte */
 		if (p[0] >= 0xc2 && p[0] <= 0xdf) {
 			need = 1; lo = 0x80; hi = 0xbf;
 		} else if (p[0] == 0xe0) {
@@ -59,11 +59,11 @@ utf8_mailbox_ok(const char *name)
 			if (p[i] < 0x80 || p[i] > 0xbf)
 				return (0);
 
-		/* RFC 5198 SS2 item 3: the C1 controls MUST NOT appear. */
+		/* RFC 5198 section 2 item 3: no C1 controls */
 		if (p[0] == 0xc2 && p[1] <= 0x9f)
 			return (0);
 
-		/* RFC 5198 SS2 item 5 bans a leading BOM; refused anywhere */
+		/* RFC 5198 section 2 item 5 bans a leading BOM; none at all */
 		if (p[0] == 0xef && p[1] == 0xbb && p[2] == 0xbf)
 			return (0);