Commit Diff


commit - 65c7cf929f78f3280baaf04805e06a3d59b8b4f2
commit + be72be56ebed8f5a27d7258f8e2c6b5ba106083e
blob - 5705b36cfc88caba91ffb269193d7c04ba72f507
blob + c59aa80793e00780ac0a04bc9ce5f96d57d19f3a
--- 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.1.7 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
+**Status:** 0.1.9 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
 
 ## What it is
 
blob - cd483dbb205734c6ecdc3591bf43d05bdd60f354
blob + 291af09dff0964c50e04f656d1205a7a788e523e
--- contrib/imapduser.8
+++ contrib/imapduser.8
@@ -2,7 +2,7 @@
 .\"
 .\" Written for the OpenIMAPD project. Public domain / no rights reserved.
 .\"
-.Dd $Mdocdate: September 28 2026 $
+.Dd $Mdocdate: September 29 2026 $
 .Dt IMAPDUSER 8
 .Os
 .Sh NAME
blob - 3156bf94c2560eef628e87b9365638328a43164e
blob + 8e8b857481d199891feabf48f917a2758c8c1a62
--- src/envelope.c
+++ src/envelope.c
@@ -39,6 +39,10 @@
 
 static size_t	 quoted_end(const char *, size_t, size_t);
 static size_t	 unquoted_find(const char *, size_t, size_t, const char *);
+static int	 address_list_add(char *, size_t, size_t *, char *, size_t,
+		    int *);
+static int	 envbuf_append_address_fields(char *, size_t, size_t *,
+		    const char *, size_t, const char *, int *);
 static int	 envelope_from_header(const char *, size_t, char *, size_t,
 		    size_t *);
 
@@ -91,7 +95,6 @@ envbuf_append_nstring(char *buf, size_t bufsize, size_
 	return (0);
 
 fail:
-	/* all or nothing */
 	*outlen = save;
 	return (-1);
 }
@@ -292,20 +295,16 @@ address_list_next(char *val, size_t vallen, size_t *po
 	return (ADDR_MAILBOX);
 }
 
-/* RFC 9051 SS7.5.2 address list, or NIL; val is rewritten in place */
-int
-envbuf_append_address_list(char *buf, size_t bufsize, size_t *outlen,
-    char *val, size_t vallen)
+static int
+address_list_add(char *buf, size_t bufsize, size_t *outlen, char *val,
+    size_t vallen, int *any)
 {
 	struct address	 a;
 	char		*tok;
-	size_t		 save = *outlen, pos = 0, toklen;
-	int		 any = 0, ingroup = 0, kind;
+	size_t		 pos = 0, toklen;
+	int		 ingroup = 0, kind;
 
 	vallen = address_uncomment(val, vallen);
-	if (envbuf_append(buf, bufsize, outlen, "(", 1) == -1)
-		return (-1);
-
 	while ((kind = address_list_next(val, vallen, &pos, &ingroup, &tok,
 	    &toklen)) != 0) {
 		memset(&a, 0, sizeof(a));
@@ -315,16 +314,49 @@ envbuf_append_address_list(char *buf, size_t bufsize, 
 		} else if (kind == ADDR_MAILBOX &&
 		    address_split(tok, toklen, &a) == -1)
 			continue;
+		if (*any == 0 &&
+		    envbuf_append(buf, bufsize, outlen, "(", 1) == -1)
+			return (-1);
+		*any = 1;
 		if (envbuf_append_one_address(buf, bufsize, outlen, &a) == -1)
 			return (-1);
-		any = 1;
 	}
+	return (0);
+}
 
-	if (!any) {
-		*outlen = save;
-		return (envbuf_append_str(buf, bufsize, outlen, "NIL"));
+/* RFC 5322 SS4.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		 ulen, i;
+	int		 any = 0, rc;
+
+	*seen = 0;
+	while (header_next_field(hdr, hdrlen, &off, &name, &namelen, &val,
+	    &vallen) == 1) {
+		if (namelen != flen || strncasecmp(name, field, flen) != 0)
+			continue;
+		if ((unf = malloc(vallen + 1)) == NULL)
+			return (-1);
+		for (i = ulen = 0; i < vallen; i++) {
+			if (val[i] == '\r' || val[i] == '\n')
+				continue;
+			if (val[i] != ' ' && val[i] != '\t')
+				*seen = 1;
+			unf[ulen++] = val[i];
+		}
+		rc = address_list_add(buf, bufsize, outlen, unf, ulen, &any);
+		free(unf);
+		if (rc == -1)
+			return (-1);
 	}
-	return (envbuf_append(buf, bufsize, outlen, ")", 1));
+	if (any)
+		return (envbuf_append(buf, bufsize, outlen, ")", 1));
+	return (envbuf_append_str(buf, bufsize, outlen, "NIL"));
 }
 
 int
@@ -349,8 +381,11 @@ static int
 envelope_from_header(const char *hdr, size_t hdrlen, char *out,
     size_t outsize, size_t *outlen)
 {
+	static const char *const fallback_fields[] = { "Sender", "Reply-To" };
+	static const char *const addr_fields[] = { "To", "Cc", "Bcc" };
 	char		 from_formatted[ENVELOPE_MAX];
-	size_t		 from_len = 0;
+	size_t		 from_len = 0, fi, save;
+	int		 seen;
 
 	if (envbuf_append(out, outsize, outlen, "(", 1) == -1)
 		return (-1);
@@ -366,86 +401,37 @@ envelope_from_header(const char *hdr, size_t hdrlen, c
 	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
 		return (-1);
 
-	{
-		char	*val;
-		size_t	 vallen;
-
-		if (extract_header_field(hdr, hdrlen, "From", &val,
-		    &vallen) == 0) {
-			int	rc = envbuf_append_address_list(from_formatted,
-			    sizeof(from_formatted), &from_len, val, vallen);
-			free(val);
-			if (rc == -1)
-				return (-1);
-		} else {
-			if (envbuf_append_str(from_formatted,
-			    sizeof(from_formatted), &from_len, "NIL") == -1)
-				return (-1);
-		}
-	}
+	if (envbuf_append_address_fields(from_formatted,
+	    sizeof(from_formatted), &from_len, hdr, hdrlen, "From",
+	    &seen) == -1)
+		return (-1);
 	if (envbuf_append(out, outsize, outlen, from_formatted,
 	    from_len) == -1)
 		return (-1);
 	if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
 		return (-1);
 
-	{
-		static const char *const fallback_fields[] =
-		    { "Sender", "Reply-To" };
-		size_t	 fi;
-
-		for (fi = 0; fi < 2; fi++) {
-			char	*val;
-			size_t	 vallen;
-			int	 used_value = 0;
-
-			if (extract_header_field(hdr, hdrlen,
-			    fallback_fields[fi], &val, &vallen) == 0) {
-				if (vallen > 0) {
-					int	rc = envbuf_append_address_list(
-					    out, outsize, outlen, val,
-					    vallen);
-					used_value = 1;
-					free(val);
-					if (rc == -1)
-						return (-1);
-				} else
-					free(val);
-			}
-			if (!used_value &&
-			    envbuf_append(out, outsize, outlen,
+	for (fi = 0; fi < 2; fi++) {
+		save = *outlen;
+		if (envbuf_append_address_fields(out, outsize, outlen, hdr,
+		    hdrlen, fallback_fields[fi], &seen) == -1)
+			return (-1);
+		if (!seen) {
+			*outlen = save;
+			if (envbuf_append(out, outsize, outlen,
 			    from_formatted, from_len) == -1)
 				return (-1);
-			if (envbuf_append(out, outsize, outlen,
-			    " ", 1) == -1)
-				return (-1);
 		}
+		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+			return (-1);
 	}
 
-	{
-		static const char *const addr_fields[] = { "To", "Cc", "Bcc" };
-		size_t	 fi;
-
-		for (fi = 0; fi < 3; fi++) {
-			char	*val;
-			size_t	 vallen;
-
-			if (extract_header_field(hdr, hdrlen,
-			    addr_fields[fi], &val, &vallen) == 0) {
-				int	rc = envbuf_append_address_list(out,
-				    outsize, outlen, val, vallen);
-				free(val);
-				if (rc == -1)
-					return (-1);
-			} else {
-				if (envbuf_append_str(out, outsize, outlen,
-				    "NIL") == -1)
-					return (-1);
-			}
-			if (envbuf_append(out, outsize, outlen,
-			    " ", 1) == -1)
-				return (-1);
-		}
+	for (fi = 0; fi < 3; fi++) {
+		if (envbuf_append_address_fields(out, outsize, outlen, hdr,
+		    hdrlen, addr_fields[fi], &seen) == -1)
+			return (-1);
+		if (envbuf_append(out, outsize, outlen, " ", 1) == -1)
+			return (-1);
 	}
 
 	if (append_field_nstring(out, outsize, outlen, hdr, hdrlen,
blob - 89cc5d2c5094633ba2e50057c592a262ca6ef0ec
blob + 53289376fe16e21ab8f9917fe0f74f8ccbb29f0b
--- 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: September 28 2026 $
+.Dd $Mdocdate: September 29 2026 $
 .Dt IMAPD 8
 .Os
 .Sh NAME
@@ -972,3 +972,18 @@ afterwards is refused until it selects a mailbox again
 served
 .Li INBOX
 under the deleted mailbox's name.
+.Pp
+All sessions of one account are served by one process, which runs one
+command at a time.
+.Li FETCH
+and
+.Li SEARCH
+give way to the account's other sessions as they go;
+.Li STORE ,
+.Li EXPUNGE ,
+.Li COPY
+and
+.Li MOVE
+do not.
+One of these over a very large mailbox therefore delays every other
+session of the same account until it completes.
blob - 6eb1a0411a611331b59b79c7139120c4135ecfc8
blob + 39657eccd76572179da9fd1272ca3f9e4935ea00
--- src/imapd.h
+++ src/imapd.h
@@ -28,7 +28,7 @@
 #include <imsg.h>
 #include <stdint.h>
 
-#define IMAPD_VERSION	"0.1.8"
+#define IMAPD_VERSION	"0.1.9"
 
 #define IMAPD_USER	"_imapd"
 #define IMAPD_AUTH_USER	"_imapauth"
blob - 3088dce4bc163aa0100848cfe302e99edd45c2d5
blob + 24b638d4b47dfecd0463e58273e3821e1b60219a
--- src/mbox_fetch.c
+++ src/mbox_fetch.c
@@ -20,7 +20,6 @@
 #include <sys/file.h>
 #include <sys/stat.h>
 
-#include <dirent.h>
 #include <errno.h>
 #include <event.h>
 #include <fcntl.h>
@@ -608,8 +607,6 @@ handle_mbox_status(struct imsg_mbox_status *req, struc
 	struct mbox_index		 idx;
 	struct imsg_mbox_status_result	 reply;
 	struct index_lock		 il = INDEX_LOCK_INIT;
-	DIR				*dp;
-	struct dirent			*de;
 	const char			*target;
 	int				 tfd = -1, locked;
 
@@ -642,55 +639,12 @@ handle_mbox_status(struct imsg_mbox_status *req, struc
 		goto send;
 	}
 
-	if (index_load(il.fd, &idx) == -1) {
+	if (refresh_index(tfd, &idx, il.fd) == -1) {
 		index_lock_release(&il);
 		reply.error = MBOX_OP_ERR_GENERIC;
 		goto send;
 	}
 
-	/* STATUS may be first to touch a mailbox: persist a fresh header */
-	if (idx.fresh && index_save(tfd, &idx) == -1)
-		log_warnx("session %u: STATUS: could not persist the new "
-		    "index header", session_id);
-
-	{
-		int	newfd;
-
-		dp = NULL;
-		newfd = openat(tfd, "new",
-		    O_RDONLY | O_DIRECTORY);
-		if (newfd != -1 && (dp = fdopendir(newfd)) == NULL)
-			close(newfd);
-	}
-	if (dp == NULL) {
-		if (errno != ENOENT)
-			log_warn("session %u: opendir new", session_id);
-	} else {
-		while ((de = readdir(dp)) != NULL) {
-			if (de->d_name[0] == '.')
-				continue;
-			if (index_has_basename(&idx, de->d_name))
-				continue;
-			if (index_append(&idx, idx.uidnext, de->d_name)
-			    == -1) {
-				closedir(dp);
-				index_free(&idx);
-				index_lock_release(&il);
-				reply.error = MBOX_OP_ERR_GENERIC;
-				goto send;
-			}
-			idx.uidnext++;
-		}
-		closedir(dp);
-	}
-
-	if (index_save(tfd, &idx) == -1) {
-		index_free(&idx);
-		index_lock_release(&il);
-		reply.error = MBOX_OP_ERR_GENERIC;
-		goto send;
-	}
-
 	reply.error = MBOX_OP_OK;
 	reply.messages = (uint32_t)idx.nlines;
 	reply.uidnext = idx.uidnext;
blob - fb8ba5603a5446fd95335c1e1ef8654a5f73daf2
blob + 40b33bfac7582be921b04b3c91cd3267464ec215
--- src/mbox_search.c
+++ src/mbox_search.c
@@ -140,8 +140,9 @@ merge_keywords(int mode, const char *old_kws, const ch
 /* A content key's value before the parser-worker has answered */
 #define SEARCH_UNKNOWN		2
 
-/* Parser requests a SEARCH makes before it lets other sessions run */
+/* Parser requests, or messages, a SEARCH takes before others run */
 #define SEARCH_YIELD_REQUESTS	64
+#define SEARCH_YIELD_MESSAGES	1000
 
 static void	 search_walk_step(struct store_session *);
 static void	 search_walk_finish(struct store_session *, int);
@@ -427,6 +428,14 @@ search_walk_step(struct store_session *ss)
 		char			 suffix[64];
 		int			 r;
 
+		if (++sw->walked > SEARCH_YIELD_MESSAGES) {
+			sw->walked = 0;
+			sw->next = i;
+			tv.tv_sec = 0;
+			tv.tv_usec = 0;
+			evtimer_add(&sw->yield_ev, &tv);
+			return;
+		}
 		if (index_parse_line(sw->idx.lines[i - 1], &rec) == -1)
 			continue;
 
blob - 4061bcf4257fbef8e964f7f25980a2576a5360c8
blob + 4df03f1fb49ab72829be8f927238bad22709c4f4
--- src/mbox_store.c
+++ src/mbox_store.c
@@ -45,6 +45,7 @@ struct store_step {
 	int		 in_new;
 	char		 oldsuffix[64];
 	char		 letters[8];
+	char		*newline;
 };
 
 static int
@@ -54,12 +55,11 @@ store_plan(struct store_session *ss, const struct imsg
 {
 	char		 suffix[64], newkeywords[MBOX_FLAGS_MAX];
 	const char	*lp;
-	off_t		 size;
 	uint32_t	 old_sysflags, new_sysflags;
 	int		 len;
 
 	if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
-	    rec->basename, &size, suffix, sizeof(suffix)) == -1) {
+	    rec->basename, NULL, suffix, sizeof(suffix)) == -1) {
 		log_warnx("session %u: message %s (uid %u) indexed but missing "
 		    "on disk, skipped", session_id, rec->basename, rec->uid);
 		return (0);
@@ -158,92 +158,105 @@ store_apply(struct store_session *ss, const struct ims
     struct store_step **stepsp, size_t *nstepsp)
 {
 	struct store_step	*steps = NULL, *grown;
-	size_t			 nsteps = 0, maxsteps = 0;
+	size_t			 nsteps = 0, maxsteps = 0, applied, k;
 	uint32_t		 i, max_hi;
 	uint64_t		 new_modseq = idx->highestmodseq + 1;
-	int			 ok = 1, changed = 0, pass;
+	int			 ok = 1, changed = 0;
 
 	max_hi = seqset_max_hi(resolved, nresolved);
-	for (pass = 0; pass < 2 && ok; pass++) {
-		for (i = 1; i <= (uint32_t)idx->nlines; i++) {
-			struct index_rec	 rec;
-			struct store_step	 st;
-			enum seqset_pos		 pos;
-			char			 newline[STORE_INDEX_LINE_MAX];
-			char			 oldpath[600], newpath[600];
-			char			*dup;
-			int			 rc;
+	for (i = 1; i <= (uint32_t)idx->nlines; i++) {
+		struct index_rec	 rec;
+		struct store_step	 st;
+		enum seqset_pos		 pos;
+		char			 newline[STORE_INDEX_LINE_MAX];
+		char			 oldpath[600], newpath[600];
+		int			 rc;
 
-			if (index_parse_line(idx->lines[i - 1], &rec) == -1)
+		if (index_parse_line(idx->lines[i - 1], &rec) == -1)
+			continue;
+		pos = seqset_position(resolved, nresolved, max_hi, by_uid,
+		    rec.uid, i);
+		if (pos == SEQSET_PAST_END)
+			break;
+		if (pos == SEQSET_SKIP ||
+		    (since != NULL && rec.modseq <= *since))
+			continue;
+
+		memset(&st, 0, sizeof(st));
+		st.seqno = i;
+		if (req->has_unchangedsince &&
+		    rec.modseq > req->unchangedsince) {
+			st.modified = 1;
+			st.modseq = rec.modseq;
+		} else {
+			rc = store_plan(ss, req, &rec, new_modseq, &st,
+			    newline, sizeof(newline));
+			if (rc == 0)
 				continue;
-			pos = seqset_position(resolved, nresolved, max_hi,
-			    by_uid, rec.uid, i);
-			if (pos == SEQSET_PAST_END)
+			if (rc == -1 || (st.rename && store_paths(rec.basename,
+			    &st, oldpath, sizeof(oldpath), newpath,
+			    sizeof(newpath)) == -1)) {
+				ok = 0;
 				break;
-			if (pos == SEQSET_SKIP ||
-			    (since != NULL && rec.modseq <= *since))
-				continue;
-
-			memset(&st, 0, sizeof(st));
-			st.seqno = i;
-			if (req->has_unchangedsince &&
-			    rec.modseq > req->unchangedsince) {
-				st.modified = 1;
-				st.modseq = rec.modseq;
-			} else {
-				rc = store_plan(ss, req, &rec, new_modseq, &st,
-				    newline, sizeof(newline));
-				if (rc == 0)
-					continue;
-				if (rc == -1 || (st.rename &&
-				    store_paths(rec.basename, &st, oldpath,
-				    sizeof(oldpath), newpath,
-				    sizeof(newpath)) == -1)) {
-					ok = 0;
-					break;
-				}
 			}
-			if (pass == 0)
-				continue;
-
-			if (nsteps == maxsteps) {
-				size_t	newmax = maxsteps ? maxsteps * 2 : 64;
-
-				if ((grown = reallocarray(steps, newmax,
-				    sizeof(*steps))) == NULL) {
-					log_warn("session %u: STORE: "
-					    "reallocarray", session_id);
-					ok = 0;
-					break;
-				}
-				steps = grown;
-				maxsteps = newmax;
-			}
-			if (st.modified) {
-				steps[nsteps++] = st;
-				continue;
-			}
-			if ((dup = strdup(newline)) == NULL) {
+			if ((st.newline = strdup(newline)) == NULL) {
 				log_warn("session %u: strdup index line",
 				    session_id);
 				ok = 0;
 				break;
 			}
-			if (st.rename && renameat(ss->mailbox_dir_fd, oldpath,
+		}
+
+		if (nsteps == maxsteps) {
+			size_t	newmax = maxsteps ? maxsteps * 2 : 64;
+
+			if ((grown = reallocarray(steps, newmax,
+			    sizeof(*steps))) == NULL) {
+				log_warn("session %u: STORE: reallocarray",
+				    session_id);
+				free(st.newline);
+				ok = 0;
+				break;
+			}
+			steps = grown;
+			maxsteps = newmax;
+		}
+		steps[nsteps++] = st;
+	}
+
+	for (applied = 0; ok && applied < nsteps; applied++) {
+		struct store_step	*st = &steps[applied];
+		struct index_rec	 rec;
+		char			 oldpath[600], newpath[600];
+
+		if (st->modified)
+			continue;
+		if (st->rename) {
+			if (index_parse_line(idx->lines[st->seqno - 1],
+			    &rec) == -1 || store_paths(rec.basename, st,
+			    oldpath, sizeof(oldpath), newpath,
+			    sizeof(newpath)) == -1) {
+				ok = 0;
+				break;
+			}
+			if (renameat(ss->mailbox_dir_fd, oldpath,
 			    ss->mailbox_dir_fd, newpath) == -1) {
 				log_warn("session %u: rename %s -> %s",
 				    session_id, oldpath, newpath);
-				free(dup);
 				ok = 0;
 				break;
 			}
-			steps[nsteps++] = st;
-			free(idx->lines[i - 1]);
-			idx->lines[i - 1] = dup;
-			if (st.changed)
-				changed = 1;
 		}
+		free(idx->lines[st->seqno - 1]);
+		idx->lines[st->seqno - 1] = st->newline;
+		st->newline = NULL;
+		if (st->changed)
+			changed = 1;
 	}
+	for (k = 0; k < nsteps; k++) {
+		free(steps[k].newline);
+		steps[k].newline = NULL;
+	}
 
 	if (ok && changed) {
 		idx->highestmodseq = new_modseq;
@@ -251,7 +264,7 @@ store_apply(struct store_session *ss, const struct ims
 			ok = 0;
 	}
 	if (!ok)
-		store_undo(ss, idx, steps, nsteps);
+		store_undo(ss, idx, steps, applied);
 	else if (synced && changed && new_modseq > ss->idle_baseline.modseq)
 		/* LOCK_EX held since the sync: new_modseq is ours alone */
 		ss->idle_baseline.modseq = new_modseq;
@@ -426,14 +439,13 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req,
 		const char	*lp;
 		uint32_t	 sysflags;
 		char		 suffix[64];
-		off_t		 size;
 
 		if (index_parse_line(idx.lines[in], &rec) == -1) {
 			idx.lines[out++] = idx.lines[in];
 			continue;
 		}
 		if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
-		    rec.basename, &size, suffix, sizeof(suffix)) == -1) {
+		    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);
blob - 0efcbefdf39129fc1141c56af54fa50edb9a656c
blob + a7f71c2b4615291f9471cd67ab552d19672ed753
--- src/mime.c
+++ src/mime.c
@@ -195,10 +195,30 @@ locate_message_file(struct cur_snapshot *snap, int dfd
     off_t *size_out, char *suffix_out, size_t suffix_out_size)
 {
 	struct stat	 st;
+	const char	*hit;
 	char		 path[600];
+	size_t		 baselen = strlen(basename);
 
 	suffix_out[0] = '\0';
 
+	if ((hit = cur_snapshot_find(snap, dfd, basename, baselen)) != NULL) {
+		if (cur_entry_take(hit, baselen, path, sizeof(path),
+		    suffix_out, suffix_out_size) == -1)
+			return (-1);
+		/* no size wanted: STORE and EXPUNGE read cur/ under LOCK_EX */
+		if (size_out == NULL)
+			return (0);
+		if (fstatat(dfd, path, &st, 0) == 0) {
+			*size_out = st.st_size;
+			return (0);
+		}
+		if (errno != ENOENT)
+			return (-1);
+		/* renamed since cur/ was read: read it again */
+		cur_snapshot_discard(snap);
+		suffix_out[0] = '\0';
+	}
+
 	if (snprintf(path, sizeof(path), "new/%s", basename) >=
 	    (int)sizeof(path)) {
 		log_warnx("session %u: basename too long: %s", session_id,
@@ -206,7 +226,8 @@ locate_message_file(struct cur_snapshot *snap, int dfd
 		return (-1);
 	}
 	if (fstatat(dfd, path, &st, 0) == 0) {
-		*size_out = st.st_size;
+		if (size_out != NULL)
+			*size_out = st.st_size;
 		return (0);
 	}
 	if (errno != ENOENT) {
@@ -217,6 +238,8 @@ locate_message_file(struct cur_snapshot *snap, int dfd
 	if (scan_cur_for_basename(snap, dfd, basename, path, sizeof(path),
 	    suffix_out, suffix_out_size) == -1)
 		return (-1);
+	if (size_out == NULL)
+		return (0);
 	if (fstatat(dfd, path, &st, 0) == -1)
 		return (-1);
 	*size_out = st.st_size;
@@ -245,6 +268,12 @@ open_message_file(struct cur_snapshot *snap, int dfd, 
 	if (scan_cur_for_basename(snap, dfd, basename, path, sizeof(path),
 	    NULL, 0) == -1)
 		return (-1);
+	if ((fd = openat(dfd, path, O_RDONLY)) != -1 || errno != ENOENT)
+		return (fd);
+	cur_snapshot_discard(snap);
+	if (scan_cur_for_basename(snap, dfd, basename, path, sizeof(path),
+	    NULL, 0) == -1)
+		return (-1);
 	return (openat(dfd, path, O_RDONLY));
 }
 
blob - 83080cc8b4803a2e5695bc813d3e09df2b69d5dd
blob + 0fc76f71c80271d4856ee9038dcac60a2f275f4f
--- src/search_match.c
+++ src/search_match.c
@@ -488,12 +488,12 @@ match_leaf(const char *hdr, size_t hdrlen,
 		return (0);
 	}
 
+	/* only the first Date: or Subject: counts, as ENVELOPE shows it */
 	while (!hit && header_next_field(hdr, hdrlen, &off, &name, &namelen,
 	    &val, &vallen) == 1) {
 		if (namelen != wantlen || strncasecmp(name, want, wantlen) != 0)
 			continue;
 		if (date) {
-			/* the first Date: decides; RFC 5322 SS3.6 allows one */
 			if (date_field_day(val, vallen, &day) == -1)
 				return (0);
 			if (l->op == SEARCH_OP_SENTBEFORE)
@@ -513,6 +513,8 @@ match_leaf(const char *hdr, size_t hdrlen,
 			return (-1);
 		hit = ci_contains(dec, dlen, pool + l->str_off, l->str_len);
 		free(dec);
+		if (l->op == SEARCH_OP_SUBJECT)
+			break;
 	}
 	return (hit);
 }
blob - 3c83389b2eb5b76d0b40c888d57458d13bb30233
blob + 48f76022adc5e6e524a657288cc45ed786e32ccd
--- src/store_internal.h
+++ src/store_internal.h
@@ -74,7 +74,7 @@ struct store_fetch_walk {
 	uint64_t		 seen_modseq;
 };
 
-/* RFC 9051 SS6.4.4 SEARCH, yielding every SEARCH_YIELD_REQUESTS */
+/* RFC 9051 SS6.4.4 SEARCH, yielding to the account's other sessions */
 struct store_search_walk {
 	int			 active;
 	int			 wait_parser;	/* paused until one comes */
@@ -92,6 +92,7 @@ struct store_search_walk {
 	uint32_t		 next;		/* index line to resume at */
 	uint32_t		 sent;
 	uint32_t		 asked;
+	uint32_t		 walked;
 };
 
 /* two stat(2) calls, no lock: "." and "new" */
@@ -284,8 +285,6 @@ int	 envbuf_append_nstring(char *, size_t, size_t *, c
 		    size_t);
 int	 envbuf_append_one_address(char *, size_t, size_t *,
 		    const struct address *);
-int	 envbuf_append_address_list(char *, size_t, size_t *, char *,
-		    size_t);
 int	 append_field_nstring(char *, size_t, size_t *, const char *,
 		    size_t, const char *);
 int	 build_envelope(int, const char *, char **, uint32_t *);