commit 845c36149fc11758db2f7d64937a70de2a56ed67 from: David Williams date: Sat Aug 29 00:44:55 2026 UTC trim comments throughout tree commit - a96b2723a3021df065f3860ba0115f2bfea97764 commit + 845c36149fc11758db2f7d64937a70de2a56ed67 blob - d52e35c2e1f4e621ef4078d51583e7add8cb3d06 blob + 7f147c923fac828c5211a4b66dfef861d9307ab8 --- README.md +++ README.md @@ -1,24 +1,24 @@ # OpenIMAPD -A from-scratch IMAP4rev2 ([RFC 9051](https://www.rfc-editor.org/rfc/rfc9051)) server for OpenBSD, written in traditional C in the privilege-separated tradition of `smtpd(8)`, `httpd(8)`, and `ntpd(8)`. No third-party IMAP library, no borrowed protocol engine. +A from-scratch IMAP4rev2 ([RFC 9051](https://www.rfc-editor.org/rfc/rfc9051)) server for OpenBSD, written in traditional C in the privilege-separated tradition of `smtpd(8)`, `httpd(8)`, and `ntpd(8)`. No third-party IMAP library. -**Status:** pre-release, version 0.1.1. Actively developed. Not yet a port — see [Getting the source](#getting-the-source) below for the repository. +**Status:** pre-release, version 0.1.1 Actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository. ## What it is - **Privilege-separated**, `smtpd`-style: a root *parent* process reads configuration and binds the listening sockets; unprivileged *listener* and *auth* children handle the network and credential checks; a *store* child is forked per authenticated session, chroots into the mail spool, and drops privileges to that session's own user before ever touching a message. `pledge(2)`, `unveil(2)`, and `chroot(2)` enforce these boundaries, not just convention. -- **Storage**: stock maildir format (`tmp/`/`new/`/`cur/`, atomic delivery via `rename(2)`) — readable with `ls` and `grep`, and natively understood by `smtpd(8)`'s own `maildir` delivery action. IMAP's extra bookkeeping (UIDs, UIDVALIDITY, per-message mod-sequences, keywords) lives in a small, `flock(2)`-guarded, line-oriented index file per mailbox — plain colon-delimited text, not a database. +- **Storage**: stock maildir format (`tmp/`/`new/`/`cur/`, atomic delivery via `rename(2)`), readable with `ls` and `grep`, and natively understood by `smtpd(8)`'s own `maildir` delivery action. IMAP's extra bookkeeping (UIDs, UIDVALIDITY, per-message mod-sequences, keywords) lives in a small, `flock(2)`-guarded, line-oriented index file per mailbox, plain colon-delimited text, not a database. - **Transport**: STARTTLS on port 143 and implicit TLS on port 993 ([RFC 8314](https://www.rfc-editor.org/rfc/rfc8314)), via `libtls`. `AUTH=PLAIN` only, refused before TLS is established. ## Protocol coverage `CAPABILITY`, `STARTTLS`, `AUTHENTICATE`, `ID`, `ENABLE`, `SELECT`, `EXAMINE`, `CREATE`, `DELETE`, `RENAME`, `LIST`, `LSUB`, `NAMESPACE`, `STATUS`, `FETCH` (including `ENVELOPE`, `BODYSTRUCTURE`, and MIME-part-addressed `BODY[]`/`BODY.PEEK[]`), `STORE`, `SEARCH`, `APPEND`, `COPY`, `MOVE`, `EXPUNGE`, `UNSELECT`, `CLOSE`, the `UID`-prefixed form of every command that supports it, `IDLE` with real cross-session push, and the [RFC 7162](https://www.rfc-editor.org/rfc/rfc7162) `CONDSTORE`/`QRESYNC` extensions. -`SUBSCRIBE`, `UNSUBSCRIBE`, and ACL/shared-mailbox support are deliberately out of scope, not unfinished — matched against real client behavior and left out on the same "every extra command is attack surface" principle `smtpd(8)` uses to justify skipping `VRFY`/`EXPN`. Full protocol-scope reasoning and other caveats (flat per-user namespace, `IDLE` push triggers, `EXAMINE` read-only enforcement, etc.) are documented in `imapd(8)`'s CAVEATS section — that man page is the authoritative reference, this file is just an overview. +`SUBSCRIBE`, `UNSUBSCRIBE`, and ACL/shared-mailbox support are deliberately left out. ## Requirements -OpenBSD only. This depends on ``, `pledge(2)`, `unveil(2)`, and libutil's `imsgbuf_*` API, none of which exist outside OpenBSD, so it will not build on any other host. Developed and tested against OpenBSD 8.0. Links against libevent, libtls/libssl/libcrypto, and libutil — all base-system libraries (see `src/Makefile`). +OpenBSD only. This depends on ``, `pledge(2)`, `unveil(2)`, and libutil's `imsgbuf_*` API, none of which exist outside OpenBSD. Developed and tested against OpenBSD 8.0. Links against libevent, libtls/libssl/libcrypto, and libutil — all base-system libraries (see `src/Makefile`). ## Building and installing @@ -30,7 +30,7 @@ doas make install Installs the daemon to `/usr/local/sbin/imapd`, man pages to `/usr/local/man/man8`, the `imapduser` account-provisioning tool alongside the daemon, and a sample config to `/usr/local/share/examples/imapd/imapd.conf`. -The `rc.d(8)` script is not installed automatically — `install(1)`, not `cp(1)`, matters here so the installed copy is executable regardless of the source tree's own permission bits: +The `rc.d(8)` script is not installed automatically. `install(1)`, not `cp(1)`, so the installed copy is executable regardless of the source tree's own permission bits: ``` doas install -o root -g wheel -m 555 src/rc.d/imapd /etc/rc.d/imapd @@ -38,7 +38,7 @@ doas install -o root -g wheel -m 555 src/rc.d/imapd /e ## Configuring -Copy the sample config into place with restrictive permissions — imapd refuses to start against a config that's group- or world-writable, *or* world-readable: +Copy the sample config into place with restrictive permissions. imapd refuses to start against a config that's group- or world-writable, *or* world-readable: ``` doas install -o root -g wheel -m 600 \ @@ -68,9 +68,9 @@ doas rcctl start imapd Beyond the deliberate protocol-scope decisions covered in `imapd(8)`'s CAVEATS: -- If the listener or auth process exits unexpectedly after startup, it is not automatically restarted — a deliberate choice, not an oversight: neither `smtpd(8)` nor `httpd(8)` auto-restarts their own equivalent core processes either. Recovery is `rcctl restart imapd`. See `imapd(8)`. +- If the listener or auth process exits unexpectedly after startup, it is not automatically restarted. Recovery is `rcctl restart imapd`. See `imapd(8)`. -`SIGHUP` reloads `spool`, `attachment max`, and the TLS certificate/key without dropping connected sessions, matching `httpd(8)`'s own documented reload behavior — `listen on` and `credentials` changes still require a restart. See `imapd(8)`. +`SIGHUP` reloads `spool`, `attachment max`, and the TLS certificate/key without dropping connected sessions, `listen on` and `credentials` changes still require a restart. See `imapd(8)`. IPv6 is supported (`listen on ::` or `listen on *` for dual-stack) but not the default — see `imapd(8)`'s `listen on` directive. blob - bfef11cc5d838831f3115309fc760b76d835c501 blob + 3011ab2b68aa384025c3db089fbdca6bfd0a28ad --- contrib/imapd-teardown +++ contrib/imapd-teardown @@ -16,65 +16,17 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -# imapd-teardown -- completely remove an installed imapd, to test +# imapd-teardown, completely remove an installed imapd, to test # repeated from-scratch installs. # -# This is a dev-tree-only tool (unlike contrib/imapduser, it is NOT -# installed by src/Makefile's afterinstall: target -- there's no real -# operational reason a production box would ever want to "uninstall -# itself," so it stays contrib/-only, run straight out of the source -# tree). It exists because a fresh-install pass (task #228) found -# five real gaps that only showed up by actually attempting an install -# on a genuinely clean system -- this script is what makes "genuinely -# clean system" repeatable without needing an actual fresh box every -# time. -# -# Scope, by design: -# -# - Everything needed to make the NEXT install genuinely fresh -- -# the daemon binary, its own admin tool (imapduser), both man -# pages, the installed sample config, the rc.d script, boot-time -# relink artifacts, the running config file, the credentials file -# (and its directory), the TLS certificate and key, and the -# _imapd/_imapauth system accounts -- is removed. -# -# - The mail spool (real mail data, not install-time cruft) is left -# alone unless -M is given explicitly, and -M has its own separate -# type-the-path confirmation that -y does not bypass. Revoking an -# install and destroying mail are different-consequence decisions, -# same reasoning as imapduser's -a/-d split. -# -# - smtpd.conf's shd_userbase table (a different daemon's config) is -# never touched. If you also want smtpd to stop trying to deliver -# into a wiped spool, that's a separate, deliberate edit to that -# table. -# # Usage: # doas ./imapd-teardown [-y] [-M] [-c credentials-dir] [-f config-file] # [-s spool-root] [-T tls-cert] [-K tls-key] # # -y skip the general confirmation prompt (still stops to ask -# separately if -M is also given -- see above) +# separately if -M is also given) # -M also remove the mail spool # -# -c/-f/-s/-T/-K override the v1 defaults documented in imapd.8, in -# case this premio install (or some future one) ever diverges from -# them; BINDIR/MANDIR are not overridable here since they're a build- -# time Makefile decision (BINDIR=/usr/local/sbin, MANDIR=/usr/local/ -# man/man in src/Makefile), not a runtime config one. -# -# Stops and disables the running service via rcctl(8) before removing -# anything -- per rcctl(8) itself, "when a package daemon is disabled, -# it is removed from pkg_scripts and its variables are removed if -# any" (man.openbsd.org/rcctl.8): the real mechanism for cleanly -# reversing what "rcctl enable imapd" did, rather than hand-editing -# rc.conf.local's pkg_scripts line the way an earlier pass in this -# project's history had to hand-edit smtpd.conf. -# -# Safe to re-run: every removal is conditional on the target actually -# existing, so a partial or already-torn-down install just reports -# nothing left to do for whatever's already gone. - set -e BINDIR=/usr/local/sbin blob - 4a3837a3ad12eadd36f78dce37c7223f721d5187 blob + b2ac6c85f01332165cfe0472ac57af93f72ca3de --- contrib/imapduser +++ contrib/imapduser @@ -18,75 +18,19 @@ # # imapduser -- add or delete an imapd mailbox account. # -# Renamed and given a real -a/-d mode split from its previous identity -# as "newimapuser", a contrib/-only dev script with add-only behavior. -# It's now an actual installed part of the package (${PREFIX}/sbin/ -# imapduser, via src/Makefile's afterinstall: target), because there's -# real OpenBSD ports precedent for exactly this shape of tool: a -# daemon that keeps its own bespoke, non-system credentials store -# needs its own installed admin tool to manage it, since useradd(8)/ -# userdel(8)/vipw(8) don't apply to a store that isn't /etc/passwd. -# Confirmed directly against OpenBSD's own cyrus-sasl2 port PLIST -# (github.com/openbsd/ports, security/cyrus-sasl2/pkg/PLIST, read -# directly rather than assumed): it installs "@bin sbin/saslpasswd2" -# and "@man man/man8/saslpasswd2.8" for precisely this reason -- -# managing sasldb2, SASL's own bespoke secrets store. -# -# imapd's credentials file is deliberately self-contained the same -# way: auth.c's cred_lookup() reads "username:passwordhash:uid:gid: -# maildir" lines straight out of it and never calls getpwnam(3) for an -# IMAP end user (see auth.c's own header comment -- that decision is -# specifically about end users, not about auth's own _imapauth service -# identity). -# store.c's per-session store child then drops privileges directly to -# that uid/gid (IMSG_STORE_INIT handling) before chroot(2)-ing into -# spool_root and chdir(2)-ing into "/maildir" -- chdir(2) is NOT -# tolerant of a missing directory (it fatal()s), so the maildir itself -# has to already exist, owned by that uid/gid, before the first login; -# store.c only ever mkdir(2)s tmp/new/cur *inside* it on demand. -# -# This script exists purely to keep those three things (a credentials -# line, the on-disk maildir's ownership, and the uid/gid tying them -# together) consistent with each other -- and, in -d mode, to remove -# the credentials line safely without disturbing the mail data it -# pointed at. It does NOT create or remove a real system account: no -# useradd(8)/userdel(8)/adduser(8), nothing written to /etc/passwd or -# /etc/group. That's the whole point of the design this script is -# automating. -# # Usage: # imapduser -a [-c credentials-file] [-s spool-root] [-u uid] [-g gid] username # imapduser -d [-c credentials-file] username # -# -a adds a new mailbox account: creates its maildir (owned by the -# given, or automatically picked, uid:gid) and appends a credentials- -# file line, prompting for a password via encrypt(1) (OpenBSD base, -# man.openbsd.org/encrypt.1), which produces the Blowfish hash -# crypt_checkpass(3) -- and so auth.c's auth_verify() -- expects. +# -a adds a new mailbox account. # -# -d removes a mailbox account's credentials-file line ONLY. It -# deliberately does NOT touch the account's maildir or any mail data -# in it: revoking login ability and destroying mail are two very -# different decisions with very different blast radii, and this tool -# doesn't conflate them. The maildir's path is printed on success so -# the operator can remove it by hand if that's actually what's wanted. +# -d removes a mailbox account's credentials-file line ONLY. # -# Exactly one of -a or -d is required. -c/-s default to imapd.8's own -# documented v1 defaults (/etc/imapd/credentials, /var/mail/imapd). In -# -a mode, if -u/-g are omitted, the same free id is picked -# automatically and used for both (matching the uid==gid==1000 pattern -# already used for the real "dhw" account on premio) -- see -# next_free_id() below for exactly how "free" is decided. -s/-u/-g are -# ignored in -d mode (nothing left to size or own once the credentials -# line is gone). +# Exactly one of -a or -d is required. # # Must be run as root (or via doas/su): -a chown(8)s a maildir to an # arbitrary uid/gid and both modes write to a file that should stay -# root-owned, same spirit as parse.y's check_file_secrecy() check on -# imapd.conf itself (that check isn't applied to the credentials file -# by auth.c today, but keeping it root-owned/non-world-readable is -# still the obviously correct posture for a file full of password -# hashes). +# root-owned. set -e @@ -136,25 +80,6 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -# Canonical ownership/mode for $CRED_FILE, applied any time this script -# creates or rewrites it (fresh creation in -a mode, or after removing -# a line in -d mode). Real bug found on a genuinely fresh install: -# auth.c's auth_main() chroot()s into this file's directory and drops -# privileges to the fixed _imapauth daemon account BEFORE ever opening -# this file -- cred_lookup()'s fopen() happens later, per auth request, -# already running as _imapauth. A root:wheel/0600 file is therefore -# unreadable by the dropped-privilege process no matter what: auth -# logs "fopen credentials: Permission denied" and every AUTHENTICATE -# fails, surfacing to a real IMAP client as a generic "invalid -# credentials" rather than anything that points at the actual cause. -# Fixed by owning the file root:_imapauth (group-readable by the one -# daemon account that ever needs to read it, not group- or world- -# writable, and not readable by any *other* unprivileged account -# either) instead of root:wheel. If the _imapauth group doesn't exist -# yet (daemon accounts not provisioned before this script ran), chgrp -# fails silently below and the warning tells the operator exactly what -# to fix and why, rather than leaving a working-looking credentials -# file that auth can never actually read. set_cred_perms() { chown root:wheel "$CRED_FILE" 2>/dev/null || true if ! chgrp _imapauth "$CRED_FILE" 2>/dev/null; then @@ -169,11 +94,6 @@ set_cred_perms() { chmod 640 "$CRED_FILE" } -# True (exit 0) if $1 is already claimed by /etc/passwd's or /etc/group's -# id column, or by either the uid or gid column of an existing -# credentials-file line -- checked jointly (not per-namespace) so the -# same free number is always safe to hand out as *both* a uid and a gid -# at once, which is what happens below when neither -u nor -g was given. id_in_use() { _id=$1 awk -F: -v id="$_id" '$3 == id { f=1 } END { exit !f }' /etc/passwd && return 0 @@ -193,13 +113,7 @@ next_free_id() { do_add() { if [ ! -e "$CRED_FILE" ]; then - # Real gap found on a genuinely fresh install (no leftover - # /etc/imapd/ from an earlier pass): this used to assume - # CRED_FILE's parent directory already existed, and failed - # with a bare "No such file or directory" from the shell - # redirection below if it didn't -- true for a truly fresh - # box, not just a hypothetical. mkdir -p is a no-op if the - # directory is already there, so this is safe either way. + CRED_DIR=${CRED_FILE%/*} if [ "$CRED_DIR" != "$CRED_FILE" ] && [ ! -d "$CRED_DIR" ]; then mkdir -p "$CRED_DIR" @@ -230,12 +144,6 @@ do_add() { *[!0-9]*|"") echo "${0##*/}: invalid gid: $NEWGID" 1>&2; exit 1 ;; esac - # Credentials-file maildir field is just the bare directory name - # under spool_root (store.c's own log line for a real session shows - # this exactly: "maildir dhw", not a full or nested path) -- see - # store.c's unveil_path construction ("/" + init.maildir) for why - # store.c itself requires this to resolve as a single path - # component relative to its chroot. MAILDIR=$USERNAME MAILDIR_PATH=$SPOOL_ROOT/$MAILDIR @@ -272,8 +180,7 @@ do_delete() { exit 1 fi - # Captured only to tell the operator where the mail data is -- this - # tool never touches it itself, see the usage comment up top for why. + # Captured only to tell the operator where the mail data is. MAILDIR_FIELD=$(awk -F: -v u="$USERNAME" '$1 == u { print $5; exit }' "$CRED_FILE") TMP_CRED_FILE=$(mktemp "${CRED_FILE}.XXXXXXXX") || { blob - ac69fe628610f731e8f0c4d4be75f349932e41d4 blob + c805bc504412d3fbd4b2ce04c460968abe84b808 --- contrib/imapduser.8 +++ contrib/imapduser.8 @@ -1,7 +1,6 @@ .\" $OpenIMAPD$ .\" -.\" Written for the OpenIMAPD project. Public domain / no rights reserved, -.\" matching the project's ports-oriented, OpenBSD-base-inclusion goal. +.\" Written for the OpenIMAPD project. Public domain / no rights reserved. .\" .Dd $Mdocdate: August 19 2026 $ .Dt IMAPDUSER 8 @@ -171,18 +170,4 @@ Default spool root; see .Xr imapd 8 .Sh HISTORY .Nm -was written for the OpenIMAPD project. -It replaces an earlier, add-only, contrib/-only script named -.Nm newimapuser , -renamed and split into -.Fl a Ns / Ns Fl d . -.Nm -is now installed rather than left as a dev-tree-only script, following -the same real precedent as OpenBSD's own -.Sy cyrus-sasl2 -port, which installs -.Xr saslpasswd2 8 -to -.Pa ${PREFIX}/sbin -for the same reason: managing a daemon's own bespoke, non-system -credentials store. +was written for the OpenIMAPD project. \ No newline at end of file blob - a2453cb647b3f6b3ce336c5fa0ada94d5d63bfd8 blob + b470a59c6c9e616124c7179097afd260205e1236 --- src/Makefile +++ src/Makefile @@ -4,35 +4,8 @@ # build on a non-OpenBSD host: it depends on , pledge(2), unveil(2), # and libutil's imsgbuf_* API, none of which exist outside OpenBSD. -# Renamed from "openimap" to "imapd" to match OpenBSD's own naming -# convention for its "Open*" projects: the installed daemon drops the -# "Open" prefix and just goes by "d". Confirmed against -# OpenBSD's own innovations page (openbsd.org/innovations.html): -# OpenNTPD ships ntpd, OpenSMTPD ships smtpd, OpenBGPD ships bgpd, -# OpenIKED ships iked -- and in each case the "D" is already part of -# the *project* name itself, not added at the daemon-naming step. -# OpenSSH is the outlier (no "D"), and only because it ships a whole -# toolkit -- ssh/scp/sftp/ssh-keygen/etc -- not one daemon. This -# project fits the single-daemon shape, so the project itself was -# renamed OpenIMAP -> OpenIMAPD to match. PROG= imapd -# listener.c and store.c were each a single file through the end of -# 0.1 (10,619 and 7,768 lines respectively) -- far larger than any file -# in smtpd (24 files, largest 3,104 lines) or httpd (largest 2,029 -# lines), the two base-system daemons this project otherwise follows -# most closely. Split by responsibility, matching that precedent: the -# listener process's own source is now listener.c (core: accept loop, -# session I/O, line dispatch) + auth_cmd.c + mailbox_cmd.c + append_cmd.c -# + fetch_cmd.c + search_cmd.c + store_cmd.c + store_ipc.c, sharing -# struct session and cross-file prototypes via listener.h (not installed, -# not part of the wire protocol -- see that file). The store process's -# own source is now store.c (core: imsg dispatch loop) + index.c + mime.c -# + envelope.c + mbox_fetch.c + mbox_search.c + mbox_store.c + -# mbox_manage.c + mbox_copy.c, sharing struct mbox_index and cross-file -# prototypes via store_internal.h the same way. Pure move, no behavior -# change -- see each new file's own header comment for exactly which -# commands/responsibility it holds. SRCS= main.c parent.c log.c imsgev.c parse.y \ listener.c auth_cmd.c mailbox_cmd.c append_cmd.c fetch_cmd.c \ search_cmd.c store_cmd.c store_ipc.c \ @@ -40,83 +13,24 @@ SRCS= main.c parent.c log.c imsgev.c parse.y \ store.c index.c mime.c envelope.c mbox_fetch.c mbox_search.c \ mbox_store.c mbox_manage.c mbox_copy.c -# imapd isn't in OpenBSD base and has no ports-framework Makefile of its -# own (no bsd.port.mk, no PREFIX), so BINDIR/MANDIR must be set explicitly: -# neither bsd.own.mk nor bsd.prog.mk defaults BINDIR (confirmed by grepping -# both -- share/mk/bsd.own.mk and share/mk/bsd.prog.mk -- neither contains -# a "BINDIR?=" line; base daemons that aren't building via bsd.port.mk set -# it themselves per-Makefile, e.g. smtpd's own Makefile: BINDIR=/usr/sbin). -# /usr/local is "where to install things in general" for locally- -# administered, non-base software per share/man/man7/ports.7 (PREFIX -# description) -- so /usr/local/sbin + /usr/local/man/man match the same -# convention ports use for daemons, without requiring the ports framework. BINDIR= /usr/local/sbin MANDIR= /usr/local/man/man -# RELINK: a shell command bsd.prog.mk uses to smoke-test a from-source -# relink of ${PROG} at "make install" time. Building this triggers bsd. -# prog.mk's documented re-link-kit mechanism (share/mk/bsd.prog.mk, -# confirmed against the live upstream file): install produces ${PROG}.tar -# (containing ${OBJS} + a generated install.sh that recompiles from those -# objects in random link order, runs this RELINK command against the -# result, then installs it) and drops it at -# /usr/share/relink/${BINDIR}/${PROG}/${PROG}.tar. Matches smtpd's own -# precedent verbatim (RELINK= "./${PROG} -V > /dev/null", confirmed -# against smtpd's live Makefile) -- "-V" prints the version and exits 0 -# with no side effects (see main.c), so this just proves the relinked -# binary starts and runs correctly before anything overwrites the -# installed copy. -# -# NOTE: unlike base's libc/libcrypto/ld.so/sshd, nothing on this system -# automatically *consumes* this tarball at boot -- /etc/rc's reorder_libs() -# has a hardcoded allowlist that doesn't include imapd (confirmed by -# reading etc/rc directly) and won't be patched to add it (fragile against -# base upgrades). rc.d/imapd's own rc_pre() hook is the consumer -# instead -- see that script for the actual relink-at-service-start logic. -# NOTE: deliberately no embedded quotes around this value -- bsd.prog.mk's -# own recipe for the RELINK feature already does "echo \"${RELINK}\" >> $@" -# when generating install.sh, so wrapping this value in its own literal -# quotes (matching smtpd's Makefile precedent verbatim) causes a doubled- -# quote collision: the generated line becomes -# echo ""./${PROG} -V > /dev/null"" >> install.sh, which the shell parses -# as two stacked redirects (> /dev/null, then >> install.sh) rather than -# literal text -- the later redirect wins for the same fd, so "> /dev/null" -# silently drops and install.sh ends up with a bare "./${PROG} -V" instead -# of the intended output-suppressed form. Confirmed by reading the actual -# generated line in a real "make install" transcript on test hardware: "-V"'s -# stdout leaked into rc.d/imapd's rc_pre() relink log instead of being -# discarded. Harmless (doesn't affect correctness -- install.sh's -# "set -o errexit" still aborts on real failures either way), but not the -# intended behavior, so leaving the quotes off here instead. RELINK= ./${PROG} -V > /dev/null # bsd.prog.mk's built-in .y suffix rule runs yacc(1) on parse.y and -# compiles the result -- no extra machinery needed here, matching every -# other base-system daemon that ships a parse.y (ripd, smtpd, httpd, -# ntpd, etc. all just list it in SRCS the same way). -y (POSIX-mode -# output naming, y.tab.c/y.tab.h) is yacc(1)'s default on OpenBSD, so no -# YFLAGS override is needed either. +# compiles the result. # imsg_init(3): imsgbuf_init/imsgbuf_read/imsgbuf_write/imsg_get/ # imsg_compose live in libutil on OpenBSD. LDADD= -lutil DPADD= ${LIBUTIL} -# event_init/event_set/event_add/event_del/event_dispatch (, -# used throughout listener.c/parent.c/auth.c/store.c's event loops) live -# in libevent on OpenBSD, which -- like libtls below -- is base-system -# but not linked in automatically. Confirmed against httpd's own -# Makefile: LDADD=-levent -ltls -lssl -lcrypto -lutil. +# event_init/event_set/event_add/event_del/event_dispatch. LDADD+= -levent DPADD+= ${LIBEVENT} -# TLS (STARTTLS on 143, implicit TLS on 993 per RFC 8314): listener.c now -# actually terminates TLS via libtls (tls_server/tls_configure/ -# tls_accept_socket/tls_handshake/tls_read/tls_write/tls_close), sourced -# against src/lib/libtls/tls.h and httpd's server_tls_init()/server_tls_ -# handshake(). -ltls pulls in libssl/libcrypto itself on OpenBSD, but -# both are listed explicitly anyway, matching how httpd's own Makefile -# links it. +# TLS (STARTTLS on 143, implicit TLS on 993 per RFC 8314). LDADD+= -ltls -lssl -lcrypto DPADD+= ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} @@ -133,16 +47,7 @@ CFLAGS+= -Wsign-compare -Wcast-qual -Wcast-align DEBUG= -g # Sample imapd.conf, installed read-only at /usr/local/share/examples/ -# imapd/imapd.conf -- matching the real OpenBSD ports convention for -# sample configs (ports(7), the @sample PLIST keyword: a port installs -# its sample under ${PREFIX}/share/examples/${PKGNAME}/ and pkg_add(1) -# copies it into place on first install). Confirmed by reading share/ -# mk/bsd.prog.mk directly rather than guessed: /etc/examples/ itself is -# NOT an option here -- that mechanism is base-only, populated by base's -# own etc/Makefile during a release build, with no hook for locally- -# installed software at all. Using the ports-convention path now, even -# before this project has an actual port, means the eventual port's -# PLIST can just reference this same path rather than needing rework. +# imapd/imapd.conf. # # afterinstall: is bsd.prog.mk's own documented extension point for # exactly this (".if !target(afterinstall)" guards its default no-op, @@ -160,17 +65,6 @@ afterinstall: # imapduser: the account-provisioning tool for imapd's own bespoke # credentials store (see contrib/imapduser's own header comment and -# imapduser.8). It isn't compiled -- it's a shell script -- so it can't -# be a second bsd.prog.mk PROG (that machinery only supports one per -# Makefile); installed here via the same afterinstall: hook as the -# sample config above instead, same ${BINDIR}/${MANDIR} destinations -# and 555/444 modes bsd.prog.mk itself would use for PROG/MAN. Matches -# real OpenBSD ports precedent for a daemon shipping its own bespoke- -# credentials-store admin tool as an installed binary rather than a -# dev-tree-only script: cyrus-sasl2's port PLIST installs saslpasswd2 -# to ${PREFIX}/sbin with its own man page for exactly the same reason -# (confirmed by reading that PLIST directly: github.com/openbsd/ports, -# security/cyrus-sasl2/pkg/PLIST -- "@bin sbin/saslpasswd2" / "@man -# man/man8/saslpasswd2.8"). +# imapduser.8). .include blob - 0af51913734a344c0d44415b736ff248d1c5fd40 blob + 3e52fad9d3e1bff8f2aa3c844b32c0036673d4cb --- src/append_cmd.c +++ src/append_cmd.c @@ -15,7 +15,7 @@ */ /* - * append_cmd.c -- APPEND: literal-driven message upload, and its + * append_cmd.c, APPEND: literal-driven message upload, and its * asynchronous IMSG_MBOX_APPENDED completion handling. */ @@ -89,7 +89,7 @@ parse_date_time(const char *s, int64_t *out) return (0); } -/* Result struct for parse_append_args() -- avoids an unwieldy number of out-parameters. */ +/* Result struct for parse_append_args(), avoids an unwieldy number of out-parameters. */ struct append_parsed { char mailbox[MBOX_NAME_MAX]; uint32_t sysflags; @@ -249,9 +249,9 @@ parse_append_args(char *args, struct append_parsed *ou out->litlen = (uint64_t)litlen; if (out->litnonsync && out->litlen > 4096) { - /* RFC 9051 SS4.3: non-sync literals capped at 4096 octets -- BAD, not cmd_append()'s NO size cap. */ + /* RFC 9051 SS4.3: non-sync literals capped at 4096 octets, BAD, not cmd_append()'s NO size cap. */ *errmsg = "non-synchronizing literal exceeds RFC " - "9051 SS4.3's 4096-octet limit -- use a " + "9051 SS4.3's 4096-octet limit, use a " "synchronizing literal instead"; return (-1); } @@ -284,15 +284,15 @@ cmd_append(struct session *s, const char *tag, char *a } if (parsed.litlen > APPEND_LITERAL_MAX) { - /* RFC 5530 LIMIT code -- matches APPEND_LITERAL_MAX's situation precisely. */ + /* RFC 5530 LIMIT code, matches APPEND_LITERAL_MAX's situation precisely. */ session_reply(s, tag, "NO", - "[LIMIT] message too large for this server (v1 size " - "limit -- see APPEND_LITERAL_MAX)"); + "[LIMIT] message too large for this server " + "limit, see APPEND_LITERAL_MAX)"); return (1); } if (s->store_iev == NULL) { - /* Same store_iev invariant as cmd_select()/cmd_fetch() -- ST_AUTH requires it already wired. */ + /* Same store_iev invariant as cmd_select()/cmd_fetch(), ST_AUTH requires it already wired. */ log_warnx("session %u: APPEND with no store channel wired", s->id); session_reply(s, tag, "NO", "[SERVERBUG] internal error"); @@ -352,7 +352,7 @@ session_finish_append(struct session *s) sizeof(req.mailbox) || strlcpy(req.keywords, s->append_keywords, sizeof(req.keywords)) >= sizeof(req.keywords)) { - log_warnx("session %u: APPEND mailbox/keywords truncated -- " + log_warnx("session %u: APPEND mailbox/keywords truncated, " "can't happen (both already bounded when first stored)", s->id); session_reply(s, s->pending_tag, "NO", "[SERVERBUG] internal error"); @@ -416,7 +416,7 @@ session_handle_mbox_appended(struct session *s, if (res->error != MBOX_OP_OK) { if (res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX) session_reply(s, s->pending_tag, "NO", - "[TRYCREATE] no such mailbox"); /* SS6.3.12: reports why, not a promise CREATE would help (v1 has none) */ + "[TRYCREATE] no such mailbox"); /* SS6.3.12: reports why, not a promise CREATE would help */ else session_reply(s, s->pending_tag, "NO", "APPEND failed"); @@ -431,7 +431,7 @@ session_handle_mbox_appended(struct session *s, appended_to_selected = (strcmp(s->append_mailbox, s->selected_mailbox) == 0); - /* RFC 9051 SS6.3.13: APPEND always adds one message -- unlike EXPUNGE/CLOSE, no res->count gate needed. */ + /* RFC 9051 SS6.3.13: APPEND always adds one message, unlike EXPUNGE/CLOSE, no res->count gate needed. */ session_notify_idle_peers(s); if (s->append_prev_state == SESSION_SELECTED && appended_to_selected) { blob - 03591a478c2cae7027e77e94fad9b6e7472667f3 blob + 374c9f5dc398eca7c5d2b03b65e9d36cc15cf558 --- src/auth.c +++ src/auth.c @@ -14,7 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -/* auth.c -- credential verification process: AUTHENTICATE PLAIN against the flat cred file. */ +/* auth.c, credential verification process: AUTHENTICATE PLAIN against the flat cred file. */ #include @@ -34,14 +34,14 @@ struct cred_entry { char username[AUTH_USERNAME_MAX]; - char passwordhash[128]; /* bcrypt "$2b$NN$..." -- generous */ + char passwordhash[128]; /* bcrypt "$2b$NN$...", generous */ uid_t uid; gid_t gid; char maildir[AUTH_MAILDIR_MAX]; }; static struct imsgev iev_listener; -static struct imsgev iev_parent; /* fd 3, alive for the process's lifetime -- task #321 */ +static struct imsgev iev_parent; /* fd 3, alive for the process's lifetime */ static char cred_file_basename[256]; static int cred_lookup(const char *, const char *username, @@ -119,20 +119,12 @@ auth_main(void) event_init(); imsgev_init(&iev_listener, peer_fd, auth_dispatch, NULL); - /* task #321: reuses fd 3's populated ibuf3 -- a fresh imsgbuf_init() - * would drop buffered bytes. Kept alive post-boot (unlike before) - * so successful logins can IMSG_AUTH_CRED parent directly; mirrors - * listener.c's own boot -> iev_parent conversion. */ imsgev_init_from_ibuf(&iev_parent, &ibuf3, auth_dispatch_parent, NULL); /* unveil() path is relative to the chroot above: "/" + basename. */ { char unveil_path[512]; - /* cred_file_basename[256] is already strlcpy(3)-truncation- - * checked above; "/" + up to 255 bytes can never approach - * this 512-byte buffer, so snprintf(3) here can't truncate -- - * the return value is explicitly discarded, not overlooked. */ (void)snprintf(unveil_path, sizeof(unveil_path), "/%s", cred_file_basename); if (unveil(unveil_path, "r") == -1) @@ -188,7 +180,7 @@ auth_dispatch(int fd, short event, void *arg) log_warnx("bad IMSG_AUTH_REQUEST"); break; } - /* imsg_get_data() guarantees size, not NUL termination -- force it */ + /* imsg_get_data() guarantees size, not NUL termination, force it */ req.username[sizeof(req.username) - 1] = '\0'; req.password[sizeof(req.password) - 1] = '\0'; memset(&res, 0, sizeof(res)); @@ -203,10 +195,6 @@ auth_dispatch(int fd, short event, void *arg) log_warn("imsg_compose IMSG_AUTH_RESULT"); imsgev_add(iev); - /* task #321: parent, not listener, is who actually - * spawns the store child -- send it uid/gid/maildir - * directly rather than trusting listener to relay - * what we just told it. */ if (res.ok) { struct imsg_auth_cred cred; @@ -215,7 +203,7 @@ auth_dispatch(int fd, short event, void *arg) cred.uid = res.uid; cred.gid = res.gid; /* cred.maildir and res.maildir are both sized - * AUTH_MAILDIR_MAX -- truncation is structurally + * AUTH_MAILDIR_MAX, truncation is structurally * impossible, so the return value is discarded * deliberately, same as imsg_store_init's own * maildir field elsewhere. */ @@ -241,7 +229,7 @@ auth_dispatch(int fd, short event, void *arg) (void)fd; } -/* task #321: parent never sends auth anything post-boot, so this exists to flush queued IMSG_AUTH_CRED writes and notice if parent's end closes */ +/* parent never sends auth anything post-boot, so this exists to flush queued IMSG_AUTH_CRED writes and notice if parent's end closes */ static void auth_dispatch_parent(int fd, short event, void *arg) { @@ -303,7 +291,7 @@ auth_verify(struct imsg_auth_request *req, struct imsg explicit_bzero(&ce, sizeof(ce)); } -/* linear scan of "username:passwordhash:uid:gid:maildir" lines -- fine for v1's small cred files */ +/* linear scan of "username:passwordhash:uid:gid:maildir" lines */ static int cred_lookup(const char *path, const char *username, struct cred_entry *out) { blob - 4a48404e352ed638dd3f5cf8012c9b7d2326dd79 blob + fa4f85eeb4c4c347226d0d001e2580ec44713e2e --- src/auth_cmd.c +++ src/auth_cmd.c @@ -15,7 +15,7 @@ */ /* - * auth_cmd.c -- CAPABILITY/NOOP/LOGOUT/ID/LOGIN/STARTTLS/ + * auth_cmd.c, CAPABILITY/NOOP/LOGOUT/ID/LOGIN/STARTTLS/ * AUTHENTICATE/ENABLE: command-any and command-nonauth handlers that * don't need an established mailbox session. */ @@ -44,11 +44,7 @@ #include "listener.h" /* - * RFC 9051 SS6.1.1 capability strings, selected by session->tls_active; - * CONDSTORE/QRESYNC (RFC 7162) advertised regardless of TLS. LOGINDISABLED - * is advertised in both strings -- cmd_login() below refuses LOGIN - * unconditionally, pre- and post-TLS, so both capability strings should - * say so rather than implying LOGIN might work once TLS is up. + * RFC 9051 SS6.1.1 capability strings, selected 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" @@ -57,7 +53,7 @@ int cmd_capability(struct session *s, const char *tag, char *args) { - (void)args; /* RFC 9051: "Arguments: none" -- extra args ignored, not rejected */ + (void)args; /* RFC 9051: "Arguments: none", extra args ignored, not rejected */ session_untagged(s, s->tls_active ? "CAPABILITY " CAPABILITY_POST_TLS : "CAPABILITY " CAPABILITY_PRE_TLS); @@ -102,15 +98,7 @@ cmd_id(struct session *s, const char *tag, char *args) /* * LOGIN is permanently disabled, matching LOGINDISABLED in both - * CAPABILITY strings above. AUTHENTICATE PLAIN (SASL) is the only - * supported credential path -- one fewer parser/credential-handling - * code path than supporting both, per this project's smaller-feature- - * set-is-smaller-attack-surface design principle. (A full LOGIN - * implementation was written and real-hardware-verified during Canary - * Mail interop testing, since Canary sends LOGIN, never AUTHENTICATE - * PLAIN; every other client tested -- Apple Mail, Airmail -- uses - * AUTHENTICATE PLAIN without issue, so LOGIN was re-disabled rather - * than kept enabled solely for one client's benefit.) + * CAPABILITY strings above. */ int cmd_login(struct session *s, const char *tag, char *args) @@ -175,17 +163,6 @@ sasl_plain_finish(struct session *s, const char *tag, } } - /* - * rawlen==0 (the allow_empty_equals "=" case just above) would end - * up here anyway -- memchr(3) can't find a NUL in a zero-length - * buffer -- but reading raw's still-uninitialized stack contents - * through memchr(3) to get there is needless even though every - * real memchr(3) treats n==0 as a no-op that never dereferences - * the pointer. Short-circuit it explicitly instead of relying on - * that (cppcheck flagged this as a genuine uninitialized-variable - * read, which is technically correct about raw's contents even - * though the call itself is harmless). - */ if (rawlen == 0) { session_reply(s, tag, "BAD", "malformed SASL PLAIN message"); explicit_bzero(raw, sizeof(raw)); @@ -210,7 +187,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 SHALL fail verification -- NO not BAD, framing is fine */ + /* RFC 4616 SS2: empty prep result SHALL fail verification, NO not BAD, framing is fine */ if (authcidlen == 0 || passwdlen == 0) { session_reply(s, tag, "NO", "[AUTHENTICATIONFAILED] authentication failed"); explicit_bzero(raw, sizeof(raw)); @@ -309,7 +286,7 @@ cmd_authenticate(struct session *s, const char *tag, c return (1); } - /* v1 only implements PLAIN, matching CAPABILITY_POST_TLS */ + /* only implements PLAIN, matching CAPABILITY_POST_TLS */ if (strcasecmp(mech, "PLAIN") != 0) { session_reply(s, tag, "NO", "authentication mechanism not available"); @@ -377,9 +354,7 @@ cmd_enable(struct session *s, const char *tag, char *a /* * buf[64] can never truncate here: the only strings ever appended * are these two fixed literals plus one separator space, 18 bytes - * total in the worst case ("QRESYNC CONDSTORE") -- but the return - * value is still explicitly discarded rather than silently ignored, - * per this project's check-every-return-value standard. + * total in the worst case ("QRESYNC CONDSTORE"). */ buf[0] = '\0'; if (newly_qresync) blob - 8c03107f9c2ffdf3e003d2b7297d2dbb48146bb3 blob + 51f65325e114d9eab73fcbd8cff51ff4da44736c --- src/envelope.c +++ src/envelope.c @@ -14,7 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -/* envelope.c -- the ENVELOPE and BODYSTRUCTURE FETCH response builders. */ +/* envelope.c, the ENVELOPE and BODYSTRUCTURE FETCH response builders. */ #include #include @@ -131,7 +131,7 @@ envbuf_append_one_address(char *buf, size_t bufsize, s } } if (gt >= toklen) - return (-1); /* unmatched '<' -- malformed, skip */ + return (-1); /* unmatched '<', malformed, skip */ { const char *disp = tok; @@ -469,7 +469,7 @@ build_envelope(const char *basename, char **buf_out, u fail: log_warnx("session %u: message %s: formatted ENVELOPE exceeds " - "ENVELOPE_MAX -- ENVELOPE skipped", session_id, basename); + "ENVELOPE_MAX, ENVELOPE skipped", session_id, basename); free(hdrbuf); return (-1); } @@ -681,5 +681,3 @@ build_bodystructure(const char *basename, char **buf_o *len_out = (uint32_t)outlen; return (0); } - -/* Parses RFC 9051 SS6.4.5.1 section-part (e.g. "3.1") into 1-based part numbers; re-validates untrusted input. -1 on bad syntax or > maxpath. */ blob - 075fcd37dae4bde752279995e76c977001f0e82a blob + c30aafc310af2638e430368765831fed1d627e96 --- src/fetch_cmd.c +++ src/fetch_cmd.c @@ -15,7 +15,7 @@ */ /* - * fetch_cmd.c -- FETCH: attribute/section-spec parsing and + * fetch_cmd.c, FETCH: attribute/section-spec parsing and * response building. */ @@ -335,7 +335,7 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int } else if (strcasecmp(tok, "RFC822.SIZE") == 0) { attrs |= MBOX_FETCH_RFC822_SIZE; } else if (strcasecmp(tok, "MODSEQ") == 0) { - attrs |= MBOX_FETCH_MODSEQ; /* RFC 7162 SS3.1.4.2 -- also CONDSTORE-enabling, cmd_fetch() checks this bit */ + attrs |= MBOX_FETCH_MODSEQ; /* RFC 7162 SS3.1.4.2, also CONDSTORE-enabling, cmd_fetch() checks this bit */ } else if (strcasecmp(tok, "BODY.PEEK[HEADER]") == 0) { attrs |= MBOX_FETCH_BODY_HEADER; /* the one exact-match BODY[...]; plain BODY[HEADER] would need \Seen, unimplemented */ } else if (strncasecmp(tok, "BODY.PEEK[", strlen("BODY.PEEK[")) == @@ -350,7 +350,7 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int close = strchr(bracket_start, ']'); if (close == NULL) { - degraded = 1; /* not well-bracketed -- same lenient skip as other unsupported forms */ + degraded = 1; /* not well-bracketed, same lenient skip as other unsupported forms */ continue; } if ((size_t)(close - bracket_start) >= sizeof(inner)) { @@ -397,7 +397,7 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int return (-1); } if (attrs & MBOX_FETCH_HEADER_FIELDS) - continue; /* already captured one -- ignore any further duplicates */ + continue; /* already captured one, ignore any further duplicates */ if (toklen - strlen("BODY.PEEK[") - 1 >= sizeof(inner)) { @@ -443,7 +443,7 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int if (attrs == 0) { /* every requested item was unsupported, e.g. BODY[] or RFC822(.HEADER/.TEXT) alone */ - *errmsg = "cannot fetch that message content yet -- " + *errmsg = "cannot fetch that message content yet, " "supported: FLAGS/UID/INTERNALDATE/RFC822.SIZE/MODSEQ/" "ENVELOPE/(BODY|BODYSTRUCTURE)/BODY.PEEK[...]"; return (-2); @@ -468,7 +468,7 @@ const char *fetch_month_names[12] = { "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; -/* RFC 9051 SS9 date-time; always formats in UTC "+0000" -- ts carries no tz info and a chroot'd store child has no tzdata */ +/* RFC 9051 SS9 date-time; always formats in UTC "+0000", ts carries no tz info and a chroot'd store child has no tzdata */ void format_internaldate(int64_t ts, char *out, size_t outsize) { @@ -479,7 +479,7 @@ format_internaldate(int64_t ts, char *out, size_t outs if (strlcpy(out, "01-Jan-1970 00:00:00 +0000", outsize) >= outsize) log_warnx("format_internaldate: fallback string " - "truncated -- caller's buffer too small"); + "truncated, caller's buffer too small"); return; } @@ -777,7 +777,7 @@ parse_fetch_modifiers(char *modspec, struct imsg_mbox_ return (0); } -/* RFC 9051 SS6.4.5 fetch + RFC 4466/7162 modifier list; plain BODY[...] and BODY[] are a deliberate v1 scope cut */ +/* RFC 9051 SS6.4.5 fetch + RFC 4466/7162 modifier list; plain BODY[...] and BODY[] are a deliberate scope cut */ int cmd_fetch(struct session *s, const char *tag, char *args) { @@ -824,7 +824,7 @@ fetch_dispatch(struct session *s, const char *tag, cha if (strchr(seqtok, ',') != NULL) { session_reply(s, tag, "BAD", - "comma-separated sequence sets not supported in v1 -- " + "comma-separated sequence sets not supported " "issue separate FETCH commands"); return (1); } @@ -853,7 +853,7 @@ fetch_dispatch(struct session *s, const char *tag, cha log_debug("session %u: %s: one or more unsupported message " "data items silently dropped (plain BODY[...]/BODY[]" "/BODY.PEEK[] with MESSAGE/RFC822|GLOBAL or MULTIPART" - " nested numbering/RFC822[.HEADER/.TEXT]) -- answering " + " nested numbering/RFC822[.HEADER/.TEXT]), answering " "with whatever was recognized", s->id, cmdname); memset(&req, 0, sizeof(req)); @@ -873,7 +873,7 @@ fetch_dispatch(struct session *s, const char *tag, cha req.partial_start = partial_start; req.partial_count = partial_count; - /* verbatim client-typed label never crosses to store.c -- stashed here for session_send_fetch_response() to echo */ + /* verbatim client-typed label never crosses to store.c, stashed here for session_send_fetch_response() to echo */ if (attrs & MBOX_FETCH_BODY_HEADER) { if (strlcpy(s->pending_header_label, "HEADER", sizeof(s->pending_header_label)) >= @@ -943,7 +943,7 @@ fetch_dispatch(struct session *s, const char *tag, cha req.attrs |= MBOX_FETCH_UID; if (s->store_iev == NULL) { - /* same invariant check as cmd_select() -- ST_SELECTED requires store_iev already wired */ + /* same invariant check as cmd_select(), ST_SELECTED requires store_iev already wired */ log_warnx("session %u: %s with no store channel wired", s->id, cmdname); session_reply(s, tag, "NO", "[SERVERBUG] internal error"); blob - 2ce9b6536055d078f1333f243a394898dd6243aa blob + f0352b50e26676e1d971d79906d320fb43b27b6a --- src/imapd.8 +++ src/imapd.8 @@ -377,7 +377,7 @@ by default; these, along with the listen address, are .Pa /etc/imapd.conf (or the file named by .Fl f ) -at startup, falling back to those v1 defaults for any +at startup, falling back to defaults for any .Ic listen directive the file omits. IPv6 and dual-stack binding are available but not the default; see the @@ -419,122 +419,4 @@ This implementation is under active development. .Li UNSUBSCRIBE , and shared or multi-user mailboxes .Pq no Li ACL support -are deliberate scope decisions, not gaps awaiting implementation: -matched against real Apple Mail client behavior, both features exist -primarily to manage large numbers of shared or public mailboxes on a -multi-user server, a scenario this single-user, no-shared-mailbox -implementation does not have. -This mirrors -.Xr smtpd 8 Ns 's -own precedent of omitting -.Li VRFY Ns / Ns Li EXPN -despite RFC recommendation, on the same reasoning: every additional -command is attack surface, and neither of these two currently serves -this implementation's actual usage. -Revisit if a real, concrete need for either ever materializes. -Mailboxes form a single flat namespace per user: there is no nested -hierarchy, and -.Li CREATE -refuses a name containing the -.Ql / -hierarchy-delimiter character rather than creating intermediate -levels. -.Li CREATE -and -.Li DELETE -of -.Li INBOX -itself are refused -.Pq Li CANNOT , -as is -.Li RENAME -of -.Li INBOX -as the source -.Pq permitted by RFC 9051 but explicitly sanctioned there as a -server-side refusal ; renaming -.Em to -.Li INBOX -is unreachable, since that name always already exists. -.Li COPY -and -.Li MOVE -may target any real, existing mailbox, not only the one currently -selected; a destination naming a mailbox that does not exist is -refused with -.Li TRYCREATE , -and a syntactically invalid destination name is refused outright -.Pq Li BAD -with no attempt to look it up. -.Li DELETE -of a mailbox that does not exist, and -.Li RENAME -with a source that does not exist, are refused with -.Pq Li NONEXISTENT ; -.Li CREATE -of a mailbox that already exists, and -.Li RENAME -to a destination that already exists, are refused with -.Pq Li ALREADYEXISTS -(RFC 5530). -If a mailbox is renamed or deleted while a -.Em different -session -.Pq same user -has it selected, that other session's own notion of what it has -selected can go stale until its next -.Li SELECT , -.Li EXAMINE , -.Li CLOSE , -or -.Li UNSELECT ; -no unsolicited notification of the rename or deletion is sent to it. -.Pp -Plain, non-PEEK -.Li BODY -section forms, which implicitly set the -.Li \eSeen -flag, are not implemented for any content item. -BODYSTRUCTURE and part-addressed -.Li BODY.PEEK -fetches do not address into a MULTIPART container's own combined -content, or into MESSAGE/RFC822 or MESSAGE/GLOBAL nested part -numbering. -A mailbox selected via -.Li EXAMINE -refuses -.Li STORE , -.Li EXPUNGE , -and -.Li MOVE -with a tagged -.Li NO -.Pq Li CANNOT -response; -.Li CLOSE -on such a mailbox instead succeeds without removing anything, per -RFC 9051. -.Li APPEND -is not restricted by the currently selected mailbox's read-only state. -.Pp -.Li IDLE -pushes unsolicited -.Li EXISTS -and -.Li EXPUNGE -responses only; unsolicited -.Li FETCH -on flag change (e.g. another session's -.Li STORE ) -is not sent. -Pushes are triggered only by mutations made through -.Nm -itself -.Pq Li APPEND , Li EXPUNGE , Li "UID EXPUNGE" , a read-write Li CLOSE , and Li MOVE ; -mail delivered directly into a mailbox's maildir by an external MTA or -LDA is not detected until some other command against that session -happens to trigger a refresh. -.Pp -It has been built and run against a real OpenBSD 8.0 system, -including real Apple Mail client traffic exercising most of the -commands listed above as implemented. +are deliberately left out. blob - c74a9983b8e5823e65048a325460d25d23be1b18 blob + 990a13c9f6bb93fc272d9a2c6c0f30821042744d --- src/imapd.conf.example +++ src/imapd.conf.example @@ -1,13 +1,13 @@ # -# imapd.conf example -- see imapd(8) for the full directive list. +# imapd.conf example, see imapd(8) for the full directive list. # # This file is installed read-only at /usr/local/share/examples/imapd/ # imapd.conf (matching the OpenBSD ports convention for sample configs, -# see ports(7) and the @sample PLIST keyword) -- it is NOT read by imapd +# see ports(7) and the @sample PLIST keyword), it is NOT read by imapd # itself. To use it, copy it to /etc/imapd.conf and edit as needed. # # imapd.conf must be owned by root (or the user running imapd) and must -# NOT be group- or world-writable -- nor world-readable, which trips +# NOT be group- or world-writable, nor world-readable, which trips # people up more often, since it's easy to forget: check_file_secrecy() # (parse.y) rejects a world-readable file too, not just a writable one. # A plain "cp" or "tee" leaves the copy at the default umask (typically @@ -28,9 +28,9 @@ listen on 0.0.0.0 port 143 listen on 0.0.0.0 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 +# 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 -- see imapd(8) for why. For example, +# address. Hostnames are not accepted, see imapd(8) for why. For example, # to listen on both address families: #listen on * port 143 #listen on * tls port 993 @@ -40,7 +40,7 @@ listen on 0.0.0.0 tls port 993 #spool "/var/mail/imapd" # Credentials file: one line per user, "username:passwordhash:uid:gid: -# maildir" -- see imapduser(8) and imapd(8) FILES. Defaults to +# maildir", see imapduser(8) and imapd(8) FILES. Defaults to # /etc/imapd/credentials. #credentials "/etc/imapd/credentials" @@ -51,10 +51,10 @@ listen on 0.0.0.0 tls port 993 #tls key "/etc/ssl/private/imapd.key" # Largest message imapd will read from disk while deriving BODYSTRUCTURE -# or a MIME part-addressed BODY[] fetch -- see imapd(8). Must be +# or a MIME part-addressed BODY[] fetch, see imapd(8). Must be # between 12000 and 1073741824 (1 GiB) bytes. Defaults to 41943040 # (40 MiB, sized off Gmail's documented attachment limit plus base64 -# encoding overhead -- see the BODYSTRUCTURE_READ_DEFAULT comment in +# encoding overhead, see the BODYSTRUCTURE_READ_DEFAULT comment in # imapd.h for the full rationale). Uncomment and adjust if your mail # routinely carries larger attachments than that. attachment max 41943040 blob - 88870413145be405657cc25906d29343f8a98aea blob + 7ebe46f03770374d5b2bb7631a129c716bc566cb --- src/imapd.h +++ src/imapd.h @@ -15,34 +15,7 @@ */ /* - * Shared definitions for all four imapd(8) process roles: parent, - * listener, auth, store -- the process split, imsg message catalog, - * pledge strings, and the fork-per-session store mechanism are all - * defined here. - * - * struct/enum names below still say "openimap" in places (struct - * openimap_config, enum openimap_proc_type) even after the imapd(8) - * rename -- those are internal identifiers with no user-visible effect - * (nothing an admin or a client ever sees), touching hundreds of call - * sites across every .c file for a purely cosmetic change, so they were - * deliberately left alone. - * - * "OpenIMAP" (the project/brand name) was itself later renamed to - * "OpenIMAPD" -- a correction, not a reversal, of the daemon-rename - * reasoning above. The original assumption was that OpenSSH/OpenNTPD/ - * OpenSMTPD all "keep Open, daemon drops it," so OpenIMAP should stay - * OpenIMAP the same way. Checked directly against OpenBSD's own - * innovations page (openbsd.org/innovations.html) rather than assumed - * further: OpenNTPD, OpenSMTPD, OpenBGPD, and OpenIKED all carry the - * "D" in the *project* name itself (ships ntpd/smtpd/bgpd/iked) -- - * OpenSSH is the one exception, and only because it ships a whole - * toolkit (ssh/scp/sftp/ssh-keygen/...), not a single daemon. This - * project is shaped like the single-daemon case, so "OpenIMAP" was - * the wrong analogy. Only - * the installed daemon's own identity (PROG, man page, rc.d script, - * default file paths), this header's own filename, and its include - * guard/version macro (below) changed as part of that *earlier* - * daemon rename -- unaffected by this later project-name correction. + * Shared definitions for all four imapd(8) process roles */ #ifndef IMAPD_H @@ -56,13 +29,6 @@ #include #include -/* - * No formal release process yet (this project has never run "make install" - * before task #196's rc.d/RELINK work) -- this exists mainly so "-V" (see - * main.c) has something concrete to print, and so the RELINK smoke-test - * command has stable, greppable output if that's ever wanted. Bump by hand - * until something better (git describe, etc.) is worth wiring in. - */ #define IMAPD_VERSION "0.1.1" /* @@ -76,9 +42,7 @@ enum openimap_proc_type { }; /* - * imsg message catalog. IMSG_SETUP_PEER / IMSG_SETUP_DONE are the - * boot-time handshake, also reused for the per-session store peer-wiring - * handshake after IMSG_STORE_INIT. + * imsg message catalog. */ enum imsg_type { IMSG_NONE, @@ -89,11 +53,7 @@ enum imsg_type { /* parent -> listener, at boot */ IMSG_LISTENER_SOCKET_CLEARTEXT, /* one bound, listening fd for the - * cleartext/STARTTLS port -- sent - * once per resolved address (1 - * normally, 2 for "listen on *", - * dual-stack -- see struct imsg_ - * listener_init's n_cleartext_addrs) */ + * cleartext/STARTTLS port */ IMSG_LISTENER_SOCKET_TLS, /* same, for the implicit-TLS port */ IMSG_TLS_CERT, IMSG_TLS_KEY, @@ -106,15 +66,10 @@ enum imsg_type { IMSG_AUTH_REQUEST, IMSG_AUTH_RESULT, - /* auth -> parent, per successful login (task #321: parent's - * privilege decision is sourced directly from auth, keyed by - * session_id, instead of being relayed through the network-facing - * listener process) */ + /* auth -> parent, per successful login */ IMSG_AUTH_CRED, - /* per-session store spawn: auth's IMSG_AUTH_CRED triggers the spawn - * in parent; IMSG_STORE_FORK is now parent -> listener only, used - * solely as a failure notification when the spawn couldn't proceed */ + /* per-session store spawn */ IMSG_STORE_FORK, IMSG_STORE_INIT, IMSG_STORE_PEER, @@ -127,38 +82,15 @@ enum imsg_type { IMSG_MBOX_FETCH, IMSG_MBOX_FETCH_META, IMSG_MBOX_FETCH_HEADER, /* raw BODY.PEEK[HEADER] bytes for one - * message (store -> listener), sent - * immediately before that message's own - * IMSG_MBOX_FETCH_META -- see struct - * imsg_mbox_fetch_header's comment for - * why this ordering is a contract, not - * a convention */ + * message (store -> listener) */ IMSG_MBOX_FETCH_BODY, /* raw BODY.PEEK[] / BODY.PEEK[TEXT] bytes for - * one message (store -> listener), same - * "sent immediately before that message's - * IMSG_MBOX_FETCH_META" contract as - * IMSG_MBOX_FETCH_HEADER above -- see - * struct imsg_mbox_fetch_body's comment */ + * one message (store -> listener) */ IMSG_MBOX_FETCH_ENVELOPE, /* pre-formatted ENVELOPE parenthesized- * list text for one message (store -> - * listener), same "sent immediately - * before that message's IMSG_MBOX_ - * FETCH_META" contract as IMSG_MBOX_ - * FETCH_HEADER above, but -- unlike that - * one -- carrying already-formatted - * response text, not raw message bytes; - * see struct imsg_mbox_fetch_envelope's - * comment */ + * listener)*/ IMSG_MBOX_FETCH_BODYSTRUCTURE, /* pre-formatted BODYSTRUCTURE * parenthesized-list text for one - * message (store -> listener), same - * "sent immediately before that - * message's IMSG_MBOX_FETCH_META" - * contract and same "already-formatted - * response text, not raw bytes" shape - * as IMSG_MBOX_FETCH_ENVELOPE above; - * see struct imsg_mbox_fetch_ - * bodystructure's comment */ + * message (store -> listener) */ IMSG_MBOX_STORE, IMSG_MBOX_APPEND, IMSG_MBOX_APPENDED, @@ -179,12 +111,7 @@ enum imsg_type { IMSG_MBOX_UNSOLICITED, /* - * RFC 7162 (CONDSTORE/QRESYNC) additions -- see this header's - * imsg_mbox_select/imsg_mbox_store comments below for the wire shape - * each carries. Both are streamed store -> listener, the same - * "zero or more of these, then one terminal IMSG_MBOX_SELECTED/ - * IMSG_MBOX_RESULT" pattern IMSG_MBOX_FETCH_META/IMSG_MBOX_EXPUNGED/ - * IMSG_MBOX_SEARCH_MATCH already establish. + * RFC 7162 (CONDSTORE/QRESYNC) additions */ IMSG_MBOX_SELECT_VANISHED, /* one vanished UID during a QRESYNC * SELECT resync (store -> listener, @@ -194,13 +121,7 @@ enum imsg_type { * listener, before IMSG_MBOX_RESULT) */ /* - * RFC 9051 SS6.3.13 (IDLE) additions. No payload on the request -- - * "refresh this session's view of its already-selected mailbox" is - * fully determined by which store child the request arrives on, same - * as IMSG_STORE_SHUTDOWN needing none. The reply is the same - * "zero or more streamed items, then one terminal reply" shape as - * IMSG_MBOX_FETCH_META/IMSG_MBOX_SELECT_VANISHED above -- see struct - * imsg_mbox_idle_uid/imsg_mbox_idle_refreshed comments below. + * RFC 9051 SS6.3.13 (IDLE) additions. */ IMSG_MBOX_IDLE_REFRESH, /* listener -> store, no payload */ IMSG_MBOX_IDLE_UID, /* one currently-existing UID, in @@ -209,50 +130,14 @@ enum imsg_type { IMSG_MBOX_IDLE_REFRESHED, /* terminal reply (store -> listener) */ /* - * RFC 9051 SS6.3.4-SS6.3.6 (CREATE/DELETE/RENAME) and SS6.3.9 (LIST), - * this pass -- flat (non-nested) multi-mailbox support, mailboxes as - * sibling subdirectories of the session's own per-user maildir root. - * IMSG_MBOX_CREATE/IMSG_MBOX_DELETE/IMSG_MBOX_RENAME and - * IMSG_MBOX_LIST itself were already reserved in this enum from an - * earlier skeleton pass (store_dispatch()'s "TODO: none of these - * payload shapes are designed yet" case) -- only their payload - * structs and one new streaming-item type for LIST are added here. - * CREATE/DELETE/RENAME all reply with the existing, already-generic - * struct imsg_mbox_result (only its "ok" field is meaningful for - * these three -- count/highestmodseq stay 0), the same reuse - * CLOSE already gets by riding EXPUNGE's reply shape. LIST follows - * the "stream zero or more items, then one terminal reply" pattern - * IMSG_MBOX_IDLE_UID/IMSG_MBOX_IDLE_REFRESHED above (and IMSG_MBOX_ - * FETCH_META, IMSG_MBOX_SELECT_VANISHED, ...) already establish -- - * its terminal reply also reuses struct imsg_mbox_result, with - * "count" now meaningful (number of IMSG_MBOX_LIST_ITEM messages - * that preceded it), matching that field's existing doc comment - * ("equivalent, for a future op"). + * RFC 9051 SS6.3.4-SS6.3.6 (CREATE/DELETE/RENAME) and SS6.3.9 (LIST). */ IMSG_MBOX_LIST_ITEM /* one mailbox name (store -> listener), - * before the terminal IMSG_MBOX_RESULT - * -- INBOX itself is never included: - * listener.c already special-cases - * INBOX into every LIST response - * locally (RFC 9051 SS6.3.9: "The - * special name INBOX is included in - * the output from LIST... if INBOX is - * supported by this server for this - * user", true unconditionally in v1), - * so store.c only needs to report the - * *named* mailboxes it actually finds - * on disk */ + * before the terminal IMSG_MBOX_RESULT */ }; /* - * Standard privsep imsg-over-event(3) wrapper. This struct's fields and - * imsgev.c's functions are this project's own implementation, not copied - * from a specific quoted definition -- but the name "imsgev"/"struct - * imsgev" for an imsgbuf+event(3) wrapper matches Eric Faurot's - * usr.sbin/ldapd/imsgev.c in OpenBSD base closely enough (not a generic - * name) that his copyright is carried in imsgev.c's own header as - * attribution for that naming/conceptual lineage, even though the actual - * function signatures and dispatch design here differ from his. + * privsep imsg-over-event(3) wrapper. */ struct imsgev { struct imsgbuf ibuf; @@ -262,88 +147,33 @@ struct imsgev { short events; }; -/* - * Config, as read from imapd.conf by parent via config_load() (parse.y - * -- a real yacc-based grammar as of this pass, covering exactly these - * eight fields: "listen on [tls] port " x2, "spool ", - * "credentials ", "tls certificate ", "tls key ", - * "attachment max "). See parse.y's header comment for the - * grammar's full design and sourcing. - */ -/* - * Max number of sockets bind_listen_socket() (parent.c) ever binds for a - * single "listen on " line: 1 for a literal IPv4 or IPv6 address, or - * 2 for the "*" wildcard, which binds one IPv4-any and one IPv6-any socket - * -- OpenBSD's IPv6 sockets are always IPv6-only (ip6(4): "With OpenBSD - * IPv6 sockets are always IPv6-only, so the socket option is read-only"), - * so unlike Linux there is no single dual-mapped socket to bind instead; - * this matches how smtpd's own host_v4()/host_v6()/host_dns() (src/ - * usr.sbin/smtpd/parse.y, read directly this pass) build one struct - * listener per resolved address/family rather than one shared socket. - * "*" itself matches httpd.conf(5)'s own documented address semantics - * (man.openbsd.org/httpd.conf.5): "'*' ... listen on all IPv4 and IPv6 - * addresses ... '0.0.0.0' means to listen on all IPv4 addresses and '::' - * all IPv6 addresses." - */ #define LISTENER_MAX_ADDRS 2 struct openimap_config { char listen_addr[64]; /* "0.0.0.0" (default), "::", a literal * IPv4/IPv6 address, or "*" for both - * -- see LISTENER_MAX_ADDRS above */ + *, see LISTENER_MAX_ADDRS above */ uint16_t port_cleartext; /* 143, STARTTLS */ uint16_t port_implicit_tls; /* 993, RFC 8314 */ char spool_root[1024]; /* mail spool root, store's chroot */ - char cred_file[1024]; /* auth's credential file -- one + char cred_file[1024]; /* auth's credential file, one * line per user, format * username:passwordhash:uid:gid: * maildir */ char tls_cert_file[1024]; char tls_key_file[1024]; - uint32_t bodystructure_read_max; /* "attachment max" directive -- - * see BODYSTRUCTURE_READ_DEFAULT - * below for the default value and - * full rationale; store children get - * their own copy of this via - * struct imsg_store_init, since they - * never read imapd.conf themselves. */ + uint32_t bodystructure_read_max; /* "attachment max" directive */ }; /* - * imsg payload wire structs. The design doc's imsg catalog describes - * these only in prose ("mechanism, decoded username, decoded password" / - * "ok/fail, mailbox identifier on success", etc.) -- these fixed-size - * structs are this implementation's concrete choice, not something the - * design doc itself specifies. Fixed-size, no length-prefixed strings, - * for v1 simplicity; revisit if that turns out to be too small anywhere. + * imsg payload wire structs. */ #define AUTH_USERNAME_MAX 64 #define AUTH_PASSWORD_MAX 128 #define AUTH_MAILDIR_MAX 256 /* - * Boot-time config-delivery payloads, closing the gap flagged in an - * earlier pass: listener/auth don't read imapd.conf themselves (kept - * off their rpath/unveil surface deliberately, per each role's own - * pledge(2) string below), but nothing ever specified how they'd get - * their slice of it otherwise. Modeled directly on - * IMSG_STORE_INIT's existing precedent: parent, which alone reads the - * real config, hands over only the fields that role actually needs, not - * the whole struct openimap_config. - * - * auth's is not optional polish -- auth_main() derives its chroot - * directory from cred_file, so without this message it would chroot - * into the dirname of an empty string. listener's used to be only a - * startup log line, on the theory that the listening fds themselves are - * always fd-passed directly, never rebuilt from listen_addr/ports -- that - * stopped being true the moment dual-stack ("listen on *") support was - * added: listener now needs n_cleartext_addrs/n_tls_addrs from this - * message to know how many IMSG_LISTENER_SOCKET_CLEARTEXT/_TLS messages - * to expect (1 each normally, 2 each for "*") before its boot-time drain - * loop can know it has received all of them. This message must therefore - * arrive before listener can finish that loop, though not necessarily - * before the socket fds themselves -- see listener.c's listener_main() - * for how the loop tolerates any arrival order. + * Boot-time config-delivery payloads. */ struct imsg_listener_init { char listen_addr[64]; @@ -373,18 +203,6 @@ struct imsg_auth_result { char maildir[AUTH_MAILDIR_MAX]; }; -/* - * task #321: sent auth -> parent directly, over auth's own existing - * channel (the one IMSG_AUTH_INIT already arrives on), immediately after - * a successful login. This is the sole source parent uses to spawn a - * session's store child -- see parent_handle_store_fork() in parent.c. - * Previously this data (uid/gid/maildir, resolved by auth.c from the - * credential file) was relayed to parent by listener.c instead, meaning - * parent's privilege decision ultimately trusted a value repeated by the - * network-facing process rather than the process that actually verified - * the login. See docs/SECURITY-PATCHES.md's F1 writeup for the original - * finding and the recommended complete fix this implements. - */ struct imsg_auth_cred { uint32_t session_id; uid_t uid; @@ -392,12 +210,6 @@ struct imsg_auth_cred { char maildir[AUTH_MAILDIR_MAX]; }; -/* - * task #321: parent -> listener only now, a failure notification when - * parent couldn't spawn (or lost) a session's store child. No longer - * carries uid/gid/maildir -- those arrive on IMSG_AUTH_CRED above -- so - * a failure reply is just session_id, always zeroed for the rest. - */ #define STORE_MAILDIR_MAX AUTH_MAILDIR_MAX struct imsg_store_fork { @@ -408,87 +220,29 @@ struct imsg_store_init { uint32_t session_id; uid_t uid; gid_t gid; - char spool_root[1024]; /* store needs this to chroot() - * -- store children don't read - * imapd.conf themselves (see - * main.c's NOTE on why), so - * parent has to hand it over - * explicitly here rather than - * store already having it. */ + char spool_root[1024]; /* store needs this to chroot() */ char maildir[STORE_MAILDIR_MAX]; /* THIS session's own * mailbox subdirectory, relative - * to spool_root above -- distinct - * from spool_root itself, which - * is shared by every store child - * regardless of user. store.c - * scopes its unveil(2) to this - * path specifically (not the - * whole chroot), and every - * mailbox file it opens is - * relative to it. */ + * to spool_root above */ uint32_t bodystructure_read_max; /* copied from struct * openimap_config's field of the - * same name -- see - * BODYSTRUCTURE_READ_DEFAULT's - * comment for what this gates. - * Same "parent read the config, - * child gets only what it needs" - * pattern as spool_root/maildir - * above. */ + * same name */ }; /* * IMSG_MBOX_SELECT (listener -> store) / IMSG_MBOX_SELECTED (store -> - * listener): the first IMSG_MBOX_* pair to actually get a wire payload - * shape -- the rest of the family (FETCH/STORE/APPEND/...) is still exactly - * as undesigned as store.c's own header comment says. v1 is single-mailbox - * (INBOX only, and the still-unresolved hierarchy-separator question is - * flagged in listener.c's cmd_namespace()) -- readonly distinguishes - * EXAMINE (RFC 9051 SS6.3.3) - * from SELECT, wired up in listener.c's select_or_examine(). store.c still - * does nothing different for readonly than for a normal SELECT: v1's single - * mailbox returns the identical EXISTS/UIDVALIDITY/UIDNEXT/highestmodseq - * either way, so read-only enforcement (refusing STORE/EXPUNGE/MOVE, - * short-circuiting CLOSE) lives entirely in listener.c via s->mbox_readonly - * -- a pure session-local invariant that doesn't need store.c's - * involvement to check. + * listener). */ #define MBOX_NAME_MAX 256 /* * Shared specific-error type for the store-process operation-result imsg * structs (imsg_mbox_selected, imsg_mbox_status_result, imsg_mbox_result, - * imsg_mbox_appended) -- replaces the old "int ok" plus a bolted-on "int - * no_such_mailbox" that some of these structs used to maintain - * independently for the exact same concept. MBOX_ERR_UNSET is deliberately - * the zero value, not MBOX_OP_OK: every producer memset()s its result - * struct to 0 before filling it in, so a codepath that forgets to set this - * field ends up reporting failure, not silent success. + * imsg_mbox_appended). * * MBOX_OP_ERR_NO_SUCH_MAILBOX and MBOX_OP_ERR_ALREADY_EXISTS are * client-visible via RFC 5530 SS3's NONEXISTENT and ALREADYEXISTS codes - * respectively (confirmed directly against the RFC text, not assumed): - * NONEXISTENT's own worked example is a RENAME failing because the - * source doesn't exist, and ALREADYEXISTS's is a CREATE/RENAME target - * that already exists -- both apply directly to CREATE/DELETE/RENAME's - * not-found and already-exists failure modes (added 2026-08-27, task - * #317), correcting an earlier, unverified claim in this comment that no - * RFC 5530 code fit those cases. NO_SUCH_MAILBOX is also COPY/MOVE/ - * APPEND's TRYCREATE trigger (RFC 9051 SS6.4.7/SS6.4.8/SS6.3.12). Every - * other failure cause (mkdir/stat/flock/index races, truncated internal - * buffers, etc.) still collapses to a plain " failed" NO via - * MBOX_OP_ERR_GENERIC -- those genuinely don't fit any RFC 5530 code. - * - * Defined here, ahead of imsg_mbox_select(ed)/imsg_mbox_status_result - * below, rather than down by imsg_mbox_result where it was first added -- - * a straight compile (caught by the 2026-08-27 -Wcast-qual/-Wcast-align - * addition finally forcing a real, non-sandboxed syntax check of this - * header) showed the enum being referenced by struct fields hundreds of - * lines before its old definition, which every C compiler rejects as an - * incomplete type. This project's enum-error-type pass (task #307-309) - * had never actually been compile-checked end to end before now -- - * task #310 ("compile-check + real-hardware verify") was still open - * when this was found. + * respectively. */ enum mbox_op_error { MBOX_ERR_UNSET = 0, @@ -498,111 +252,51 @@ enum mbox_op_error { MBOX_OP_ERR_ALREADY_EXISTS, }; -/* - * QRESYNC select-param additions (RFC 7162 SS3.2.5): `"QRESYNC" SP "(" - * uidvalidity SP mod-sequence-value [SP known-uids [SP seq-match-data]] - * ")"`. v1 scope, sourced against this project's own established pattern - * of accepting exactly one sequence-set range (never a comma-separated - * list) everywhere a sequence-set appears (FETCH/STORE/SEARCH's UID - * ranges) -- known-uids gets the same restriction here. seq-match-data is - * parsed and syntax-validated by listener.c but never sent down this wire - * at all: this implementation's chosen QRESYNC state model (see the - * imsg_mbox_select_vanished comment below) never uses it to narrow - * anything, so there's nothing for store.c to do with it -- explicitly - * sanctioned by RFC 7162 SS5.2 ("A client providing message sequence - * match data can reduce the scope as above. In the case where there have - * been no expunges, the server can ignore this data"). - */ +/* QRESYNC select-param (RFC 7162 SS3.2.5). */ struct imsg_mbox_select { char mailbox[MBOX_NAME_MAX]; int readonly; /* 1 = EXAMINE, 0 = SELECT */ - int qresync; /* 1 if a QRESYNC select-param was - * given and passed listener.c's - * "ENABLE QRESYNC already issued" - * gate (RFC 7162 SS3.2.5) */ + int qresync; /* 1 if QRESYNC was requested and + * enabled (RFC 7162 SS3.2.5) */ uint32_t qresync_uidvalidity; /* client's last-known - * UIDVALIDITY -- store.c ignores the - * rest of the qresync_* fields below - * if this doesn't match the mailbox's - * actual current UIDVALIDITY (SS3.2.5: - * "the server MUST ignore the - * remaining parameters and behave as - * if no dynamic message data - * changed") */ + * UIDVALIDITY; a mismatch means the + * rest of qresync_* is ignored */ uint64_t qresync_modseq; /* client's last-known mailbox * mod-sequence */ - int qresync_has_uids; /* 0 => client omitted known-uids; - * SS3.2.5.1: "the server acts as if - * the client has specified - * '1:'" -- store.c resolves - * that default itself, since it's the - * one that knows UIDNEXT */ - uint32_t qresync_uid_lo; /* known-uids range, v1's usual - * single-range restriction (no comma - * lists) -- ignored if + int qresync_has_uids; /* 0 = client omitted known-uids; + * store.c then defaults to the full + * UID range (SS3.2.5.1) */ + uint32_t qresync_uid_lo; /* known-uids range, single range only + * (no comma lists); ignored if * !qresync_has_uids */ uint32_t qresync_uid_hi; }; struct imsg_mbox_selected { - enum mbox_op_error error; /* MBOX_OP_ERR_GENERIC covers both "no - * such mailbox" and an index I/O failure -- - * session_handle_mbox_selected() replies - * "[NONEXISTENT] no such mailbox" (RFC 5530) - * unconditionally on any failure here, so - * there's no NO_SUCH_MAILBOX/other distinction - * for this struct to carry, unlike COPY/MOVE/ - * APPEND's TRYCREATE case */ + enum mbox_op_error error; /* MBOX_OP_ERR_GENERIC covers "no + * such mailbox" and I/O failure alike + *, always reported as + * [NONEXISTENT] */ uint32_t exists; /* RFC 9051 SS7.4.1 EXISTS */ uint32_t uidvalidity; /* RFC 9051 SS2.3.1.1 */ uint32_t uidnext; /* RFC 9051 SS2.3.1.1 */ - /* - * RFC 7162 SS3.1.2.1: highest mod-sequence of all messages in the - * mailbox. Always populated (v1's index format now tracks a - * per-mailbox mod-sequence counter unconditionally -- see store.c's - * struct mbox_index comment), whether or not this particular - * session has issued a CONDSTORE-enabling command yet -- listener.c - * is the one that decides whether to actually surface it to the - * client via the HIGHESTMODSEQ OK response code, and also caches it - * in s->mbox_highestmodseq for the "CONDSTORE enabled later, mailbox - * already selected" unsolicited-HIGHESTMODSEQ case (RFC 7162 SS3.1: - * "A first CONDSTORE enabling command executed in the session with a - * mailbox selected MUST cause the server to return HIGHESTMODSEQ"). - * Since v1's only mailbox always supports persistent mod-sequence - * storage, the NOMODSEQ response code (SS3.1.2.2) is simply - * unreachable in this implementation -- not emitted anywhere. - */ + /* RFC 7162 SS3.1.2.1: mailbox's highest mod-sequence, always + * populated; listener.c decides whether to surface it via the + * HIGHESTMODSEQ response code. */ uint64_t highestmodseq; - /* - * Before this struct, store.c streams (in this order, per RFC 7162 - * SS3.2.6's "VANISHED (EARLIER) responses MUST be returned before - * any FETCH responses" ordering rule, which this implementation - * also applies to the QRESYNC-SELECT resync case by the same - * reasoning): zero or more IMSG_MBOX_SELECT_VANISHED (uid_lo/ - * uid_hi range), then zero or more IMSG_MBOX_FETCH_META (with - * .modseq set, for messages - * in the requested known-uids range whose current mod-sequence is - * greater than qresync_modseq) -- only when req->qresync was set - * and the UIDVALIDITY check passed. See imsg_mbox_select_vanished's - * comment for why the vanished set ignores qresync_modseq entirely - * (this implementation's chosen minimal QRESYNC state model). - */ + /* store.c streams, before this struct: zero or more + * IMSG_MBOX_SELECT_VANISHED, then zero or more IMSG_MBOX_FETCH_META + * (modseq set), per RFC 7162 SS3.2.6's VANISHED-before-FETCH + * ordering, only when qresync was requested and UIDVALIDITY + * matched. */ }; -/* - * RFC 9051 SS6.3.11 status-att-val values, plus RFC 7162 SS3.1.7's - * HIGHESTMODSEQ addition (`status-att =/ "HIGHESTMODSEQ"`). Request-parsing - * order only -- listener.c's response formatting uses its own fixed - * canonical order (MESSAGES, UIDNEXT, UIDVALIDITY, UNSEEN, DELETED, SIZE, - * HIGHESTMODSEQ), directly sourced from the RFC's own worked example - * (SS6.3.11: "C: A042 STATUS blurdybloop (UIDNEXT MESSAGES)" answered - * "S: * STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)" -- the server - * reordered the client's own request order), not from this bitmask's bit - * order. - */ +/* RFC 9051 SS6.3.11 status-att-val bits, plus RFC 7162 SS3.1.7's + * HIGHESTMODSEQ. Request-parsing order only, listener.c's response uses + * its own fixed order, not this bitmask's. */ #define STATUS_ATT_MESSAGES (1U << 0) #define STATUS_ATT_UIDNEXT (1U << 1) #define STATUS_ATT_UIDVALIDITY (1U << 2) @@ -610,89 +304,42 @@ struct imsg_mbox_selected { #define STATUS_ATT_DELETED (1U << 4) #define STATUS_ATT_SIZE (1U << 5) #define STATUS_ATT_HIGHESTMODSEQ (1U << 6) -#define STATUS_ATT_RECENT (1U << 7) /* IMAP4rev2 (RFC 9051 - * SS2.3.2) formally - * dropped \Recent and - * RECENT from STATUS's - * status-att grammar, - * but real clients - * (Canary Mail seen - * requesting it - * directly against - * this server) still - * ask for it out of - * IMAP4rev1 habit -- - * always answered "0" - * (no \Recent tracking - * exists in this - * server) rather than - * failing the whole - * STATUS command with - * BAD over one legacy - * attribute name. */ +#define STATUS_ATT_RECENT (1U << 7) /* IMAP4rev2 dropped + * \Recent/RECENT, but + * some clients still + * ask, always + * answered "0" rather + * than BAD */ /* * IMSG_MBOX_STATUS (listener -> store) / IMSG_MBOX_STATUS_RESULT (store -> - * listener): RFC 9051 SS6.3.11 STATUS command. Single-request/single- - * combined-reply pair, same shape as IMSG_MBOX_SELECT/IMSG_MBOX_SELECTED -- - * STATUS's response is one aggregated line, not per-message data, so - * (unlike FETCH/STORE/SEARCH/EXPUNGE) there's no streamed-then-terminal - * shape here. + * listener): RFC 9051 SS6.3.11 STATUS, one combined reply, no per-message + * streaming. * - * mailbox field added for RFC 9051 SS6.3.4-SS6.3.6/SS6.3.9 (flat multi- - * mailbox support): this comment previously argued a mailbox-name field - * was unnecessary, "v1 has - * exactly one mailbox (INBOX) and no CREATE" -- no longer true. SS6.3.11 - * itself requires STATUS to target *any* named mailbox independent of - * whatever this session currently has selected ("asks the server to - * return... status of a mailbox... without... opening a mailbox"), so - * store.c's handle_mbox_status() can't just answer for whatever cwd - * happens to be (that would incorrectly reflect the *selected* mailbox, - * conflating two independent concepts). It resolves this field the same - * way handle_mbox_append() resolves its own independent destination: - * temporarily visiting the target via select_mailbox_dir() and restoring - * whatever was selected before, rather than leaving the session's actual - * selection state changed by a STATUS call. + * mailbox targets any named mailbox independent of what's selected + * (SS6.3.11); store.c visits it via select_mailbox_dir() and restores the + * prior selection afterward. */ struct imsg_mbox_status { char mailbox[MBOX_NAME_MAX]; - uint32_t attrs; /* STATUS_ATT_* bitmask -- MESSAGES/UIDNEXT/ - * UIDVALIDITY/HIGHESTMODSEQ are always computed - * by store.c regardless of this mask (all four - * are free reads from the index header, same - * "always compute, listener decides whether to - * print" precedent as imsg_mbox_selected's own - * fields); UNSEEN/DELETED/SIZE are the - * deliberate exception -- store.c only runs the - * per-message locate_message_file() scan their - * computation requires when at least one of - * the three is set here, per RFC 9051 - * SS6.3.11's own warning: "the STATUS command - * SIZE...can take a significant amount of - * time...clients should use STATUS SIZE - * cautiously". Once that scan does run, all - * three are computed together regardless of - * which subset was actually requested -- - * locate_message_file() already returns both - * the flag suffix and the size in one call, so - * there's no marginal cost to computing all - * three vs. one. */ + uint32_t attrs; /* STATUS_ATT_* bitmask. MESSAGES/UIDNEXT/ + * UIDVALIDITY/HIGHESTMODSEQ are always + * computed (free reads); UNSEEN/DELETED/SIZE + * only trigger a per-message scan if + * requested (RFC 9051 SS6.3.11 warns SIZE can + * be slow) */ }; struct imsg_mbox_status_result { - enum mbox_op_error error; /* MBOX_OP_ERR_GENERIC -- bad mailbox - * name or the open/flock/index_load sequence - * itself failed, same causes handle_mbox_ - * select() can hit; session_handle_mbox_ - * status_result() replies "[NONEXISTENT] no - * such mailbox" unconditionally on any - * failure, so no further distinction needed */ + enum mbox_op_error error; /* MBOX_OP_ERR_GENERIC only -- + * always reported as + * [NONEXISTENT] */ uint32_t messages; /* STATUS_ATT_MESSAGES */ uint32_t uidnext; /* STATUS_ATT_UIDNEXT */ uint32_t uidvalidity; /* STATUS_ATT_UIDVALIDITY */ uint64_t highestmodseq; /* STATUS_ATT_HIGHESTMODSEQ, RFC 7162 * SS3.1.7 */ - uint32_t unseen; /* STATUS_ATT_UNSEEN -- 0 if not + uint32_t unseen; /* STATUS_ATT_UNSEEN, 0 if not * requested, see imsg_mbox_status.attrs * comment */ uint32_t deleted; /* STATUS_ATT_DELETED, same as above */ @@ -701,42 +348,16 @@ struct imsg_mbox_status_result { /* * IMSG_MBOX_SELECT_VANISHED (store -> listener, zero or more, before the - * terminal IMSG_MBOX_SELECTED or IMSG_MBOX_RESULT): one *range* - * [uid_lo, uid_hi] (inclusive) of UIDs no longer present in the mailbox, - * drawn from the range being resolved. Despite the name (kept from when - * this only existed for QRESYNC SELECT resync), also reused as of this - * pass for RFC 7162 SS3.2.6's VANISHED UID FETCH modifier -- same "report - * gaps in a UID range" computation, same wire shape, just a different - * range source (a UID FETCH's own seq_lo/seq_hi instead of QRESYNC's - * known-uids) and a different terminal message (IMSG_MBOX_RESULT, since - * a UID FETCH isn't a SELECT). listener.c tells the two apart by s->state - * (SESSION_SELECTING vs SESSION_FETCHING) when it arrives. + * terminal IMSG_MBOX_SELECTED or IMSG_MBOX_RESULT): one range [uid_lo, + * uid_hi] of UIDs no longer present. Used both for QRESYNC SELECT resync + * and RFC 7162 SS3.2.6's VANISHED UID FETCH modifier; listener.c tells + * them apart by s->state. * - * A range, not a single UID: store.c computes these by walking its - * *present*-message list once (bounded by mailbox size, this file's usual - * "personal use, modest mailbox size" scale) and reporting the gaps - * between consecutive present UIDs -- never by iterating the requested - * UID range one number at a time, which a client could set arbitrarily - * large (e.g. "known-uids 1:4000000000" against a five-message mailbox) - * independent of real mailbox size. Since UIDs are assigned strictly - * sequentially and never reused (RFC 9051 SS2.3.1.1), every UID gap in - * the present-message list genuinely was assigned-then-expunged at some - * point, so this is exactly the vanished set, computed in O(mailbox - * size) rather than O(requested range size). - * - * Deliberately reports every such range regardless of qresync_modseq -- - * this implementation adopts RFC 7162 SS5.1's explicitly-sanctioned - * minimal-state QRESYNC model ("a server implementation that doesn't - * remember mod-sequences associated with expunged messages can be - * considered compliant... Such implementations return all expunged - * messages specified in the UID set... every time, without paying - * attention to the specified CHANGEDSINCE mod-sequence"), rather than - * persisting a queue of expunge history (SS5.3's - * "Additional State Required" option) -- v1's index format has no place - * to put that history, and the RFC treats the simpler behavior as fully - * compliant, just less bandwidth-optimal than a server that remembers - * more. listener.c formats these ranges directly into the VANISHED - * (EARLIER) response's known-uids list. + * Computed in O(mailbox size) by walking the present-message list once + * and reporting gaps, not by iterating the (client-controlled) requested + * range. Ignores qresync_modseq entirely. This implementation uses RFC + * 7162 SS5.1's minimal-state model rather than persisting expunge + * history (SS5.3). */ struct imsg_mbox_select_vanished { uint32_t uid_lo; @@ -745,468 +366,156 @@ struct imsg_mbox_select_vanished { /* * IMSG_MBOX_FETCH (listener -> store) / IMSG_MBOX_FETCH_META (store -> - * listener, one per matching message, sent in ascending sequence-number - * order) / IMSG_MBOX_RESULT (store -> listener, exactly once, after the - * last IMSG_MBOX_FETCH_META -- "no more responses coming for this FETCH, - * safe to send the tagged OK/NO"). - * - * v1 FETCH scope: message METADATA (FLAGS, UID, INTERNALDATE, RFC822.SIZE), - * plus, across five successive real-client-testing passes, six content - * items -- BODY.PEEK[HEADER] (see MBOX_FETCH_BODY_HEADER / IMSG_ - * MBOX_FETCH_HEADER below); BODY.PEEK[] and BODY.PEEK[TEXT] (see MBOX_ - * FETCH_BODY_WHOLE/MBOX_FETCH_BODY_TEXT / IMSG_MBOX_FETCH_BODY below); - * BODY.PEEK[HEADER.FIELDS (...)]/BODY.PEEK[HEADER.FIELDS.NOT (...)] (see - * MBOX_FETCH_HEADER_FIELDS below, which reuses IMSG_MBOX_FETCH_HEADER - * wholesale rather than adding a fifth imsg type) -- all four raw-byte - * extraction/filtering with no MIME awareness; ENVELOPE (see MBOX_ - * FETCH_ENVELOPE / IMSG_MBOX_FETCH_ENVELOPE below), the first content item - * that's a *parsed*, structured response rather than raw or filtered - * message bytes; and BODYSTRUCTURE/bare BODY (see MBOX_FETCH_BODYSTRUCTURE - * / IMSG_MBOX_FETCH_BODYSTRUCTURE below), the first content item that - * requires real MIME parsing -- deliberately scoped out of the same pass - * that added ENVELOPE (user choice, via AskUserQuestion, to land ENVELOPE - * first and take up BODYSTRUCTURE as its own, later pass), then - * implemented as full recursive multipart parsing bounded by depth/part- - * count caps (a second AskUserQuestion, favoring correctness for the very - * common nested multipart/mixed(multipart/alternative(...), attachment) - * shape over a simpler single-level or single-part-only implementation), - * with RFC 9051's optional extension data (body MD5/disposition/language/ - * location) omitted entirely -- see MBOX_FETCH_BODYSTRUCTURE's own comment - * for the full scoping story. A seventh content item, BODY[]/ - * BODY.PEEK[] (see MBOX_FETCH_BODY_PART below), followed as - * real-hardware testing of BODYSTRUCTURE surfaced the natural next gap: a - * client that knows (via BODYSTRUCTURE) a message has an attachment still - * had no way to actually retrieve that attachment's bytes -- BODYSTRUCTURE - * only ever describes the part tree, never returns part content. Bundled - * into the same pass: <> byte-range support (SS6.4.5's - * "" suffix), applying uniformly to whole/TEXT/section-part - * BODY[...] fetches -- added after real Apple Mail traffic was observed - * issuing "BODY.PEEK[TEXT]<0.16384>", which this server silently failed to - * handle at all (no "<...>" parsing existed anywhere in listener.c before - * this pass). Everything else content-related -- BODY[]/BODY[TEXT]/ - * BODY[HEADER.FIELDS...]/BODY[] without .PEEK (the \Seen- - * setting side effect, still not implemented for any content item, this - * one included -- same scoping as every .PEEK-only item above) and part - * addressing into a MULTIPART container or a MESSAGE/RFC822|GLOBAL part's - * own nested numbering (matching BODYSTRUCTURE's own established message/ - * rfc822 scope cut) -- remains deliberately out of this pass. See - * listener.c's cmd_fetch() comment for the full scoping reasoning. - * Also v1-scoped: exactly one - * sequence-set range per request (a single number, "a:b", or "*" at either - * end) -- listener.c rejects a comma-separated sequence-set before ever - * sending this message, rather than silently fetching only the first - * sub-range. + * listener, one per matching message, ascending sequence order) / + * IMSG_MBOX_RESULT (store -> listener, once, after the last META). */ -#define MBOX_FLAGS_MAX 256 /* generous -- the five standard flags plus a - * handful of keywords comfortably fits; - * truncated (not rejected) if a message - * somehow has more, same truncate-rather- - * than-overflow style as listener.c's - * session_reply() */ +#define MBOX_FLAGS_MAX 256 /* generous; truncated (not rejected) if + * exceeded */ #define MBOX_FETCH_FLAGS (1U << 0) #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 MODSEQ - * fetch-att -- set whenever the client - * named MODSEQ explicitly, used - * CHANGEDSINCE (which "implicitly adds - * the MODSEQ FETCH message data item", - * SS3.1.4.1), or (this implementation's - * simplifying choice, see cmd_fetch()'s - * comment) the session already has - * CONDSTORE enabled at all */ -#define MBOX_FETCH_BODY_HEADER (1U << 5) /* BODY.PEEK[HEADER] only -- - * RFC 9051 SS6.4.5: "the [RFC5322] - * header of the message" for the - * HEADER section-msgtext specifier, i.e. - * the raw, unparsed header block, not a - * structured ENVELOPE. Deliberately not - * set for plain BODY[HEADER] (without - * .PEEK) -- that variant "implicitly - * sets the \Seen flag" per the same - * section, and this pass doesn't - * implement that side effect (would need - * the same flag-rename + modseq-bump - * machinery STORE already has, plus - * reflecting the change back in this - * FETCH's own response -- scoped - * out). listener.c's - * parse_fetch_atts() only recognizes the - * exact token "BODY.PEEK[HEADER]"; plain - * BODY[HEADER] still degrades like every - * other unsupported BODY[...] variant. */ -#define MBOX_FETCH_BODY_WHOLE (1U << 6) /* BODY.PEEK[] only -- RFC 9051 - * SS6.4.5: "If BODY[] is specified - * (the section specification is - * omitted), the FETCH is requesting the - * [RFC5322] expression of the entire - * message." Raw bytes, header and body - * together, no MIME parsing -- same - * .PEEK-only, exact-token-match scoping - * as MBOX_FETCH_BODY_HEADER above, for - * the same \Seen-side-effect reason. */ -#define MBOX_FETCH_BODY_TEXT (1U << 7) /* BODY.PEEK[TEXT] only -- SS6.4.5.1: - * "The TEXT part specifier refers to - * the text body of the message, - * omitting the [RFC5322] header." - * store.c's read_message_body() finds - * the same header/body blank-line - * separator read_message_header() - * already scans for, just returns - * everything after it instead of - * everything through it. If a client - * requests both MBOX_FETCH_BODY_WHOLE - * and MBOX_FETCH_BODY_TEXT in the same - * FETCH (legal per SS6.4.5, unseen from - * any real client so far), store.c - * answers WHOLE and silently drops - * TEXT -- one IMSG_MBOX_FETCH_BODY per - * message keeps the wire protocol - * symmetric with IMSG_MBOX_FETCH_HEADER - * rather than needing an array; see - * struct imsg_mbox_fetch_body's is_text - * field and handle_mbox_fetch()'s - * comment. */ +#define MBOX_FETCH_MODSEQ (1U << 4) /* RFC 7162 SS3.1.4.2 -- + * set if MODSEQ was named, + * CHANGEDSINCE was used, or + * CONDSTORE is already enabled */ +#define MBOX_FETCH_BODY_HEADER (1U << 5) /* BODY.PEEK[HEADER] only + * (SS6.4.5): raw header block, not + * ENVELOPE. Plain BODY[HEADER] is + * unimplemented (\Seen side effect). */ +#define MBOX_FETCH_BODY_WHOLE (1U << 6) /* BODY.PEEK[] only + * (SS6.4.5): raw header+body, no MIME + * parsing. */ +#define MBOX_FETCH_BODY_TEXT (1U << 7) /* BODY.PEEK[TEXT] only + * (SS6.4.5.1): body after the + * header/body blank line. If both + * WHOLE and TEXT are requested, + * store.c answers WHOLE only. */ #define MBOX_FETCH_HEADER_FIELDS (1U << 8) /* BODY.PEEK[HEADER.FIELDS - * (name ...)] or BODY.PEEK[HEADER. - * FIELDS.NOT (name ...)] -- SS6.4.5.1. - * Reuses IMSG_MBOX_FETCH_HEADER/struct - * imsg_mbox_fetch_header wholesale (see - * that struct's comment): from store.c's - * and listener.c's wire-protocol point of - * view this is just "header-region bytes, - * possibly filtered", the same shape as - * plain BODY.PEEK[HEADER], just produced - * by store.c's read_message_header_ - * fields() instead of read_message_ - * header(). req->header_fields_not and - * req->header_fields (below) carry the - * NOT flag and the space-joined field- - * name list; the exact client-typed - * label text ("HEADER.FIELDS (DATE - * FROM)", etc.) never crosses the imsg - * boundary at all -- listener.c already - * has it from parsing the client's own - * command line, and echoes it back - * verbatim in the FETCH response rather - * than reconstructing it (see listener.c's - * parse_header_fields_att() and struct - * session's pending_header_label - * comment). If a client requests both - * plain BODY.PEEK[HEADER] and a HEADER. - * FIELDS variant in the same FETCH (legal - * per SS6.4.5, unseen from any real - * client so far), listener.c's parse_ - * fetch_atts() has HEADER win and drops - * HEADER_FIELDS -- same "more general - * variant wins" precedent as MBOX_FETCH_ - * BODY_WHOLE vs. MBOX_FETCH_BODY_TEXT. */ -#define MBOX_FETCH_ENVELOPE (1U << 9) /* RFC 9051 SS7.5.2 ENVELOPE -- - * "computed by the server by - * parsing the [RFC5322] header - * into the component parts, - * defaulting various fields as - * necessary." Unlike every MBOX_ - * FETCH_BODY_* item above, this is - * a *parsed*, structured response - * (date/subject/address lists), - * built entirely by store.c's - * build_envelope() -- see struct - * imsg_mbox_fetch_envelope below - * for why the wire payload is - * already-formatted response text - * rather than raw bytes. No .PEEK - * variant exists for ENVELOPE (SS6.4.5's - * fetch-att grammar has no "ENVELOPE. - * PEEK" production) and it has no - * \Seen-setting side effect to avoid - * in the first place, unlike the BODY[...] - * family -- so, unlike MBOX_FETCH_BODY_*, - * this bit is set directly from the - * bare "ENVELOPE" token. */ + * [.NOT] (names)] (SS6.4.5.1); reuses + * IMSG_MBOX_FETCH_HEADER. req-> + * header_fields_not/header_fields + * carry the NOT flag and field list; + * listener.c echoes the client's own + * label text back verbatim. Plain + * BODY.PEEK[HEADER] wins if both are + * requested. */ +#define MBOX_FETCH_ENVELOPE (1U << 9) /* RFC 9051 SS7.5.2 -- + * parsed response via store.c's + * build_envelope(). No .PEEK + * variant, no \Seen side + * effect. */ #define MBOX_FETCH_BODYSTRUCTURE (1U << 10) /* RFC 9051 SS7.5.2 - * BODYSTRUCTURE, and its non- - * extensible sibling "BODY" - * (SS9's fetch-att: `"BODY" - * ["STRUCTURE"]` -- bare "BODY", - * no brackets, is a synonym for - * BODYSTRUCTURE-without- - * extension-data, distinct from - * "BODY[section]", which is - * content). This implementation - * never emits extension data - * (body MD5/disposition/ - * language/location) even for - * BODYSTRUCTURE -- RFC 9051 SS7.5.2 - * says extension data "can be - * returned" with BODYSTRUCTURE, - * not that it MUST be, so BODY - * and BODYSTRUCTURE produce - * byte-identical output in this - * server, both setting this one - * bit. Like ENVELOPE, this is a - * *parsed* response -- here, - * recursive MIME structure - * parsing via store.c's build_ - * bodystructure()/build_body_ - * structure() (see struct imsg_ - * mbox_fetch_bodystructure's - * comment) -- not raw or - * filtered bytes, and has no - * .PEEK variant or \Seen side - * effect, so (like MBOX_FETCH_ - * ENVELOPE, unlike MBOX_FETCH_ - * BODY_*) this bit is set - * directly from the bare token. */ + * BODYSTRUCTURE and its synonym + * bare "BODY", extension data + * (MD5/disposition/language/ + * location) is never emitted, + * so both produce identical + * output. Recursive MIME + * parsing via store.c's + * build_bodystructure(). */ #define MBOX_FETCH_BODY_PART (1U << 11) /* BODY.PEEK[] - * only -- same .PEEK-only - * scoping as every other - * MBOX_FETCH_BODY_* bit (plain - * BODY[], which - * would implicitly set \Seen, - * is not implemented, same as - * plain BODY[]/BODY[TEXT]/ - * BODY[HEADER...] already - * aren't). SS6.4.5.1's numeric-only - * section-part grammar - * (`section-part = nz-number - * *("." nz-number)`, e.g. "2" or - * "3.1"), addressing one specific - * leaf MIME part's raw (still - * transfer-encoded -- SS6.4.5's - * BODY[] never decodes Content- - * Transfer-Encoding, that's - * BINARY[]'s job, out of scope - * here same as BODYSTRUCTURE's own - * extension-data cut) body bytes. - * Distinct bit from MBOX_FETCH_ - * BODY_WHOLE/_TEXT since it needs - * an extra parameter (req-> - * section_part below) those don't. - * Deliberately v1-scoped to leaf - * parts only: a section-part - * naming a MULTIPART container - * itself, or reaching into a - * MESSAGE/RFC822 or MESSAGE/GLOBAL - * part's own nested numbering - * (SS6.4.5.1: "also has nested - * part numbers, referring to - * parts of the MESSAGE part's - * body") is "not found" for this - * item -- matching BODYSTRUCTURE's - * own established message/rfc822 - * scope cut (store.c's build_ - * body_structure() already refuses - * to describe such a message's - * structure at all, so this - * implementation was never going - * to be able to name a part inside - * one). The non-numeric part - * specifiers (HEADER, HEADER. - * FIELDS[.NOT], MIME, TEXT) - * standing alone are already - * MBOX_FETCH_BODY_HEADER/_TEXT/ - * HEADER_FIELDS; this bit is only - * for the purely-numeric form. */ + * only (SS6.4.5.1, numeric + * section-part). Leaf parts + * only, a MULTIPART container + * or nested MESSAGE/RFC822| + * GLOBAL numbering is "not + * found", matching + * BODYSTRUCTURE's own scope + * cut. Needs req->section_part, + * hence a separate bit from + * WHOLE/TEXT. */ /* - * Cap on the dotted-numeric section-part string (imapd.h's own - * MBOX_FETCH_BODY_PART comment) a BODY[]/BODY.PEEK[] fetch-att - * carries from listener.c to store.c (struct imsg_mbox_fetch's - * section_part below). Sized for MIME_MAX_DEPTH (10) levels of - * MIME_MAX_PARTS (64, i.e. up to 2 digits per level) numbers plus - * separating dots: 10*2 + 9 = 29 characters worst case: 40 leaves - * comfortable headroom without being large enough to matter for the - * imsg-size arithmetic every other MAX constant in this file cares - * about. listener.c's tokenizer rejects (BAD) a section-part that - * would exceed this rather than truncate it, same precedent as every - * other MAX constant here. + * Cap on the dotted-numeric section-part string (struct imsg_mbox_fetch's + * section_part below). Sized for MIME_MAX_DEPTH (10) levels x 2 digits + * plus dots: 29 worst case; 40 leaves headroom. listener.c rejects (BAD) + * an oversized section-part rather than truncating it. */ #define SECTION_PART_MAX 40 /* - * Cap on the raw header bytes IMSG_MBOX_FETCH_HEADER can carry, for the same - * reason APPEND_LITERAL_MAX exists in listener.c: struct imsg_mbox_fetch_ - * header's fixed fields plus this many trailing bytes need to fit under - * MAX_IMSGSIZE (16384, imsg.h) alongside the imsg header itself. Real-world - * RFC 5322 headers are essentially always well under 8192 bytes even with a - * long Received:/DKIM-Signature: chain; a header that somehow exceeds this - * is treated as "not found" for BODY.PEEK[HEADER] purposes (that one - * message's header is silently omitted, same as a message missing on disk - * -- see handle_mbox_fetch()'s existing "indexed but missing on disk" - * skip), not truncated, matching APPEND_LITERAL_MAX's own "reject rather - * than silently do something the client didn't ask for" precedent. + * Cap on raw header bytes IMSG_MBOX_FETCH_HEADER can carry (must fit + * under imsg's MAX_IMSGSIZE, 16384). Real RFC 5322 headers are well + * under 8192; an oversized header is "not found" for BODY.PEEK[HEADER], + * not truncated. */ #define FETCH_HEADER_MAX 8192 /* - * Cap on a whole message's size, both when APPEND writes one and when - * BODY.PEEK[]/BODY.PEEK[TEXT] read one back out. Originally listener.c- - * local (only APPEND needed it); moved here when store.c's read_message_ - * body() needed the identical number, so the two enforcement points share - * one symbol instead of two independently-maintained constants that could - * silently drift apart. See listener.c's own comment at this symbol's - * former definition site for the full MAX_IMSGSIZE arithmetic (12000 - * leaves headroom under imsg's 16384-byte ceiling alongside either - * struct's own fixed fields and the imsg header itself). A message larger - * than this needs real fd-passing, not implemented this pass; rejected - * with a plain NO/omitted from the FETCH response (RFC 9051 defines no - * response code for a size cap) rather than truncating. + * Cap on a whole message's size, for both APPEND and BODY.PEEK[]/ + * BODY.PEEK[TEXT] (shared so the two enforcement points can't drift). + * 12000 leaves headroom under imsg's 16384-byte MAX_IMSGSIZE. Larger + * messages need real fd-passing, not implemented; rejected rather than + * truncated. */ #define APPEND_LITERAL_MAX 12000 /* - * Cap on the bytes any single BODY[
]/BODY.PEEK[
] - * response (whole message, TEXT-only, or a numeric section-part) can - * carry on one IMSG_MBOX_FETCH_BODY, reusing APPEND_LITERAL_MAX's own - * value and "comfortable headroom under imsg's MAX_IMSGSIZE" reasoning - * rather than a new constant, since it's the same underlying limit - * (one struct imsg_mbox_fetch_body's fixed fields plus this many - * trailing bytes have to fit under MAX_IMSGSIZE alongside the imsg - * header itself). Before this pass, only whole/TEXT fetches existed - * and this cap was simply "the message is too big, fail the whole - * item" (APPEND_LITERAL_MAX's original framing). Section-part fetches - * change the picture: MIME parts (attachments especially) routinely - * exceed this on their own -- that's the entire reason BODYSTRUCTURE_ - * READ_MAX exists as a separate, much larger cap on what store.c is - * willing to *read* off disk. A client fetching a large part without - * a <> range still can't get more than this many bytes back - * in one response (treated as "not found" for that item, same reject- - * not-truncate precedent as everywhere else) -- real clients handle - * this by re-fetching in <> ranged chunks instead (confirmed - * against real Apple Mail traffic this pass, which already issues - * BODY.PEEK[TEXT]<0.16384>-style ranged fetches unprompted). A - * <> request's own requested count is silently clamped down - * to this cap rather than rejected outright if it's larger -- RFC 9051 - * SS6.4.5's BODY[]<> semantics already require truncating a - * range that runs past the end of the available text, so a server- - * side response-size cap truncating a too-large *count* the same way - * (returning fewer bytes than asked, letting the client re-fetch the - * remainder at a later origin octet) is consistent with that existing - * "truncate the count, don't fail the fetch" spirit, not a new kind of - * behavior this cap invents. + * Cap on bytes any single BODY[
]/BODY.PEEK[
] response + * can carry on one IMSG_MBOX_FETCH_BODY; reuses APPEND_LITERAL_MAX's + * value and MAX_IMSGSIZE-headroom reasoning. Real clients (confirmed: + * Apple Mail) re-fetch large parts via <> ranges rather than + * expecting a whole part in one response. A <> count larger + * than this is clamped down, per RFC 9051 SS6.4.5's own truncate-past- + * end-of-text precedent, rather than rejected. */ #define FETCH_PART_MAX APPEND_LITERAL_MAX /* * Cap on the space-joined header-field-name list a BODY.PEEK[HEADER. - * FIELDS (...)]/BODY.PEEK[HEADER.FIELDS.NOT (...)] fetch-att carries from - * listener.c to store.c (struct imsg_mbox_fetch's header_fields below). - * 256 bytes comfortably covers any realistic request (RFC 9051 SS6.4.5's - * own example asks for two: "DATE FROM"; even a dozen longish field names - * like "Content-Type"/"Message-Id" fit easily) -- listener.c's parse_ - * header_fields_att() rejects (BAD) a field-name list that would exceed - * this rather than truncate it, same "reject rather than silently do - * something the client didn't ask for" precedent as every other MAX - * constant in this file. + * FIELDS[.NOT] (...)] fetch-att carries to store.c. 256 bytes covers + * any realistic request; listener.c rejects (BAD) an oversized list + * rather than truncating it. */ #define HEADER_FIELDS_MAX 256 /* - * Cap on the fully-formatted ENVELOPE parenthesized-list text store.c's - * build_envelope() can carry on one IMSG_MBOX_FETCH_ENVELOPE (struct imsg_ - * mbox_fetch_envelope below). Sized off the same reasoning as FETCH_HEADER_ - * MAX (8192): every field in an envelope is extracted from a header that - * itself can't exceed FETCH_HEADER_MAX, and while IMAP quoted-string - * escaping (backslash/double-quote doubling) plus the address-structure - * parenthesization overhead can inflate the formatted size somewhat versus - * the raw header, a header dense enough with backslashes/quotes/addresses - * to actually approach 8192 bytes of *formatted* envelope text from a - * header that's itself under 8192 bytes is already an extreme case -- - * matching FETCH_HEADER_MAX's own "essentially always well under" framing. - * A message whose formatted envelope somehow exceeds this is treated as - * "not found" for ENVELOPE purposes (same reject-rather-than-truncate - * precedent as FETCH_HEADER_MAX/APPEND_LITERAL_MAX), not truncated. + * Cap on the fully-formatted ENVELOPE text store.c's build_envelope() + * can carry on one IMSG_MBOX_FETCH_ENVELOPE. Sized off FETCH_HEADER_MAX + * (8192), since every envelope field comes from a header bounded by + * that same cap. An oversized envelope is "not found", not truncated. */ #define ENVELOPE_MAX 8192 /* - * Caps on store.c's recursive BODYSTRUCTURE builder (build_body_structure(), - * store.c), bounding both the work it does and the size of what it can ever - * produce -- a message is entirely attacker/sender-controlled data (MIME - * part count and multipart nesting depth are both just numbers the message's - * own headers claim), so both need a hard ceiling rather than trusting - * whatever a message says about its own structure. MIME_MAX_DEPTH (10) is - * generous for any real mail this personal-use server will see -- deeply - * nested multipart is already unusual beyond 2-3 levels (e.g. multipart/ - * mixed containing a multipart/alternative) -- while still bounding - * recursion (and hence worst-case stack use) at a small, fixed number. - * MIME_MAX_PARTS (64) similarly bounds total part count across the whole - * recursive walk (a running counter threaded through every recursive call, - * not a per-multipart-parent limit), bounding both output size and total - * work independent of depth. Exceeding either cap is treated as "not found" - * for this message's BODYSTRUCTURE (reject, not silently truncate the part - * tree into something that no longer accurately describes the message) -- - * same precedent as every other MAX constant in this header. + * Caps on store.c's recursive BODYSTRUCTURE builder (build_body_ + * structure()): a message's own headers claim its own part count and + * nesting depth, so both need a hard ceiling rather than trusting them. + * Exceeding either is "not found" for that message's BODYSTRUCTURE + * rather than a truncated part tree. */ #define MIME_MAX_DEPTH 10 #define MIME_MAX_PARTS 64 /* - * Cap on the fully-formatted BODYSTRUCTURE parenthesized-list text store.c's - * build_bodystructure() can carry on one IMSG_MBOX_FETCH_BODYSTRUCTURE - * (struct imsg_mbox_fetch_bodystructure below). Unlike ENVELOPE_MAX, this - * isn't derived from a single header's own size cap -- a BODYSTRUCTURE's - * size instead scales with MIME_MAX_PARTS (each part contributing its own - * type/subtype/parameter-list/encoding/octet-count fields) and MIME_MAX_ - * DEPTH (each nesting level adding its own wrapping parens and multipart - * subtype). 12000 mirrors APPEND_LITERAL_MAX's own reasoning: comfortable - * headroom under imsg(3)'s MAX_IMSGSIZE (16384) alongside this struct's own - * fixed fields and the imsg header itself, generous enough that MIME_MAX_ - * PARTS/MIME_MAX_DEPTH -- not this byte cap -- are expected to be the - * limiting factor in practice for any real message. Same reject-rather- - * than-truncate handling as every other MAX constant here if somehow - * exceeded anyway. + * Cap on the fully-formatted BODYSTRUCTURE text store.c's + * build_bodystructure() can carry on one IMSG_MBOX_FETCH_BODYSTRUCTURE. + * 12000 mirrors APPEND_LITERAL_MAX's MAX_IMSGSIZE-headroom reasoning; + * MIME_MAX_PARTS/MIME_MAX_DEPTH, not this byte cap, are expected to be + * the limiting factor in practice. */ #define BODYSTRUCTURE_MAX 12000 /* - * Cap on the raw on-disk bytes store.c's build_bodystructure() (via read_ - * message_body(), store.c) will read into memory while deriving a - * message's MIME structure. Deliberately independent from APPEND_LITERAL_ - * MAX/FETCH_BODY_MAX: that constant's "no on-disk message can legally - * exceed this" reasoning only holds for messages that arrived through - * this server's own APPEND command, not for mail delivered by an - * external MTA into the spool directly, which this server doesn't - * control the size of at all. Real-hardware testing (an Apple Mail - * message with a small image attachment) confirmed this in practice -- - * base64-encoded attachment data routinely pushes even a modest image - * past 12000 bytes, so sharing that cap made BODYSTRUCTURE fail on - * essentially any real attachment-bearing mail. + * Cap on raw on-disk bytes build_bodystructure() (via read_message_ + * body()) will read while deriving a message's MIME structure. + * Independent from APPEND_LITERAL_MAX: mail delivered by an external + * MTA isn't size-bounded by this server's own APPEND cap, and + * real-hardware testing showed base64 attachments routinely exceed it. + * The read is never sent back over the wire whole (only the derived, + * already-bounded structure summary is), so it can afford to be large. * - * build_bodystructure() only *derives* a small, MIME_MAX_PARTS/MIME_MAX_ - * DEPTH/BODYSTRUCTURE_MAX-bounded structure summary from these bytes -- - * it never sends the raw bytes themselves back to the client over the - * wire -- so, unlike BODY.PEEK[]/BODY.PEEK[TEXT] (still capped at - * APPEND_LITERAL_MAX, since those responses do carry the raw bytes - * whole on a single imsg and are therefore still bound by imsg's own - * MAX_IMSGSIZE ceiling), this read can afford to be much larger. + * 41943040 (40 MiB) is sized off Gmail's documented 25MB attachment + * limit (support.google.com/mail/answer/6584) plus base64 overhead, not + * a protocol requirement. Exceeding it is "not found" for that + * message's BODYSTRUCTURE, not truncated. * - * 41943040 (40 MiB) is sized off Gmail's own documented 25MB attachment - * limit (https://support.google.com/mail/answer/6584), the most common - * real-world ceiling a personal mailbox is likely to receive mail under, - * plus headroom for base64's ~37% encoding overhead (a 25MB attachment - * becomes roughly 34MB once base64-encoded and wrapped in MIME headers) - * -- not a hard protocol requirement, just a generous, cited real-world - * bound rather than an arbitrary guess. Exceeding it is "not found" for - * this message's BODYSTRUCTURE (reject, not truncate -- same precedent - * as every other MAX constant in this header), same as any other reason - * this message's structure can't be produced. - * - * As of this pass, this value is operator-configurable via imapd.conf's - * "attachment max " directive (parse.y), since the whole reason - * this cap is sized off Gmail's own attachment limit rather than derived - * from any protocol constant is that it's inherently a judgment call - * about the operator's own expected mail, not a fixed property of the - * implementation -- see parse.y's grammar rule for the directive and - * struct openimap_config's bodystructure_read_max field. This macro - * changed meaning accordingly: no code reads it directly anymore (store.c - * uses the runtime value received via IMSG_STORE_INIT instead); it now - * exists solely as config_load()'s default when the directive is absent - * from imapd.conf, so an empty/absent config file still gets the same - * behavior this implementation always had. + * Operator-configurable via imapd.conf's "attachment max " + * directive (parse.y, struct openimap_config's bodystructure_read_max). + * store.c uses the runtime value from IMSG_STORE_INIT; this macro now + * only serves as config_load()'s default when the directive is absent. */ #define BODYSTRUCTURE_READ_DEFAULT 41943040 @@ -1216,108 +525,52 @@ struct imsg_mbox_fetch { uint32_t seq_hi; /* 1-based, inclusive; ignored if * hi_is_star */ int lo_is_star; - int hi_is_star; /* "*" is resolved by store against - * its own live message count at - * fetch time, not against listener's - * -- possibly stale -- count from the - * last SELECT reply (mail could have - * arrived since) */ + int hi_is_star; /* "*" resolves against store's live + * message count, not listener's + * possibly-stale SELECT-time count */ uint32_t attrs; /* bitmask of MBOX_FETCH_* above */ - /* - * RFC 7162 SS3.1.4.1 CHANGEDSINCE fetch-modifier: "The information - * described by message data items is only returned for messages - * that have a mod-sequence bigger than ." has_ - * changedsince distinguishes "not specified" from a legal value of - * 0, the same has_/value pairing this header already uses for - * APPEND's optional date-time (see imsg_mbox_append's append_has_ - * date, in listener.c's struct session). - */ + /* RFC 7162 SS3.1.4.1 CHANGEDSINCE; has_changedsince distinguishes + * "not specified" from a legal value of 0. */ int has_changedsince; uint64_t changedsince; - /* - * RFC 9051 SS6.4.9 (UID command): "the numbers in the sequence-set - * argument are unique identifiers instead of message sequence - * numbers" for a UID FETCH -- by_uid tells store.c to resolve seq_lo/ - * seq_hi (and "*") against UID space rather than 1-based index - * position. listener.c separately forces MBOX_FETCH_UID into attrs - * whenever by_uid is set (SS6.4.9: "server implementations MUST - * implicitly include the UID message data item as part of any FETCH - * response caused by a UID command"), so store.c itself needs no - * special-casing for *that* part -- meta.uid is already unconditionally - * populated regardless (see imsg_mbox_fetch_meta below). + /* by_uid: RFC 9051 SS6.4.9 UID FETCH, resolve seq_lo/seq_hi + * against UID space. listener.c separately forces MBOX_FETCH_UID + * into attrs whenever this is set. * - * want_vanished is RFC 7162 SS3.2.6's VANISHED UID FETCH modifier - * (only legal alongside CHANGEDSINCE, and only on UID FETCH -- - * listener.c's parse_fetch_modifiers()/fetch_dispatch() enforce both - * restrictions before this ever reaches store.c). Per this - * implementation's RFC 7162 SS5.1 minimal-state QRESYNC decision - * (see imsg_mbox_select_vanished below), store.c doesn't track - * expunge-event history, so it can't actually filter by CHANGEDSINCE - * here either -- it reports every UID in [seq_lo, seq_hi] that isn't - * currently present, unconditionally, which SS3.2.6's own note - * explicitly sanctions ("A server that receives a mod-sequence - * smaller than ... MUST behave as if it was requested to - * report all expunged messages from the provided UID set parameter" -- - * this implementation always behaves that way, having no memory of - * at all). + * want_vanished: RFC 7162 SS3.2.6 VANISHED modifier (only legal + * with CHANGEDSINCE, only on UID FETCH; listener.c enforces both). + * Per this implementation's minimal-state QRESYNC model, store.c + * reports every UID in range that isn't present, unconditionally + *, explicitly sanctioned by SS3.2.6. */ int by_uid; int want_vanished; - /* - * BODY.PEEK[HEADER.FIELDS (...)]/BODY.PEEK[HEADER.FIELDS.NOT (...)] - * (see MBOX_FETCH_HEADER_FIELDS above): header_fields_not is 0 for - * HEADER.FIELDS (include only the listed names), 1 for HEADER. - * FIELDS.NOT (exclude the listed names). header_fields is the - * requested field-name list, space-joined, exactly as the client - * typed each name (matching is ASCII-range case-insensitive per - * SS6.4.5.1, done by store.c's read_message_header_fields() -- - * this field is not itself normalized to any particular case). - * Both are only meaningful alongside attrs & MBOX_FETCH_HEADER_ - * FIELDS; listener.c's parse_header_fields_att() has already fully - * validated the header-list grammar before either field is ever - * populated, so store.c can assume header_fields is well-formed - * (non-empty, space-separated, no embedded quotes -- see that - * function's comment for why a bare-atom-only field name is this - * implementation's own scope cut). + /* BODY.PEEK[HEADER.FIELDS[.NOT] (...)] (MBOX_FETCH_HEADER_FIELDS): + * header_fields_not is 0 for FIELDS (include), 1 for FIELDS.NOT + * (exclude). header_fields is the space-joined field-name list, + * exactly as typed (matching is ASCII case-insensitive, done by + * store.c). listener.c has already validated the grammar before + * either field is populated. */ int header_fields_not; char header_fields[HEADER_FIELDS_MAX]; - /* - * BODY[]/BODY.PEEK[] (MBOX_FETCH_BODY_ - * PART above): section_part is the client-typed dotted-numeric part - * path verbatim (e.g. "3.1"), non-empty only alongside attrs & - * MBOX_FETCH_BODY_PART -- listener.c's tokenizer has already - * validated it's 1*(digit) *("." 1*digit) with no leading zeros - * before this is ever populated, so store.c can assume it parses - * cleanly into a path of nz-numbers. Single shared field, same "one - * instance assumed per FETCH command" simplification as header_ - * fields above -- a client requesting two different section-parts - * in one FETCH (legal per SS6.4.5.1, unseen from any real client so - * far) only gets the first one honored, matching that same - * precedent rather than restructuring this struct into an array for - * a case no real client actually does. + /* BODY[]/BODY.PEEK[] + * (MBOX_FETCH_BODY_PART): section_part is the client-typed + * dotted-numeric path (e.g. "3.1"), pre-validated by listener.c's + * tokenizer. Single shared field, a client requesting two + * section-parts in one FETCH only gets the first honored. * - * has_partial/partial_start/partial_count carry a <> range - * (SS6.4.5's "" suffix, e.g. "BODY.PEEK[3.1]<0.65536>" - * or "BODY.PEEK[TEXT]<0.16384>" -- the latter is real, observed - * Apple Mail traffic this project's own real-hardware testing - * turned up, previously silently unhandled since no "<...>" parsing - * existed anywhere in listener.c at all before this pass). Applies - * uniformly to whichever BODY[...]/BODY.PEEK[...] variant attrs - * selects (whole, TEXT, or section_part) -- store.c slices the - * already-extracted content to [partial_start, partial_start + - * partial_count) before ever composing the response imsg, clamped - * to FETCH_PART_MAX and to the content's own actual length (RFC - * 9051 SS6.4.5: "If the starting octet is beyond the end of the - * text, an empty string is returned... Any partial fetch that - * attempts to read beyond the end of the text is truncated as - * appropriate"). has_partial distinguishes "no range requested" - * from a legal partial_start of 0, same has_/value pairing this - * header already uses for CHANGEDSINCE above. + * has_partial/partial_start/partial_count carry a <> + * range (SS6.4.5), applying uniformly to whole/TEXT/section_part. + * store.c slices the extracted content to + * [partial_start, partial_start + partial_count), clamped to + * FETCH_PART_MAX and the content's actual length (SS6.4.5's + * truncate-past-end-of-text rule). has_partial distinguishes + * "no range" from a legal partial_start of 0. */ char section_part[SECTION_PART_MAX]; int has_partial; @@ -1330,290 +583,168 @@ struct imsg_mbox_fetch_meta { uint32_t uid; uint64_t size; /* on-disk file size, octets -- * RFC822.SIZE (RFC 9051 SS2.3.4). - * F13 fix: 64-bit so a message larger - * than 4 GiB reports a correct size. */ + * 64-bit so a message over 4 GiB + * reports correctly (F13 fix). */ int64_t internaldate; /* Unix timestamp, parsed from the - * maildir basename's own leading - * delivery-time field -- see store.c's - * handle_mbox_fetch() comment for why - * that's used instead of the file's - * mtime */ + * maildir basename's delivery-time + * field, not the file's mtime */ char flags[MBOX_FLAGS_MAX]; /* space-separated IMAP flag - * names, e.g. "\Seen \Flagged foo" -- - * pre-formatted by store.c, the only - * side that has both the maildir - * flag-suffix letters and the index's - * keyword list */ - uint64_t modseq; /* RFC 7162 per-message mod-sequence -- - * always populated by store.c (cheap: - * it's already parsed the index line), - * same "always compute, let listener.c - * decide whether to print it" split as - * every other struct imsg_mbox_fetch_ - * meta field; shared by FETCH, STORE's - * FETCH echo, and QRESYNC SELECT - * resync's FETCH-with-UID responses, - * all three of which reuse this struct - * (see imsg_mbox_selected's comment) */ + * names, e.g. "\Seen \Flagged foo", + * pre-formatted by store.c */ + uint64_t modseq; /* RFC 7162 per-message mod-sequence, + * always populated; shared by FETCH, + * STORE's FETCH echo, and QRESYNC + * resync's FETCH-with-UID responses */ }; /* * IMSG_MBOX_FETCH_HEADER: sent by store.c immediately before the - * IMSG_MBOX_FETCH_META for the same message, if and only if req->attrs & - * MBOX_FETCH_BODY_HEADER -- listener.c relies on this exact ordering - * (rather than matching on seqno/uid) to fold the header bytes into the - * same untagged "* N FETCH (...)" response line as the message's other - * requested data items, per RFC 9051's own SS6.4.5/SS7.5.2 worked examples, - * which always show every FETCH data item for a message on one response - * line, not spread across several. This mirrors imsg_mbox_append's - * "fixed struct, then trailing variable-length bytes on the same imsg" - * shape (listener.c's session_finish_append(): malloc(sizeof(struct) + - * len), memcpy both pieces in, one imsg_compose() call) -- just sent in - * the opposite direction (store -> listener) and read back the same way - * imsg_mbox_append already is (store.c's handle_mbox_append(): - * imsg_get_buf() for the fixed struct, then imsg_get_len()/imsg_get_buf() - * again for whatever trailing bytes remain). + * IMSG_MBOX_FETCH_META for the same message, iff req->attrs & + * MBOX_FETCH_BODY_HEADER, listener.c relies on this exact ordering to + * fold the header bytes into the same "* N FETCH (...)" response line. + * Same "fixed struct + trailing variable-length bytes on one imsg" shape + * as imsg_mbox_append. */ struct imsg_mbox_fetch_header { - uint32_t seqno; /* 1-based -- matches the seqno on the - * IMSG_MBOX_FETCH_META that follows */ + uint32_t seqno; /* 1-based, matches the following + * IMSG_MBOX_FETCH_META */ uint32_t uid; - int found; /* 0 if locate_message_file() (well, - * store.c's own open_message_file(), - * see its comment for why this is a - * separate lookup rather than a shared - * one) couldn't find the message file, - * or its header exceeded - * FETCH_HEADER_MAX -- listener.c omits - * BODY[HEADER] from this one message's + int found; /* 0 if the message file couldn't be + * found or its header exceeded + * FETCH_HEADER_MAX; listener.c omits + * BODY[HEADER] from this message's * response rather than failing the - * whole FETCH, same as a metadata - * lookup failure already does. hdrlen - * and the trailing bytes are only - * meaningful when found is 1. */ + * whole FETCH. hdrlen and the trailing + * bytes are only meaningful if 1. */ uint32_t hdrlen; /* length of the trailing raw header - * bytes on this same imsg, capped at + * bytes on this imsg, capped at * FETCH_HEADER_MAX */ }; /* * IMSG_MBOX_FETCH_BODY: sent by store.c immediately before the - * IMSG_MBOX_FETCH_META for the same message, if and only if req->attrs & - * (MBOX_FETCH_BODY_WHOLE | MBOX_FETCH_BODY_TEXT) -- same ordering contract, - * same "fixed struct + trailing variable-length bytes on one imsg" shape, - * as struct imsg_mbox_fetch_header above (see that struct's comment); this - * is the same wire idiom used a third time, not a new one. + * IMSG_MBOX_FETCH_META for the same message, iff req->attrs & + * (MBOX_FETCH_BODY_WHOLE | MBOX_FETCH_BODY_TEXT). Same ordering contract + * and wire shape as imsg_mbox_fetch_header above. */ struct imsg_mbox_fetch_body { - uint32_t seqno; /* 1-based -- matches the seqno on the - * IMSG_MBOX_FETCH_META that follows */ + uint32_t seqno; /* 1-based, matches the following + * IMSG_MBOX_FETCH_META */ uint32_t uid; - int found; /* 0 if open_message_file() couldn't find - * the message file, the file exceeded - * APPEND_LITERAL_MAX (listener.c), the - * content contained a NUL byte, or (for - * is_text only) no header/body separator - * could be found within the file -- - * listener.c omits BODY[]/BODY[TEXT] from - * this one message's response rather than - * failing the whole FETCH, same precedent - * as imsg_mbox_fetch_header's found field. - * bodylen and the trailing bytes are only - * meaningful when found is 1. */ - int is_text; /* 0 -- these are BODY.PEEK[] bytes (whole - * message, header and body together); 1 -- - * these are BODY.PEEK[TEXT] bytes (body - * only, header omitted). Set by store.c - * based on which of MBOX_FETCH_BODY_WHOLE/ - * MBOX_FETCH_BODY_TEXT req->attrs actually - * had set (WHOLE wins if a client somehow - * requested both, see MBOX_FETCH_BODY_ - * TEXT's comment) -- listener.c uses this - * to label the literal "BODY[]" vs - * "BODY[TEXT]" correctly rather than - * re-deriving it from its own fetch_attrs, - * which could have both bits set. */ - uint32_t bodylen; /* length of the trailing raw body bytes on - * this same imsg, capped at - * APPEND_LITERAL_MAX (listener.c) -- same - * cap APPEND itself already enforces when - * writing a message, reused here rather - * than inventing a second constant, since - * no message on disk can legally exceed it - * in the first place */ + int found; /* 0 if the message file couldn't be + * found, it exceeded + * APPEND_LITERAL_MAX, it contained a + * NUL byte, or (is_text only) no + * header/body separator was found. + * bodylen and the trailing bytes are + * only meaningful if 1. */ + int is_text; /* 0 = BODY.PEEK[] bytes (whole + * message); 1 = BODY.PEEK[TEXT] bytes + * (body only). Set from which of + * MBOX_FETCH_BODY_WHOLE/_TEXT was + * requested (WHOLE wins if both). */ + uint32_t bodylen; /* length of the trailing raw body + * bytes on this imsg, capped at + * APPEND_LITERAL_MAX */ }; /* * IMSG_MBOX_FETCH_ENVELOPE: sent by store.c immediately before the - * IMSG_MBOX_FETCH_META for the same message, if and only if req->attrs & - * MBOX_FETCH_ENVELOPE -- same ordering contract as struct imsg_mbox_fetch_ - * header/imsg_mbox_fetch_body above. Unlike those two, whose trailing bytes - * are raw message content that listener.c wraps in a `{n}` literal, the - * trailing bytes here are the *complete*, already-formatted RFC 9051 - * SS7.5.2 envelope parenthesized-list text -- e.g. `("Wed, 17 Jul 1996 - * 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry - * Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" - * "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) - * ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" - * "CNRI.Reston.VA.US")("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL - * "")`, RFC 9051's own SS7.5.2 worked - * example -- fully quoted/escaped and ready to splice verbatim into the - * FETCH response right after the literal string "ENVELOPE ". store.c does - * all the RFC 5322 header parsing, field unfolding, and address-list - * decomposition (build_envelope(), store.c) -- that's where the raw header - * bytes already live and where BODY.PEEK[HEADER.FIELDS...]'s folding-aware - * parsing already lives, so this keeps parsing logic in one place rather - * than splitting RFC 5322 semantics across both processes; listener.c's job - * stays purely wire framing, same division of labor as every other FETCH - * content item this project has implemented. No literal-block wrapping - * needed (unlike BODY[HEADER]/BODY[]/BODY[TEXT]): envelope fields are short - * quoted strings built by build_envelope()'s own IMAP-quoted-string escaper, - * never raw message bytes, so they're never CRLF-bearing and always fit - * directly on the response line. + * IMSG_MBOX_FETCH_META for the same message, iff req->attrs & + * MBOX_FETCH_ENVELOPE. Unlike fetch_header/fetch_body, the trailing bytes + * are the complete, already-formatted RFC 9051 SS7.5.2 envelope + * parenthesized-list text, ready to splice verbatim after "ENVELOPE " in + * the FETCH response. store.c's build_envelope() does all RFC 5322 + * parsing and address-list decomposition; listener.c does pure wire + * framing. No literal-block wrapping needed, envelope fields are short + * quoted strings, never raw message bytes, so never CRLF-bearing. */ struct imsg_mbox_fetch_envelope { - uint32_t seqno; /* 1-based -- matches the seqno on the - * IMSG_MBOX_FETCH_META that follows */ + uint32_t seqno; /* 1-based, matches the following + * IMSG_MBOX_FETCH_META */ uint32_t uid; - int found; /* 0 if open_message_file()/read_message_ - * header() couldn't find or read the - * message's header, or the formatted - * envelope text exceeded ENVELOPE_MAX -- - * listener.c omits ENVELOPE from this one - * message's response rather than failing - * the whole FETCH, same precedent as - * imsg_mbox_fetch_header/imsg_mbox_fetch_ - * body's own found fields. envlen and the - * trailing bytes are only meaningful when - * found is 1. */ - uint32_t envlen; /* length of the trailing, already- - * formatted envelope text on this same - * imsg, capped at ENVELOPE_MAX */ + int found; /* 0 if the message's header couldn't + * be found/read, or the formatted + * envelope exceeded ENVELOPE_MAX. + * envlen and the trailing bytes are + * only meaningful if 1. */ + uint32_t envlen; /* length of the trailing formatted + * envelope text, capped at + * ENVELOPE_MAX */ }; /* * IMSG_MBOX_FETCH_BODYSTRUCTURE: sent by store.c immediately before the - * IMSG_MBOX_FETCH_META for the same message, if and only if req->attrs & - * MBOX_FETCH_BODYSTRUCTURE -- same ordering contract and same "already- - * formatted response text, not raw bytes" shape as struct imsg_mbox_fetch_ - * envelope above (see that struct's comment). The trailing bytes are the - * *complete* RFC 9051 SS7.5.2 BODYSTRUCTURE parenthesized-list text, e.g. - * `("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 2279 48)` for a - * simple message (RFC 9051's own SS7.5.2 example), or, for a multipart - * message, a nested structure like `(("TEXT" "PLAIN" ("CHARSET" "US-ASCII") - * NIL NIL "7BIT" 1152 23)("TEXT" "PLAIN" ("CHARSET" "US-ASCII" "NAME" - * "cc.diff") "<...>" "Compiler diff" "BASE64" 4554 73) "MIXED")` (also RFC - * 9051's own example) -- built entirely by store.c's build_bodystructure()/ - * build_body_structure(), which do all the MIME parsing (Content-Type/ - * Content-Transfer-Encoding/Content-ID/Content-Description extraction, - * multipart boundary splitting, recursion into sub-parts bounded by MIME_ - * MAX_DEPTH/MIME_MAX_PARTS) -- same "all the semantic parsing lives in one - * process, listener.c does pure wire framing" division of labor as - * envelope. This implementation deliberately never emits RFC 9051's - * optional extension data (body MD5/disposition/language/location -- see - * MBOX_FETCH_BODYSTRUCTURE's own comment for why), so a BODYSTRUCTURE fetch - * and a bare BODY fetch (the explicitly non-extensible form) produce - * identical text in this server. No literal-block wrapping needed, same - * reasoning as envelope (build_body_structure()'s own IMAP-quoted-string - * escaping, reused from envbuf_append_nstring(), guarantees no raw CRLF). + * IMSG_MBOX_FETCH_META for the same message, iff req->attrs & + * MBOX_FETCH_BODYSTRUCTURE. Same "already-formatted response text" + * shape as imsg_mbox_fetch_envelope. The trailing bytes are the complete + * RFC 9051 SS7.5.2 BODYSTRUCTURE parenthesized-list text, built by + * store.c's build_bodystructure()/build_body_structure() (Content-Type/ + * CTE/Content-ID/Content-Description extraction, multipart boundary + * splitting, recursion bounded by MIME_MAX_DEPTH/MIME_MAX_PARTS). + * Extension data (MD5/disposition/language/location) is never emitted, + * so BODYSTRUCTURE and bare BODY produce identical text. No + * literal-block wrapping needed, same reasoning as envelope. */ struct imsg_mbox_fetch_bodystructure { - uint32_t seqno; /* 1-based -- matches the seqno on the - * IMSG_MBOX_FETCH_META that follows */ + uint32_t seqno; /* 1-based, matches the following + * IMSG_MBOX_FETCH_META */ uint32_t uid; - int found; /* 0 if the message's raw bytes couldn't - * be read, MIME_MAX_DEPTH/MIME_MAX_PARTS - * was exceeded, the message contains a - * message/rfc822 or message/global part - * (see MBOX_FETCH_BODYSTRUCTURE's comment - * for why those are scoped out), or the - * formatted text exceeded BODYSTRUCTURE_ - * MAX -- listener.c omits BODYSTRUCTURE - * from this one message's response rather - * than failing the whole FETCH, same - * precedent as every other content item's - * own found field. bslen and the trailing - * bytes are only meaningful when found is - * 1. */ - uint32_t bslen; /* length of the trailing, already- - * formatted BODYSTRUCTURE text on this - * same imsg, capped at BODYSTRUCTURE_MAX */ + int found; /* 0 if the message's raw bytes + * couldn't be read, MIME_MAX_DEPTH/ + * MIME_MAX_PARTS was exceeded, the + * message contains a message/rfc822 + * or message/global part, or the + * formatted text exceeded + * BODYSTRUCTURE_MAX. bslen and the + * trailing bytes are only meaningful + * if 1. */ + uint32_t bslen; /* length of the trailing formatted + * BODYSTRUCTURE text, capped at + * BODYSTRUCTURE_MAX */ }; /* enum mbox_op_error is defined earlier in this file, above - * imsg_mbox_select -- moved there because every struct using it, - * including this one, needs the complete type in scope, and - * imsg_mbox_selected/imsg_mbox_status_result appear before this point. - * See that definition's own comment for the full rationale. */ + * imsg_mbox_select, since several structs before this point need the + * complete type in scope. */ -/* Generic per-operation completion signal -- FETCH is the first user, - * but the name (and the enum's own placement of IMSG_MBOX_RESULT after - * every other IMSG_MBOX_* type) suggests it's meant to close out - * STORE/APPEND/etc. too once those exist. */ +/* Generic per-operation completion signal; FETCH is the first user, but + * intended for STORE/APPEND/etc. too. */ struct imsg_mbox_result { enum mbox_op_error error; uint32_t count; /* number of IMSG_MBOX_FETCH_META (or * equivalent, for a future op) messages that * preceded this one */ - /* - * RFC 7162: the mailbox's HIGHESTMODSEQ after this operation. - * Always populated for STORE/EXPUNGE (the two operations that can - * change it); left 0 for a plain FETCH, which never mutates - * anything. listener.c is the one that decides whether/how to - * surface it: EXPUNGE's tagged OK MAY include it (SS3.2.7, "If at - * least one message got expunged and QRESYNC was enabled, the - * server MUST send" it -- this implementation does so whenever - * CONDSTORE is enabled at all, a safe superset, see cmd_expunge()'s - * comment), STORE's tagged OK/NO doesn't need to (SS3.1.3's own - * examples show it appearing on some responses and not others -- - * "presumably because this was the first CONDSTORE enabling - * command", i.e. it's the same "first enabling command" case - * covered by session_condstore_enable(), not something every STORE - * repeats), and CLOSE's tagged OK MUST NOT include it (SS3.2.8, - * explicit "MUST NOT ... as this might cause loss of - * synchronization on the client" -- cmd_close()'s existing - * was_close branch just never reads this field). + /* RFC 7162: mailbox HIGHESTMODSEQ after this operation. Always + * populated for STORE/EXPUNGE; 0 for plain FETCH. listener.c + * decides whether to surface it: EXPUNGE's tagged OK includes it + * whenever CONDSTORE is enabled (SS3.2.7); STORE's tagged OK only + * on the first CONDSTORE-enabling command (SS3.1.3); CLOSE's + * tagged OK MUST NOT include it (SS3.2.8). */ uint64_t highestmodseq; - /* - * RFC 9051 SS7.1's COPYUID response code: "the UIDVALIDITY of the - * destination mailbox". Populated only for COPY/MOVE (see imapd.h's - * imsg_mbox_copy comment) -- v1 has no CREATE, so the destination is - * always the same mailbox as the source (the currently selected - * mailbox), making this identical to that mailbox's own UIDVALIDITY; - * left 0 for FETCH/STORE/EXPUNGE/SEARCH, which have no COPYUID to - * report. Same "populated for the operations that need it, 0 - * otherwise, listener.c decides what to do with it" split as - * highestmodseq above. + /* RFC 9051 SS7.1 COPYUID response code: destination mailbox's + * UIDVALIDITY. Populated only for COPY/MOVE; 0 for + * FETCH/STORE/EXPUNGE/SEARCH. */ uint32_t uidvalidity; }; /* - * IMSG_MBOX_STORE (listener -> store): RFC 9051 SS6.4.6 STORE command -- - * `store = "STORE" SP sequence-set SP store-att-flags`, `store-att-flags = - * (["+" / "-"] "FLAGS" [".SILENT"]) SP (flag-list / (flag *(SP flag)))`. - * Replies reuse IMSG_MBOX_FETCH_META (one per modified message, sent only - * if !silent) and the terminal IMSG_MBOX_RESULT -- SS6.4.6 itself says - * STORE's only response is "untagged responses: FETCH", the exact same - * shape FETCH already produces ("* FETCH (FLAGS (...))"), so - * there was no reason to invent a second reply pair. + * IMSG_MBOX_STORE (listener -> store): RFC 9051 SS6.4.6 STORE. Replies + * reuse IMSG_MBOX_FETCH_META (one per modified message, only if + * !silent) and the terminal IMSG_MBOX_RESULT, SS6.4.6's only response + * is an untagged FETCH, the same shape FETCH itself produces. * - * System flags are a fixed 5-bit set (v1 supports exactly the five RFC - * 9051 SS2.3.2 system flags: \Answered \Flagged \Deleted \Seen \Draft -- - * \Recent is explicitly excluded from the `flag` ABNF production itself, - * and any other "\"-prefixed token is a `flag-extension` this server - * doesn't define, so listener.c rejects both before ever building this - * struct). Keywords (arbitrary non-"\" atoms, SS2.3.2's `flag-keyword`) - * are carried separately as a comma-separated list -- matching the - * index's own on-disk keyword delimiter so store.c can merge them - * directly without a format conversion; listener.c - * rejects any client-supplied keyword containing ':' or ',' (both valid - * in IMAP's `atom` grammar, but the index format has no escaping - * mechanism for its own field/record delimiters -- see cmd_store_cmd()'s - * comment) rather than silently corrupting the index. + * System flags are the fixed 5-bit RFC 9051 SS2.3.2 set (\Answered + * \Flagged \Deleted \Seen \Draft; \Recent and any flag-extension are + * rejected by listener.c before this struct is built). Keywords are + * carried as a comma-separated list matching the index's own + * delimiter; listener.c rejects a keyword containing ':' or ',' (legal + * IMAP atoms, but the index format has no escaping for its own + * delimiters). */ #define MBOX_FLAG_ANSWERED (1U << 0) #define MBOX_FLAG_FLAGGED (1U << 1) @@ -1621,9 +752,9 @@ struct imsg_mbox_result { #define MBOX_FLAG_SEEN (1U << 3) #define MBOX_FLAG_DRAFT (1U << 4) -#define MBOX_STORE_SET 0 /* FLAGS -- replace outright */ -#define MBOX_STORE_ADD 1 /* +FLAGS -- union in */ -#define MBOX_STORE_REMOVE 2 /* -FLAGS -- subtract out */ +#define MBOX_STORE_SET 0 /* FLAGS, replace outright */ +#define MBOX_STORE_ADD 1 /* +FLAGS, union in */ +#define MBOX_STORE_REMOVE 2 /* -FLAGS, subtract out */ struct imsg_mbox_store { uint32_t seq_lo; @@ -1631,36 +762,23 @@ struct imsg_mbox_store { int lo_is_star; int hi_is_star; int mode; /* MBOX_STORE_* above */ - int silent; /* 1 if the ".SILENT" suffix was given - * -- suppress the untagged FETCH - * response per message */ + int silent; /* 1 = ".SILENT", suppress the + * untagged FETCH per message */ uint32_t sysflags; /* MBOX_FLAG_* bitmask named in this - * STORE (the flags being set/added/ - * removed, not the message's - * resulting flags -- store.c computes - * that) */ + * STORE (flags being set/added/ + * removed, not the resulting flags) */ char keywords[MBOX_FLAGS_MAX]; /* comma-separated keyword * atoms named in this STORE, "" if * none */ - /* - * RFC 7162 SS3.1.3 UNCHANGEDSINCE store-modifier. has_unchangedsince - * distinguishes "not specified" from the legal value 0 (SS3.1.3 - * Example 8: "Use of UNCHANGEDSINCE with a modification sequence of - * 0 always fails if the metadata item exists" -- a deliberate, - * always-fails conditional test, not the same as omitting the - * modifier entirely). + /* RFC 7162 SS3.1.3 UNCHANGEDSINCE; has_unchangedsince distinguishes + * "not specified" from the legal (always-fails) value 0. */ int has_unchangedsince; uint64_t unchangedsince; - /* - * RFC 9051 SS6.4.9: same UID-vs-sequence-number resolution switch as - * imsg_mbox_fetch's by_uid, for UID STORE. meta.uid (in the shared - * imsg_mbox_fetch_meta STORE echo) is already unconditionally - * populated regardless of this flag -- only listener.c's decision to - * *print* it changes based on whether the in-flight command was UID - * STORE (see struct session's cmd_by_uid in listener.c). + /* RFC 9051 SS6.4.9: UID-vs-sequence-number switch for UID STORE, + * same as imsg_mbox_fetch's by_uid. */ int by_uid; }; @@ -1668,13 +786,9 @@ struct imsg_mbox_store { /* * IMSG_MBOX_STORE_MODIFIED (store -> listener, zero or more, only when * req->has_unchangedsince, before the terminal IMSG_MBOX_RESULT): one - * message whose mod-sequence exceeded the UNCHANGEDSINCE value, so the - * requested STORE operation was *not* performed for it (RFC 7162 SS3.1.3: - * "the message number (or unique identifier in the case of the UID STORE - * command) is added to the list of messages that failed the UNCHANGEDSINCE - * test"). listener.c range-compacts these into the tagged response's - * MODIFIED response code, same compaction helper SEARCH/VANISHED already - * use. + * message whose mod-sequence exceeded UNCHANGEDSINCE, so the STORE was + * not performed for it (RFC 7162 SS3.1.3). listener.c range-compacts + * these into the tagged response's MODIFIED response code. */ struct imsg_mbox_store_modified { uint32_t seqno; @@ -1683,47 +797,27 @@ struct imsg_mbox_store_modified { /* * IMSG_MBOX_EXPUNGE (listener -> store) / IMSG_MBOX_EXPUNGED (store -> - * listener, one per removed message, streamed in the order store.c - * actually removes them) / IMSG_MBOX_RESULT (store -> listener, exactly - * once, terminal -- same generic completion signal FETCH/STORE already - * use). + * listener, one per removed message, in removal order) / IMSG_MBOX_RESULT + * (store -> listener, terminal). * - * RFC 9051 SS6.4.3: EXPUNGE "permanently removes all messages that have - * the \Deleted flag set from the currently selected mailbox," sending one - * untagged EXPUNGE response per removed message before the tagged OK. - * SS7.5.1 (the EXPUNGE response itself): "The message sequence number for - * each successive message in the mailbox is immediately decremented by 1" - * -- so which sequence number gets reported for each removal depends on - * removal order; this server removes lowest-numbered first ("a 'lower to - * higher' server," SS7.5.1's own term), matching SS6.4.3's own worked - * example exactly (message 3, then 3 again, then 5, then 8, for original - * positions 3/4/7/11) -- see store.c's handle_mbox_expunge() for the - * compaction algorithm that produces those numbers. + * RFC 9051 SS6.4.3 EXPUNGE removes all \Deleted messages, one untagged + * EXPUNGE per removal before the tagged OK. Per SS7.5.1's "sequence + * number immediately decremented by 1" rule, this server removes + * lowest-numbered first (a "lower to higher" server), matching SS6.4.3's + * own worked example; see store.c's handle_mbox_expunge() for the + * compaction algorithm. * - * silent exists so CLOSE (RFC 9051 SS6.4.1: "permanently removes all - * messages that have the \Deleted flag set ... No untagged EXPUNGE - * responses are sent") can reuse this exact request/reply pair instead of - * inventing a second one -- the same ".SILENT"-suffix pattern STORE - * already uses for the same "same operation, client doesn't want the - * per-message notifications" reason. + * silent lets CLOSE (SS6.4.1: no untagged EXPUNGE responses) reuse this + * request/reply pair, same ".SILENT" pattern as STORE. */ struct imsg_mbox_expunge { int silent; /* 1 for CLOSE, 0 for a real EXPUNGE command */ - /* - * RFC 9051 SS6.4.9's second UID command form: "the UID command takes - * an EXPUNGE command with an extra parameter that specifies a - * sequence set of UIDs to operate on... permanently removes all - * messages that have both the \Deleted flag set and a UID that is - * included in the specified sequence set... If a message either does - * not have the \Deleted flag set or has a UID that is not included in - * the specified sequence set, it is not affected." by_uid is never - * set together with silent=1 -- CLOSE has no UID-restricted form - * (there is no "UID CLOSE"), only plain EXPUNGE does. seq_lo/seq_hi/ - * lo_is_star/hi_is_star mirror imsg_mbox_fetch's own naming exactly - * (same seq-range/UID-range resolution convention throughout this - * header); meaningless when !by_uid, since a plain EXPUNGE takes no - * arguments at all (RFC 9051 SS6.4.3: "Arguments: none"). + /* RFC 9051 SS6.4.9's UID EXPUNGE form: only \Deleted messages whose + * UID is in [seq_lo, seq_hi] are removed. Never set together with + * silent=1 (no "UID CLOSE"). seq_lo/seq_hi/lo_is_star/hi_is_star + * mirror imsg_mbox_fetch's naming; meaningless when !by_uid (plain + * EXPUNGE takes no arguments). */ int by_uid; uint32_t seq_lo; @@ -1733,45 +827,29 @@ struct imsg_mbox_expunge { }; struct imsg_mbox_expunged { - uint32_t seqno; /* the message's sequence number at the moment - * of removal, per SS7.5.1's "immediately - * decremented" rule -- NOT its UID, and NOT - * its pre-EXPUNGE sequence number */ - uint32_t uid; /* RFC 7162 addition: the same message's UID, - * needed once QRESYNC is enabled -- SS3.2.10.2 - * replaces the untagged EXPUNGE (seqno-based) - * response with VANISHED (UID-based) in that - * case, and listener.c has no other way to - * learn the removed message's UID (the index - * line is already gone by the time this imsg - * is sent -- see store.c's handle_mbox_ - * expunge()) */ + uint32_t seqno; /* sequence number at the moment of removal + * (SS7.5.1's "immediately decremented" + * rule), not the UID, not the + * pre-EXPUNGE sequence number */ + uint32_t uid; /* RFC 7162: same message's UID, needed once + * QRESYNC is enabled (SS3.2.10.2 replaces + * EXPUNGE with VANISHED in that case); the + * index line is already gone by the time + * this is sent, so there's no other way to + * learn it */ }; /* - * IMSG_MBOX_COPY (listener -> store, RFC 9051 SS6.4.7 COPY) / IMSG_MBOX_MOVE - * (listener -> store, SS6.4.8 MOVE) share this exact request shape -- same - * "one struct, two message types, differ only in which store.c handler - * fires" pattern imsg_mbox_expunge already established for EXPUNGE/CLOSE - * (distinguished there by .silent; here by which IMSG_MBOX_* type arrived). + * IMSG_MBOX_COPY (RFC 9051 SS6.4.7) / IMSG_MBOX_MOVE (SS6.4.8) share this + * request shape, distinguished by which IMSG_MBOX_* type arrived (same + * pattern as EXPUNGE/CLOSE's .silent). * - * destname: the destination mailbox, resolved and validated by listener.c's - * copy_move_dispatch() exactly the way cmd_rename()'s oldname/newname - * already are -- mailbox_name_is_inbox()/mailbox_name_valid() first, with - * zero store round trip for a syntactically invalid name. Before flat - * multi-mailbox support (RFC 9051 SS6.3.4-SS6.3.6) this struct had no - * destination field at all: v1 had - * no CREATE, so the destination was *always* the same mailbox as the - * source (the currently selected mailbox, itself always INBOX) -- - * explicitly RFC-sanctioned even then (SS6.4.8: "moving a message to the - * currently selected mailbox... is allowed when copying the message to the - * currently selected mailbox is allowed"). Now that named mailboxes are - * real, a genuinely different destination is real too; store.c's handle_ - * mbox_copy()/handle_mbox_move() still fast-path the "destname names the - * mailbox already selected" case as a single-index operation identical to - * that original v1 code, and only take the new two-index cross-mailbox - * path (see those functions' own header comments, including the lock- - * ordering discussion) when it genuinely differs. + * destname is validated by listener.c's copy_move_dispatch() the same + * way cmd_rename()'s oldname/newname are. store.c's handle_mbox_copy()/ + * handle_mbox_move() fast-path destname == the already-selected mailbox + * as a single-index operation, and only take the cross-mailbox two-index + * path (see those functions' own comments, including lock ordering) when + * it genuinely differs. */ struct imsg_mbox_copy { int by_uid; @@ -1784,29 +862,17 @@ struct imsg_mbox_copy { /* * IMSG_MBOX_COPY_MAPPING (store -> listener, zero or more, before the - * terminal IMSG_MBOX_RESULT): one message's COPYUID mapping -- RFC 9051 - * SS7.1's COPYUID response code carries "a UID set containing the UIDs of - * the message(s) in the source mailbox that were copied... followed by - * another UID set containing the UIDs assigned... in the destination - * mailbox... in the order the message(s) was copied." Streamed one pair - * per message, ascending, rather than pre-compacted into ranges here -- - * matching this header's own established "store streams raw values, - * listener compacts into ranges at formatting time" split (format_seq_ - * list() already does exactly this for ESEARCH/MODIFIED); listener.c - * accumulates src_uid/dest_uid into two parallel growable arrays and - * range-compacts each independently once the terminal reply arrives. + * terminal IMSG_MBOX_RESULT): one message's COPYUID mapping (RFC 9051 + * SS7.1). Streamed one pair per message, ascending; listener.c + * accumulates src_uid/dest_uid into parallel arrays and range-compacts + * each once the terminal reply arrives, same "store streams raw values, + * listener compacts" split as ESEARCH/MODIFIED. * * Used for both COPY and MOVE. For MOVE, listener.c also buffers each - * IMSG_MBOX_EXPUNGED that arrives during the same round trip (rather than - * writing it to the client immediately, which is what happens for a real - * EXPUNGE/CLOSE) and flushes both buffers, in order, only once the - * terminal reply arrives -- COPYUID first, then EXPUNGE/VANISHED -- per - * SS6.4.8: "servers are also REQUIRED to send the COPYUID response code in - * an untagged OK before sending EXPUNGE". This mirrors session_handle_ - * mbox_selected()'s existing vanished_ranges/qresync_fetches dual-buffer- - * then-flush-in-fixed-order pattern for a QRESYNC SELECT resync -- same - * underlying problem (control final wire order across two streamed - * sub-types that arrive interleaved with other traffic), same solution. + * IMSG_MBOX_EXPUNGED from the same round trip and flushes both buffers + * in order. COPYUID first, then EXPUNGE/VANISHED, per SS6.4.8's + * requirement to send COPYUID before EXPUNGE. Mirrors session_handle_ + * mbox_selected()'s dual-buffer-then-flush pattern for QRESYNC resync. */ struct imsg_mbox_copy_mapping { uint32_t src_uid; @@ -1815,146 +881,70 @@ struct imsg_mbox_copy_mapping { /* * IMSG_MBOX_APPEND (listener -> store) / IMSG_MBOX_APPENDED (store -> - * listener, exactly once). APPEND handles exactly one message per - * request in v1 (no [MULTIAPPEND]), so unlike FETCH/STORE/EXPUNGE there's - * no per-message streaming reply -- one request, one reply. + * listener, exactly once). * - * RFC 9051 SS6.3.12: `append = "APPEND" SP mailbox [SP flag-list] [SP - * date-time] SP literal`. The literal -- the message body itself -- is - * carried as variable-length trailing data appended directly after this - * fixed struct within the SAME imsg, not as a separate message and not - * fd-passed. Verified directly against the real imsg.c/imsg-buffer.c this - * session: imsg_get_data() requires an *exact* length match (its own - * source: `if (ibuf_size(imsg->buf) != len) { errno = EBADMSG; return - * (-1); }`), so it cannot be used to read a fixed header out of a longer - * imsg. imsg_get_buf() is the sequential-read alternative (no length - * check, just `ibuf_get()`, which advances the ibuf's internal read - * position); imsg_get_len() reflects bytes *remaining*, not total size, - * because it calls ibuf_size(), which is literally `wpos - rpos`. So - * store.c's handle_mbox_append() reads this struct with imsg_get_buf(), - * then treats whatever imsg_get_len() reports afterward as the message - * body length and reads that with a second imsg_get_buf() call. + * RFC 9051 SS6.3.12 append literal (the message body) is carried as + * variable-length trailing data on the SAME imsg after this fixed + * struct, not fd-passed: store.c's handle_mbox_append() reads the + * struct with imsg_get_buf(), then reads whatever imsg_get_len() + * reports afterward as the body (imsg_get_data() can't be used here, + * it requires an exact length match against the whole imsg). * - * This one-message-one-imsg design only works because the message is - * capped at APPEND_LITERAL_MAX (listener.c) to fit comfortably under - * MAX_IMSGSIZE (16384, imsg.h) alongside this struct's own ~550 bytes -- - * see APPEND_LITERAL_MAX's comment in listener.c for the exact arithmetic - * and the imsg_create() source check ("datalen += IMSG_HEADER_SIZE; if - * (datalen > imsgbuf->maxsize) ... return NULL") it's based on. A message - * larger than that needs real fd-passing -- the same mechanism BODY[] - * FETCH still needs and doesn't have -- not implemented this pass; - * listener.c rejects an oversized literal announcement with a plain NO - * (RFC 9051 defines no response code for a size cap) before ever reading - * it, rather than truncating or crashing. + * Only works because the message is capped at APPEND_LITERAL_MAX to + * fit under imsg's MAX_IMSGSIZE (16384) alongside this struct. A larger + * message needs real fd-passing, not implemented; listener.c rejects an + * oversized literal announcement with a plain NO before reading it. */ struct imsg_mbox_append { char mailbox[MBOX_NAME_MAX]; - uint32_t sysflags; /* MBOX_FLAG_* bitmask -- an omitted or - * empty "()" flag-list both mean 0, - * per SS6.3.12: "otherwise the flag - * list of the resulting message is - * set to 'empty' by default" */ + uint32_t sysflags; /* MBOX_FLAG_* bitmask; an omitted or + * empty flag-list means 0 (SS6.3.12) */ char keywords[MBOX_FLAGS_MAX]; /* comma-separated, same * convention as imsg_mbox_store's */ - int has_date; /* 0 -- SS6.3.12: "otherwise the - * internal date... is set to the - * current date and time" -- store.c - * uses time(NULL) at delivery time - * in that case */ + int has_date; /* 0 = use current time at delivery + * (SS6.3.12) */ int64_t date; /* Unix timestamp; meaningful only if * has_date */ uint32_t msglen; /* length of the trailing message - * bytes -- redundant with what - * imsg_get_len() reports after the - * header is read, kept anyway as an - * explicit value store.c cross-checks - * against that, rather than trusting - * a single source for something this - * consequential (a mismatch likely - * means a build-time struct-layout - * skew between listener and store, - * or a truncated imsg). */ + * bytes; redundant with imsg_get_len() + * after the header is read, kept as + * an explicit cross-check against + * struct-layout skew or truncation */ }; struct imsg_mbox_appended { - enum mbox_op_error error; /* MBOX_OP_ERR_NO_SUCH_MAILBOX - * distinguishes "not INBOX" -- - * listener.c must send the tagged NO - * with a "[TRYCREATE]" prefix per - * SS6.3.12 -- from any other failure - * (MBOX_OP_ERR_GENERIC: plain NO, no - * response code) */ + enum mbox_op_error error; /* MBOX_OP_ERR_NO_SUCH_MAILBOX, "not + * INBOX", tagged NO gets [TRYCREATE] + * per SS6.3.12; MBOX_OP_ERR_GENERIC, + * plain NO */ uint32_t uidvalidity; - uint32_t uid; /* the appended message's own UID -- - * together with uidvalidity, this is - * SS7.1's APPENDUID response code */ + uint32_t uid; /* appended message's UID; with + * uidvalidity, this is SS7.1's + * APPENDUID response code */ uint32_t exists; /* mailbox's new total message count, - * so listener.c can send SS6.3.12's - * "SHOULD notify the client - * immediately via an untagged EXISTS - * response" -- sent only if this - * session currently has the mailbox - * selected; see cmd_append()'s and - * session_handle_mbox_appended()'s - * comments */ + * for SS6.3.12's untagged EXISTS + * notification, sent only if this + * session has the mailbox selected */ }; /* * IMSG_MBOX_SEARCH (listener -> store) / IMSG_MBOX_SEARCH_MATCH (store -> - * listener, one per matching message, streamed in ascending sequence - * order -- same per-message streaming shape as IMSG_MBOX_FETCH_META and - * IMSG_MBOX_EXPUNGED) / IMSG_MBOX_RESULT (store -> listener, exactly - * once, terminal -- the same generic completion signal FETCH/STORE/ - * EXPUNGE/APPEND already reuse; `count` is the number of matches - * streamed, which listener.c uses directly as COUNT if requested). + * listener, one per match, ascending sequence order) / IMSG_MBOX_RESULT + * (store -> listener, terminal; count is the number of matches, used + * directly as COUNT if requested). * - * RFC 9051 SS6.4.4 SEARCH's `search-key` grammar nests arbitrarily (NOT - * wraps one key, OR takes two, a parenthesized list ANDs N of them), so - * the parsed criteria can't be a single fixed-size struct the way - * STORE's flag-list or FETCH's fetch-att bitmask can. Instead, - * listener.c's parse_search_key()/parse_search_key_list() compile the - * whole search-program into a flat postfix (reverse Polish) array of - * struct search_node, sent as variable-length trailing data on this - * imsg after a small fixed header -- the same imsg_get_buf()/imsg_get_ - * len() wire technique IMSG_MBOX_APPEND already established and had - * verified against the real imsg-buffer.c this project (see that - * struct's own comment above): the header's nnodes times sizeof(struct - * search_node) tells store_dispatch() exactly how many trailing bytes - * to expect. SEARCH_PROGRAM_MAX_NODES bounds both the array's wire size - * (comfortably under imsg's MAX_IMSGSIZE even with every node using its - * full fixed-size keyword field) and store.c's postfix-evaluation stack - * depth -- generous for a hand-typed v1 query, not a hard IMAP-mandated - * ceiling; listener.c rejects a query that compiles to more nodes than - * this with a plain BAD ("search criteria too complex"), the same "cap - * and refuse cleanly" choice APPEND makes for an oversized message. - * - * v1 scope, matching this project's smaller-feature-set design - * philosophy: search keys that need actual message content or headers - * -- BCC/BODY/CC/FROM/HEADER/SENTBEFORE/ - * SENTON/SENTSINCE/SUBJECT/TEXT/TO -- are rejected by listener.c's - * parser with a specific NO before this imsg is ever built, the same - * "recognized, can't do it right now" category FETCH's BODY[] rejection - * already uses; store.c never sees a SEARCH_OP_* for any of them because - * none exists. The "UID SEARCH" command-level wrapper (RFC 9051 - * SS6.4.9's generic UID prefix, which would make ESEARCH's data refer to - * UIDs instead of sequence numbers) is also out of scope this pass -- - * IMSG_MBOX_SEARCH_MATCH always carries both seqno and uid, but - * listener.c's v1 ESEARCH formatter only ever uses seqno. The "UID - * " *search key* (filtering by UID range, one of many - * possible criteria within an ordinary sequence-number-space SEARCH) is - * unrelated to that command-level wrapper and is fully supported - * (SEARCH_OP_UIDSET) -- SS6.4.4's own example list includes it as a - * plain search key, not as a UID-command variant. - * - * Exactly one sequence-set range per SEQSET/UIDSET node (no internal - * comma-separated list) -- the same v1 restriction already established - * for FETCH/STORE's own sequence-set argument (see imsg_mbox_fetch's - * comment above); listener.c rejects a comma inside a SEARCH sequence- - * set token before ever building a node for it. + * RFC 9051 SS6.4.4 SEARCH's search-key grammar nests arbitrarily, so it + * can't be a single fixed-size struct. listener.c's parse_search_key()/ + * parse_search_key_list() compile the whole search-program into a flat + * postfix array of struct search_node, sent as variable-length trailing + * data after a small fixed header, same imsg_get_buf()/imsg_get_len() + * technique as IMSG_MBOX_APPEND. SEARCH_PROGRAM_MAX_NODES bounds both + * wire size and evaluation stack depth; an oversized query gets a plain + * BAD. */ #define SEARCH_PROGRAM_MAX_NODES 100 #define SEARCH_KEYWORD_MAX 64 /* one flag-keyword atom, not - * a list -- MBOX_FLAGS_MAX is + * a list, MBOX_FLAGS_MAX is * sized for STORE's comma- * joined keyword *list* and * would be the wrong constant @@ -1986,38 +976,23 @@ struct imsg_mbox_appended { #define SEARCH_OP_AND 20 /* postfix binary combinator */ #define SEARCH_OP_OR 21 /* postfix binary combinator */ #define SEARCH_OP_NOT 22 /* postfix unary combinator */ -#define SEARCH_OP_MODSEQ 23 /* RFC 7162 SS3.1.5 -- operand: num - * (mod-sequence-valzer threshold, - * message matches if its own - * mod-sequence is >= this). The +#define SEARCH_OP_MODSEQ 23 /* RFC 7162 SS3.1.5, operand: num, + * matches if the message's own + * mod-sequence is >= this. The * optional / prefix (SS3.1.5: "If the server - * doesn't store separate mod-sequences - * for different metadata items, it - * MUST ignore and ") is parsed by listener.c's - * parse_search_key() for syntax only - * and never reaches this struct -- v1's - * per-message mod-sequence (see store.c's - * struct mbox_index comment) is exactly - * the "doesn't store separate mod- - * sequences per metadata item" case the - * RFC anticipates, so there's nothing - * for store.c to narrow by */ + * req> prefix is parsed by + * listener.c for syntax only and + * never reaches this struct */ struct search_node { int op; /* SEARCH_OP_* above */ int64_t num; /* BEFORE/ON/SINCE/LARGER/SMALLER/MODSEQ * operand */ - uint32_t seq_lo; /* SEQSET/UIDSET operand -- see - * imsg_mbox_fetch's seq_lo/seq_hi/ - * lo_is_star/hi_is_star comment for - * the "*" resolution convention this - * mirrors; store.c resolves it here - * against its own live idx.nlines - * (SEQSET) or highest in-use UID - * (UIDSET) up front, once, before - * scanning messages */ + uint32_t seq_lo; /* SEQSET/UIDSET operand, same "*" + * convention as imsg_mbox_fetch; + * store.c resolves it against live + * idx.nlines (SEQSET) or highest + * in-use UID (UIDSET) up front */ uint32_t seq_hi; int lo_is_star; int hi_is_star; @@ -2033,54 +1008,29 @@ struct imsg_mbox_search { struct imsg_mbox_search_match { uint32_t seqno; uint32_t uid; - uint64_t modseq; /* RFC 7162 SS3.1.6: "If a client - * specifies a MODSEQ criterion in a - * SEARCH ... command and the server - * returns a non-empty SEARCH result, - * the server MUST also append ... the - * highest mod-sequence for all messages - * being returned." Always populated - * (cheap, store.c already has it - * per-message) -- listener.c only - * tracks the running max across - * matches when the client's search - * program actually used SEARCH_OP_ - * MODSEQ, same "store always computes, - * listener decides whether to use it" - * split as imsg_mbox_fetch_meta.modseq */ + uint64_t modseq; /* RFC 7162 SS3.1.6: highest + * mod-sequence among returned + * matches, required whenever the + * client used a MODSEQ criterion. + * Always populated (cheap); + * listener.c tracks the running max + * only when SEARCH_OP_MODSEQ was + * actually used. */ }; /* * RFC 9051 SS6.3.13 (IDLE). IMSG_MBOX_IDLE_REFRESH (listener -> store, no - * payload) / IMSG_MBOX_IDLE_UID (store -> listener, one per currently- - * existing message, in ascending UID order) / IMSG_MBOX_IDLE_REFRESHED - * (store -> listener, exactly once, terminal). + * payload) / IMSG_MBOX_IDLE_UID (store -> listener, one per existing + * message, ascending UID order) / IMSG_MBOX_IDLE_REFRESHED (store -> + * listener, terminal). * - * Used two ways by listener.c: once, synchronously after "+ idling" is - * sent, purely to seed s->idle_known_uids with a baseline (no diffing or - * pushing yet, since there's nothing to diff against the first time); and - * again, any time session_notify_idle_peers() (triggered by another same- - * uid session's successful EXPUNGE/UID EXPUNGE/CLOSE-with-removal/APPEND/ - * MOVE) asks an idling session to recheck its mailbox. Both cases reuse - * the identical request/reply shape -- only what listener.c does with the - * result differs. store.c doesn't need to know or care which case this - * is; it just reports current, authoritative state, via the same refresh_ - * index() helper handle_mbox_select() itself uses (including that - * function's new/ directory scan for undiscovered message files), so an - * idle-refresh is exactly as fresh as a fresh SELECT would be -- including - * picking up mail placed directly in new/ by something other than this - * daemon's own APPEND, if an idle-refresh happens to run after it landed. - * - * v1 scope decision (confirmed with the user): EXISTS and EXPUNGE only. - * RFC 9051 SS6.3.13 says the server is merely "free to" send EXISTS/ - * EXPUNGE/FETCH while idling, none of the three is mandatory -- pushing - * unsolicited FETCH (e.g. for a flag changed by another session) is - * deferred to a follow-up pass, since it needs the same per-message - * mod-sequence diffing this server already has for QRESYNC, just re- - * triggered the same way, and RFC 9051 doesn't require it. That's why - * this reply carries only the UID list (enough for listener.c to compute - * seqno-based EXPUNGE lines and an EXISTS count) and not per-message - * flags/modseq. + * Used two ways by listener.c: once, synchronously after "+ idling", to + * seed s->idle_known_uids with a baseline; and again whenever + * session_notify_idle_peers() (triggered by another session's + * EXPUNGE/APPEND/MOVE) asks an idling session to recheck. Both reuse the + * same shape; store.c just reports current state via the same + * refresh_index() helper handle_mbox_select() uses, so an idle-refresh + * is as fresh as a fresh SELECT. */ struct imsg_mbox_idle_uid { uint32_t uid; @@ -2095,13 +1045,13 @@ struct imsg_mbox_idle_refreshed { }; /* - * RFC 9051 SS6.3.4/SS6.3.5 (CREATE/DELETE) and SS6.3.9 (LIST), this pass -- + * RFC 9051 SS6.3.4/SS6.3.5 (CREATE/DELETE) and SS6.3.9 (LIST), this pass, * flat, non-nested mailboxes as sibling subdirectories of the * session's own per-user maildir root; CREATE/DELETE/RENAME all reply with * the existing struct imsg_mbox_result, only "ok" meaningful). listener.c * has already validated the name syntactically (non-empty, not "INBOX", * no hierarchy-delimiter character, within MBOX_NAME_MAX) before either of - * these is ever sent -- store.c re-validates independently rather than + * these is ever sent, store.c re-validates independently rather than * trusting that, the same defense-in-depth every other mailbox-name- * carrying imsg in this file already gets across the privsep boundary. */ @@ -2115,10 +1065,9 @@ struct imsg_mbox_delete { /* * RFC 9051 SS6.3.6 (RENAME). oldname/newname, not "mailbox"/"destination", - * to avoid any confusion with imsg_mbox_copy's identically-purposed but - * differently-named destination field -- RENAME's two names are peers - * (both mailbox names in the same flat namespace), not a source-message- - * range-plus-destination-mailbox pairing the way COPY/MOVE's is. + * to avoid confusion with imsg_mbox_copy's destination field, RENAME's + * two names are peers in the same flat namespace, not a source-range- + * plus-destination pairing like COPY/MOVE. */ struct imsg_mbox_rename { char oldname[MBOX_NAME_MAX]; @@ -2126,15 +1075,10 @@ struct imsg_mbox_rename { }; /* - * RFC 9051 SS6.3.9 (LIST). One IMSG_MBOX_LIST_ITEM per real, on-disk - * mailbox subdirectory store.c finds under the session's maildir root - * (INBOX itself excluded -- see the IMSG_MBOX_LIST_ITEM enum comment - * above), in no particular guaranteed order; listener.c does its own - * wildcard matching (list_pattern_match()) against each name plus the - * literal "INBOX" it already handles locally. Terminal reply reuses struct - * imsg_mbox_result ("count" = number of names streamed, "ok" = 0 only on a - * real I/O error opening the maildir root itself, not on finding zero - * mailboxes -- an account with no named mailboxes yet is not a failure). + * RFC 9051 SS6.3.9 (LIST). One IMSG_MBOX_LIST_ITEM per on-disk mailbox + * subdirectory (INBOX excluded), unordered; listener.c does its own + * wildcard matching. Terminal reply reuses imsg_mbox_result ("ok" = 0 + * only on a real I/O error, not on finding zero mailboxes). */ struct imsg_mbox_list_item { char mailbox[MBOX_NAME_MAX]; @@ -2148,39 +1092,24 @@ int config_load(const char *, struct openimap_config int cmdline_symset(char *); /* parent.c */ -/* - * First argument is the config file path, threaded through from main.c's - * "conffile" local (default "/etc/imapd.conf" or -f's argument) -- added - * for SIGHUP reload support: parent.c's sighup_handler() needs to re-run - * config_load() against the exact same path main() originally used, and - * had no way to reach it before (main()'s "conffile" was a local, never - * passed down; parent.c only ever received the already-parsed struct). +/* Config file path, threaded from main.c's conffile local, so + * sighup_handler() can re-run config_load() against the same path. */ __dead void parent_main(const char *, int, char *[], struct openimap_config *); -/* - * listener.c / auth.c / store.c take no struct openimap_config * -- - * none of the three re-exec'd child roles read imapd.conf, and as of - * this pass none of them receive it as a stub parameter either (an - * earlier draft did, unused/misleadingly for listener and store, and - * actively wrong for auth -- see the imsg_listener_init/imsg_auth_init - * comment above). Each gets exactly the config it needs over its fd-3 - * channel from parent instead: IMSG_LISTENER_INIT, IMSG_AUTH_INIT, - * IMSG_STORE_INIT respectively. +/* listener.c / auth.c / store.c take no struct openimap_config *, each + * gets exactly the config it needs over its fd-3 channel instead: + * IMSG_LISTENER_INIT, IMSG_AUTH_INIT, IMSG_STORE_INIT respectively. */ __dead void listener_main(void); __dead void auth_main(void); __dead void store_main(void); -/* - * imsg helpers shared by all roles -- see each role's setup_proc(). The - * "handler" passed to imsgev_init() is the real libevent callback: it's - * expected to do its own imsgbuf_read()/imsg_get() loop, matching - * parent.c's parent_dispatch_child() as the reference shape. This mirrors - * the imsg_event_add()-style pattern common to OpenBSD privsep daemons - * (relayd, httpd) -- not itself quoted from any uploaded source file this - * session, flagged as a standard-pattern choice, not a sourced one. +/* imsg helpers shared by all roles. The "handler" passed to + * imsgev_init() is the libevent callback, expected to run its own + * imsgbuf_read()/imsg_get() loop (parent_dispatch_child() is the + * reference shape). */ void imsgev_init(struct imsgev *, int, void (*)(int, short, void *), void *); @@ -2188,15 +1117,9 @@ void imsgev_init_from_ibuf(struct imsgev *, const st void (*)(int, short, void *), void *); void imsgev_add(struct imsgev *); -/* - * Boot-time setup-loop helpers, sourced against smtpd.c's setup_proc() - * shape: a freshly exec'd child blocks reading fd 3 for zero or more - * IMSG_SETUP_PEER messages, then an IMSG_SETUP_DONE, and acks. v1's - * boot-time wiring is 1:1 (listener gets exactly one peer, auth gets - * exactly one) so setup_recv_one_peer() covers both; store's later, - * per-session peer wiring happens post-boot, through the normal event - * loop, not through these blocking helpers -- see store.c. - */ +/* Boot-time setup-loop helpers: a freshly exec'd child blocks reading + * fd 3 for zero or more IMSG_SETUP_PEER messages, then IMSG_SETUP_DONE, + * and acks. */ int setup_recv_one_peer(struct imsgbuf *); void setup_recv_done_and_ack(struct imsgbuf *); blob - 55cbf3e19a7d339b4fb4f479c379a596688464fb blob + 556721d06f0b054a849cf2b37d31b5a2697799e4 --- src/imsgev.c +++ src/imsgev.c @@ -4,7 +4,7 @@ * * 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 + * (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 @@ -24,7 +24,7 @@ */ /* - * imsgev.c -- shared wrapper around imsgbuf + event(3), used by + * imsgev.c, shared wrapper around imsgbuf + event(3), used by * parent.c, listener.c, auth.c, and store.c. */ blob - 2cb78dfc0e89505b92cf682de96fbeced91d8781 blob + 6d496e3ee37c7d87346833e35d43c6e96ce2e0d4 --- src/index.c +++ src/index.c @@ -14,7 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -/* index.c -- the maildir index file format: load/save/append, QRESYNC resync, and vanished-UID tracking. */ +/* index.c, the maildir index file format: load/save/append, QRESYNC resync, and vanished-UID tracking. */ #include #include @@ -200,7 +200,7 @@ index_parse_line(const char *line, struct index_rec *r return (-1); } } else { - /* no MODSEQ field -- pre-CONDSTORE line (struct index_rec backward-compatibility) */ + /* no MODSEQ field, pre-CONDSTORE line (struct index_rec backward-compatibility) */ if (strlcpy(rec->keywords, p, sizeof(rec->keywords)) >= sizeof(rec->keywords)) { log_warnx("session %u: keywords too long in index " @@ -228,7 +228,7 @@ index_max_uid(struct mbox_index *idx) errno = 0; v = (uint32_t)strtoul(line, &ep, 10); if (*ep != ':') - return (0); /* corrupt last line -- treat as "no UIDs in use" rather than guessing */ + return (0); /* corrupt last line, treat as "no UIDs in use" rather than guessing */ return (v); } @@ -281,7 +281,7 @@ send_vanished_range(const struct mbox_index *idx, uint } } -/* Linear scan for a basename already in the index; O(n) per lookup, fine for v1's modest-mailbox-size scope. */ +/* Linear scan for a basename already in the index; O(n) per lookup, fine for modest-mailbox-size scope. */ int index_has_basename(struct mbox_index *idx, const char *basename) { @@ -435,15 +435,15 @@ qresync_send_resync(const struct imsg_mbox_select *req uid_hi = idx->uidnext - 1; } if (uid_hi < uid_lo) - return; /* empty requested range -- nothing to do */ + return; /* empty requested range, nothing to do */ - /* single forward pass; want tracks the lowest UID not yet accounted for -- a gap before it means vanished */ + /* single forward pass; want tracks the lowest UID not yet accounted for, a gap before it means vanished */ want = uid_lo; for (i = 0; i < idx->nlines && want <= uid_hi; i++) { struct index_rec rec; if (index_parse_line(idx->lines[i], &rec) == -1) - continue; /* corrupt line, already logged -- not reported either way */ + continue; /* corrupt line, already logged, not reported either way */ if (rec.uid < want) continue; /* below the requested range, or already accounted for */ if (rec.uid > uid_hi) @@ -511,15 +511,15 @@ refresh_index(struct mbox_index *idx, int fd) dp = opendir("new"); if (dp == NULL) { if (errno == ENOENT) - return (0); /* no new/ yet on a never-used mailbox -- not an error */ + return (0); /* no new/ yet on a never-used mailbox, not an error */ log_warn("session %u: opendir new", session_id); index_free(idx); return (-1); } while ((de = readdir(dp)) != NULL) { if (de->d_name[0] == '.') - continue; /* ".", "..", and dotfiles -- maildir delivery never creates the latter */ - /* never index a filename with ':' or newline -- would corrupt the index line format */ + continue; /* ".", "..", and dotfiles, maildir delivery never creates the latter */ + /* never index a filename with ':' or newline, would corrupt the index line format */ if (strpbrk(de->d_name, ":\r\n") != NULL) { log_warnx("session %u: skipping new/ file with unsafe " "name (contains ':' or newline): %s", session_id, blob - b418cd4079c0ac53d846b137d7487da81c37c0d0 blob + c4aa587b33bd334d3a01ebbe554cc1e058b1c0f4 --- src/listener.c +++ src/listener.c @@ -14,7 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -/* listener.c -- protocol/network process: client sockets, IMAP dispatch, TLS. */ +/* listener.c, protocol/network process: client sockets, IMAP dispatch, TLS. */ #include #include @@ -58,7 +58,7 @@ static struct event ev_accept_cleartext[LISTENER_MAX_ static struct event ev_accept_tls[LISTENER_MAX_ADDRS]; static struct tls_config *listener_tls_config; -struct tls *listener_tls_ctx; /* NULL if TLS setup failed -- degrades to no-TLS, not fatal */ +struct tls *listener_tls_ctx; /* NULL if TLS setup failed, degrades to no-TLS, not fatal */ /* Matches parent.c's send_tls_certs() read buffer size. */ #define TLS_CERT_MAX 8192 @@ -250,19 +250,19 @@ listener_main(void) setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1) fatal("cannot drop privileges to _imapd"); - /* Failure here isn't fatal -- degrades to no-TLS, checked via listener_tls_ctx == NULL below. */ + /* Failure here isn't fatal, degrades to no-TLS, checked via listener_tls_ctx == NULL below. */ if (cert_len == 0 || key_len == 0) { - log_warnx("listener: no TLS cert/key received -- TLS " + log_warnx("listener: no TLS cert/key received, TLS " "disabled for this run"); } else if ((listener_tls_config = tls_config_new()) == NULL) { - log_warnx("listener: tls_config_new failed -- TLS disabled"); + log_warnx("listener: tls_config_new failed, TLS disabled"); } else if ((listener_tls_ctx = tls_server()) == NULL) { - log_warnx("listener: tls_server failed -- TLS disabled"); + log_warnx("listener: tls_server failed, TLS disabled"); tls_config_free(listener_tls_config); listener_tls_config = NULL; } else if (tls_config_set_ciphers(listener_tls_config, "secure") != 0) { - log_warnx("listener: tls_config_set_ciphers: %s -- " + log_warnx("listener: tls_config_set_ciphers: %s, " "TLS disabled", tls_config_error(listener_tls_config)); tls_free(listener_tls_ctx); tls_config_free(listener_tls_config); @@ -271,7 +271,7 @@ listener_main(void) } else if (tls_config_set_keypair_mem(listener_tls_config, (const uint8_t *)cert_buf, cert_len, (const uint8_t *)key_buf, key_len) != 0) { - log_warnx("listener: tls_config_set_keypair_mem: %s -- " + log_warnx("listener: tls_config_set_keypair_mem: %s, " "TLS disabled", tls_config_error(listener_tls_config)); tls_free(listener_tls_ctx); tls_config_free(listener_tls_config); @@ -279,7 +279,7 @@ listener_main(void) listener_tls_config = NULL; } else if (tls_configure(listener_tls_ctx, listener_tls_config) != 0) { - log_warnx("listener: tls_configure: %s -- TLS disabled", + log_warnx("listener: tls_configure: %s, TLS disabled", tls_error(listener_tls_ctx)); tls_free(listener_tls_ctx); tls_config_free(listener_tls_config); @@ -295,7 +295,7 @@ listener_main(void) imsgev_init(&iev_auth, peer_fd, listener_dispatch_auth, NULL); - /* Reuses fd 3's populated ibuf3 -- a fresh imsgbuf_init() would drop buffered bytes. */ + /* Reuses fd 3's populated ibuf3, a fresh imsgbuf_init() would drop buffered bytes. */ imsgev_init_from_ibuf(&iev_parent, &ibuf3, listener_dispatch_parent, NULL); @@ -352,7 +352,7 @@ listener_accept(int fd, short event, void *arg) if (s->implicit_tls) { if (listener_tls_ctx == NULL) { log_warnx("session %u: implicit-TLS port, but TLS " - "isn't configured -- closing", s->id); + "isn't configured, closing", s->id); session_teardown(s); return; } @@ -528,7 +528,7 @@ session_dispatch_client(int fd, short event, void *arg if (s->inbuflen < 2) break; /* trailing CRLF hasn't arrived yet */ if (s->inbuf[0] != '\r' || s->inbuf[1] != '\n') { - /* no reliable resync point -- give up */ + /* no reliable resync point, give up */ log_warnx("session %u: expected CRLF after " "literal data, closing", s->id); session_teardown(s); @@ -574,9 +574,9 @@ session_dispatch_client(int fd, short event, void *arg alive = session_handle_line(s, s->inbuf); } if (alive == 0) - return; /* s was torn down (LOGOUT) -- do not touch */ + return; /* s was torn down (LOGOUT), do not touch */ - /* cmd_starttls() zeroes inbuflen to discard pipelined plaintext -- clamp to avoid underflow. */ + /* cmd_starttls() zeroes inbuflen to discard pipelined plaintext, clamp to avoid underflow. */ if (consumed > s->inbuflen) consumed = s->inbuflen; @@ -585,18 +585,18 @@ session_dispatch_client(int fd, short event, void *arg } if (s->inbuflen == sizeof(s->inbuf)) { - /* Buffer full, no CRLF -- matches the spirit of RFC 9051 SS7.1.3's example text. */ + /* Buffer full, no CRLF, matches the spirit of RFC 9051 SS7.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", s->id); - /* was a raw write(2) -- wrong on a TLS session, bytes would land unencrypted on the wire */ + /* was a raw write(2), wrong on a TLS session, bytes would land unencrypted on the wire */ session_write(s, bad, sizeof(bad) - 1); session_teardown(s); } } -/* Blocking write(2)/tls_write(): v1 simplification (short fixed responses); never tears s down on failure. */ +/* Blocking write(2)/tls_write(): never tears s down on failure. */ #define SESSION_WRITE_POLL_TIMEOUT_MS 5000 void @@ -710,14 +710,7 @@ session_reply(struct session *s, const char *tag, cons return; if ((size_t)len >= sizeof(buf)) { /* - * text can legitimately exceed this buffer (e.g. COPYUID's - * sequence-set text, up to ~4KB) -- snprintf(3) already - * truncated safely, but a bare truncation drops the trailing - * CRLF, and this project's clients are strictly line-based - * (RFC 9051 SS2.2.1). A response missing its terminator would - * merge with whatever comes next, corrupting every later - * response on this connection. Force the last two bytes back - * to CRLF rather than send a non-terminated line. + * Force the last two bytes back to CRLF rather than send a non-terminated line. */ buf[sizeof(buf) - 3] = '\r'; buf[sizeof(buf) - 2] = '\n'; @@ -806,7 +799,7 @@ parse_command_line(char *line, char **tag, char **name return (0); } -/* True only for the post-auth async-round-trip states; pre-auth states (AUTHENTICATING/STORE_PENDING) deliberately excluded -- see SESSION_CMD_QUEUE_MAX's comment. */ +/* True only for the post-auth async-round-trip states; pre-auth states (AUTHENTICATING/STORE_PENDING) deliberately excluded, see SESSION_CMD_QUEUE_MAX's comment. */ static int session_is_busy(const struct session *s) { @@ -829,7 +822,7 @@ session_is_busy(const struct session *s) } } -/* Appends a pipelined line to s->cmd_queue; returns 0 on a full queue or strdup(3) failure -- caller must teardown. */ +/* Appends a pipelined line to s->cmd_queue; returns 0 on a full queue or strdup(3) failure, caller must teardown. */ static int session_enqueue_cmd(struct session *s, const char *line) { @@ -868,7 +861,7 @@ session_dequeue_next(struct session *s) return (1); } -/* Returns 1 if the session is still alive, 0 if torn down (LOGOUT) -- caller must not touch *s* if 0. */ +/* Returns 1 if the session is still alive, 0 if torn down (LOGOUT), caller must not touch *s* if 0. */ int session_handle_line(struct session *s, char *line) { @@ -882,7 +875,7 @@ session_handle_line(struct session *s, char *line) return (1); } - /* Checked once here, not per strlcpy(3) site -- an overlong tag must not be echoed back truncated. */ + /* Checked once here, not per strlcpy(3) site, an overlong tag must not be echoed back truncated. */ if (strlen(tag) >= IMAP_TAG_MAX) { session_reply(s, "*", "BAD", "Tag too long"); return (1); @@ -901,7 +894,7 @@ session_handle_line(struct session *s, char *line) return (1); } if (!(imap_cmds[i].states & (1U << s->state))) { - /* RFC 9051 SS3: BAD or NO for wrong-state command -- BAD chosen here. */ + /* RFC 9051 SS3: BAD or NO for wrong-state command, BAD chosen here. */ session_reply(s, tag, "BAD", "Command not permitted in this state"); return (1); @@ -959,11 +952,7 @@ listener_dispatch_auth(int fd, short event, void *arg) "[AUTHENTICATIONFAILED] authentication failed"); break; } - /* task #321: parent spawns the store child off auth's - * own direct IMSG_AUTH_CRED now, not a request relayed - * from here -- this just tracks state while we wait - * for parent's IMSG_SETUP_PEER (success) or - * IMSG_STORE_FORK (failure). */ + s->uid = res.uid; /* session_notify_idle_peers() groups by this */ s->state = SESSION_STORE_PENDING; break; @@ -990,25 +979,25 @@ listener_reload_tls(const char *cert_buf, size_t cert_ struct tls_config *old_config; if (cert_len == 0 || key_len == 0) { - log_warnx("listener: SIGHUP reload: empty cert or key -- " + log_warnx("listener: SIGHUP reload: empty cert or key, " "keeping previous TLS configuration"); return; } if ((new_config = tls_config_new()) == NULL) { - log_warnx("listener: SIGHUP reload: tls_config_new failed -- " + log_warnx("listener: SIGHUP reload: tls_config_new failed, " "keeping previous TLS configuration"); return; } if ((new_ctx = tls_server()) == NULL) { - log_warnx("listener: SIGHUP reload: tls_server failed -- " + log_warnx("listener: SIGHUP reload: tls_server failed, " "keeping previous TLS configuration"); tls_config_free(new_config); return; } if (tls_config_set_ciphers(new_config, "secure") != 0) { log_warnx("listener: SIGHUP reload: tls_config_set_ciphers: " - "%s -- keeping previous TLS configuration", + "%s, keeping previous TLS configuration", tls_config_error(new_config)); tls_free(new_ctx); tls_config_free(new_config); @@ -1017,14 +1006,14 @@ listener_reload_tls(const char *cert_buf, size_t cert_ if (tls_config_set_keypair_mem(new_config, (const uint8_t *)cert_buf, cert_len, (const uint8_t *)key_buf, key_len) != 0) { log_warnx("listener: SIGHUP reload: tls_config_set_keypair_" - "mem: %s -- keeping previous TLS configuration", + "mem: %s, keeping previous TLS configuration", tls_config_error(new_config)); tls_free(new_ctx); tls_config_free(new_config); return; } if (tls_configure(new_ctx, new_config) != 0) { - log_warnx("listener: SIGHUP reload: tls_configure: %s -- " + log_warnx("listener: SIGHUP reload: tls_configure: %s, " "keeping previous TLS configuration", tls_error(new_ctx)); tls_free(new_ctx); tls_config_free(new_config); @@ -1229,7 +1218,7 @@ session_teardown(struct session *s) close(s->client_fd); } - /* All NULL-safe -- can still be set on a mid-stream teardown (FETCH/SEARCH/etc. in flight). */ + /* All NULL-safe, can still be set on a mid-stream teardown (FETCH/SEARCH/etc. in flight). */ free(s->literal_buf); free(s->search_matches); free(s->vanished_ranges); blob - 22356e4c4e6c46c8052d0c9f57aa0dff3df46b29 blob + d646dbe0c9832601d042c2b1c7c8a0123ace2182 --- src/listener.h +++ src/listener.h @@ -15,17 +15,9 @@ */ /* - * listener.h -- internal, listener-process-only shared declarations. - * - * Not installed, not part of imapd's public wire protocol (that's - * imapd.h) -- this exists solely so listener.c's core (the accept loop - * and session dispatch machinery) and the eight files split out of what - * used to be one 10,619-line listener.c (auth_cmd.c, mailbox_cmd.c, - * append_cmd.c, fetch_cmd.c, search_cmd.c, store_cmd.c, store_ipc.c, and - * listener.c itself) can all see struct session, enum session_state, - * and each other's entry points. Nothing in here crosses process - * boundaries or gets seen by auth.c/store.c/parent.c -- those live - * entirely behind imapd.h's imsg wire structs instead. + * listener.h, internal, listener-process-only shared declarations, so + * listener.c's split files can all see struct session, enum + * session_state, and each other's entry points. */ #ifndef LISTENER_H @@ -41,177 +33,106 @@ enum session_state { SESSION_NOT_AUTH, SESSION_AUTHENTICATING, /* IMSG_AUTH_REQUEST sent, awaiting reply */ - SESSION_STORE_PENDING, /* auth succeeded; awaiting parent's store-child handshake (task #321) */ + SESSION_STORE_PENDING, /* auth succeeded; awaiting parent's + * store-child handshake */ SESSION_AUTHENTICATED, - SESSION_SELECTING, /* IMSG_MBOX_SELECT sent, awaiting reply -- - * see cmd_select()/session_store_dispatch() */ + SESSION_SELECTING, /* IMSG_MBOX_SELECT sent, awaiting reply */ SESSION_SELECTED, SESSION_FETCHING, /* IMSG_MBOX_FETCH sent, awaiting the * IMSG_MBOX_FETCH_META stream + terminal - * IMSG_MBOX_RESULT -- see cmd_fetch()/ - * session_handle_mbox_result() */ - SESSION_STORING, /* IMSG_MBOX_STORE sent, awaiting the same - * IMSG_MBOX_FETCH_META stream + terminal - * IMSG_MBOX_RESULT shape SESSION_FETCHING - * uses -- see cmd_store_cmd()'s comment on - * why STORE reuses FETCH's reply types */ - SESSION_EXPUNGING, /* IMSG_MBOX_EXPUNGE sent (by EXPUNGE itself, - * or by CLOSE with silent=1), awaiting the - * IMSG_MBOX_EXPUNGED stream + terminal - * IMSG_MBOX_RESULT -- see cmd_expunge()/ - * cmd_close()/session_handle_mbox_result() */ + * IMSG_MBOX_RESULT */ + SESSION_STORING, /* IMSG_MBOX_STORE sent; reuses FETCH's + * reply shape (see cmd_store_cmd()) */ + SESSION_EXPUNGING, /* IMSG_MBOX_EXPUNGE sent (EXPUNGE, or CLOSE + * with silent=1), awaiting IMSG_MBOX_EXPUNGED + * stream + terminal IMSG_MBOX_RESULT */ SESSION_APPENDING, /* IMSG_MBOX_APPEND sent, awaiting the single - * terminal IMSG_MBOX_APPENDED reply -- see - * cmd_append()/session_finish_append()/ - * session_handle_mbox_appended(). Distinct - * from the *client-literal-read* phase that - * precedes it (s->literal_pending -- see that - * field's comment): this state only covers - * the store round trip, once the full - * message has already been read off the - * wire. */ + * terminal IMSG_MBOX_APPENDED reply. Distinct + * from the client-literal-read phase + * (s->literal_pending) that precedes it -- + * this covers only the store round trip. */ SESSION_SEARCHING, /* IMSG_MBOX_SEARCH sent, awaiting the * IMSG_MBOX_SEARCH_MATCH stream + terminal - * IMSG_MBOX_RESULT -- see cmd_search()/ - * session_finish_search(). Same per-match- - * stream-then-terminal-result reply shape as - * SESSION_FETCHING/STORING/EXPUNGING, so it's - * handled by the same session_handle_mbox_ - * result() entry point, which branches to - * session_finish_search() first. */ + * IMSG_MBOX_RESULT; handled by + * session_handle_mbox_result(), which + * branches to session_finish_search(). */ SESSION_STATUSING, /* IMSG_MBOX_STATUS sent, awaiting the single - * terminal IMSG_MBOX_STATUS_RESULT reply -- - * see cmd_status()/session_handle_mbox_ - * status_result(). Same single-request/single- - * reply shape as SESSION_SELECTING, but - * (unlike SELECT) never changes s->state's - * SELECTED-ness -- STATUS "does not change the - * currently selected mailbox" (RFC 9051 - * SS6.3.11) -- so s->status_prev_state records - * whichever ST_AUTH state was current when - * cmd_status() was called, for session_handle_ - * mbox_status_result() to restore. */ + * terminal IMSG_MBOX_STATUS_RESULT reply. + * Never changes s->state's SELECTED-ness + * (RFC 9051 SS6.3.11); s->status_prev_state + * records the state to restore. */ SESSION_COPYING, /* IMSG_MBOX_COPY or IMSG_MBOX_MOVE sent * (s->cmd_is_move says which), awaiting the * IMSG_MBOX_COPY_MAPPING stream (plus, for a * MOVE, an interleaved IMSG_MBOX_EXPUNGED - * stream -- see s->move_expunged's comment) + - * terminal IMSG_MBOX_RESULT -- see cmd_copy()/ - * cmd_move()/session_finish_copy_or_move(). - * Same per-message-stream-then-terminal-result - * shape as SESSION_FETCHING/STORING/EXPUNGING/ - * SEARCHING, so it's handled by the same - * session_handle_mbox_result() entry point, - * which branches to session_finish_copy_or_ - * move() first, the same way it already - * branches to session_finish_search(). */ + * stream) + terminal IMSG_MBOX_RESULT; + * branches to + * session_finish_copy_or_move(). */ /* * RFC 9051 SS6.3.4-SS6.3.6/SS6.3.9 additions (flat multi-mailbox - * support). All four are - * command-auth (valid in Authenticated or Selected state) and never - * change s->state's SELECTED-ness -- same "record whichever ST_AUTH - * state was current before, restore it after" pattern SESSION_ - * STATUSING's s->status_prev_state already established, reused here - * as s->mbox_op_prev_state (one shared field: only one of these four - * can ever be in flight for a given session at once, so there's no - * need for four separate fields the way SESSION_APPENDING has its - * own append_prev_state). + * support). All four are command-auth and never change s->state's + * SELECTED-ness; s->mbox_op_prev_state records the state to + * restore (one shared field, only one of these four is ever in + * flight per session). */ - SESSION_CREATING, /* IMSG_MBOX_CREATE sent, awaiting the single - * terminal IMSG_MBOX_RESULT reply -- see - * cmd_create()/session_finish_mbox_op(). */ - SESSION_DELETING, /* IMSG_MBOX_DELETE sent, same single-terminal- - * reply shape as SESSION_CREATING -- see - * cmd_delete()/session_finish_mbox_op(). */ - SESSION_RENAMING, /* IMSG_MBOX_RENAME sent, same single-terminal- - * reply shape as SESSION_CREATING -- see - * cmd_rename()/session_finish_mbox_op(). */ + SESSION_CREATING, /* IMSG_MBOX_CREATE sent, single terminal + * IMSG_MBOX_RESULT reply */ + SESSION_DELETING, /* IMSG_MBOX_DELETE sent, same shape as + * SESSION_CREATING */ + SESSION_RENAMING, /* IMSG_MBOX_RENAME sent, same shape as + * SESSION_CREATING */ SESSION_LISTING /* IMSG_MBOX_LIST sent, awaiting the * IMSG_MBOX_LIST_ITEM stream + terminal - * IMSG_MBOX_RESULT -- see list_dispatch()/ - * session_finish_list(). Same per-item-stream- - * then-terminal-result shape as SESSION_ - * SEARCHING/COPYING, so it's handled by the - * same session_handle_mbox_result() entry - * point too, branching to session_finish_ - * list() first. */ + * IMSG_MBOX_RESULT; branches to + * session_finish_list(). */ }; /* - * Generous line-length cap for the raw CRLF-delimited read buffer below. - * RFC 9051 doesn't mandate a specific limit -- SS7.1.3's "* BAD Command - * line too long" is example text, not a normative value -- but any real - * server needs one to bound memory for a client that never sends CRLF. - * Revisit once IMAP literals ({n}-prefixed octet counts, SS4.3) are - * implemented: those need a different, larger mechanism than a flat line - * buffer, since a literal's byte count is part of the command syntax - * itself, not just "a longer line". + * Line-length cap for the raw CRLF-delimited read buffer below. RFC 9051 + * doesn't mandate a specific limit, but any real server needs one to + * bound memory for a client that never sends CRLF. */ #define SESSION_INBUF_MAX 8192 /* - * Generous bound for a client-chosen tag we need to remember across an - * async IMSG_AUTH_REQUEST/IMSG_AUTH_RESULT (and, on success, IMSG_STORE_ - * FORK/IMSG_SETUP_PEER) round trip -- RFC 9051 SS9 doesn't specify a tag - * length limit (`tag = 1*`), so this is the - * same kind of deliberate, flagged simplification as session_reply()'s - * 512-byte response buffer: truncated via strlcpy() rather than rejected, - * matching this file's existing truncate-rather-than-overflow style. + * Bound for a client-chosen tag remembered across an async IMSG_AUTH_ + * REQUEST/IMSG_AUTH_RESULT round trip. RFC 9051 SS9 specifies no tag + * length limit; truncated via strlcpy() rather than rejected. */ #define IMAP_TAG_MAX 64 /* - * Bound on how many complete command lines a session may have pipelined - * ahead of the one currently awaiting an async store round trip (RFC 9051 - * SS5.5 permits a client to send further commands before a prior one's - * tagged response arrives, as long as the server processes them in order -- - * see session_enqueue_cmd()/session_dequeue_next() in listener.c). Deep - * enough for any real client (the iOS Mail bug this was written for only - * ever overlapped two: LIST then SELECT); a session that queues past this - * is either buggy or hostile, so it's disconnected rather than given - * unbounded memory -- same philosophy as SESSION_INBUF_MAX/IMAP_TAG_MAX - * above. Deliberately does NOT apply to the pre-authentication states - * (SESSION_AUTHENTICATING/STORE_PENDING) -- see session_is_busy()'s - * comment -- so an unauthenticated client gains no new memory-allocation - * surface from this queue at all. + * Bound on pipelined command lines ahead of the one awaiting an async + * store round trip (RFC 9051 SS5.5 permits pipelining as long as the + * server processes them in order). A session that queues past this is + * disconnected rather than given unbounded memory. Does NOT apply to + * the pre-authentication states (see session_is_busy()). */ #define SESSION_CMD_QUEUE_MAX 8 /* - * Cap on the verbatim label text (e.g. "HEADER.FIELDS (DATE FROM)" or - * "HEADER.FIELDS.NOT (X-SPAM-STATUS)") stashed on struct session's - * pending_header_label and echoed back into the FETCH response's - * "BODY[