commit - a96b2723a3021df065f3860ba0115f2bfea97764
commit + 845c36149fc11758db2f7d64937a70de2a56ed67
blob - d52e35c2e1f4e621ef4078d51583e7add8cb3d06
blob + 7f147c923fac828c5211a4b66dfef861d9307ab8
--- README.md
+++ README.md
# 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[<part>]`/`BODY.PEEK[<part>]`), `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 `<imsg.h>`, `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 `<imsg.h>`, `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
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
## 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 \
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
# 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
#
# 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
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
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
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"
*[!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
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
.\" $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
.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
# build on a non-OpenBSD host: it depends on <imsg.h>, 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 "<protocol>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 \
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 (<event.h>,
-# 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}
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,
# 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 <bsd.prog.mk>
blob - 0af51913734a344c0d44415b736ff248d1c5fd40
blob + 3e52fad9d3e1bff8f2aa3c844b32c0036673d4cb
--- src/append_cmd.c
+++ src/append_cmd.c
*/
/*
- * 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.
*/
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;
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);
}
}
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");
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");
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");
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
* 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 <sys/types.h>
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,
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)
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));
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;
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. */
(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)
{
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
*/
/*
- * 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.
*/
#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"
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);
/*
* 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)
}
}
- /*
- * 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));
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));
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");
/*
* 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
* 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 <sys/types.h>
#include <sys/file.h>
}
}
if (gt >= toklen)
- return (-1); /* unmatched '<' -- malformed, skip */
+ return (-1); /* unmatched '<', malformed, skip */
{
const char *disp = tok;
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);
}
*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
*/
/*
- * fetch_cmd.c -- FETCH: attribute/section-spec parsing and
+ * fetch_cmd.c, FETCH: attribute/section-spec parsing and
* response building.
*/
} 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[")) ==
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)) {
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)) {
if (attrs == 0) {
/* every requested item was unsupported, e.g. BODY[<part>] 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);
"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)
{
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;
}
return (0);
}
-/* RFC 9051 SS6.4.5 fetch + RFC 4466/7162 modifier list; plain BODY[...] and BODY[<part>] are a deliberate v1 scope cut */
+/* RFC 9051 SS6.4.5 fetch + RFC 4466/7162 modifier list; plain BODY[...] and BODY[<part>] are a deliberate scope cut */
int
cmd_fetch(struct session *s, const char *tag, char *args)
{
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);
}
log_debug("session %u: %s: one or more unsupported message "
"data items silently dropped (plain BODY[...]/BODY[<part>]"
"/BODY.PEEK[<part>] 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));
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)) >=
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
.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
.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
#
-# 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
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
#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"
#tls key "/etc/ssl/private/imapd.key"
# Largest message imapd will read from disk while deriving BODYSTRUCTURE
-# or a MIME part-addressed BODY[<part>] fetch -- see imapd(8). Must be
+# or a MIME part-addressed BODY[<part>] 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
*/
/*
- * 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
#include <imsg.h>
#include <stdint.h>
-/*
- * 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"
/*
};
/*
- * 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,
/* 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,
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,
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,
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,
* 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
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;
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 <addr> [tls] port <port>" x2, "spool <path>",
- * "credentials <path>", "tls certificate <path>", "tls key <path>",
- * "attachment max <bytes>"). 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 <addr>" 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];
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;
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 {
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 "<CMD> 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,
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:<maxuid>'" -- 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)
#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 */
/*
* 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 <UID set, mod-sequence> 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;
/*
* 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[<section-part>]/
- * BODY.PEEK[<section-part>] (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: <<partial>> byte-range support (SS6.4.5's
- * "<start.count>" 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[<section-part>] 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[<section-part>]
- * only -- same .PEEK-only
- * scoping as every other
- * MBOX_FETCH_BODY_* bit (plain
- * BODY[<section-part>], 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[<n>]/BODY.PEEK[<n>] 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[<section>]/BODY.PEEK[<section>]
- * 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 <<partial>> 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 <<partial>> ranged chunks instead (confirmed
- * against real Apple Mail traffic this pass, which already issues
- * BODY.PEEK[TEXT]<0.16384>-style ranged fetches unprompted). A
- * <<partial>> 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[]<<partial>> 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[<section>]/BODY.PEEK[<section>] 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 <<partial>> ranges rather than
+ * expecting a whole part in one response. A <<partial>> 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 <bytes>" 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 <bytes>"
+ * 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
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 <mod-sequence>." 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 <minmodseq> ... 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
- * <minmodseq> 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[<section-part>]/BODY.PEEK[<section-part>] (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[<section-part>]/BODY.PEEK[<section-part>]
+ * (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 <<partial>> range
- * (SS6.4.5's "<start.count>" 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 <<partial>>
+ * 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;
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
- * "<B27397-0100000@cac.washington.edu>")`, 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 ("* <seqno> 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)
#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;
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;
};
/*
* 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;
/*
* 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;
};
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;
/*
* 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;
/*
* 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
- * <sequence-set>" *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
#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 <entry-name>/<entry-type-
- * req> prefix (SS3.1.5: "If the server
- * doesn't store separate mod-sequences
- * for different metadata items, it
- * MUST ignore <entry-name> and <entry-
- * type-req>") 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;
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;
};
/*
- * 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.
*/
/*
* 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];
};
/*
- * 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];
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 *);
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
*
* 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
*/
/*
- * 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
* 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 <sys/types.h>
#include <sys/file.h>
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 "
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);
}
}
}
-/* 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)
{
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)
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
* 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 <sys/types.h>
#include <sys/queue.h>
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
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);
} 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);
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);
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);
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;
}
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);
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;
}
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
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';
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)
{
}
}
-/* 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)
{
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)
{
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);
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);
"[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;
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);
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);
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
*/
/*
- * 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
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*<any ASTRING-CHAR except "+">`), 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[<label>]" -- see that field's own comment for the full
- * reasoning. Defined up here (rather than down near parse_fetch_atts(),
- * where it's actually used) because struct session, just below,
- * declares a fixed array of this size -- needs to be visible before that
- * point. Never crosses the imsg boundary (listener.c has this text
- * straight from the client's own command line), so this is a
- * listener.c-local constant, not a shared imapd.h one like HEADER_
- * FIELDS_MAX. Sized generously above HEADER_FIELDS_MAX (256, imapd.h)
- * to cover the "HEADER.FIELDS.NOT (...)" wrapper text around whatever
- * field-name list already fits under that cap.
+ * Cap on the verbatim label text (e.g. "HEADER.FIELDS (DATE FROM)")
+ * stashed on struct session's pending_header_label and echoed back as
+ * "BODY[<label>]". Listener-local, since this text never crosses the
+ * imsg boundary. Sized above HEADER_FIELDS_MAX (256, imapd.h) to cover
+ * the wrapper text around the field-name list.
*/
#define HEADER_FIELDS_LABEL_MAX 288
/*
- * RFC 9051 SS6.4.4's SEARCH result options this server understands as
- * ESEARCH return items (SS7.3.4) -- SAVE (the "$" search result
- * variable, SS6.4.4.1) is recognized but rejected with a flagged NO; see
- * parse_search_return_opts()'s comment for why it's out of scope this
- * pass. Shared here (not file-local to search_cmd.c, where they're
- * mostly used) because store_cmd.c's session_finish_search() also tests
- * these bits when assembling the ESEARCH response.
+ * RFC 9051 SS6.4.4 SEARCH result options understood as ESEARCH return
+ * items (SS7.3.4). SAVE ("$", SS6.4.4.1) is recognized but rejected
+ * with a flagged NO. Shared here since store_cmd.c's
+ * session_finish_search() also tests these bits.
*/
#define SEARCH_RETURN_MIN (1U << 0)
#define SEARCH_RETURN_MAX (1U << 1)
#define SEARCH_RETURN_ALL (1U << 2)
#define SEARCH_RETURN_COUNT (1U << 3)
-/* One RFC 7162 VANISHED (EARLIER) range -- see struct session's vanished_
- * ranges comment and format_range_list(). Named (not anonymous) so format_
- * range_list() can take it as a parameter type. */
+/* One RFC 7162 VANISHED (EARLIER) range. Named so format_range_list()
+ * can take it as a parameter type. */
struct vanished_range {
uint32_t lo;
uint32_t hi;
struct event client_ev;
enum session_state state;
int implicit_tls; /* accepted on port 993, per
- * RFC 8314 -- vs. port 143,
+ * RFC 8314, vs. port 143,
* where TLS only starts after
* STARTTLS */
struct imsgev *store_iev; /* NULL until STORE_PENDING
* resolves */
int client_ev_added; /* client_ev is only safe to
* event_del() once this is
- * set -- see listener_accept()'s
+ * set, see listener_accept()'s
* implicit-TLS early-teardown
- * path, which never reaches
- * the event_set()/event_add()
- * call below it. */
+ * path */
int tls_active; /* 1 once a real TLS handshake
- * (implicit-TLS accept or
- * STARTTLS) has completed --
- * threaded through CAPABILITY's
- * state-appropriate response and
- * AUTHENTICATE's TLS gate. */
- struct tls *tls_ctx; /* NULL until tls_accept_socket()
- * -- non-NULL during handshake
- * too, not just once tls_active */
+ * (implicit-TLS or STARTTLS)
+ * has completed; gates
+ * CAPABILITY/AUTHENTICATE */
+ struct tls *tls_ctx; /* NULL until tls_accept_socket();
+ * non-NULL during handshake too */
int pending_greeting; /* implicit-TLS only: the
- * greeting can't be sent until
- * after the handshake completes
- * (RFC 8314 -- no protocol bytes
- * before TLS), so listener_accept()
- * defers it and session_tls_
- * handshake() sends it on success */
+ * greeting is deferred until
+ * the handshake completes (RFC
+ * 8314 forbids protocol bytes
+ * before TLS) */
char inbuf[SESSION_INBUF_MAX];
size_t inbuflen; /* bytes of unparsed input
* currently in inbuf */
int auth_cont; /* 1 while the next raw client
- * line is a SASL continuation-
- * response (base64 or "*"), not
- * a fresh tagged command -- set
- * by cmd_authenticate() when the
- * client sends bare "AUTHENTICATE
- * PLAIN" with no inline initial
- * response, cleared by session_
- * handle_auth_continuation() as
- * soon as that line arrives. */
+ * line is a SASL continuation
+ * response, not a fresh tagged
+ * command; set by
+ * cmd_authenticate(), cleared
+ * by session_handle_auth_
+ * continuation() */
int idling; /* 1 while the next raw client
- * line is IDLE's "DONE"
- * continuation, not a fresh
- * tagged command -- set by
- * cmd_idle(), cleared by
- * session_handle_idle_
- * continuation() (RFC 9051
- * SS6.3.13). Same shape as
- * auth_cont above, checked
- * second in session_dispatch_
- * client()'s read loop. */
+ * line is IDLE's "DONE" (RFC
+ * 9051 SS6.3.13); same shape as
+ * auth_cont, checked second in
+ * the read loop */
char *cmd_queue[SESSION_CMD_QUEUE_MAX]; /* malloc(3)'d
- * command lines the client
- * pipelined while session_is_
- * busy() was true -- e.g. iOS
- * Mail sending SELECT right
- * behind LIST, before LIST's
- * tagged OK arrives (RFC 9051
- * SS5.5 permits this). Entries
- * 0..cmd_queue_n-1 are valid,
- * always compacted (no holes);
- * session_enqueue_cmd() appends,
- * session_dequeue_next() pops
- * the front and shifts. Never
- * touched for auth_cont/idling
- * continuation lines -- those
- * bypass this queue entirely,
- * same as they bypass session_
- * handle_line()'s tag/name/args
- * parser. */
+ * command lines pipelined while
+ * session_is_busy() (RFC 9051
+ * SS5.5). Entries 0..cmd_queue_n-1
+ * are valid, always compacted;
+ * session_enqueue_cmd()/
+ * session_dequeue_next() manage
+ * it. Bypassed entirely for
+ * auth_cont/idling continuation
+ * lines. */
uint32_t cmd_queue_n; /* count of valid entries in
* cmd_queue, 0..SESSION_CMD_
* QUEUE_MAX */
- uid_t uid; /* set once, in session_request_
- * store(), from the same imsg_
- * auth_result the store-fork
- * request itself uses -- exists
- * purely so session_notify_
- * idle_peers() can find this
- * session's *other* concurrent
- * sessions (same user, v1 has
- * no shared mailboxes so that's
- * the only case that can ever
- * matter) without a second round
- * trip through auth just to ask
- * "whose session is this." */
+ uid_t uid; /* set once from the auth
+ * result; lets session_notify_
+ * idle_peers() find this
+ * session's other concurrent
+ * sessions (same user) without
+ * a second auth round trip */
char pending_tag[IMAP_TAG_MAX]; /* copy of whichever
* command's tag is waiting on an
- * async imsg round trip right now
- * (AUTHENTICATE -> auth, or SELECT
- * -> store) -- tag itself is a
- * pointer into inbuf, which gets
- * overwritten by the next read()
- * long before IMSG_AUTH_RESULT (or
- * the later IMSG_SETUP_PEER/
- * IMSG_STORE_FORK store-spawn
- * reply, or IMSG_MBOX_SELECTED)
- * comes back, so it has to be
- * saved out. Only ever one such
- * round trip in flight at a time
- * per session (SESSION_
- * AUTHENTICATING/STORE_PENDING/
- * SELECTING/FETCHING are all
- * mutually exclusive states), so
+ * async imsg round trip, the
+ * tag itself points into inbuf,
+ * which gets overwritten by the
+ * next read() before the reply
+ * arrives. Only one round trip is
+ * ever in flight per session, so
* one field is enough. */
uint32_t fetch_attrs; /* MBOX_FETCH_* bitmask for
- * the FETCH currently in flight
- * (SESSION_FETCHING) -- needed
- * because struct imsg_mbox_
- * fetch_meta's fields (uid,
- * size, ...) are always
- * populated by store regardless
- * of what was actually
- * requested, so session_send_
- * fetch_response() needs to
- * know which ones to print. */
+ * the in-flight FETCH, needed
+ * because imsg_mbox_fetch_meta's
+ * fields are always populated by
+ * store regardless of what was
+ * requested */
char *pending_header_buf; /* malloc(3)'d raw header
* bytes from the most recent
* IMSG_MBOX_FETCH_HEADER, held
- * here until the very next
- * IMSG_MBOX_FETCH_META (same
- * message, always sent
- * immediately after -- see that
- * struct's comment in
- * imapd.h) folds it into one
- * FETCH response line; freed and
- * NULL'd there. Never allocated
- * for more than one message at a
- * time -- store.c's ordering
- * contract means this is never
- * still set when the next
- * IMSG_MBOX_FETCH_HEADER
- * arrives, but session_teardown()
- * frees it defensively anyway in
- * case a FETCH is torn down
- * mid-stream. */
+ * until the following
+ * IMSG_MBOX_FETCH_META folds it
+ * into one FETCH response line;
+ * freed and NULL'd there.
+ * session_teardown() also frees
+ * it defensively. */
uint32_t pending_header_len; /* valid only alongside
* pending_header_buf != NULL */
- int pending_header_found; /* 0 -- this message's
- * header couldn't be read; 1
- * -- pending_header_buf/_len are
- * valid. Distinguishes "BODY[HEADER]
- * was requested but this message
- * has none to give" from "BODY[HEADER]
- * wasn't requested at all", the
- * latter never populating this or
- * pending_header_buf in the first
- * place. */
+ int pending_header_found; /* 0 = this message's
+ * header couldn't be read; 1 =
+ * pending_header_buf/_len valid.
+ * Distinguishes "requested but
+ * none to give" from "not
+ * requested". */
char pending_header_label[HEADER_FIELDS_LABEL_MAX];
- /* verbatim client-typed section-spec
- * text -- "HEADER" for plain BODY.PEEK
- * [HEADER], or e.g. "HEADER.FIELDS
- * (DATE FROM)" for that variant --
- * echoed back as-is in the FETCH
- * response's "BODY[<label>]" rather than
- * reconstructed, since RFC 9051 doesn't
- * mandate an exact echo format and
- * verbatim is simplest and trivially
- * correct. Set once in fetch_dispatch(),
- * read by session_send_fetch_response()
- * -- never needs its own free()/reset
- * since it's a fixed-size buffer, not a
- * malloc(3)'d pointer like pending_
- * header_buf. */
- char *pending_body_buf; /* same stash-until-
- * the-next-IMSG_MBOX_FETCH_META
- * shape as pending_header_buf above,
- * just for IMSG_MBOX_FETCH_BODY
- * (BODY.PEEK[]/BODY.PEEK[TEXT])
- * instead -- see struct imsg_mbox_
- * fetch_body's comment in
- * imapd.h. Freed and NULL'd by
+ /* verbatim client-typed section
+ * spec ("HEADER", or "HEADER.FIELDS
+ * (DATE FROM)"), echoed as-is in
+ * "BODY[<label>]" rather than
+ * reconstructed. Set once in
+ * fetch_dispatch(), read by
* session_send_fetch_response();
- * session_teardown() frees it
- * defensively too, same reasoning as
- * pending_header_buf. */
+ * fixed-size, no free needed. */
+ char *pending_body_buf; /* same stash-until-next-
+ * IMSG_MBOX_FETCH_META shape as
+ * pending_header_buf, for
+ * IMSG_MBOX_FETCH_BODY
+ * (BODY.PEEK[]/[TEXT]) */
uint32_t pending_body_len; /* valid only alongside
* pending_body_buf != NULL */
int pending_body_found; /* same found/not-found
* distinction as pending_header_
- * found, for BODY.PEEK[]/
- * BODY.PEEK[TEXT] */
+ * found */
char pending_body_label[SECTION_PART_MAX];
- /* verbatim section text for the
- * "BODY[<label>]" response --
- * "" for BODY.PEEK[] (whole
- * message), "TEXT" for BODY.PEEK
- * [TEXT], or the client-typed
- * dotted-numeric path (e.g. "3.1")
- * for BODY.PEEK[<section-part>] --
- * same "precompute the full
- * verbatim label at dispatch time"
- * idiom as pending_header_label,
- * unifying what used to be a
- * pending_body_is_text boolean
- * (adequate when only WHOLE/TEXT
- * existed, not once a third,
- * client-supplied-text variant --
- * PART -- joined them). Set once in
- * fetch_dispatch() from whichever of
- * MBOX_FETCH_BODY_WHOLE/_TEXT/_PART
- * attrs selects (same WHOLE > TEXT >
- * PART precedence store.c's
- * handle_mbox_fetch() applies --
- * see that function's comment),
- * read by session_send_fetch_
- * response(). */
+ /* verbatim section text for
+ * "BODY[<label>]", "" for
+ * whole message, "TEXT", or a
+ * dotted-numeric part path.
+ * Set once in fetch_dispatch()
+ * from whichever of WHOLE/TEXT/
+ * PART attrs selects (same
+ * precedence as store.c's
+ * handle_mbox_fetch()). */
int pending_body_has_partial; /* 1 if the
- * client's own BODY.PEEK[...]
- * token carried a <<start.count>>
- * suffix (RFC 9051 SS6.4.5) for
- * whichever of WHOLE/TEXT/PART was
- * actually selected -- see struct
- * imsg_mbox_fetch's has_partial
- * comment in imapd.h. Only
- * pending_body_partial_origin (the
- * *requested* start octet, not
- * store.c's own bodylen -- SS6.4.5:
- * "The origin octet facility MUST
- * NOT be used ... unless the client
- * specifically requested it", and
- * the response echoes only the
- * origin, never the count) is echoed
- * back; set alongside pending_body_
- * label in fetch_dispatch(). */
+ * client's BODY.PEEK[...] token
+ * carried a <<start.count>>
+ * suffix (RFC 9051 SS6.4.5). Only
+ * the origin is echoed back, never
+ * the count. */
uint32_t pending_body_partial_origin;
- char *pending_envelope_buf; /* same stash-
- * until-the-next-IMSG_MBOX_
- * FETCH_META shape as pending_
- * header_buf/pending_body_buf
- * above, for IMSG_MBOX_FETCH_
- * ENVELOPE -- see struct imsg_
- * mbox_fetch_envelope's comment
- * in imapd.h. Unlike those
- * two, holds already-formatted
- * "(...)" envelope response
- * text, not raw message bytes,
- * so session_send_fetch_
+ char *pending_envelope_buf; /* same stash-until-
+ * next shape as pending_header_buf,
+ * for IMSG_MBOX_FETCH_ENVELOPE.
+ * Holds already-formatted "(...)"
+ * text, so session_send_fetch_
* response() writes it directly
* rather than wrapping it in a
- * `{n}` literal. Freed and
- * NULL'd by session_send_fetch_
- * response(); session_teardown()
- * frees it defensively too, same
- * reasoning as pending_header_buf. */
+ * literal. */
uint32_t pending_envelope_len; /* valid only
* alongside pending_envelope_buf
* != NULL */
int pending_envelope_found; /* same found/
* not-found distinction as
- * pending_header_found, for
- * ENVELOPE */
- char *pending_bodystructure_buf; /* same stash-
- * until-the-next-IMSG_MBOX_
- * FETCH_META shape, same
- * already-formatted-text shape,
- * as pending_envelope_buf above,
- * for IMSG_MBOX_FETCH_
- * BODYSTRUCTURE -- see struct
- * imsg_mbox_fetch_bodystructure's
- * comment in imapd.h. */
+ * pending_header_found */
+ char *pending_bodystructure_buf; /* same shape as
+ * pending_envelope_buf, for
+ * IMSG_MBOX_FETCH_BODYSTRUCTURE */
uint32_t pending_bodystructure_len; /* valid only
* alongside pending_
* bodystructure_buf != NULL */
int pending_bodystructure_found; /* same found/
* not-found distinction as
- * pending_header_found, for
- * BODYSTRUCTURE */
- char pending_bodystructure_label[16]; /*
- * "BODY" or "BODYSTRUCTURE" --
- * RFC 9051 SS9's msg-att-static:
- * `"BODY" ["STRUCTURE"] SP body`
- * means the response label
- * itself echoes whichever bare
- * token the client used (both
- * set the same MBOX_FETCH_
- * BODYSTRUCTURE bit and produce
- * identical body text in this
- * implementation -- see that
- * bit's imapd.h comment --
- * but the label still has to
- * match what was asked for).
- * Set once in fetch_dispatch(),
- * read by session_send_fetch_
- * response() -- same fixed-
- * buffer, no-free-needed shape
- * as pending_header_label. */
+ * pending_header_found */
+ char pending_bodystructure_label[16]; /* "BODY"
+ * or "BODYSTRUCTURE", both set
+ * the same bit and produce
+ * identical text, but the
+ * response label must match what
+ * was asked for */
int close_after_expunge; /* 1 if the in-flight
* SESSION_EXPUNGING round trip
* was started by CLOSE, not a
- * real EXPUNGE command -- both
- * send the identical IMSG_MBOX_
- * EXPUNGE (CLOSE with silent=1)
- * and get back the identical
- * IMSG_MBOX_RESULT, so this is
- * the only way session_handle_
- * mbox_result() can tell which
- * tagged completion text/next
- * state applies: CLOSE goes to
- * SESSION_AUTHENTICATED, a real
- * EXPUNGE goes back to SESSION_
- * SELECTED. */
+ * real EXPUNGE, both send the
+ * identical imsg pair, so this is
+ * the only way to tell which next
+ * state applies: CLOSE ->
+ * SESSION_AUTHENTICATED, EXPUNGE
+ * -> SESSION_SELECTED */
int literal_pending; /* 1 while the next bytes off
* the wire are a client
* literal's raw octets (RFC 9051
* SS4.3), not a CRLF-delimited
- * line -- set by cmd_append()
- * when it finds a trailing
- * "{n}"/"{n+}" on the APPEND
- * command line, cleared once
- * literal_buf_len reaches
- * literal_len. Checked at the
- * very top of session_dispatch_
- * client()'s read loop, *before*
- * the normal CRLF search -- so
- * unlike every async-imsg-wait
- * state above, this needs no
- * ST_* dispatch-table exclusion:
- * while it's set, session_
+ * line, set by cmd_append() on
+ * a trailing "{n}"/"{n+}",
+ * cleared once literal_buf_len
+ * reaches literal_len. Checked at
+ * the top of session_dispatch_
+ * client()'s read loop, before the
+ * CRLF search, so no ST_* dispatch
+ * exclusion is needed: session_
* handle_line() is never reached
- * at all, regardless of s->state,
- * so no new complete "tag SP
- * command CRLF" can ever be
- * mis-parsed out of literal
- * bytes. */
+ * while it's set. */
char *literal_buf; /* malloc(3)'d accumulator for
* the literal currently being
* read, literal_len bytes, freed
- * once handed off to session_
- * finish_append() (or on early
- * teardown -- see session_
- * teardown()) */
+ * once handed to session_finish_
+ * append() (or on early teardown) */
uint64_t literal_len; /* total announced literal size
- * (what "{n}" both this and
- * literal_remaining start from) */
- uint64_t literal_remaining; /* bytes of the literal still
- * needed -- literal_len minus
- * however much has landed in
- * literal_buf so far, across
- * possibly many reads */
+ * ("{n}"), what literal_remaining
+ * starts from */
+ uint64_t literal_remaining; /* bytes of the literal
+ * still needed */
char append_mailbox[MBOX_NAME_MAX]; /* APPEND's
* arguments, parsed by
- * cmd_append() *before* the
- * literal is read (mailbox/
- * flags/date all precede the
- * literal in RFC 9051 SS6.3.12's
- * grammar) and held here across
- * the literal-read phase until
- * session_finish_append() needs
- * them to build IMSG_MBOX_
- * APPEND */
+ * cmd_append() before the literal
+ * is read, held here until
+ * session_finish_append() builds
+ * IMSG_MBOX_APPEND */
uint32_t append_sysflags;
char append_keywords[MBOX_FLAGS_MAX];
int append_has_date;
int64_t append_date;
- enum session_state append_prev_state; /* SESSION_AUTHENTICATED or
- * SESSION_SELECTED -- whichever
+ enum session_state append_prev_state; /* SESSION_AUTHENTICATED
+ * or SESSION_SELECTED, whichever
* s->state was when cmd_append()
- * was first called (APPEND is
- * valid in either, RFC 9051
- * command-auth), so session_
- * handle_mbox_appended() knows
- * which one to restore once
- * IMSG_MBOX_APPENDED arrives --
- * unlike FETCH/STORE/EXPUNGE,
- * which are only ever dispatched
- * from (and so only ever return
- * to) SESSION_SELECTED, APPEND's
- * "go back to" state isn't
- * fixed. */
+ * was called, so session_handle_
+ * mbox_appended() knows which to
+ * restore, unlike FETCH/STORE/
+ * EXPUNGE, APPEND's return state
+ * isn't fixed */
uint32_t status_attrs; /* STATUS_ATT_* bitmask for the
- * STATUS currently in flight
- * (SESSION_STATUSING) -- which
- * status-att values to include
- * in the response, in listener.c's
- * fixed canonical order; store.c
+ * in-flight STATUS, which
+ * values to include, in
+ * listener.c's fixed order; store.c
* always computes MESSAGES/
* UIDNEXT/UIDVALIDITY/
- * HIGHESTMODSEQ regardless (see
- * imapd.h's imsg_mbox_status
- * comment), same "store always
- * computes, listener decides
- * whether to print" split as
- * s->fetch_attrs. */
- char status_mailbox[MBOX_NAME_MAX]; /* the mailbox
- * name STATUS was asked about
- * (RFC 9051 SS6.3.4-SS6.3.6's
- * flat multi-mailbox support --
- * before that, STATUS could only
- * ever mean INBOX, so this field
- * didn't need to exist) --
- * stashed here so session_
- * handle_mbox_status_result() can
- * echo it back in the untagged
- * "* STATUS <mailbox> (...)"
- * response; store.c's own reply
- * (struct imsg_mbox_status_
- * result) has no mailbox field of
- * its own to read it back from,
- * since listener.c already knows
- * what it asked. */
- enum session_state status_prev_state; /* SESSION_AUTHENTICATED or
- * SESSION_SELECTED -- same
- * "STATUS is command-auth, valid
- * in either state" reasoning as
- * s->append_prev_state above, and
- * for the identical reason:
- * session_handle_mbox_status_
- * result() needs to know which
- * one to restore. */
- enum session_state mbox_op_prev_state; /* SESSION_AUTHENTICATED or
- * SESSION_SELECTED -- same
- * "record whichever ST_AUTH
- * state was current before,
- * restore it after" pattern as
- * s->status_prev_state just
- * above, shared across all four
- * of SESSION_CREATING/DELETING/
- * RENAMING/LISTING (see that enum
- * block's own comment) since only
- * one of the four can ever be in
- * flight at once for a given
- * session. session_finish_mbox_
- * op()/session_finish_list() both
- * restore it. */
+ * HIGHESTMODSEQ regardless */
+ char status_mailbox[MBOX_NAME_MAX]; /* mailbox
+ * name STATUS was asked about,
+ * stashed so session_handle_
+ * mbox_status_result() can echo
+ * it in the untagged response --
+ * store.c's reply has no mailbox
+ * field of its own */
+ enum session_state status_prev_state; /* SESSION_AUTHENTICATED
+ * or SESSION_SELECTED, same
+ * reasoning as append_prev_state */
+ enum session_state mbox_op_prev_state; /* SESSION_AUTHENTICATED
+ * or SESSION_SELECTED, shared
+ * across SESSION_CREATING/
+ * DELETING/RENAMING/LISTING
+ * (only one in flight at a time).
+ * session_finish_mbox_op()/
+ * session_finish_list() restore
+ * it. */
char list_pattern[2 * MBOX_NAME_MAX]; /* canonical
- * LIST/LSUB pattern (reference
- * concatenated with the mailbox
- * pattern by list_dispatch(),
- * same construction the old
- * synchronous 'canon' local
- * used) stashed across the
- * SESSION_LISTING round trip so
- * session_handle_mbox_list_
- * item() can test each IMSG_
- * MBOX_LIST_ITEM name against it
- * as it streams in, one at a
- * time -- unlike s->search_
- * matches, nothing needs to be
- * accumulated first, since each
- * match can be turned into its
- * untagged response
- * immediately. */
- int list_is_lsub; /* 1 if the in-flight SESSION_
- * LISTING round trip was started
- * by LSUB rather than LIST --
- * picks the untagged response
- * keyword and the tagged
- * completion text, same is_lsub
- * parameter list_dispatch()
- * already threads through
- * synchronously today. */
+ * LIST/LSUB pattern (reference +
+ * mailbox pattern, concatenated
+ * by list_dispatch()), tested
+ * against each IMSG_MBOX_LIST_
+ * ITEM name as it streams in --
+ * unlike search_matches, nothing
+ * needs accumulating first, since
+ * each match becomes its
+ * untagged response immediately */
+ int list_is_lsub; /* 1 if the in-flight LISTING
+ * round trip was started by LSUB
+ * rather than LIST, picks the
+ * untagged keyword and tagged
+ * completion text */
char rename_oldname[MBOX_NAME_MAX]; /* RENAME's
- * two arguments, stashed by
- * cmd_rename() across the
- * SESSION_RENAMING round trip
- * purely so session_finish_
- * mbox_op() can compare
- * rename_oldname against s->
- * selected_mailbox on success --
- * if this session had the
- * just-renamed mailbox itself
- * selected, its own s->selected_
- * mailbox needs to follow along
- * to rename_newname, the same
- * way store.c's handle_mbox_
- * rename() already makes its own
- * cwd/current_mailbox_dir follow
- * (see that function's own real-
- * hardware-bug comment). Neither
- * name is otherwise needed here
- * -- req.oldname/req.newname
- * already made the trip to
- * store.c in the imsg itself. */
+ * arguments, stashed so
+ * session_finish_mbox_op() can
+ * compare oldname against s->
+ * selected_mailbox: if this
+ * session had the renamed
+ * mailbox selected, it needs to
+ * follow along to newname */
char rename_newname[MBOX_NAME_MAX];
uint32_t search_return_opts; /* SEARCH_RETURN_* bitmask
- * for the SEARCH currently in
- * flight (SESSION_SEARCHING) --
- * which of MIN/MAX/ALL/COUNT
- * session_finish_search() should
+ * for the in-flight SEARCH --
+ * which of MIN/MAX/ALL/COUNT to
* include in the ESEARCH
- * response; store.c never needs
- * this, since it only computes
- * *which* messages match, not
- * how the client wants that
- * reported. */
+ * response; store.c doesn't need
+ * this, it only computes matches */
uint32_t *search_matches; /* malloc(3)'d/realloc(3)'d,
- * growable -- ascending sequence
- * numbers streamed in via
- * IMSG_MBOX_SEARCH_MATCH, kept
- * in full (not range-compacted
- * until formatting time) so
- * MIN/MAX/COUNT/ALL can all be
- * derived from one array. Freed
- * by session_finish_search() once
- * the ESEARCH response is sent,
- * or by session_teardown() on
- * early disconnect. */
+ * growable, ascending sequence
+ * numbers streamed via
+ * IMSG_MBOX_SEARCH_MATCH, kept in
+ * full (not range-compacted until
+ * formatting) so MIN/MAX/COUNT/ALL
+ * can all derive from one array.
+ * Freed by session_finish_search()
+ * or session_teardown(). */
uint32_t search_nmatches; /* entries actually in
* search_matches */
uint32_t search_matches_cap; /* allocated capacity of
* nmatches */
int search_alloc_failed; /* 1 if a realloc(3) in
* session_handle_mbox_search_
- * match() ever failed for this
- * SEARCH -- makes session_
- * finish_search() send a NO
- * instead of a silently
- * incomplete ESEARCH response;
- * see that function's comment on
- * why "some matches dropped" is
- * treated as a hard failure, not
- * a best-effort partial result. */
- int search_used_modseq; /* 1 if the SEARCH program
- * currently in flight
- * contained a MODSEQ
- * search-key -- RFC 7162
- * SS3.1.5/SS3.1.8: makes
- * this SEARCH a CONDSTORE-
- * enabling command, and
- * makes session_finish_
- * search() append "(MODSEQ
- * n)" using the running
- * max below. */
+ * match() ever failed, makes
+ * session_finish_search() send
+ * a NO instead of a silently
+ * incomplete ESEARCH response */
+ int search_used_modseq; /* 1 if the in-flight
+ * SEARCH program contained a
+ * MODSEQ search-key (RFC 7162
+ * SS3.1.5/SS3.1.8: a CONDSTORE-
+ * enabling command); makes
+ * session_finish_search() append
+ * "(MODSEQ n)" using the running
+ * max below */
uint64_t search_max_modseq; /* highest modseq seen
- * across all matches of
- * the SEARCH currently in
- * flight, tracked by
- * session_handle_mbox_
- * search_match() --
- * store.c always sends
- * modseq per match (cheap,
- * see imapd.h's struct
- * imsg_mbox_search_match
- * comment), so this is
- * just a running max, not
- * conditional on search_
- * used_modseq itself. */
+ * across matches of the
+ * in-flight SEARCH; store.c
+ * always sends modseq per match
+ * (cheap), so this is just a
+ * running max */
- /*
- * RFC 7162 (CONDSTORE/QRESYNC) session state, added this pass.
- * condstore_enabled/qresync_enabled are both sticky for the whole
- * connection once set (SS3.1: "Once a client issues a CONDSTORE
- * enabling command, it has announced itself... until the connection
- * is closed"; SS3.2.3 says the same for QRESYNC) -- never cleared
- * back to 0 anywhere in this file. qresync_enabled implies
- * condstore_enabled is also 1 (SS3.2.3: "the presence of the
- * 'QRESYNC' capability implies support for the CONDSTORE IMAP
- * extension"), enforced at every place qresync_enabled gets set.
+ /* RFC 7162 (CONDSTORE/QRESYNC) session state. condstore_enabled/
+ * qresync_enabled are sticky for the whole connection once set
+ * (SS3.1, SS3.2.3), never cleared. qresync_enabled implies
+ * condstore_enabled (SS3.2.3).
*/
int condstore_enabled;
int qresync_enabled;
- /*
- * Best-known HIGHESTMODSEQ of the currently selected mailbox --
- * cached from every IMSG_MBOX_SELECTED/IMSG_MBOX_RESULT reply that
- * carries one (store.c always populates it; see imapd.h's
- * imsg_mbox_selected/imsg_mbox_result comments), regardless of
- * whether this session is CONDSTORE-aware yet. Exists specifically
- * for session_condstore_enable()'s "CONDSTORE enabling command
- * issued while a mailbox is already selected" case (RFC 7162 SS3.1:
- * the server MUST emit an unsolicited HIGHESTMODSEQ OK response at
- * that moment) -- without a cached value there would be nothing to
- * report without an extra store round trip just for this edge case.
+ /* Best-known HIGHESTMODSEQ of the currently selected mailbox,
+ * cached from every reply that carries one, regardless of whether
+ * this session is CONDSTORE-aware yet. Exists for session_
+ * condstore_enable()'s "enabling command issued while a mailbox
+ * is already selected" case (RFC 7162 SS3.1 requires an
+ * unsolicited HIGHESTMODSEQ OK at that moment).
*/
uint64_t mbox_highestmodseq;
/*
- * RFC 9051 SS6.3.3: whether the currently selected mailbox was
- * opened via EXAMINE (1) rather than SELECT (0) -- meaningful only
- * while s->state == SESSION_SELECTED (and, transiently, SESSION_
- * SELECTING/SESSION_EXPUNGING while a select_or_examine()/session_
- * request_expunge() round trip is in flight). Set synchronously by
- * select_or_examine() before the IMSG_MBOX_SELECT round trip even
- * starts -- like s->state = SESSION_SELECTING itself, this doesn't
- * need to wait for store.c's reply, since it's derived purely from
- * which command the client typed, not from anything store.c knows.
- * Consulted by session_handle_mbox_selected() (READ-ONLY/READ-WRITE
- * response code, PERMANENTFLAGS) and by every command that would otherwise
- * mutate the selected mailbox's permanent state -- store_do(),
- * session_request_expunge(), copy_move_dispatch()'s is_move case --
- * to refuse (or, for CLOSE specifically, silently no-op) per SS6.3.3
- * ("No changes to the permanent state of the mailbox... are
- * permitted") and SS6.4.1's CLOSE exception.
+ * RFC 9051 SS6.3.3: whether the selected mailbox was opened via
+ * EXAMINE (1) rather than SELECT (0); meaningful while s->state ==
+ * SESSION_SELECTED (and transiently during the SELECT/EXPUNGE
+ * round trip). Set synchronously by select_or_examine() before the
+ * round trip starts, since it's derived from the client's command,
+ * not store.c's reply. Consulted for READ-ONLY/READ-WRITE/
+ * PERMANENTFLAGS and by every command that would mutate the
+ * mailbox's permanent state, to refuse per SS6.3.3 (with SS6.4.1's
+ * CLOSE exception).
*/
int mbox_readonly;
char selected_mailbox[MBOX_NAME_MAX]; /* which
- * mailbox SESSION_SELECTED actually
- * refers to -- didn't need to exist
- * before RFC 9051 SS6.3.4-SS6.3.6's flat
- * multi-mailbox support, since
- * "selected" and "INBOX is selected"
- * were the same fact. Written
- * optimistically by select_or_examine()
- * at the same point s->state moves to
- * SESSION_SELECTING -- safe even though
- * the SELECT/EXAMINE might still fail,
- * because every reader of this field
- * also gates on s->state ==
- * SESSION_SELECTED first, and a failed
+ * mailbox SESSION_SELECTED refers to.
+ * Written optimistically by select_
+ * or_examine() when s->state moves to
+ * SESSION_SELECTING, safe because
+ * every reader also gates on s->state
+ * == SESSION_SELECTED, and a failed
* SELECT/EXAMINE never reaches that
- * state (RFC 9051 SS6.3.2: "the session
- * is in authenticated state" on
- * failure, deselected same as before
- * the attempt). Consulted so far by
- * session_handle_mbox_appended()'s own
- * "is the mailbox APPEND just targeted
- * the one actually selected right now"
- * check -- see that function's
- * comment. */
+ * state. Consulted by session_handle_
+ * mbox_appended()'s "did APPEND
+ * target the selected mailbox" check. */
/*
- * RFC 9051 SS6.3.13 (IDLE) v1 scope: EXISTS and EXPUNGE only (see
- * imapd.h's imsg_mbox_idle_uid comment for the sourcing/reasoning
- * behind not also pushing unsolicited FETCH). idle_known_uids is this
- * session's own cached, ordered snapshot of "which UIDs existed as of
- * the last time this session's view was authoritative" -- populated
- * from an IMSG_MBOX_IDLE_REFRESH round trip, either the baseline one
- * cmd_idle() triggers right after sending "+ idling" (idle_baseline_
- * valid is 0 until that first one lands, so session_handle_idle_
- * refreshed() knows not to diff against garbage), or a later one
- * triggered by session_notify_idle_peers() when some *other* session
- * belonging to the same uid successfully mutates the mailbox.
- * Diffing this array against a freshly streamed IMSG_MBOX_IDLE_UID
- * list is what lets session_handle_idle_refreshed() compute correct
- * seqno-based "* N EXPUNGE" lines for whatever UIDs dropped out --
- * same "position in the shrinking list, removed in ascending order"
- * algorithm handle_mbox_expunge() already needs on the store side,
- * just recomputed here in listener.c from two UID arrays instead of
- * from the index file directly.
+ * RFC 9051 SS6.3.13 (IDLE) EXISTS and EXPUNGE only.
+ * idle_known_uids is this session's cached, ordered snapshot of
+ * which UIDs existed as of the last authoritative view, populated
+ * from an IMSG_MBOX_IDLE_REFRESH round trip (the baseline one after
+ * "+ idling", or a later one from session_notify_idle_peers() when
+ * another session with the same uid mutates the mailbox).
+ * idle_baseline_valid gates against diffing before the first one
+ * lands. Diffing this array against a freshly streamed
+ * IMSG_MBOX_IDLE_UID list lets session_handle_idle_refreshed()
+ * compute correct seqno-based EXPUNGE lines for dropped UIDs.
*
- * idle_refresh_pending/idle_refresh_again exist because a refresh is
- * itself an async imsg round trip: if a second trigger arrives while
- * one is already in flight for this session (e.g. two sibling
- * sessions both mutate in quick succession), there's no way to ask
- * store.c to hurry up or to safely start a second overlapping
- * request on the same store_iev channel -- idle_refresh_again just
- * remembers to immediately re-request once the in-flight one's
- * IMSG_MBOX_IDLE_REFRESHED reply lands, rather than either blocking
- * or silently dropping the second trigger.
+ * idle_refresh_pending/idle_refresh_again exist because a refresh
+ * is itself an async round trip: if a second trigger arrives while
+ * one is in flight, idle_refresh_again remembers to immediately
+ * re-request once the in-flight reply lands, rather than blocking
+ * or dropping the second trigger.
*/
uint32_t *idle_known_uids;
uint32_t idle_known_nuids;
int idle_refresh_pending;
int idle_refresh_again;
- /*
- * Accumulates the freshly streamed IMSG_MBOX_IDLE_UID list for an
- * in-flight refresh -- deliberately a *separate* array from idle_
- * known_uids above, not built in place over it, since session_
- * handle_idle_refreshed() needs both the old and the new list at
- * once to diff them. Same "held back until the terminal reply"
- * shape as s->vanished_ranges/s->qresync_fetches during a QRESYNC
- * SELECT resync.
+ /* Accumulates the freshly streamed IMSG_MBOX_IDLE_UID list for an
+ * in-flight refresh, a separate array from idle_known_uids,
+ * since session_handle_idle_refreshed() needs both old and new
+ * lists at once to diff them.
*/
uint32_t *idle_incoming_uids;
uint32_t idle_incoming_n;
uint32_t idle_incoming_cap;
/*
- * Accumulates VANISHED (EARLIER) ranges streamed in via zero or more
+ * Accumulates VANISHED (EARLIER) ranges streamed via zero or more
* IMSG_MBOX_SELECT_VANISHED during an in-flight QRESYNC SELECT
- * resync (s->state == SESSION_SELECTING) -- store.c already reports
- * these pre-compacted into ranges (see imsg_mbox_select_vanished's
- * comment in imapd.h), so this array just collects them in
- * arrival order (already ascending -- store.c streams its single
- * forward pass in increasing-UID order) for session_handle_mbox_
- * selected() to join into one combined "* VANISHED (EARLIER) ..."
- * line once IMSG_MBOX_SELECTED arrives -- RFC 7162 SS3.2.6 requires
- * every VANISHED (EARLIER) response to precede any FETCH response
- * in the same resync, which is only guaranteed by holding all of it
- * (and the FETCH data below) until the terminal reply, not by
- * relaying each piece to the client as it streams in.
+ * resync. Already pre-compacted and ascending, collected here so
+ * session_handle_mbox_selected() can join them into one combined
+ * response line once IMSG_MBOX_SELECTED arrives. RFC 7162 SS3.2.6
+ * requires VANISHED (EARLIER) to precede any FETCH in the same
+ * resync, guaranteed only by holding everything until the terminal
+ * reply.
*/
struct vanished_range *vanished_ranges;
uint32_t vanished_nranges;
uint32_t vanished_cap;
- /*
- * Accumulates the QRESYNC resync's per-message FETCH-with-UID-and-
- * MODSEQ data, streamed in via IMSG_MBOX_FETCH_META while s->state
- * == SESSION_SELECTING (see session_store_dispatch()'s IMSG_MBOX_
- * FETCH_META case) -- held back for the same "VANISHED must precede
- * FETCH" ordering reason vanished_ranges is. Holds full struct
- * imsg_mbox_fetch_meta copies (not just seqno/uid, unlike search_
- * matches) because session_send_qresync_fetch_response() needs
- * FLAGS too -- heavier per-entry than search_matches, but v1-scale
- * resyncs are small (this project's usual "personal use, modest
- * mailbox size" tolerance).
+ /* Accumulates the QRESYNC resync's per-message FETCH-with-UID-and-
+ * MODSEQ data, held back for the same VANISHED-before-FETCH
+ * ordering reason as vanished_ranges. Holds full struct
+ * imsg_mbox_fetch_meta copies (not just seqno/uid) since
+ * session_send_qresync_fetch_response() needs FLAGS too.
*/
struct imsg_mbox_fetch_meta *qresync_fetches;
uint32_t qresync_nfetches;
uint32_t qresync_fetches_cap;
/*
- * COPY/MOVE (RFC 9051 SS6.4.7/SS6.4.8), SESSION_COPYING. Two parallel
- * growable arrays accumulating each IMSG_MBOX_COPY_MAPPING streamed
- * in during the round trip -- src_uid/dest_uid pairs, already
- * ascending (store.c streams its single forward pass in increasing
- * order, same guarantee vanished_ranges above relies on), range-
- * compacted independently via format_seq_list() into COPYUID's two
- * UID sets once the terminal reply arrives. cmd_is_move distinguishes
- * which of COPY/MOVE is in flight (same session-state-plus-flag
- * pattern as s->close_after_expunge for EXPUNGE/CLOSE) -- both share
- * SESSION_COPYING since their in-flight bookkeeping is otherwise
- * identical, differing only in the terminal formatting (see session_
- * finish_copy_or_move()). move_expunged buffers each IMSG_MBOX_
- * EXPUNGED that arrives during a MOVE specifically (rather than
- * writing it to the client immediately, the way a real EXPUNGE/CLOSE
- * does) -- SS6.4.8 requires COPYUID to precede any EXPUNGE/VANISHED
- * for the same operation, which (like vanished_ranges/qresync_fetches
- * above) is only guaranteed by holding everything until the terminal
- * reply, not by relaying each piece as it streams in.
+ * COPY/MOVE (RFC 9051 SS6.4.7/SS6.4.8), SESSION_COPYING. Two
+ * parallel growable arrays accumulate each IMSG_MBOX_COPY_MAPPING
+ * (src_uid/dest_uid, already ascending), range-compacted into
+ * COPYUID's two UID sets once the terminal reply arrives.
+ * cmd_is_move distinguishes COPY from MOVE (both share
+ * SESSION_COPYING, differing only in terminal formatting).
+ * move_expunged buffers each IMSG_MBOX_EXPUNGED from a MOVE rather
+ * than writing it immediately, since SS6.4.8 requires COPYUID to
+ * precede any EXPUNGE/VANISHED for the same operation.
*/
uint32_t *copy_src_uids;
uint32_t *copy_dest_uids;
uint32_t copy_n;
uint32_t copy_cap;
- int copy_alloc_failed; /* same "don't silently drop
- * a mapping" reasoning as
+ int copy_alloc_failed; /* same reasoning as
* s->search_alloc_failed */
int cmd_is_move;
struct imsg_mbox_expunged *move_expunged;
uint32_t move_expunged_n;
uint32_t move_expunged_cap;
- /*
- * Accumulates messages that failed a STORE's UNCHANGEDSINCE test,
- * streamed in via zero or more IMSG_MBOX_STORE_MODIFIED while
- * s->state == SESSION_STORING, for session_handle_mbox_result() to
- * range-compact (format_seq_list(), same helper SEARCH/ESEARCH
- * already use) into the tagged response's MODIFIED response code
- * (RFC 7162 SS3.1.3). Holds sequence numbers for a plain STORE, UIDs
- * for a UID STORE (SS3.1.3: "the message number (or unique
- * identifier in the case of the UID STORE command)") -- a single
- * array, not a parallel uid array, since exactly one of the two
- * values is ever meaningful for a given STORE; see session_handle_
- * store_modified()'s use of s->cmd_by_uid to pick which.
+ /* Accumulates messages that failed a STORE's UNCHANGEDSINCE test
+ * (IMSG_MBOX_STORE_MODIFIED), range-compacted by session_handle_
+ * mbox_result() into the tagged response's MODIFIED code (RFC 7162
+ * SS3.1.3). Holds sequence numbers for a plain STORE, UIDs for a
+ * UID STORE, a single array, since only one is ever meaningful
+ * per STORE (s->cmd_by_uid picks which).
*/
uint32_t *store_modified;
uint32_t store_modified_n;
uint32_t store_modified_cap;
/*
- * RFC 9051 SS6.4.9 (UID command), added this pass: 1 if the async
- * operation currently in flight (SESSION_FETCHING/STORING/SEARCHING/
- * EXPUNGING) was dispatched via UID <cmd> rather than the bare
- * command. Every entry point that starts one of those four states
- * sets this explicitly (fetch_dispatch()/store_do()/search_dispatch()/
- * session_request_expunge()), so a stale value from a previous,
- * different-mode command can never leak into the next one -- there is
- * no "clear it back to 0" step anywhere else, matching this struct's
- * existing condstore_enabled/qresync_enabled precedent of "every
- * setter is unconditional, so no separate reset is needed."
+ * RFC 9051 SS6.4.9 (UID command): 1 if the in-flight async
+ * operation (FETCHING/STORING/SEARCHING/EXPUNGING) was dispatched
+ * via UID <cmd> rather than the bare command. Every entry point
+ * that starts one of those states sets this explicitly, so no
+ * separate reset is needed.
*
- * Consulted by: session_send_store_fetch_response() (UID STORE's
- * FETCH echo must include UID, SS6.4.9's "MUST implicitly include
- * the UID message data item"), session_handle_mbox_search_match()/
- * session_finish_search() (UID SEARCH reports UIDs instead of
- * sequence numbers, plus the "UID" ESEARCH correlator token),
- * session_handle_store_modified() (UID STORE's MODIFIED response
- * code lists UIDs, not sequence numbers -- RFC 7162 SS3.1.3: "the
- * message number (or unique identifier in the case of the UID STORE
- * command)"), and session_handle_mbox_result() (every one of the four
- * commands' tagged completion text becomes "UID <CMD> completed" --
- * RFC 9051's own SS6.4.9 example shows "UID FETCH completed"
- * verbatim; UID EXPUNGE's own worked example shows "UID EXPUNGE
- * completed"; UID STORE/UID SEARCH aren't shown as literal worked
- * examples but follow the same "UID <cmd> completed" pattern by
- * direct symmetry with those two, not fabricated). Plain FETCH
- * doesn't need to consult this at all -- forcing MBOX_FETCH_UID into
- * s->fetch_attrs at dispatch time (see fetch_dispatch()) already
- * makes session_send_fetch_response()'s existing attrs-driven UID
- * printing do the right thing with no further changes there.
+ * Consulted by session_send_store_fetch_response() (UID STORE's
+ * FETCH echo must include UID), session_handle_mbox_search_match()/
+ * session_finish_search() (UID SEARCH reports UIDs, plus the "UID"
+ * ESEARCH correlator), session_handle_store_modified() (MODIFIED
+ * lists UIDs for UID STORE), and session_handle_mbox_result()
+ * (tagged completion text becomes "UID <CMD> completed"). Plain
+ * FETCH doesn't need this, forcing MBOX_FETCH_UID into
+ * s->fetch_attrs at dispatch time already handles it.
*/
int cmd_by_uid;
TAILQ_ENTRY(session) entry;
};
-/*
- * Named (not anonymous) so the type matches between this extern
- * declaration and listener.c's actual definition -- TAILQ_HEAD(, session)
- * with an empty name expands to a distinct anonymous struct at each use,
- * which the original single-file listener.c got away with (only one use
- * existed at all) but which is a hard redefinition error once that one
- * use is split across a header and a .c file.
+/* Named (not anonymous) so the type matches between this extern
+ * declaration and listener.c's definition, an anonymous
+ * TAILQ_HEAD(, session) would be a redefinition error once split across
+ * a header and a .c file.
*/
TAILQ_HEAD(session_list, session);
/*
* Globals shared across the files this process's source is split into
- * (definitions live in listener.c's core; see that file for why each
- * exists).
+ * (definitions live in listener.c's core).
*/
extern struct session_list sessions;
extern struct imsgev iev_auth;
extern struct imsgev iev_parent;
extern struct tls *listener_tls_ctx;
-/*
- * Cross-file entry points. This is the same set of forward declarations
- * listener.c carried as `static` prototypes before the file was split
- * into listener.c (core) + auth_cmd.c + mailbox_cmd.c + append_cmd.c +
- * fetch_cmd.c + search_cmd.c + store_cmd.c + store_ipc.c -- moved here
- * verbatim (minus `static`, which no longer applies once callers and
- * callees can live in different translation units) rather than
- * redesigned, so this split is a pure move with no behavior change.
+/* Cross-file entry points: forward declarations for listener.c (core) +
+ * auth_cmd.c + mailbox_cmd.c + append_cmd.c + fetch_cmd.c + search_cmd.c
+ * + store_cmd.c + store_ipc.c.
*/
void listener_accept(int, short, void *);
void listener_dispatch_auth(int, short, void *);
int session_finish_append(struct session *);
void session_handle_mbox_appended(struct session *,
const struct imsg_mbox_appended *);
-/*
- * Real, initialized definition lives in fetch_cmd.c alongside
- * format_internaldate() itself; append_cmd.c's parse_date_time() and
- * search_cmd.c's parse_search_date() both reuse it for the reverse
- * (name-to-index) direction. The original single-file listener.c got
- * away with a same-file tentative "const char *fetch_month_names[12];"
- * forward declaration ahead of its real, initialized definition further
- * down -- valid C, but only within one translation unit; split across
- * files, that trick doesn't reach across them, so this needs to be a
- * real extern instead.
+/* Definition lives in fetch_cmd.c alongside format_internaldate();
+ * append_cmd.c's parse_date_time() and search_cmd.c's parse_search_date()
+ * reuse it for the reverse (name-to-index) direction.
*/
extern const char *fetch_month_names[12];
void format_internaldate(int64_t, char *, size_t);
int cmd_starttls(struct session *, const char *, char *);
int cmd_authenticate(struct session *, const char *, char *);
-/* command-auth (RFC 9051 SS6.3 -- valid in Authenticated or Selected
- * state): ENABLE is real (see cmd_enable()'s comment for why that's a
- * complete implementation, not a stub, for v1). Everything else here
- * needs a mailbox layer that doesn't exist yet -- store.c's IMSG_MBOX_*
- * family is entirely unimplemented (see store.c's own header comment),
- * and its wire payload shapes aren't designed, so these all reply a
- * generic NO via stub_not_implemented() rather than doing anything real.
- * They're in the dispatch table anyway, correctly gated by session
- * state, because "recognized command, can't do it right now" (NO) and
- * "not a real IMAP command at all" (BAD, session_handle_line()'s unknown-
- * command fallback) are different, both spec-meaningful responses. */
+/* command-auth (RFC 9051 SS6.3, valid in Authenticated or Selected
+ * state). stub_not_implemented() remains for any future command added
+ * to the dispatch table before its handler is real, "recognized,
+ * can't do it right now" (NO) is spec-meaningful, distinct from
+ * "unknown command" (BAD). */
int stub_not_implemented(struct session *, const char *,
const char *);
int cmd_enable(struct session *, const char *, char *);
int cmd_append(struct session *, const char *, char *);
int cmd_idle(struct session *, const char *, char *);
-/* command-select (RFC 9051 SS6.4 -- valid only in Selected state).
- * SESSION_SELECTED is currently unreachable (SELECT itself is a stub
- * above), so none of these can be dispatched to yet in practice -- listed
- * for the same "correctly gated, honestly stubbed" reason as command-auth
- * above. */
+/* command-select (RFC 9051 SS6.4, valid only in Selected state). */
int cmd_close(struct session *, const char *, char *);
int cmd_unselect(struct session *, const char *, char *);
int cmd_expunge(struct session *, const char *, char *);
blob - 4df72d97e7332fa0c20a4bd7bfd9e9ae4b99ce13
blob + faf6403faeed92cc39bf3f8af63c7b7d54a31e53
--- src/log.c
+++ src/log.c
* credit him in their own log.c). This file's implementation details
* (log_procname storage, log_init()'s parameters) are this project's own,
* but the API shape and name are that same shared idiom, so his copyright
- * is carried forward here too -- same rationale as parse.y's copyright
+ * is carried forward here too, same rationale as parse.y's copyright
* chain.
*
* Permission to use, copy, modify, and distribute this software for any
blob - 0e32858221d95f544b0de3cc47589ed40fdcbd5a
blob + e9c855b6203bc27068a064fdffffcec766212edb
--- src/log.h
+++ src/log.h
* Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
*
* This header's call signatures were deliberately matched to smtpd's own
- * log.h (see the comment below) -- that log_init()/fatal()/fatalx()/
+ * log.h (see the comment below), that log_init()/fatal()/fatalx()/
* log_warn()/log_debug() API is the same shared daemon-logging idiom
* Henning Brauer wrote and that smtpd, bgpd, and most other privsep
* daemons in OpenBSD base carry his copyright for. Same rationale as
/*
* Minimal logging, matching the call signatures actually observed in the
- * uploaded smtpd.c this session (fatal("pledge"), fatalx("chdir"),
- * log_debug("setup_done: %s[%d] done", p->name, p->pid),
- * log_procinit(proc_title(smtpd_process))) -- those call sites are real,
- * quoted material; this implementation of them is this project's own, not
- * copied from smtpd's actual log.c (not among the uploaded files).
+ * uploaded smtpd.c this session.
*/
#ifndef OPENIMAP_LOG_H
blob - 057cc76fd1e66b7c12523384292371c4cdd5bba9
blob + 1b18e20d63c7e39c3f55c43dedf0da5e1ef134c8
--- src/mailbox_cmd.c
+++ src/mailbox_cmd.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* mailbox_cmd.c -- SELECT/EXAMINE/CREATE/DELETE/RENAME/SUBSCRIBE/LIST/NAMESPACE/STATUS handlers. */
+/* mailbox_cmd.c, SELECT/EXAMINE/CREATE/DELETE/RENAME/SUBSCRIBE/LIST/NAMESPACE/STATUS handlers. */
#include <sys/types.h>
#include <sys/queue.h>
p++;
}
if (strchr(tok, ',') != NULL) {
- *errmsg = "comma-separated known-uids not supported in v1";
+ *errmsg = "comma-separated known-uids not supported";
return (-1);
}
{
if (parse_seq_range(tok, &lo, &hi, &lo_star, &hi_star) == -1 ||
lo_star || hi_star) {
- *errmsg = "invalid known-uids -- '*' is not allowed "
+ *errmsg = "invalid known-uids, '*' is not allowed "
"here (RFC 7162 SS3.2.5.1)";
return (-1);
}
}
if (s->store_iev == NULL) {
- /* should already be wired here -- ST_AUTH requires SESSION_AUTHENTICATED/SELECTED */
+ /* should already be wired here, ST_AUTH requires SESSION_AUTHENTICATED/SELECTED */
log_warnx("session %u: %s with no store channel wired",
s->id, cmdname);
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return stub_not_implemented(s, tag, "UNSUBSCRIBE");
}
-/* RFC 9051 SS6.3.9 wildcards, collapsed to "zero or more of anything" -- v1's flat namespace has no delimiter */
+/* RFC 9051 SS6.3.9 wildcards, collapsed to "zero or more of anything", flat namespace has no delimiter */
int
list_pattern_match(const char *pat, const char *name, int ci)
{
p++;
if (*p == '(') {
- /* SS6.3.9 condition 1: first word after the command starts with "(" -- list-select-opts */
+ /* SS6.3.9 condition 1: first word after the command starts with "(", list-select-opts */
snprintf(text, sizeof(text),
"extended %s selection options not supported", cmdname);
session_reply(s, tag, "NO", text);
p++;
if (*p == '(') {
- /* SS6.3.9 condition 2: second word starts with "(" -- parenthesized `patterns` form */
+ /* SS6.3.9 condition 2: second word starts with "(", parenthesized `patterns` form */
snprintf(text, sizeof(text),
"extended %s mailbox-pattern lists not supported",
cmdname);
while (*p == ' ')
p++;
if (*p != '\0') {
- /* SS6.3.9 condition 3: more than 2 parameters -- trailing list-return-opts */
+ /* SS6.3.9 condition 3: more than 2 parameters, trailing list-return-opts */
snprintf(text, sizeof(text),
"extended %s return options not supported", cmdname);
session_reply(s, tag, "NO", text);
return (1);
}
- /* RFC 9051 SS6.3.9: empty pattern requests the delimiter/root; v1 has no hierarchy, always empty root */
+ /* RFC 9051 SS6.3.9: empty pattern requests the delimiter/root; always empty root */
if (pattern[0] == '\0') {
snprintf(text, sizeof(text), "%s (\\Noselect) \"/\" \"\"", kw);
session_untagged(s, text);
/* SS6.3.9: unaccepted pattern MUST be silently ignored; INBOX answered synchronously, no store round trip */
if (list_pattern_match(canon, "INBOX", 1)) {
- /* "()" -- SS7.3.1 makes every attribute optional; INBOX has no children and is selectable */
+ /* "()", SS7.3.1 makes every attribute optional; INBOX has no children and is selectable */
snprintf(text, sizeof(text), "%s () \"/\" INBOX", kw);
session_untagged(s, text);
}
return (1);
}
-
-/* v1's whole-message-in-one-imsg design caps literal size to fit MAX_IMSGSIZE (16384); also read-side cap */
blob - 3296ea513e7104ad1d7c66bbd2ab5a8277a1bacf
blob + e023cd32c16c4ccb8bd55c98e948d355ee0047e7
--- src/main.c
+++ src/main.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* main.c -- imapd(8) entry point: parses argv, dispatches to the role named by "-x". */
+/* main.c, imapd(8) entry point: parses argv, dispatches to the role named by "-x". */
#include <sys/types.h>
blob - a84ea914205d6836057a543fd1d357b47cb41d6f
blob + ccd5b0e54b123e49096895d2a72789ba83ac746e
--- src/mbox_copy.c
+++ src/mbox_copy.c
*/
/*
- * mbox_copy.c -- COPY and MOVE request handling, both
+ * mbox_copy.c, COPY and MOVE request handling, both
* same-mailbox and cross-mailbox.
*/
if (locate_message_file(rec.basename, &size, suffix,
sizeof(suffix)) == -1) {
log_warnx("session %u: COPY: message %s indexed but "
- "missing on disk -- failing whole COPY (partial "
+ "missing on disk, failing whole COPY (partial "
"copy not permitted, RFC 9051 SS6.4.7)",
session_id, rec.basename);
goto fail;
/* refuse before allocating if this message or the running total exceeds the staging limits */
if ((uint64_t)size > COPY_STAGE_MSG_MAX) {
log_warnx("session %u: COPY: message %s is %lld bytes, "
- "over the per-message staging limit -- failing COPY",
+ "over the per-message staging limit, failing COPY",
session_id, rec.basename, (long long)size);
goto fail;
}
if ((uint64_t)size > COPY_STAGE_TOTAL_MAX - staged_total) {
log_warnx("session %u: COPY: staged data would exceed the "
- "total staging limit -- failing COPY", session_id);
+ "total staging limit, failing COPY", session_id);
goto fail;
}
staged_total += (uint64_t)size;
if (strlcpy(cs.keywords, rec.keywords, sizeof(cs.keywords)) >=
sizeof(cs.keywords)) {
log_warnx("session %u: COPY: keywords too long for "
- "%s -- failing COPY", session_id, rec.basename);
+ "%s, failing COPY", session_id, rec.basename);
goto fail;
}
lp = strstr(suffix, "2,");
if (strlcpy(saved, current_mailbox_dir, sizeof(saved)) >=
sizeof(saved)) {
- log_warnx("session %u: COPY: current_mailbox_dir truncated -- "
+ log_warnx("session %u: COPY: current_mailbox_dir truncated, "
"can't happen (same-size buffers)", session_id);
ok = 0;
goto done;
strlcpy(second, desttarget, sizeof(second)) >=
sizeof(second)) {
log_warnx("session %u: COPY: mailbox name "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"buffers)", session_id);
ok = 0;
goto done;
strlcpy(second, saved, sizeof(second)) >=
sizeof(second)) {
log_warnx("session %u: COPY: mailbox name "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"buffers)", session_id);
ok = 0;
goto done;
srcidx = first_is_dest ? &idx_b : &idx_a;
destidx = first_is_dest ? &idx_a : &idx_b;
- /* Back to source -- staging below reads relative to cwd. */
+ /* Back to source, staging below reads relative to cwd. */
if (select_mailbox_dir(saved) == -1) {
log_warnx("session %u: COPY: couldn't return to %s "
"to stage messages", session_id, saved);
imsgev_add(iev);
}
-/* MOVE within same mailbox: range membership below must test "in" (read pos), not "out" (compaction write index) -- "out" under-consumed the range. */
+/* MOVE within same mailbox: range membership below must test "in" (read pos), not "out" (compaction write index), "out" under-consumed the range. */
static int
move_same_mailbox(struct imsg_mbox_copy *req, struct mbox_index *idx,
sizeof(moved[nmoved].keywords)) >=
sizeof(moved[nmoved].keywords)) {
log_warnx("session %u: MOVE: basename/keywords too "
- "long for %s -- failing MOVE", session_id,
+ "long for %s, failing MOVE", session_id,
rec.basename);
free(idx->lines[in]);
ok = 0;
goto cleanup;
}
- /* destination commit is durable -- now remove originals from source; cwd is at destination, return to saved dir first */
+ /* destination commit is durable, now remove originals from source; cwd is at destination, return to saved dir first */
if (select_mailbox_dir(saved) == -1) {
log_warnx("session %u: MOVE: committed to destination but "
"couldn't return to source %s to remove the originals "
if (index_save(srcidx) == -1) {
log_warnx("session %u: MOVE: destination commit succeeded "
- "but saving the source's compacted index failed -- "
+ "but saving the source's compacted index failed, "
"message(s) may remain duplicated", session_id);
ok = 0;
goto cleanup;
if (strlcpy(saved, current_mailbox_dir, sizeof(saved)) >=
sizeof(saved)) {
- log_warnx("session %u: MOVE: current_mailbox_dir truncated -- "
+ log_warnx("session %u: MOVE: current_mailbox_dir truncated, "
"can't happen (same-size buffers)", session_id);
ok = 0;
goto done;
strlcpy(second, desttarget, sizeof(second)) >=
sizeof(second)) {
log_warnx("session %u: MOVE: mailbox name "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"buffers)", session_id);
ok = 0;
goto done;
strlcpy(second, saved, sizeof(second)) >=
sizeof(second)) {
log_warnx("session %u: MOVE: mailbox name "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"buffers)", session_id);
ok = 0;
goto done;
session_id);
imsgev_add(iev);
}
-
blob - 335ff535eb3abdf54b70601be59012780c46af82
blob + d861857885440699c0eaa29c21ea45c9d67a66e4
--- src/mbox_fetch.c
+++ src/mbox_fetch.c
*/
/*
- * mbox_fetch.c -- FETCH and STATUS request handling.
+ * mbox_fetch.c, FETCH and STATUS request handling.
*/
#include <sys/types.h>
if (!req->by_uid && hi > (uint32_t)idx.nlines)
hi = (uint32_t)idx.nlines;
- /* RFC 7162 SS3.2.6: VANISHED (EARLIER) MUST precede FETCH responses -- guaranteed by send order here */
+ /* RFC 7162 SS3.2.6: VANISHED (EARLIER) MUST precede FETCH responses, guaranteed by send order here */
if (req->by_uid && req->want_vanished)
send_vanished_range(&idx, lo, hi, iev);
if (locate_message_file(rec.basename, &size, suffix,
sizeof(suffix)) == -1) {
log_warnx("session %u: message %s (uid %u) "
- "indexed but missing on disk -- skipped",
+ "indexed but missing on disk, skipped",
session_id, rec.basename, meta.uid);
continue;
}
if (locate_message_file(rec.basename, &size, suffix,
sizeof(suffix)) == -1) {
log_warnx("session %u: message %s indexed but "
- "missing on disk -- skipped for STATUS "
+ "missing on disk, skipped for STATUS "
"UNSEEN/DELETED/SIZE", session_id,
rec.basename);
continue;
/* restore whatever this session had selected before STATUS (no-op if already there) */
if (switched && select_mailbox_dir(saved) == -1)
log_warnx("session %u: STATUS: failed to restore selection "
- "to %s -- session may be left in an inconsistent state",
+ "to %s, session may be left in an inconsistent state",
session_id, saved[0] != '\0' ? saved : "INBOX");
if (imsg_compose(&iev->ibuf, IMSG_MBOX_STATUS_RESULT, 0, 0, -1, &reply,
sizeof(reply)) == -1)
blob - 3509890981573d844a3f5185b7e774a973ac2410
blob + d8b7cd3d2492401da8e5323e7f7f54ab57647d1e
--- src/mbox_manage.c
+++ src/mbox_manage.c
*/
/*
- * mbox_manage.c -- CREATE/DELETE/RENAME/LIST/APPEND/SELECT
+ * mbox_manage.c, CREATE/DELETE/RENAME/LIST/APPEND/SELECT
* request handling.
*/
goto send;
}
- /* mkdir/ensure_maildir_dirs() below need cwd at maildir root, not wherever a SELECTed mailbox left it -- visit root first */
+ /* mkdir/ensure_maildir_dirs() below need cwd at maildir root, not wherever a SELECTed mailbox left it, visit root first */
if (strlcpy(saved, current_mailbox_dir, sizeof(saved)) >=
sizeof(saved)) {
log_warnx("session %u: CREATE %s: current_mailbox_dir "
- "truncated -- can't happen (same-size buffers)",
+ "truncated, can't happen (same-size buffers)",
session_id, req->mailbox);
result.error = MBOX_OP_ERR_GENERIC;
goto send;
continue;
}
if (!S_ISREG(st.st_mode)) {
- log_warnx("session %u: DELETE: refusing -- "
+ log_warnx("session %u: DELETE: refusing, "
"%s is not a regular file", session_id,
entpath);
ok = 0;
return (0);
}
-/* IMSG_MBOX_DELETE (RFC 9051 SS6.3.5); no check for "is this session's own SELECTed mailbox" -- leaves store child at root until next SELECT resolves it */
+/* IMSG_MBOX_DELETE (RFC 9051 SS6.3.5); no check for "is this session's own SELECTed mailbox", leaves store child at root until next SELECT resolves it */
void
handle_mbox_delete(struct imsg_mbox_delete *req, struct imsgev *iev)
if (strlcpy(saved, current_mailbox_dir, sizeof(saved)) >=
sizeof(saved)) {
log_warnx("session %u: DELETE %s: current_mailbox_dir "
- "truncated -- can't happen (same-size buffers)",
+ "truncated, can't happen (same-size buffers)",
session_id, req->mailbox);
result.error = MBOX_OP_ERR_GENERIC;
goto send;
if (strlcpy(saved, current_mailbox_dir, sizeof(saved)) >=
sizeof(saved)) {
log_warnx("session %u: RENAME %s -> %s: current_mailbox_dir "
- "truncated -- can't happen (same-size buffers)",
+ "truncated, can't happen (same-size buffers)",
session_id, req->oldname, req->newname);
result.error = MBOX_OP_ERR_GENERIC;
goto send;
memset(&item, 0, sizeof(item));
if (strlcpy(item.mailbox, de->d_name, sizeof(item.mailbox)) >=
sizeof(item.mailbox)) {
- log_warnx("session %u: LIST: %s truncated -- can't "
+ log_warnx("session %u: LIST: %s truncated, can't "
"happen (mailbox_name_valid() already bounds it "
"under MBOX_NAME_MAX)", session_id, de->d_name);
continue;
if (strlcpy(hostbuf, "imapd", sizeof(hostbuf)) >=
sizeof(hostbuf))
log_warnx("session %u: gethostname fallback "
- "truncated -- can't happen", session_id);
+ "truncated, can't happen", session_id);
}
resolved = 1;
}
if (strlcpy(saved, current_mailbox_dir, sizeof(saved)) >=
sizeof(saved)) {
log_warnx("session %u: APPEND %s: current_mailbox_dir "
- "truncated -- can't happen (same-size buffers)",
+ "truncated, can't happen (same-size buffers)",
session_id, req->mailbox);
reply.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
goto done_reply;
locked = 0;
index_free(&idx);
- /* index committed -- now the rename; index-then-rename is the safer order to fail partway through */
+ /* index committed, now the rename; index-then-rename is the safer order to fail partway through */
sysflags_to_letters(req->sysflags, letters, sizeof(letters));
if (snprintf(curpath, sizeof(curpath), "cur/%s:2,%s", basename,
letters) >= (int)sizeof(curpath)) {
session_id);
imsgev_add(iev);
}
-
-/* resolves a mailbox name to select_mailbox_dir()'s expected "target" string; checks syntax only, not existence */
blob - 300c2c9bd0fed6acd4ee626f7994d3596fb66784
blob + 16d378de99112160ecb1631e514644ca190af35a
--- src/mbox_search.c
+++ src/mbox_search.c
*/
/*
- * mbox_search.c -- SEARCH key evaluation.
+ * mbox_search.c, SEARCH key evaluation.
*/
#include <sys/types.h>
if (strlcpy(tmp, list, sizeof(tmp)) >= sizeof(tmp)) {
log_warnx("session %u: kw_list_contains: keyword list "
- "truncated -- can't happen (both same-size "
+ "truncated, can't happen (both same-size "
"MBOX_FLAGS_MAX buffers); treating as not-found",
session_id);
return (0);
if (mode == MBOX_STORE_REMOVE) {
if (strlcpy(tmp, old_kws, sizeof(tmp)) >= sizeof(tmp)) {
log_warnx("session %u: merge_keywords: old_kws "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"MBOX_FLAGS_MAX buffers); refusing to guess at "
"the keyword set", session_id);
return;
}
if (strlcat(out, tok, outsize) >= outsize) {
log_warnx("session %u: merge_keywords: out "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"MBOX_FLAGS_MAX buffers)", session_id);
return;
}
if (mode == MBOX_STORE_ADD) {
if (strlcpy(tmp, old_kws, sizeof(tmp)) >= sizeof(tmp)) {
log_warnx("session %u: merge_keywords: old_kws "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"MBOX_FLAGS_MAX buffers); refusing to guess at "
"the keyword set", session_id);
return;
}
if (strlcat(out, tok, outsize) >= outsize) {
log_warnx("session %u: merge_keywords: out "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"MBOX_FLAGS_MAX buffers)", session_id);
return;
}
/* SET starts from nothing; ADD continues from old_kws already built above; either way append new_kws not already present */
if (strlcpy(tmp, new_kws, sizeof(tmp)) >= sizeof(tmp)) {
- log_warnx("session %u: merge_keywords: new_kws truncated -- "
+ log_warnx("session %u: merge_keywords: new_kws truncated, "
"can't happen (same-size MBOX_FLAGS_MAX buffers); "
"refusing to guess at the keyword set", session_id);
return;
if (!first) {
if (strlcat(out, ",", outsize) >= outsize) {
log_warnx("session %u: merge_keywords: out "
- "truncated -- can't happen (same-size "
+ "truncated, can't happen (same-size "
"MBOX_FLAGS_MAX buffers)", session_id);
return;
}
if (locate_message_file(rec.basename, &size, suffix,
sizeof(suffix)) == -1) {
log_warnx("session %u: message %s (uid %u) indexed "
- "but missing on disk -- skipped", session_id,
+ "but missing on disk, skipped", session_id,
rec.basename, m.uid);
continue;
}
blob - d87a5cd657f34a7b8e207398875e4bd8e2f99e1a
blob + 831abb312f39b67c75e6dd453dca123db64ede3f
--- src/mbox_store.c
+++ src/mbox_store.c
*/
/*
- * mbox_store.c -- STORE and EXPUNGE request handling.
+ * mbox_store.c, STORE and EXPUNGE request handling.
*/
#include <sys/types.h>
if (locate_message_file(rec.basename, &size, suffix,
sizeof(suffix)) == -1) {
log_warnx("session %u: message %s (uid %u) indexed "
- "but missing on disk -- skipped", session_id,
+ "but missing on disk, skipped", session_id,
rec.basename, rec.uid);
continue;
}
if (locate_message_file(rec.basename, &size, suffix,
sizeof(suffix)) == -1) {
log_warnx("session %u: message %s indexed but missing "
- "on disk -- kept in index, not counted as "
+ "on disk, kept in index, not counted as "
"expunged", session_id, rec.basename);
idx.lines[out++] = idx.lines[in];
continue;
if (snprintf(path, sizeof(path), "%s/%s%s",
suffix[0] == '\0' ? "new" : "cur", rec.basename, suffix) >=
(int)sizeof(path)) {
- log_warnx("session %u: path too long for %s -- kept "
+ log_warnx("session %u: path too long for %s, kept "
"in index", session_id, rec.basename);
idx.lines[out++] = idx.lines[in];
continue;
blob - aee52ad2f5fc396dbbc89817817034937985e94e
blob + a14d666a57d963e0630fd248e8b19416d7b92a64
--- src/mime.c
+++ src/mime.c
*/
/*
- * mime.c -- header and MIME parsing shared across FETCH,
+ * mime.c, header and MIME parsing shared across FETCH,
* ENVELOPE, and BODYSTRUCTURE: content-type, multipart splitting, and
* per-part location.
*/
if (strlcpy(suffix_out, de->d_name + baselen,
suffix_out_size) >= suffix_out_size) {
log_warnx("session %u: %s: flag "
- "suffix truncated -- refusing to "
+ "suffix truncated, refusing to "
"report a possibly-wrong flag "
"set", session_id, de->d_name);
} else {
for (i = 0; i + 1 < (size_t)total; i++) {
if (readbuf[i] == '\0') {
log_warnx("session %u: message %s has a NUL byte in "
- "its header -- BODY.PEEK[HEADER] skipped",
+ "its header, BODY.PEEK[HEADER] skipped",
session_id, basename);
return (-1);
}
if (sepindex == -1 || sepindex > FETCH_HEADER_MAX) {
log_warnx("session %u: message %s: no header/body separator "
- "found within %d bytes -- BODY.PEEK[HEADER] skipped",
+ "found within %d bytes, BODY.PEEK[HEADER] skipped",
session_id, basename, FETCH_HEADER_MAX);
return (-1);
}
close(fd);
if (total > (ssize_t)maxlen) {
- log_warnx("session %u: message %s exceeds %zu bytes -- "
+ log_warnx("session %u: message %s exceeds %zu bytes, "
"%s skipped", session_id, basename, maxlen, label);
free(readbuf);
return (-1);
for (i = 0; i < (size_t)total; i++) {
if (readbuf[i] == '\0') {
- log_warnx("session %u: message %s has a NUL byte -- "
+ log_warnx("session %u: message %s has a NUL byte, "
"%s skipped", session_id, basename, label);
free(readbuf);
return (-1);
}
if (sepindex == -1) {
log_warnx("session %u: message %s: no header/body "
- "separator found -- %s skipped",
+ "separator found, %s skipped",
session_id, basename, label);
free(readbuf);
return (-1);
if (strlcpy(listcopy, list, sizeof(listcopy)) >= sizeof(listcopy)) {
log_warnx("session %u: header_field_name_matches: list "
- "truncated -- can't happen (list bounded by same-size "
+ "truncated, can't happen (list bounded by same-size "
"HEADER_FIELDS_MAX buffer upstream); treating as "
"not-found", session_id);
return (0);
if (read_message_header(basename, &hdrbuf, &hdrlen) == -1)
return (-1);
- /* filtered output can never exceed the unfiltered header's size -- every byte copied below comes verbatim from hdrbuf */
+ /* filtered output can never exceed the unfiltered header's size, every byte copied below comes verbatim from hdrbuf */
if ((out = malloc(hdrlen)) == NULL) {
log_warn("session %u: malloc HEADER.FIELDS buffer (%s)",
session_id, basename);
while (i < hdrlen && hdr[i] != '\n')
i++;
if (i >= hdrlen)
- break; /* malformed -- no trailing blank line found */
+ break; /* malformed, no trailing blank line found */
line_end = (i > field_start && hdr[i - 1] == '\r') ? i - 1 : i;
off = i + 1;
if (line_end == field_start)
- break; /* blank line -- end of header, not found */
+ break; /* blank line, end of header, not found */
for (k = field_start; k < line_end; k++) {
if (hdr[k] == ':')
break;
}
- /* consume obs-fold lines regardless of name match -- off must land past them to stay positioned for the next field */
+ /* consume obs-fold lines regardless of name match, off must land past them to stay positioned for the next field */
while (off < hdrlen && (hdr[off] == ' ' || hdr[off] == '\t')) {
size_t j = off;
strlcpy(subtype_out, "PLAIN", subtypesize) >=
subtypesize) {
log_warnx("session %u: parse_content_type: default "
- "type/subtype truncated -- caller-supplied "
+ "type/subtype truncated, caller-supplied "
"buffer too small for \"TEXT\"/\"PLAIN\"",
session_id);
return (-1);
int closed = 0;
if (strlcpy(needle, "--", sizeof(needle)) >= sizeof(needle)) {
- log_warnx("session %u: split_multipart: \"--\" truncated -- "
+ log_warnx("session %u: split_multipart: \"--\" truncated, "
"can't happen (2 bytes into a 73-byte buffer)",
session_id);
return (-1);
}
needlelen = strlcat(needle, boundary, sizeof(needle));
if (needlelen >= sizeof(needle))
- return (-1); /* boundary too long -- non-conformant */
+ return (-1); /* boundary too long, non-conformant */
*nparts_out = 0;
after++;
if (after < bodylen && body[after] != '\r' &&
body[after] != '\n') {
- pos++; /* not followed by CRLF/LF/EOF -- coincidental match inside content, not a delimiter */
+ pos++; /* not followed by CRLF/LF/EOF, coincidental match inside content, not a delimiter */
continue;
}
else if (after < bodylen && body[after] == '\n')
after += 1;
else
- break; /* delimiter runs to end of body with no CRLF -- no body-part can follow */
+ break; /* delimiter runs to end of body with no CRLF, no body-part can follow */
if (n >= maxparts)
return (-1);
return (-1);
if (pathlen == 1) {
- /* path consumed; must be a genuine leaf (not MULTIPART/MESSAGE-RFC822|GLOBAL) -- no "combined children" concept exists */
+ /* path consumed; must be a genuine leaf (not MULTIPART/MESSAGE-RFC822|GLOBAL), no "combined children" concept exists */
char ctype[64], csub[64], cparams[600];
char cboundary[70 + 1];
int chb;
continue;
if (!first && strlcat(out, " ", outsize) >= outsize) {
log_warnx("session %u: build_flags_string: out "
- "truncated -- caller-supplied buffer too small "
+ "truncated, caller-supplied buffer too small "
"for the flag list; stopping early", session_id);
return;
}
if (strlcat(out, stdflags[i].flag, outsize) >= outsize) {
log_warnx("session %u: build_flags_string: out "
- "truncated -- caller-supplied buffer too small "
+ "truncated, caller-supplied buffer too small "
"for the flag list; stopping early", session_id);
return;
}
if (strlcpy(kwbuf, keywords, sizeof(kwbuf)) >= sizeof(kwbuf)) {
log_warnx("session %u: build_flags_string: keywords "
- "too long for kwbuf -- omitting keyword flags "
+ "too long for kwbuf, omitting keyword flags "
"rather than guessing at a truncated list",
session_id);
return;
kw = strtok_r(NULL, ",", &save)) {
if (!first && strlcat(out, " ", outsize) >= outsize) {
log_warnx("session %u: build_flags_string: "
- "out truncated -- caller-supplied "
+ "out truncated, caller-supplied "
"buffer too small for the flag list; "
"stopping early", session_id);
return;
}
if (strlcat(out, kw, outsize) >= outsize) {
log_warnx("session %u: build_flags_string: "
- "out truncated -- caller-supplied "
+ "out truncated, caller-supplied "
"buffer too small for the flag list; "
"stopping early", session_id);
return;
return ((int64_t)time(NULL));
return ((int64_t)ts);
}
-
blob - d7ddcce302acb1f06be1b9c92ea56a678fbe9360
blob + 7d38934e0e41ad7eaf36f5e19be868d6c5f63f6a
--- src/parent.c
+++ src/parent.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* parent.c -- privileged supervisor: process management, SETUP_PEER/SETUP_DONE handshake, privilege drop. */
+/* parent.c, privileged supervisor: process management, SETUP_PEER/SETUP_DONE handshake, privilege drop. */
#include <sys/types.h>
#include <sys/queue.h>
static void sigchld_handler(int, short, void *);
static void reap_child(pid_t, int);
-/* config_load() -- the full imapd.conf grammar -- lives in parse.y */
+/* config_load(), the full imapd.conf grammar, lives in parse.y */
__dead void
parent_main(const char *conffile, int argc, char *argv[],
struct openimap_config *conf)
if (realpath(argv[0], progpath) == NULL)
fatal("realpath");
- /* bind(2) on <1024 needs root -- done here, before any privdrop. */
+ /* bind(2) on <1024 needs root, done here, before any privdrop. */
n_cleartext = bind_listen_socket(conf->listen_addr,
conf->port_cleartext, cleartext_fds);
n_tls = bind_listen_socket(conf->listen_addr,
event_init();
- /* boot-time children: listener and auth only -- store is spawned per-session, see parent_handle_store_fork() */
+ /* boot-time children: listener and auth only, store is spawned per-session, see parent_handle_store_fork() */
fork_child(PROC_LISTENER, &iev_listener, parent_dispatch_child);
fork_child(PROC_AUTH, &iev_auth, parent_dispatch_child);
- /* IMSG_AUTH_INIT before the peer handshake -- auth_main() derives its chroot dir from cred_file */
+ /* IMSG_AUTH_INIT before the peer handshake, auth_main() derives its chroot dir from cred_file */
send_auth_init(iev_auth, conf);
/* IMSG_SETUP_PEER / IMSG_SETUP_DONE, sourced from smtpd.c's setup_peers()/setup_done(); listener<->auth only */
signal_add(&ev_sighup, NULL);
signal_add(&ev_sigterm, NULL);
signal_add(&ev_sigchld, NULL);
- /* SIGPIPE is ignored process-wide in main.c, before fork_child() -- too late here to reach listener/auth, which are already spawned above */
+ /* SIGPIPE is ignored process-wide in main.c, before fork_child(), too late here to reach listener/auth, which are already spawned above */
/* proc/exec/sendfd stay for the process lifetime, not narrowed post-boot, since store is fork-per-session */
#ifdef __OpenBSD__
* char *const argv[] for historical reasons predating C
* const-correctness, and never writes through argv's
* pointers, so this cast is the standard, unavoidable idiom
- * for building an exec() argv array. Routed through
- * uintptr_t, not a direct (char *) cast, matching the real
- * precedent for this exact situation (a const process-title
- * string spliced into an execvp() argv[]) in OpenBSD base's
- * own privsep proc.c, shared by relayd/vmd/iked/snmpd (e.g.
- * usr.sbin/relayd/proc.c's "nargv[proc_i] =
- * (char *)(uintptr_t)p->p_title;") -- confirmed directly
- * against that source, not assumed. Verified with gcc that
- * the uintptr_t hop (unlike a direct (char *) cast) does not
- * trigger -Wcast-qual, since the diagnostic only tracks
- * qualifier loss across a direct pointer-to-pointer cast. */
+ * for building an exec() argv array.
+ */
nargv[n++] = (char *)(uintptr_t)log_procname(type);
for (i = 1; saved_argv[i] != NULL && n < 13; i++) {
/* skip a pre-existing -x <role> from our own argv, everything else passes through */
fatal("calloc");
c->pid = pid;
c->type = type;
- /* NULL here, not "c" -- handlers expect arg == &iev; imsgev_init()'s own fallback self-references &c->iev */
+ /* NULL here, not "c", handlers expect arg == &iev; imsgev_init()'s own fallback self-references &c->iev */
imsgev_init(&c->iev, sp[0], handler, NULL);
TAILQ_INSERT_TAIL(&children, c, entry);
imsg_free(&imsg);
}
-/* dispatch for boot-time listener/auth channels post-boot; task #321: IMSG_AUTH_CRED arrives here from auth, triggering a store spawn directly (no longer relayed via listener's old IMSG_STORE_FORK request) */
+/* dispatch for boot-time listener/auth channels post-boot; IMSG_AUTH_CRED arrives here from auth, triggering a store spawn directly (no longer relayed via listener's old IMSG_STORE_FORK request) */
static void
parent_dispatch_child(int fd, short event, void *arg)
{
struct imsg imsg;
ssize_t n;
- /* EV_WRITE: imsg_compose() only queues -- imsgbuf_write() puts bytes on the wire */
+ /* EV_WRITE: imsg_compose() only queues, imsgbuf_write() puts bytes on the wire */
if (event & EV_WRITE) {
if (imsgbuf_write(&iev->ibuf) == -1)
fatal("imsgbuf_write");
if ((n = imsgbuf_read(&iev->ibuf)) == -1)
fatal("imsgbuf_read");
if (n == 0) {
- /* child's end closed -- reaped via SIGCHLD */
+ /* child's end closed, reaped via SIGCHLD */
event_del(&iev->ev);
return;
}
return (1);
}
-/* task #321: per-session store spawn triggered directly by auth's IMSG_AUTH_CRED; mirrors fork_child() but with a timeout, not fatal() */
+/* per-session store spawn triggered directly by auth's IMSG_AUTH_CRED; mirrors fork_child() but with a timeout, not fatal() */
static void
parent_handle_store_fork(uint32_t session_id, uid_t uid, gid_t gid,
const char *maildir)
struct store_child *it;
unsigned int nchildren = 0;
- /*
- * task #321: uid/gid/maildir now arrive directly from auth, the
- * process that actually resolved them from the credentials file --
- * not relayed through the network-facing listener. These checks stay
- * as defense in depth regardless: they guard against a misconfigured
- * credentials file (e.g. a "root:...:0:0:/" line) or a future bug in
- * auth, not just a hostile sender.
- */
if (uid == 0 || gid == 0) {
log_warnx("refusing store spawn: privileged uid=%u gid=%u "
"for session %u", (unsigned)uid, (unsigned)gid,
goto fail;
}
- /* F5 fix: cap concurrent store children. */
TAILQ_FOREACH(it, &store_children, entry)
nchildren++;
if (nchildren >= STORE_CHILD_MAX) {
close(pair[1]);
- /* allocated once, in place -- sc->iev is never copied afterward (same reason as struct store_child) */
+ /* allocated once, in place, sc->iev is never copied afterward (same reason as struct store_child) */
sc = calloc(1, sizeof(*sc));
if (sc == NULL)
fatal("calloc");
sc->listener_iev = iev_listener;
imsgev_init(&sc->iev, pair[0], store_child_dispatch, sc);
- /* IMSG_STORE_INIT: the one message a store child needs that boot-time children don't -- runtime privilege target */
+ /* IMSG_STORE_INIT: the one message a store child needs that boot-time children don't, runtime privilege target */
init_payload.session_id = session_id;
init_payload.uid = uid;
init_payload.gid = gid;
sizeof(init_payload.spool_root)) >=
sizeof(init_payload.spool_root)) {
log_warnx("session %u: spool_root truncated spawning store "
- "child -- refusing to wire a session to a possibly "
+ "child, refusing to wire a session to a possibly "
"wrong spool root", session_id);
goto fail_kill;
}
- /* task #321: maildir came directly from auth's IMSG_AUTH_CRED, passed through verbatim */
+
if (strlcpy(init_payload.maildir, maildir,
sizeof(init_payload.maildir)) >= sizeof(init_payload.maildir)) {
log_warnx("session %u: maildir truncated spawning store "
- "child -- refusing to wire a session to a possibly "
+ "child, refusing to wire a session to a possibly "
"wrong mailbox", session_id);
goto fail_kill;
}
struct imsg imsg;
ssize_t n;
- /* EV_WRITE: same as parent_dispatch_child() -- imsg_compose() only queues */
+ /* EV_WRITE: same as parent_dispatch_child(), imsg_compose() only queues */
if (event & EV_WRITE) {
if (imsgbuf_write(&sc->iev.ibuf) == -1) {
store_child_fail(sc);
imsg_get_type(&imsg), sc->session_id);
imsg_free(&imsg);
}
- imsgev_add(&sc->iev); /* re-arm -- see this function's header comment */
+ imsgev_add(&sc->iev); /* re-arm, see this function's header comment */
(void)fd;
}
store_child_teardown(sc, 0);
}
-/* binds, sets SO_REUSEADDR, and listen(2)s one socket -- the common tail end of every bind_listen_socket() case */
+/* binds, sets SO_REUSEADDR, and listen(2)s one socket, the common tail end of every bind_listen_socket() case */
static int
bind_one(int family, const struct sockaddr *sa, socklen_t salen,
uint16_t port)
size_t n;
struct stat st;
- /* cert is public -- existence/readability is all that matters */
+ /* cert is public, existence/readability is all that matters */
n = 0;
if ((fp = fopen(conf->tls_cert_file, "r")) == NULL) {
log_warn("fopen %s", conf->tls_cert_file);
log_warn("fstat %s", conf->tls_key_file);
fclose(fp);
} else if (st.st_uid != 0) {
- log_warnx("%s: not owned by uid 0 -- refusing to load "
+ log_warnx("%s: not owned by uid 0, refusing to load "
"(TLS will be disabled)", conf->tls_key_file);
fclose(fp);
} else if (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO) & ~0740) {
- log_warnx("%s: insecure permissions -- must be at most "
+ log_warnx("%s: insecure permissions, must be at most "
"rwxr----- (TLS will be disabled)", conf->tls_key_file);
fclose(fp);
} else {
memset(&init, 0, sizeof(init));
if (strlcpy(init.listen_addr, conf->listen_addr,
sizeof(init.listen_addr)) >= sizeof(init.listen_addr))
- fatalx("listen_addr truncated sending IMSG_LISTENER_INIT -- "
+ fatalx("listen_addr truncated sending IMSG_LISTENER_INIT, "
"config value too long for the wire struct's field");
init.port_cleartext = conf->port_cleartext;
init.port_implicit_tls = conf->port_implicit_tls;
fatal("imsgbuf_flush");
}
-/* IMSG_AUTH_INIT: auth's slice of config -- just cred_file, to derive its chroot dir and unveil() path */
+/* IMSG_AUTH_INIT: auth's slice of config, just cred_file, to derive its chroot dir and unveil() path */
static void
send_auth_init(struct imsgev *iev, struct openimap_config *conf)
{
memset(&init, 0, sizeof(init));
if (strlcpy(init.cred_file, conf->cred_file, sizeof(init.cred_file))
>= sizeof(init.cred_file))
- fatalx("cred_file truncated sending IMSG_AUTH_INIT -- config "
+ fatalx("cred_file truncated sending IMSG_AUTH_INIT, config "
"value too long for the wire struct's field");
if (imsg_compose(&iev->ibuf, IMSG_AUTH_INIT, 0, 0, -1,
memset(&newconf, 0, sizeof(newconf));
if (config_load(conf_path, &newconf) == -1) {
- log_warnx("SIGHUP: %s: reload failed -- keeping the "
+ log_warnx("SIGHUP: %s: reload failed, keeping the "
"already-running configuration", conf_path);
return;
}
newconf.port_cleartext != gconf->port_cleartext ||
newconf.port_implicit_tls != gconf->port_implicit_tls) {
log_warnx("SIGHUP: %s: \"listen on\" changed but listening "
- "sockets cannot be rebound without a restart -- still "
+ "sockets cannot be rebound without a restart, still "
"serving %s:%u / %s:%u", conf_path, gconf->listen_addr,
gconf->port_cleartext, gconf->listen_addr,
gconf->port_implicit_tls);
}
if (strcmp(newconf.cred_file, gconf->cred_file) != 0) {
log_warnx("SIGHUP: %s: \"credentials\" changed but auth is "
- "already chrooted for the old path -- a restart is "
+ "already chrooted for the old path, a restart is "
"required for this to take effect", conf_path);
}
strlen(newconf.tls_cert_file) >= sizeof(gconf->tls_cert_file) ||
strlen(newconf.tls_key_file) >= sizeof(gconf->tls_key_file)) {
log_warnx("SIGHUP: %s: reloaded value too long for gconf's "
- "field -- keeping the already-running configuration",
+ "field, keeping the already-running configuration",
conf_path);
return;
}
- /* Safe to adopt immediately -- see this function's header comment. */
+ /* Safe to adopt immediately, see this function's header comment. */
(void)strlcpy(gconf->spool_root, newconf.spool_root,
sizeof(gconf->spool_root));
gconf->bodystructure_read_max = newconf.bodystructure_read_max;
"(already-authenticated sessions are unaffected)";
if (WIFSIGNALED(status))
- log_warnx("%s[%d] terminated by signal %d -- "
+ log_warnx("%s[%d] terminated by signal %d, "
"%s; run \"rcctl restart imapd\" to "
"recover", log_procname(c->type), pid,
WTERMSIG(status), what);
else if (WIFEXITED(status))
log_warnx("%s[%d] exited unexpectedly, "
- "status %d -- %s; run \"rcctl restart "
+ "status %d, %s; run \"rcctl restart "
"imapd\" to recover",
log_procname(c->type), pid,
WEXITSTATUS(status), what);
else
- log_warnx("%s[%d] exited unexpectedly -- %s; "
+ log_warnx("%s[%d] exited unexpectedly, %s; "
"run \"rcctl restart imapd\" to recover",
log_procname(c->type), pid, what);
TAILQ_REMOVE(&children, c, entry);
blob - 9846dc4ba7e91776deb0293561504738804eddee
blob + 3833ac0b7d22c93dcfaa39efaa5c0ea8d2b8894f
--- src/parse.y
+++ src/parse.y
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * parse.y -- imapd.conf grammar.
- *
- * config_load() (the only function this file exposes -- see imapd.h's
- * prototype) replaces parent.c's old stub of the same name, which only
- * ever filled in v1 defaults and ignored the config file path entirely.
- *
- * Structure and most of the machinery below (the pushfile()/popfile()
- * file stack for "include", the lgetc()/lungetc()/findeol() character-
- * pushback pair, the hand-written yylex() built on top of them, the
- * symset()/symget()/cmdline_symset() $macro/-D variable table, and
- * check_file_secrecy()) are this project's own rewrite of the same
- * pattern used throughout the OpenBSD base system's own daemons -- sourced
- * directly against ripd's parse.y (src/usr.sbin/ripd/parse.y, read in
- * full this pass) for the generic boilerplate, and against smtpd's and
- * httpd's parse.y (src/usr.sbin/smtpd/parse.y, src/usr.sbin/httpd/
- * parse.y) for the "include" grammar rule and the "listen on <addr>
- * [tls] port <port>" grammar shape respectively. None of these daemons'
- * domain-specific grammar (RIP redistribution rules, SMTP rulesets,
- * HTTP server blocks) is reused here -- only the parser skeleton and the
- * listen-line shape. This project's own grammar covers exactly the eight
- * fields in struct openimap_config (imapd.h): the two listeners,
- * spool_root, cred_file, tls_cert_file, tls_key_file, and (added this
- * pass) bodystructure_read_max via the "attachment max <bytes>"
- * directive -- see that grammar rule's own comment below for why this
- * one field gets range-validated where the others don't.
- *
- * Deliberately NOT following ripd/smtpd's convention in one place: those
- * daemons' top-level parse function allocates and returns their own
- * config struct. config_load()'s signature is already fixed by imapd.h
- * (a caller-supplied struct openimap_config * to fill in, matching the
- * existing prototype main.c already calls) and by the man page already
- * documenting it, so this file fills that struct in place instead of
- * allocating its own.
- *
- * Full-parity scope decision (AskUserQuestion, this pass): rather than a
- * trimmed-down parser with just the core grammar, this includes the same
- * $macro / -D command-line variable support and "include" file support
- * every OpenBSD base daemon's config grammar has, even though a flat
- * seven-field config has only modest use for either -- matching upstream
- * convention closely was judged more valuable than the modest size
- * savings a trimmed version would give, particularly given this project's
- * long-term (if distant) OpenBSD base-inclusion aspiration.
- */
+/* parse.y, imapd.conf grammar. */
%{
#include <sys/types.h>
static struct openimap_config *conf;
static int errors = 0;
-/*
- * Tracks whether a "listen on ..." line for each of the two listeners has
- * already been seen this parse, both to reject a second one for the same
- * listener and to detect a second listener whose address doesn't match
- * the first -- struct openimap_config has only one listen_addr shared by
- * both ports (bind_listen_socket() is called once per port against that
- * same field, parent.c), so two "listen on" lines naming different
- * addresses would silently mean "the second one wins" without this check,
- * which is far more likely to be a config mistake than an intentional
- * per-listener address split this implementation doesn't actually support.
- */
static int have_cleartext = 0;
static int have_tls_listen = 0;
YYERROR;
}
- /*
- * "*" (dual-stack, both IPv4-any and IPv6-any) or a
- * literal IPv4/IPv6 address only -- no getaddrinfo(3)
- * hostname resolution, a deliberate scope decision
- * for this pass (see LISTENER_MAX_ADDRS's comment in
- * imapd.h for the full reasoning). Checked here, at
- * parse time, so a bad address is rejected with a
- * clear config-file error instead of surfacing later
- * as parent.c's bind_listen_socket() fatal()ing at
- * startup.
- */
+ /* "*" dual-stack, both IPv4-any and IPv6-any */
if (strcmp($3, "*") != 0) {
struct in_addr ina;
struct in6_addr ina6;
if (strcmp(conf->listen_addr, $3) != 0) {
yyerror("listen address \"%s\" does "
"not match earlier \"listen on "
- "%s\" -- imapd binds both "
+ "%s\", imapd binds both "
"listeners to the same address",
$3, conf->listen_addr);
free($3);
}
| ATTACHMENT MAX NUMBER {
/*
- * Range-validated, unlike the other directives above:
- * this value flows straight into a per-FETCH malloc()
- * in store.c's read_message_body() (readbuf_size =
- * maxlen + 1), once per session-child process, so an
- * operator typo here (an extra zero, a value meant as
- * megabytes typed as bytes) has a real memory-exhaustion
- * consequence that a bad path string or port number
- * doesn't. Lower bound matches BODYSTRUCTURE_MAX
- * (imapd.h) as a floor -- below that, no message could
- * ever produce a usable BODYSTRUCTURE anyway, so a
- * smaller value is certainly a mistake, not a real
- * intended restriction. Upper bound (1 GiB) is a
- * generous, round sanity ceiling, not a protocol limit
- * -- personal mail attachments this large are not a
- * real-world case this implementation targets (see
- * BODYSTRUCTURE_READ_DEFAULT's own Gmail-sourced
- * reasoning), and rejecting it here surfaces the
- * mistake at config-parse time rather than as a
- * confusing malloc failure or slow FETCH later.
+ * Range-validated
*/
if ($3 < BODYSTRUCTURE_MAX || $3 > 1073741824) {
yyerror("attachment max out of range "
x != ','))
/*
- * '*' added alongside the pre-existing ':' (there for bare IPv6
- * literals like "::") so the dual-stack "listen on *" wildcard from
- * the IPv6 pass works unquoted, matching every other address form
- * documented in imapd.conf.example -- found as a real bug, not
- * designed in up front: an unquoted "listen on * port 143" line
- * failed with a bare "syntax error" on real hardware before
- * this fix, since '*' alone satisfied none of the original starting
- * characters here and fell through to being returned as a raw,
- * unexpected single-character token instead of ever entering this
- * bareword-accumulation loop. Quoting it ("listen on \"*\" ...")
- * already worked, since the quoted-string branch above this one
- * doesn't consult allowed_in_string() at all -- this fix is only
- * about making the unquoted form work too.
+ * '*' added alongside the pre-existing ':'
*/
if (isalnum(c) || c == ':' || c == '_' || c == '*') {
do {
/*
* Same root-owned-or-current-user, not-group-or-world-writable check this
- * project already applies to the TLS private key file (parent.c's
- * send_tls_certs(), added when that boot-deadlock/permission-check pass
- * closed a real gap there) -- applied here to the top-level config file
- * itself, since it can name the credentials file path and TLS key path,
- * matching every base-system daemon's own parse.y (ripd's is the direct
- * source for this function, byte-for-byte apart from the log_warn/
- * log_warnx call signatures matching this project's own log.h instead of
- * ripd's warn(3)/warnx(3)-based logit() wrappers). Not applied to
- * "include"d files (pushfile()'s second argument is 0 for those) --
- * matching smtpd's own convention of only checking the top-level file.
+ * project already applies to the TLS private key file
*/
static int
check_file_secrecy(int fd, const char *fname)
}
/*
- * config_load(): imapd.h's public entry point (replaces parent.c's old
- * config_load() stub of the same name/signature -- see this file's header
- * comment). Fills xconf with the v1 defaults first (same values the old
- * stub always used unconditionally), then parses path over those defaults
- * -- so a config file that sets only, say, "spool" leaves every other
- * field at its documented v1 default rather than requiring a client to
- * spell out all seven fields every time. Returns 0 on success (xconf
- * fully populated) or -1 (a parse error occurred; xconf's contents are
- * unspecified) -- same "reject rather than run with a half-parsed config"
- * precedent as every other MAX-constant/parse-failure case in this
- * codebase, left to the caller (main.c) to treat as fatal.
+ * config_load(): imapd.h's public entry point.
*/
int
config_load(const char *path, struct openimap_config *xconf)
}
/*
- * cmdline_symset(): "-D name=value" command-line macro definitions,
- * matching ripd/smtpd's own "-D" flag exactly -- called directly from
- * main.c's getopt(3) loop, before config_load() runs, so these persist
- * (the "1" argument to symset()) across the parse: a "-D" definition
- * always wins over the same name defined inside the config file itself.
+ * cmdline_symset(): "-D name=value" command-line macro definitions.
*/
int
cmdline_symset(char *s)
blob - bbbf2d8cb4c55ae27eed5cb70cec9c3ff817bb28
blob + b3a1b82e5edd90490b6a9157af573b2455daa5eb
--- src/rc.d/imapd
+++ src/rc.d/imapd
#
# $OpenIMAPD$
#
-# rc.d(8) service script for imapd(8). imapd does not detach from
-# its controlling terminal or otherwise background itself (see
-# imapd.8) -- rc_bg=YES tells rc.subr to run it under "set -o monitor"
-# instead (own process group, avoids SIGHUP at boot), which is rc.subr's
-# documented idiom for exactly this kind of foreground-only daemon. No
-# rc_start override is needed: the default "rc_exec ${daemon}
-# ${daemon_flags}" is sufficient with rc_bg=YES alone -- confirmed against
-# ypbind's real, shipped rc.d script (/etc/rc.d/ypbind), which uses the
-# identical rc_bg=YES-with-default-rc_start pattern for the same stated
-# reason ("avoid SIGHUP at boot").
-#
-# rc_stop needs no override either: parent.c's sigterm_handler already
-# cascades SIGTERM to every child (listener, auth, each per-session store
-# child) on receipt of a single SIGTERM to the root parent -- exactly what
-# rc.subr's default rc_stop() sends (pkill -TERM against the tracked
-# process). See parent.c for that cascade logic.
+# rc.d(8) service script for imapd(8).
daemon="/usr/local/sbin/imapd"
rc_bg=YES
-# --- boot-time relink consumer -------------------------------------------
-#
-# "make install" (see ../Makefile's RELINK=) drops a re-link kit at
-# ${_relink_tar} if the Makefile's RELINK variable is set, per bsd.prog.
-# mk's documented re-link-kit mechanism. Nothing on this system consumes
-# it automatically the way base does for libc/libcrypto/ld.so/sshd:
-# /etc/rc's reorder_libs() has a hardcoded allowlist (confirmed by reading
-# /etc/rc directly) that doesn't and won't include imapd -- extending
-# it would mean patching base /etc/rc, which gets clobbered on every base
-# upgrade. This hook is the substitute consumer.
-#
-# It runs every time this service is (re)started -- at boot via
-# pkg_scripts, or by hand via "rcctl restart imapd" -- checks for a
-# pending tarball, and if present, extracts it and runs the install.sh
-# that bsd.prog.mk generated inside it. That install.sh recompiles ${PROG}
-# from the tarball's own object files in a freshly randomized link order,
-# smoke-tests the result via the Makefile's RELINK command ("imapd -V",
-# see main.c -- prints a version string and exits 0 with no other side
-# effects), and only then installs the result over the currently running
-# binary.
-#
-# Failure here is deliberately non-fatal to starting the service: if the
-# relink or its smoke test fails, install.sh's own "install -c -s ..."
-# step never runs, so the binary already at ${daemon} (from the last
-# successful relink, or a plain "make install") is untouched. We log a
-# warning, leave the tarball in place for investigation, and fall through
-# to starting that known-good binary rather than taking the mail server
-# down over a relink hiccup -- KARL's own kernel relink (reorder_kernel.sh)
-# follows the same non-fatal-on-failure philosophy for the same reason.
_relink_tar="/usr/share/relink/usr/local/sbin/imapd/imapd.tar"
rc_pre() {
blob - ad9e1ac1f13afb0e026401cb2f682807957c83e1
blob + 21cf4cd6b50c99435f432d649865f16d0c19b57b
--- src/search_cmd.c
+++ src/search_cmd.c
*/
/*
- * search_cmd.c -- SEARCH: key parsing, evaluation dispatch, and
+ * search_cmd.c, SEARCH: key parsing, evaluation dispatch, and
* the async IMSG_MBOX_SEARCH_MATCH/IMSG_MBOX_RESULT completion path.
*/
tm.tm_mday = day;
tm.tm_mon = i;
tm.tm_year = year - 1900;
- /* tm_hour/tm_min/tm_sec already 0 from memset -- UTC midnight */
+ /* tm_hour/tm_min/tm_sec already 0 from memset, UTC midnight */
if ((t = timegm(&tm)) == (time_t)-1)
return (-1);
return (0);
}
-/* reads one sequence-set token, rejects an internal comma (v1's single-range restriction), via parse_seq_range() */
+/* reads one sequence-set token, rejects an internal comma, via parse_seq_range() */
static int
parse_search_seqset_token(char **pp, uint32_t *lo, uint32_t *hi,
int *lo_star, int *hi_star, const char **errmsg)
if (strchr(tok, ',') != NULL) {
*errmsg = "comma-separated sequence sets not "
- "supported in v1 -- issue separate SEARCH "
+ "supported, issue separate SEARCH "
"commands";
return (-1);
}
else if (strcasecmp(word, "COUNT") == 0)
*opts_out |= SEARCH_RETURN_COUNT;
else if (strcasecmp(word, "SAVE") == 0) {
- *errmsg = "SEARCH RETURN (SAVE) -- the \"$\" search "
- "result variable -- is not supported in this "
+ *errmsg = "SEARCH RETURN (SAVE), the \"$\" search "
+ "result variable, is not supported in this "
"pass";
return (-2);
} else {
(unsigned long long)s->search_max_modseq);
/*
- * len holds snprintf(3)'s "would-be" length, which can run past
- * sizeof(buf) (e.g. a huge ALL sequence-set plus COUNT/MODSEQ
- * tails pushes it over even though format_seq_list() itself was
- * bounded). The two explicit buf[len++] writes below need two
- * free slots, so reserve two bytes, not one -- "- 1" here was an
- * off-by-one that let buf[sizeof(buf)] get written, one byte past
- * the end of this stack array.
+ * len holds snprintf(3)'s "would-be" length, which can run past sizeof(buf).
*/
if (len >= sizeof(buf) - 1)
len = sizeof(buf) - 2;
blob - b0c7909da499fde858f7d85ef79e114888e3e7dc
blob + 913c52e22655c40876409ed6770e1b31da4e9cef
--- src/store.c
+++ src/store.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* store.c -- mailbox-store process: one per session, privilege-dropped, handles IMSG_MBOX_*. */
+/* store.c, mailbox-store process: one per session, privilege-dropped, handles IMSG_MBOX_*. */
#include <sys/types.h>
#include <sys/file.h>
for (i = 0; i < len; i++) {
unsigned char c = (unsigned char)name[i];
- /* RFC 9051 SS5.1.1: "/" is the hierarchy delimiter; v1 is flat, so it's never addressable */
+ /* RFC 9051 SS5.1.1: "/" is the hierarchy delimiter */
if (c == '/')
return (0);
/* SS5.1 point 2: MAY refuse CTL/non-graphic names; taking that MAY for ASCII C0/DEL */
return (0);
}
- /* "tmp"/"new"/"cur" are INBOX's own maildir internals -- refusing them here prevents cross-mailbox corruption */
+ /* "tmp"/"new"/"cur" are INBOX's own maildir internals, refusing them here prevents cross-mailbox corruption */
if (strcmp(name, "tmp") == 0 || strcmp(name, "new") == 0 ||
strcmp(name, "cur") == 0)
return (0);
if ((n = imsgbuf_read(&iev->ibuf)) == -1)
fatal("imsgbuf_read");
if (n == 0) {
- /* listener's end closed -- treat like IMSG_STORE_SHUTDOWN: nothing left to serve */
+ /* listener's end closed, treat like IMSG_STORE_SHUTDOWN: nothing left to serve */
store_shutdown();
/* NOTREACHED */
}
break;
}
case IMSG_MBOX_IDLE_REFRESH:
- /* no request payload -- see imapd.h's imsg_mbox_idle_uid comment */
+ /* no request payload, see imapd.h's imsg_mbox_idle_uid comment */
handle_mbox_idle_refresh(iev);
break;
case IMSG_MBOX_APPEND: {
blob - b93a07a57b01c11ccb66e38b1ab919530c1e1f65
blob + 90e9426c1da3331eb9f0a61f8a8db04c5a1f0a38
--- src/store_cmd.c
+++ src/store_cmd.c
*/
/*
- * store_cmd.c -- STORE/EXPUNGE/CLOSE/UNSELECT/IDLE/COPY/MOVE/UID:
+ * store_cmd.c, STORE/EXPUNGE/CLOSE/UNSELECT/IDLE/COPY/MOVE/UID:
* command-select handlers plus their async completion paths.
*/
return (1);
}
-/* RFC 9051 SS6.4.1: CLOSE removes \Deleted with no untagged EXPUNGE -- via session_request_expunge(silent=1). */
+/* RFC 9051 SS6.4.1: CLOSE removes \Deleted with no untagged EXPUNGE, via session_request_expunge(silent=1). */
int
cmd_close(struct session *s, const char *tag, char *args)
{
return session_request_expunge(s, tag, 1, 0, 0, 0, 0, 0);
}
-/* RFC 9051 SS6.4.2: like CLOSE but removes nothing -- a purely local state change. */
+/* RFC 9051 SS6.4.2: like CLOSE but removes nothing, a purely local state change. */
int
cmd_unselect(struct session *s, const char *tag, char *args)
{
else if (strcasecmp(tok, "\\Draft") == 0)
sysflags |= MBOX_FLAG_DRAFT;
else if (strcasecmp(tok, "\\Recent") == 0) {
- *errmsg = "\\Recent cannot be set -- RFC 9051 "
+ *errmsg = "\\Recent cannot be set, RFC 9051 "
"deprecates it and excludes it from the "
"flag grammar entirely";
return (-2);
} else {
- *errmsg = "unsupported system flag -- v1 only "
+ *errmsg = "unsupported system flag"
"supports \\Answered/\\Flagged/\\Deleted/"
"\\Seen/\\Draft";
return (-2);
}
if (strchr(tok, ':') != NULL || strchr(tok, ',') != NULL) {
- *errmsg = "keyword contains ':' or ',' -- not "
+ *errmsg = "keyword contains ':' or ',', not "
"representable in this server's index format";
return (-2);
}
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 STORE commands");
return (1);
}
}
if (s->mbox_readonly) {
- /* RFC 9051 SS6.3.3: no changes on EXAMINE'd mailbox (RFC 5530 CANNOT) -- same check as session_request_expunge(). */
+ /* RFC 9051 SS6.3.3: no changes on EXAMINE'd mailbox (RFC 5530 CANNOT), same check as session_request_expunge(). */
session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
"(selected via EXAMINE)");
return (1);
return (1);
}
- /* req already memset(3)'d and has_unchangedsince set earlier -- not re-zeroed here, to avoid wiping it out. */
+ /* req already memset(3)'d and has_unchangedsince set earlier, not re-zeroed here, to avoid wiping it out. */
req.seq_lo = lo;
req.seq_hi = hi;
req.lo_is_star = lo_star;
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 COPY/MOVE commands");
return (1);
}
}
if (is_move && s->mbox_readonly) {
- /* MOVE removes source messages (SS6.4.8) -- a change SS6.3.3 prohibits when EXAMINE'd; COPY is unaffected. */
+ /* MOVE removes source messages (SS6.4.8), a change SS6.3.3 prohibits when EXAMINE'd; COPY is unaffected. */
session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
"(selected via EXAMINE)");
return (1);
session_untagged(s, buf);
}
-/* RFC 7162 SS3.2.6: one VANISHED (EARLIER) range, written immediately -- unlike QRESYNC SELECT's buffered resync. */
+/* RFC 7162 SS3.2.6: one VANISHED (EARLIER) range, written immediately, unlike QRESYNC SELECT's buffered resync. */
void
session_handle_fetch_vanished(struct session *s,
const struct imsg_mbox_select_vanished *v)
if (s->mbox_readonly) {
if (is_close) {
- /* RFC 9051 SS6.4.1: EXAMINE'd -- skip the round trip, just deselect and reply OK, no error. */
+ /* RFC 9051 SS6.4.1: EXAMINE'd, skip the round trip, just deselect and reply OK, no error. */
s->state = SESSION_AUTHENTICATED;
session_reply(s, tag, "OK", "CLOSE completed");
return (1);
}
- /* Unlike CLOSE, plain/UID EXPUNGE has no read-only exception -- SS6.3.3 applies directly (RFC 5530 CANNOT). */
+ /* Unlike CLOSE, plain/UID EXPUNGE has no read-only exception, SS6.3.3 applies directly (RFC 5530 CANNOT). */
session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
"(selected via EXAMINE)");
return (1);
}
if (strchr(args, ',') != NULL) {
session_reply(s, tag, "BAD",
- "comma-separated sequence sets not supported in v1 -- "
+ "comma-separated sequence sets not supported"
"issue separate UID EXPUNGE commands");
return (1);
}
if (res->error != MBOX_OP_OK || s->copy_alloc_failed) {
char text[48];
- /* RFC 9051 SS6.4.7/SS6.4.8: destname valid but not found -- TRYCREATE, mirroring imsg_mbox_appended's field. */
+ /* RFC 9051 SS6.4.7/SS6.4.8: destname valid but not found, TRYCREATE, mirroring imsg_mbox_appended's field. */
if (!s->copy_alloc_failed &&
res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX)
snprintf(text, sizeof(text), "[TRYCREATE] no such "
blob - 14b7c31bb86a1912212a2074a0f5bb4d66a11915
blob + db2690adc00fb3bbeeb0b7eceb072944d0994ccf
--- src/store_internal.h
+++ src/store_internal.h
*/
/*
- * store_internal.h -- internal, store-process-only shared declarations.
- *
- * Not installed, not part of imapd's public wire protocol (that's
- * imapd.h) -- this exists solely so store.c's core (the imsg dispatch
- * loop) and the eight files split out of what used to be one 7,768-line
- * store.c (index.c, mime.c, envelope.c, mbox_fetch.c, mbox_search.c,
- * mbox_store.c, mbox_manage.c, mbox_copy.c, and store.c itself) can all
- * see struct mbox_index/struct index_rec and each other's entry points.
- * Nothing in here crosses process boundaries -- that's imapd.h's job,
- * behind the imsg wire structs instead.
+ * store_internal.h, internal, store-process-only shared declarations.
*/
#ifndef STORE_INTERNAL_H
uint64_t highestmodseq; /* RFC 7162 SS3.1: per-mailbox highest
* mod-sequence, persisted as the
* index header's third field (see
- * index_load()/index_save()). v1's
- * only mailbox always supports this
- * (there is no on-disk format that
- * predates it in a real deployment of
- * this not-yet-released server), so
- * the NOMODSEQ response code
- * (RFC 7162 SS3.1.2.2) is simply
- * unreachable in this implementation. */
+ * index_load()/index_save()). Always
+ * supported, so NOMODSEQ (RFC 7162
+ * SS3.1.2.2) is unreachable here. */
char **lines; /* raw "UID:basename:keywords:MODSEQ"
* lines, no trailing newline, one
- * malloc(3) each -- the MODSEQ field
- * is this pass's addition; see
+ * malloc(3) each; see
* index_parse_line() */
size_t nlines;
size_t cap;
};
/*
- * One index message line, parsed. Introduced this pass (RFC 7162) to stop
- * handle_mbox_fetch()/handle_mbox_store()/handle_mbox_expunge() from each
- * hand-rolling their own "UID:basename:keywords" strchr() chain -- adding
- * a fourth colon-delimited field (MODSEQ) to every one of those independently
- * would have tripled the size of this change and the chance of one of them
- * drifting out of sync with the on-disk format. basename/keywords are
- * fixed-size copies (same 512-byte bound index_load()'s own STORE_INDEX_
- * LINE_MAX line buffer already implies is generous for either field), not
- * pointers into the original line -- callers are free to mutate or discard
- * the line string after parsing.
+ * One index message line, parsed, so handle_mbox_fetch()/handle_mbox_
+ * store()/handle_mbox_expunge() don't each hand-roll their own
+ * "UID:basename:keywords:MODSEQ" strchr() chain. Basename/keywords are
+ * fixed-size copies, not pointers into the original line, callers may
+ * mutate or discard the line string after parsing.
*
- * Backward compatibility: the MODSEQ field is new this pass. A line with
- * only three colon-delimited fields (written by a pre-CONDSTORE build of
- * this server) parses successfully with modseq defaulted to 1 -- this
- * server has never had a real deployment to be compatible *with*, so this
- * is a defensive nicety, not a migration guarantee this project is making
- * any promise about.
+ * A line with only three colon-delimited fields (pre-CONDSTORE) parses
+ * successfully with modseq defaulted to 1.
*/
struct index_rec {
uint32_t uid;
/*
* One message staged by stage_copy_messages() (mbox_copy.c) for
- * handle_mbox_copy()/handle_mbox_move() -- read fully into memory from
- * the source mailbox, not yet written anywhere. Shared here (not local
- * to mbox_copy.c) because commit_copy_messages() and move_cross_mailbox()
- * take it as a parameter type too.
+ * handle_mbox_copy()/handle_mbox_move(). Read fully into memory from
+ * the source mailbox, not yet written. Shared here since commit_copy_
+ * messages() and move_cross_mailbox() also take it as a parameter type.
*/
struct copy_staged {
uint32_t src_uid;
size_t bodylen;
};
-/*
- * F6 fix: bound how much message data COPY/MOVE holds in memory at once.
- * stage_copy_messages() reads every message in the range fully into RAM
- * before commit (required for RFC 9051 6.4.7 all-or-nothing COPY), so an
- * unbounded range of large, externally-delivered messages could exhaust
- * the store child's address space. Exceeding either limit fails the whole
- * COPY/MOVE cleanly (no partial copy). Tunable for larger deployments.
- */
#define COPY_STAGE_MSG_MAX ((uint64_t)64 * 1024 * 1024) /* per message */
#define COPY_STAGE_TOTAL_MAX ((uint64_t)512 * 1024 * 1024) /* per operation */
-/*
- * Globals shared across the files this process's source is split into
- * (definitions live in store.c's core; see that file for why each
- * exists).
+/* Globals shared across the files this process's source is split into
+ * (definitions live in store.c's core).
*/
extern uint32_t session_id;
-extern uint32_t append_counter;
-extern char current_mailbox_dir[MBOX_NAME_MAX];
+extern uint32_t append_counter; /* per-store-child monotonic counter
+ * feeding the maildir basename uniquer
+ * (see handle_mbox_append()); shared with
+ * handle_mbox_copy() so a copy's minted
+ * basename can't collide with an APPEND's */
+extern char current_mailbox_dir[MBOX_NAME_MAX]; /* named mailbox (if
+ * any) this store child's cwd is chdir'd
+ * into, relative to the maildir root; ""
+ * means INBOX/nothing selected.
+ * handle_mbox_select() is the only writer */
-/*
- * Cross-file entry points. Same set of forward declarations store.c
- * carried as `static` prototypes before the file was split into store.c
- * (core) + index.c + mime.c + envelope.c + mbox_fetch.c + mbox_search.c
- * + mbox_store.c + mbox_manage.c + mbox_copy.c -- moved here verbatim
- * (minus `static`) rather than redesigned, so this split is a pure move
- * with no behavior change.
+/* Cross-file entry points: forward declarations for store.c (core) +
+ * index.c + mime.c + envelope.c + mbox_fetch.c + mbox_search.c +
+ * mbox_store.c + mbox_manage.c + mbox_copy.c.
*/
-extern uint32_t bodystructure_read_max; /* from IMSG_STORE_INIT --
- * see imapd.h's struct imsg_store_init
- * comment and BODYSTRUCTURE_READ_
- * DEFAULT's comment for what this
- * gates and where the operator-
- * configured value comes from. Missing
- * `extern` here once meant every file
- * that included this header got its
- * own tentative definition -- harmless
- * within a single translation unit
- * (which is how the original,
- * unsplit store.c got away with the
- * same bare declaration), but nine
- * separate definitions once split
- * across files, caught as a real
- * duplicate-symbol link error. */
+extern uint32_t bodystructure_read_max; /* from IMSG_STORE_INIT; see
+ * imapd.h's imsg_store_init and
+ * BODYSTRUCTURE_READ_DEFAULT
+ * comments */
-/*
- * Per-store-child monotonic counter feeding the maildir basename uniquer
- * (see handle_mbox_append()'s header comment for the full `<timestamp>.
- * <pid>_<counter>.<hostname>` scheme). Was originally a function-local
- * static inside handle_mbox_append() alone; hoisted to file scope this
- * pass so handle_mbox_copy() -- COPY's own message-duplication path, which
- * mints a brand new basename per copied message the same way APPEND does
- * -- shares the same monotonic sequence rather than starting its own at 0,
- * which could otherwise collide with an APPEND's basename minted in the
- * same second by the same pid.
- */
-
-/*
- * RFC 9051 SS6.3.4-SS6.3.6/SS6.3.9 addition (flat multi-mailbox support):
- * which named mailbox (if any) this store child's cwd is currently
- * chdir'd into,
- * relative to the session's own maildir root. Empty string means "at the
- * root" -- i.e. INBOX, or nothing selected yet -- the same state every
- * store child has always implicitly been in before this pass, when INBOX
- * was the only mailbox that could ever exist. handle_mbox_select() is the
- * only function that ever changes this; every other IMSG_MBOX_* handler
- * (FETCH/STORE/EXPUNGE/IDLE-refresh/QRESYNC/...) keeps using bare
- * relative paths exactly as before, unmodified, since cwd is always
- * "wherever the currently selected mailbox is" by construction.
- */
-
void store_dispatch(int, short, void *);
void store_shutdown(void);
void handle_mbox_select(struct imsg_mbox_select *, struct imsgev *);
/*
* The maildir+index format: stock maildir (tmp/new/cur) for message
* bodies, plus one small line-oriented text index per mailbox holding
- * UIDVALIDITY/UIDNEXT and the UID<->basename(<->keywords) map. v1 is
- * INBOX-only, and that index lives directly in
- * the maildir root store.c is now chdir'd into -- a sibling of tmp/new/
- * cur, not inside any of them. Name is plain and `ls`-visible on
- * purpose, matching this project's own stated preference for
- * boring/inspectable formats over hidden dotfiles.
+ * UIDVALIDITY/UIDNEXT and the UID<->basename(<->keywords) map. The index
+ * lives directly in that mailbox's maildir root, a sibling of tmp/new/
+ * cur. Name is plain and `ls`-visible on purpose.
*/
#define STORE_INDEX_NAME "imapd.index"
#define STORE_INDEX_TMP_NAME "imapd.index.tmp"
-#define STORE_INDEX_LINE_MAX 1024 /* matches auth.c's cred_lookup()
- * line-buffer precedent for the same
- * kind of small, personal-scope flat
- * text file */
+#define STORE_INDEX_LINE_MAX 1024
-/*
- * In-memory copy of one mailbox's index while a single IMSG_MBOX_SELECT
- * (or, later, any other mutating mbox op) is being handled -- built fresh
- * from the on-disk file, mutated, and rewritten in full per the design
- * doc's resolved "whole file rewritten to a temp file and rename(2)'d
- * over on every mutation" rule, then discarded. Never kept around between
- * imsg messages.
+/* In-memory copy of one mailbox's index while a single mutating mbox op
+ * is handled. Built fresh from the on-disk file, mutated, and
+ * rewritten in full (temp file + rename(2)) on every mutation, then
+ * discarded. Never kept around between imsg messages.
*/
-
int index_load(int, struct mbox_index *);
int index_has_basename(struct mbox_index *, const char *);
int index_append(struct mbox_index *, uint32_t, const char *);
/*
* The remaining six are single-purpose helpers that happen to be called
- * from more than one of the split-out files (unlike the bulk of each
- * file's own static helpers, which stayed file-local) -- see each
- * definition's own comment for why.
+ * from more than one of the split-out files.
*/
const char *append_hostname(void);
int ensure_maildir_dirs(const char *);
blob - 17b76e30c7c676291164a0bd047b0c49a090866d
blob + 4e312adf98b142d52cacc81428dddf30a46cd113
--- src/store_ipc.c
+++ src/store_ipc.c
*/
/*
- * store_ipc.c -- listener-side dispatch of the store process's async imsg
+ * store_ipc.c, listener-side dispatch of the store process's async imsg
* protocol: session_handle_mbox_*()/session_finish_*() reply handlers.
*/
struct imsg_mbox_fetch_body bodyhdr;
size_t bodylen;
- /* Same technique as IMSG_MBOX_FETCH_HEADER just above -- see that case's comment. */
+ /* Same technique as IMSG_MBOX_FETCH_HEADER just above, see that case's comment. */
if (imsg_get_buf(&imsg, &bodyhdr, sizeof(bodyhdr)) ==
-1) {
log_warnx("bad IMSG_MBOX_FETCH_BODY (header)");
s->pending_body_buf = NULL;
s->pending_body_len = 0;
s->pending_body_found = 0;
- /* pending_body_label/has_partial/partial_origin are NOT reset here -- set once per FETCH in fetch_dispatch(). */
+ /* pending_body_label/has_partial/partial_origin are NOT reset here, set once per FETCH in fetch_dispatch(). */
bodylen = imsg_get_len(&imsg);
if (!bodyhdr.found)
struct imsg_mbox_fetch_envelope envhdr;
size_t envlen;
- /* Same technique as IMSG_MBOX_FETCH_HEADER/IMSG_MBOX_FETCH_BODY -- see IMSG_MBOX_FETCH_HEADER's comment. */
+ /* Same technique as IMSG_MBOX_FETCH_HEADER/IMSG_MBOX_FETCH_BODY, see IMSG_MBOX_FETCH_HEADER's comment. */
if (imsg_get_buf(&imsg, &envhdr, sizeof(envhdr)) ==
-1) {
log_warnx("bad IMSG_MBOX_FETCH_ENVELOPE (header)");
struct imsg_mbox_fetch_bodystructure bshdr;
size_t bslen;
- /* Same technique as IMSG_MBOX_FETCH_HEADER/IMSG_MBOX_FETCH_ENVELOPE -- see IMSG_MBOX_FETCH_HEADER's comment. */
+ /* Same technique as IMSG_MBOX_FETCH_HEADER/IMSG_MBOX_FETCH_ENVELOPE, see IMSG_MBOX_FETCH_HEADER's comment. */
if (imsg_get_buf(&imsg, &bshdr, sizeof(bshdr)) ==
-1) {
log_warnx("bad IMSG_MBOX_FETCH_BODYSTRUCTURE "
imsgev_add(s->store_iev); /* re-arm for the next round of IMSG_MBOX_* replies */
(void)fd;
- /* If the reply just processed above was the terminal one, s->state is idle again -- drain anything pipelined behind it. */
+ /* If the reply just processed above was the terminal one, s->state is idle again, drain anything pipelined behind it. */
if (!session_dequeue_next(s))
- return; /* s was torn down by a queued LOGOUT -- do not touch */
+ return; /* s was torn down by a queued LOGOUT, do not touch */
}
/* Finishes SELECT (SS6.3.2); flushes QRESYNC resync in RFC order: VANISHED (EARLIER), then FETCH, then tagged OK. */
size_t vlen;
int flen;
- /* session_untagged()'s 512-byte buffer is too small here -- same bypass session_finish_search() uses for ESEARCH. */
+ /* session_untagged()'s 512-byte buffer is too small here, same bypass session_finish_search() uses for ESEARCH. */
vlen = format_range_list(vbuf, sizeof(vbuf),
s->vanished_ranges, s->vanished_nranges, &truncated);
if (truncated)
s->state = s->status_prev_state;
if (res->error != MBOX_OP_OK) {
- /* Valid-but-missing name or store.c's open/flock/index_load failure -- can't tell apart, so NONEXISTENT covers both. */
+ /* Valid-but-missing name or store.c's open/flock/index_load failure, can't tell apart, so NONEXISTENT covers both. */
session_reply(s, s->pending_tag, "NO",
"[NONEXISTENT] no such mailbox");
return;
}
- /* Quoted, not bare -- mailbox names can contain spaces; no backslash-escaping, same gap parse_list_token() has. */
+ /* Quoted, not bare, mailbox names can contain spaces; no backslash-escaping, same gap parse_list_token() has. */
len = (size_t)snprintf(buf, sizeof(buf), "STATUS \"%s\" (",
s->status_mailbox);
if (s->status_attrs & STATUS_ATT_MESSAGES)
STATUS_APPEND("MESSAGES %u", res->messages);
if (s->status_attrs & STATUS_ATT_RECENT)
- /* IMAP4rev2 dropped \Recent (RFC 9051 SS2.3.2) -- this
+ /* IMAP4rev2 dropped \Recent (RFC 9051 SS2.3.2), this
* server tracks no such state, so always answer 0; placed
* here to mirror IMAP4rev1's conventional MESSAGES/RECENT
* ordering, though RFC 9051 response order is unspecified
"[ALREADYEXISTS] mailbox already exists");
return;
default:
- /* mkdir/stat/rename(2) I/O error or truncated buffer -- no RFC 5530 code fits, plain NO. */
+ /* mkdir/stat/rename(2) I/O error or truncated buffer, no RFC 5530 code fits, plain NO. */
snprintf(text, sizeof(text), "%s failed", cmdname);
session_reply(s, s->pending_tag, "NO", text);
return;
strlcpy(s->selected_mailbox, s->rename_newname,
sizeof(s->selected_mailbox)) >= sizeof(s->selected_mailbox))
log_warnx("session %u: selected_mailbox truncated after "
- "RENAME -- can't happen (both same size)", s->id);
+ "RENAME, can't happen (both same size)", s->id);
snprintf(text, sizeof(text), "%s completed", cmdname);
session_reply(s, s->pending_tag, "OK", text);
if (!list_pattern_match(s->list_pattern, item->mailbox, 0))
return;
- /* Quoted, not bare -- same reasoning as session_handle_mbox_status_result(); "()" = no attributes (SS7.3.1). */
+ /* Quoted, not bare, same reasoning as session_handle_mbox_status_result(); "()" = no attributes (SS7.3.1). */
snprintf(buf, sizeof(buf), "%s () \"/\" \"%s\"", kw, item->mailbox);
session_untagged(s, buf);
}
-/* Terminal LIST/LSUB reply; a non-OK error means a real I/O error only -- mismatches already produce no item, silently. */
+/* Terminal LIST/LSUB reply; a non-OK error means a real I/O error only, mismatches already produce no item, silently. */
void
session_finish_list(struct session *s, const struct imsg_mbox_result *res)
{
}
}
-/* Terminal IDLE_REFRESH reply; push gated on s->idling && SELECTED -- client may've sent DONE/a new SELECT meanwhile. */
+/* Terminal IDLE_REFRESH reply; push gated on s->idling && SELECTED, client may've sent DONE/a new SELECT meanwhile. */
void
session_handle_idle_refreshed(struct session *s,
const struct imsg_mbox_idle_refreshed *res)
return;
}
if (s->store_iev == NULL)
- return; /* no store child wired -- nothing to ask */
+ return; /* no store child wired, nothing to ask */
s->idle_refresh_pending = 1;
if (imsg_compose(&s->store_iev->ibuf, IMSG_MBOX_IDLE_REFRESH, 0, 0,
imsgev_add(s->store_iev);
}
-/* Notifies same-uid idling sessions after a UID-changing op (not plain STORE); no mailbox filter -- harmless over-notify. */
+/* Notifies same-uid idling sessions after a UID-changing op (not plain STORE); no mailbox filter, harmless over-notify. */
void
session_notify_idle_peers(const struct session *s)
{
}
if (s->state == SESSION_CREATING || s->state == SESSION_DELETING ||
s->state == SESSION_RENAMING) {
- /* RFC 9051 SS6.3.4-SS6.3.6: same peel-off pattern as SEARCH/COPY -- reply text doesn't fit this function's cmdname table. */
+ /* RFC 9051 SS6.3.4-SS6.3.6: same peel-off pattern as SEARCH/COPY, reply text doesn't fit this function's cmdname table. */
session_finish_mbox_op(s, res);
return;
}
return;
}
- /* RFC 9051 SS6.4.9: becomes "UID <CMD>" when s->cmd_by_uid is set -- CLOSE is the exception (no "UID CLOSE"). */
+ /* RFC 9051 SS6.4.9: becomes "UID <CMD>" when s->cmd_by_uid is set, CLOSE is the exception (no "UID CLOSE"). */
if (s->state == SESSION_STORING) {
cmdname = s->cmd_by_uid ? "UID STORE" : "STORE";
was_storing = 1;
s->state = was_close ? SESSION_AUTHENTICATED : SESSION_SELECTED;
if (res->error != MBOX_OP_OK) {
- /* v1's only failure mode is an index I/O error; RFC 9051 has no specific code for it, so a plain NO is honest. */
+ /* only failure mode is an index I/O error; RFC 9051 has no specific code for it, so a plain NO is honest. */
char text[32];
free(s->store_modified);
return;
}
- /* RFC 7162 SS3.1.3: a failed-conditional STORE gets MODIFIED on its tagged OK; v1 never needs the tagged-NO form. */
+ /* RFC 7162 SS3.1.3: a failed-conditional STORE gets MODIFIED on its tagged OK */
if (was_storing && s->store_modified_n > 0) {
char rbuf[2048];
char text[2048 + 32];
/*
* text's ~32-byte margin over rbuf's 2048 is enough for the
* wrapper words but not always for wrapper + a near-max
- * rbuf + "UID STORE" together (worst case ~2106 bytes) --
- * session_reply() itself can no longer mangle the line if
- * this truncates (it preserves the trailing CRLF), but the
- * MODIFIED list could still lose its tail silently. Check
- * and log it, same as rbuf's own truncation just above.
+ * rbuf + "UID STORE" together.
*/
if (snprintf(text, sizeof(text), "[MODIFIED %s] Conditional "
"%s failed for some messages", rbuf, cmdname) >=
s->store_modified_n = 0;
s->store_modified_cap = 0;
- /* RFC 9051 SS6.3.13: gated on was_expunging not res->count (CLOSE's count is always 0) -- harmless extra refresh. */
+ /* RFC 9051 SS6.3.13: gated on was_expunging not res->count (CLOSE's count is always 0), harmless extra refresh. */
if (was_expunging)
session_notify_idle_peers(s);
- /* RFC 7162 SS3.2.7: real EXPUNGE (not CLOSE -- SS3.2.8 forbids it) with count>0 gets HIGHESTMODSEQ, once CONDSTORE-aware. */
+ /* RFC 7162 SS3.2.7: real EXPUNGE (not CLOSE, SS3.2.8 forbids it) with count>0 gets HIGHESTMODSEQ, once CONDSTORE-aware. */
if (was_expunging && !was_close && s->condstore_enabled &&
res->count > 0) {
char text[64];
session_reply(s, s->pending_tag, "OK", text);
}
}
-