commit - 65c7cf929f78f3280baaf04805e06a3d59b8b4f2
commit + be72be56ebed8f5a27d7258f8e2c6b5ba106083e
blob - 5705b36cfc88caba91ffb269193d7c04ba72f507
blob + c59aa80793e00780ac0a04bc9ce5f96d57d19f3a
--- README.md
+++ README.md
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
.\"
.\" 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
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 *);
return (0);
fail:
- /* all or nothing */
*outlen = save;
return (-1);
}
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));
} 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
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);
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
.\" 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
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
#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
#include <sys/file.h>
#include <sys/stat.h>
-#include <dirent.h>
#include <errno.h>
#include <event.h>
#include <fcntl.h>
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;
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
/* 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);
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
int in_new;
char oldsuffix[64];
char letters[8];
+ char *newline;
};
static int
{
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);
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;
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;
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
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,
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) {
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;
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
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)
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
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 */
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" */
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 *);