commit - be72be56ebed8f5a27d7258f8e2c6b5ba106083e
commit + 2bd90b642f5b0f2eee6ef497269e1e983b8966c5
blob - c59aa80793e00780ac0a04bc9ce5f96d57d19f3a
blob + 56d10fc49bb84b6df9a0a994afdcfd4b8338a295
--- README.md
+++ README.md
A from-scratch IMAP4rev2 ([RFC 9051](https://www.rfc-editor.org/rfc/rfc9051)) server for OpenBSD, written in C in the privilege-separated tradition of `smtpd(8)`, `httpd(8)`, and `ntpd(8)`. No third-party IMAP library.
-**Status:** 0.1.9 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
+**Status:** 0.2.0 Pre-release, actively developed. Not a port. See [Getting the source](#getting-the-source) below for the repository.
## What it is
## 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`, `SUBSCRIBE`, `UNSUBSCRIBE` (including LIST's [RFC 9051](https://www.rfc-editor.org/rfc/rfc9051) section 6.3.9.1 `SUBSCRIBED` selection option), the `UID`-prefixed form of every command that supports it, `IDLE` (with the cross-session caveat noted below), and the [RFC 7162](https://www.rfc-editor.org/rfc/rfc7162) `CONDSTORE`/`QRESYNC` extensions.
+`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` (including `RETURN (SAVE)` and the `$` saved-result-set marker, [RFC 5182](https://www.rfc-editor.org/rfc/rfc5182), folded into RFC 9051), `APPEND`, `COPY`, `MOVE`, `EXPUNGE`, `UNSELECT`, `CLOSE`, `SUBSCRIBE`, `UNSUBSCRIBE` (including LIST's [RFC 9051](https://www.rfc-editor.org/rfc/rfc9051) section 6.3.9.1 `SUBSCRIBED` selection option), the `UID`-prefixed form of every command that supports it, `IDLE` (with the cross-session caveat noted below), and the [RFC 7162](https://www.rfc-editor.org/rfc/rfc7162) `CONDSTORE`/`QRESYNC` extensions.
ACL/shared-mailbox support is deliberately left out.
doas make install
```
-Installs the daemon to `/usr/local/sbin/imapd`, man pages to `/usr/local/man/man8`, the `imapduser` account-provisioning tool alongside the daemon, and a sample config to `/usr/local/share/examples/imapd/imapd.conf`.
+Installs the daemon to `/usr/local/sbin/imapd`, man pages to `/usr/local/man/man5` and `/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)`, so the installed copy is executable regardless of the source tree's own permission bits:
/usr/local/share/examples/imapd/imapd.conf /etc/imapd.conf
```
-Every directive is documented inline in the sample file; the full reference is in `imapd(8)`'s FILES section.
+Every directive is documented inline in the sample file; the full reference is `imapd.conf(5)`.
## Creating the daemon accounts
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. Recovery is `rcctl restart imapd`. See `imapd(8)`.
+- If the keymgr process, which holds the TLS private key, exits unexpectedly, it is not restarted and new TLS handshakes fail. Recovery is `rcctl restart imapd`. See `imapd(8)`.
-`SIGHUP` reloads `spool`, `append max`, `attachment max`, `account sessions`, `connections max`, `idle poll`, `lock timeout`, `login grace`, `startups`, and the TLS certificate/key without dropping connected sessions, `listen on` and `credentials` changes still require a restart. See `imapd(8)`.
+`SIGHUP` reloads `spool`, `append max`, `attachment max`, `account sessions`, `connections max`, `idle poll`, `lock timeout`, `login grace`, `startups`, and the TLS certificate/key without dropping connected sessions, `listen on` and `credentials` changes still require a restart. See `imapd.conf(5)`.
-IPv6 is supported (`listen on ::` or `listen on *` for dual-stack) but not the default, see `imapd(8)`'s `listen on` directive.
+IPv6 is supported (`listen on ::` or `listen on *` for dual-stack) but not the default, see the `listen on` directive in `imapd.conf(5)`.
## Getting the source
## More
-`imapd(8)` and `imapduser(8)` are the authoritative technical reference.
+`imapd(8)`, `imapd.conf(5)` and `imapduser(8)` are the authoritative technical reference.
blob - 38f2e42d3962909ce6815417a407d690a860f0f2
blob + a30d9db8820ab2924812e137fded1dd535a0e58e
--- contrib/imapd-teardown
+++ contrib/imapd-teardown
set -e
BINDIR=/usr/local/sbin
+MAN5DIR=/usr/local/man/man5
MAN8DIR=/usr/local/man/man8
EXAMPLEDIR=/usr/local/share/examples/imapd
RELINKDIR=/usr/share/relink/usr/local/sbin/imapd
do_rm "installed daemon binary" "$BINDIR/imapd" f
do_rm "installed imapduser tool" "$BINDIR/imapduser" f
do_rm "imapd.8 man page" "$MAN8DIR/imapd.8" f
+ do_rm "imapd.conf.5 man page" "$MAN5DIR/imapd.conf.5" f
do_rm "imapduser.8 man page" "$MAN8DIR/imapduser.8" f
do_rm "installed sample config directory" "$EXAMPLEDIR" r
do_rm "rc.d script" "/etc/rc.d/imapd" f
blob - 291af09dff0964c50e04f656d1205a7a788e523e
blob + cba2e724253c971c0d0706a3c356dd098acd931c
--- contrib/imapduser.8
+++ contrib/imapduser.8
.\"
.\" Written for the OpenIMAPD project. Public domain / no rights reserved.
.\"
-.Dd $Mdocdate: September 29 2026 $
+.Dd $Mdocdate: September 30 2026 $
.Dt IMAPDUSER 8
.Os
.Sh NAME
.Xr imapd 8 Ns 's
.Em auth
child reads
-.Sy username : passwordhash : uid : gid : maildir
+.Ql username:passwordhash:uid:gid:maildir
lines directly out of a credentials file rather than calling
.Xr getpwnam 3 ,
and its
.Bl -tag -width Ds
.It Fl a
Add
-.Ar username .
-Creates its maildir under
+.Ar username ,
+which may contain only letters, digits,
+.Sq \&. ,
+.Sq _
+and
+.Sq - .
+Creates its maildir, named
+.Ar username ,
+under
.Ar spool-root ,
owned by
.Ar uid : Ns Ar gid ,
+mode 0700,
and appends a line to the credentials file, prompting for a password
via
.Xr encrypt 1 .
.Fl u
and
.Fl g
-are both omitted, the same free id is chosen automatically and used
-for both.
-Fails without effect if
+are both omitted, the lowest id from 2000 up that is not used in
+.Pa /etc/passwd ,
+.Pa /etc/group
+or the credentials file is chosen and used for both.
+Fails without adding anything if
.Ar username
already has a credentials-file line, or if its maildir already exists.
.It Fl d
does not conflate them.
The maildir's path is printed on success so it can be removed by hand
if that is actually what is wanted.
-.Fl s ,
-.Fl u ,
+.Fl u
and
.Fl g
-are ignored in this mode.
+are ignored in this mode, and
+.Fl s
+only changes the path printed.
Fails without effect if
.Ar username
has no credentials-file line.
maildir and to record in its credentials-file line, in
.Fl a
mode.
+If only one is given, the other takes the same value.
+.Xr imapd 8
+ignores an entry whose uid or gid is 0.
.El
.Pp
Must be run as root: adding an account
.Ar uid : Ns Ar gid ,
and both modes write to a file that must stay root-owned.
.Pp
-Every write to the credentials file, whether creating it for the
-first account or rewriting it after a deletion, resets its ownership
-and mode to
+When
+.Nm
+creates the credentials file, or rewrites it after a deletion, it sets
+its ownership and mode to
.Sy root : Ns Sy _imapauth ,
mode 0640: the
.Em auth
.Nm
leaves the file group-owned by
.Sy wheel
-and prints a warning rather than failing silently; re-run
-.Nm ,
-or run
+and prints a warning rather than failing silently; run
.Xr chgrp 1
-by hand, once that account is provisioned.
+by hand once that account is provisioned.
.Sh FILES
.Bl -tag -width "/etc/imapd/credentialsXXX" -compact
.It Pa /etc/imapd/credentials
.Sh SEE ALSO
.Xr encrypt 1 ,
.Xr crypt_checkpass 3 ,
+.Xr imapd.conf 5 ,
.Xr imapd 8
.Sh HISTORY
.Nm
blob - 4bf7b96ec5aa1ffb1dbf3308e5cf4a0445e3b7a0
blob + 28d70578cab1d0af11363ec0d4b50101db9f8375
--- src/Makefile
+++ src/Makefile
LDADD+= -ltls -lssl -lcrypto
DPADD+= ${LIBTLS} ${LIBSSL} ${LIBCRYPTO}
-MAN= imapd.8
+MAN= imapd.8 imapd.conf.5
CFLAGS+= -Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes
CFLAGS+= -Wmissing-declarations -Wshadow -Wpointer-arith
blob - a9edf8444f61eb110fa9d62bde920efa5aa13ed5
blob + c89f711d6103856cce71d763e5eb1631cb7d9d37
--- src/auth.c
+++ src/auth.c
static char cred_file_basename[256];
static int cred_lookup(const char *, const char *username,
- struct cred_entry *);
+ struct cred_entry *, int *);
+static int cred_cost(const char *);
static void auth_verify(struct imsg_auth_request *,
struct imsg_auth_result *);
static void auth_dispatch(int, short, void *);
out[i] = '\0';
}
-/* unknown users still pay for crypt_checkpass(), against timing */
+/* an unknown user pays one hash at the file's highest cost, like a real one */
static void
auth_verify(struct imsg_auth_request *req, struct imsg_auth_result *res)
{
struct cred_entry ce;
const char *hash = NULL;
- int found;
+ char dummy[_PASSWORD_LEN];
+ int found, maxcost;
char safename[AUTH_USERNAME_MAX];
if (auth_failures >= AUTH_MAX_TRIES) {
return;
}
- found = (cred_lookup(cred_file_basename, req->username, &ce) == 0);
+ found = (cred_lookup(cred_file_basename, req->username, &ce,
+ &maxcost) == 0);
if (found)
hash = ce.passwordhash;
+ else if (maxcost > 0) {
+ (void)snprintf(dummy, 8, "$2b$%02d$", maxcost);
+ memset(dummy + 7, '.', 53);
+ dummy[60] = '\0';
+ hash = dummy;
+ }
if (crypt_checkpass(req->password, hash) == 0 && found &&
strlcpy(res->maildir, ce.maildir, sizeof(res->maildir)) <
}
static int
-cred_lookup(const char *path, const char *username, struct cred_entry *out)
+cred_cost(const char *h)
{
- FILE *fp;
- char line[1024];
- int found = 0;
+ int cost;
+ if (h[2] == '\0' || h[3] != '$' || !isdigit((unsigned char)h[4]) ||
+ !isdigit((unsigned char)h[5]) || h[6] != '$')
+ return (0);
+ cost = (h[4] - '0') * 10 + (h[5] - '0');
+ return (cost >= 4 && cost <= 31 ? cost : 0);
+}
+
+static int
+cred_lookup(const char *path, const char *username, struct cred_entry *out,
+ int *maxcost)
+{
+ FILE *fp;
+ char line[1024];
+ struct cred_entry ce;
+ int found = 0, cost;
+
+ *maxcost = 0;
if ((fp = fopen(path, "r")) == NULL) {
log_warn("fopen %s", path);
return (-1);
log_warnx("%s: over-long line, refusing to parse the "
"credential file", path);
explicit_bzero(line, sizeof(line));
+ explicit_bzero(&ce, sizeof(ce));
fclose(fp);
return (-1);
}
if (i != 5)
continue; /* malformed line, skip */
- if (strcmp(fields[0], username) != 0)
+ if (strlcpy(ce.username, fields[0], sizeof(ce.username))
+ >= sizeof(ce.username) ||
+ strlcpy(ce.passwordhash, fields[1],
+ sizeof(ce.passwordhash)) >= sizeof(ce.passwordhash))
continue;
-
- if (strlcpy(out->username, fields[0], sizeof(out->username))
- >= sizeof(out->username) ||
- strlcpy(out->passwordhash, fields[1],
- sizeof(out->passwordhash)) >= sizeof(out->passwordhash))
- continue;
/* crypt_checkpass(3) passes an empty hash and password */
if (fields[1][0] != '$' || fields[1][1] != '2')
continue;
if (*ep != '\0' || errno != 0 || ulval == 0 ||
ulval >= (unsigned long)(uid_t)-1)
continue;
- out->uid = (uid_t)ulval;
+ ce.uid = (uid_t)ulval;
errno = 0;
ulval = strtoul(fields[3], &ep, 10);
if (*ep != '\0' || errno != 0 || ulval == 0 ||
ulval >= (unsigned long)(gid_t)-1)
continue;
- out->gid = (gid_t)ulval;
- if (strlcpy(out->maildir, fields[4], sizeof(out->maildir)) >=
- sizeof(out->maildir))
+ ce.gid = (gid_t)ulval;
+ if (strlcpy(ce.maildir, fields[4], sizeof(ce.maildir)) >=
+ sizeof(ce.maildir))
continue;
- found = 1;
- break;
+ if ((cost = cred_cost(ce.passwordhash)) > *maxcost)
+ *maxcost = cost;
+ /* the whole file is read, found or not */
+ if (!found && strcmp(ce.username, username) == 0) {
+ *out = ce;
+ found = 1;
+ }
}
- /* line[] held credential records */
+ /* line[] and ce held credential records */
explicit_bzero(line, sizeof(line));
+ explicit_bzero(&ce, sizeof(ce));
fclose(fp);
return (found ? 0 : -1);
}
blob - d2ffc23c6531d1c39bc449b58c991c54b69674d8
blob + e04d21fe95064d78b9b7c40c63a0569221f683f7
--- src/fetch_cmd.c
+++ src/fetch_cmd.c
memcpy(tok, start, len);
tok[len] = '\0';
+ if (strcmp(tok, "$") == 0 && *p == '\0') {
+ memset(&ranges[n++], 0, sizeof(ranges[0]));
+ break;
+ }
if (parse_one_seq_range(tok, &ranges[n]) == -1) {
*errmsg = "invalid sequence set";
return (-1);
session_untagged(s, buf);
}
-/* RFC 4466 modifiers */
-char *
-split_trailing_modifiers(char *spec)
-{
- char *p = spec;
-
- if (*p == '(') {
- int depth = 0;
-
- for (;;) {
- if (*p == '(')
- depth++;
- else if (*p == ')') {
- depth--;
- if (depth == 0) {
- p++;
- break;
- }
- } else if (*p == '\0')
- return (NULL);
- p++;
- }
- } else {
- while (*p != '\0' && *p != ' ')
- p++;
- }
-
- if (*p == '\0')
- return (NULL);
- *p++ = '\0';
- while (*p == ' ')
- p++;
- if (*p == '\0')
- return (NULL);
- return (p);
-}
-
/* RFC 4466, RFC 7162 SS3.1.4.1 and SS3.2.6 modifiers */
int
parse_fetch_modifiers(char *modspec, struct imsg_mbox_fetch *req,
return (session_arg_error(s, tag, errmsg));
attspec = unlit;
- modspec = split_trailing_modifiers(attspec);
+ fetch_att_tok(attspec, &modspec);
+ while (*modspec == ' ')
+ modspec++;
+ if (*modspec == '\0')
+ modspec = NULL;
rc = parse_fetch_atts(attspec, &attrs, °raded, &header_fields_not,
header_fields, sizeof(header_fields), header_fields_label,
blob - 53289376fe16e21ab8f9917fe0f74f8ccbb29f0b
blob + 8bd071198b174dfe4b1d2aaa8c6426cdfbae4646
--- src/imapd.8
+++ src/imapd.8
.\" Written for the OpenIMAPD project. Public domain / no rights reserved,
.\" matching the project's ports-oriented, OpenBSD-base-inclusion goal.
.\"
-.Dd $Mdocdate: September 29 2026 $
+.Dd $Mdocdate: September 30 2026 $
.Dt IMAPD 8
.Os
.Sh NAME
.Nm
is an Internet Message Access Protocol
.Pq IMAP
-daemon implementing the subset of RFC 9051
-.Pq IMAP4rev2
-described below.
-It is privilege-separated in the style of
-.Xr smtpd 8 :
-a root
-.Em parent
-process reads configuration, binds the listening sockets, and
-re-executes unprivileged
-.Em listener
-and
-.Em auth
-children over
-.Xr imsg_init 3
-control channels.
-One
-.Em store
-child serves each logged-in account, shared by all of that account's
-sessions; it chroots into the mail spool and drops privileges to the
-account's own user before ever touching mailbox data.
-The
-.Fl x
-flag referenced internally by these re-executed children is not
-meant for direct operator use.
+daemon implementing a subset of IMAP4rev2, as defined by RFC 9051,
+and the RFC 7162 CONDSTORE and QRESYNC extensions.
+It serves mail stored in maildir format.
.Pp
+By default,
.Nm
-listens on port 143
-.Pq cleartext, upgradable via STARTTLS
-and port 993
-.Pq implicit TLS, per RFC 8314 ,
-both via
-.Xr tls_init 3 .
-Those are the defaults, used when
-.Pa /etc/imapd.conf
-contains no
-.Ic listen
-directive at all.
-A file that contains any
-.Ic listen
-directive selects listeners as well as configuring them: only the
-listeners it names are bound.
+listens on port 143, where clients may upgrade to TLS with
+.Li STARTTLS ,
+and on port 993 with implicit TLS, as recommended by RFC 8314.
Authentication is
.Li AUTH=PLAIN
-only, and is refused before TLS is established;
-.Li LOGINDISABLED
-is advertised on the cleartext, pre-TLS connection.
+only, and is refused until TLS is established;
+.Li LOGIN
+is always refused.
+The listening addresses and ports are set in
+.Xr imapd.conf 5 .
.Pp
-The current implementation is intentionally minimal: each user's
-mailboxes form a single flat namespace
-.Pq no nested hierarchy , Li INBOX
-plus zero or more sibling mailboxes created via
-.Li CREATE ,
-and no shared or multi-user mailboxes
-.Pq no Li ACL support .
-Within that scope it implements
+Each user's mailboxes form a single flat namespace:
+.Li INBOX
+and zero or more sibling mailboxes, with no hierarchy and no shared
+mailboxes.
+.Li INBOX
+is the account's maildir itself, and each other mailbox is a maildir
+in a subdirectory of it with the mailbox's name.
+The commands implemented are
.Li CAPABILITY ,
+.Li NOOP ,
+.Li LOGOUT ,
.Li STARTTLS ,
.Li AUTHENTICATE ,
.Li ID ,
.Li EXPUNGE ,
.Li UNSELECT ,
.Li CLOSE ,
-.Li UID ,
-.Li IDLE ,
-and the RFC 7162 CONDSTORE and QRESYNC extensions
-.Pq mod-sequence tracking, conditional STORE, VANISHED responses .
-.Li LIST
-accepts the
-.Li SUBSCRIBED
-selection option of RFC 9051 Section 6.3.9.1.
-.Li LSUB ,
-which RFC 9051 deprecates in favour of that option, is retained and
-reports the same set of names.
-An account that has never issued
-.Li UNSUBSCRIBE
-has every mailbox subscribed; see
-.Pa imapd.subscriptions
-under FILES.
+.Li UID
+and
+.Li IDLE .
.Pp
.Nm
-logs to
+is privilege separated.
+A root parent process reads the configuration, binds the listening
+sockets and accepts connections.
+Each connection is handled by unprivileged processes that are
+.Xr chroot 2 Ns ed
+and restricted with
+.Xr pledge 2 .
+The TLS private key is held by a separate process and never enters
+the process that speaks to the network.
+Mailbox access for each logged-in account is performed by one process,
+shared by all of that account's sessions, which is
+.Xr chroot 2 Ns ed
+into the mail spool and runs as the account's own user.
+.Nm
+requires the
+.Sy _imapd ,
+.Sy _imapauth
+and
+.Sy _imapkey
+users to exist.
+.Pp
+.Nm
+does not detach from its controlling terminal and is expected to be
+started by
+.Xr rc.d 8 .
+It logs to
.Xr syslogd 8
with the
.Dv LOG_MAIL
-facility, the same one
-.Xr smtpd 8
-uses, so its messages go wherever
-.Xr syslog.conf 5
-directs the mail facility.
-Releases before 0.1.5 used
-.Dv LOG_DAEMON .
+facility.
.Pp
+.Nm
+rereads its configuration file when it receives
+.Dv SIGHUP ;
+see
+.Xr imapd.conf 5
+for the settings that require a restart instead.
+.Pp
The options are as follows:
.Bl -tag -width Ds
+.It Fl D Ar macro Ns = Ns Ar value
+Define
+.Ar macro
+to be set to
+.Ar value
+on the command line.
+Overrides the definition of
+.Ar macro
+in the configuration file.
.It Fl d
Log to
.Em stderr
instead of
.Xr syslogd 8 .
-.Nm
-does not detach from its controlling terminal or otherwise
-background itself in either mode; an
-.Xr rc.d 8
-script or equivalent supervisor is expected to do so.
-An
-.Xr rc.d 8
-script is provided in
-.Pa rc.d/imapd
-in the source tree; it is not installed automatically by
-.Ic make install
-and must be installed to
-.Pa /etc/rc.d/imapd
-by hand, owned by
-.Sy root : Ns Sy wheel ,
-mode 555
-.Pq matching every base rc.d 8 script's own installed permissions :
-.Bd -literal -offset indent
-doas install -o root -g wheel -m 555 rc.d/imapd /etc/rc.d/imapd
-.Ed
-.Pp
-Using
-.Xr install 1
-rather than
-.Xr cp 1
-matters here: a plain
-.Xr cp 1
-preserves the source file's own permission bits, so a source tree
-where
-.Pa rc.d/imapd
-happens to be non-executable produces a non-executable, and therefore
-invisible to
-.Xr rcctl 8 ,
-copy at
-.Pa /etc/rc.d/imapd
---
-.Xr rcctl 8
-considers a service to not exist at all unless its script is
-executable.
-.Xr install 1
-sets the destination's mode explicitly instead, independent of
-whatever the source happened to be.
-It also applies any pending boot-time relink
-(see
-.Fl V
-below)
-before starting the daemon.
-.It Fl D Ar macro Ns = Ns Ar value
-Define
-.Ar macro
-to
-.Ar value ,
-overriding any definition of the same macro inside the configuration
-file, for use with
-.Ic $ Ns Ar macro
-references there.
.It Fl f Ar file
-Specify an alternative configuration file.
-Defaults to
+Use
+.Ar file
+as the configuration file, instead of the default
.Pa /etc/imapd.conf .
.It Fl V
Print the version and exit.
-Performs no other action
-.Pq no configuration file is read, no privileged setup occurs ;
-this is also the command
-.Xr make 1 Ns 's
-.Ic RELINK
-step in the Makefile uses to smoke-test a relinked binary before
-installing it, and what
-.Pa rc.d/imapd Ns 's
-boot-time relink hook relies on to confirm a relink succeeded.
.It Fl v
Produce more verbose logging.
.El
-.Pp
-Sending
+.Sh AUTHENTICATION
+Users of
.Nm
-.Dv SIGHUP
-rereads
-.Pa /etc/imapd.conf
-.Pq or the file given via Fl f
-and reloads the
-.Ic spool ,
-.Ic append max ,
-.Ic attachment max ,
-.Ic account sessions ,
-.Ic connections max ,
-.Ic idle poll ,
-.Ic lock timeout ,
-.Ic login grace ,
-.Ic startups ,
-.Ic tls certificate ,
+are not system accounts.
+They are listed in a credentials file, by default
+.Pa /etc/imapd/credentials ,
+one per line in the form:
+.Bd -literal -offset indent
+username:passwordhash:uid:gid:maildir
+.Ed
+.Pp
+The password hash is a bcrypt hash as accepted by
+.Xr crypt_checkpass 3 .
+.Ar uid
and
-.Ic tls key
-directives without disrupting sessions already connected, matching
-.Xr httpd 8 Ns 's
-own documented SIGHUP behavior.
-.Ic listen on
-and
-.Ic credentials
-cannot be changed this way: the listening sockets are already bound and
-the
-.Em auth
-child is already
-.Xr chroot 2 Ns d
-to the original credentials path by the time SIGHUP arrives, so a change
-to either is logged as a warning and otherwise ignored until
-.Nm
-is restarted.
-A malformed configuration file logs a warning and leaves the running
-daemon on its current configuration rather than exiting.
+.Ar gid
+are the user and group the account's mailbox process runs as, and
+.Ar maildir
+is the account's maildir, relative to the spool set in
+.Xr imapd.conf 5 .
+Entries that share all three name the same account.
.Pp
-If the
-.Em listener
+Empty lines and lines beginning with
+.Sq #
+are ignored, and malformed lines are skipped.
+A line is also skipped if its hash is not a bcrypt hash or its
+.Ar uid
or
-.Em auth
-process exits unexpectedly after startup, it is not automatically
-restarted, matching
-.Xr smtpd 8 Ns 's
-own treatment of its equivalent core processes and
-.Xr httpd 8 Ns 's
-even more hands-off one.
-The
-.Em listener Ns 's
-death makes
-.Nm
-unreachable entirely, since it owns every listening socket;
-.Em auth Ns 's
-death leaves already-authenticated sessions unaffected but new
-.Ic AUTHENTICATE
-attempts will fail.
-Either is logged as a warning; recovery is
-.Ic rcctl restart imapd .
+.Ar gid
+is 0.
+The first valid line naming a user is the one used.
+A login is refused if the
+.Ar maildir
+is empty, is an absolute path, or has a
+.Pa \&.
+or
+.Pa ..
+component.
+A line of 1023 or more characters causes every login to be refused.
+.Xr imapduser 8
+adds and removes entries.
+.Pp
+The file is read by a process running as
+.Sy _imapauth ,
+so it must be readable by that user.
+It must be owned by root or
+.Sy _imapauth
+and must not be group writable or executable, or accessible by
+others; otherwise every login is refused.
+.Xr imapduser 8
+creates it owned by root, group
+.Sy _imapauth ,
+mode 0640.
+A failed login for a name with no entry costs one hash at the highest
+cost in the file, so an entry hashed at a lower cost can be told from a
+missing one by timing; rehash such entries at the file's cost.
.Sh FILES
-.Bl -tag -width "/etc/imapd/credentialsXXX" -compact
+.Bl -tag -width "/etc/ssl/private/imapd.keyXX" -compact
.It Pa /etc/imapd.conf
-Default
-.Nm
-configuration file, read by the parent process only.
-Must be owned by root or the current user, and not group- or
-world-writable.
-Recognized directives, one per line:
-.Bl -tag -width Ds -compact
-.It Ic listen on Ar address Op Ic tls Ic port Ar port
-Bind a listener to
-.Ar address .
-Specify once without
-.Ic tls
-for the cleartext/STARTTLS listener (default port 143) and once with
-.Ic tls
-for the implicit-TLS listener (default port 993).
-Both listeners share one
-.Ar address ;
-a second
-.Ic listen
-line naming a different address is a configuration error.
-.Pp
-A
-.Ic listen
-directive also selects which listeners run.
-If the file contains no
-.Ic listen
-directive, both listeners are bound on their default ports.
-If it contains any, only the listeners it names are bound: a file whose
-only
-.Ic listen
-directive is
-.Pp
-.Dl listen on * tls port 993
-.Pp
-serves implicit TLS alone, with nothing on port 143, which is the
-deployment RFC 8314 Section 3 asks for.
-.Pp
-.Ar address
-must be a literal IPv4 address, a literal IPv6 address,
-.Ql ::
-(all IPv6 interfaces),
-.Ql 0.0.0.0
-(all IPv4 interfaces), or
-.Ql *
-(all IPv4 and IPv6 interfaces, matching
-.Xr httpd.conf 5 Ns 's
-own convention for the same character).
-Hostnames are not accepted: resolving one would add a DNS dependency to
-.Nm Ns 's
-boot path for no benefit a single-operator personal mail server actually
-needs.
-Since
-.Ox Ns 's
-IPv6 sockets are always IPv6-only
-.Pq no v4-mapped-address dual binding , unlike Linux ,
-.Ql *
-binds two sockets per listener, one
-.Dv AF_INET
-and one
-.Dv AF_INET6 ,
-rather than one dual-stack socket.
-.It Ic spool Ar path
-Mail spool root, chrooted into by the
-.Em store
-child.
-Defaults to
-.Pa /var/mail/imapd .
-.Ar path
-and everything directly under it
-.Pq one entry per mail user's maildir
-must be owned by
-.Sy root
-and not writable by mail users.
-Each user's maildir path under
-.Ar path
-is validated lexically only; if it names a symlink, the
-.Em store
-child's
-.Xr chroot 2
-follows it.
-That is contained:
-.Xr chroot 2
-resolves the target inside itself, and
-.Xr unveil 2
-in the
-.Em store
-child narrows the view further.
-A mail user able to create a symlink at their own maildir path would
-nonetheless choose that child's starting directory within the spool.
-The ownership requirement above is therefore a deployment assumption
-.Nm
-does not enforce in software.
-.It Ic credentials Ar path
-Credentials file (see below).
-Defaults to
-.Pa /etc/imapd/credentials .
-.It Ic tls certificate Ar path
-TLS certificate file.
-Defaults to
-.Pa /etc/ssl/imapd.crt .
-.It Ic tls key Ar path
-TLS private key file.
-Defaults to
-.Pa /etc/ssl/private/imapd.key .
-Must be owned by root, mode 0740 or stricter.
-.It Ic idle poll Ar seconds
-How often a session in
-.Li IDLE
-rechecks its selected mailbox.
-.Nm
-does not receive an event when mail arrives, so
-.Li IDLE
-is served by polling: every
-.Ar seconds ,
-the session asks its
-.Em store
-child whether anything has changed, and reports new messages and
-expunges if so.
-New mail is therefore announced within one interval rather than
-instantly, whether it was delivered by an external
-.Xr smtpd 8
-or by another IMAP session.
-.Pp
-Polling is cheap by design.
-The
-.Em store
-child first compares the modification times of the mailbox directory
-and its
-.Pa new
-subdirectory against the previous look; if neither has moved it answers
-immediately, taking no lock and reading nothing.
-Only a mailbox that has actually been touched costs an index read, and
-only one holding an unindexed delivery costs an exclusive lock.
-.Pp
-.Ar seconds
-must be between 1 and 300 inclusive, or 0 to disable polling
-altogether.
-With polling disabled a session in
-.Li IDLE
-is told nothing until it sends
-.Li DONE ,
-which is rarely what an operator wants.
-Defaults to 5.
-.It Ic startups begin Ar count Ic rate Ar percent Ic full Ar count
-Admission control on concurrent connections that have not yet
-authenticated, using
-.Xr sshd_config 5 Ns 's
-MaxStartups algorithm; see that manual for the canonical description.
-.Pp
-Below
-.Ic begin
-unauthenticated connections,
-.Nm
-accepts every new connection.
-Between
-.Ic begin
-and
-.Ic full
-it refuses new connections with a probability rising linearly from
-.Ic rate
-percent at
-.Ic begin
-to 100 percent at
-.Ic full .
-At or above
-.Ic full
-every new connection is refused.
-A connection stops counting the moment it authenticates, so the limit
-bounds unfinished logins rather than established sessions.
-A refused connection is answered as one past
-.Ic connections max
-is.
-.Pp
-Both counts must be between 0 and 1000000 inclusive,
-.Ar percent
-between 0 and 100 inclusive, and
-.Ic full
-must not be smaller than
-.Ic begin ;
-anything else is a configuration error.
-Setting
-.Ic full
-to 0 disables the throttle entirely and accepts unconditionally,
-which is the only way to turn it off:
-.Ic begin
-and
-.Ic rate
-have no such special value.
-Defaults to
-.Ic begin
-10
-.Ic rate
-30
-.Ic full
-100, matching
-.Xr sshd_config 5 Ns 's
-own default of 10:30:100.
-.It Ic startups per-source Ar count | Ic none
-The most connections that have not yet authenticated
-.Nm
-accepts from any one address, as
-.Xr sshd_config 5 Ns 's
-PerSourceMaxStartups does.
-It stops one address holding every connection the
-.Ic startups
-limit allows, which would refuse everyone else.
-A connection stops counting the moment it authenticates, and a refused
-one is answered as one past
-.Ic connections max
-is.
-.Pp
-Each address counts on its own, IPv6 addresses included, so a host able
-to use many IPv6 addresses can open this many from each of them; only
-.Ic startups
-and
-.Ic connections max
-bound that.
-Users behind one NAT address share it, and after a restart may briefly
-exceed it and have to retry.
-Raise it for them, or set
-.Ic none
-to count no address separately.
-.Pp
-Must be between 1 and 1000000 inclusive, or
-.Ic none .
-Defaults to 5.
-.It Ic login grace Ar seconds
-How long a connection may go without authenticating before
-.Nm
-closes it.
-.Pp
-Every accepted connection costs two processes until it logs in, a
-listener-worker and an auth-worker, and a connection that completes the
-TCP handshake and then sends nothing would otherwise hold both
-indefinitely.
-Enough such connections reach the
-.Ic startups full
-limit and every later connection is refused, so the timer bounds that
-exposure.
-It covers a connection that never begins a TLS handshake on the implicit
-TLS port as well as one that never sends a command, since the former never
-reaches the command path at all.
-.Pp
-RFC 9051, section 5.4 permits this explicitly: servers
-.Qq are allowed to use a shortened pre-authentication timer to protect
-.Qq themselves from Denial-of-Service attacks .
-The timer is cancelled the moment a session authenticates and never applies
-to an established session.
-It is not the post-authentication autologout timer that the same section
-requires to be at least 30 minutes;
-.Nm
-has no such timer.
-.Pp
-Must be between 1 and 3600 seconds inclusive, or 0 to disable it, which
-reopens the denial of service described above.
-Defaults to 60.
-.It Ic lock timeout Ar seconds
-How long a command waits for a mailbox's index lock held by another
-process before giving up and answering
-.Li NO
-with the RFC 9051, section 7.1
-.Li INUSE
-response code.
-.Pp
-All of one account's sessions are served by its one
-.Em store
-child, one command at a time, so they never wait for each other's lock.
-A command that changes a mailbox holds the lock for the whole of the
-change, and marking a large mailbox read can take the better part of a
-minute; the account's other sessions are answered only once it is done,
-and this directive does not bound that.
-Another process holds the lock when a
-.Em store
-child whose sessions have all ended is still finishing a command as a new
-login for the same account starts another, and this directive bounds how
-long the new one waits.
-.Pp
-It is deliberately generous, because it is a safety net for a holder that
-is stuck rather than a cure for one that is merely slow.
-A deadline shorter than an ordinary command's own duration would refuse
-ordinary concurrent use, and a client that does not retry
-.Li INUSE
-would discard the user's change with nothing shown on screen.
-Raise it for mailboxes much larger than a hundred thousand messages, where
-a single command can legitimately run longer than the default.
-.Pp
-Must be between 1 and 3600 seconds inclusive, or 0 to disable the bound,
-which restores an unbounded wait.
-Defaults to 120.
-.It Ic account sessions Ar count
-The most sessions one account may have open at once.
-An account is one uid, gid and maildir from the credentials file,
-so entries that share all three are one account.
-Every session counts, whichever client opened it, and a mail client
-that opens several connections for one account uses several.
-A client that vanishes without closing its connection keeps its session
-until TCP keepalive finds it gone: after the
-.Va net.inet.tcp.keepidle
-.Xr sysctl 8
-plus eight
-.Va net.inet.tcp.keepintvl
-intervals, 2 hours 10 minutes by default.
-.Pp
-A login past the limit is answered
-.Li NO
-with the RFC 9051, section 7.1
-.Li LIMIT
-response code, and the connection is closed.
-The password was accepted, so the client may log in again on a new
-connection once one of the account's sessions has ended.
-.Pp
-Must be between 1 and 4096 inclusive.
-Defaults to 8.
-.It Ic connections max Ar count
-The most connections
-.Nm
-holds open at once, logged in or not, from all clients together.
-Past it, a new connection on the cleartext port is answered with the
-RFC 9051, section 7.1.5 rejected-connection greeting, a
-.Li BYE
-with the
-.Li UNAVAILABLE
-response code, and closed.
-On the implicit TLS port that greeting would have to travel inside TLS,
-so the connection is closed with nothing sent.
-Connections already open are not affected.
-.Pp
-Each connection costs one process, and a second until it logs in.
-Each account with a session logged in costs one more, and a second
-while a message is being parsed for it.
-So C connections, L of them not yet logged in, for A accounts need up
-to C + L + 2A processes, plus two for
-.Nm
-itself, and the parent keeps a descriptor to each of them.
-The processes count against the
-.Va kern.maxproc
-.Xr sysctl 8 ,
-shared with the rest of the system, and the descriptors against the
-.Cm openfiles
-limit of the
-.Xr login.conf 5
-class
-.Nm
-runs in; raise those before raising this.
-The default fits
-.Ox Ns 's
-default
-.Va kern.maxproc
-and
-.Cm daemon
-class even if every connection is a different account.
-.Pp
-Must be between 1 and 4096 inclusive.
-Defaults to 256.
-.It Ic append max Ar bytes
-Largest message a client may upload with
-.Li APPEND .
-A larger one is refused with a
-.Li NO
-response carrying the RFC 5530
-.Li LIMIT
-code, and nothing is stored.
-.Ar bytes
-must be between 1 and 1073741824 (1 GiB) inclusive; values outside that
-range are a configuration error.
-Defaults to 36700160 (35 MiB), the same as the
-.Ic max-message-size
-default of
-.Xr smtpd.conf 5 .
-It may not exceed
-.Ic attachment max ,
-since a message larger than that could be stored but its structure
-could not be fetched; such a configuration is an error.
-.It Ic attachment max Ar bytes
-Largest message
-.Nm
-will read from disk while deriving
-.Li BODYSTRUCTURE
-or a MIME part-addressed
-.Li BODY Ns Bq Ar part
-fetch.
-A message larger than this is treated the same as any other reason its
-structure cannot be produced, not truncated.
-.Ar bytes
-must be between 12000 and 1073741824 (1 GiB) inclusive; values outside
-that range are a configuration error.
-It must be at least
-.Ic append max .
-Defaults to 41943040 (40 MiB).
-.It Ic include Ar path
-Parse
-.Ar path
-as though its contents appeared in place of this line.
-.It Ic macro Ns = Ns Ar value
-Define a macro, referenced elsewhere in the file as
-.Ic $ Ns Ar macro .
-See also
-.Fl D .
-.El
-.Pp
-A directive not given in the file falls back to its documented default;
-an empty or absent
-.Pa /etc/imapd.conf
-is equivalent to every directive using its default.
-Lines beginning with
-.Sq #
-are comments.
-A sample configuration file, with every directive documented inline, is
-installed at
-.Pa /usr/local/share/examples/imapd/imapd.conf .
-It is not read by
-.Nm
-itself; copy it to
-.Pa /etc/imapd.conf
-and edit as needed.
+Default configuration file.
.It Pa /etc/imapd/credentials
-Default credentials file, read by the
-.Em auth
-child.
-One line per user, colon-delimited:
-.Sy username : passwordhash : uid : gid : maildir ,
-with the password hash in a
-.Xr crypt_checkpass 3 Ns Ns -compatible
-.Pq bcrypt
-form.
-Must be owned by
-.Sy root
-and group-owned by the
-.Sy _imapauth
-account, mode 0640 or stricter: the
-.Em auth
-child chroots and drops privileges to
-.Sy _imapauth
-before ever opening this file, so it must be group-readable by that
-account specifically, not just root-readable.
-.Xr imapduser 8
-sets this automatically.
+Default credentials file.
+.It Pa /etc/ssl/imapd.crt
+Default TLS certificate.
+.It Pa /etc/ssl/private/imapd.key
+Default TLS private key.
.It Pa /var/mail/imapd
-Default spool root.
-The
-.Em store
-child
-.Xr chroot 2 Ns s
-into this directory before dropping privileges.
-.It Pa imapd.uidvalidity
-Per-user record, at the root of each maildir, of the highest
-.Li UIDVALIDITY
-ever issued to that user.
-A mailbox that is created, or recreated after a
-.Li DELETE ,
-is given a value greater than every value in this file rather than the
-current time alone, so that a mailbox recreated quickly cannot be handed
-a
-.Li UIDVALIDITY
-it has used before.
-RFC 9051 Section 2.3.1.1 requires that; a bare timestamp only
-approximates it, because two mailboxes created in the same second get
-the same value.
-.Pp
-The file is created on demand, holds one decimal number, and is its own
-lock.
-It is removed with the maildir and needs no separate administration.
-Note that
-.Xr imapduser 8 Fl d
-deliberately leaves a maildir in place, so an account removed and
-re-added keeps its history here, which is the desired behaviour;
-removing a maildir by hand and recreating it resets the record, and a
-client holding a cache from before that point could in principle be
-misled.
+Default mail spool.
+.It Pa imapd.index
+Index of a mailbox's messages, in each mailbox's maildir, with
+.Pa imapd.index.lock
+and
+.Pa imapd.index.tmp
+beside it.
.It Pa imapd.subscriptions
-Per-user list of subscribed mailboxes, at the root of each maildir, one
-mailbox name per line.
-.Pp
-The file is created on demand by the first
-.Li UNSUBSCRIBE .
-While it is absent, every mailbox is subscribed.
-That rule is what lets an account upgraded from a version without
-subscriptions keep the mailbox list its client already had, rather than
-come back subscribed to nothing; the first
-.Li UNSUBSCRIBE
-therefore writes out every mailbox then present, less the one being
-removed.
-.Pp
-A name stays in the file after its mailbox is deleted, which RFC 9051
-Section 6.3.8 requires.
-.Li LIST
-with the
-.Li SUBSCRIBED
-option reports such a name with the
-.Li \eNonExistent
-attribute, and
-.Li LSUB
-reports it with
-.Li \eNoselect .
-.Li INBOX
-is never named in the file.
-.Pp
-Neither
-.Li CREATE
-nor
-.Li RENAME
-changes the file.
-A new mailbox is subscribed only when a client subscribes it, and renaming
-a subscribed mailbox leaves the old name in the file rather than moving it
-to the new one.
-RFC 9051 Section 6.3.8 tells a server not to remove a name from the
-subscription list because the mailbox by that name no longer exists, and
-moving it is exactly that, so what is subscribed is left to the client to
-say.
-A subscription stranded by a rename is repaired with one
-.Li SUBSCRIBE .
-.Pp
-A file that cannot be read is treated as absent, so damage shows too many
-mailboxes rather than too few, and the failure is logged.
-It is removed with the maildir and needs no separate administration.
+Subscribed mailboxes, one per line, at the root of each maildir, with
+.Pa imapd.subscriptions.lock
+and
+.Pa imapd.subscriptions.tmp
+beside it.
+Created by the first
+.Li UNSUBSCRIBE ;
+while it is absent, every mailbox is subscribed.
+.It Pa imapd.uidvalidity
+Highest
+.Li UIDVALIDITY
+issued, at the root of each maildir.
+Created on demand.
+.It Pa /usr/local/share/examples/imapd/imapd.conf
+Example configuration file.
.El
-.Sh NETWORK
-.Nm
-binds
-.Pa 0.0.0.0
-.Pq IPv4 only
-port 143
-.Pq cleartext/STARTTLS
-and port 993
-.Pq implicit TLS
-by default; these, along with the listen address, are read from
-.Pa /etc/imapd.conf
-(or the file named by
-.Fl f )
-at startup.
-A file containing no
-.Ic listen
-directive leaves both listeners on those defaults; a file containing any
-.Ic listen
-directive binds only the listeners it names.
-IPv6 and dual-stack binding are available but not the default; see the
-.Ic listen on
-directive under FILES above.
.Sh SEE ALSO
.Xr crypt_checkpass 3 ,
-.Xr imsg_init 3 ,
-.Xr tls_init 3 ,
-.Xr httpd.conf 5 ,
-.Xr login.conf 5 ,
-.Xr sshd_config 5 ,
-.Xr syslog.conf 5 ,
-.Xr httpd 8 ,
+.Xr imapd.conf 5 ,
.Xr imapduser 8 ,
+.Xr rc.d 8 ,
.Xr smtpd 8 ,
-.Xr sysctl 8 ,
.Xr syslogd 8
.Sh STANDARDS
.Rs
.Re
.Pp
.Rs
+.%A K. Zeilenga
+.%D August 2006
+.%R RFC 4616
+.%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
+.Re
+.Pp
+.Rs
.%A J. Klensin
.%A M. Padlipsky
.%D March 2008
.%R RFC 5198
.%T Unicode Format for Network Interchange
.Re
+.Pp
+.Rs
+.%A K. Moore
+.%A C. Newman
+.%D January 2018
+.%R RFC 8314
+.%T Cleartext Considered Obsolete: Use of Transport Layer Security (TLS) for Email Submission and Access
+.Re
.Sh HISTORY
.Nm
is a from-scratch IMAP server written for the OpenIMAPD project, in
.Sh CAVEATS
This implementation is under active development.
.Pp
-.Li INBOX
-cannot be unsubscribed.
-RFC 9051 Section 5.1 guarantees that it always exists and that nothing
-can delete it, so it is treated as permanently subscribed:
-.Li SUBSCRIBE INBOX
-succeeds and changes nothing, and
-.Li UNSUBSCRIBE INBOX
-replies
-.Li NO .
-A subscription-filtered view therefore always includes it.
+.Li IDLE
+is served by polling the selected mailbox, so new mail is reported
+within one
+.Ic idle poll
+interval rather than at once.
.Pp
-.Li LIST
-selection options other than
-.Li SUBSCRIBED
-are refused rather than ignored.
-.Li REMOTE
-and
-.Li RECURSIVEMATCH
-are not implemented, and accepting either silently would misreport which
-names the response contains.
-.Pp
-Shared or multi-user mailboxes
-.Pq no Li ACL support
-are deliberately out of scope.
-.Pp
-Mailbox names are required to be well-formed UTF-8.
-RFC 9051 Section 5.1 encodes them in Net-Unicode
-.Pq RFC 5198 ,
-and requires a server to prohibit the creation of 8-bit names that do not
-comply.
-.Nm
-enforces three of Net-Unicode's requirements and not the other three, which
-is worth stating plainly rather than leaving to be discovered:
-.Bl -bullet -offset indent -compact
-.It
-UTF-8 well-formedness per RFC 3629 is enforced.
-Overlong encodings, UTF-16 surrogates and anything above U+10FFFF are
-refused.
-.It
-The C1 controls U+0080 to U+009F are refused, as Net-Unicode requires.
-C0 and DEL were already refused, which RFC 9051 Section 5.1 permits.
-.It
-U+FEFF is refused anywhere in a name.
-Net-Unicode forbids it only at the beginning;
-.Nm
-is deliberately stricter, because a zero-width no-break space inside a name
-produces two mailboxes no user can tell apart.
-.It
-Normalization to NFC is
-.Em not
-performed and
-.Em not
-required.
-Two canonically equivalent spellings of the same name, such as U+00E9 and
-U+0065 U+0301, are therefore two distinct mailboxes with distinct
-.Li UIDVALIDITY
-values.
-A client that normalizes differently from another client used on the same
-account will see both.
-.It
-Names are
-.Em not
-checked against a Unicode version, so a name containing an unassigned code
-point is accepted.
-Net-Unicode forbids that, and honouring it would require a Unicode character
-database inside the daemon.
-.El
-.Pp
-A name that fails these checks is refused with
-.Li NO [CANNOT]
-by
-.Li CREATE ,
-.Li RENAME
-and
-.Li COPY Ns / Ns Li MOVE ,
-and reported as nonexistent by the commands that only ask whether a mailbox
-is there.
-A directory whose name fails them is skipped by
-.Li LIST
-and cannot be selected; the first such skip in a connection is logged, naming
-the directory.
-This last case can only arise for a mailbox created before these checks
-existed, or created outside
-.Nm
-altogether.
-.Pp
-RFC 9051 Section 6.3.5 does not say what becomes of a session that
-.Li DELETE Ns s
-the mailbox it currently has selected, and states no condition under which
-such a
-.Li DELETE
-must be refused.
-.Nm
-allows it and returns the session to the authenticated state, as
-.Li CLOSE
-does: the mailbox no longer exists, so nothing is selected.
-A client that issues
-.Li FETCH ,
-.Li STORE ,
-.Li SEARCH ,
-.Li COPY ,
-.Li MOVE
-or
-.Li EXPUNGE
-afterwards is refused until it selects a mailbox again, rather than being
-served
-.Li INBOX
-under the deleted mailbox's name.
-.Pp
-All sessions of one account are served by one process, which runs one
-command at a time.
+All sessions of one account are served by one process, one command
+at a time.
.Li FETCH
and
.Li SEARCH
.Li COPY
and
.Li MOVE
-do not.
-One of these over a very large mailbox therefore delays every other
+do not, so one of these over a very large mailbox delays every other
session of the same account until it completes.
+.Pp
+If the process holding the TLS private key exits, it is not restarted,
+and new TLS handshakes fail until
+.Nm
+is restarted.
+.Pp
+.Li INBOX
+cannot be unsubscribed:
+.Li UNSUBSCRIBE INBOX
+replies
+.Li NO .
+A subscription is kept when its mailbox is deleted, as RFC 9051
+section 6.3.7 recommends, and is not moved when its mailbox is renamed,
+as section 6.3.6 describes.
+.Pp
+.Li RENAME INBOX
+is refused, which RFC 9051 section 6.3.6 notes some servers do.
+.Pp
+.Li LIST
+selection options other than
+.Li SUBSCRIBED ,
+return options, and more than one mailbox pattern are refused.
+.Pp
+.Li FETCH
+does not support
+.Li BINARY ,
+.Li BINARY.PEEK
+or
+.Li BINARY.SIZE ,
+which are answered
+.Li BAD .
+It does not return a section that combines a part number with
+.Li HEADER ,
+.Li HEADER.FIELDS ,
+.Li TEXT
+or
+.Li MIME ,
+nor
+.Li RFC822 ,
+.Li RFC822.HEADER
+or
+.Li RFC822.TEXT ;
+it returns the other items asked for and answers
+.Li NO .
+.Pp
+A session that deletes its selected mailbox is returned to the
+authenticated state, as if by
+.Li CLOSE .
+.Pp
+Mailbox names must be well-formed UTF-8 per RFC 3629.
+Of the Net-Unicode
+.Pq RFC 5198
+requirements,
+.Nm
+refuses the C1 controls and refuses U+FEFF anywhere in a name, which is
+stricter than RFC 5198.
+It does not normalize names to NFC, so two canonically equivalent
+spellings are two distinct mailboxes, and it does not check names
+against a Unicode version.
+.Li CREATE ,
+.Li RENAME ,
+.Li SUBSCRIBE ,
+.Li UNSUBSCRIBE ,
+.Li COPY
+and
+.Li MOVE
+refuse a name that fails these checks with
+.Li NO [CANNOT] ,
+and other commands report it as nonexistent.
+An existing directory with such a name is not listed and cannot be
+selected.
+.Pp
+The names
+.Pa tmp ,
+.Pa new
+and
+.Pa cur ,
+and the names of the files listed under
+.Sx FILES
+that are kept in a maildir, cannot be used as mailbox names.
blob - ac8f63423c2f281213f976240a94b8fa2e18ebd3
blob + ba320d62cfd97a54d1625488669effc1319157c3
--- src/imapd.conf.example
+++ src/imapd.conf.example
#
-# imapd.conf example, see imapd(8) for the full directive list.
+# imapd.conf example, see imapd.conf(5) 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,
# The address may also be "::" (all IPv6 interfaces) or "*" (both IPv4 and
# IPv6, binds two sockets per listener, one of each family, matching
# httpd.conf(5)'s own "*" convention), or a single literal IPv4/IPv6
-# address. Hostnames are not accepted, see imapd(8) for why. For example,
-# to listen on both address families:
+# address. Hostnames are not accepted. 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). 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.conf(5). 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
# routinely carries larger attachments than that.
attachment max 41943040
-# Largest message a client may upload with APPEND, see imapd(8). Must be
+# Largest message a client may upload with APPEND, see imapd.conf(5). Must be
# between 1 and 1073741824 (1 GiB) bytes. Defaults to 36700160 (35 MiB),
# the same as smtpd.conf(5)'s max-message-size default, so a message the
# local MTA accepts can also be saved by a client. It may not exceed
blob - /dev/null
blob + f46a691dfe5110714c6b3d4809ba96583710a330 (mode 644)
--- /dev/null
+++ src/imapd.conf.5
+.\" $OpenIMAPD$
+.\"
+.\" Written for the OpenIMAPD project. Public domain / no rights reserved,
+.\" matching the project's ports-oriented, OpenBSD-base-inclusion goal.
+.\"
+.Dd $Mdocdate: September 30 2026 $
+.Dt IMAPD.CONF 5
+.Os
+.Sh NAME
+.Nm imapd.conf
+.Nd IMAP daemon configuration file
+.Sh DESCRIPTION
+.Nm
+is the configuration file for the IMAP daemon,
+.Xr imapd 8 .
+Every setting has a default, so an empty file is a valid configuration.
+.Xr imapd 8
+refuses to start if the file does not exist.
+.Pp
+The file must be owned by root or by the user running
+.Xr imapd 8 ,
+and must not be group writable, group executable or accessible by
+others.
+.Pp
+The current line can be extended over multiple lines using a backslash
+.Pq Sq \e .
+Comments can be put anywhere in the file using a hash mark
+.Pq Sq # ,
+and extend to the end of the current line.
+Arguments not beginning with a letter, digit, colon, underscore or
+asterisk must be quoted.
+.Pp
+Additional configuration files can be included with the
+.Ic include
+keyword, for example:
+.Bd -literal -offset indent
+include "/etc/imapd.conf.local"
+.Ed
+.Pp
+An included file must meet the same ownership and permission
+requirements.
+.Pp
+When
+.Xr imapd 8
+receives
+.Dv SIGHUP
+it rereads the file and applies the new settings to connections
+accepted afterwards, except for
+.Ic listen on
+and
+.Ic credentials ,
+which take effect only when
+.Xr imapd 8
+is restarted.
+Sessions already connected are not disturbed and keep the settings
+they started with.
+A file that fails to parse is logged and the running configuration is
+kept.
+.Ss Macros
+A macro is defined with a command of the form
+.Ar name Ns = Ns Ar value .
+The macro
+.Ar name
+cannot contain whitespace.
+Within unquoted arguments, the string
+.Pf $ Ar name
+is later expanded to
+.Ar value .
+Macros can also be defined on the command line with the
+.Fl D
+option of
+.Xr imapd 8 .
+.Ss Global configuration
+.Bl -tag -width Ds
+.It Ic account sessions Ar count
+The most sessions one account may have open at once, from all of its
+clients together.
+A login past the limit is answered
+.Li NO
+with the
+.Li LIMIT
+response code and the connection is closed.
+A client that vanishes without closing its connection holds its
+session until TCP keepalive notices, which by default is 2 hours 10
+minutes; see
+.Va net.inet.tcp.keepidle
+and
+.Va net.inet.tcp.keepintvl
+in
+.Xr sysctl 8 .
+Must be between 1 and 4096.
+The default is 8.
+.It Ic append max Ar bytes
+The largest message a client may upload with
+.Li APPEND .
+A larger one is refused with the
+.Li LIMIT
+response code and nothing is stored.
+Must be between 1 and 1073741824 and may not exceed
+.Ic attachment max .
+The default is 36700160 (35 MiB), the same as the
+.Ic smtp max-message-size
+default of
+.Xr smtpd.conf 5 .
+.It Ic attachment max Ar bytes
+The largest message
+.Xr imapd 8
+will read to produce its
+.Li BODYSTRUCTURE
+or one of its MIME parts.
+A larger message is treated as one whose structure cannot be produced;
+it is not truncated.
+The same limit bounds what
+.Li SEARCH
+reads of a message, so a search on
+.Li BODY
+or
+.Li TEXT
+that reaches a larger message fails with
+.Li NO .
+Must be between 12000 and 1073741824 and may not be less than
+.Ic append max .
+The default is 41943040 (40 MiB).
+.It Ic connections max Ar count
+The most connections open at once, logged in or not.
+Past it, a new connection on the cleartext port is answered with a
+.Li BYE
+carrying the
+.Li UNAVAILABLE
+response code, and one on the implicit TLS port is closed with nothing
+sent.
+.Pp
+C connections, L of them not yet logged in, for A accounts need up to
+C + L + 2A processes plus two, and the parent holds a descriptor to each.
+These count against the
+.Va kern.maxproc
+.Xr sysctl 8
+and the
+.Cm openfiles
+limit of the
+.Xr login.conf 5
+class
+.Xr imapd 8
+runs in; raise those before raising this.
+Must be between 1 and 4096.
+The default is 256, which fits the
+.Ox
+defaults.
+.It Ic credentials Ar path
+The credentials file; see
+.Xr imapd 8 .
+The default is
+.Pa /etc/imapd/credentials .
+.It Ic idle poll Ar seconds
+How often a session in
+.Li IDLE
+checks its selected mailbox for changes.
+Must be between 1 and 300, or 0 to disable polling, in which case a
+session in
+.Li IDLE
+is told nothing until it sends
+.Li DONE .
+The default is 5.
+.It Ic listen on Ar address Oo Ic tls Oc Ic port Ar port
+Listen on
+.Ar address
+and
+.Ar port .
+Without
+.Ic tls ,
+the listener is cleartext and offers
+.Li STARTTLS ;
+with
+.Ic tls ,
+it uses implicit TLS.
+Each may be given once, and both must use the same
+.Ar address .
+.Pp
+.Ar address
+is a literal IPv4 or IPv6 address,
+.Ar 0.0.0.0
+for all IPv4 addresses,
+.Ar ::
+for all IPv6 addresses, or
+.Sq *
+for all IPv4 and IPv6 addresses.
+Hostnames are not accepted.
+.Pp
+If no
+.Ic listen on
+is given,
+.Xr imapd 8
+listens on 0.0.0.0 port 143 and 0.0.0.0 tls port 993.
+If any is given, only the listeners named are bound.
+.It Ic lock timeout Ar seconds
+How long a command waits for a mailbox lock held by another process
+before answering
+.Li NO
+with the
+.Li INUSE
+response code.
+This bounds a holder that is stuck, not one that is slow, and should
+be raised for mailboxes much larger than a hundred thousand messages.
+Must be between 1 and 3600, or 0 to wait indefinitely.
+The default is 120.
+.It Ic login grace Ar seconds
+How long a connection may remain unauthenticated before it is closed,
+including one that never begins a TLS handshake.
+This is the pre-authentication timer permitted by RFC 9051 section 5.4;
+it does not apply once a session has authenticated.
+Must be between 1 and 3600, or 0 to disable it.
+The default is 60.
+.It Ic spool Ar path
+The mail spool.
+Each account's mailbox process is
+.Xr chroot 2 Ns ed
+into
+.Ar path ,
+and maildirs in the credentials file are relative to it.
+.Ar path
+and every entry directly under it must be owned by root and not be
+writable by mail users;
+.Xr imapd 8
+does not check this.
+The default is
+.Pa /var/mail/imapd .
+.It Ic startups begin Ar count Ic rate Ar percent Ic full Ar count
+Limit concurrent connections that have not yet authenticated, using
+the algorithm of
+.Cm MaxStartups
+in
+.Xr sshd_config 5 .
+Below
+.Ic begin
+such connections, every new connection is accepted.
+From
+.Ic begin ,
+new connections are refused with a probability rising linearly from
+.Ic rate
+percent to 100 percent at
+.Ic full .
+A refused connection is answered as for
+.Ic connections max .
+.Pp
+The counts must be between 0 and 1000000,
+.Ar percent
+between 0 and 100, and
+.Ic full
+may not be less than
+.Ic begin .
+Setting
+.Ic full
+to 0 disables the limit.
+The default is
+.Ic begin
+10
+.Ic rate
+30
+.Ic full
+100.
+.It Ic startups per-source Ar count | Ic none
+The most connections that have not yet authenticated from any one
+address, as
+.Cm PerSourceMaxStartups
+in
+.Xr sshd_config 5 .
+Each IPv6 address counts separately.
+A refused connection is answered as for
+.Ic connections max .
+Must be between 1 and 1000000, or
+.Ic none
+to disable the limit.
+The default is 5.
+.It Ic tls certificate Ar path
+The TLS certificate.
+The default is
+.Pa /etc/ssl/imapd.crt .
+.It Ic tls key Ar path
+The TLS private key, which must be owned by root and have no
+permissions beyond 0740.
+A key that fails this check is not loaded, and a warning is logged.
+The default is
+.Pa /etc/ssl/private/imapd.key .
+.El
+.Sh FILES
+.Bl -tag -width "/usr/local/share/examples/imapd/imapd.conf" -compact
+.It Pa /etc/imapd.conf
+.Xr imapd 8
+configuration file.
+.It Pa /usr/local/share/examples/imapd/imapd.conf
+Example configuration file.
+.El
+.Sh EXAMPLES
+Listen with implicit TLS only, as RFC 8314 section 3 recommends, on all
+IPv4 and IPv6 addresses:
+.Bd -literal -offset indent
+listen on * tls port 993
+.Ed
+.Pp
+Use a certificate and key named for the host, and allow each account
+more concurrent sessions:
+.Bd -literal -offset indent
+tls certificate "/etc/ssl/mail.example.com.fullchain.pem"
+tls key "/etc/ssl/private/mail.example.com.key"
+account sessions 16
+.Ed
+.Sh SEE ALSO
+.Xr login.conf 5 ,
+.Xr smtpd.conf 5 ,
+.Xr sshd_config 5 ,
+.Xr imapd 8 ,
+.Xr sysctl 8
+.Sh HISTORY
+.Nm
+was written for the OpenIMAPD project.
blob - 39657eccd76572179da9fd1272ca3f9e4935ea00
blob + 1399571babfb3228b192fa93d49b66a9bbe29e86
--- src/imapd.h
+++ src/imapd.h
#include <imsg.h>
#include <stdint.h>
-#define IMAPD_VERSION "0.1.9"
+#define IMAPD_VERSION "0.2.0"
#define IMAPD_USER "_imapd"
#define IMAPD_AUTH_USER "_imapauth"
/* RFC 9051 SS6.3.7/SS6.3.8 (SUBSCRIBE/UNSUBSCRIBE) */
IMSG_MBOX_SUBSCRIBE,
- IMSG_MBOX_UNSUBSCRIBE
+ IMSG_MBOX_UNSUBSCRIBE,
+ IMSG_MBOX_SAVED_CLEAR
};
struct imsgev {
#define MBOX_NAME_MAX 256
-/* RFC 5530 NONEXISTENT/ALREADYEXISTS, RFC 9051 SS7.1 INUSE */
+/* RFC 5530 NONEXISTENT/ALREADYEXISTS/LIMIT; RFC 9051 INUSE, NOTSAVED */
enum mbox_op_error {
MBOX_ERR_UNSET = 0,
MBOX_OP_OK,
MBOX_OP_ERR_ALREADY_EXISTS,
MBOX_OP_ERR_BUSY,
MBOX_OP_ERR_EXPUNGEISSUED,
+ MBOX_OP_ERR_LIMIT,
+ MBOX_OP_ERR_NOTSAVED,
};
/* QRESYNC select-param (RFC 7162 SS3.2.5). */
int hi_is_star;
};
+/* RFC 9051 SS9 seq-last-command, "$": the all-zero range */
+#define SEQ_RANGE_SAVED(r) ((r)->lo == 0 && !(r)->lo_is_star)
+
/* keeps the trailing array under MAX_IMSGSIZE */
#define SEQSET_MAX_RANGES 500
#define SEARCH_OP_BCC 32
#define SEARCH_OP_BODY 33
#define SEARCH_OP_TEXT 34
+#define SEARCH_OP_SAVED 35
struct search_node {
int op;
uint32_t name_len;
};
+/* RFC 9051 SS6.4.4 search-return-opt */
+#define SEARCH_RETURN_MIN (1U << 0)
+#define SEARCH_RETURN_MAX (1U << 1)
+#define SEARCH_RETURN_ALL (1U << 2)
+#define SEARCH_RETURN_COUNT (1U << 3)
+#define SEARCH_RETURN_SAVE (1U << 4)
+
struct imsg_mbox_search {
uint32_t nnodes;
uint32_t poollen;
+ uint32_t return_opts;
char pool[SEARCH_OPERANDS_MAX];
};
blob - fe42950141044244657ac834101c6d2d9009749c
blob + 2a1b2706eaad3081c7d655ba21ff1fcba647e5ee
--- src/index.c
+++ src/index.c
memset(idx, 0, sizeof(*idx));
}
-/* RFC 7162 SS3.2.5.1 QRESYNC resync */
+/* RFC 7162 SS3.2.5.1 QRESYNC resync; "$" is the one before SELECT */
void
qresync_send_resync(const struct imsg_mbox_select *req,
const struct seq_range *ranges, uint32_t nranges,
struct mbox_index *idx, struct store_session *ss)
{
struct imsgev *iev = &ss->iev;
- struct seq_range resolved[SEQSET_MAX_RANGES];
- uint32_t nresolved, max_hi, i;
+ struct seq_range resolved[2 * SEQSET_MAX_RANGES];
+ uint32_t nresolved, max_hi, i, used;
if (req->qresync_has_uids) {
- nresolved = seqset_resolve(ranges, nranges,
+ used = nranges > 0 && SEQ_RANGE_SAVED(&ranges[nranges - 1]);
+ nresolved = seqset_resolve(ranges, nranges - used,
index_max_uid(idx), 0, resolved);
+ if (used) {
+ memcpy(resolved + nresolved, ss->saved,
+ ss->nsaved * sizeof(*resolved));
+ nresolved += ss->nsaved;
+ }
} else {
/* RFC 7162 SS3.2.5.1: no known-uids means 1:uidnext-1 */
if (idx->uidnext <= 1)
*by_uid = 1;
}
+/* view_resolve(), a last "$" appended as UIDs: -1 over the cap */
+int
+saved_resolve(const struct store_session *ss, const struct seq_range *in,
+ uint32_t nin, struct seq_range *out, uint32_t *nout, int *by_uid,
+ int *used)
+{
+ int u;
+
+ u = *used = nin > 0 && SEQ_RANGE_SAVED(&in[nin - 1]);
+ if (u)
+ nin--;
+ view_resolve(ss, in, nin, out, nout, by_uid);
+ if (!u)
+ return (0);
+ if (*by_uid == 0 && nin > 0)
+ return (-2);
+ *by_uid = 1;
+ if (*nout + ss->nsaved > SEQSET_MAX_RANGES)
+ return (-1);
+ memcpy(out + *nout, ss->saved, ss->nsaved * sizeof(*out));
+ *nout += ss->nsaved;
+ return (0);
+}
+
/* RFC 2180 SS4.1.2: does the set name a message expunged, not yet told? */
int
view_names_ghost(const struct store_session *ss, const struct mbox_index *idx,
blob - 3726d921cf0beb10b9d3633754337174daf18e4a
blob + 0741e76249dd96e3781a2f87114e35397b1d0fcd
--- src/listener.c
+++ src/listener.c
(1U << SESSION_DELETING) | (1U << SESSION_RENAMING) | \
(1U << SESSION_LISTING))
#define ST_NOTAUTH (1U << SESSION_NOT_AUTH)
+#define ST_PREAUTH \
+ (ST_NOTAUTH | (1U << SESSION_AUTHENTICATING) | \
+ (1U << SESSION_STORE_PENDING))
/* RFC 9051 SS9; excludes transient states, which have their own pending_tag. */
#define ST_AUTH \
for (;;) {
uint64_t nonsync_len;
size_t consumed, linelen;
- int alive;
+ int alive, resume;
if (s->literal_discard > 0) {
uint64_t take;
s->literal_discard -= take;
}
if (s->literal_discard > 0)
- break; /* need more data */
+ break;
s->literal_skipline = 1;
continue;
}
}
if (s->literal_remaining > 0)
- break; /* need more data */
+ break;
if (s->inbuflen < 2)
break;
take = s->inbuflen < s->cmd_octets ?
s->inbuflen : s->cmd_octets;
if (take == 0)
- break; /* need more data */
+ break;
if (memchr(s->inbuf, '\0', take) != NULL) {
/* RFC 9051 SS9: CHAR8 excludes NUL */
session_cmd_reply(s, "BAD", "NUL in a literal");
return;
}
+ resume = 0;
if (s->literal_skipline) {
s->literal_skipline = 0;
alive = 1;
+ resume = 1;
} else if (s->cmd_gather) {
s->cmd_gather = 0;
alive = 1;
linelen + 1);
if (!s->cmd_queued)
alive = session_run_cmd(s, s->cmdbuf);
- else if (nonsync_len > 0)
+ else if (nonsync_len > 0) {
+ s->cmdlen += linelen;
(void)session_gather(s, nonsync_len);
- else {
+ } else {
s->cmd_queued = 0;
alive = session_queue(s, s->cmdbuf);
}
}
- if (alive && nonsync_len == 0 && !s->cmd_gather &&
- s->cmd_queue_n > 0 && !session_is_busy(s))
- alive = session_dequeue_next(s);
+ resume = 1;
} else if (s->auth_cont) {
/* the line is the user's base64 password, see below */
s->scrub_inbuf = 1;
} else {
alive = session_run_cmd(s, s->inbuf);
}
+ if (alive && resume && nonsync_len == 0)
+ alive = session_dequeue_next(s);
if (alive == 0)
return; /* s was torn down (LOGOUT), do not touch */
event_active(&s->client_ev, EV_READ, 1);
}
-#define SESSION_WRITE_POLL_TIMEOUT_MS 5000
+/* RFC 9051 SS5.4: a short timer before login, 30 minutes after */
+#define SESSION_WRITE_PREAUTH_MS 5000
+#define SESSION_WRITE_AUTH_MS (1800 * 1000)
void
session_write(struct session *s, const char *buf, size_t len)
{
size_t sent = 0;
+ int timeout;
if (s->write_failed)
return;
+ timeout = (ST_PREAUTH & (1U << s->state)) ?
+ SESSION_WRITE_PREAUTH_MS : SESSION_WRITE_AUTH_MS;
if (log_getverbose() > 0) {
char dbuf[301];
if (errno == EAGAIN || errno == EWOULDBLOCK) {
pfd.fd = s->client_fd;
pfd.events = POLLOUT;
- if (poll(&pfd, 1,
- SESSION_WRITE_POLL_TIMEOUT_MS)
- <= 0) {
+ if (poll(&pfd, 1, timeout) <= 0) {
log_warnx("session %u: write: "
"timed out or poll error",
s->id);
- s->write_failed = 1;
- return;
+ goto fail;
}
continue;
}
log_warn("session %u: write", s->id);
- s->write_failed = 1;
- return;
+ goto fail;
}
sent += (size_t)n;
}
if (n == TLS_WANT_POLLIN || n == TLS_WANT_POLLOUT) {
pfd.fd = s->client_fd;
pfd.events = (n == TLS_WANT_POLLIN) ? POLLIN : POLLOUT;
- pret = poll(&pfd, 1, SESSION_WRITE_POLL_TIMEOUT_MS);
+ pret = poll(&pfd, 1, timeout);
if (pret == -1) {
log_warn("session %u: poll (tls_write retry)",
s->id);
- s->write_failed = 1;
- return;
+ goto fail;
}
if (pret == 0) {
log_warnx("session %u: tls_write: timed out "
"waiting for socket", s->id);
- s->write_failed = 1;
- return;
+ goto fail;
}
continue;
}
if (n == -1) {
log_warnx("session %u: tls_write: %s", s->id,
tls_error(s->tls_ctx));
- s->write_failed = 1;
- return;
+ goto fail;
}
sent += (size_t)n;
}
+ return;
+
+fail:
+ s->write_failed = 1;
+ /* the client may never send again, so do not wait for it */
+ event_active(&s->client_ev, EV_READ, 1);
}
blob - 272fe1eb376922247b102eb04ff8e4fc7e165ebc
blob + 900627b3d2989560a5d97bc7a6a83e9a5e7e8ed8
--- src/listener.h
+++ src/listener.h
/* RFC 7162 SS7: a mod-sequence is a positive 63-bit integer */
#define MODSEQ_MAX INT64_MAX
-/* RFC 9051 SS6.4.4 ESEARCH result options; SAVE is refused */
-#define SEARCH_RETURN_MIN (1U << 0)
-#define SEARCH_RETURN_MAX (1U << 1)
-#define SEARCH_RETURN_ALL (1U << 2)
-#define SEARCH_RETURN_COUNT (1U << 3)
-
struct vanished_range {
uint32_t lo;
uint32_t hi;
int parse_qresync_group(char *, struct imsg_mbox_select *,
struct seq_range[SEQSET_MAX_RANGES], uint32_t *,
const char **);
-char *split_trailing_modifiers(char *);
int parse_fetch_modifiers(char *, struct imsg_mbox_fetch *,
const struct session *, int, int *, const char **);
int parse_store_modifiers(char *, struct imsg_mbox_store *,
blob - c64b5e7d6cf102bfad49200a205fe00d009d82b6
blob + c91609ea6e57b9ca568497435bfef3cbb3f02b97
--- src/mbox_copy.c
+++ src/mbox_copy.c
index_lock_release(il_a);
index_lock_release(il_b);
- if (result->error != MBOX_OP_ERR_NO_SUCH_MAILBOX)
+ if (result->error != MBOX_OP_ERR_NO_SUCH_MAILBOX &&
+ result->error != MBOX_OP_ERR_LIMIT)
result->error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
index_free(idx_a);
index_free(idx_b);
int ok = 1;
char desttarget[MBOX_NAME_MAX];
int cross_mailbox;
- int destfd = -1, dfd, got;
+ int destfd = -1, dfd, got, used;
memset(&idx_a, 0, sizeof(idx_a));
memset(&idx_b, 0, sizeof(idx_b));
}
dfd = cross_mailbox ? destfd : ss->mailbox_dir_fd;
/* RFC 2180 SS4.4.2: the numbers are those before the EXPUNGEs */
- view_resolve(ss, ranges, nranges, vr, &nvr, &req->by_uid);
+ if ((got = saved_resolve(ss, ranges, nranges, vr, &nvr,
+ &req->by_uid, &used)) != 0) {
+ if (got == -1)
+ result.error = MBOX_OP_ERR_LIMIT;
+ ok = 0;
+ goto close_dest;
+ }
(void)view_sync(ss, srcidx, 1);
if (!stage_copy_messages(ss, srcidx, req, vr, nvr, &staged,
int ok = 1;
char desttarget[MBOX_NAME_MAX];
int cross_mailbox;
- int destfd = -1, got;
+ int destfd = -1, got, used;
struct seq_range vr[SEQSET_MAX_RANGES];
uint32_t nvr;
goto done;
}
- view_resolve(ss, ranges, nranges, vr, &nvr, &req->by_uid);
+ if ((got = saved_resolve(ss, ranges, nranges, vr, &nvr,
+ &req->by_uid, &used)) != 0) {
+ if (got == -1)
+ result.error = MBOX_OP_ERR_LIMIT;
+ ok = 0;
+ if (destfd != -1)
+ close(destfd);
+ goto done;
+ }
(void)view_sync(ss, srcidx, 1);
if (!cross_mailbox) {
if (!move_same_mailbox(req, vr, nvr, srcidx, &nmoved, ss))
blob - 24b638d4b47dfecd0463e58273e3821e1b60219a
blob + 8b2586daa09bc057f91e13740e82ca128037d069
--- src/mbox_fetch.c
+++ src/mbox_fetch.c
struct index_lock il = INDEX_LOCK_INIT;
struct seq_range vr[SEQSET_MAX_RANGES];
uint32_t i, nvr;
- int locked, synced, rc, set_seen;
+ int locked, synced, rc, set_seen, used;
set_seen = (req->attrs & MBOX_FETCH_SET_SEEN) != 0;
/* Before the reset below, so that a busy return changes nothing. */
pcache_check_mailbox(ss->selected_mailbox, idx->uidvalidity);
synced = view_sync(ss, idx, req->by_uid) == 0;
- view_resolve(ss, ranges, nranges, vr, &nvr, &fw->req.by_uid);
+ if ((rc = saved_resolve(ss, ranges, nranges, vr, &nvr,
+ &fw->req.by_uid, &used)) != 0) {
+ if (set_seen)
+ index_lock_release(&il);
+ fw->limit = rc == -1;
+ fetch_walk_finish(ss, 0);
+ return (0);
+ }
/* RFC 9051 SS6.4.9 */
fw->nresolved = seqset_resolve(vr, nvr, fw->req.by_uid ?
index_max_uid(idx) : (uint32_t)idx->nlines, !fw->req.by_uid,
fw->resolved);
fw->max_hi = seqset_max_hi(fw->resolved, fw->nresolved);
- fw->ghost = !req->by_uid &&
+ fw->ghost = !req->by_uid && !used &&
view_names_ghost(ss, idx, fw->resolved, fw->nresolved);
if (set_seen) {
cur_snapshot_discard(&ss->cur_snap);
memset(&result, 0, sizeof(result));
- result.error = !ok ? MBOX_OP_ERR_GENERIC : fw->ghost ?
+ result.error = !ok ? (fw->limit ? MBOX_OP_ERR_LIMIT :
+ MBOX_OP_ERR_GENERIC) : fw->ghost ?
MBOX_OP_ERR_EXPUNGEISSUED : MBOX_OP_OK;
result.count = fw->sent;
if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result,
blob - db797c1b29abb06e90a0be290a8fd944fd711c7f
blob + 8e433d36a9774891a96bfabd88ce6621fbbabced
--- src/mbox_manage.c
+++ src/mbox_manage.c
if (req->qresync && req->qresync_uidvalidity == idx.uidvalidity)
qresync_send_resync(req, ranges, nranges, &idx, ss);
+ ss->nsaved = 0;
idle_baseline_seed(ss, &idx);
index_free(&idx);
blob - 40b33bfac7582be921b04b3c91cd3267464ec215
blob + f27afc00ad6efce70e76eb235576e2801e307357
--- src/mbox_search.c
+++ src/mbox_search.c
static void search_walk_step(struct store_session *);
static void search_walk_finish(struct store_session *, int);
static void search_walk_yield(int, short, void *);
+static void search_save_match(struct store_search_walk *, uint32_t,
+ uint32_t);
+static int search_save_finish(struct store_session *, int);
struct search_msg_ctx {
uint32_t seqno;
int64_t internaldate;
uint64_t size;
uint64_t modseq; /* RFC 7162 SS3.1.5 MODSEQ search key */
+ const struct seq_range *saved;
+ uint32_t nsaved;
};
static int
return (m->uid >= n->seq_lo && m->uid <= n->seq_hi);
case SEARCH_OP_MODSEQ:
return (m->modseq >= (uint64_t)n->num);
+ case SEARCH_OP_SAVED:
+ return (seqset_contains(m->saved, m->nsaved, m->uid));
default:
return (0);
}
search_walk_abort(ss);
}
memset(sw, 0, sizeof(*sw));
+ sw->opts = req->return_opts;
evtimer_set(&sw->yield_ev, search_walk_yield, ss);
if (locked == -1) {
m.seqno = view_seqno(ss, rec.uid, i);
m.uid = rec.uid;
m.modseq = rec.modseq;
+ m.saved = ss->saved;
+ m.nsaved = ss->nsaved;
if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
rec.basename, &size, suffix, sizeof(suffix)) == -1) {
if (r == 1) {
struct imsg_mbox_search_match match;
+ if (sw->opts & SEARCH_RETURN_SAVE)
+ search_save_match(sw, i, m.uid);
memset(&match, 0, sizeof(match));
match.seqno = m.seqno;
match.uid = m.uid;
cur_snapshot_discard(&ss->cur_snap);
memset(&result, 0, sizeof(result));
- result.error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
+ result.error = !search_save_finish(ss, ok) ? MBOX_OP_ERR_NOTSAVED :
+ ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
result.count = sw->sent;
if (imsg_compose(&ss->iev.ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result,
sizeof(result)) == -1)
sw->active = 0;
}
+/* RFC 9051 SS6.4.4.1: matches on adjacent index lines share a range */
static void
+search_save_match(struct store_search_walk *sw, uint32_t line, uint32_t uid)
+{
+ if (sw->sv_first == 0)
+ sw->sv_first = uid;
+ sw->sv_last = uid;
+ if (sw->nsv > 0 && sw->sv_line + 1 == line)
+ sw->sv[sw->nsv - 1].hi = uid;
+ else if (sw->nsv < SEQSET_MAX_RANGES) {
+ memset(&sw->sv[sw->nsv], 0, sizeof(sw->sv[0]));
+ sw->sv[sw->nsv].lo = sw->sv[sw->nsv].hi = uid;
+ sw->nsv++;
+ } else
+ sw->sv_full = 1;
+ sw->sv_line = line;
+}
+
+/* RFC 9051 SS6.4.4.1 and Table 4; 0 when refused, as NOTSAVED */
+static int
+search_save_finish(struct store_session *ss, int ok)
+{
+ struct store_search_walk *sw = &ss->search;
+ uint32_t o = sw->opts, n = 0;
+
+ if (!(o & SEARCH_RETURN_SAVE))
+ return (1);
+ ss->nsaved = 0;
+ if (!ok)
+ return (1);
+ if ((o & (SEARCH_RETURN_MIN | SEARCH_RETURN_MAX)) &&
+ !(o & (SEARCH_RETURN_ALL | SEARCH_RETURN_COUNT))) {
+ if (sw->sv_first == 0)
+ return (1);
+ memset(ss->saved, 0, 2 * sizeof(ss->saved[0]));
+ if (o & SEARCH_RETURN_MIN) {
+ ss->saved[n].lo = ss->saved[n].hi = sw->sv_first;
+ n++;
+ }
+ if ((o & SEARCH_RETURN_MAX) && (n == 0 ||
+ sw->sv_last != sw->sv_first)) {
+ ss->saved[n].lo = ss->saved[n].hi = sw->sv_last;
+ n++;
+ }
+ ss->nsaved = n;
+ return (1);
+ }
+ if (sw->sv_full)
+ return (0);
+ memcpy(ss->saved, sw->sv, sw->nsv * sizeof(sw->sv[0]));
+ ss->nsaved = sw->nsv;
+ return (1);
+}
+
+static void
search_walk_yield(int fd, short event, void *arg)
{
struct store_session *ss = arg;
blob - 4df03f1fb49ab72829be8f927238bad22709c4f4
blob + 7bb5732cb223f7c13c61e2c4dcfe219cd96454ab
--- src/mbox_store.c
+++ src/mbox_store.c
size_t nsteps = 0, k;
uint32_t nresolved, nvr, sent = 0;
uint64_t reported = 0;
- int ok = 1, by_uid, synced;
- int ghost;
+ int ok = 1, by_uid, synced, used, rc;
+ int ghost, limit = 0;
memset(&idx, 0, sizeof(idx));
synced = view_sync(ss, &idx, req->by_uid) == 0;
by_uid = req->by_uid;
- view_resolve(ss, ranges, nranges, vr, &nvr, &by_uid);
+ if ((rc = saved_resolve(ss, ranges, nranges, vr, &nvr, &by_uid,
+ &used)) != 0) {
+ limit = rc == -1;
+ ok = 0;
+ goto done;
+ }
reported = idx.highestmodseq;
/* RFC 9051 SS6.4.9 */
nresolved = seqset_resolve(vr, nvr, by_uid ?
index_max_uid(&idx) : (uint32_t)idx.nlines, !by_uid, resolved);
- ghost = !req->by_uid && view_names_ghost(ss, &idx, resolved,
- nresolved);
+ ghost = !req->by_uid && !used && view_names_ghost(ss, &idx,
+ resolved, nresolved);
ok = store_apply(ss, req, &idx, resolved, nresolved, by_uid, synced,
NULL, &steps, &nsteps);
memset(&result, 0, sizeof(result));
/* RFC 2180 SS4.2.1-SS4.2.3 */
- result.error = !ok ? MBOX_OP_ERR_GENERIC : ghost && !req->silent ?
+ result.error = !ok ? (limit ? MBOX_OP_ERR_LIMIT :
+ MBOX_OP_ERR_GENERIC) : ghost && !req->silent ?
MBOX_OP_ERR_EXPUNGEISSUED : MBOX_OP_OK;
result.count = sent;
/* never a value the index does not hold */
struct imsg_mbox_result result;
struct index_lock il = INDEX_LOCK_INIT;
struct seq_range resolved[SEQSET_MAX_RANGES];
+ struct seq_range vr[SEQSET_MAX_RANGES];
struct expunged *gone = NULL, *grown;
size_t in, out, ngone = 0, maxgone = 0, k;
- uint32_t sent = 0, nresolved = 0;
+ uint32_t sent = 0, nresolved = 0, nvr;
uint64_t reported = 0;
- int ok = 1, locked;
+ int ok = 1, locked, by_uid = 1, used, limit = 0;
memset(&idx, 0, sizeof(idx));
if (!req->silent)
(void)view_sync(ss, &idx, 1);
- if (req->by_uid)
- nresolved = seqset_resolve(ranges, nranges,
- index_max_uid(&idx), 0, resolved);
+ if (req->by_uid) {
+ if (saved_resolve(ss, ranges, nranges, vr, &nvr, &by_uid,
+ &used) != 0) {
+ limit = 1;
+ ok = 0;
+ goto done;
+ }
+ nresolved = seqset_resolve(vr, nvr, index_max_uid(&idx), 0,
+ resolved);
+ }
out = 0;
for (in = 0; in < idx.nlines; in++) {
index_lock_release(&il);
memset(&result, 0, sizeof(result));
- result.error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
+ result.error = ok ? MBOX_OP_OK : limit ? MBOX_OP_ERR_LIMIT :
+ MBOX_OP_ERR_GENERIC;
result.count = sent;
/* never a value the index does not hold */
result.highestmodseq = reported;
blob - 419823c5e4586ed3d978ce3412b312479e50daa4
blob + e6e6dcf7bba24a7986c91c5d690b1721c12eeb23
--- src/parent.c
+++ src/parent.c
* Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net>
* Copyright (c) 2008 Gilles Chehade <gilles@poolp.org>
* Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
+ * Copyright (c) 2008 Reyk Floeter <reyk@openbsd.org>
*
* This file's boot-time peer-wiring handshake -- setup_peer_send() and
* setup_done_send(), and the IMSG_SETUP_PEER/IMSG_SETUP_DONE message
* message protocol and handshake shape are smtpd's; see the inline
* citations at this file's setup_peer_send() and setup_done_send().
*
+ * send_keymgr_init()'s TLS key permission check -- fstat(2), then
+ * st_uid != 0 and st_mode & (S_IRWXU|S_IRWXG|S_IRWXO) & ~0740 -- reuses
+ * smtpd's ssl_load_key() check (usr.sbin/smtpd/ssl.c:112-140) verbatim
+ * for that mask and rejection order; only the fixed 0740 bound and the
+ * imsg delivery around it are this file's own.
+ *
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
blob - 7afa4a6273c203c4638a8f55ff478bdda2a2aeb0
blob + fa25214c72a54d8338fdec64ec0a4114adcfd77c
--- src/search_cmd.c
+++ src/search_cmd.c
struct search_node node;
memset(&node, 0, sizeof(node));
- node.op = op;
+ node.op = SEQ_RANGE_SAVED(&ranges[i]) ? SEARCH_OP_SAVED : op;
node.seq_lo = ranges[i].lo;
node.seq_hi = ranges[i].hi;
node.lo_is_star = ranges[i].lo_is_star;
return (0);
}
- if (isdigit((unsigned char)*p) || *p == '*') {
+ if (isdigit((unsigned char)*p) || *p == '*' || *p == '$') {
if (parse_search_seqset(&p, ctx, SEARCH_OP_SEQSET, errmsg) ==
-1)
return (-1);
return (0);
}
-/* RFC 9051 SS6.4.4 search-return-opts; SAVE is refused */
+/* RFC 9051 SS6.4.4 search-return-opts */
int
parse_search_return_opts(char **pp, uint32_t *opts_out, const char **errmsg)
{
*opts_out |= SEARCH_RETURN_ALL;
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 "
- "pass";
- return (-2);
- } else {
+ else if (strcasecmp(word, "SAVE") == 0)
+ *opts_out |= SEARCH_RETURN_SAVE;
+ else {
*errmsg = "unsupported SEARCH RETURN option";
return (-1);
}
static void search_dispatch_finish(struct session *,
const struct search_parse_result *, struct search_node *);
+/* RFC 9051 SS6.4.4.1: a SAVE answered NO here empties "$" too */
+static void
+search_saved_clear(struct session *s, uint32_t opts)
+{
+ if ((opts & SEARCH_RETURN_SAVE) && s->store_iev != NULL)
+ (void)send_mbox_request(s, IMSG_MBOX_SAVED_CLEAR, "SEARCH",
+ "IMSG_MBOX_SAVED_CLEAR", NULL, 0, NULL, 0, 0);
+}
+
/* RFC 9051 SS6.4.4 SEARCH; US-ASCII and UTF-8 only */
int
cmd_search(struct session *s, const char *tag, char *args)
session_reply(s, tag, "BAD", errmsg);
return (1);
}
- if (rc == -2) {
- session_reply(s, tag, "NO", errmsg);
- return (1);
- }
while (*p == ' ')
p++;
}
if (strcasecmp(charset, "US-ASCII") != 0 &&
strcasecmp(charset, "UTF-8") != 0) {
+ search_saved_clear(s, return_opts);
/* RFC 9051 SS9 puts the charset list in parens */
session_reply(s, tag, "NO",
"[BADCHARSET (US-ASCII UTF-8)] unsupported "
if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
sizeof(s->pending_tag)) {
+ search_saved_clear(s, return_opts);
session_reply(s, tag, "NO",
"[SERVERBUG] internal error");
return (1);
return;
}
if (res->rc == -2) {
+ search_saved_clear(s, s->search_return_opts);
session_reply(s, s->pending_tag, "NO", res->errmsg);
s->state = SESSION_SELECTED;
return;
memset(&req, 0, sizeof(req));
req.nnodes = res->nnodes;
req.poollen = res->poollen;
+ req.return_opts = s->search_return_opts;
memcpy(req.pool, res->pool, res->poollen);
if (!send_mbox_request(s, IMSG_MBOX_SEARCH, "SEARCH",
"IMSG_MBOX_SEARCH", &req, sizeof(req), nodes, res->nnodes,
sizeof(struct search_node))) {
+ search_saved_clear(s, s->search_return_opts);
session_reply(s, s->pending_tag, "NO",
"[SERVERBUG] internal error");
s->state = SESSION_SELECTED;
session_reply(s, s->pending_tag, "NO", IMAP_BUSY_TEXT);
goto cleanup;
}
+ if (res->error == MBOX_OP_ERR_NOTSAVED) {
+ session_reply(s, s->pending_tag, "NO",
+ "[NOTSAVED] too many results to save");
+ goto cleanup;
+ }
if (res->error != MBOX_OP_OK || s->search_alloc_failed)
goto fail;
+ /* RFC 9051 SS6.4.4: SAVE alone suppresses ESEARCH */
+ if (s->search_return_opts == SEARCH_RETURN_SAVE)
+ goto done;
bufsize = SEARCH_RESP_PREFIX_MAX +
(size_t)s->search_nmatches * SEARCH_ALL_PER_MATCH + 1;
session_write(s, buf, len);
free(buf);
+done:
/* as RFC 9051 SS6.4.9's "UID <cmd> completed" */
session_reply(s, s->pending_tag, "OK",
s->cmd_by_uid ? "UID SEARCH completed" : "SEARCH completed");
blob - e861c44b599d3f715f3058d691d8bba81707ce65
blob + 39c29d5483acbef9f5451c27c58821b90f75bc4e
--- src/store.c
+++ src/store.c
store_attach(uint32_t id, int fd)
{
struct store_session *ss;
+ int flags;
if (fd == -1) {
log_warnx("session %u: IMSG_SETUP_PEER carried no fd", id);
return;
}
+ /* a listener that stops reading must not stop the account */
+ if ((flags = fcntl(fd, F_GETFL)) == -1 ||
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
+ log_warn("session %u: fcntl O_NONBLOCK", id);
+ close(fd);
+ return;
+ }
TAILQ_FOREACH(ss, &sessions, entry) {
if (ss->id == id)
break;
size_t len = 0;
uint32_t rtype = 0;
+ /* RFC 9051 SS6.4.4.1: a SAVE answered NO empties "$" */
+ if (type == IMSG_MBOX_SEARCH &&
+ (ss->deferred.req.search.return_opts & SEARCH_RETURN_SAVE))
+ ss->nsaved = 0;
switch (type) {
case IMSG_MBOX_STORE:
case IMSG_MBOX_EXPUNGE:
free(nodes);
break;
}
+ case IMSG_MBOX_SAVED_CLEAR:
+ if (imsg_get_len(&imsg) != 0) {
+ log_warnx("bad IMSG_MBOX_SAVED_CLEAR");
+ break;
+ }
+ ss->nsaved = 0;
+ break;
case IMSG_MBOX_STATUS: {
struct imsg_mbox_status req;
blob - 6ea1823a6c6737cef6a9d3176be27be5ab7ac21d
blob + 17234463d2693ea76eb2ecc26bc1cd5a72c89042
--- src/store_cmd.c
+++ src/store_cmd.c
else if (!s->copy_alloc_failed &&
res->error == MBOX_OP_ERR_BUSY)
snprintf(text, sizeof(text), "%s", IMAP_BUSY_TEXT);
+ else if (!s->copy_alloc_failed &&
+ res->error == MBOX_OP_ERR_LIMIT)
+ snprintf(text, sizeof(text), "[LIMIT] %s failed",
+ cmdname);
else
snprintf(text, sizeof(text), "%s failed", cmdname);
session_reply(s, s->pending_tag, "NO", text);
blob - 48f76022adc5e6e524a657288cc45ed786e32ccd
blob + 256654b5b1046de87793173ceb0b869e49e58304
--- src/store_internal.h
+++ src/store_internal.h
int wait_parser; /* paused until one comes */
int no_parser; /* none to be had: go without */
int ghost;
+ int limit;
uint64_t seen_modseq;
};
uint32_t sent;
uint32_t asked;
uint32_t walked;
+ uint32_t opts;
+ struct seq_range sv[SEQSET_MAX_RANGES];
+ uint32_t nsv;
+ uint32_t sv_line;
+ uint32_t sv_first;
+ uint32_t sv_last;
+ int sv_full;
};
/* two stat(2) calls, no lock: "." and "new" */
struct store_idle_probe idle_probe;
struct store_idle_baseline idle_baseline;
struct store_append append;
+
+ /* RFC 9051 SS6.4.4.1 "$", as UID ranges */
+ struct seq_range saved[SEQSET_MAX_RANGES];
+ uint32_t nsaved;
};
extern uint32_t session_id;
int view_sync(struct store_session *, const struct mbox_index *, int);
void view_resolve(const struct store_session *, const struct seq_range *,
uint32_t, struct seq_range *, uint32_t *, int *);
+int saved_resolve(const struct store_session *,
+ const struct seq_range *, uint32_t, struct seq_range *,
+ uint32_t *, int *, int *);
uint32_t view_seqno(const struct store_session *, uint32_t, uint32_t);
int view_names_ghost(const struct store_session *,
const struct mbox_index *, const struct seq_range *,
blob - 9a98af9d87a28cfac11ea767354ca2ceeecbbafd
blob + a7dcc836654c3556913a0bde6116b715907c581d
--- src/store_ipc.c
+++ src/store_ipc.c
}
snprintf(text, sizeof(text), "%s%s failed",
res->error == MBOX_OP_ERR_EXPUNGEISSUED ?
- "[EXPUNGEISSUED] " : "", cmdname);
+ "[EXPUNGEISSUED] " : res->error == MBOX_OP_ERR_LIMIT ?
+ "[LIMIT] " : "", cmdname);
session_reply(s, s->pending_tag, "NO", text);
return;
}