commit - e059bbcc10b86ee36a3b805c784b4057c0970bee
commit + 7d9334a7d40e0d8a8bbba80f1e1026a34ec35911
blob - a048b0269361225d8c0318f6c477b70062f00f9b
blob + 0ecebc52b885ae539520e05c47e61857f7d8be59
--- README.md
+++ README.md
## What it is
-- **Privilege-separated**, `smtpd`-style, across six processes: a root *parent* reads configuration and binds the listening sockets; unprivileged *listener* and *auth* children handle the network and credential checks; *keymgr* holds the TLS private key and performs every private-key operation on request, so the process terminating TLS never has the key in its address space; a per-connection *search-oracle* parses the `SEARCH` grammar, the largest attacker-reachable parser in the daemon — in a process with no descriptors and no filesystem; and 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: *parent* is the only process that can pass a file descriptor at all, and *search-oracle* runs on bare `stdio`.
+- **Privilege-separated**, `smtpd`-style, across six processes: a root *parent* reads configuration and binds the listening sockets; unprivileged *listener* and *auth* children handle the network and credential checks; *keymgr* holds the TLS private key and performs every private-key operation on request, so the process terminating TLS never has the key in its address space; a *parser* child, spun up by a *store* child on demand and retired after a spell idle, does every parse of attacker-reachable content — `SEARCH`'s grammar and `FETCH`'s `ENVELOPE`/`BODYSTRUCTURE`/header-field parsing alike — confined to `pledge(2)` `"stdio recvfd"`: it opens nothing itself, reading each message over a descriptor the store child passes it already open; and a *store* child is forked per authenticated account, shared by all of that account's sessions, chroots into the mail spool, and drops privileges to that account's own user before ever touching a message. `pledge(2)`, `unveil(2)`, and `chroot(2)` enforce these boundaries, not just convention: *parent* is the only process that can pass a file descriptor at all.
- **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 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`, `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.
-`SUBSCRIBE`, `UNSUBSCRIBE`, and ACL/shared-mailbox support are deliberately left out.
+ACL/shared-mailbox support is deliberately left out.
**`IDLE` is a poll, not a kernel-driven push.** An `IDLE`ing session rechecks its selected mailbox every `idle poll` seconds (default 5, see `imapd.conf`), so new mail, whether delivered by an external MTA or by another IMAP session, is reported within one interval rather than instantly. Most polls are two `stat(2)` calls and no lock: the store child only re-reads the index and streams UIDs when the mailbox directory or `new/` has actually been touched. Setting `idle poll 0` disables polling entirely, which restores the earlier behaviour where an `IDLE`ing session saw nothing until it sent `DONE`.
- 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`, `idle poll`, `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(8)`.
IPv6 is supported (`listen on ::` or `listen on *` for dual-stack) but not the default, see `imapd(8)`'s `listen on` directive.
blob - df2d90ea0e7fe4d18d6356afd2387738fd7053cb
blob + 275b0f615d50095ed9373998a6bc4fa669c369b6
--- contrib/imapduser.8
+++ contrib/imapduser.8
.\"
.\" Written for the OpenIMAPD project. Public domain / no rights reserved.
.\"
-.Dd $Mdocdate: September 18 2026 $
+.Dd $Mdocdate: September 25 2026 $
.Dt IMAPDUSER 8
.Os
.Sh NAME
.Xr imapd 8
.Sh HISTORY
.Nm
-was written for the OpenIMAPD project.
\ No newline at end of file
+was written for the OpenIMAPD project.
blob - d2d44336dd52fce32311a046d5ceb0a96265e1fc
blob + d5bf488c9c64067329371bc773787b88e8e5e738
--- src/Makefile
+++ src/Makefile
search_cmd.c store_cmd.c store_ipc.c \
auth.c \
keymgr.c \
- search_oracle.c \
+ parser.c \
store.c index.c mime.c envelope.c mbox_fetch.c mbox_search.c \
- mbox_store.c mbox_manage.c mbox_copy.c
+ mbox_store.c mbox_manage.c mbox_copy.c search_match.c
BINDIR= /usr/local/sbin
MANDIR= /usr/local/man/man
blob - 0b06f37a593afdff9c8300f25cf94fe6ea57708b
blob + b6d33d4ccde5be554ed9a3d89cfe51ff2455903e
--- src/append_cmd.c
+++ src/append_cmd.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* append_cmd.c: APPEND literal upload, async IMSG_MBOX_APPENDED completion. */
#include <sys/types.h>
#include <sys/queue.h>
#include "listener.h"
#include "mboxname.h"
-/* RFC 9051 SS9 date-time via sscanf(3); timegm(3) normalizes bad dates. */
+/* RFC 9051 SS9 date-time */
int
parse_date_time(const char *s, int64_t *out)
{
if (zsign == '-')
zoff = -zoff;
- /*
- * Reject out-of-range zone offsets -- sscanf/RFC 9051 don't bound
- * them, and an extreme offset can produce a maildir basename unsafe
- * for shell globs.
- */
+ /* refuse a zone offset that makes the time negative */
if ((int64_t)t - zoff < 0)
return (-1);
return (0);
}
-/* Result struct for parse_append_args(), avoids many out-parameters. */
struct append_parsed {
char mailbox[MBOX_NAME_MAX];
uint32_t sysflags;
int litnonsync;
};
-/* RFC 9051 SS6.3.12 append grammar; reuses parse_store_flags() for flags. */
+/* RFC 9051 SS6.3.12 APPEND */
int
parse_append_args(char *args, struct append_parsed *out, const char **errmsg)
{
return (-1);
}
- /* one parser for every mailbox argument; see mailbox_cmd.c */
if (parse_mailbox_name(&p, out->mailbox, sizeof(out->mailbox),
errmsg) == -1)
return (-1);
memcpy(digitsbuf, start, digits_len);
digitsbuf[digits_len] = '\0';
- /*
- * RFC 9051 SS9's number64 is unsigned, but strtoull(3) accepts
- * a sign, so "{-1}" would arrive as ULLONG_MAX and get a
- * misleading NO [LIMIT] instead of BAD -- same digit guard
- * listener.c's literal pre-scan already applies.
- */
+ /* strtoull(3) accepts a sign */
if (digitsbuf[0] < '0' || digitsbuf[0] > '9') {
*errmsg = "malformed literal octet count";
return (-1);
out->litlen = (uint64_t)litlen;
if (out->litnonsync && out->litlen > 4096) {
- /*
- * RFC 9051 SS4.3: non-sync literals capped at 4096B,
- * BAD not NO.
- */
+ /* RFC 9051 SS4.3 caps a non-synchronizing literal */
*errmsg = "non-synchronizing literal exceeds RFC "
"9051 SS4.3's 4096-octet limit, use a "
"synchronizing literal instead";
return (0);
}
-/* Parses the literal announcement, starts the store's file, reads the rest. */
int
cmd_append(struct session *s, const char *tag, char *args)
{
if (parsed.litlen > listener_append_max) {
char text[96];
- /*
- * RFC 5530 LIMIT code. The text states the number, as the
- * example in RFC 9051 SS7.1 does, since it reaches the client.
- */
+ /* RFC 5530 LIMIT, stating the number as RFC 9051 SS7.1 does */
(void)snprintf(text, sizeof(text), "[LIMIT] message exceeds "
"this server's %llu octet limit",
(unsigned long long)listener_append_max);
}
if (s->store_iev == NULL) {
- /*
- * Same store_iev invariant as cmd_select()/cmd_fetch(); ST_AUTH
- * needs it.
- */
log_warnx("session %u: APPEND with no store channel wired",
s->id);
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
req.msglen = parsed.litlen;
s->append_prev_state = s->state;
- /* tag is IMAP_TAG_MAX-bounded by session_handle_line(); rechecked */
if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
sizeof(s->pending_tag)) {
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return (1);
}
- /*
- * The store opens the message's tmp/ file now and is sent the literal
- * in pieces as it arrives, rather than this process holding it whole.
- */
+ /* the literal goes to the store in pieces as it arrives */
if (imsg_compose(&s->store_iev->ibuf, IMSG_MBOX_APPEND, 0, 0, -1,
&req, sizeof(req)) == -1) {
log_warn("session %u: imsg_compose IMSG_MBOX_APPEND", s->id);
s->literal_remaining = parsed.litlen;
s->literal_pending = 1;
- /*
- * RFC 9051 SS4.3: "+" continuation is only for synchronizing
- * literals. Uses sizeof()-1, not a hand count -- a wrong
- * hand-counted length once wrote a stray NUL onto the wire, same
- * idiom as listener.c's session_write() calls.
- */
+ /* RFC 9051 SS4.3: "+" only for synchronizing literals */
if (!parsed.litnonsync) {
static const char cont[] = "+ Ready for literal data\r\n";
return (1);
}
-/* Literal and its CRLF are in: tells the store to commit the message. */
int
session_finish_append(struct session *s)
{
s->state = SESSION_APPENDING;
- /*
- * Same fail-soft shape as send_mbox_request(): without it, a
- * compose failure leaves s->state stuck at SESSION_APPENDING and
- * session_is_busy() blocks every further command.
- */
+ /* a failed compose must not leave the session busy */
if (imsg_compose(&s->store_iev->ibuf, IMSG_MBOX_APPEND_END, 0, 0, -1,
NULL, 0) == -1) {
log_warn("session %u: imsg_compose IMSG_MBOX_APPEND_END",
return (1);
}
-/* Terminal APPEND reply; restores s->state to s->append_prev_state. */
void
session_handle_mbox_appended(struct session *s,
const struct imsg_mbox_appended *res)
if (res->error != MBOX_OP_OK) {
if (res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX)
session_reply(s, s->pending_tag, "NO",
- /*
- * SS6.3.12: reports why, not a promise CREATE would
- * help
- */
+ /* RFC 9051 SS6.3.12 */
"[TRYCREATE] no such mailbox");
else if (res->error == MBOX_OP_ERR_BUSY)
session_reply(s, s->pending_tag, "NO",
return;
}
- /*
- * INBOX compared case-insensitively (SS5.1); other names
- * case-sensitively.
- */
+ /* RFC 9051 SS5.1: INBOX is case-insensitive */
if (mailbox_name_is_inbox(s->append_mailbox) &&
mailbox_name_is_inbox(s->selected_mailbox))
appended_to_selected = 1;
blob - 9e9fdeab180b7849b4578bc3b08810eb0bdc06be
blob + 15fc87a7f400101a1cce03f1c440bc849107d673
--- src/auth.c
+++ src/auth.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* auth.c: credential verification, AUTHENTICATE PLAIN vs flat cred file. */
-
#include <sys/types.h>
#include <sys/stat.h>
};
static struct imsgev iev_listener;
-/* fd 3, alive for the process's lifetime */
static struct imsgev iev_parent;
static char cred_file_basename[256];
static void auth_dispatch(int, short, void *);
static void auth_dispatch_parent(int, short, void *);
-/*
- * Refuses a second IMSG_AUTH_REQUEST for an already-resolved session_id,
- * defending against a compromised/buggy listener replaying a grant; tracked in
- * a fixed-size ring (not a TAILQ) since auth is never notified of session end,
- * and session_id is unique-per-daemon so an evicted entry is harmless.
- */
-/*
- * Caps bcrypt-costing auth attempts per connection (sshd's MaxAuthTries
- * default) so a client retrying without limit can't convert cheap packets into
- * unbounded server CPU; past the cap, requests are refused without calling
- * crypt_checkpass(3), closing the cost asymmetry (though not dropping the
- * connection).
- */
+/* refuse a replayed request for a resolved session_id */
+/* sshd's MaxAuthTries default */
#define AUTH_MAX_TRIES 6
static unsigned int auth_failures;
#define AUTH_RESOLVED_MAX 256
static uint32_t auth_resolved[AUTH_RESOLVED_MAX];
-static size_t auth_resolved_next; /* ring cursor */
-static int auth_resolved_full; /* 1 once the ring has wrapped once */
+static size_t auth_resolved_next;
+static int auth_resolved_full;
static int
auth_session_already_resolved(uint32_t sid)
char chrootdir[1024];
ssize_t n;
- /*
- * fd-passing allowed here for IMSG_SETUP_PEER below; see
- * imsgev_ibuf_init()
- */
imsgev_ibuf_init(&ibuf3, 3);
- /*
- * IMSG_AUTH_INIT read first: cred_file needed before chroot() is
- * computed
- */
for (;;) {
if ((n = imsgbuf_get(&ibuf3, &imsg)) == -1)
fatal("imsgbuf_get");
imsg_get_type(&imsg));
if (imsg_get_data(&imsg, &init, sizeof(init)) == -1)
fatalx("auth: bad IMSG_AUTH_INIT payload");
- /*
- * imsg_get_data() guarantees size, not NUL termination -- force it,
- * since strlcpy(3) would otherwise read unboundedly past this stack
- * struct while computing the chroot(2) target.
- */
+ /* imsg_get_data() does not NUL-terminate */
init.cred_file[sizeof(init.cred_file) - 1] = '\0';
imsg_free(&imsg);
- /* auth's own daemon-user identity; distinct from listener's _imapd. */
if ((pw = getpwnam("_imapauth")) == NULL)
fatalx("getpwnam _imapauth: no such user "
"(expected, not yet provisioned by an install script)");
- /*
- * chroot into dir holding cred file, not itself; basename kept for
- * unveil()
- */
if (strlcpy(chrootdir, init.cred_file, sizeof(chrootdir)) >=
sizeof(chrootdir))
fatalx("cred_file too long: %s", init.cred_file);
- /*
- * Actually verifies what the fatalx() below claims: strrchr() finding
- * '/' only proves a directory component exists, not an absolute path
- * (e.g. "etc/creds" would otherwise chroot(2) relative to cwd), and
- * parse.y doesn't enforce this upstream.
- */
if (init.cred_file[0] != '/')
fatalx("cred_file must be an absolute path: %s",
init.cred_file);
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
fatal("cannot drop privileges to _imapauth");
- /* no session id yet; retitled on the first request below */
/* the imsg id cannot carry one: listener.c reads id 0 as "auth peer" */
setproctitle("auth");
- /*
- * Wires auth-worker's one and only peer via parent.c's
- * spawn_connection()/setup_peer_send(), with no IMSG_SETUP_DONE ack
- * needed since this boot sequence already reads a fixed,
- * statically-known message set before touching the event loop.
- */
peer_fd = setup_recv_one_peer(&ibuf3);
event_init();
imsgev_init_from_ibuf(&iev_parent, &ibuf3, auth_dispatch_parent, NULL);
- /* unveil() path is relative to the chroot above: "/" + basename. */
{
char unveil_path[512];
fatal("unveil lock");
}
- /*
- * No recvfd, no sendfd: this process gets its one peer fd via
- * setup_recv_one_peer() before this line and never attaches a
- * descriptor to an imsg itself (only parent.c does); a plain imsg with
- * fd == -1 needs neither pledge promise, and a wrong guess here is an
- * uncatchable SIGABRT, not silent breakage.
- */
+ /* no recvfd or sendfd: the one peer fd came before this */
#ifdef __OpenBSD__
if (pledge("stdio rpath", NULL) == -1)
fatal("pledge");
fatalx("auth: exited event loop");
}
-/* EV_WRITE must be handled: imsg_compose() queues, imsgbuf_write() sends */
static void
auth_dispatch(int fd, short event, void *arg)
{
if ((n = imsgbuf_read(&iev->ibuf)) == -1)
fatal("imsgbuf_read");
if (n == 0) {
- /*
- * This auth-worker was spawned to serve exactly one
- * connection and will never serve another, so it exits
- * here on listener EOF rather than idling in
- * event_dispatch() forever -- the ordinary, expected
- * end of a session per parent.c's reap_child().
- */
log_debug("auth-worker: listener closed channel, "
"exiting");
exit(0);
log_warnx("bad IMSG_AUTH_REQUEST");
break;
}
- /*
- * imsg_get_data() guarantees size, not NUL termination,
- * force it
- */
+ /* imsg_get_data() does not NUL-terminate */
req.username[sizeof(req.username) - 1] = '\0';
req.password[sizeof(req.password) - 1] = '\0';
cred.session_id = res.session_id;
cred.uid = res.uid;
cred.gid = res.gid;
- /*
- * cred.maildir and res.maildir are both sized
- * AUTH_MAILDIR_MAX, so truncation is
- * structurally impossible and the strlcpy()
- * return value is discarded deliberately.
- */
(void)strlcpy(cred.maildir, res.maildir,
sizeof(cred.maildir));
if (imsg_compose(&iev_parent.ibuf,
(void)fd;
}
-/* parent never sends auth anything post-boot; flushes CRED writes, sees EOF */
static void
auth_dispatch_parent(int fd, short event, void *arg)
{
if (n == 0)
break;
+ if (imsg_get_type(&imsg) == IMSG_AUTH_EXIT) {
+ imsg_free(&imsg);
+ /* the listener must have its answer before we go */
+ if (imsgbuf_flush(&iev_listener.ibuf) == -1)
+ log_warn("imsgbuf_flush IMSG_AUTH_RESULT");
+ log_debug("auth-worker: login granted, exiting");
+ exit(0);
+ }
log_debug("auth_dispatch_parent: unhandled %d",
imsg_get_type(&imsg));
imsg_free(&imsg);
(void)fd;
}
-/*
- * Usernames come off the network and reach syslog only through this: anything
- * outside printable ASCII becomes '?', since auth can't assume listener.c's
- * CR/LF rejection held.
- */
+/* network usernames reach syslog only through here */
static void
auth_safe_name(const char *in, char *out, size_t outsize)
{
out[i] = '\0';
}
-/* calls crypt_checkpass() with hash NULL on unknown user, avoids timing leak */
+/* unknown users still pay for crypt_checkpass(), against timing */
static void
auth_verify(struct imsg_auth_request *req, struct imsg_auth_result *res)
{
int found;
char safename[AUTH_USERNAME_MAX];
- /*
- * Budget spent: refuses before cred_lookup() so a client past
- * AUTH_MAX_TRIES can't even trigger a re-read/re-scan of the credential
- * file; reported identically to any other failure.
- */
if (auth_failures >= AUTH_MAX_TRIES) {
char overname[AUTH_USERNAME_MAX];
auth_failures++;
}
- /*
- * Logs every authentication outcome (previously nothing did, leaving
- * password-guessing runs untraceable for fail2ban-style tooling);
- * log_info() is always emitted, and the failure line deliberately
- * doesn't distinguish "no such user" from "wrong password" to avoid an
- * enumeration oracle.
- */
+ /* every outcome is logged, for fail2ban-style tools */
auth_safe_name(req->username, safename, sizeof(safename));
if (res->ok)
log_info("session %u: authentication succeeded for \"%s\" "
explicit_bzero(&ce, sizeof(ce));
}
-/*
- * True if a privileged file at st is safe to trust here: owned by root or the
- * current (post-chroot, post-setresuid) uid, and not group-writable,
- * group-executable, or accessible to world at all; same policy parse.y's
- * check_file_secrecy() applies to imapd.conf (parse.y:678-695), kept as its own
- * function since that one runs pre-privsep against an fd the parent still owns
- * and logs a different message.
- */
static int
cred_file_secure(const struct stat *st)
{
return (1);
}
-/* linear scan of "username:passwordhash:uid:gid:maildir" lines */
static int
cred_lookup(const char *path, const char *username, struct cred_entry *out)
{
return (-1);
}
- /*
- * Rejects a credentials file not owned by root or the current uid, or
- * that is group-writable, group-executable, or accessible to world at
- * all (parent.c already enforces an equivalent policy for the TLS key,
- * and parse.y's check_file_secrecy() for imapd.conf itself; this file
- * holding every bcrypt hash had no such check until this one);
- * group-read stays permissive so the documented "root:_imapauth 0640"
- * layout keeps working, and a bad mode or owner fails closed.
- */
{
struct stat st;
char *ep;
unsigned long ulval;
- /*
- * fgets(3) silently splits an over-long line, which would
- * otherwise parse the tail as a phantom credential entry --
- * refuses to read the whole file rather than guess.
- */
+ /* fgets(3) splits an over-long line */
if (strchr(line, '\n') == NULL &&
strlen(line) == sizeof(line) - 1) {
log_warnx("%s: over-long line, refusing to parse the "
if (strcmp(fields[0], username) != 0)
continue;
- /*
- * skip rather than truncate a field, same as other malformed
- * lines
- */
if (strlcpy(out->username, fields[0], sizeof(out->username))
>= sizeof(out->username) ||
strlcpy(out->passwordhash, fields[1],
sizeof(out->passwordhash)) >= sizeof(out->passwordhash))
continue;
- /*
- * Requires a bcrypt hash ("$2" prefix): crypt_checkpass(3)
- * treats an empty stored hash plus empty password as a
- * successful login rather than a disabled account, so a blank
- * field must be rejected here, and non-bcrypt values are
- * skipped the same way as every other malformed entry to avoid
- * an enumeration oracle -- use imapduser -d to disable an
- * account instead.
- */
+ /* crypt_checkpass(3) passes an empty hash and password */
if (fields[1][0] != '$' || fields[1][1] != '2')
continue;
- /*
- * strtoul(3) accepts a leading '-', so "-1" would parse as
- * 0xffffffff (and "0" is root); the credential file shouldn't
- * be able to express either uid/gid, so both are rejected here
- * before the wrap case slips through unnoticed.
- */
+ /* strtoul(3) accepts "-1", and uid 0 is root */
if (fields[2][0] < '0' || fields[2][0] > '9' ||
fields[3][0] < '0' || fields[3][0] > '9')
continue;
break;
}
- /*
- * line[] held the raw credential record (username, bcrypt hash, uid,
- * gid, maildir) for every entry scanned; auth_verify() and
- * auth_dispatch() scrub their own copies, so this buffer is the one
- * left behind.
- */
+ /* line[] held credential records */
explicit_bzero(line, sizeof(line));
fclose(fp);
return (found ? 0 : -1);
blob - f40fc88dc9f436f73ddc512da25a07fc77ded283
blob + d12b562475f766678ff91628fbb404cb544a5d9c
--- src/auth_cmd.c
+++ src/auth_cmd.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* auth_cmd.c: CAPABILITY/NOOP/LOGOUT/ID/LOGIN/AUTH/STARTTLS/ENABLE handlers. */
#include <sys/types.h>
#include <sys/queue.h>
int
cmd_capability(struct session *s, const char *tag, char *args)
{
- /* RFC 9051: "Arguments: none", extra args ignored, not rejected */
(void)args;
session_untagged(s, s->tls_active ?
int
cmd_id(struct session *s, const char *tag, char *args)
{
- /* RFC 2971 SS3.1: field/value list logged, not parsed; replies NIL */
+ /* RFC 2971 SS3.1: logged, not parsed; the reply is NIL */
log_debug("session %u: ID params: %s", s->id,
args != NULL ? args : "(none)");
session_untagged(s, "ID NIL");
}
-/* LOGIN permanently disabled, matching LOGINDISABLED in CAPABILITY strings. */
int
cmd_login(struct session *s, const char *tag, char *args)
{
cmd_starttls(struct session *s, const char *tag, char *args)
{
if (args != NULL) {
- /* RFC 9051 SS6.2.1 Result: "BAD - ... arguments invalid". */
session_reply(s, tag, "BAD", "STARTTLS takes no arguments");
return (1);
}
return (1);
}
if (listener_tls_ctx == NULL) {
- /*
- * RFC 9051 SS6.2.1 NO + RFC 5530 UNAVAILABLE: cert/key load
- * failed at boot
- */
+ /* RFC 5530 UNAVAILABLE: no certificate or key at boot */
session_reply(s, tag, "NO",
"[UNAVAILABLE] TLS negotiation unavailable");
return (1);
/* precedes TLS */
session_reply(s, tag, "OK", "Begin TLS negotiation now");
- /* command-injection mitigation: discard buffered plaintext */
+ /* discard buffered plaintext, against command injection */
s->inbuflen = 0;
session_tls_start(s);
/* RFC 4616 SS2: authzid/authcid/passwd up to 255 octets + 2 NULs = 767 */
#define SASL_PLAIN_MAX 768
-/* Stores the username for the close line; mirrors auth.c's auth_safe_name() */
-/* non-printable becomes '?', so s->user is safe wherever it is logged */
+/* as auth.c's auth_safe_name(): '?' for anything unprintable */
static void
session_set_user(struct session *s, const unsigned char *in,
size_t inlen)
s->user[i] = '\0';
}
-/* decodes+verifies one SASL PLAIN msg (RFC 4616 SS2); never tears down */
int
sasl_plain_finish(struct session *s, const char *tag, const char *b64,
int allow_empty_equals)
explicit_bzero(raw, sizeof(raw));
return (1);
}
- /*
- * too big for imsg_auth_request's fixed fields; generic NO avoids an
- * oracle
- */
+ /* a generic NO, so no oracle */
if (authcidlen >= AUTH_USERNAME_MAX || passwdlen >= AUTH_PASSWORD_MAX) {
session_reply(s, tag, "NO",
"[AUTHENTICATIONFAILED] authentication failed");
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return (1);
}
- /*
- * The auth-worker may not exist if its fork failed (parent.c), leaving
- * iev_auth.ibuf.fd at -1 -- fail gracefully rather than compose to an
- * unwired imsgev.
- */
if (iev_auth.ibuf.fd == -1) {
session_reply(s, tag, "NO", "[UNAVAILABLE] authentication "
"temporarily unavailable");
return (1);
}
-/* reply to "+ " continuation after "AUTHENTICATE PLAIN" (cmd_authenticate) */
int
session_handle_auth_continuation(struct session *s, const char *line)
{
- /* next line back to ordinary tagged command either way */
s->auth_cont = 0;
/* RFC 9051 SS6.2.2: lone "*" cancels exchange */
return sasl_plain_finish(s, s->pending_tag, line, 0);
}
-/* reply to our "+ idling" continuation (SS6.3.13); only "DONE" ends IDLE */
int
session_handle_idle_continuation(struct session *s, const char *line)
{
const char *initial;
if (args == NULL) {
- /* RFC 9051 SS6.2.2 Result: "BAD - ... arguments invalid". */
session_reply(s, tag, "BAD", "Missing SASL mechanism name");
return (1);
}
return (1);
}
- /* only implements PLAIN, matching CAPABILITY_POST_TLS */
if (strcasecmp(mech, "PLAIN") != 0) {
session_reply(s, tag, "NO",
"authentication mechanism not available");
return (1);
}
- /* RFC 9051 SS6.2.2 initial-resp: one round trip */
if (initial != NULL) {
- /*
- * `initial` is base64 cleartext password into s->inbuf; reader
- * scrubs it
- */
s->scrub_inbuf = 1;
return sasl_plain_finish(s, tag, initial, 1);
}
- /* no initial response: send "+"; auth_cont routes the reply line */
if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
sizeof(s->pending_tag)) {
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return (1);
}
-/* reply for a command store.c can't run yet (no payload); NO not BAD */
int
stub_not_implemented(struct session *s, const char *tag, const char *cmdname)
{
return (1);
}
-/* RFC 9051 SS6.3.1 ENABLE: unknown exts ignored; ENABLED lists only new ones */
+/* RFC 9051 SS6.3.1 ENABLE */
int
cmd_enable(struct session *s, const char *tag, char *args)
{
if (!s->condstore_enabled)
newly_condstore = 1;
}
- /* anything else: unadvertised extension, SS6.3.1 says ignore */
}
if (newly_condstore || newly_qresync)
blob - 37d8cbb635a00549cdc76e86d82a9256c9d165a3
blob + 724a37ec1dd67f91057de94d728c98a17534e5f5
--- 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. */
#include <sys/types.h>
#include <sys/file.h>
return (envbuf_append(buf, bufsize, outlen, s, strlen(s)));
}
-/* Appends one RFC 9051 nstring: NIL if val NULL, else quoted+escaped. */
+/* RFC 9051 nstring */
int
envbuf_append_nstring(char *buf, size_t bufsize, size_t *outlen,
const char *val, size_t vallen)
for (i = 0; i < vallen; i++) {
char c = val[i];
- /*
- * RFC 9051 SS4.3 quoted strings exclude NUL/CR/LF; substitute
- * rather than reject so one bad byte doesn't drop the field.
- */
+ /* RFC 9051 SS4.3: substituted, not dropped */
if (c == '\0' || c == '\r' || c == '\n')
c = ' ';
if ((c == '"' || c == '\\') &&
return (0);
fail:
- /*
- * All-or-nothing: a partial append leaves an unterminated quoted
- * string.
- */
+ /* all or nothing */
*outlen = save;
return (-1);
}
-/* Formats an RFC 5322 mailbox as an address tuple (RFC 9051 SS9); no groups. */
int
-envbuf_append_one_address(char *buf, size_t bufsize, size_t *outlen,
- const char *tok, size_t toklen)
+address_split(const char *tok, size_t toklen, const char **name_out,
+ size_t *namelen_out, const char **mailbox_out, size_t *mailboxlen_out,
+ const char **host_out, size_t *hostlen_out)
{
- char addrbuf[1024];
- size_t addrlen = 0;
const char *name = NULL;
size_t namelen = 0;
const char *spec;
if (displen >= 2 && disp[0] == '"' &&
disp[displen - 1] == '"') {
- /*
- * emission loop below re-escapes for the wire;
- * no unescape pass needed
- */
disp++;
displen -= 2;
}
host = spec + at + 1;
hostlen = speclen - at - 1;
- /*
- * strip quotes from a quoted local-part (unescaped "@" inside not
- * handled)
- */
+ /* unquote a quoted local-part */
if (mailboxlen >= 2 && mailbox[0] == '"' &&
mailbox[mailboxlen - 1] == '"') {
mailbox++;
mailboxlen -= 2;
}
+ *name_out = name;
+ *namelen_out = namelen;
+ *mailbox_out = mailbox;
+ *mailboxlen_out = mailboxlen;
+ *host_out = host;
+ *hostlen_out = hostlen;
+ return (0);
+}
+
+/* RFC 9051 SS9 address; no groups */
+int
+envbuf_append_one_address(char *buf, size_t bufsize, size_t *outlen,
+ const char *tok, size_t toklen)
+{
+ char addrbuf[1024];
+ size_t addrlen = 0;
+ const char *name, *mailbox, *host;
+ size_t namelen, mailboxlen, hostlen;
+
+ if (address_split(tok, toklen, &name, &namelen, &mailbox, &mailboxlen,
+ &host, &hostlen) == -1)
+ return (-1);
+
if (envbuf_append(addrbuf, sizeof(addrbuf), &addrlen, "(", 1) == -1)
return (-1);
j++;
c = name[j];
}
+ /* substituted as in envbuf_append_nstring() */
+ if (c == '\0' || c == '\r' || c == '\n')
+ c = ' ';
if ((c == '"' || c == '\\') &&
envbuf_append(addrbuf, sizeof(addrbuf), &addrlen,
"\\", 1) == -1)
if (envbuf_append(addrbuf, sizeof(addrbuf), &addrlen, ")", 1) == -1)
return (-1);
- /*
- * One atomic append: the whole "(...)" tuple lands or none of it
- * does, since envbuf_append() leaves *outlen untouched on failure.
- */
return (envbuf_append(buf, bufsize, outlen, addrbuf, addrlen));
}
-/*
- * Formats an RFC 5322 address-list as "(" 1*address ")", or NIL if none
- * parse (RFC 9051 SS7.5.2); splits on top-level commas only.
- */
int
-envbuf_append_address_list(char *buf, size_t bufsize, size_t *outlen,
- const char *val, size_t vallen)
+address_list_next(const char *val, size_t vallen, size_t *pos,
+ const char **tok_out, size_t *toklen_out)
{
- size_t save = *outlen;
- size_t i = 0;
- int any = 0;
+ size_t i = *pos;
- while (vallen > 0 && (val[0] == ' ' || val[0] == '\t')) {
- val++;
- vallen--;
- }
- while (vallen > 0 && (val[vallen - 1] == ' ' ||
- val[vallen - 1] == '\t'))
- vallen--;
-
- if (vallen == 0)
- return (envbuf_append_str(buf, bufsize, outlen, "NIL"));
-
- if (envbuf_append(buf, bufsize, outlen, "(", 1) == -1)
- return (-1);
-
while (i < vallen) {
size_t tok_start;
size_t tok_len;
tok_len--;
if (tok_len > 0) {
- /*
- * Safe to skip a malformed or non-fitting address and
- * continue: envbuf_append_one_address() builds the
- * tuple locally before one atomic append, leaving
- * buf/outlen untouched on failure.
- */
- if (envbuf_append_one_address(buf, bufsize, outlen,
- val + tok_start, tok_len) == 0)
- any = 1;
+ *tok_out = val + tok_start;
+ *toklen_out = tok_len;
+ *pos = i;
+ return (1);
}
}
+ *pos = i;
+ return (0);
+}
+/* RFC 9051 SS7.5.2 address list, or NIL */
+int
+envbuf_append_address_list(char *buf, size_t bufsize, size_t *outlen,
+ const char *val, size_t vallen)
+{
+ const char *tok;
+ size_t save = *outlen;
+ size_t i = 0, toklen;
+ int any = 0;
+
+ while (vallen > 0 && (val[0] == ' ' || val[0] == '\t')) {
+ val++;
+ vallen--;
+ }
+ while (vallen > 0 && (val[vallen - 1] == ' ' ||
+ val[vallen - 1] == '\t'))
+ vallen--;
+
+ if (vallen == 0)
+ return (envbuf_append_str(buf, bufsize, outlen, "NIL"));
+
+ if (envbuf_append(buf, bufsize, outlen, "(", 1) == -1)
+ return (-1);
+
+ while (address_list_next(val, vallen, &i, &tok, &toklen) == 1) {
+ if (envbuf_append_one_address(buf, bufsize, outlen, tok,
+ toklen) == 0)
+ any = 1;
+ }
+
if (!any) {
*outlen = save;
return (envbuf_append_str(buf, bufsize, outlen, "NIL"));
return (envbuf_append(buf, bufsize, outlen, ")", 1));
}
-/* Looks up header `name`, appends nstring (NIL if absent); for ENVELOPE. */
int
append_field_nstring(char *out, size_t outsize, size_t *outlen,
const char *hdrbuf, uint32_t hdrlen, const char *name)
return (rc);
}
-/*
- * Builds RFC 9051 SS7.5.2 ENVELOPE list; Sender/Reply-To default to From
- * if absent/empty; -1 if unreadable or over ENVELOPE_MAX.
- */
+/* RFC 9051 SS7.5.2 ENVELOPE; Sender and Reply-To default to From */
int
-build_envelope(int dfd, const char *basename, char **buf_out,
+build_envelope(int fd, const char *basename, char **buf_out,
uint32_t *len_out)
{
char *hdrbuf = NULL;
*buf_out = NULL;
*len_out = 0;
- if (read_message_header(dfd, basename, &hdrbuf, &hdrlen) == -1)
+ if (read_header_from_fd(fd, basename, &hdrbuf, &hdrlen) == -1)
return (-1);
if (envbuf_append(out, sizeof(out), &outlen, "(", 1) == -1)
return (-1);
}
-/*
- * BODYSTRUCTURE (RFC 9051 SS7.5.2): recursive RFC 2045/2046 MIME parse,
- * bounded by MIME_MAX_DEPTH/MIME_MAX_PARTS; no extension data,
- * message/rfc822, or RFC 2231 continuations.
- */
+/* RFC 9051 SS7.5.2 BODYSTRUCTURE; no extension data */
int
build_body_structure(int depth, int *nparts_used, const char *hdr,
size_t hdrlen, const char *body, size_t bodylen, char *out,
size_t plen = part_ends[i] - part_starts[i];
size_t phdrend;
- /*
- * zero-length body-part is spec-legal (RFC 2046
- * SS5.1.1); treat as 0/0
- */
+ /* RFC 2046 SS5.1.1: an empty part is legal */
if (plen == 0)
phdrend = 0;
else if (find_header_body_split(pbuf, plen,
if (strcasecmp(type, "MESSAGE") == 0 &&
(strcasecmp(subtype, "RFC822") == 0 ||
strcasecmp(subtype, "GLOBAL") == 0))
- /* scoped out, see BODYSTRUCTURE comment above */
+ /* MESSAGE/RFC822 and MESSAGE/GLOBAL are not supported */
return (-1);
{
}
}
-/*
- * Top-level entry: reads message once (capped at bodystructure_read_max),
- * finds header/body split, walks from depth 0; -1 on any failure.
- */
int
-build_bodystructure(int dfd, const char *basename, char **buf_out,
+build_bodystructure(int fd, const char *basename, char **buf_out,
uint32_t *len_out)
{
char *wholebuf = NULL;
*buf_out = NULL;
*len_out = 0;
- if (read_message_body(dfd, basename, 0, bodystructure_read_max,
+ if (read_body_from_fd(fd, basename, 0, bodystructure_read_max,
"BODYSTRUCTURE", &wholebuf, &wholelen) == -1)
return (-1);
if (wholelen == 0 ||
blob - b3a8877617850b49d567d824e44dfc14c5f6f6ca
blob + 024c85828cc7b50984c280f0be02c9abad1a68af
--- src/fetch_cmd.c
+++ src/fetch_cmd.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* FETCH: attribute/section-spec parsing and response building. */
-
#include <sys/types.h>
#include <sys/queue.h>
#include <sys/socket.h>
return (0);
}
-/*
- * Parses one range token (single optional colon, no comma) into r; factored out
- * of the old parse_seq_range() so parse_sequence_set() can reuse it per
- * comma-separated segment.
- */
static int
parse_one_seq_range(const char *tok, struct seq_range *r)
{
return (0);
}
-/*
- * Parses an RFC 9051 SS9 sequence-set (comma-separated seq-number/seq-range) by
- * splitting on top-level commas and parsing each with parse_one_seq_range(),
- * writing up to SEQSET_MAX_RANGES entries to ranges[] and the count to
- * *nranges, or returning -1 with *errmsg set on a malformed, empty, or excess
- * segment.
- */
+/* RFC 9051 SS9 sequence-set */
int
parse_sequence_set(const char *text, struct seq_range ranges[SEQSET_MAX_RANGES],
uint32_t *nranges, const char **errmsg)
return (0);
}
-/* like strtok_r(); space isn't a delim inside an unclosed '[' or '(' (SS9) */
+/* a space inside an unclosed "[" or "(" is not a delimiter */
static char *
fetch_att_tok(char *str, char **savep)
{
return (start);
}
-/* parses HEADER.FIELDS[.NOT] body (SS9); -1 is BAD, not a silent drop */
+/* RFC 9051 SS9 HEADER.FIELDS[.NOT]; -1 is BAD */
int
parse_header_fields_att(const char *inner, int *not_out, char *fields_out,
size_t fields_outsize)
return (0);
}
-/* SS6.4.5.1 section-part check; string still reaches parse_section_part() */
+/* RFC 9051 SS6.4.5.1 section-part */
int
section_part_valid(const char *s)
{
return (1);
}
-/* SS6.4.5 "<start.count>"; count 0 handled by partial_range() */
+/* RFC 9051 SS6.4.5 <start.count> */
int
parse_partial_suffix(const char *s, int *has_partial_out,
uint32_t *start_out, uint32_t *count_out)
return (0);
}
-/*
- * generic BODY.PEEK[...] tok (already known not to be HEADER.FIELDS): [],
- * [TEXT], or [<section-part>], optional <<start.count>> (SS6.4.5); updates
- * *attrs_inout/section_part_out/partial-range out-params. Returns 1 on success,
- * 0 if silently degraded (*degraded_out set, same lenient skip as other
- * unsupported forms), -1 on a hard parse error (*errmsg set).
- */
static int
parse_body_peek_section_tok(const char *tok, uint32_t *attrs_inout,
char *section_part_out, size_t section_part_outsize,
return (1);
}
-/*
- * BODY.PEEK[HEADER.FIELDS...] tok: extracts the bracket body, dedupes a second
- * HEADER.FIELDS item, delegates to parse_header_fields_att(). Returns 1 on
- * success (*attrs_inout and the header_fields_*_out params updated), 0 if this
- * token should be silently ignored (a duplicate), -1 on a hard parse error
- * (*errmsg set).
- */
static int
parse_body_peek_header_fields_tok(const char *tok, uint32_t *attrs_inout,
int *header_fields_not_out, char *header_fields_out,
return (1);
}
-/* SS6.4.5: ALL/FULL/FAST; unsupported skipped (*degraded_out=1), else -2/NO */
+/* RFC 9051 SS6.4.5 fetch-att; unsupported items are skipped */
int
parse_fetch_atts(char *spec, uint32_t *attrs_out, int *degraded_out,
int *header_fields_not_out, char *header_fields_out,
for (tok = fetch_att_tok(p, &save); tok != NULL;
tok = fetch_att_tok(NULL, &save)) {
if (strcasecmp(tok, "FAST") == 0) {
- /* SS6.4.5 macro: FLAGS INTERNALDATE RFC822.SIZE. */
+ /* RFC 9051 SS6.4.5 FAST macro */
attrs |= MBOX_FETCH_FLAGS | MBOX_FETCH_INTERNALDATE |
MBOX_FETCH_RFC822_SIZE;
} else if (strcasecmp(tok, "ALL") == 0) {
- /* SS6.4.5 macro: FAST + ENVELOPE. */
+ /* FAST + ENVELOPE */
attrs |= MBOX_FETCH_FLAGS | MBOX_FETCH_INTERNALDATE |
MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_ENVELOPE;
} else if (strcasecmp(tok, "FULL") == 0) {
- /*
- * SS6.4.5 macro: ALL + bare BODY
- * (bodystructure_full_out stays 0)
- */
+ /* ALL + bare BODY */
attrs |= MBOX_FETCH_FLAGS | MBOX_FETCH_INTERNALDATE |
MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_ENVELOPE |
MBOX_FETCH_BODYSTRUCTURE;
} else if (strcasecmp(tok, "RFC822.SIZE") == 0) {
attrs |= MBOX_FETCH_RFC822_SIZE;
} else if (strcasecmp(tok, "MODSEQ") == 0) {
- /* SS3.1.4.2, CONDSTORE; cmd_fetch() checks */
+ /* RFC 7162 SS3.1.4.2 */
attrs |= MBOX_FETCH_MODSEQ;
} else if (strcasecmp(tok, "BODY.PEEK[HEADER]") == 0) {
/* exact BODY[...]; \Seen unimplemented */
attrs |= MBOX_FETCH_ENVELOPE;
} else if (strcasecmp(tok, "BODY") == 0 ||
strcasecmp(tok, "BODYSTRUCTURE") == 0) {
- /*
- * both produce identical output, exact-matched ahead of
- * "BODY" catch-all
- */
attrs |= MBOX_FETCH_BODYSTRUCTURE;
*bodystructure_full_out =
(strcasecmp(tok, "BODYSTRUCTURE") == 0);
strcasecmp(tok, "RFC822") == 0 ||
strcasecmp(tok, "RFC822.HEADER") == 0 ||
strcasecmp(tok, "RFC822.TEXT") == 0) {
- /*
- * MIME part BODY[...] and RFC822(.HEADER/.TEXT)
- * shorthands unimplemented
- */
+ /* other BODY forms and RFC822 items: unimplemented */
degraded = 1;
} else {
*errmsg = "unknown message data item";
}
if (attrs == 0) {
- /*
- * every item unsupported, e.g. BODY[<part>] or
- * RFC822(.HEADER/.TEXT) alone
- */
*errmsg = "cannot fetch that message content yet, "
"supported: FLAGS/UID/INTERNALDATE/RFC822.SIZE/MODSEQ/"
"ENVELOPE/(BODY|BODYSTRUCTURE)/BODY.PEEK[...]";
return (-2);
}
- /*
- * both HEADER/HEADER.FIELDS requested (legal, SS6.4.5): HEADER wins
- * here
- */
+ /* RFC 9051 SS6.4.5: HEADER wins over HEADER.FIELDS */
if ((attrs & MBOX_FETCH_BODY_HEADER) &&
(attrs & MBOX_FETCH_HEADER_FIELDS))
attrs &= ~MBOX_FETCH_HEADER_FIELDS;
*attrs_out = attrs;
*degraded_out = degraded;
- /*
- * accumulated in attrs, copied out so HEADER-wins is the one adjust
- * point
- */
*has_partial_out = has_partial;
*partial_start_out = partial_start;
*partial_count_out = partial_count;
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
-/* SS9 date-time; always UTC "+0000": ts has no tz, chroot child lacks tzdata */
+/* RFC 9051 SS9 date-time, always "+0000": no tzdata in the chroot */
void
format_internaldate(int64_t ts, char *out, size_t outsize)
{
tm.tm_hour, tm.tm_min, tm.tm_sec);
}
-/* growing-buf helper; clamps *len, truncation can't underflow bufsize */
static void
fetch_append(char *buf, size_t bufsize, size_t *len, const char *fmt, ...)
{
*len = bufsize;
}
-/*
- * Writes len octets of fd, starting at off, to the client. The literal's
- * length is already on the wire, so a short read cannot be repaired and
- * the connection is shut down instead of desynchronized.
- */
+/* a short read cannot be repaired: the length is on the wire */
static void
session_write_file_range(struct session *s, int fd, uint64_t off,
uint64_t len)
}
}
-/* sends untagged FETCH response (SS7.5.2); literals flush buf, write raw */
+/* RFC 9051 SS7.5.2 */
void
session_send_fetch_response(struct session *s,
struct imsg_mbox_fetch_meta *meta)
need_sp = 1;
}
if (have_body) {
- /*
- * SS6.4.5: echo origin octet only if client sent one,
- * not store.c's count
- */
+ /* echo the origin only if the client sent one */
len = 0;
if (s->pending_body_has_partial)
fetch_append(buf, sizeof(buf), &len,
session_untagged(s, buf);
}
- /*
- * Reset pending_*_found when have_* is false, so it can't leak to
- * the next reply. Requested but not found also means the store
- * could not produce the item, which RFC 9051 SS6.4.5 answers with a
- * tagged NO once the command finishes.
- */
if (have_header) {
free(s->pending_header_buf);
s->pending_header_buf = NULL;
}
}
-/* STORE's FETCH (SS6.4.6) shows FLAGS; MODSEQ if CONDSTORE-aware (SS3.1.3) */
+/* RFC 9051 SS6.4.6 STORE echo */
void
session_send_store_fetch_response(struct session *s,
const struct imsg_mbox_fetch_meta *meta)
char buf[MBOX_FLAGS_MAX + 96];
size_t len;
- /*
- * RFC 9051 SS6.4.9: a UID STORE's echo must include UID, right after
- * FLAGS
- */
+ /* RFC 9051 SS6.4.9: UID STORE echoes UID after FLAGS */
len = (size_t)snprintf(buf, sizeof(buf), "%u FETCH (FLAGS (%s)",
meta->seqno, meta->flags);
if (s->cmd_by_uid && len < sizeof(buf))
session_untagged(s, buf);
}
-/* splits trailing RFC4466 modifiers off spec; NUL-terminates spec in place */
+/* RFC 4466 modifiers */
char *
split_trailing_modifiers(char *spec)
{
break;
}
} else if (*p == '\0')
- /*
- * unterminated; caller's parser produces the
- * BAD for this
- */
return (NULL);
p++;
}
return (p);
}
-/*
- * FETCH's trailing fetch-modifier list (RFC 4466 + RFC 7162 SS3.1.4.1/SS3.2.6);
- * *want_vanished lets fetch_dispatch() pair-check later.
- */
+/* RFC 4466, RFC 7162 SS3.1.4.1 and SS3.2.6 modifiers */
int
parse_fetch_modifiers(char *modspec, struct imsg_mbox_fetch *req,
const struct session *s, int by_uid, int *want_vanished,
"mod-sequence value";
return (-1);
}
- /*
- * RFC 7162 SS7 mod-sequence-values are unsigned only,
- * but strtoull(3) accepts a leading sign, so a guard
- * rejects non-digit-leading input to stop "-1" silently
- * becoming ULLONG_MAX (same check as
- * auth.c/index.c/listener.c's literal parser).
- */
+ /* strtoull(3) accepts a sign */
if (*valtok < '0' || *valtok > '9') {
*errmsg = "invalid CHANGEDSINCE mod-sequence";
return (-1);
req->attrs |= MBOX_FETCH_MODSEQ;
} else if (strcasecmp(tok, "VANISHED") == 0) {
if (!by_uid) {
- /*
- * RFC 7162 SS3.2.6: VANISHED with plain FETCH
- * MUST return tagged BAD
- */
+ /* RFC 7162 SS3.2.6: BAD */
*errmsg = "VANISHED is only valid as a UID "
"FETCH modifier (RFC 7162 SS3.2.6)";
return (-1);
return (0);
}
-/* SS6.4.5 fetch+RFC4466/7162 modifiers; BODY[...]/BODY[<part>] a scope cut */
int
cmd_fetch(struct session *s, const char *tag, char *args)
{
return fetch_dispatch(s, tag, args, 0);
}
-/* shared cmd_fetch/cmd_uid FETCH (uid 0/1); forces MBOX_FETCH_UID (SS6.4.9) */
+/* RFC 9051 SS6.4.9: UID FETCH forces UID */
int
fetch_dispatch(struct session *s, const char *tag, char *args, int by_uid)
{
req.by_uid = by_uid;
req.header_fields_not = header_fields_not;
- /* bounds-checked above; applies per WHOLE/TEXT/PART winner */
if (strlcpy(req.header_fields, header_fields,
sizeof(req.header_fields)) >= sizeof(req.header_fields) ||
strlcpy(req.section_part, section_part, sizeof(req.section_part))
req.partial_start = partial_start;
req.partial_count = partial_count;
- /* client label never reaches store.c; stashed to echo in the reply */
if (attrs & MBOX_FETCH_BODY_HEADER) {
if (strlcpy(s->pending_header_label, "HEADER",
sizeof(s->pending_header_label)) >=
}
}
- /*
- * same, for BODY.PEEK[]/[TEXT]/[<part>]; matches handle_mbox_fetch()
- * order
- */
if (attrs & MBOX_FETCH_BODY_WHOLE) {
s->pending_body_label[0] = '\0';
} else if (attrs & MBOX_FETCH_BODY_TEXT) {
s->pending_body_has_partial = has_partial;
s->pending_body_partial_origin = partial_start;
- /*
- * same, BODYSTRUCTURE: echoes "BODY"/"BODYSTRUCTURE" bare token client
- * used
- */
if (attrs & MBOX_FETCH_BODYSTRUCTURE) {
if (strlcpy(s->pending_bodystructure_label,
bodystructure_full ? "BODYSTRUCTURE" : "BODY",
}
if (want_vanished && !req.has_changedsince) {
- /*
- * RFC 7162 SS3.2.6: VANISHED MUST pair with CHANGEDSINCE, else
- * tagged BAD
- */
+ /* RFC 7162 SS3.2.6: VANISHED needs CHANGEDSINCE */
session_reply(s, tag, "BAD",
"VANISHED requires CHANGEDSINCE also be specified "
"(RFC 7162 SS3.2.6)");
req.attrs |= MBOX_FETCH_UID;
if (s->store_iev == NULL) {
- /*
- * same invariant as cmd_select(): ST_SELECTED requires
- * store_iev wired
- */
log_warnx("session %u: %s with no store channel wired",
s->id, cmdname);
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
req.nranges = nranges;
- /*
- * RFC 7162 SS3.1: MODSEQ fetch-att and CHANGEDSINCE both
- * CONDSTORE-enabling
- */
+ /* RFC 7162 SS3.1: both enable CONDSTORE */
if (req.attrs & MBOX_FETCH_MODSEQ)
session_condstore_enable(s);
blob - f3d26fd552c675eab5d75707a86c008bcd83c659
blob + fe08b5b977abd9e830855e87f2de1c8f2f51072f
--- 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 18 2026 $
+.Dd $Mdocdate: September 25 2026 $
.Dt IMAPD 8
.Os
.Sh NAME
.Em auth
children over
.Xr imsg_init 3
-control channels; a
+control channels.
+One
.Em store
-child is forked per authenticated session, chroots into the mail
-spool, and drops privileges to that session's own user before ever
-touching mailbox data.
+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
.Ic spool ,
.Ic append max ,
.Ic attachment max ,
+.Ic account sessions ,
+.Ic connections max ,
.Ic idle poll ,
.Ic lock timeout ,
.Ic login grace ,
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
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 three processes, a listener-worker, an
-auth-worker and a search-oracle, and a connection that completes the TCP
-handshake and then sends nothing would otherwise hold all three
+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
reopens the denial of service described above.
Defaults to 60.
.It Ic lock timeout Ar seconds
-How long a command waits for another session of the same user to release a
-mailbox's index lock before giving up and answering
+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
-Two connections for one account are ordinary, and a command that changes a
-mailbox holds its index lock for the whole of the change.
-A client marking a large mailbox read can therefore hold the lock for the
-better part of a minute, and another of that user's clients waits behind it.
-This directive bounds that wait.
+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.
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 .
.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 imapduser 8 ,
.Xr smtpd 8 ,
+.Xr sysctl 8 ,
.Xr syslogd 8
.Sh STANDARDS
.Rs
blob - 5b86996154c9b3069bea23f9c2a7cf037c026690
blob + ac8f63423c2f281213f976240a94b8fa2e18ebd3
--- src/imapd.conf.example
+++ src/imapd.conf.example
#idle poll 5
# How long a connection may go without authenticating before imapd
-# closes it. Every accepted connection costs three processes (a
-# listener-worker, an auth-worker and a search-oracle), and a connection
+# closes it. Every accepted connection costs two processes (a
+# listener-worker and an auth-worker) until it logs in, and a connection
# that completes TCP and then says nothing would otherwise hold them for
# ever, so a few dozen silent connections can reach the "startups full"
# limit below and lock everyone else out. RFC 9051 section 5.4 permits
# disable the bound (an unbounded wait). Defaults to 120.
#lock timeout 120
+# The most sessions one account (one uid, gid and maildir in the
+# credentials file) may have open at once, from all its clients
+# together. A login past it is answered NO [LIMIT] (RFC 9051 section
+# 7.1) and the connection is closed; the client may log in again on a
+# new connection once one of the account's sessions has ended. Must be
+# 1-4096. Defaults to 8.
+#account sessions 8
+
+# The most connections open at once, logged in or not. A new connection
+# past it gets a BYE [UNAVAILABLE] greeting on port 143 and is closed;
+# on port 993 it is closed with nothing sent. C connections, L of them
+# not yet logged in, for A accounts need up to C + L + 2A processes and
+# as many descriptors in the parent, so raise kern.maxproc and the
+# login class's openfiles before raising this. The default fits
+# OpenBSD's defaults. Must be 1-4096. Defaults to 256.
+#connections max 256
+
# Admission-control throttle on concurrent, not-yet-authenticated
# connections, modeled on sshd_config(5)'s MaxStartups (see that
# man page for the canonical description of this algorithm).
# accepted normally. Between "begin" and "full", new connections
# are refused with linearly increasing probability, starting at
# "rate" percent at "begin" and reaching 100% at "full". At or
-# above "full", every new connection is refused outright.
+# above "full", every new connection is refused outright, and answered
+# as for "connections max".
# Defaults to sshd_config(5)'s own default, 10:30:100.
#startups begin 10 rate 30 full 100
+
+# The most not-yet-authenticated connections from any one address, as
+# sshd_config(5)'s PerSourceMaxStartups, so one address cannot hold
+# every "startups" slot. Each IPv6 address counts on its own. Users
+# behind one NAT address share it; raise it for them, or set "none"
+# to turn it off. A refused connection is answered as for
+# "connections max". Must be 1-1000000 or none. Defaults to 5.
+#startups per-source 5
blob - 4b279ff164f1c69ee2048b601d4709ae2c096844
blob + 36a55e455f441bce94458123f671b274b6e98577
--- src/imapd.h
+++ src/imapd.h
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* Shared definitions for every imapd(8) process role. */
-
#ifndef IMAPD_H
#define IMAPD_H
#include <sys/types.h>
-#include <sys/cdefs.h> /* __dead */
+#include <sys/cdefs.h>
#include <sys/queue.h>
-#include <sys/socket.h> /* sockaddr_storage, socklen_t */
+#include <sys/socket.h>
#include <event.h>
#include <imsg.h>
#include <stdint.h>
-#define IMAPD_VERSION "0.1.5"
+#define IMAPD_VERSION "0.1.6"
-/* Process roles, selected at exec time via "-x <role>". See main.c. */
enum openimap_proc_type {
PROC_PARENT,
PROC_LISTENER,
PROC_AUTH,
PROC_STORE,
PROC_KEYMGR,
- PROC_SEARCH /* per-connection SEARCH-grammar
- * parsing oracle */
+ PROC_PARSER
};
-/* imsg message catalog. */
enum imsg_type {
IMSG_NONE,
IMSG_SETUP_PEER,
IMSG_SETUP_DONE,
- /* parent -> listener-worker at fork, and -> keymgr at boot and on */
- /* every SIGHUP. The certificate is public; each gets its own copy. */
IMSG_TLS_CERT,
- /* parent -> listener-worker at fork: one already-accepted */
- /* connection, the client fd riding as this imsg's fd-pass. */
+ /* parent -> listener-worker: an accepted connection, fd-passed */
IMSG_LISTENER_SESSION_INIT,
- /* parent -> auth, at boot */
IMSG_AUTH_INIT,
/* listener <-> auth */
IMSG_AUTH_REQUEST,
IMSG_AUTH_RESULT,
- /* parent -> listener-worker and -> search-oracle at fork, wiring the */
- /* per-connection SEARCH-parsing oracle. Its own type rather than */
- /* IMSG_SETUP_PEER, whose id field is already a peer discriminator. */
- IMSG_SETUP_SEARCH_PEER,
-
- /* listener -> search-oracle: SEARCH argument text as raw trailing */
- /* bytes, no fixed struct. Oracle -> listener: struct */
- /* imsg_search_parse_result. One round trip per session at most, so */
- /* no correlation id. */
- IMSG_SEARCH_PARSE_REQUEST,
- IMSG_SEARCH_PARSE_RESULT,
-
- /* parent -> keymgr, at boot and on SIGHUP: the real TLS private key */
+ /* parent -> keymgr, at boot and on SIGHUP: the TLS private key */
IMSG_KEYMGR_INIT,
- /* listener <-> keymgr: one private-key operation, forwarded from */
- /* listener's OpenSSL engine override. Each reply reuses its */
- /* request's type, correlated by the imsg id field. */
+ /* listener <-> keymgr: the reply reuses the request's type and id */
IMSG_KEYMGR_RSA_PRIVENC,
IMSG_KEYMGR_RSA_PRIVDEC,
IMSG_KEYMGR_ECDSA_SIGN,
- /* parent -> keymgr, on shutdown: exit, this was not a crash */
IMSG_KEYMGR_SHUTDOWN,
- /* auth -> parent, per successful login */
IMSG_AUTH_CRED,
+ IMSG_AUTH_EXIT,
- /* per-session store spawn */
IMSG_STORE_FORK,
IMSG_STORE_INIT,
IMSG_STORE_SHUTDOWN,
+ IMSG_STORE_EXIT,
- /* listener <-> store, once a session's store child is wired up. */
- /* SELECT carries EXAMINE too, as a request with "readonly" set. */
+ /* parent -> parser: the uid to drop to; the parser opens nothing */
+ IMSG_PARSER_INIT,
+ /* store -> parent: start a parser; it arrives as IMSG_SETUP_PEER */
+ IMSG_PARSER_WANT,
+ IMSG_PARSER_NONE,
+
+ /* store <-> parser: the reply reuses the request's type and id */
+ IMSG_PARSER_ENVELOPE,
+ IMSG_PARSER_BODYSTRUCTURE,
+ IMSG_PARSER_HEADER_FIELDS,
+ IMSG_PARSER_PART,
+ IMSG_PARSER_SEARCH,
+
+ /* listener <-> store; SELECT carries EXAMINE as readonly */
IMSG_MBOX_SELECT,
IMSG_MBOX_SELECTED,
IMSG_MBOX_FETCH,
IMSG_MBOX_FETCH_META,
- /* the four below are all store -> listener, one per message */
- IMSG_MBOX_FETCH_HEADER, /* raw BODY.PEEK[HEADER] bytes */
- IMSG_MBOX_FETCH_BODY, /* body descriptor and octet range */
- IMSG_MBOX_FETCH_ENVELOPE, /* formatted ENVELOPE text */
- IMSG_MBOX_FETCH_BODYSTRUCTURE, /* formatted BODYSTRUCTURE text */
+ IMSG_MBOX_FETCH_HEADER,
+ IMSG_MBOX_FETCH_BODY,
+ IMSG_MBOX_FETCH_ENVELOPE,
+ IMSG_MBOX_FETCH_BODYSTRUCTURE,
IMSG_MBOX_STORE,
- IMSG_MBOX_APPEND, /* opens the message's tmp/ file */
- IMSG_MBOX_APPEND_DATA, /* one piece of the literal */
- IMSG_MBOX_APPEND_END, /* literal complete, commit it */
+ IMSG_MBOX_APPEND,
+ IMSG_MBOX_APPEND_DATA,
+ IMSG_MBOX_APPEND_END,
IMSG_MBOX_APPENDED,
IMSG_MBOX_COPY,
IMSG_MBOX_MOVE,
IMSG_MBOX_RENAME,
IMSG_MBOX_RESULT,
- /* RFC 7162 CONDSTORE/QRESYNC. Both store -> listener, streamed */
- /* before the terminal reply. */
- IMSG_MBOX_SELECT_VANISHED, /* one vanished UID, QRESYNC resync */
- IMSG_MBOX_STORE_MODIFIED, /* one UNCHANGEDSINCE failure */
+ /* RFC 7162 CONDSTORE/QRESYNC */
+ IMSG_MBOX_SELECT_VANISHED,
+ IMSG_MBOX_STORE_MODIFIED,
/* RFC 9051 SS6.3.13 (IDLE) */
- IMSG_MBOX_IDLE_REFRESH, /* listener -> store, seed or diff */
- IMSG_MBOX_IDLE_EXPUNGE, /* one untagged EXPUNGE to print */
- IMSG_MBOX_IDLE_FETCH, /* one flag change, as fetch_meta */
- IMSG_MBOX_IDLE_REFRESHED, /* terminal reply */
+ IMSG_MBOX_IDLE_REFRESH,
+ IMSG_MBOX_IDLE_EXPUNGE,
+ IMSG_MBOX_IDLE_FETCH,
+ IMSG_MBOX_IDLE_REFRESHED,
- /* RFC 9051 SS6.3.9 (LIST): one mailbox name, store -> listener, */
- /* before the terminal IMSG_MBOX_RESULT. */
+ /* RFC 9051 SS6.3.9 (LIST) */
IMSG_MBOX_LIST_ITEM,
- /* RFC 9051 SS6.3.7/SS6.3.8: both carry struct imsg_mbox_subscribe */
- /* and reply with IMSG_MBOX_RESULT. */
+ /* RFC 9051 SS6.3.7/SS6.3.8 (SUBSCRIBE/UNSUBSCRIBE) */
IMSG_MBOX_SUBSCRIBE,
IMSG_MBOX_UNSUBSCRIBE
};
-/* privsep imsg-over-event(3) wrapper. */
struct imsgev {
struct imsgbuf ibuf;
void (*handler)(int, short, void *);
#define LISTENER_MAX_ADDRS 2
struct openimap_config {
- char listen_addr[64]; /* "0.0.0.0" (default), "::", a literal
- * address, or "*" for both */
- /* A port of 0 means this listener is not configured. parse.y clears */
- /* the one a config did not name, but only if it named either. */
+ char listen_addr[64];
uint16_t port_cleartext; /* 143, STARTTLS; 0 = not configured */
uint16_t port_implicit_tls; /* 993, RFC 8314; 0 = not configured */
- char spool_root[1024]; /* mail spool root, store's chroot */
- char cred_file[1024]; /* auth's credential file, one
- * line per user, format
- * username:passwordhash:uid:gid:
- * maildir */
+ char spool_root[1024];
+ char cred_file[1024];
char tls_cert_file[1024];
char tls_key_file[1024];
- uint32_t bodystructure_read_max; /* "attachment max" directive */
- uint64_t append_max; /* "append max" directive */
+ uint32_t bodystructure_read_max;
+ uint64_t append_max;
- /* the "startups begin/rate/full" directive. config_load() defaults */
- /* to 10/30/100, matching sshd_config(5)'s own "10:30:100". */
uint32_t max_startups_begin;
uint32_t max_startups_rate; /* percent, 0-100 */
uint32_t max_startups_full;
+ uint32_t max_startups_per_source;
- /* "idle poll": how often an IDLEing session asks its store child */
- /* whether the mailbox changed. 0 disables polling, and an IDLEing */
- /* session then sees nothing until it sends DONE. */
uint32_t idle_poll_secs;
-
- /* "lock timeout": seconds a command waits for another session's */
- /* index lock before answering NO [INUSE]. 0 disables the bound, */
- /* restoring the unbounded wait it replaced. */
uint32_t lock_timeout_secs;
-
- /* "login grace": seconds a connection may go without authenticating */
- /* before it is closed. 0 disables the timer, which reopens the */
- /* denial of service it exists to stop. */
uint32_t login_grace_secs;
+ uint32_t account_sessions;
+ uint32_t connections_max;
};
-/* imsg payload wire structs. */
#define AUTH_USERNAME_MAX 64
#define AUTH_PASSWORD_MAX 128
#define AUTH_MAILDIR_MAX 256
-/* Boot-time config-delivery payloads. */
-
-/* IMSG_LISTENER_SESSION_INIT's payload. The client fd rides as the imsg's */
-/* fd-pass, not a field here. remote_ss/remote_sslen are the raw sockaddr */
-/* from accept(2), so the worker does its own getnameinfo() formatting. */
struct imsg_listener_session_init {
uint32_t session_id;
int implicit_tls;
struct sockaddr_storage remote_ss;
socklen_t remote_sslen;
- /* carried per connection, since this process is spawned fresh per */
- /* connection and so needs no reload path of its own */
uint32_t idle_poll_secs;
uint32_t login_grace_secs;
uint64_t append_max;
char cred_file[1024];
};
-/*
- * IMSG_KEYMGR_RSA_PRIVENC / _RSA_PRIVDEC / _ECDSA_SIGN (listener to keymgr
- * and back, same imsg type each way, correlated by the imsg id field).
- * Fixed header plus trailing raw bytes on one imsg, as imsg_mbox_append
- * below does.
- *
- * hash is libtls's tls_cert_pubkey_hash() format ("SHA256:" plus lowercase
- * hex of the certificate's DER SubjectPublicKeyInfo digest); keymgr.c
- * recomputes it from the certificate it holds and refuses a mismatch.
- * padding is an OpenSSL RSA_PKCS1_PADDING-style constant, ignored for
- * ECDSA_SIGN. KEYMGR_DATA_MAX (1024) covers an RSA buffer up to an
- * 8192-bit key and any ECDSA digest or signature.
- */
+/* keymgr request: fixed header, then fromlen bytes */
#define KEYMGR_HASH_MAX 80 /* "SHA256:" + 64 hex chars + NUL, generous */
#define KEYMGR_DATA_MAX 1024
struct imsg_keymgr_sign_request {
char hash[KEYMGR_HASH_MAX];
- /* RSA padding mode; ignored for ECDSA */
+ /* ignored for ECDSA */
uint32_t padding;
- /* trailing input bytes, <= KEYMGR_DATA_MAX */
uint32_t fromlen;
};
struct imsg_keymgr_sign_reply {
- /*
- * 0 = refused/failed; listener's engine callback returns this straight
- * to OpenSSL, which fails that one RSA/EC operation, same as any other
- * engine failure -- see keymgr.c's header comment on why this is a
- * reply, not a fatalx()
- */
+ /* a failure fails one TLS operation, not the listener */
int ok;
- /* trailing output bytes, meaningful only if ok */
uint32_t tolen;
};
struct imsg_store_fork {
uint32_t session_id;
+ int limit; /* refused by "account sessions" */
};
+struct imsg_parser_init {
+ uint32_t session_id;
+ uid_t uid;
+ gid_t gid;
+ uint32_t bodystructure_read_max;
+};
+
struct imsg_store_init {
uint32_t session_id;
uid_t uid;
gid_t gid;
- /* store needs this to chroot() */
char spool_root[1024];
- /*
- * THIS session's own mailbox subdirectory, relative to spool_root above
- */
+ /* relative to spool_root */
char maildir[STORE_MAILDIR_MAX];
- /*
- * copied from struct openimap_config's field of the same name
- */
uint32_t bodystructure_read_max;
uint64_t append_max;
uint32_t lock_timeout_secs;
};
-/*
- * IMSG_MBOX_SELECT (listener -> store) / IMSG_MBOX_SELECTED (store ->
- * 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).
- *
- * 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, and MBOX_OP_ERR_BUSY via RFC 9051 SS7.1's INUSE.
- */
+/* RFC 5530 NONEXISTENT/ALREADYEXISTS, RFC 9051 SS7.1 INUSE */
enum mbox_op_error {
MBOX_ERR_UNSET = 0,
MBOX_OP_OK,
MBOX_OP_ERR_GENERIC,
MBOX_OP_ERR_NO_SUCH_MAILBOX,
MBOX_OP_ERR_ALREADY_EXISTS,
- /* gave up waiting for another session's index lock */
MBOX_OP_ERR_BUSY,
};
char mailbox[MBOX_NAME_MAX];
int readonly; /* 1 = EXAMINE, 0 = SELECT */
- int qresync; /* 1 if QRESYNC was requested and
- * enabled (RFC 7162 SS3.2.5) */
- uint32_t qresync_uidvalidity; /* client's last-known
- * 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;
- * store.c then defaults to the full
- * UID range (SS3.2.5.1) */
- uint32_t qresync_nranges; /* count of the trailing struct
- * seq_range array (RFC 9051 SS9
- * sequence-set; "*" is forbidden in
- * known-uids per SS3.2.5.1, already
- * rejected by parse_qresync_group());
- * ignored (and 0) if
- * !qresync_has_uids */
+ int qresync;
+ uint32_t qresync_uidvalidity;
+ uint64_t qresync_modseq;
+ int qresync_has_uids;
+ uint32_t qresync_nranges;
};
struct imsg_mbox_selected {
- 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: mailbox's highest mod-sequence, always
- * populated; listener.c decides whether to surface it via the
- * HIGHESTMODSEQ response code. */
+ enum mbox_op_error error;
+ uint32_t exists;
+ uint32_t uidvalidity;
+ uint32_t uidnext;
uint64_t highestmodseq;
- /* 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. */
+ /* preceded by VANISHED, then FETCH_META (RFC 7162 SS3.2.6) */
};
-/* 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. */
+/* RFC 9051 SS6.3.11, plus RFC 7162 SS3.1.7 HIGHESTMODSEQ */
#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 dropped
- * \Recent/RECENT, but
- * some clients still
- * ask, always
- * answered "0" rather
- * than BAD */
+#define STATUS_ATT_RECENT (1U << 7) /* answered "0" */
-/*
- * IMSG_MBOX_STATUS (listener -> store) / IMSG_MBOX_STATUS_RESULT (store ->
- * listener): RFC 9051 SS6.3.11 STATUS, one combined reply, no per-message
- * streaming.
- *
- * mailbox targets any named mailbox independent of what's selected
- * (SS6.3.11); store.c opens it with mailbox_open_dir() and closes it
- * afterward, leaving the selection alone.
- */
+/* RFC 9051 SS6.3.11 STATUS; the selection is left alone */
struct imsg_mbox_status {
char mailbox[MBOX_NAME_MAX];
- 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) */
+ uint32_t attrs;
};
struct imsg_mbox_status_result {
- 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
- * requested, see imsg_mbox_status.attrs
- * comment */
- uint32_t deleted; /* STATUS_ATT_DELETED, same as above */
- uint64_t size; /* STATUS_ATT_SIZE, same as above */
+ enum mbox_op_error error;
+ uint32_t messages;
+ uint32_t uidnext;
+ uint32_t uidvalidity;
+ uint64_t highestmodseq;
+ uint32_t unseen;
+ uint32_t deleted;
+ uint64_t size;
};
-/*
- * 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] 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.
- *
- * 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).
- */
+/* one gap [uid_lo, uid_hi]; RFC 7162 SS5.1 minimal state */
struct imsg_mbox_select_vanished {
uint32_t uid_lo;
uint32_t uid_hi;
};
-/*
- * IMSG_MBOX_FETCH (listener -> store) / IMSG_MBOX_FETCH_META (store ->
- * listener, one per matching message, ascending sequence order) /
- * IMSG_MBOX_RESULT (store -> listener, once, after the last META).
- */
-#define MBOX_FLAGS_MAX 256 /* generous; truncated (not rejected) if
- * exceeded */
+#define MBOX_FLAGS_MAX 256 /* truncated 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 --
- * 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
- * [.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 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 (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. */
+#define MBOX_FETCH_MODSEQ (1U << 4) /* RFC 7162 SS3.1.4.2 */
+#define MBOX_FETCH_BODY_HEADER (1U << 5)
+#define MBOX_FETCH_BODY_WHOLE (1U << 6)
+#define MBOX_FETCH_BODY_TEXT (1U << 7)
+#define MBOX_FETCH_HEADER_FIELDS (1U << 8)
+#define MBOX_FETCH_ENVELOPE (1U << 9)
+#define MBOX_FETCH_BODYSTRUCTURE (1U << 10) /* and bare BODY */
+#define MBOX_FETCH_BODY_PART (1U << 11)
-/* Cap on the dotted-numeric section-part string. An oversized one is */
-/* dropped as an unsupported fetch-att, not truncated and not an error. */
#define SECTION_PART_MAX 40
-/* Cap on raw header bytes one IMSG_MBOX_FETCH_HEADER carries. An */
-/* oversized header is "not found", not truncated. */
#define FETCH_HEADER_MAX 8192
-/* "append max" default and ceiling. The default matches smtpd.conf(5)'s */
-/* max-message-size default of 35M. */
+/* smtpd.conf(5)'s max-message-size default */
#define APPEND_MAX_DEFAULT (35 * 1024 * 1024)
#define APPEND_MAX_MAX 1073741824
-/* Bytes a FETCH walk composes before it pauses to let them drain, so */
-/* the store holds this much of a reply rather than all of it. */
#define FETCH_BATCH_MAX (1024 * 1024)
-/* Descriptors a FETCH walk passes before it pauses. Each holds a slot in */
-/* the system-wide file table, kern.maxfiles, until it has been sent. */
+/* each fd holds a kern.maxfiles slot until sent */
#define FETCH_FD_MAX 16
-/* Cap on the space-joined header-field-name list; an oversized list is */
-/* rejected, not truncated. */
#define HEADER_FIELDS_MAX 256
-/* Cap on formatted ENVELOPE text; an oversized one is "not found". */
#define ENVELOPE_MAX 8192
-/* Ceilings on the recursive BODYSTRUCTURE builder: a message's own */
-/* headers claim its part count and depth, so neither may be trusted. */
-/* Exceeding either is "not found" rather than a truncated part tree. */
+/* relayd's RELAY_TLS_PRIV_TIMEOUT bounds a similar wait */
+#define PARSER_REPLY_TIMEOUT_SEC 10
+
+/* SIGXCPU at soft, SIGKILL at hard (sys/kern/kern_resource.c) */
+#define PARSER_CPU_SOFT_SEC 8
+#define PARSER_CPU_HARD_SEC 10
+
+/* half the soft limit, so honest work retires before SIGXCPU */
+#define PARSER_CPU_RETIRE_SEC (PARSER_CPU_SOFT_SEC / 2)
+
+#define PARSER_IDLE_SEC 60
+
+#define PARSER_STRIKES 3
+
+/* a message's own headers claim these, so neither is trusted */
#define MIME_MAX_DEPTH 10
#define MIME_MAX_PARTS 64
-/* Cap on formatted BODYSTRUCTURE text; MIME_MAX_PARTS and */
-/* MIME_MAX_DEPTH above are the limits that bite first in practice. */
#define BODYSTRUCTURE_MAX 12000
-/* "idle poll" bounds. The default is short because an unchanged mailbox */
-/* costs two stat(2) calls and no lock. IDLE_POLL_MAX is a sanity bound. */
-#define IDLE_POLL_DEFAULT 5 /* seconds */
+#define IDLE_POLL_DEFAULT 5
#define IDLE_POLL_MAX 300 /* seconds; 0 disables polling */
-/* "login grace" bounds. RFC 9051 SS5.4 permits a shortened */
-/* pre-authentication timer specifically against denial of service; the */
-/* 30 minute floor in that section governs a POST-authentication */
-/* autologout, which this server does not have. */
-#define LOGIN_GRACE_DEFAULT 60 /* seconds */
+/* RFC 9051 SS5.4 permits a short pre-authentication timer */
+#define LOGIN_GRACE_DEFAULT 60
#define LOGIN_GRACE_MAX 3600 /* seconds; 0 disables the timer */
-/* "lock timeout" bounds. A command that cannot take a mailbox's index */
-/* lock waits this long before answering NO [INUSE] (RFC 9051 SS7.1). It */
-/* is a safety net for a holder that is stuck, not a cure for one that is */
-/* merely slow: an ordinary STORE over a large mailbox holds the lock for */
-/* the better part of a minute on modest hardware, and a deadline under */
-/* that would refuse ordinary concurrent use. */
-#define LOCK_TIMEOUT_DEFAULT 120 /* seconds */
+/* lock wait, then NO [INUSE] (RFC 9051 SS7.1) */
+#define LOCK_TIMEOUT_DEFAULT 120
#define LOCK_TIMEOUT_MAX 3600 /* seconds; 0 disables the bound */
-/* Cap on on-disk bytes read while deriving a message's MIME structure. */
-/* Deliberately above APPEND_MAX_DEFAULT: mail from an external MTA is */
-/* not bounded by "append max", and only the derived summary */
-/* goes back over the wire. Exceeding it is "not found", not truncated. */
-/* config_load()'s default for imapd.conf's "attachment max". */
+/* "account sessions" bounds; the maximum is parent.c's OPEN_SESSION_MAX */
+#define ACCOUNT_SESSIONS_DEFAULT 8
+#define ACCOUNT_SESSIONS_MAX 4096
+
+/* "connections max" bounds; the maximum is parent.c's OPEN_SESSION_MAX */
+#define CONNECTIONS_MAX_DEFAULT 256
+#define CONNECTIONS_MAX_MAX 4096
+
+#define STARTUPS_PER_SOURCE_DEFAULT 5
+
+/* "attachment max"; mail from an MTA is not bound by "append max" */
#define BODYSTRUCTURE_READ_DEFAULT 41943040
-/*
- * One comma-separated range of an RFC 9051 SS9 sequence-set. "*" travels
- * unresolved via lo_is_star/hi_is_star, since only the store knows the live
- * value to resolve it against. A whole sequence-set rides as an imsg
- * request's trailing seq_range[nranges] array.
- */
+/* each request carries an O_RDONLY descriptor on the message */
+struct imsg_parser_req {
+ char basename[512];
+ char fields[HEADER_FIELDS_MAX];
+ int fields_not;
+ int path[MIME_MAX_DEPTH];
+ int pathlen;
+ uint32_t nleaves;
+ uint32_t poollen;
+};
+
+/* RFC 9051 SS6.4.4 content key; strings are offsets into the pool */
+struct imsg_parser_leaf {
+ int32_t op;
+ uint32_t str_off;
+ uint32_t str_len;
+ uint32_t name_off;
+ uint32_t name_len;
+ int64_t num; /* SENT*: that day's UTC midnight */
+};
+
+struct imsg_parser_rep {
+ int found;
+ uint32_t len;
+ uint64_t part_off;
+ uint64_t part_len;
+ uint32_t retiring;
+};
+
+/* RFC 9051 SS9 sequence-set range; the store resolves "*" */
struct seq_range {
- uint32_t lo; /* 1-based, inclusive; ignored if lo_is_star */
- uint32_t hi; /* 1-based, inclusive; ignored if hi_is_star */
+ uint32_t lo;
+ uint32_t hi;
int lo_is_star;
int hi_is_star;
};
-/* Bounds a sequence-set's range count, so the trailing seq_range array */
-/* cannot grow an imsg past MAX_IMSGSIZE. */
+/* keeps the trailing array under MAX_IMSGSIZE */
#define SEQSET_MAX_RANGES 500
struct imsg_mbox_fetch {
- uint32_t nranges; /* count of the trailing struct
- * seq_range array (RFC 9051 SS9
- * sequence-set); "*" resolves
- * against store's live message
- * count, not listener's possibly-
- * stale SELECT-time count */
- uint32_t attrs; /* bitmask of MBOX_FETCH_* above */
+ uint32_t nranges;
+ uint32_t attrs;
- /* RFC 7162 SS3.1.4.1 CHANGEDSINCE; has_changedsince distinguishes
- * "not specified" from a legal value of 0. */
+ /* RFC 7162 SS3.1.4.1 CHANGEDSINCE */
int has_changedsince;
uint64_t changedsince;
- /* by_uid: RFC 9051 SS6.4.9 UID FETCH, resolve the trailing
- * sequence-set ranges against UID space rather than sequence-
- * number space. listener.c separately forces MBOX_FETCH_UID
- * into attrs whenever this is set.
- *
- * 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.
- */
+ /* RFC 9051 SS6.4.9 UID FETCH; RFC 7162 SS3.2.6 VANISHED */
int by_uid;
int want_vanished;
- /* 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): 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), applying uniformly to whole/TEXT/section_part.
- * store.c slices the extracted content to
- * [partial_start, partial_start + partial_count), clamped to
- * the content's actual length (SS6.4.5's truncate-past-end-of-text
- * rule) and to nothing else. has_partial distinguishes
- * "no range" from a legal partial_start of 0.
- */
+ /* a second section-part in one FETCH is ignored */
char section_part[SECTION_PART_MAX];
int has_partial;
uint32_t partial_start;
};
struct imsg_mbox_fetch_meta {
- uint32_t seqno; /* 1-based */
+ uint32_t seqno;
uint32_t uid;
- uint64_t size; /* on-disk file size, octets --
- * RFC822.SIZE (RFC 9051 SS2.3.4).
- * 64-bit so a message over 4 GiB
- * reports correctly (F13 fix). */
- int64_t internaldate; /* Unix timestamp, parsed from the
- * 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 */
- 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 */
+ uint64_t size;
+ int64_t internaldate; /* from the basename, not mtime */
+ char flags[MBOX_FLAGS_MAX];
+ uint64_t modseq;
};
-/*
- * IMSG_MBOX_FETCH_HEADER: sent by store.c immediately before the
- * 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.
- */
+/* sent just before the same message's IMSG_MBOX_FETCH_META */
struct imsg_mbox_fetch_header {
- uint32_t seqno; /* 1-based, matches the following
- * IMSG_MBOX_FETCH_META */
+ uint32_t seqno;
uint32_t uid;
- 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. hdrlen and the trailing
- * bytes are only meaningful if 1. */
- uint32_t hdrlen; /* length of the trailing raw header
- * bytes on this imsg, capped at
- * FETCH_HEADER_MAX */
+ int found;
+ uint32_t hdrlen;
};
-/*
- * IMSG_MBOX_FETCH_BODY: sent by store.c immediately before the
- * IMSG_MBOX_FETCH_META for the same message, iff req->attrs &
- * (MBOX_FETCH_BODY_WHOLE | MBOX_FETCH_BODY_TEXT | MBOX_FETCH_BODY_PART).
- * The octets do not ride on the imsg. A found, non-empty body carries a
- * read-only descriptor on the message file, and offset and length say
- * which octets the literal is; the store has already done any parsing,
- * so the listener only reads and writes.
- */
+/* the octets travel as a passed descriptor, not in the imsg */
struct imsg_mbox_fetch_body {
- uint32_t seqno; /* 1-based, matches the following
- * IMSG_MBOX_FETCH_META */
+ uint32_t seqno;
uint32_t uid;
- int found; /* 0 if the message file couldn't be
- * found, it contained a NUL byte, or
- * (TEXT only) no header/body
- * separator was found */
- uint64_t offset; /* first octet, from the file's start */
+ int found;
+ uint64_t offset;
uint64_t length; /* octets; no descriptor when 0 */
};
-/*
- * IMSG_MBOX_FETCH_ENVELOPE: sent by store.c immediately before the
- * 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.
- */
+/* CRLF-free is enforced by parser_reply_safe() in store.c */
struct imsg_mbox_fetch_envelope {
- uint32_t seqno; /* 1-based, matches the following
- * IMSG_MBOX_FETCH_META */
+ uint32_t seqno;
uint32_t uid;
- 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 */
+ int found;
+ uint32_t envlen;
};
-/*
- * IMSG_MBOX_FETCH_BODYSTRUCTURE: sent by store.c immediately before the
- * 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 following
- * IMSG_MBOX_FETCH_META */
+ uint32_t seqno;
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, 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 */
+ int found;
+ uint32_t bslen;
};
-/* enum mbox_op_error is defined earlier in this file, above
- * 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
- * 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: 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).
- */
+ uint32_t count;
uint64_t highestmodseq;
- /* RFC 9051 SS7.1 COPYUID response code: destination mailbox's
- * UIDVALIDITY. Populated only for COPY/MOVE; 0 for
- * FETCH/STORE/EXPUNGE/SEARCH.
- */
+ /* RFC 9051 SS7.1 COPYUID; COPY and MOVE only */
uint32_t uidvalidity;
};
-/*
- * 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 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).
- */
+/* keywords may not hold ':' or ','; the index cannot escape them */
#define MBOX_FLAG_ANSWERED (1U << 0)
#define MBOX_FLAG_FLAGGED (1U << 1)
#define MBOX_FLAG_DELETED (1U << 2)
#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 */
+#define MBOX_STORE_ADD 1 /* +FLAGS */
+#define MBOX_STORE_REMOVE 2 /* -FLAGS */
struct imsg_mbox_store {
- uint32_t nranges; /* count of the trailing struct
- * seq_range array (RFC 9051 SS9
- * sequence-set), same header-plus-
- * trailing-array shape as
- * imsg_mbox_fetch above; always >= 1,
- * STORE always requires a
- * sequence-set */
- int mode; /* MBOX_STORE_* above */
- int silent; /* 1 = ".SILENT", suppress the
- * untagged FETCH per message */
- uint32_t sysflags; /* MBOX_FLAG_* bitmask named in this
- * 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 */
+ uint32_t nranges;
+ int mode;
+ int silent;
+ uint32_t sysflags;
+ char keywords[MBOX_FLAGS_MAX];
- /* RFC 7162 SS3.1.3 UNCHANGEDSINCE; has_unchangedsince distinguishes
- * "not specified" from the legal (always-fails) value 0.
- */
+ /* RFC 7162 SS3.1.3 UNCHANGEDSINCE */
int has_unchangedsince;
uint64_t unchangedsince;
- /* 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 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.
- */
+/* RFC 7162 SS3.1.3 MODIFIED */
struct imsg_mbox_store_modified {
uint32_t seqno;
uint32_t uid;
};
-/*
- * IMSG_MBOX_EXPUNGE (listener -> store) / IMSG_MBOX_EXPUNGED (store ->
- * listener, one per removed message, in removal order) / IMSG_MBOX_RESULT
- * (store -> listener, terminal).
- *
- * 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 lets CLOSE (SS6.4.1: no untagged EXPUNGE responses) reuse this
- * request/reply pair, same ".SILENT" pattern as STORE.
- */
+/* RFC 9051 SS6.4.3; lowest-numbered first (SS7.5.1) */
struct imsg_mbox_expunge {
int silent; /* 1 for CLOSE, 0 for a real EXPUNGE command */
- /* RFC 9051 SS6.4.9's UID EXPUNGE form: only \Deleted messages in
- * the trailing struct seq_range array (nranges entries, same
- * header-plus-trailing-array shape as imsg_mbox_fetch above) are
- * removed. Never set together with silent=1 (no "UID CLOSE").
- * Meaningless when !by_uid (plain EXPUNGE/CLOSE take no
- * arguments) -- nranges is 0 and there's no trailing data in
- * that case, unlike every other sequence-set-bearing imsg here,
- * which always require nranges >= 1.
- */
+ /* UID EXPUNGE; nranges is 0 unless by_uid */
int by_uid;
uint32_t nranges;
};
struct imsg_mbox_expunged {
- 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 */
+ uint32_t seqno; /* after earlier EXPUNGEs (RFC 9051 SS7.5.1) */
+ uint32_t uid; /* for VANISHED (RFC 7162 SS3.2.10.2) */
};
-/*
- * 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 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.
- */
+/* RFC 9051 SS6.4.7 COPY and SS6.4.8 MOVE */
struct imsg_mbox_copy {
int by_uid;
- uint32_t nranges; /* count of the trailing struct
- * seq_range array, same header-plus-
- * trailing-array shape as
- * imsg_mbox_fetch above; always >= 1,
- * COPY/MOVE always require a
- * sequence-set */
+ uint32_t nranges;
char destname[MBOX_NAME_MAX];
};
-/*
- * IMSG_MBOX_COPY_MAPPING (store -> listener, zero or more, before the
- * 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 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.
- */
+/* COPYUID pairs, sent before EXPUNGE for MOVE (RFC 9051 SS6.4.8) */
struct imsg_mbox_copy_mapping {
uint32_t src_uid;
uint32_t dest_uid;
};
-/*
- * IMSG_MBOX_APPEND (listener -> store) / IMSG_MBOX_APPENDED (store ->
- * listener, exactly once).
- *
- * The RFC 9051 SS6.3.12 literal does not ride on this imsg. This struct
- * goes alone when the literal is announced, the octets follow as
- * IMSG_MBOX_APPEND_DATA pieces as they are read, and IMSG_MBOX_APPEND_END
- * follows the command's closing CRLF. The store answers only after END.
- * Each piece is at most SESSION_INBUF_MAX, under MAX_IMSGSIZE.
- */
+/* the literal follows as APPEND_DATA pieces, then APPEND_END */
struct imsg_mbox_append {
char mailbox[MBOX_NAME_MAX];
- 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 = use current time at delivery
- * (SS6.3.12) */
- int64_t date; /* Unix timestamp; meaningful only if
- * has_date */
- uint64_t msglen; /* announced literal length; the
- * DATA pieces must add up to it */
+ uint32_t sysflags;
+ char keywords[MBOX_FLAGS_MAX];
+ int has_date;
+ int64_t date;
+ uint64_t msglen;
};
struct imsg_mbox_appended {
- 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 */
+ enum mbox_op_error error; /* NO_SUCH_MAILBOX gets [TRYCREATE] */
uint32_t uidvalidity;
- 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,
- * for SS6.3.12's untagged EXISTS
- * notification, sent only if this
- * session has the mailbox selected */
+ uint32_t uid; /* APPENDUID, with uidvalidity */
+ uint32_t exists;
};
-/*
- * IMSG_MBOX_SEARCH (listener -> store) / IMSG_MBOX_SEARCH_MATCH (store ->
- * 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, so it
- * can't be a single fixed-size struct. search_cmd.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.
- */
+/* RFC 9051 SS6.4.4 search-program, as a postfix array of nodes */
#define SEARCH_PROGRAM_MAX_NODES 100
-#define SEARCH_KEYWORD_MAX 64 /* one flag-keyword atom, not
- * a list, MBOX_FLAGS_MAX is
- * sized for STORE's comma-
- * joined keyword *list* and
- * would be the wrong constant
- * to reuse here */
+#define SEARCH_KEYWORD_MAX 64 /* one keyword, not a list */
+/* operand pool; RFC 9051 SS4.3's non-synchronizing literal cap */
+#define SEARCH_OPERANDS_MAX 4096
-#define SEARCH_OP_ALL 0 /* leaf: matches every message */
+#define SEARCH_OP_ALL 0
#define SEARCH_OP_ANSWERED 1
#define SEARCH_OP_UNANSWERED 2
#define SEARCH_OP_DELETED 3
#define SEARCH_OP_UNFLAGGED 8
#define SEARCH_OP_SEEN 9
#define SEARCH_OP_UNSEEN 10
-#define SEARCH_OP_KEYWORD 11 /* operand: keyword */
-#define SEARCH_OP_UNKEYWORD 12 /* operand: keyword */
-#define SEARCH_OP_BEFORE 13 /* operand: num (UTC day-start epoch) */
-#define SEARCH_OP_ON 14 /* operand: num (UTC day-start epoch) */
-#define SEARCH_OP_SINCE 15 /* operand: num (UTC day-start epoch) */
-#define SEARCH_OP_LARGER 16 /* operand: num (octets) */
-#define SEARCH_OP_SMALLER 17 /* operand: num (octets) */
-#define SEARCH_OP_SEQSET 18 /* operand: seq_lo/seq_hi/lo_is_star/
- * hi_is_star, matched against sequence
- * number */
-#define SEARCH_OP_UIDSET 19 /* operand: seq_lo/seq_hi/lo_is_star/
- * hi_is_star, matched against UID */
-#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,
- * matches if the message's own
- * mod-sequence is >= this. The
- * optional <entry-name>/<entry-type-
- * req> prefix is parsed by
- * listener.c for syntax only and
- * never reaches this struct */
+#define SEARCH_OP_KEYWORD 11
+#define SEARCH_OP_UNKEYWORD 12
+#define SEARCH_OP_BEFORE 13
+#define SEARCH_OP_ON 14
+#define SEARCH_OP_SINCE 15
+#define SEARCH_OP_LARGER 16
+#define SEARCH_OP_SMALLER 17
+#define SEARCH_OP_SEQSET 18
+#define SEARCH_OP_UIDSET 19
+#define SEARCH_OP_AND 20
+#define SEARCH_OP_OR 21
+#define SEARCH_OP_NOT 22
+#define SEARCH_OP_MODSEQ 23 /* RFC 7162 SS3.1.5 */
+/* RFC 9051 SS6.4.4 content keys, answered by the parser-worker */
+#define SEARCH_OP_SUBJECT 24
+#define SEARCH_OP_HEADER 25
+#define SEARCH_OP_SENTBEFORE 26
+#define SEARCH_OP_SENTON 27
+#define SEARCH_OP_SENTSINCE 28
+#define SEARCH_OP_FROM 29
+#define SEARCH_OP_TO 30
+#define SEARCH_OP_CC 31
+#define SEARCH_OP_BCC 32
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, same "*"
- * convention as imsg_mbox_fetch;
- * store.c resolves it against live
- * idx.nlines (SEQSET) or highest
- * in-use UID (UIDSET) up front */
+ int op;
+ int64_t num;
+ uint32_t seq_lo;
uint32_t seq_hi;
int lo_is_star;
int hi_is_star;
- char keyword[SEARCH_KEYWORD_MAX]; /* KEYWORD/UNKEYWORD
- * operand */
+ char keyword[SEARCH_KEYWORD_MAX];
+ uint32_t str_off; /* into the pool */
+ uint32_t str_len;
+ uint32_t name_off;
+ uint32_t name_len;
};
struct imsg_mbox_search {
- uint32_t nnodes; /* number of struct search_node entries
- * in this imsg's trailing data */
+ uint32_t nnodes;
+ uint32_t poollen;
+ char pool[SEARCH_OPERANDS_MAX];
};
-/*
- * IMSG_SEARCH_PARSE_REQUEST's raw trailing bytes (already-
- * buffered SEARCH argument text, post RETURN/CHARSET) are sized
- * against this rather than left unbounded: generously bigger
- * than any single argument list could legitimately be, since
- * the whole command line it was sliced from is already capped
- * at listener.h's 8192-byte SESSION_INBUF_MAX.
- */
-#define SEARCH_ORACLE_ARGS_MAX 8192
+#define SEARCH_ARGS_MAX 8192
-/*
- * IMSG_SEARCH_PARSE_RESULT (search-oracle -> listener): reply to
- * IMSG_SEARCH_PARSE_REQUEST, echoing parse_search_key_list()'s own
- * (rc, errmsg) contract (search_cmd.c) -- rc == 0: nnodes valid,
- * struct search_node[nnodes] trails, same wire shape
- * imsg_mbox_search above already uses; errmsg unused. rc == -1:
- * BAD, errmsg set, nnodes/trailing data unused. rc == -2: NO,
- * errmsg set, same. Every errmsg parse_search_key_list() and its
- * helpers produce is a static string literal (search_cmd.c has no
- * runtime-formatted SEARCH parse error), so
- * SEARCH_ORACLE_ERRMSG_MAX only needs to cover the longest one,
- * with headroom. uses_modseq mirrors struct search_parse_ctx's
- * own field of the same name (search_cmd.c, opaque to every
- * caller outside that file) -- RFC 7162 SS3.1: a SEARCH
- * including the MODSEQ data item is a CONDSTORE-enabling
- * command. Valid only when rc == 0, same as nnodes; a
- * rejected parse never reaches search_dispatch()'s CONDSTORE
- * check.
- */
-#define SEARCH_ORACLE_ERRMSG_MAX 128
-struct imsg_search_parse_result {
- int rc; /* 0 ok, -1 BAD, -2 NO */
- /*
- * valid when rc == 0; struct search_node[nnodes] trails, same technique
- * as imsg_mbox_search above
- */
+/* rc: 0 ok, -1 BAD, -2 NO; errmsg is a static string */
+#define SEARCH_ERRMSG_MAX 128
+struct search_parse_result {
+ int rc;
uint32_t nnodes;
- int uses_modseq; /* valid when rc == 0, see this
- * struct's own comment */
- char errmsg[SEARCH_ORACLE_ERRMSG_MAX]; /* valid when
- * rc != 0 */
+ int uses_modseq;
+ char errmsg[SEARCH_ERRMSG_MAX];
+ uint32_t poollen;
+ char pool[SEARCH_OPERANDS_MAX];
};
struct imsg_mbox_search_match {
uint32_t seqno;
uint32_t uid;
- 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. */
+ uint64_t modseq; /* RFC 7162 SS3.1.6 */
};
-/*
- * RFC 9051 SS6.3.13 (IDLE). IMSG_MBOX_IDLE_REFRESH (listener -> store),
- * IMSG_MBOX_IDLE_EXPUNGE (store -> listener, one per untagged EXPUNGE to
- * print, in order), IMSG_MBOX_IDLE_FETCH (store -> listener, one per
- * message whose flags changed, carrying imsg_mbox_fetch_meta as FETCH and
- * QRESYNC resync do), IMSG_MBOX_IDLE_REFRESHED (terminal).
- *
- * SS6.3.13 names flag changes among what IDLE exists to report, and
- * requires an unsolicited FETCH to carry a UID item (SS7.5.2 repeats it).
- * Messages that arrived since the last refresh are reported by EXISTS
- * alone: their flags are news to nobody, and a client that wants them
- * asks.
- *
- * Sent once after "+ idling" with seed set, then once per "idle poll"
- * interval without it. store.c answers from refresh_index(), so a refresh
- * sees what a fresh SELECT would, new mail from an external MTA included.
- * Most polls cost two stat(2) calls and no lock; see index.c's
- * idle_probe_unchanged().
- *
- * The store child holds the UID list it last reported and compares in one
- * walk, so what crosses the socket is what to print rather than the whole
- * mailbox: a change to one message costs one imsg, not one per message.
- */
+/* RFC 9051 SS6.3.13 IDLE: seeded once, then a diff per poll */
struct imsg_mbox_idle_refresh {
- /*
- * Adopt the current state as the baseline and report nothing. Set
- * whenever the listener cannot vouch for what the client has already
- * been told: at "+ idling", since the mailbox may have changed while
- * the session was not idling.
- */
+ /* adopt the current state as baseline, report nothing */
int seed;
};
struct imsg_mbox_idle_expunge {
- uint32_t seqno; /* 1-based, already decremented for
- * the EXPUNGEs sent before it
- * (RFC 9051 SS7.5.1) */
+ uint32_t seqno;
};
struct imsg_mbox_idle_refreshed {
int ok;
- /*
- * Set when the store child's cheap probe found neither the mailbox
- * directory nor new/ touched since the last look, in which case no
- * IMSG_MBOX_IDLE_EXPUNGE messages preceded this one and every field
- * below is left zero and is meaningless.
- */
+ /* the probe saw no change; the fields below are zero */
int unchanged;
- int exists_changed; /* print exists as "* n EXISTS" */
+ int exists_changed;
uint32_t exists;
int busy; /* lock held elsewhere; ok says if it seeded */
};
-/*
- * 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
- * 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.4/SS6.3.5; store.c re-validates the name */
struct imsg_mbox_create {
char mailbox[MBOX_NAME_MAX];
};
char mailbox[MBOX_NAME_MAX];
};
-/*
- * RFC 9051 SS6.3.6 (RENAME). oldname/newname, not "mailbox"/"destination",
- * 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.
- */
+/* RFC 9051 SS6.3.6 RENAME */
struct imsg_mbox_rename {
char oldname[MBOX_NAME_MAX];
char newname[MBOX_NAME_MAX];
};
-/*
- * RFC 9051 SS6.3.9 (LIST). One IMSG_MBOX_LIST_ITEM per mailbox, unordered,
- * INBOX excluded; 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).
- *
- * subscribed_only picks WHICH set of names is streamed: 0 is every mailbox
- * on disk, 1 is every subscribed name, which SS6.3.9.1 says may include names
- * with no mailbox behind them. The store reads the subscription file only
- * when this is 1, so a plain LIST -- what a client sends on every connection
- * -- costs exactly what it did before subscriptions existed.
- */
+/* RFC 9051 SS6.3.9 LIST; unordered, INBOX excluded */
struct imsg_mbox_list {
int subscribed_only;
};
-/*
- * One mailbox name for the LIST above. `exists` is 0 only in a
- * subscribed_only stream, naming something subscribed with no mailbox on
- * disk: SS6.3.8 forbids dropping such a name from the list, and SS6.3.9.6's
- * Table 3 has the server report it rather than stay silent. Every item in a
- * subscribed_only stream is subscribed by construction, so no field says so.
- */
+/* exists 0: subscribed, no mailbox (RFC 9051 SS6.3.9.6) */
struct imsg_mbox_list_item {
char mailbox[MBOX_NAME_MAX];
int exists;
};
-/*
- * RFC 9051 SS6.3.7 (SUBSCRIBE) / SS6.3.8 (UNSUBSCRIBE). One struct for
- * both, as IMSG_MBOX_COPY and IMSG_MBOX_MOVE already share imsg_mbox_copy.
- * Terminal reply is imsg_mbox_result, only "error" meaningful.
- */
+/* RFC 9051 SS6.3.7 SUBSCRIBE and SS6.3.8 UNSUBSCRIBE */
struct imsg_mbox_subscribe {
char mailbox[MBOX_NAME_MAX];
};
int cmdline_symset(char *);
/* parent.c */
-/* 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 *, each
- * gets exactly the config it needs over its fd-3 channel instead:
- * IMSG_LISTENER_SESSION_INIT, IMSG_AUTH_INIT, IMSG_STORE_INIT respectively.
- * search_oracle.c needs no config at all -- see its own comment.
- */
+/* each role has its own file; its config arrives over fd 3 */
__dead void listener_main(void);
__dead void auth_main(void);
__dead void store_main(void);
__dead void keymgr_main(void);
-__dead void search_oracle_main(void);
+__dead void parser_main(void);
-/*
- * search_oracle.c's one entry point into search_cmd.c's otherwise-
- * private struct search_parse_ctx, see search_oracle_parse()'s own
- * comment (search_cmd.c). Declared here, not in listener.h, on
- * purpose: search_oracle.c is a separate role, not part of listener.h's
- * listener.c/auth_cmd.c/mailbox_cmd.c/append_cmd.c/fetch_cmd.c/
- * search_cmd.c/store_cmd.c/store_ipc.c family, and pulling that whole
- * header in for one prototype is what caused search_oracle.c's own
- * file-scope `static struct imsgev iev_parent` to collide with
- * listener.h's unrelated `extern struct imsgev iev_parent` (listener's
- * own long-lived fd-3 channel) -- same identifier, incompatible
- * linkage, a real build failure on premio. search_cmd.c already
- * includes this header too, so its own view of the prototype is
- * unchanged by the move.
- */
-int search_oracle_parse(char *, struct search_node *, uint32_t *,
- int *, char *, size_t);
-
-/* imsg helpers shared by all roles. The "handler" passed to
- * imsgev_init() is the libevent callback, expected to run its own
- * imsgbuf_read()/imsgbuf_get() loop (parent_dispatch_child() is the
- * reference shape), and to end with imsgev_rearm_read().
- */
+/* imsgev.c */
void imsgev_ibuf_init(struct imsgbuf *, int);
void imsgev_init(struct imsgev *, int,
void (*)(int, short, void *), void *);
void imsgev_init_from_ibuf(struct imsgev *, const struct imsgbuf *,
void (*)(int, short, void *), void *);
-/* You do NOT need to call this after an imsg_compose(). imsgev_init() installs
- * imsgev_on_compose() as the channel's imsg close callback, so libutil arms
- * EV_WRITE from inside imsg_close() on every queueing path. This is now only
- * for the rare caller that must arm a channel it did not just compose on. */
void imsgev_add(struct imsgev *);
-/* Same work as imsgev_add(), deliberately under a different name: this is
- * the one a dispatch handler MUST call before returning. See its definition
- * in imsgev.c for why the two are spelled apart. */
+/* a dispatch handler must call this before returning */
void imsgev_rearm_read(struct imsgev *);
-/* 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 *);
+int setup_recv_one_peer_id(struct imsgbuf *, uint32_t *);
void setup_recv_done_and_ack(struct imsgbuf *);
#endif /* IMAPD_H */
blob - 52f95b54c764213581617143650d8138ecb2e94b
blob + 4abc10cf7e54c09814b8651cf7a107c6d0ec0c43
--- src/imsgev.c
+++ src/imsgev.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * Shared imsgbuf+event(3) wrapper for parent/listener/auth/store, built on the
- * current imsgbuf_*() API (imsg_get() and friends were removed upstream);
- * imsgbuf_get()'s 1/0/-1 return is handled exactly as before.
- */
-
#include <sys/types.h>
#include <event.h>
#include "imapd.h"
#include "log.h"
-/*
- * Sets imapd-wide imsgbuf settings for every channel: imsgbuf_set_maxsize()
- * raises the whole-message limit by IMSG_HEADER_SIZE since its argument is
- * payload-only, and imsgbuf_allow_fdpass() is needed since the parent fd-passes
- * on these channels at spawn time.
- */
void
imsgev_ibuf_init(struct imsgbuf *ibuf, int fd)
{
imsgbuf_allow_fdpass(ibuf);
}
-/*
- * Arms EV_WRITE via libutil's imsg_close() callback so every queued message
- * gets it exactly once; the early return skips re-arming once EV_WRITE is
- * already pending mid-batch.
- */
static void
imsgev_on_compose(struct imsgbuf *ibuf, void *arg)
{
event_set(&iev->ev, fd, iev->events, iev->handler, iev->data);
event_add(&iev->ev, NULL);
- /*
- * Must follow event_set()/event_add() (callback touches iev->ev) and
- * imsgev_ibuf_init() (imsgbuf_init() memset()s the struct); not done
- * inside imsgev_ibuf_init() itself since roles call it pre-event-loop
- * on fd 3.
- */
+ /* after event_set() and imsgbuf_init() */
imsgbuf_set_userdata(&iev->ibuf, iev);
imsgbuf_set_close_callback(&iev->ibuf, imsgev_on_compose);
}
-/* like imsgev_init(), but copies an already-init'd *ibuf (no re-init) */
void
imsgev_init_from_ibuf(struct imsgev *iev, const struct imsgbuf *ibuf,
void (*handler)(int, short, void *), void *data)
imsgbuf_set_close_callback(&iev->ibuf, imsgev_on_compose);
}
-/* re-arm after imsg_compose(); adds EV_WRITE if output is queued */
void
imsgev_add(struct imsgev *iev)
{
event_add(&iev->ev, NULL);
}
-/*
- * Re-arms EV_READ (which imsgev_init() sets without EV_PERSIST, so it drops
- * after firing) at the end of every dispatch handler, delegating to
- * imsgev_add() -- kept as a separate name for clarity, not different behavior.
- */
+/* EV_READ lacks EV_PERSIST: every handler must re-arm it */
void
imsgev_rearm_read(struct imsgev *iev)
{
imsgev_add(iev);
}
-/*
- * blocks for one IMSG_SETUP_PEER, returns its fd-passed fd; imsgbuf_get()
- * checked before imsgbuf_read() to avoid coalesced-message stalls
- */
+/* imsgbuf_get() before imsgbuf_read(): replies may coalesce */
int
-setup_recv_one_peer(struct imsgbuf *ibuf3)
+setup_recv_one_peer_id(struct imsgbuf *ibuf3, uint32_t *id_out)
{
struct imsg imsg;
ssize_t n;
if ((fd = imsg_get_fd(&imsg)) == -1)
fatalx("setup_recv_one_peer: IMSG_SETUP_PEER carried no fd");
+ if (id_out != NULL)
+ *id_out = imsg_get_id(&imsg);
+
imsg_free(&imsg);
return (fd);
}
-/*
- * blocks for IMSG_SETUP_DONE, then sends one back as an ack (see
- * setup_recv_one_peer() re: imsgbuf_get() ordering)
- */
+int
+setup_recv_one_peer(struct imsgbuf *ibuf3)
+{
+ return (setup_recv_one_peer_id(ibuf3, NULL));
+}
+
void
setup_recv_done_and_ack(struct imsgbuf *ibuf3)
{
blob - 294bf25f0d4dfad1fb3c5ff997f48046af9e8abe
blob + cf71d95cf2cc0880c154fda71df483e955db9cae
--- src/index.c
+++ src/index.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* index.c: maildir index format -- load/save/append, QRESYNC, vanished-UID. */
#include <sys/types.h>
#include <sys/file.h>
#include "log.h"
#include "store_internal.h"
-/*
- * Index lines are colon-delimited text, so no field may contain ':', CR, or LF
- * -- centralized here since keywords-field callers bypass index_append() and
- * hand-build lines.
- */
-/*
- * RFC 7162 SS7 bounds a mod-sequence to a positive 63-bit integer; values read
- * back from the index are bounded the same way the wire-facing parsers already
- * are.
- */
+/* no ':', CR or LF in any index field */
+/* RFC 7162 SS7 */
#define INDEX_MODSEQ_MAX INT64_MAX
int
return (field != NULL && strpbrk(field, ":\r\n") == NULL);
}
-/*
- * A basename read from the index is pasted into paths for
- * open(2)/stat(2)/rename(2); unveil(2) only stops it leaving the maildir, so
- * load-time enforces the same format rules as the write side.
- */
+/* unveil(2) only keeps a path inside the maildir */
int
index_basename_valid(const char *basename)
{
const unsigned char *p;
- /*
- * Strictly stronger than index_field_valid(): whatever is unsafe to
- * write into a line is also unsafe to paste into a path.
- */
if (!index_field_valid(basename))
return (0);
/* excludes "", ".", "..", and dotfiles in one test */
return (1);
}
-/*
- * Grows idx->lines by doubling (from 16) when it is full; index_load() and
- * index_append() carried byte-identical copies of this block, so the growth
- * policy and its failure log now live in one place. Returns 0 when there is
- * room for one more line, -1 on allocation failure (already logged).
- */
static int
index_lines_grow(struct mbox_index *idx)
{
}
while (fgets(line, sizeof(line), fp) != NULL) {
- /*
- * fgets(3) silently splits an over-long line; peek at the next
- * byte to distinguish a legal max-length line (next byte is
- * '\n' or EOF) from an actual split record.
- */
+ /* fgets(3) splits an over-long line */
if (strchr(line, '\n') == NULL &&
strlen(line) == sizeof(line) - 1) {
int c = fgetc(fp);
goto fail;
}
*colon = '\0';
- /*
- * Each header field is digits-or-nothing:
- * strtoul(3)/strtoull(3) accept leading whitespace and
- * a sign, so unguarded input like "-1:1:1" would
- * silently parse into a bogus value; same guard used
- * elsewhere. The two uint32 fields are range-checked
- * before narrowing as well: RFC 9051 SS2.3.1.1 makes
- * UIDVALIDITY and UIDNEXT non-zero 32-bit values, and
- * "4294967296" would otherwise truncate to 0 and
- * "4294967297" to 1, the second handing out UIDs that
- * are already in use.
- */
+ /* strtoul(3) accepts leading space and a sign */
if (line[0] < '0' || line[0] > '9') {
log_warnx("session %u: malformed "
"UIDVALIDITY: %s", session_id, line);
}
idx->uidvalidity = (uint32_t)parsed;
- /*
- * RFC 7162: optional third field HIGHESTMODSEQ; NULL
- * means older two-field header, defaults to 1
- */
+ /* RFC 7162: optional HIGHESTMODSEQ; absent means 1 */
if ((colon2 = strchr(colon + 1, ':')) != NULL)
*colon2 = '\0';
return (0);
fail:
- /*
- * idx may hold partially-allocated lines here; index_free() is a safe
- * no-op, making "-1 means idx is already freed" true for every caller
- * including refresh_index().
- */
index_free(idx);
fclose(fp);
return (-1);
}
-/* Parses "UID:basename:keywords[:MODSEQ]"; -1 (logged) on corrupt line. */
int
index_parse_line(const char *line, struct index_rec *rec)
{
memset(rec, 0, sizeof(*rec));
- /*
- * strtoul(3) accepts leading whitespace and a sign, so an unguarded UID
- * field could parse ":x:y:1" as 0 or "-1:x:y:1" as 4294967295; the
- * field must be digits-or-nothing.
- */
if (line[0] < '0' || line[0] > '9') {
log_warnx("session %u: corrupt index line (UID field is not "
"a decimal number)", session_id);
return (-1);
}
- /*
- * Narrowed only after the range test, the same four-part form the
- * UIDVALIDITY floor read uses: a value strtoul(3) accepts but a
- * uint32_t cannot hold was truncating, so "4294967296" became UID 0.
- */
errno = 0;
parsed = strtoul(line, &ep, 10);
if (*ep != ':' || errno != 0 || parsed > UINT32_MAX) {
}
memcpy(rec->basename, p, (size_t)(q - p));
rec->basename[q - p] = '\0';
- /*
- * Refuses traversal, hidden names, and control bytes before this
- * basename is pasted into open(2)/stat(2)/rename(2) paths, since
- * unveil(2) only stops paths leaving the maildir; logged by UID, never
- * by the untrusted basename itself.
- */
if (!index_basename_valid(rec->basename)) {
log_warnx("session %u: refusing index line with unsafe "
"basename (UID %u)", session_id, rec->uid);
memcpy(rec->keywords, p, (size_t)(r - p));
rec->keywords[r - p] = '\0';
- /*
- * Same digit-or-nothing guard as the UID field, now applied to
- * MODSEQ: RFC 7162 SS7 bounds it at 9,223,372,036,854,775,807,
- * but strtoull(3)'s sign handling would otherwise turn "-1"
- * into 18446744073709551615 and leak into
- * CHANGEDSINCE/UNCHANGEDSINCE and client-visible MODSEQ.
- */
if (r[1] < '0' || r[1] > '9') {
log_warnx("session %u: malformed per-message MODSEQ "
"in index line: %s", session_id, line);
return (-1);
}
} else {
- /*
- * no MODSEQ field: pre-CONDSTORE line (index_rec
- * backward-compat)
- */
+ /* a pre-CONDSTORE line */
if (strlcpy(rec->keywords, p, sizeof(rec->keywords)) >=
sizeof(rec->keywords)) {
log_warnx("session %u: keywords too long in index "
return (0);
}
-/*
- * Highest UID of a *present* message (idx->lines is UID-ascending), 0 if none;
- * this is "*" for SEARCH/FETCH/STORE/EXPUNGE, not uidnext-1.
- */
+/* "*" is the highest present UID, not uidnext-1 */
uint32_t
index_max_uid(struct mbox_index *idx)
{
if (idx->nlines == 0)
return (0);
- /*
- * One parser for the UID field, not two: the hand-rolled strtoul(3)
- * that used to live here had neither guard and read "-1:name::1" as
- * 4294967295. A line index_parse_line() rejects is skipped as a
- * message by every walker of idx->lines, so it has no present UID
- * for this to return; 0 means "no UIDs in use", as before.
- */
if (index_parse_line(idx->lines[idx->nlines - 1], &rec) == -1)
return (0);
return (rec.uid);
}
-/*
- * Resolves "*" entries in a parsed sequence-set against max (index_max_uid()
- * for UID requests, idx->nlines for sequence-number requests); swaps any
- * backwards "*"-involving range per RFC 9051 SS9 (since parse_one_seq_range()
- * can't), then clamps lo up to 1 and, when clamp_hi is set, hi down to max,
- * keeping every range including degenerate ones that seqset_contains()
- * correctly treats as unmatchable.
- */
uint32_t
seqset_resolve(const struct seq_range *ranges, uint32_t nranges,
uint32_t max, int clamp_hi, struct seq_range resolved[SEQSET_MAX_RANGES])
return (n);
}
-/* True if val is in any nresolved [lo, hi] pair from seqset_resolve() above. */
int
seqset_contains(const struct seq_range *resolved, uint32_t nresolved,
uint32_t val)
return (0);
}
-/*
- * Highest hi across all resolved ranges (0 if none), letting an ascending scan
- * of idx->lines break early once past it, same as a single-range scan already
- * did.
- */
uint32_t
seqset_max_hi(const struct seq_range *resolved, uint32_t nresolved)
{
return (max);
}
-/*
- * The "does this command apply to this message?" rule for FETCH/STORE/COPY, in
- * one place: RFC 9051 SS6.4.9 makes a UID command's sequence-set UID-space and
- * a bare one position-space, so the caller passes both and by_uid picks.
- * PAST_END is a stop signal, valid only because those three walk idx->lines in
- * ascending order -- the compaction loops in
- * move_same_mailbox()/handle_mbox_expunge() deliberately don't use this, since
- * breaking early would leave the surviving lines they still have to copy down
- * unwritten.
- */
+/* RFC 9051 SS6.4.9: a UID command's set is in UID space */
enum seqset_pos
seqset_position(const struct seq_range *resolved, uint32_t nresolved,
uint32_t max_hi, int by_uid, uint32_t uid, uint32_t seqno)
return (SEQSET_MATCH);
}
-/* Reports UID in [lo, hi] absent from idx as VANISHED; RFC 7162 SS3.2.6. */
+/* RFC 7162 SS3.2.6 */
void
send_vanished_range(const struct mbox_index *idx, uint32_t lo, uint32_t hi,
struct imsgev *iev)
"IMSG_MBOX_SELECT_VANISHED", session_id);
}
- /*
- * Stop here: a UID of UINT32_MAX would wrap to 0 and make the
- * tail check trivially true, emitting a VANISHED range that
- * wrongly claims every message in the mailbox is gone.
- */
+ /* UINT32_MAX would wrap to 0 */
if (rec.uid == UINT32_MAX)
return;
want = rec.uid + 1;
}
}
-/* Linear scan for a basename in the index; O(n), fine at modest size. */
int
index_has_basename(struct mbox_index *idx, const char *basename)
{
return (0);
}
-/* Appends "UID:basename::MODSEQ"; caller owns uidnext, bumps modseq (SS3.1). */
int
index_append(struct mbox_index *idx, uint32_t uid, const char *basename)
{
char line[STORE_INDEX_LINE_MAX];
int len;
- /*
- * RFC 9051 SS9 forbids UID 0; with no ceiling on uidnext, exhaustion
- * would wrap it to 0 and silently reuse in-use UIDs (forbidden by
- * SS2.3.1.1, and breaking index_max_uid()'s ascending assumption), so
- * refuse here instead -- the RFC's real fix, changing UIDVALIDITY,
- * needs persistent state not yet kept (see index.c review's finding
- * #1).
- */
+ /* RFC 9051 SS9 forbids UID 0; refuse rather than wrap */
if (uid == 0) {
log_warnx("session %u: refusing index entry with UID 0 "
"(uidnext exhausted or index header corrupt)", session_id);
return (-1);
}
- /*
- * defense in depth: refuse a basename containing ':' or newline
- * (refresh_index() already pre-skips these)
- */
if (strpbrk(basename, ":\r\n") != NULL) {
log_warnx("session %u: refusing index entry with unsafe "
"basename: %s", session_id, basename);
return (0);
}
-/*
- * Rewrites index to STORE_INDEX_TMP_NAME, rename(2)s over STORE_INDEX_NAME so a
- * reader never sees a torn file.
- */
+/* rename(2), so a reader never sees a torn file */
int
index_save(int dfd, const struct mbox_index *idx)
{
int fd;
size_t i;
- /*
- * O_EXCL so a pre-planted symlink can't be followed; unlink any stale
- * temp from a prior crash first
- */
+ /* O_EXCL: never follow a planted symlink */
if (unlinkat(dfd, STORE_INDEX_TMP_NAME, 0) == -1 &&
errno != ENOENT) {
log_warn("session %u: unlink %s", session_id,
memset(idx, 0, sizeof(*idx));
}
-/*
- * RFC 7162 SS3.2.5.1 QRESYNC resync: streams VANISHED ranges then FETCH_META
- * for messages with modseq > qresync_modseq.
- */
+/* RFC 7162 SS3.2.5.1 QRESYNC resync */
void
qresync_send_resync(const struct imsg_mbox_select *req,
const struct seq_range *ranges, uint32_t nranges,
- struct mbox_index *idx, struct imsgev *iev)
+ 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;
if (req->qresync_has_uids) {
- /*
- * known-uids is a full RFC 9051 SS9 sequence-set resolved the
- * same way as any UID-space consumer; max is unused since "*"
- * is already rejected upstream, and clamp_hi is 0 because a
- * known UID above the current highest is exactly what RFC 7162
- * SS3.2.5.1 wants reported VANISHED, not dropped.
- */
nresolved = seqset_resolve(ranges, nranges,
index_max_uid(idx), 0, resolved);
} else {
- /*
- * SS3.2.5.1: no known-uids means "1:<uidnext-1>", empty if
- * uidnext == 1
- */
+ /* RFC 7162 SS3.2.5.1: no known-uids means 1:uidnext-1 */
if (idx->uidnext <= 1)
return;
resolved[0].lo = 1;
nresolved = 1;
}
- /*
- * RFC 7162 SS3.2.6 requires VANISHED (EARLIER) precede FETCH; ordering
- * is guaranteed by store_ipc.c's session_handle_mbox_selected(), which
- * buffers and flushes VANISHED before FETCH, the same two-pass split
- * and helper handle_mbox_fetch() uses.
- */
for (i = 0; i < nresolved; i++)
send_vanished_range(idx, resolved[i].lo, resolved[i].hi, iev);
struct index_rec rec;
if (index_parse_line(idx->lines[i], &rec) == -1)
- /*
- * corrupt line, already logged, not reported either way
- */
+ /* corrupt line, already logged */
continue;
if (rec.uid > max_hi)
break;
meta.seqno = i + 1;
meta.uid = rec.uid;
meta.modseq = rec.modseq;
- if (locate_message_file(mailbox_dir_fd, rec.basename,
- &size, suffix, sizeof(suffix)) == 0) {
+ if (locate_message_file(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename, &size, suffix,
+ sizeof(suffix)) == 0) {
build_flags_string(suffix, rec.keywords,
meta.flags, sizeof(meta.flags));
}
}
}
-/*
- * Takes the index lock (LOCK_EX/LOCK_SH) on STORE_INDEX_LOCK_NAME's stable
- * inode before opening the index, so the descriptor can't refer to an inode a
- * concurrent index_save() already renamed away; release with the idempotent
- * index_lock_release().
- *
- * Returns 0 holding the lock, or -1 having taken nothing. A caller that
- * added LOCK_NB can also get 1, meaning another process holds it: an
- * ordinary answer rather than a failure, so it is not logged, and every
- * blocking caller is unaffected because flock(2) cannot report EWOULDBLOCK
- * without LOCK_NB (flock(2), sys/kern/kern_descrip.c).
- */
+/* lock first, then open, so the fd names the current inode */
int
index_lock_acquire(int dfd, struct index_lock *il, int op)
{
return (0);
}
-/*
- * Drops whatever index_lock_acquire() took; safe to call twice, and safe on an
- * INDEX_LOCK_INIT struct that was never acquired.
- */
void
index_lock_release(struct index_lock *il)
{
}
}
-/*
- * Issues and records a new UIDVALIDITY: RFC 9051 SS2.3.1.1 requires it strictly
- * increase, so the timestamp is only a floor -- the value returned is
- * max(clock, last-issued+1), the high-water mark is persisted at the maildir
- * root (STORE_UIDVALIDITY_NAME, since per-mailbox state is gone after DELETE),
- * and every failure degrades to a bare timestamp except an
- * unparseable-but-present file, which is left untouched rather than overwritten
- * with a lower floor.
- */
+/* RFC 9051 SS2.3.1.1: it must increase, so time is only a floor */
uint32_t
uidvalidity_next(void)
{
ssize_t n;
int fd, writeback = 1;
- /* one file per account, at the maildir root */
path = STORE_UIDVALIDITY_NAME;
now = time(NULL);
return (val != 0 ? val : 1);
}
- /*
- * A zero-length file is the ordinary just-created case (floor 0 is
- * correct); anything present but unreadable is damage and is left
- * alone.
- */
if (fstat(fd, &st) == 0 && st.st_size > 0) {
if ((n = read(fd, buf, sizeof(buf) - 1)) <= 0) {
log_warn("session %u: read %s (UIDVALIDITY floor)",
} else {
buf[n] = '\0';
buf[strcspn(buf, "\r\n")] = '\0';
- /*
- * same digit guard as the index header: strtoul(3)
- * accepts a leading sign, so "-1" would read as
- * 4294967295 and pin the floor at its ceiling
- */
errno = 0;
parsed = strtoul(buf, &ep, 10);
if (buf[0] < '0' || buf[0] > '9' || *ep != '\0' ||
if (writeback) {
if (val <= floor) {
if (floor == UINT32_MAX) {
- /*
- * 4 billion issued, or clock past 2106: nothing
- * greater representable.
- */
log_warnx("session %u: UIDVALIDITY floor is "
"exhausted (%u); reusing it", session_id,
floor);
"may be issued again", session_id, path);
}
- /*
- * A successful floor consultation is otherwise silent, and a
- * quietly-wrong UIDVALIDITY looks identical to a correct one to the
- * client; -v logs what was read and what was issued.
- */
log_debug("session %u: UIDVALIDITY: floor %u in %s -> issued %u%s",
session_id, floor, path, val,
writeback ? "" : " (floor NOT updated)");
return (val);
}
-/*
- * Walks new/ for undiscovered maildir deliveries: mutate==0 only answers "is
- * there at least one?" without touching idx or the filesystem (safe under a
- * shared lock, used by the frequent IDLE poll); mutate==1 indexes everything
- * found and needs the exclusive lock. Returns 1 (found/added), 0, or -1 on
- * error -- on error with mutate set, idx is already index_free()'d, per
- * refresh_index()'s contract.
- */
static int
index_scan_new(int dfd, struct mbox_index *idx, int mutate)
{
}
while ((de = readdir(dp)) != NULL) {
if (de->d_name[0] == '.')
- /*
- * ".", "..", and dotfiles -- maildir delivery never
- * creates the latter
- */
+ /* ".", "..", and dotfiles */
continue;
- /*
- * never index a filename with ':' or newline, corrupts index
- * line format
- */
+ /* ':' or a newline would corrupt the index line */
if (strpbrk(de->d_name, ":\r\n") != NULL) {
- /*
- * Logged only on the mutating pass -- the read-only
- * pass runs every poll interval for an IDLE's whole
- * life, and a badly-named file would otherwise fill the
- * log forever.
- */
if (mutate)
log_warnx("session %u: skipping new/ file "
"with unsafe name (contains ':' or "
continue;
if (!mutate) {
closedir(dp);
- /* one is enough to answer the question */
return (1);
}
if (index_append(idx, idx->uidnext, de->d_name) == -1) {
return (added);
}
-/*
- * Loads the index (fd already flock(2)'d LOCK_EX) and indexes any new/ files
- * not yet known; on failure idx is already index_free()'d.
- */
int
refresh_index(int dfd, struct mbox_index *idx, int fd)
{
if ((added = index_scan_new(dfd, idx, 1)) == -1)
return (-1); /* index_scan_new() has already freed idx */
- /*
- * Skip index_save()'s cost on a no-op refresh, unless the header itself
- * is new -- a freshly invented UIDVALIDITY that's never written down
- * would just be invented again, differently, next call.
- */
+ /* a fresh UIDVALIDITY must still be written down */
if (!added && !idx->fresh)
return (0);
return (0);
}
-/*
- * Cheap change probe: two stat(2) calls (no lock, no read) on "." (moved by
- * every index_save()-based mutation: APPEND/STORE/EXPUNGE/COPY/MOVE) and "new"
- * (touched by an external MTA delivery before anything indexes it); sampled
- * before the caller's work so a change is never missed, at the cost of one
- * harmless extra refresh, modulo theoretical same-nanosecond races.
- */
-static struct {
- int valid;
- ino_t dir_ino;
- ino_t new_ino;
- struct timespec dir_mtim;
- struct timespec new_mtim;
-} idle_probe;
-
-/* Forces next probe to report a change; call whenever cwd changes mailbox. */
void
-idle_probe_reset(void)
+idle_probe_reset(struct store_session *ss)
{
- idle_probe.valid = 0;
+ ss->idle_probe.valid = 0;
}
-/*
- * The UID list this session was last told about, ascending, and what an IDLE
- * refresh diffs against. It lives here rather than in the listener so that a
- * change to one message costs one imsg instead of one per message in the
- * mailbox, and one walk instead of a rescan per message. About 4 bytes per
- * message.
- */
-static struct {
- uint32_t *uids;
- size_t n;
- uint64_t modseq; /* highest reported; above it is news */
- int valid;
-} idle_baseline;
-
-/* Forces the next refresh to seed rather than diff; pairs with the above. */
void
-idle_baseline_reset(void)
+idle_baseline_reset(struct store_session *ss)
{
- free(idle_baseline.uids);
- idle_baseline.uids = NULL;
- idle_baseline.n = 0;
- idle_baseline.modseq = 0;
- idle_baseline.valid = 0;
+ struct store_idle_baseline *base = &ss->idle_baseline;
+
+ free(base->uids);
+ base->uids = NULL;
+ base->n = 0;
+ base->modseq = 0;
+ base->valid = 0;
}
-/*
- * One untagged EXPUNGE per UID that went away, in order; returns how many.
- *
- * RFC 9051 SS7.5.1: each EXPUNGE decrements the sequence numbers above it, so
- * seqno counts only messages still present. Both lists are UID-ascending (see
- * index_max_uid()), so one walk does it.
- */
+/* RFC 9051 SS7.5.1: each EXPUNGE renumbers those above it */
static size_t
idle_send_expunges(const uint32_t *old, size_t oldn, const uint32_t *cur,
size_t curn, struct imsgev *iev)
return (gone);
}
-/*
- * One untagged FETCH per message whose mod-sequence passed what was last
- * reported; returns how many. RFC 9051 SS6.3.13 lists flag changes among
- * what IDLE reports and requires an unsolicited FETCH to carry a UID item.
- *
- * Only messages the client already knows about: one that arrived since the
- * last refresh is not in old, and EXISTS is all it gets. Flags live in the
- * message file's name, so each one reported costs a lookup, which is why
- * this walks the changed messages and not the mailbox.
- */
+/* RFC 9051 SS6.3.13: flag changes, as FETCH with UID */
static size_t
idle_send_flag_fetches(const struct mbox_index *idx, const uint32_t *old,
- size_t oldn, uint64_t since, struct imsgev *iev)
+ size_t oldn, uint64_t since, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct imsg_mbox_fetch_meta meta;
struct index_rec rec;
char suffix[64];
j++;
if (j == oldn || old[j] != rec.uid)
continue; /* arrived since; EXISTS covers it */
- if (locate_message_file(mailbox_dir_fd, rec.basename, &size,
- suffix, sizeof(suffix)) == -1) {
+ if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+ rec.basename, &size, suffix, sizeof(suffix)) == -1) {
log_warnx("session %u: message %s (uid %u) indexed "
"but missing on disk, no IDLE flag push",
session_id, rec.basename, rec.uid);
return (a->tv_sec == b->tv_sec && a->tv_nsec == b->tv_nsec);
}
-/* 1 = nothing can have changed since the last call; 0 = look properly. */
static int
-idle_probe_unchanged(void)
+idle_probe_unchanged(struct store_session *ss)
{
- struct stat dst, nst;
- int same;
+ struct store_idle_probe *probe = &ss->idle_probe;
+ struct stat dst, nst;
+ int same;
- if (fstatat(mailbox_dir_fd, ".", &dst, 0) == -1) {
- /*
- * Cannot tell, so do not claim to know: fall through to the
- * full refresh, which will report the failure properly.
- */
- idle_probe.valid = 0;
+ if (fstatat(ss->mailbox_dir_fd, ".", &dst, 0) == -1) {
+ probe->valid = 0;
return (0);
}
- if (fstatat(mailbox_dir_fd, "new", &nst, 0) == -1)
+ if (fstatat(ss->mailbox_dir_fd, "new", &nst, 0) == -1)
/* absent new/ is a stable state */
memset(&nst, 0, sizeof(nst));
- same = idle_probe.valid &&
- dst.st_ino == idle_probe.dir_ino &&
- nst.st_ino == idle_probe.new_ino &&
- tspec_eq(&dst.st_mtim, &idle_probe.dir_mtim) &&
- tspec_eq(&nst.st_mtim, &idle_probe.new_mtim);
+ same = probe->valid &&
+ dst.st_ino == probe->dir_ino &&
+ nst.st_ino == probe->new_ino &&
+ tspec_eq(&dst.st_mtim, &probe->dir_mtim) &&
+ tspec_eq(&nst.st_mtim, &probe->new_mtim);
- idle_probe.valid = 1;
- idle_probe.dir_ino = dst.st_ino;
- idle_probe.new_ino = nst.st_ino;
- idle_probe.dir_mtim = dst.st_mtim;
- idle_probe.new_mtim = nst.st_mtim;
+ probe->valid = 1;
+ probe->dir_ino = dst.st_ino;
+ probe->new_ino = nst.st_ino;
+ probe->dir_mtim = dst.st_mtim;
+ probe->new_mtim = nst.st_mtim;
return (same);
}
-/*
- * The UIDs an IDLE baseline records, in index order, and the mod-sequence
- * to diff from next time. Returns -1, having allocated nothing, on failure.
- */
static int
idle_uid_list(const struct mbox_index *idx, uint32_t **listp, size_t *np,
uint64_t *modseqp)
uint64_t modseq = 0;
size_t i, n = 0;
- /*
- * nlines + 1 so that an empty mailbox still asks for a nonzero
- * allocation, which keeps a NULL return meaning failure and nothing
- * else.
- */
if ((list = reallocarray(NULL, idx->nlines + 1, sizeof(*list))) ==
NULL) {
log_warn("session %u: idle refresh: reallocarray", session_id);
}
for (i = 0; i < idx->nlines; i++) {
if (index_parse_line(idx->lines[i], &rec) == -1)
- /* skip malformed line, don't fail the whole request */
continue;
list[n++] = rec.uid;
if (rec.modseq > modseq)
modseq = rec.modseq;
}
- /*
- * The header's HIGHESTMODSEQ is what a client is told, but a
- * per-message value above it would then never be reported again, so
- * take whichever is greater as the mark for next time. A
- * pre-CONDSTORE index line parses with modseq defaulted to 1, as
- * index_parse_line does, which a header of 0 would otherwise make
- * look like a change on every refresh.
- */
if (idx->highestmodseq > modseq)
modseq = idx->highestmodseq;
return (0);
}
-/*
- * Seeds the IDLE baseline from the committed index without its lock, for
- * an IDLE that found the lock busy. index_save() only ever replaces the
- * index whole, by rename(2), so this reads one committed version, never a
- * torn one. Deliveries still in new/ are left for a later refresh to
- * index and report. Returns 0 seeded, with the count in reply, or -1.
- */
+/* lockless: index_save() only ever replaces the index by rename(2) */
static int
-idle_seed_unlocked(struct imsg_mbox_idle_refreshed *reply)
+idle_seed_unlocked(struct store_session *ss,
+ struct imsg_mbox_idle_refreshed *reply)
{
- struct mbox_index idx;
- uint32_t *list;
- uint64_t modseq;
- size_t n;
- int fd;
+ struct store_idle_baseline *base = &ss->idle_baseline;
+ struct mbox_index idx;
+ uint32_t *list;
+ uint64_t modseq;
+ size_t n;
+ int fd;
- if ((fd = openat(mailbox_dir_fd, STORE_INDEX_NAME, O_RDONLY)) == -1) {
+ if ((fd = openat(ss->mailbox_dir_fd, STORE_INDEX_NAME,
+ O_RDONLY)) == -1) {
if (errno != ENOENT)
log_warn("session %u: open %s", session_id,
STORE_INDEX_NAME);
}
index_free(&idx);
- free(idle_baseline.uids);
- idle_baseline.uids = list;
- idle_baseline.n = n;
- idle_baseline.modseq = modseq;
- idle_baseline.valid = 1;
+ free(base->uids);
+ base->uids = list;
+ base->n = n;
+ base->modseq = modseq;
+ base->valid = 1;
reply->exists = (uint32_t)n;
return (0);
}
-/* RFC 9051 SS6.3.4-SS6.3.6/SS6.3.9; re-checked vs listener.c (privsep). */
void
handle_mbox_idle_refresh(const struct imsg_mbox_idle_refresh *req,
- struct imsgev *iev)
+ struct store_session *ss)
{
+ struct store_idle_baseline *base = &ss->idle_baseline;
+ struct imsgev *iev = &ss->iev;
struct mbox_index idx;
struct imsg_mbox_idle_refreshed reply;
struct index_lock il = INDEX_LOCK_INIT;
memset(&reply, 0, sizeof(reply));
/* a seed adopts what it finds rather than reporting it */
- seeded = req->seed || !idle_baseline.valid;
+ seeded = req->seed || !base->valid;
- /*
- * Cheapest question first: on an untouched mailbox this is the whole
- * job, with no lock and no index read, which matters since the poll
- * runs every few seconds.
- */
- if (idle_probe_unchanged()) {
+ if (idle_probe_unchanged(ss)) {
reply.ok = 1;
reply.unchanged = 1;
- /*
- * The poll mechanism is otherwise silent and a dead one is
- * indistinguishable from a healthy one (as the cross-session
- * push bug showed); at -v these lines make each poll and any
- * real work observable.
- */
log_debug("session %u: idle refresh: unchanged (probe: no "
"change to . or new/)", session_id);
goto send;
}
- /*
- * Something moved, so the index must be read; take the shared lock
- * since reading alone covers the common case, and escalate only when
- * new/ actually holds a delivery to index.
- */
- locked = index_lock_acquire(mailbox_dir_fd, &il, LOCK_SH | LOCK_NB);
+ locked = index_lock_acquire(ss->mailbox_dir_fd, &il, LOCK_SH | LOCK_NB);
if (locked == 1)
goto busy;
if (locked == -1)
index_lock_release(&il);
goto send;
}
- if ((pending = index_scan_new(mailbox_dir_fd, &idx, 0)) == -1) {
+ if ((pending = index_scan_new(ss->mailbox_dir_fd, &idx, 0)) == -1) {
index_free(&idx);
index_lock_release(&il);
goto send;
}
- /*
- * idx.fresh joins pending here: index_load() just invented a
- * UIDVALIDITY for a header-less mailbox, and persisting it needs the
- * exclusive lock just as indexing a delivery does.
- */
if (pending || idx.fresh) {
- /*
- * Deliberately drop the shared lock and redo everything under
- * LOCK_EX rather than upgrading in place -- flock(2) has no
- * atomic upgrade, so another process could slip in between
- * states and invalidate what was read under the shared lock.
- */
+ /* relock LOCK_EX from scratch rather than upgrade */
index_free(&idx);
index_lock_release(&il);
- locked = index_lock_acquire(mailbox_dir_fd, &il,
+ locked = index_lock_acquire(ss->mailbox_dir_fd, &il,
LOCK_EX | LOCK_NB);
if (locked == 1)
goto busy;
if (locked == -1)
goto send;
- if (refresh_index(mailbox_dir_fd, &idx, il.fd) == -1) {
+ if (refresh_index(ss->mailbox_dir_fd, &idx, il.fd) == -1) {
index_lock_release(&il);
goto send;
}
}
if (idle_uid_list(&idx, &newlist, &newn, &seen_modseq) == -1) {
- /*
- * reply.ok stays 0, so the listener keeps what it last told
- * the client and this poll simply reports nothing; the
- * baseline here is untouched for the same reason.
- */
index_free(&idx);
index_lock_release(&il);
goto send;
}
if (!seeded) {
- gone = idle_send_expunges(idle_baseline.uids, idle_baseline.n,
- newlist, newn, iev);
- changed = idle_send_flag_fetches(&idx, idle_baseline.uids,
- idle_baseline.n, idle_baseline.modseq, iev);
- reply.exists_changed = newn != idle_baseline.n;
+ gone = idle_send_expunges(base->uids, base->n, newlist, newn,
+ iev);
+ changed = idle_send_flag_fetches(&idx, base->uids, base->n,
+ base->modseq, ss);
+ reply.exists_changed = newn != base->n;
}
- free(idle_baseline.uids);
- idle_baseline.uids = newlist;
- idle_baseline.n = newn;
- idle_baseline.modseq = seen_modseq;
- idle_baseline.valid = 1;
+ free(base->uids);
+ base->uids = newlist;
+ base->n = newn;
+ base->modseq = seen_modseq;
+ base->valid = 1;
reply.ok = 1;
reply.exists = (uint32_t)newn;
goto send;
busy:
- /*
- * Another session holds the lock. A poll skips, and send: below makes
- * the next one look again. A seed cannot skip: the baseline left from
- * the last IDLE predates the client's own commands since, and diffing
- * against it would resend EXPUNGEs the client has already had (RFC
- * 9051 SS7.5.1). So a seed reads the committed index instead, and if
- * it cannot, drops the baseline so that the next refresh seeds.
- */
+ /* RFC 9051 SS7.5.1: a seed cannot skip, or EXPUNGEs repeat */
reply.busy = 1;
- if (seeded && idle_seed_unlocked(&reply) == 0) {
+ if (seeded && idle_seed_unlocked(ss, &reply) == 0) {
reply.ok = 1;
/* so the next poll reads the index, not the probe */
- idle_probe_reset();
+ idle_probe_reset(ss);
} else if (seeded) {
- idle_baseline_reset();
+ idle_baseline_reset(ss);
}
log_debug("session %u: idle refresh: index lock busy, %s", session_id,
!seeded ? "skipped this poll" : reply.ok ? "seeded without it" :
"next refresh seeds");
send:
- /*
- * A refresh that gives up has already consumed the change:
- * idle_probe_unchanged() records the new mtimes whatever its caller
- * does next, so without this the next poll reports "unchanged" for
- * something the client was never told.
- */
+ /* the probe has already consumed this change */
if (!reply.ok)
- idle_probe_reset();
+ idle_probe_reset(ss);
if (imsg_compose(&iev->ibuf, IMSG_MBOX_IDLE_REFRESHED, 0, 0, -1,
&reply, sizeof(reply)) == -1)
blob - e97c389fb7ab36d090a992f9435cd362e94a1bd5
blob + 2c1312d3accc2cbb04903f56af503731f6550e94
--- src/keymgr.c
+++ src/keymgr.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * keymgr.c: holds the real TLS private key for listener.c's fake-key/imsg
- * forwarding; boot-time plumbing failures are fatal, but content failures
- * (bad cert/key) and per-request failures degrade gracefully, keeping the
- * process alive with no usable key rather than crashing.
- */
-
#include <sys/types.h>
#include <sys/queue.h>
#include "imapd.h"
#include "log.h"
-/*
- * Matches parent.c's read buffer size (8192), kept as a separate local constant
- * rather than a shared imapd.h macro since nothing else needs to agree on the
- * exact value.
- */
#define KEYMGR_CERT_MAX 8192
#define KEYMGR_KEY_MAX 8192
-/*
- * keymgr serves every live connection's listener-worker via its own peer
- * entry, wired in by IMSG_SETUP_PEER and torn down on channel close; named
- * so keymgr_dispatch_parent() doesn't need a forward declaration.
- */
struct keymgr_peer {
uint32_t session_id;
struct imsgev iev;
static struct keymgr_peer_list keymgr_peers =
TAILQ_HEAD_INITIALIZER(keymgr_peers);
-/* fd 3, alive for the process's lifetime */
static struct imsgev iev_parent;
-/*
- * SIGHUP reload staging; keymgr_dispatch_parent() fires once both flags are set
- * -- same pattern listener.c used for its own now-removed cert/key reload
- * gating.
- */
static char reload_cert_buf[KEYMGR_CERT_MAX];
static char reload_key_buf[KEYMGR_KEY_MAX];
static size_t reload_cert_len, reload_key_len;
static int reload_got_cert, reload_got_key;
-/*
- * The currently-loaded real key and its libtls-compatible pubkey hash;
- * NULL/empty iff no usable key has ever loaded successfully.
- */
static EVP_PKEY *keymgr_pkey;
static char keymgr_hash[KEYMGR_HASH_MAX];
-/*
- * The explicit permission gate: true once the boot-time cert+key pair has
- * been processed at all (even if it was rejected as unusable) -- distinct from
- * keymgr_pkey being non-NULL, which tracks whether a *usable* key is currently
- * loaded. A signing request arriving before this is set is refused outright,
- * not merely "refused because no key is loaded yet", so the gate is checkable
- * on its own rather than an incidental side effect of message ordering.
- */
+/* set once the boot pair was processed, even if rejected */
static int keymgr_got_init;
static void keymgr_key_free(void);
size_t cert_len = 0, key_len = 0;
int got_cert = 0, got_key = 0;
- /*
- * fd-passing is allowed on this channel for the fd-passed
- * IMSG_SETUP_PEER peer fds; see imsgev_ibuf_init()'s own comment
- */
imsgev_ibuf_init(&ibuf3, 3);
- /*
- * Both IMSG_TLS_CERT and IMSG_KEYMGR_INIT must be read before the peer
- * handshake so keymgr_got_init/keymgr_pkey are final before any signing
- * request can arrive; sent as two imsgs (mirroring parent.c's
- * send_tls_cert()/send_keymgr_key() split) rather than one, to stay
- * comfortably under MAX_IMSGSIZE.
- */
+ /* the key is final before any peer can ask for a signature */
while (!got_cert || !got_key) {
if ((n = imsgbuf_get(&ibuf3, &imsg)) == -1)
fatal("imsgbuf_get");
imsg_free(&imsg);
}
- /* Content failure here is not fatal, see this file's header comment. */
+ /* a bad key is not fatal: its operations fail instead */
if (cert_len == 0 || key_len == 0)
log_warnx("no usable TLS cert/key at boot, TLS "
"private-key operations will fail until the next "
explicit_bzero(key_buf, sizeof(key_buf));
keymgr_got_init = 1;
- /*
- * keymgr's own daemon-user identity: a dedicated account, following
- * imapd's per-role convention (_imapd for listener, _imapauth for auth)
- * over smtpd's literal SMTPD_USER reuse.
- */
if ((pw = getpwnam("_imapkey")) == NULL)
fatalx("getpwnam _imapkey: no such user "
"(expected, not yet provisioned by an install script)");
- /*
- * No filesystem access needed at all -- the key arrives over imsg from
- * parent, never touches disk in this process.
- */
+ /* the key arrives over imsg; nothing is read from disk */
if (chroot("/var/empty") == -1)
fatal("chroot /var/empty");
if (chdir("/") == -1)
setproctitle("keymgr");
- /*
- * keymgr stays the one boot-time, daemon-lifetime child, but no peer is
- * wired to it at boot -- parent.c sends only IMSG_SETUP_DONE; every
- * listener-worker peer arrives later over this same channel via
- * IMSG_SETUP_PEER, handled below.
- */
setup_recv_done_and_ack(&ibuf3);
event_init();
NULL);
#ifdef __OpenBSD__
- /*
- * recvfd only: keymgr keeps receiving peer fds via IMSG_SETUP_PEER for
- * its whole life but never sends one (only parent attaches descriptors
- * to imsgs, and keymgr_reply() composes with fd == -1); no rpath either
- * since keymgr touches no filesystem.
- */
+ /* recvfd only: peers arrive, nothing is sent */
if (pledge("stdio recvfd", NULL) == -1)
fatal("pledge");
#endif
fatalx("exited event loop");
}
-/*
- * Replicates smtpd's ssl.c hash_x509() byte-for-byte: SHA256 of the cert's DER
- * SubjectPublicKeyInfo, formatted "SHA256:" plus lowercase hex -- the exact
- * format is load-bearing (see file header), not cosmetic.
- */
+/* as smtpd's ssl.c hash_x509() */
static int
keymgr_pubkey_hash(X509 *cert, char *hash, size_t hashlen)
{
static const char hex[] = "0123456789abcdef";
- /*
- * Uses unsigned char/unsigned int, not smtpd hash_x509()'s signed
- * types, to match X509_pubkey_digest(3)'s prototype and avoid
- * -Wpointer-sign warnings; the emitted string is unchanged since
- * digest[i] is already unsigned.
- */
unsigned char digest[EVP_MAX_MD_SIZE];
size_t off;
unsigned int dlen, i;
return (0);
}
-/* Frees the loaded key; EVP_PKEY_free wipes it via BN_free */
-/* unnecessary -- freed pages are zeroed -- but right on every exit path */
static void
keymgr_key_free(void)
{
}
}
-/*
- * Parses a new cert+key pair fully before replacing the live one, so a
- * malformed SIGHUP reload leaves the last known-good key in place instead of
- * none; not sourced from smtpd's ca.c reload logic. cert_buf/key_buf aren't
- * retained past this call -- callers must scrub key_buf themselves.
- */
+/* a bad reload leaves the last good key in place */
static int
keymgr_load(const char *cert_buf, size_t cert_len, const char *key_buf,
size_t key_len)
goto fail;
}
- /*
- * A cert and key can each parse fine yet not correspond to each other
- * (e.g. a rotation that replaced only one) -- checked here via
- * X509_check_private_key() before swapping, since the per-request hash
- * check elsewhere can't catch a cert/key mismatch.
- */
+ /* a cert and key may each parse yet not match */
if (X509_check_private_key(cert, pkey) != 1) {
log_warnx("certificate and private key do not match, not "
"(re)loading");
keymgr_key_free();
keymgr_pkey = pkey;
pkey = NULL;
- /*
- * strlcpy, not memcpy of sizeof(): keymgr_pubkey_hash() only writes 72
- * of KEYMGR_HASH_MAX's 80 bytes, and memcpy would drag uninitialised
- * stack bytes into a static.
- */
+ /* strlcpy: the hash does not fill the buffer */
(void)strlcpy(keymgr_hash, hash, sizeof(keymgr_hash));
log_info("TLS key loaded (%s)", keymgr_hash);
return (-1);
}
-/*
- * Commits a SIGHUP reload once BOTH halves have arrived: IMSG_TLS_CERT and
- * IMSG_KEYMGR_INIT can land in either order, so both cases call this and only
- * the second one finds the pair complete. The key buffer is scrubbed whether or
- * not the load succeeded, and both flags clear so the next reload starts from a
- * clean pair rather than half of this one.
- */
static void
keymgr_try_reload(void)
{
reload_got_cert = reload_got_key = 0;
}
-/*
- * PARENT channel (fd 3): SIGHUP reload's IMSG_TLS_CERT/IMSG_KEYMGR_INIT pair
- * (same paired-flags shape listener.c used for its own now-removed cert/key
- * reload gating), plus IMSG_SETUP_PEER wiring in a fresh listener-worker's peer
- * (one per parent.c's spawn_connection() call, imsg_get_id() carries
- * session_id -- see listener.c's own IMSG_SETUP_PEER (store) case for the same
- * pattern).
- */
static void
keymgr_dispatch_parent(int fd, short event, void *arg)
{
if ((n = imsgbuf_read(&iev->ibuf)) == -1)
fatal("imsgbuf_read");
if (n == 0) {
- /*
- * Parent gone means this process is done: it used to
- * linger serving existing peers, but keymgr is only
- * ever consulted during a TLS handshake, so that only
- * kept a key-holding process alive for as long as any
- * client held a connection open; log_warnx (an operator
- * should notice) and exit(0) (not a failure, just
- * following the parent's death) rather than fatalx().
- */
log_warnx("parent closed channel, exiting");
keymgr_key_free();
exit(0);
(void)fd;
}
-/*
- * LISTENER channel: handles the three signing/decrypt request types;
- * arg is the owning struct keymgr_peer (not a bare imsgev) so the EOF
- * path knows which of possibly many live peers just went away.
- */
static void
keymgr_dispatch_listener(int fd, short event, void *arg)
{
struct imsg imsg;
ssize_t n;
- /*
- * A transport failure on one listener-worker's channel drops only that
- * peer, not the whole process -- fatal()ing here would turn one
- * connection's worker dying into a daemon-wide TLS outage, since keymgr
- * is never restarted by parent.c's reap_child();
- * keymgr_dispatch_parent() (the fd-3 channel) stays strict since losing
- * the parent leaves keymgr with no future.
- */
+ /* a transport failure drops this peer only */
if (event & EV_WRITE) {
if (imsgbuf_write(&iev->ibuf) == -1) {
log_warnx("session %u: write error on listener "
(void)fd;
}
-/*
- * Drops one listener-worker peer: unregisters its event, closes and clears its
- * channel, unlinks and frees it -- shared by every exit path in
- * keymgr_dispatch_listener() so EOF and transport error give the same outcome;
- * imsgbuf_clear() is required or imsgbuf_init()'s allocation leaks.
- */
static void
keymgr_peer_teardown(struct keymgr_peer *kp)
{
free(kp);
}
-/*
- * Bound-checked read of this imsg's trailing raw bytes into a caller-supplied
- * fixed buffer; same "fixed header + trailing raw bytes on one imsg" shape as
- * store.c's recv_trailing_array()/imapd.h's imsg_mbox_append, without the
- * malloc since KEYMGR_DATA_MAX is a small fixed cap rather than
- * message-dependent.
- */
static int
keymgr_recv_trailing(struct imsg *imsg, uint32_t len, unsigned char *buf,
size_t bufsize)
return (1);
}
-/*
- * Composes a struct imsg_keymgr_sign_reply plus its trailing output bytes,
- * reusing the SAME imsg type as the request (correlated by id) -- matches
- * ca_imsg()'s own convention of replying on imsg->hdr.type rather than a
- * distinct reply type.
- */
+/* the reply reuses the request's type, correlated by id */
static void
keymgr_reply(struct imsgev *iev, uint32_t type, uint32_t id, int ok,
const void *to, size_t tolen)
unsigned char combined[sizeof(rep) +
KEYMGR_DATA_MAX];
- /*
- * Every caller already bounds its own result, but combined[] is a fixed
- * stack buffer holding the private key's output, so this function
- * bound-checks independently rather than relying on that discipline
- * holding forever; an oversized result is reported as a failed
- * operation.
- */
if (ok && tolen > KEYMGR_DATA_MAX) {
log_warnx("keymgr_reply: %zu-byte result exceeds "
"KEYMGR_DATA_MAX (%d), refusing", tolen,
sizeof(rep) + (ok ? tolen : 0)) == -1)
log_warn("imsg_compose reply");
- /*
- * imsg_compose() has copied the reply; this buffer may hold a decrypted
- * premaster secret (on RSA_PRIVDEC), so it's scrubbed here like every
- * other key-material buffer in this file.
- */
+ /* may hold a decrypted premaster secret */
explicit_bzero(combined, sizeof(combined));
}
-/*
- * IMSG_KEYMGR_RSA_PRIVENC / IMSG_KEYMGR_RSA_PRIVDEC: mirrors ca_imsg()'s
- * RSA_private_encrypt()/RSA_private_decrypt() dispatch (ca.c:216-253), on
- * imapd's own single-key state rather than ca.c's hash-keyed dict.
- */
+/* as smtpd's ca.c ca_imsg() */
static void
keymgr_handle_rsa(struct imsgev *iev, struct imsg *imsg, uint32_t type,
uint32_t id)
keymgr_reply(iev, type, id, 0, NULL, 0);
return;
}
- /*
- * imsg_get_buf() guarantees size, not NUL termination, force it (same
- * reasoning as auth.c's inbound username/password fields).
- */
+ /* imsg_get_buf() does not NUL-terminate */
req.hash[sizeof(req.hash) - 1] = '\0';
if (!keymgr_recv_trailing(imsg, req.fromlen, from, sizeof(from))) {
return;
}
keymgr_reply(iev, type, id, 1, to, (size_t)ret);
- /*
- * RSA_PRIVDEC's output is the session's decrypted premaster secret;
- * don't leave it on this process's stack.
- */
+ /* the decrypted premaster secret */
explicit_bzero(to, sizeof(to));
}
-/* IMSG_KEYMGR_ECDSA_SIGN: mirrors ca_imsg()'s ECDSA_sign() (ca.c:255-279). */
+/* as smtpd's ca.c ca_imsg() */
static void
keymgr_handle_ecdsa(struct imsgev *iev, struct imsg *imsg, uint32_t id)
{
blob - 1b39f7831b03ae04c5fdd26c4b5eeee105463eb1
blob + a44a4d1a1971a77be423a9b5bb97ba7fe47a958c
--- 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. Real TLS private-key operations are forwarded to
- * keymgr(8); see keymgr_engine_init() below.
- */
#include <sys/types.h>
#include <sys/queue.h>
struct session_list sessions = TAILQ_HEAD_INITIALIZER(sessions);
-/*
- * Channel to the AUTH process; .ibuf.fd == -1 if this connection's
- * auth-worker spawn failed, checked by auth_cmd.c's
- * sasl_plain_finish() before sending IMSG_AUTH_REQUEST.
- */
struct imsgev iev_auth;
-/*
- * Channel to the search-oracle process; .ibuf.fd == -1 if
- * spawn failed, checked by search_cmd.c's search_dispatch() before
- * sending IMSG_SEARCH_PARSE_REQUEST.
- */
-struct imsgev iev_search;
-struct imsgev iev_parent; /* fd 3, alive for the process's lifetime */
+struct imsgev iev_parent;
-/*
- * Channel to the keymgr process: private-key ops are forwarded here
- * synchronously from OpenSSL callbacks, never via imsgev dispatch, so
- * it's a plain struct imsgbuf; see
- * keymgr_forward_rsa()/keymgr_forward_ecdsa().
- */
+static int auth_granted;
+
+/* keymgr: read synchronously from OpenSSL callbacks, never dispatched */
static struct imsgbuf keymgr_ibuf;
static struct tls_config *listener_tls_config;
-/* NULL if TLS setup failed, no-TLS fallback */
struct tls *listener_tls_ctx;
-/* set from imsg */
uint32_t listener_idle_poll_secs = IDLE_POLL_DEFAULT;
uint32_t listener_login_grace_secs = LOGIN_GRACE_DEFAULT;
uint64_t listener_append_max = APPEND_MAX_DEFAULT;
struct imap_cmd_entry {
const char *name;
- unsigned int states; /* bitmask of 1U << SESSION_* */
+ unsigned int states;
int (*handler)(struct session *, const char *, char *);
};
(1U << SESSION_SELECTING) | (1U << SESSION_SELECTED) | \
(1U << SESSION_FETCHING) | (1U << SESSION_STORING) | \
(1U << SESSION_EXPUNGING) | (1U << SESSION_APPENDING) | \
- (1U << SESSION_SEARCH_PARSING) | (1U << SESSION_SEARCHING) | \
+ (1U << SESSION_SEARCHING) | \
(1U << SESSION_STATUSING) | \
(1U << SESSION_COPYING) | (1U << SESSION_CREATING) | \
(1U << SESSION_DELETING) | (1U << SESSION_RENAMING) | \
};
#define NUM_IMAP_CMDS (sizeof(imap_cmds) / sizeof(imap_cmds[0]))
-/*
- * tls_config_use_fake_private_key() is an internal, undeclared libtls
- * symbol forward-declared here, same as smtpd's smtp.c does. Being
- * an internal symbol, it can change or vanish without notice.
- */
+/* internal to libtls, declared as smtpd/smtp.c does */
void tls_config_use_fake_private_key(struct tls_config *);
-/*
- * Installs libtls's placeholder private key plus the real certificate
- * (smtp.c:187-193's call shape) in one function so listener_main()'s
- * tls_config-building if/else-if chains need only one call per
- * branch.
- */
static int
keymgr_set_fake_keypair(struct tls_config *config, const char *cert_buf,
size_t cert_len)
cert_len, NULL, 0);
}
-/*
- * RSA/ECDSA privsep engine, installed once process-wide: intercepts
- * every private-key operation OpenSSL performs against this
- * process's fake key and forwards it to keymgr; adapted from smtpd's
- * ca.c (ca.c:289-558) with imapd's own imsg framing.
- */
+/* private-key operations go to keymgr, after smtpd's ca.c */
static const RSA_METHOD *keymgr_rsa_default;
static RSA_METHOD *keymgr_rsae_method;
static const EC_KEY_METHOD *keymgr_ecdsa_default;
static EC_KEY_METHOD *keymgr_ecdsae_method;
-/*
- * Blocks reading keymgr_ibuf directly from inside an OpenSSL
- * RSA_METHOD callback; unlike ca.c's rsae_send_imsg(), nothing else
- * is ever multiplexed on this channel so there's no need to hand off
- * unrelated imsgs.
- */
static int
keymgr_forward_rsa(uint32_t type, const char *hash, const unsigned char *from,
int fromlen, unsigned char *to, size_t tosize, int padding)
imsg_free(&imsg);
break;
}
- /*
- * Bound by tosize (OpenSSL's actual output buffer, e.g. 256
- * bytes for a 2048-bit key), not by the larger
- * KEYMGR_DATA_MAX wire cap, or an oversized reply could
- * overrun it; mirrors ca.c's own RSA_size() bound.
- */
+ /* bounded by OpenSSL's buffer, tosize */
if (rep.ok && rep.tolen <= tosize &&
imsg_get_len(&imsg) == rep.tolen) {
if (imsg_get_buf(&imsg, to, rep.tolen) == -1)
return (sig);
}
-/*
- * Runs checks A, B and C1, each described at its own test below,
- * before any key op is forwarded to keymgr; fatalx(), not a log
- * line, since a failure here means privilege separation isn't
- * actually in effect.
- */
+/* a failure means privilege separation is not in effect */
static void
keymgr_assert_fake_key(const char *hash, const BIGNUM *priv, const char *op)
{
size_t i;
- /*
- * Check A: a public-key-only object from
- * tls_config_use_fake_private_key() never has d/priv_key set,
- * so a non-NULL priv here means libtls is no longer using the
- * placeholder key.
- */
+ /* check A: the placeholder key has no private part */
if (priv != NULL)
fatalx("%s: key object carries a private component -- "
"libtls is no longer using a placeholder key, and this "
"process is not separated from the TLS private key", op);
- /*
- * Check B: unlike smtpd's ca.c, listener configures exactly one
- * keypair and never reaches this callback for an unrelated
- * key, so a missing pubkey-hash tag is itself the regression,
- * not a benign case to fall through on.
- */
+ /* check B: one keypair, so a missing tag is a regression */
if (hash == NULL)
fatalx("%s: no pubkey-hash tag on the key object -- libtls's "
"ex_data slot 0 tagging has changed", op);
- /*
- * Check C1, done before the tag is read as a string:
- * strlcpy(3) has no bound once the destination is full, so
- * this bounded loop (not memchr/strnlen) confirms the tag is
- * NUL-terminated within KEYMGR_HASH_MAX bytes before anything
- * trusts it.
- */
+ /* check C1: NUL-terminated within KEYMGR_HASH_MAX */
for (i = 0; i < KEYMGR_HASH_MAX; i++)
if (hash[i] == '\0')
return;
{
const char *hash = EC_KEY_get_ex_data(eckey, 0);
- /*
- * inv/rp: ECDSA_sign_setup() precomputation, unused; keymgr does the
- * op.
- */
(void)inv;
(void)rp;
EC_KEY_set_default_method(keymgr_ecdsae_method);
}
-/*
- * Installs both engine overrides; call exactly once, before any
- * tls_config touches a key -- listener_main() calls this right
- * before its TLS setup block, mirroring ca_engine_init()'s call from
- * smtpd's dispatcher() (dispatcher.c:135).
- */
static void
keymgr_engine_init(void)
{
keymgr_ecdsa_engine_init();
}
-/* Builds/starts this one session; forward-declared for listener_main(). */
static void listener_start_session(uint32_t, int, int,
const struct sockaddr_storage *, socklen_t);
struct passwd *pw;
int auth_peer_fd = -1;
int keymgr_peer_fd = -1;
- int search_peer_fd = -1;
struct imsg imsg;
struct imsg_listener_session_init sinit;
ssize_t n;
memset(&sinit, 0, sizeof(sinit));
- /*
- * fd-passing allowed here: receives fd-passed peer/session
- * messages below; see imsgev_ibuf_init().
- */
imsgev_ibuf_init(&ibuf3, 3);
- /*
- * This process is spawned fresh per connection, so the
- * peer handshake is drained in this same synchronous loop
- * rather than separate blocking calls; the auth peer may never
- * arrive, and IMSG_SETUP_PEER's id (0 vs session_id) tells
- * auth from keymgr, matching parent.c's setup_peer_send().
- */
while (!got_cert || !got_session_init || !got_keymgr_peer) {
if ((n = imsgbuf_get(&ibuf3, &imsg)) == -1)
fatal("imsgbuf_get");
"carried no fd");
break;
}
- /*
- * A repeat can't happen today, but this runs
- * pre-pledge/pre-privdrop where trusting the parent
- * matters most, so state the invariant rather than
- * silently overwrite; imsg_get_fd(3) already handed
- * us peer_fd, so the duplicate must be closed here.
- */
if (id == 0) {
if (auth_peer_fd != -1) {
log_warnx("listener: duplicate auth "
}
break;
}
- case IMSG_SETUP_SEARCH_PEER: {
- int peer_fd = imsg_get_fd(&imsg);
-
- /*
- * Optional, like the auth peer above -- not
- * gated by the while() condition, spawn_connection()
- * may not have wired one at all
- */
- if (peer_fd == -1)
- log_warnx("listener: IMSG_SETUP_SEARCH_PEER "
- "carried no fd");
- else if (search_peer_fd != -1) {
- /* already claimed above, so close it here */
- log_warnx("listener: duplicate "
- "IMSG_SETUP_SEARCH_PEER, ignoring");
- close(peer_fd);
- } else
- search_peer_fd = peer_fd;
- break;
- }
case IMSG_TLS_CERT:
cert_len = imsg_get_len(&imsg);
if (cert_len > sizeof(cert_buf)) {
log_warnx("bad IMSG_LISTENER_SESSION_INIT");
break;
}
- /*
- * Refuse before claiming, unlike the peer cases
- * above: an unclaimed fd on this imsg is closed by
- * imsg_free() below, so there's nothing to clean up
- * by hand.
- */
if (client_fd != -1) {
log_warnx("listener: duplicate "
"IMSG_LISTENER_SESSION_INIT, ignoring");
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
fatal("cannot drop privileges to _imapd");
- /*
- * Installs RSA_METHOD/EC_KEY_METHOD override before tls_config touches
- * key.
- */
keymgr_engine_init();
- /*
- * Failure isn't fatal; degrades to no-TLS, checked via
- * listener_tls_ctx.
- */
+ /* no TLS rather than no daemon */
if (cert_len == 0) {
log_warnx("listener: no TLS cert received, TLS "
"disabled for this session");
event_init();
- /*
- * auth_peer_fd may be -1 (no auth-worker spawned); iev_auth.
- * ibuf.fd is left at -1 rather than defaulting to fd 0, and
- * auth_cmd.c's sasl_plain_finish() checks that before composing
- * to it.
- */
if (auth_peer_fd != -1)
imsgev_init(&iev_auth, auth_peer_fd, listener_dispatch_auth,
NULL);
"connection gets one", sinit.session_id);
}
- /*
- * search_peer_fd may be -1 (no search-oracle spawned,
- * independent of auth's fork); iev_search.ibuf.fd is left at
- * -1, checked by search_cmd.c's search_dispatch() before
- * composing to it, mirroring iev_auth above.
- */
- if (search_peer_fd != -1)
- imsgev_init(&iev_search, search_peer_fd,
- listener_dispatch_search,
- NULL);
- else {
- iev_search.ibuf.fd = -1;
- log_warnx("session %u: no search-oracle was spawned for this "
- "connection, SEARCH will fail until a new connection gets "
- "one", sinit.session_id);
- }
-
if (imsgbuf_init(&keymgr_ibuf, keymgr_peer_fd) == -1)
fatal("imsgbuf_init keymgr");
imsgbuf_set_maxsize(&keymgr_ibuf, MAX_IMSGSIZE);
- /*
- * Reuses fd 3's populated ibuf; imsgbuf_init() would drop buffered
- * bytes.
- */
+ /* keeps fd 3's buffered bytes */
imsgev_init_from_ibuf(&iev_parent, &ibuf3, listener_dispatch_parent,
NULL);
- /*
- * This process's one and only session, built from what boot
- * just drained; must run after event_init() and the TLS setup
- * above since session_tls_start()/session_arm_client_read()
- * register libevent events needing listener_tls_ctx already
- * set.
- */
listener_idle_poll_secs = sinit.idle_poll_secs;
listener_login_grace_secs = sinit.login_grace_secs;
listener_append_max = sinit.append_max;
listener_start_session(sinit.session_id, client_fd,
sinit.implicit_tls, &sinit.remote_ss, sinit.remote_sslen);
- /*
- * pledge(2) promises: no socket/connect/bind/listen/accept call
- * remains here (parent.c owns them), so "inet" is
- * dropped since getnameinfo(3) below only formats
- * already-numeric bytes; "recvfd" stays for the store child's
- * peer fd arriving later; "sendfd" goes since this process
- * never attaches a descriptor to an imsg.
- */
+ /* no "inet": getnameinfo(3) only formats numeric bytes */
#ifdef __OpenBSD__
if (pledge("stdio recvfd", NULL) == -1)
fatal("pledge");
fatalx("listener: exited event loop");
}
-/*
- * A connection that completes TCP and then says nothing held a
- * listener-worker, an auth-worker and a search-oracle for ever, and at
- * MaxStartups "full" that refuses every later connection. RFC 9051 SS5.4
- * permits a shortened pre-authentication timer for exactly this; its 30
- * minute floor governs a post-authentication autologout, which this server
- * does not have. sshd's LoginGraceTime and smtpd's SMTPD_SESSION_TIMEOUT
- * are the base-system analogues, and both time out in the process holding
- * the client descriptor, as this does.
- */
static void
session_login_grace_expired(int fd, short event, void *arg)
{
session_teardown(s, "login-grace");
}
-/*
- * Covers every pre-authentication stall, not just a missing command: an
- * implicit-TLS connection that never sends a ClientHello never reaches the
- * command path at all, so a timeout armed on one event would miss it.
- */
void
session_login_grace_init(struct session *s)
{
evtimer_del(&s->grace_ev);
}
-/*
- * Builds and starts this process's one and only session from the
- * IMSG_LISTENER_SESSION_INIT payload drained at boot, doing
- * what the old accept()-driven listener_accept() did: build struct
- * session, format remote_addr, log, then begin the TLS handshake or
- * send the plaintext greeting.
- */
static void
listener_start_session(uint32_t session_id, int client_fd, int implicit_tls,
const struct sockaddr_storage *ss, socklen_t sslen)
if (s == NULL) {
log_warn("calloc");
close(client_fd);
- /*
- * Exit directly rather than return: nothing else will
- * ever run in this process, and returning would park it
- * in event_dispatch() forever holding a MaxStartups slot
- * with no client and no session to tear down.
- */
exit(1);
}
s->pending_body_fd = -1; /* calloc(3)'s 0 is a real descriptor */
s->state = SESSION_NOT_AUTH;
s->implicit_tls = implicit_tls;
session_login_grace_init(s);
- /* CLOCK_MONOTONIC; see connected_at in listener.h. */
if (clock_gettime(CLOCK_MONOTONIC, &s->connected_at) == -1)
log_warn("session %u: clock_gettime", s->id);
TAILQ_INSERT_TAIL(&sessions, s, entry);
{
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
- /*
- * NI_NUMERIC*: pure formatting of already-numeric address
- * bytes, no resolver or network I/O -- the fact
- * listener_main()'s pledge() comment rests dropping
- * "inet" on.
- */
+ /* numeric: no resolver or network I/O */
if (getnameinfo((const struct sockaddr *)ss, sslen, hbuf,
sizeof(hbuf), sbuf, sizeof(sbuf),
NI_NUMERICHOST | NI_NUMERICSERV) == 0)
session_send_greeting(s);
}
-/* (Re-)registers client_ev for steady-state reads; guards re-registration. */
void
session_arm_client_read(struct session *s)
{
s->client_ev_added = 1;
}
-/* Creates per-conn struct tls (non-blocking), arms client_ev to drive it. */
void
session_tls_start(struct session *s)
{
s->client_ev_added = 1;
}
-/* Drives non-blocking TLS handshake, re-arms client_ev for wanted direction. */
void
session_tls_handshake(int fd, short event, void *arg)
{
session_write(s, greeting, sizeof(greeting) - 1);
}
-/* Forward decls: defined below session_dispatch_client() but called from it. */
static int session_is_busy(const struct session *);
static int session_enqueue_cmd(struct session *, const char *);
/* RFC 9051 SS4.3 hard cap on a non-synchronizing literal. */
#define IMAP_NONSYNC_LITERAL_MAX 4096
-/*
- * True if `line` ends in a non-synchronizing literal announcement
- * "{n+}" (RFC 9051 SS4.3), whose octets are already in flight and
- * must be accounted for regardless of the command's fate; octet
- * count returned in *lenp.
- */
static int
line_nonsync_literal(const char *line, uint64_t *lenp)
{
len = strlen(line);
if (len < 4 || line[len - 1] != '}' || line[len - 2] != '+')
return (0);
- stop = &line[len - 2]; /* one past the last digit */
+ stop = &line[len - 2];
if ((open = memrchr(line, '{', len)) == NULL || open + 1 >= stop)
return (0);
open++;
return (1);
}
-/*
- * RFC 9051 SS9: tag = 1*<ASTRING-CHAR except "+">; previously only
- * length was checked, so a tag of "+" could turn session_reply()'s
- * own reply into a command continuation request.
- */
+/* RFC 9051 SS9: a tag may not contain "+" */
static int
tag_is_valid(const char *tag)
{
return (1);
}
-/* Splits s->inbuf into CRLF lines (bare LF isn't one, SS2.2); may free *s*. */
void
session_dispatch_client(int fd, short event, void *arg)
{
struct session *s = arg;
ssize_t n;
char *crlf;
- /*
- * Bytes offered to tls_read(); re-armed at the end of this
- * function -- named tls_want, not want, to avoid shadowing the
- * literal-assembly loop's own uint64_t want (-Wshadow).
- */
size_t tls_want = 0;
(void)event;
if (s->write_failed) {
- /* A prior session_write() couldn't finish; see listener.h. */
session_teardown(s, "io-error");
return;
}
tls_want = sizeof(s->inbuf) - s->inbuflen;
n = tls_read(s->tls_ctx, s->inbuf + s->inbuflen, tls_want);
if (n == TLS_WANT_POLLIN || n == TLS_WANT_POLLOUT) {
- /*
- * tls_read() can want to write (renegotiation); re-arm
- * for what it needs.
- */
+ /* tls_read() may want to write */
event_del(&s->client_ev);
event_set(&s->client_ev, s->client_fd,
(n == TLS_WANT_POLLIN) ? EV_READ : EV_WRITE,
session_teardown(s, "tls-error");
return;
}
- /* restore EV_READ|EV_PERSIST; harmless if OK */
session_arm_client_read(s);
} else {
n = read(fd, s->inbuf + s->inbuflen,
sizeof(s->inbuf) - s->inbuflen);
if (n == -1) {
- /*
- * client_fd is O_NONBLOCK since parent.c's
- * parent_accept().
- */
if (errno == EINTR || errno == EAGAIN ||
errno == EWOULDBLOCK)
return;
size_t consumed, linelen;
int alive;
- /*
- * Octets of a refused non-synchronizing literal (bad syntax,
- * over cap, session busy, or not APPEND) are already on the
- * wire, so swallow them instead of parsing the message body
- * as further IMAP commands.
- */
+ /* a refused non-synchronizing literal's octets are in flight */
if (s->literal_discard > 0) {
uint64_t take;
}
if (s->literal_discard > 0)
break; /* need more data */
- /*
- * Swallow the announcing command line's trailing CRLF
- * so the line parser doesn't see a spurious
- * zero-length line and answer "* BAD Empty command
- * line" after every refused literal; anything besides
- * CRLF is left for the parser.
- */
if (s->inbuflen >= 2 && s->inbuf[0] == '\r' &&
s->inbuf[1] == '\n') {
memmove(s->inbuf, s->inbuf + 2,
continue;
}
- /*
- * RFC 9051 SS4.3 literal in flight; checked before CRLF, may
- * contain one.
- */
+ /* RFC 9051 SS4.3 literal; may contain a CRLF */
if (s->literal_pending) {
uint64_t want, take;
(uint64_t)s->inbuflen : want;
if (take > 0) {
- /* on to the store; take <= SESSION_INBUF_MAX */
if (imsg_compose(&s->store_iev->ibuf,
IMSG_MBOX_APPEND_DATA, 0, 0, -1, s->inbuf,
(size_t)take) == -1) {
if (s->literal_remaining > 0)
break; /* need more data */
- /*
- * Literal body received; `command` still needs its CRLF
- * (RFC 9051 SS9).
- */
if (s->inbuflen < 2)
- break; /* trailing CRLF hasn't arrived yet */
+ break;
if (s->inbuf[0] != '\r' || s->inbuf[1] != '\n') {
/* no reliable resync point, give up */
log_warnx("session %u: expected CRLF after "
linelen = (size_t)(crlf - s->inbuf);
consumed = linelen + 2;
- /*
- * RFC 9051 SS2.2/SS9: CR/LF only appear as the CRLF
- * terminator and NUL isn't an ASTRING-CHAR; this is the one
- * chokepoint enforcing that before client text gets echoed
- * back or silently truncated by the C-string parsers, so a
- * violation closes the connection.
- */
+ /* RFC 9051 SS2.2/SS9: no CR, LF or NUL inside a line */
if (memchr(s->inbuf, '\r', linelen) != NULL ||
memchr(s->inbuf, '\n', linelen) != NULL ||
memchr(s->inbuf, '\0', linelen) != NULL) {
*crlf = '\0';
- /*
- * Note a trailing "{n+}" before dispatch since its octets
- * follow immediately on the wire; over RFC 9051 SS4.3's
- * 4096-octet cap the client is already out of spec and we
- * can't guess how much to skip, so close instead.
- */
nonsync_len = 0;
if (line_nonsync_literal(s->inbuf, &nonsync_len) &&
nonsync_len > IMAP_NONSYNC_LITERAL_MAX) {
return;
}
- /*
- * SASL continuation, IDLE's DONE route around tag/name/args
- * parser/queue.
- */
if (s->auth_cont) {
/* the line is the user's base64 password, see below */
s->scrub_inbuf = 1;
} else if (s->idling) {
alive = session_handle_idle_continuation(s, s->inbuf);
} else if (session_is_busy(s)) {
- /*
- * Queue rather than reject a command while one is
- * already in flight (RFC 9051 SS5.5 pipelining), except
- * one carrying a non-synchronizing literal: its octets
- * are arriving now but cmd_append() wouldn't enter
- * literal-read mode until dequeued, so refuse and
- * swallow instead.
- */
+ /* RFC 9051 SS5.5: queue while one is in flight */
if (nonsync_len > 0) {
session_reply(s, "*", "BAD",
"non-synchronizing literal not accepted on "
if (alive == 0)
return; /* s was torn down (LOGOUT), do not touch */
- /*
- * Anything cmd_append() didn't take (literal_pending) is
- * swallowed here.
- */
if (nonsync_len > 0 && !s->literal_pending)
s->literal_discard = nonsync_len;
- /*
- * cmd_starttls() zeroes inbuflen to discard plaintext; clamps
- * underflow.
- */
+ /* cmd_starttls() zeroes inbuflen */
if (consumed > s->inbuflen)
consumed = s->inbuflen;
- /*
- * Don't leave a SASL response (base64 password) in a long-lived
- * buffer.
- */
+ /* don't leave a base64 password in inbuf */
if (s->scrub_inbuf) {
explicit_bzero(s->inbuf, consumed);
s->scrub_inbuf = 0;
}
if (s->inbuflen == sizeof(s->inbuf)) {
- /*
- * Buffer full, no CRLF; matches RFC 9051 SS7.1.3's example
- * text.
- */
+ /* 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
- */
session_write(s, bad, sizeof(bad) - 1);
session_teardown(s, "limit-exceeded");
return;
}
- /*
- * A TLS record can hold more than inbuf's SESSION_INBUF_MAX,
- * and level-triggered EV_READ won't refire for bytes libtls is
- * still holding, so a full read re-queues this callback via
- * event_active() (ncalls=1) to drain the rest; this terminates
- * once tls_read() returns TLS_WANT_POLLIN.
- */
+ /* libtls may hold bytes that EV_READ will not report */
if (s->tls_active && n > 0 && (size_t)n == tls_want &&
s->inbuflen < sizeof(s->inbuf))
event_active(&s->client_ev, EV_READ, 1);
}
-/*
- * Blocking write(2)/tls_write(): retries EAGAIN/TLS_WANT_POLL* via
- * poll(2) up to SESSION_WRITE_POLL_TIMEOUT_MS; on error or timeout it
- * only records the failure in s->write_failed rather than tearing s
- * down itself -- see listener.h and session_dispatch_client().
- */
#define SESSION_WRITE_POLL_TIMEOUT_MS 5000
void
if (s->write_failed)
return;
- /*
- * Permanent outbound-traffic diagnostic, gated on
- * log_getverbose() since building/scrubbing dbuf is real work
- * otherwise done on every write; session_write() carries every
- * outbound byte including literal FETCH payloads, so -v -v is a
- * message-content-exposure decision, not just a logging knob.
- */
if (log_getverbose() > 0) {
char dbuf[301];
size_t dlen = len < sizeof(dbuf) - 1 ? len : sizeof(dbuf) - 1;
}
if (!s->tls_active) {
- while (sent < len) { /* retry EINTR/EAGAIN via poll */
+ while (sent < len) {
ssize_t n;
struct pollfd pfd;
}
-/*
- * Formats one complete response line into a 512-byte buffer and
- * writes it. The one thing this must never do is emit a line the
- * client cannot frame, so on overflow it forces the last two bytes
- * back to CRLF rather than send a truncated, unterminated line --
- * that fixup is the whole reason session_reply() and
- * session_untagged() are two lines each instead of fifteen: they
- * differed only in their format string, and this is everything else
- * they had in common. fuzz/fuzz_session_reply.c exists to hold
- * exactly this invariant and carries its own stub copy of all three.
- */
+/* on overflow the line still ends in CRLF */
static void session_writef(struct session *, const char *, ...)
__attribute__((__format__ (printf, 2, 3)));
session_writef(s, "* %s\r\n", text);
}
-/*
- * Shared client-composing send for every "fixed request struct + N
- * elemsize-sized trailing elements" imsg to s->store_iev, plus the
- * degenerate no-trailing-array form CREATE/DELETE/RENAME/LIST/STATUS
- * use; malloc failure and imsg_compose failure are both reported
- * back; the caller replies NO and restores s->state.
- */
int
send_mbox_request(struct session *s, int imsg_type, const char *what,
const char *imsgname, const void *req, size_t reqlen, const void *elems,
size_t bodylen = (size_t)nelems * elemsize;
char *combined;
- /*
- * Nothing trailing: compose the caller's request where it
- * already sits rather than malloc a copy of it just to hand it
- * straight to imsg_compose(). Reached by the fixed-size commands,
- * and by SELECT/EXPUNGE whenever their sequence-set is
- * legitimately empty.
- */
if (bodylen == 0) {
if (imsg_compose(&s->store_iev->ibuf, imsg_type, 0, 0, -1,
req, reqlen) == -1) {
memcpy(combined, req, reqlen);
memcpy(combined + reqlen, elems, bodylen);
- /*
- * Report a compose failure via return value instead of just
- * logging it: previously s->state stayed at the busy value with
- * nothing in flight, so session_is_busy() blocked forever
- * waiting for a reply that would never be sent; every caller
- * already handles a 0 return by replying NO and restoring
- * state.
- */
if (imsg_compose(&s->store_iev->ibuf, imsg_type, 0, 0, -1, combined,
reqlen + bodylen) == -1) {
log_warn("session %u: imsg_compose %s", s->id, imsgname);
return (1);
}
-/* RFC 7162 SS3.1: marks CONDSTORE-aware; emits unsolicited HIGHESTMODSEQ. */
void
session_condstore_enable(struct session *s)
{
}
}
-/* Splits a CRLF-stripped line into tag/name/args (RFC 9051 `tag SP ...`). */
int
parse_command_line(char *line, char **tag, char **name, char **args)
{
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.
- */
static int
session_is_busy(const struct session *s)
{
case SESSION_STORING:
case SESSION_EXPUNGING:
case SESSION_APPENDING:
- case SESSION_SEARCH_PARSING:
case SESSION_SEARCHING:
case SESSION_STATUSING:
case SESSION_COPYING:
}
}
-/* Appends a line to s->cmd_queue; 0 on a full queue or strdup(3) failure. */
static int
session_enqueue_cmd(struct session *s, const char *line)
{
return (1);
}
-/* Dispatches queued commands while idle; returns 0 if one tore *s* down. */
int
session_dequeue_next(struct session *s)
{
return (1);
}
-/* Returns 1 if alive, 0 if torn down; caller must not touch *s* if 0. */
+/* returns 0 if s was torn down */
int
session_handle_line(struct session *s, char *line)
{
return (1);
}
- /*
- * AUTHENTICATE's optional initial response is the base64 of the
- * user's cleartext password (RFC 4616 SS2), and LOGIN sends it
- * in the clear when LOGINDISABLED is ignored, so log the
- * command but never its arguments.
- */
+ /* RFC 4616 SS2: the initial response holds the password */
if (name != NULL && (strcasecmp(name, "AUTHENTICATE") == 0 ||
strcasecmp(name, "LOGIN") == 0))
log_debug("session %u: <<< %s %s <redacted>", s->id, tag,
name != NULL ? " " : "", name != NULL ? name : "",
args != NULL ? " " : "", args != NULL ? args : "");
- /*
- * Checked once here, not per strlcpy(3) site: tag must not echo
- * truncated.
- */
if (strlen(tag) >= IMAP_TAG_MAX) {
session_reply(s, "*", "BAD", "Tag too long");
return (1);
}
- /* Replied to with "*", never with the tag: see tag_is_valid(). */
+ /* answered with "*", never the tag */
if (!tag_is_valid(tag)) {
session_reply(s, "*", "BAD", "Invalid tag");
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 allows BAD or NO */
session_reply(s, tag, "BAD",
"Command not permitted in this state");
return (1);
struct imsg imsg;
ssize_t n;
- /*
- * Without this, a queued imsg_compose() never flushes; EV_WRITE
- * busy-loops.
- */
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) {
- /*
- * This session's auth-worker may die
- * independently of the session (already
- * authenticated, or unable to AUTHENTICATE again);
- * close and mark it dead rather than just event_del(),
- * or a later AUTHENTICATE would compose onto a dead fd
- * and the client would hang waiting for a reply.
- */
- log_warnx("auth-worker closed channel");
+ /* the auth-worker exits after its grant */
+ if (auth_granted)
+ log_debug("auth-worker exited after the login");
+ else
+ log_warnx("auth-worker closed channel");
event_del(&iev->ev);
close(iev->ibuf.fd);
imsgbuf_clear(&iev->ibuf);
break;
}
+ auth_granted = 1;
s->state = SESSION_STORE_PENDING;
setproctitle("session %u [authenticated]", s->id);
break;
(void)fd;
}
-/*
- * SEARCH-ORACLE channel: at most one
- * IMSG_SEARCH_PARSE_REQUEST/RESULT round trip is ever in flight for
- * this process's one session, so TAILQ_FIRST(&sessions) is
- * unambiguously it; session_id lookup elsewhere is kept only for
- * parity with auth.c's imsg shape.
- */
void
-listener_dispatch_search(int fd, short event, void *arg)
-{
- struct imsgev *iev = arg;
- struct session *s = TAILQ_FIRST(&sessions);
- struct imsg imsg;
- ssize_t n;
-
- if (event & EV_WRITE) {
- if (imsgbuf_write(&iev->ibuf) == -1)
- fatal("imsgbuf_write");
- }
-
- if (event & EV_READ) {
- if ((n = imsgbuf_read(&iev->ibuf)) == -1)
- fatal("imsgbuf_read");
- if (n == 0) {
- /*
- * This session's search-oracle may die
- * independently of the session, same fail-soft shape
- * as listener_dispatch_auth()'s channel-EOF handling --
- * an in-flight SEARCH gets a synthesized NO, and a
- * later one fails fast via iev_search.ibuf.fd == -1.
- */
- log_warnx("search-oracle closed channel");
- event_del(&iev->ev);
- close(iev->ibuf.fd);
- imsgbuf_clear(&iev->ibuf);
- iev->ibuf.fd = -1;
-
- if (s != NULL && s->state == SESSION_SEARCH_PARSING) {
- s->state = SESSION_SELECTED;
- session_reply(s, s->pending_tag, "NO",
- "[UNAVAILABLE] search temporarily "
- "unavailable");
- if (!session_dequeue_next(s))
- /* s torn down by a queued LOGOUT */
- return;
- }
- return;
- }
- }
-
- for (;;) {
- if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1)
- fatal("imsgbuf_get");
- if (n == 0)
- break;
-
- switch (imsg_get_type(&imsg)) {
- case IMSG_SEARCH_PARSE_RESULT: {
- struct imsg_search_parse_result res;
- struct search_node *nodes = NULL;
- size_t bodylen;
-
- if (s == NULL || s->state != SESSION_SEARCH_PARSING) {
- log_debug("IMSG_SEARCH_PARSE_RESULT with no "
- "SEARCH awaiting one, ignored");
- break;
- }
-
- if (imsg_get_buf(&imsg, &res, sizeof(res)) == -1) {
- log_warnx("bad IMSG_SEARCH_PARSE_RESULT "
- "(header)");
- s->state = SESSION_SELECTED;
- session_reply(s, s->pending_tag, "NO",
- "[SERVERBUG] internal error");
- break;
- }
- /*
- * imsg_get_buf() guarantees size, not NUL termination,
- * and this field goes straight to session_reply() via
- * snprintf("%s"), so treat the least-trusted process's
- * framing as untrusted, same as auth.c's
- * username/password and parent.c's maildir.
- */
- res.errmsg[sizeof(res.errmsg) - 1] = '\0';
-
- if (res.rc == 0) {
- bodylen = imsg_get_len(&imsg);
- if (res.nnodes > SEARCH_PROGRAM_MAX_NODES ||
- bodylen != (size_t)res.nnodes *
- sizeof(struct search_node)) {
- log_warnx("bad "
- "IMSG_SEARCH_PARSE_RESULT "
- "(nnodes %u, %zu trailing "
- "bytes)", res.nnodes, bodylen);
- s->state = SESSION_SELECTED;
- session_reply(s, s->pending_tag, "NO",
- "[SERVERBUG] internal error");
- break;
- }
- if (bodylen > 0) {
- if ((nodes = malloc(bodylen)) == NULL) {
- log_warn("malloc SEARCH nodes");
- s->state = SESSION_SELECTED;
- session_reply(s,
- s->pending_tag, "NO",
- "[SERVERBUG] internal "
- "error");
- break;
- }
- if (imsg_get_buf(&imsg, nodes, bodylen)
- == -1) {
- log_warnx("bad "
- "IMSG_SEARCH_PARSE_RESULT "
- "(nodes)");
- free(nodes);
- s->state = SESSION_SELECTED;
- session_reply(s,
- s->pending_tag, "NO",
- "[SERVERBUG] internal "
- "error");
- break;
- }
- }
- }
-
- search_dispatch_finish(s, &res, nodes);
- free(nodes);
- break;
- }
- default:
- log_debug("listener_dispatch_search: unhandled %d",
- imsg_get_type(&imsg));
- break;
- }
- imsg_free(&imsg);
- }
- imsgev_rearm_read(iev);
- (void)fd;
-
- if (s != NULL) {
- if (!session_dequeue_next(s))
- return; /* s torn down by a queued LOGOUT */
- }
-}
-
-/*
- * PARENT channel (fd 3): IMSG_STORE_FORK (spawn failure) and
- * IMSG_SETUP_PEER (spawn success, imsg_get_id() has session id). No
- * SIGHUP-driven reload here any more: this process is spawned
- * fresh per connection and gets its own current TLS cert once at
- * spawn time (IMSG_TLS_CERT, in listener_main()'s boot-drain loop),
- * so it never lives long enough to need one -- see parent.c's header
- * comment.
- */
-void
listener_dispatch_parent(int fd, short event, void *arg)
{
struct imsgev *iev = arg;
struct imsg imsg;
ssize_t n;
- /* See listener_dispatch_auth()'s comment on this EV_WRITE check. */
if (event & EV_WRITE) {
if (imsgbuf_write(&iev->ibuf) == -1)
fatal("imsgbuf_write");
log_warnx("bad IMSG_STORE_FORK reply");
break;
}
- if ((s = session_find(fail.session_id)) != NULL) {
- s->state = SESSION_NOT_AUTH;
+ if ((s = session_find(fail.session_id)) == NULL)
+ break;
+ if (fail.limit) {
session_reply(s, s->pending_tag, "NO",
+ "[LIMIT] too many sessions for this "
+ "account");
+ session_teardown(s, "limit-exceeded");
+ } else {
+ session_reply(s, s->pending_tag, "NO",
"authentication succeeded but mailbox "
"store unavailable");
+ session_teardown(s, "io-error");
}
break;
}
imsgev_init(s->store_iev, store_fd,
session_store_dispatch, s);
s->state = SESSION_AUTHENTICATED;
- /*
- * Authenticated: stop the pre-authentication timer.
- * This is the same point parent.c marks the session
- * authenticated for MaxStartups.
- */
session_login_grace_disarm(s);
log_debug("session %u: store peer wired", sess_id);
/* RFC 9051 SS6.2.2's PLAIN example text, verbatim. */
return (NULL);
}
-/* Best-effort IMSG_STORE_SHUTDOWN to store child; closes fds, unregisters. */
/* reason: one of eight fixed tokens, never NULL, never attacker text */
void
session_teardown(struct session *s, const char *reason)
if (s->tls_ctx != NULL) {
int ret = tls_close(s->tls_ctx);
- /*
- * tls_close() closes the fd unless close_notify wants one more
- * round trip.
- */
if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT)
close(s->client_fd);
/* debug: a missing close_notify is routine; httpd ignores it */
close(s->client_fd);
}
- /*
- * All NULL-safe; may be set on a mid-stream teardown (FETCH/SEARCH
- * etc).
- */
free(s->search_matches);
free(s->vanished_ranges);
free(s->qresync_fetches);
free(s->pending_envelope_buf);
free(s->pending_bodystructure_buf);
- /*
- * Commands pipelined behind the in-flight one when session was torn
- * down.
- */
while (s->cmd_queue_n > 0)
free(s->cmd_queue[--s->cmd_queue_n]);
s->tls_active ? "yes" : "no", dur);
}
- /*
- * inbuf may hold a base64 SASL response; don't hand it to allocator
- * intact.
- */
+ /* inbuf may hold a base64 SASL response */
explicit_bzero(s, sizeof(*s));
free(s);
- /*
- * This process serves exactly this one session and never
- * another, so exit rather than idle forever in event_dispatch()
- * leaking a process per finished connection; parent.c's
- * reap_child() already treats this exit as expected, matching
- * store.c's store_shutdown().
- */
log_debug("listener-worker: session closed, exiting");
exit(0);
}
blob - 8755fed3f2e8207aa45e1e8304bd299107a67642
blob + 0ba65946e76d2bf2869f7b2e0ec1b71892492b88
--- src/listener.h
+++ src/listener.h
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* listener.h: declarations private to the listener process, so */
-/* listener.c's split files can see struct session and each other. */
-
#ifndef LISTENER_H
#define 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 */
+ SESSION_AUTHENTICATING,
+ SESSION_STORE_PENDING,
SESSION_AUTHENTICATED,
- SESSION_SELECTING, /* IMSG_MBOX_SELECT sent, awaiting reply */
+ SESSION_SELECTING,
SESSION_SELECTED,
- SESSION_FETCHING, /* IMSG_MBOX_FETCH sent, awaiting the
- * IMSG_MBOX_FETCH_META stream + terminal
- * 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_END sent, awaiting the
- * single 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_SEARCH_PARSING, /* IMSG_SEARCH_PARSE_REQUEST sent
- * to the per-connection search-oracle,
- * awaiting IMSG_SEARCH_PARSE_RESULT --
- * precedes SESSION_SEARCHING below, a SEARCH
- * is now a two-hop async round trip (oracle
- * parse, then store execute), not one.
- * Handled by listener_dispatch_search()
- * (listener.c), which calls
- * search_dispatch_finish() (search_cmd.c)
- * once the oracle replies. */
- SESSION_SEARCHING, /* IMSG_MBOX_SEARCH sent, awaiting the
- * IMSG_MBOX_SEARCH_MATCH stream + terminal
- * 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.
- * 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) + terminal IMSG_MBOX_RESULT;
- * branches to
- * session_finish_copy_or_move(). */
+ SESSION_FETCHING,
+ SESSION_STORING,
+ SESSION_EXPUNGING,
+ SESSION_APPENDING,
+ SESSION_SEARCHING,
+ SESSION_STATUSING,
+ SESSION_COPYING,
- /* RFC 9051 SS6.3.4-SS6.3.9. All six are command-auth and never */
- /* change s->state's SELECTED-ness; mbox_op_prev_state records what */
- /* to restore, and only one is in flight per session. */
- 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; branches to
- * session_finish_list(). */
- SESSION_SUBSCRIBING, /* IMSG_MBOX_SUBSCRIBE sent, same shape as
- * SESSION_CREATING */
- SESSION_UNSUBSCRIBING /* IMSG_MBOX_UNSUBSCRIBE sent, same shape as
- * SESSION_CREATING */
+ /* RFC 9051 SS6.3.4-SS6.3.9; mbox_op_prev_state is restored after */
+ SESSION_CREATING,
+ SESSION_DELETING,
+ SESSION_RENAMING,
+ SESSION_LISTING,
+ SESSION_SUBSCRIBING,
+ SESSION_UNSUBSCRIBING
};
-/* Line-length cap for the raw read buffer. RFC 9051 mandates no limit, */
-/* but one is needed to bound a client that never sends CRLF. */
+/* RFC 9051 sets no limit; this bounds a client that never sends CRLF */
#define SESSION_INBUF_MAX 8192
-/* Bound on a client-chosen tag held across an async round trip. RFC */
-/* 9051 SS9 sets no limit; an over-long tag is truncated, not rejected. */
+/* RFC 9051 SS9 sets no limit; a longer tag is truncated */
#define IMAP_TAG_MAX 64
-/* Bound on lines pipelined ahead of one awaiting a store round trip */
-/* (RFC 9051 SS5.5). Queueing past this disconnects the session rather */
-/* than granting unbounded memory. Not applied before authentication. */
+/* pipelined lines (RFC 9051 SS5.5); past this the session is dropped */
#define SESSION_CMD_QUEUE_MAX 8
-/* Cap on the verbatim label echoed back as "BODY[<label>]". Sized above */
-/* HEADER_FIELDS_MAX to cover the wrapper around the field-name list. */
#define HEADER_FIELDS_LABEL_MAX 288
-/* Worst case for quote_mailbox(): two DQUOTEs, a NUL, and a backslash */
-/* before every byte of a MBOX_NAME_MAX-1 name of quoted-specials. */
+/* two DQUOTEs, a NUL, and a backslash per byte */
#define MBOX_QUOTED_MAX ((2 * MBOX_NAME_MAX) + 3)
-/* RFC 7162 SS7's ceiling on a mod-sequence, a positive 63-bit integer. */
-/* The three client-facing parsers bound strtoull(3) by this; whether 0 */
-/* is legal varies by parser, and each enforces its own case. */
+/* RFC 7162 SS7: a mod-sequence is a positive 63-bit integer */
#define MODSEQ_MAX INT64_MAX
-/* RFC 9051 SS6.4.4 SEARCH result options, as ESEARCH return items. */
-/* SAVE ("$") is recognized but rejected with a flagged NO. */
+/* 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)
-/* 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 session {
uint32_t id;
int client_fd;
- /* numeric "host:port", set once in listener_start_session() with */
- /* NI_NUMERICHOST|NI_NUMERICSERV, so no resolver pledge is needed */
+ /* numeric, so no resolver pledge is needed */
char remote_addr[64];
- /* close-line username, cleared on auth failure; printable ASCII */
char user[AUTH_USERNAME_MAX];
- /* CLOCK_MONOTONIC, so a duration cannot run backwards on a step */
struct timespec connected_at;
struct event client_ev;
enum session_state state;
- int implicit_tls; /* accepted on port 993 */
- struct imsgev *store_iev; /* NULL until STORE_PENDING */
+ int implicit_tls;
+ struct imsgev *store_iev;
/* client_ev is only safe to event_del() once this is set */
int client_ev_added;
- /* set by session_write() on write error or timeout; checked at the */
- /* top of session_dispatch_client(), which tears down rather than */
- /* read another command it could not answer */
int write_failed;
- int tls_active; /* 1 once TLS is up */
- struct tls *tls_ctx; /* non-NULL during handshake */
- /* implicit-TLS only: RFC 8314 forbids protocol bytes before TLS, so */
- /* the greeting waits for the handshake */
+ int tls_active;
+ struct tls *tls_ctx;
+ /* RFC 8314: the greeting waits for the handshake */
int pending_greeting;
char inbuf[SESSION_INBUF_MAX];
- size_t inbuflen; /* unparsed bytes in inbuf */
- /* 1 when the line being consumed carried SASL credentials and must */
- /* be explicit_bzero(3)'d out of inbuf once dispatched */
+ size_t inbuflen;
+ /* inbuf held SASL credentials; explicit_bzero(3) it */
int scrub_inbuf;
- /* 1 while the next raw line is a SASL continuation response */
int auth_cont;
- /* 1 while the next raw line is IDLE's DONE (RFC 9051 SS6.3.13); */
- /* same shape as auth_cont, checked second in the read loop */
+ /* RFC 9051 SS6.3.13: the next line is DONE */
int idling;
- /* malloc(3)'d lines pipelined while session_is_busy() (RFC 9051 */
- /* SS5.5). Entries 0..cmd_queue_n-1 valid, always compacted. */
- /* Continuation lines bypass it entirely. */
+ /* pipelined lines (RFC 9051 SS5.5), malloc(3)'d */
char *cmd_queue[SESSION_CMD_QUEUE_MAX];
uint32_t cmd_queue_n;
- /* copy of the tag waiting on an async round trip: the tag itself */
- /* points into inbuf, which the next read() overwrites. One field is */
- /* enough, since only one round trip is in flight per session. */
+ /* the tag points into inbuf, which the next read overwrites */
char pending_tag[IMAP_TAG_MAX];
- /* MBOX_FETCH_* bitmask for the in-flight FETCH, needed because the */
- /* store populates imsg_mbox_fetch_meta regardless of what was asked */
uint32_t fetch_attrs;
- /*
- * The pending_* stashes below each hold one store reply until the
- * following IMSG_MBOX_FETCH_META folds it into one FETCH response
- * line, which frees the malloc(3)'d ones. session_teardown() frees
- * them defensively too. Each _found flag distinguishes "asked for
- * and unavailable" from "not asked for"; each _len is valid only
- * while its buffer is non-NULL; each _label is echoed back verbatim
- * rather than reconstructed, since the response must repeat what the
- * client typed.
- */
char *pending_header_buf;
uint32_t pending_header_len;
int pending_header_found;
char pending_header_label[HEADER_FIELDS_LABEL_MAX];
- /* BODY[...] arrives as a read-only descriptor and an octet range */
int pending_body_fd; /* -1 when none */
uint64_t pending_body_off;
uint64_t pending_body_len;
int pending_body_found;
char pending_body_label[SECTION_PART_MAX];
- /* 1 if the BODY.PEEK[...] token carried <<start.count>> (SS6.4.5); */
- /* only the origin is echoed back, never the count */
+ /* only the origin is echoed back (RFC 9051 SS6.4.5) */
int pending_body_has_partial;
uint32_t pending_body_partial_origin;
- /* envelope and bodystructure hold already-formatted "(...)" text, */
- /* written directly rather than wrapped in a literal */
char *pending_envelope_buf;
uint32_t pending_envelope_len;
int pending_envelope_found;
char *pending_bodystructure_buf;
uint32_t pending_bodystructure_len;
int pending_bodystructure_found;
- /* "BODY" or "BODYSTRUCTURE": same bit, identical text, but the */
- /* response label must match what was asked for */
char pending_bodystructure_label[16];
- /*
- * 1 if a requested item could not be produced for some message, so
- * the tagged reply is NO (RFC 9051 SS6.4.5) rather than OK. Sticky
- * across the command, cleared when the FETCH is dispatched.
- */
+ /* tagged NO rather than OK (RFC 9051 SS6.4.5) */
int fetch_incomplete;
- /* 1 if the in-flight SESSION_EXPUNGING round trip came from CLOSE */
- /* rather than EXPUNGE. Both send the same imsg pair, so this is the */
- /* only way to tell which next state applies. */
int close_after_expunge;
- /* literal_pending: 1 while the next bytes are a client literal's */
- /* raw octets (RFC 9051 SS4.3) rather than a CRLF line. Checked */
- /* before the CRLF search, so session_handle_line() is never reached */
- /* while it is set and no ST_* exclusion is needed. */
+ /* RFC 9051 SS4.3 literal octets, checked before the CRLF search */
int literal_pending;
uint64_t literal_len; /* announced size, "{n}" */
uint64_t literal_remaining;
- /* octets of a NON-synchronizing literal whose command was refused */
- /* or deferred: already on the wire, so they are swallowed rather */
- /* than parsed as commands. Mutually exclusive with literal_pending. */
+ /* a refused non-synchronizing literal's octets, swallowed */
uint64_t literal_discard;
- /* APPEND's target, for the EXISTS decision on the reply */
char append_mailbox[MBOX_NAME_MAX];
- /* APPEND's return state is not fixed, unlike FETCH/STORE/EXPUNGE */
enum session_state append_prev_state;
- /* STATUS_ATT_* bitmask for the in-flight STATUS, in listener.c's */
- /* own fixed order; the store computes the cheap ones regardless */
uint32_t status_attrs;
- /* stashed so the untagged response can echo it: the store's reply */
- /* has no mailbox field of its own */
char status_mailbox[MBOX_NAME_MAX];
enum session_state status_prev_state;
- /* shared across SESSION_CREATING/DELETING/RENAMING/LISTING and the */
- /* two SUBSCRIBING states, since only one is in flight at a time */
enum session_state mbox_op_prev_state;
- /* canonical LIST/LSUB pattern, tested against each list item as it */
- /* streams in; nothing needs accumulating first */
char list_pattern[2 * MBOX_NAME_MAX];
int list_is_lsub; /* LSUB, not LIST */
- /* 1 if the in-flight LISTING asked the store for subscribed names */
- /* rather than for what is on disk: LIST (SUBSCRIBED) or LSUB. */
- /* Picks the attribute list on each untagged response. */
int list_subscribed_only;
- /* the mailbox the in-flight CREATE/DELETE/RENAME/SUBSCRIBE/ */
- /* UNSUBSCRIBE names (RENAME's source), compared against */
- /* selected_mailbox when the terminal reply arrives: RENAME follows */
- /* the selection to rename_newname, DELETE deselects. */
+ /* RENAME moves the selection, DELETE deselects */
char mbox_op_name[MBOX_NAME_MAX];
char rename_newname[MBOX_NAME_MAX];
- /* SEARCH_RETURN_* bitmask: which of MIN/MAX/ALL/COUNT the ESEARCH */
- /* response includes. The store only computes matches. */
uint32_t search_return_opts;
- /* growable ascending sequence numbers, kept in full rather than */
- /* range-compacted so MIN/MAX/COUNT/ALL all derive from one array */
uint32_t *search_matches;
uint32_t search_nmatches;
uint32_t search_matches_cap;
- /* 1 if a realloc(3) failed: the response is refused rather than */
- /* sent silently incomplete */
+ /* a failed realloc(3) refuses the response */
int search_alloc_failed;
- /* 1 if the SEARCH program contained a MODSEQ key (RFC 7162 */
- /* SS3.1.5), which appends "(MODSEQ n)" from the running max below */
int search_used_modseq;
uint64_t search_max_modseq;
- /* RFC 7162 SS3.1/SS3.2.3: sticky for the whole connection once set, */
- /* never cleared. qresync_enabled implies condstore_enabled. */
+ /* RFC 7162 SS3.1/SS3.2.3: sticky; QRESYNC implies CONDSTORE */
int condstore_enabled;
int qresync_enabled;
- /* best-known HIGHESTMODSEQ of the selected mailbox, cached from */
- /* every reply carrying one, for SS3.1's unsolicited OK when an */
- /* enabling command arrives with a mailbox already selected */
uint64_t mbox_highestmodseq;
- /* RFC 9051 SS6.3.3: opened via EXAMINE rather than SELECT. Set */
- /* synchronously, since it comes from the command not the reply. */
- /* Consulted to refuse anything that would mutate permanent state. */
+ /* RFC 9051 SS6.3.3 EXAMINE */
int mbox_readonly;
- /* which mailbox SESSION_SELECTED refers to. Written optimistically */
- /* at SESSION_SELECTING; every reader also gates on SELECTED, which */
- /* a failed SELECT never reaches. */
char selected_mailbox[MBOX_NAME_MAX];
- /*
- * RFC 9051 SS6.3.13 IDLE: EXISTS, EXPUNGE and flag-change FETCH. The
- * UID list the client has been told about, and the mod-sequence it
- * has been told about, both live in the store child, which sends the
- * lines to print already worked out, so nothing about the mailbox is
- * kept here. idle_refresh_again remembers a
- * trigger that arrived while a refresh was in flight, and
- * idle_refresh_again_seed remembers that the folded-in trigger was a
- * seeding one.
- */
+ /* RFC 9051 SS6.3.13 IDLE; the store child keeps the UID list */
int idle_refresh_pending;
int idle_refresh_again;
int idle_refresh_again_seed;
- /* evtimer_set() once in listener_start_session() so evtimer_del() */
- /* is always safe, then armed only between "+ idling" and DONE. An */
- /* evtimer is one-shot, so session_idle_poll() re-arms itself. */
struct event idle_ev;
- /*
- * evtimer_set() once in listener_start_session() so evtimer_del() is
- * always safe, then armed until the session authenticates. One shot:
- * it fires at most once, and firing tears the session down.
- */
struct event grace_ev;
- /* VANISHED (EARLIER) ranges from a QRESYNC SELECT resync, already */
- /* compacted and ascending, held until IMSG_MBOX_SELECTED so RFC */
- /* 7162 SS3.2.6's VANISHED-before-FETCH ordering is guaranteed */
+ /* held until IMSG_MBOX_SELECTED (RFC 7162 SS3.2.6 ordering) */
struct vanished_range *vanished_ranges;
uint32_t vanished_nranges;
uint32_t vanished_cap;
- /* the resync's per-message FETCH data, held back for the same */
- /* ordering reason. Full meta copies, since the response needs FLAGS. */
struct imsg_mbox_fetch_meta *qresync_fetches;
uint32_t qresync_nfetches;
uint32_t qresync_fetches_cap;
- /* Set when either accumulator above dropped an entry. Nothing has */
- /* reached the client yet, so the SELECT is failed: completing it */
- /* would advertise a HIGHESTMODSEQ the client adopts as its sync */
- /* anchor, permanently hiding whatever was dropped. */
+ /* a dropped entry fails the SELECT: HIGHESTMODSEQ would hide it */
int qresync_alloc_failed;
- /* COPY/MOVE (RFC 9051 SS6.4.7/SS6.4.8), SESSION_COPYING. Two */
- /* parallel ascending arrays, range-compacted into COPYUID's two UID */
- /* sets once the terminal reply arrives. move_expunged buffers each */
- /* EXPUNGED because SS6.4.8 requires COPYUID to precede them. */
+ /* COPYUID precedes EXPUNGED (RFC 9051 SS6.4.8) */
uint32_t *copy_src_uids;
uint32_t *copy_dest_uids;
uint32_t copy_n;
uint32_t move_expunged_n;
uint32_t move_expunged_cap;
- /* messages that failed a STORE's UNCHANGEDSINCE test, compacted */
- /* into the tagged MODIFIED code (RFC 7162 SS3.1.3). Sequence */
- /* numbers for STORE, UIDs for UID STORE; cmd_by_uid picks which. */
+ /* RFC 7162 SS3.1.3 MODIFIED */
uint32_t *store_modified;
uint32_t store_modified_n;
uint32_t store_modified_cap;
- /* SS3.1.3 requires MODIFIED to list EVERY failure, and a client */
- /* never retries one it cannot see, so an incomplete set is refused */
+ /* MODIFIED must list every failure, so an incomplete set is refused */
int store_modified_alloc_failed;
- /* RFC 9051 SS6.4.9: 1 if the in-flight operation was dispatched as */
- /* UID <cmd>. Every entry point sets it explicitly, so it needs no */
- /* reset. Picks UIDs over sequence numbers in SEARCH and MODIFIED, */
- /* forces UID into a STORE's FETCH echo, and names the tagged reply. */
+ /* RFC 9051 SS6.4.9 UID <cmd> */
int cmd_by_uid;
TAILQ_ENTRY(session) entry;
};
-/* Named, not anonymous: an anonymous TAILQ_HEAD would be a redefinition */
-/* error once the declaration and the definition are in separate files. */
+/* named: declared and defined in different files */
TAILQ_HEAD(session_list, session);
-/*
- * Globals shared across the files this process's source is split into
- * (definitions live in listener.c's core).
- */
extern struct session_list sessions;
extern struct imsgev iev_auth;
-extern struct imsgev iev_search; /* declared in listener.c */
extern struct imsgev iev_parent;
extern struct tls *listener_tls_ctx;
-/* "idle poll" seconds, from IMSG_LISTENER_SESSION_INIT; 0 disables polling */
-/*
- * RFC 9051 SS7.1 INUSE, the one spelling of "another session holds the
- * mailbox's index lock", shared so every command that can be refused for
- * it answers alike.
- */
+
+/* RFC 9051 SS7.1 INUSE */
#define IMAP_BUSY_TEXT "[INUSE] mailbox busy, try again"
extern uint32_t listener_idle_poll_secs;
-/* "login grace" seconds, from IMSG_LISTENER_SESSION_INIT; 0 disables it */
extern uint32_t listener_login_grace_secs;
-/* "append max" octets, from IMSG_LISTENER_SESSION_INIT */
extern uint64_t listener_append_max;
-/* 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_dispatch_auth(int, short, void *);
-void listener_dispatch_search(int, short, void *);
void listener_dispatch_parent(int, short, void *);
void session_dispatch_client(int, short, void *);
void session_store_dispatch(int, short, void *);
int session_finish_append(struct session *);
void session_handle_mbox_appended(struct session *,
const struct imsg_mbox_appended *);
-/* 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 parse_nz_number(const char *, uint32_t *);
int parse_search_key_list(char **, struct search_parse_ctx *,
const char **, int);
int parse_search_return_opts(char **, uint32_t *, const char **);
-/* search_oracle_parse()'s prototype is in imapd.h, not here: */
-/* search_oracle.c is a separate role and must not pull in this */
-/* header's session declarations. */
void session_handle_mbox_search_match(struct session *,
struct imsg_mbox_search_match *);
void session_finish_search(struct session *,
void session_teardown(struct session *, const char *);
struct session *session_find(uint32_t);
-/* RFC 7162 (CONDSTORE/QRESYNC) helpers, added this pass. */
+/* RFC 7162 (CONDSTORE/QRESYNC) */
void session_condstore_enable(struct session *);
size_t format_seq_list(char *, size_t, const uint32_t *, uint32_t,
int *);
int parse_store_modifiers(char *, struct imsg_mbox_store *,
const char **);
-/* RFC 9051 SS6.4.9 (UID command) helpers, added this pass. */
+/* RFC 9051 SS6.4.9 (UID) */
int fetch_dispatch(struct session *, const char *, char *, int);
int store_do(struct session *, const char *, char *, int);
int search_dispatch(struct session *, const char *, char *, int);
-/* SS8.1: completes search_dispatch() once listener_dispatch_search()
- * (listener.c) gets this session's IMSG_SEARCH_PARSE_RESULT; see
- * search_dispatch_finish()'s own comment (search_cmd.c). */
-void search_dispatch_finish(struct session *,
- const struct imsg_search_parse_result *,
- struct search_node *);
int uid_expunge_dispatch(struct session *, const char *, const char *);
void session_handle_fetch_vanished(struct session *,
const struct imsg_mbox_select_vanished *);
-/* RFC 9051 SS6.4.7/SS6.4.8 (COPY/MOVE) helpers, added this pass. */
+/* RFC 9051 SS6.4.7/SS6.4.8 (COPY/MOVE) */
int copy_move_dispatch(struct session *, const char *, char *,
int, int);
int listener_mailbox_name_valid(const char *);
int cmd_starttls(struct session *, const char *, char *);
int cmd_authenticate(struct session *, const char *, char *);
-/* command-auth (RFC 9051 SS6.3, Authenticated or Selected state). */
-/* stub_not_implemented() remains for a command added to the dispatch */
-/* table before its handler is real: NO means "recognized, cannot do it */
-/* now", which is distinct from BAD. */
+/* command-auth (RFC 9051 SS6.3) */
int stub_not_implemented(struct session *, const char *,
const char *);
int cmd_enable(struct session *, const char *, char *);
blob - 5fa792a8c473ea2d6ca2f3d1800138abfc52c3a6
blob + b2dad4bfd9b2765481ed2214adabfe90e4b5da87
--- src/log.c
+++ src/log.c
static int log_verbose = 0;
static char log_procname[32] = "imapd";
-/*
- * log_escape_ctl(): vis(3)-style caret/meta-escapes control bytes for the
- * -d/stderr trace only (syslogd already escapes syslog output), done once here
- * rather than at ~90 call sites; other high bytes and backslash pass through so
- * UTF-8 and flag names stay legible and readable one-way.
- */
+/* escapes control bytes for the -d trace; syslogd(8) escapes its own */
static char *
log_escape_ctl(const char *s)
{
return (out);
}
-/* One write(2) per line: fprintf(3) may split one, and every */
-/* process in a -d run writes to this same stderr. */
+/* one write(2) per line: every process in a -d run shares stderr */
static void
log_write_line(const char *line, size_t len)
{
log_verbose = verbose;
/* LOG_MAIL, as smtpd uses: a site's mail log rules catch both */
- /* tag is the daemon name, not the role; vlog() adds the role */
if (!log_foreground)
openlog("imapd", LOG_PID | LOG_NDELAY, LOG_MAIL);
void
log_procinit(const char *name)
{
- /*
- * truncation just shortens the prefix; name is argv[0], not attacker
- * input
- */
if (name != NULL)
(void)strlcpy(log_procname, name, sizeof(log_procname));
}
if (log_foreground) {
char *msg = NULL, *safe = NULL, *line = NULL;
- /*
- * Formats first via vasprintf(3) (untrusted text only arrives
- * through `ap`, and truncation would lose part of a command
- * echo) then escapes; reports allocation failure explicitly
- * rather than falling back to an unescaped write.
- */
if (vasprintf(&msg, fmt, ap) == -1)
msg = NULL;
if (msg != NULL)
safe = log_escape_ctl(msg);
- /* compose first, escape having already run: a newline */
- /* added before log_escape_ctl() would come out "^J" */
- /* free()d here, not below: asprintf(3) leaves *ret */
- /* undefined on failure, so it is only ours on success */
+ /* compose after escaping, or the newline becomes "^J" */
+ /* asprintf(3) leaves *ret undefined on failure */
if (safe != NULL && asprintf(&line, "%s: %s\n",
log_procname, safe) != -1) {
log_write_line(line, strlen(line));
} else {
char *nfmt;
- /* role prefix; the tag is the daemon name, see log_init() */
/* log_procname is main.c's fixed role text, safe as a format */
/* no escaping: syslogd(8) vis(3)-encodes all it receives */
if (asprintf(&nfmt, "%s: %s", log_procname, fmt) == -1)
va_list ap;
int saved_errno = errno;
- /*
- * Uses saved_errno rather than bare errno since
- * asprintf(3)/vfprintf(3)/free(3) can clobber it before it's restored
- * for the caller, and since the asprintf-failure path below needs to
- * log strerror() again after errno has already changed.
- */
if (emsg == NULL)
logit(LOG_ERR, "%s", strerror(saved_errno));
else {
- /* best-effort in appending strerror() after the format */
if (asprintf(&nfmt, "%s: %s", emsg,
strerror(saved_errno)) == -1) {
- /*
- * On asprintf() failure, log the caller's message and
- * the errno text on separate lines so the reason isn't
- * lost just because asprintf() itself failed.
- */
va_start(ap, emsg);
vlog(LOG_ERR, emsg, ap);
va_end(ap);
blob - 035822f360ad2cff9af8122f82cf23d637164bc1
blob + f315b680566be25f414789461f75694af4c4c95e
--- src/log.h
+++ src/log.h
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * Minimal logging, matching the call signatures actually observed in the
- * uploaded smtpd.c this session.
- */
-
#ifndef OPENIMAP_LOG_H
#define OPENIMAP_LOG_H
blob - e4b386b44a9a792c718718931b05f8fd535b9263
blob + 1536d4ddcfe4de80dc7e9f9403c651fae9aa611f
--- src/mailbox_cmd.c
+++ src/mailbox_cmd.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* mailbox_cmd.c: mailbox selection/management command handlers. */
#include <sys/types.h>
#include <sys/queue.h>
*errmsg = "QRESYNC requires uidvalidity and mod-sequence";
return (-1);
}
- /*
- * RFC 7162 mod-sequence-value is 1..2^63-1; strtoull(3) accepts a
- * leading sign so "-1" would parse as ULLONG_MAX, so enforce a
- * leading-digit check plus the nonzero and 63-bit-ceiling requirements.
- */
+ /* RFC 7162: 1..2^63-1; strtoull(3) accepts a sign */
if (*tok < '0' || *tok > '9') {
*errmsg = "invalid QRESYNC mod-sequence";
return (-1);
{
uint32_t i;
- /*
- * known-uids is a full RFC 9051 sequence-set (SS3.2.5.1),
- * parsed like any other sequence-set; only the "*" restriction
- * below is specific to this call site.
- */
+ /* RFC 7162 SS3.2.5.1: "*" is not allowed */
if (parse_sequence_set(tok, ranges, nranges, errmsg) == -1)
return (-1);
for (i = 0; i < *nranges; i++) {
return (0);
}
-/* SELECT/EXAMINE select-params (RFC 4466/7162); p modified in place */
+/* RFC 4466/RFC 7162 select-params */
int
parse_select_params(char *p, struct imsg_mbox_select *req,
const struct session *s, struct seq_range ranges[SEQSET_MAX_RANGES],
return (-1);
req->qresync = 1;
- /* SS3.2.3: QRESYNC implies CONDSTORE */
+ /* RFC 7162 SS3.2.3: QRESYNC implies CONDSTORE */
*want_condstore = 1;
p = end + 1;
continue;
return (0);
}
-/* RFC 9051 SS6.3.2/6.3.3; shared by SELECT/EXAMINE, quoted-string form only */
+/* RFC 9051 SS6.3.2/SS6.3.3 */
static int
select_or_examine(struct session *s, const char *tag, char *args, int readonly)
{
return (1);
}
- /*
- * Previously scanned the argument end and stripped quotes separately,
- * so an unterminated quote left the leading DQUOTE attached and reached
- * the store as a bogus mailbox name.
- */
p = args;
if (parse_mailbox_name(&p, mailbox, sizeof(mailbox), &errmsg) == -1) {
session_reply(s, tag, "BAD", errmsg);
}
if (s->store_iev == NULL) {
- /*
- * should already be wired, 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");
}
}
- /*
- * RFC 7162 SS3.1.8/3.2.3: not session_condstore_enable(); has
- * HIGHESTMODSEQ
- */
+ /* RFC 7162 SS3.1.8/SS3.2.3 */
if (want_condstore)
s->condstore_enabled = 1;
return select_or_examine(s, tag, args, 1);
}
-/*
- * Why the listener validates a mailbox name at all, given the store validates
- * it again: answering NO here saves a round trip for a name that can never be
- * valid. This used to be a hand-copied duplicate of store.c's rule and had
- * already drifted once; both sides now call the one predicate in mboxname.c,
- * and testing/mailbox_name_test.c drives both entry points over one table so a
- * future one-sided edit fails there.
- */
+/* checked here to save a round trip; the store checks again */
int
listener_mailbox_name_valid(const char *name)
{
return (mailbox_name_syntax_ok(name));
}
-/*
- * Shared refusal for a non-UTF-8 mailbox name on
- * CREATE/RENAME-dest/COPY-MOVE-target; existing-name commands use NONEXISTENT
- * instead, and NO (not BAD) matches SS6.3.4's CREATE failure wording and RFC
- * 5530's CANNOT semantics. Returns 1 if it replied and the caller should stop.
- */
int
listener_reject_bad_utf8(struct session *s, const char *tag, const char *name)
{
return (1);
}
-/* RFC 9051 SS6.3.4 CREATE; "exists" is store.c's call (mbox_create() EEXIST) */
+/* RFC 9051 SS6.3.4 CREATE */
int
cmd_create(struct session *s, const char *tag, char *args)
{
return (1);
}
- /*
- * CREATE takes no arguments after the mailbox name; reject trailing
- * garbage rather than silently ignoring it as `CREATE "a"b` used to.
- * (Future CREATE-SPECIAL-USE, RFC 6154, would check here.)
- */
+ /* no arguments may follow the name */
while (*p == ' ')
p++;
if (*p != '\0') {
s->mbox_op_prev_state = s->state;
s->state = SESSION_CREATING;
- /*
- * A failed compose used to leave s->state stuck at SESSION_CREATING, so
- * session_is_busy() blocked forever waiting for a reply that would
- * never come; reset state and answer instead, same fail-soft shape
- * every other send_mbox_request() caller uses. The helper logs the
- * failure itself.
- */
+ /* a failed compose must not leave the session busy */
if (!send_mbox_request(s, IMSG_MBOX_CREATE, "CREATE",
"IMSG_MBOX_CREATE", &req, sizeof(req), NULL, 0, 0)) {
s->state = s->mbox_op_prev_state;
return (1);
}
-/* RFC 9051 SS6.3.5 DELETE; existence check is store.c's handle_mbox_delete() */
+/* RFC 9051 SS6.3.5 DELETE */
int
cmd_delete(struct session *s, const char *tag, char *args)
{
return (1);
}
- /*
- * DELETE takes no arguments after the mailbox name; reject trailing
- * garbage rather than silently ignoring it as `DELETE "a"b` used to.
- * (Future CREATE-SPECIAL-USE, RFC 6154, would check here.)
- */
+ /* no arguments may follow the name */
while (*p == ' ')
p++;
if (*p != '\0') {
s->mbox_op_prev_state = s->state;
s->state = SESSION_DELETING;
- /* see cmd_create()'s comment on this failure path */
if (!send_mbox_request(s, IMSG_MBOX_DELETE, "DELETE",
"IMSG_MBOX_DELETE", &req, sizeof(req), NULL, 0, 0)) {
s->state = s->mbox_op_prev_state;
return (1);
}
-/* RFC 9051 SS6.3.6 RENAME; *from* INBOX refused client-side, RFC-sanctioned */
+/* RFC 9051 SS6.3.6 RENAME */
int
cmd_rename(struct session *s, const char *tag, char *args)
{
}
if (mailbox_name_is_inbox(oldname)) {
- /*
- * RFC 9051 SS6.3.6 sanctions this refusal; RFC 5530 CANNOT is
- * closest fit
- */
+ /* RFC 9051 SS6.3.6 allows refusing this */
session_reply(s, tag, "NO", "[CANNOT] cannot rename INBOX");
return (1);
}
return (1);
}
- /*
- * RENAME takes no arguments after the mailbox name; reject trailing
- * garbage rather than silently ignoring it as `RENAME "a"b` used to.
- * (Future CREATE-SPECIAL-USE, RFC 6154, would check here.)
- */
+ /* no arguments may follow the name */
while (*p == ' ')
p++;
if (*p != '\0') {
s->mbox_op_prev_state = s->state;
s->state = SESSION_RENAMING;
- /* see cmd_create()'s comment on this failure path */
if (!send_mbox_request(s, IMSG_MBOX_RENAME, "RENAME",
"IMSG_MBOX_RENAME", &req, sizeof(req), NULL, 0, 0)) {
s->state = s->mbox_op_prev_state;
}
-/*
- * RFC 9051 SS6.3.7 (SUBSCRIBE) and SS6.3.8 (UNSUBSCRIBE): one mailbox name,
- * one terminal IMSG_MBOX_RESULT, same shape as CREATE.
- */
+/* RFC 9051 SS6.3.7 SUBSCRIBE and SS6.3.8 UNSUBSCRIBE */
static int
subscribe_dispatch(struct session *s, const char *tag, char *args,
int is_unsub)
if (listener_reject_bad_utf8(s, tag, mailbox))
return (1);
- /*
- * INBOX is permanently subscribed: SS5.1 guarantees it exists and
- * nothing can delete it, so it is never named in the subscription
- * file. SUBSCRIBE is the no-op that succeeds (SS6.3.7 returns OK for
- * an already-subscribed name); UNSUBSCRIBE is the refusal SS6.3.8's
- * result table allows. Answered here rather than round-tripping,
- * since the store would reach these two answers from the same two
- * facts.
- */
+ /* RFC 9051 SS5.1: INBOX is always subscribed */
if (mailbox_name_is_inbox(mailbox)) {
if (is_unsub) {
session_reply(s, tag, "NO",
s->mbox_op_prev_state = s->state;
s->state = is_unsub ? SESSION_UNSUBSCRIBING : SESSION_SUBSCRIBING;
- /* see cmd_create()'s comment on this failure path */
if (!send_mbox_request(s, is_unsub ? IMSG_MBOX_UNSUBSCRIBE :
IMSG_MBOX_SUBSCRIBE, cmdname, imsgname, &req, sizeof(req), NULL,
0, 0)) {
return subscribe_dispatch(s, tag, args, 1);
}
-/* RFC 9051 SS6.3.9 wildcards, "zero+ of anything"; flat namespace, no delim */
+/* RFC 9051 SS6.3.9 wildcards; a flat namespace */
int
list_pattern_match(const char *pat, const char *name, int ci)
{
const char *p = pat;
const char *s = name;
- /* pat pos past most recent wildcard run */
const char *star_p = NULL;
- /* name pos wildcard absorbed through so far */
const char *star_s = NULL;
/* iterative two-pointer glob(3) match, O(n*m); no backtrack blowup */
return (*p == '\0');
}
-/*
- * Renders a mailbox name as an RFC 9051 SS4.3 quoted string (with DQUOTEs),
- * escaping DQUOTE and backslash -- without this, a name containing those
- * characters broke the client's parse of the response. out must be
- * MBOX_QUOTED_MAX bytes; returns 0, or -1 (with a shorter well-formed result)
- * if it didn't fit.
- */
+/* RFC 9051 SS4.3 quoted string */
int
quote_mailbox(char *out, size_t outsize, const char *name)
{
char c = name[i];
size_t need;
- /*
- * SS4.3's TEXT-CHAR excludes NUL/CR/LF; substitute rather than
- * drop, matching envbuf_append_nstring() -- a boundary guard
- * since mailbox_name_valid() already refuses bytes below 0x20.
- */
+ /* RFC 9051 SS4.3: no NUL, CR or LF */
if (c == '\r' || c == '\n')
c = ' ';
return (0);
}
-/*
- * Distinguishes ASTRING-CHAR from list-char: both add resp-specials back, but
- * only list-wildcards ("%","*") differ, mattering for LIST "" * ; deliberately
- * lenient on 8-bit bytes (unquoted UTF-8 allowed) since only atom-specials
- * actually corrupt parsing.
- */
static int
atom_char_ok(unsigned char c, int wildcards)
{
return (1);
}
-/*
- * The one mailbox-argument parser: pulls a decoded SS9 astring (or list-mailbox
- * when wildcards) off *pp, replacing three divergent hand-written copies that
- * mishandled quote-escaping and atom-specials; literals are refused outright
- * since the listener's literal machinery is terminal-only; does not validate
- * the name itself. Returns 0, or -1 with *errmsg set for a tagged BAD.
- */
+/* RFC 9051 SS9 astring, or list-mailbox with wildcards */
static int
parse_mailbox_arg(char **pp, char *out, size_t outsize, int wildcards,
const char **errmsg)
break;
}
if (*p == '\\') {
- /*
- * SS9 QUOTED-CHAR escapes exactly the two
- * quoted-specials, nothing else
- */
+ /* RFC 9051 SS9 QUOTED-CHAR */
p++;
if (*p == '\0') {
- /*
- * a trailing backslash: the string ran
- * out, not a bad escape
- */
+ /* a trailing backslash */
*errmsg = "unterminated quoted string";
return (-1);
}
return (0);
}
-/*
- * Two grammars named explicitly rather than via a boolean: LIST's REFERENCE is
- * a plain mailbox while its PATTERN allows wildcards (RFC 9051 SS9); every
- * other mailbox-taking command uses the strict grammar.
- */
int
parse_mailbox_name(char **pp, char *out, size_t outsize, const char **errmsg)
{
return (parse_mailbox_arg(pp, out, outsize, 1, errmsg));
}
-/* RFC 9051 SS6.3.9 basic syntax only, no list-opts; LSUB just varies output */
+/* RFC 9051 SS6.3.9 basic syntax; LSUB varies only the output */
static int
list_dispatch(struct session *s, const char *tag, char *args, int is_lsub)
{
p++;
if (*p == '(') {
- /*
- * SS6.3.9 cond 1: the first word after the command starts
- * "(", so it is list-select-opts. SUBSCRIBED (SS6.3.9.1) is
- * the only one implemented; REMOTE and RECURSIVEMATCH are
- * not, and quietly ignoring either would misreport the set
- * of names returned. LSUB has no such form at all (RFC 3501
- * SS6.3.9), so "(" there is a syntax error.
- */
+ /* RFC 9051 SS6.3.9 condition 1: list-select-opts */
if (is_lsub) {
session_reply(s, tag, "BAD",
"LSUB takes no selection options");
p++;
}
- /*
- * SS9: list's reference is a plain `mailbox`; mbox-or-pat takes
- * wildcards
- */
if (parse_mailbox_name(&p, reference, sizeof(reference), &errmsg) ==
-1) {
session_reply(s, tag, "BAD", errmsg);
p++;
if (*p == '(') {
- /*
- * SS6.3.9 cond 2: second word starts "(", parenthesized
- * `patterns` form
- */
+ /* RFC 9051 SS6.3.9 condition 2 */
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
- */
+ /* RFC 9051 SS6.3.9 condition 3 */
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 delimiter/root; root is
- * empty
- */
+ /* RFC 9051 SS6.3.9: an empty pattern asks for the delimiter */
if (pattern[0] == '\0') {
snprintf(text, sizeof(text), "%s (\\Noselect) \"/\" \"\"", kw);
session_untagged(s, text);
return (1);
}
- /*
- * canonical LIST pattern: reference + mailbox pattern (RFC 9051
- * SS6.3.9)
- */
{
size_t n;
}
}
- /*
- * SS6.3.9: unaccepted pattern MUST be ignored; INBOX answered
- * synchronously
- */
+ /* RFC 9051 SS6.3.9: INBOX is answered here */
if (list_pattern_match(canon, "INBOX", 1)) {
- /*
- * SS7.3.1 makes attributes optional and INBOX is selectable,
- * so "()" unless the client asked about subscription state:
- * INBOX is permanently subscribed here, since SS5.1
- * guarantees it exists and nothing can delete it. LSUB keeps
- * "()" because RFC 3501 has no \Subscribed. Quoted (though
- * bare INBOX also parses) so this listener-only answer
- * matches how SELECT's LIST line spells INBOX elsewhere,
- * keeping one consistent spelling per session.
- */
+ /* RFC 9051 SS7.3.1: attributes are optional */
snprintf(text, sizeof(text), "%s (%s) \"/\" \"INBOX\"", kw,
subscribed_only ? "\\Subscribed" : "");
session_untagged(s, text);
return (1);
}
- /* real names on disk; MBOX_LIST has no payload, pattern per name */
if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
sizeof(s->pending_tag) ||
strlcpy(s->list_pattern, canon, sizeof(s->list_pattern)) >=
return (1);
}
s->list_is_lsub = is_lsub;
- /* LSUB asks for subscribed names by definition (RFC 3501 SS6.3.9) */
+ /* LSUB lists subscribed names (RFC 3501 SS6.3.9) */
s->list_subscribed_only = subscribed_only || is_lsub;
s->mbox_op_prev_state = s->state;
s->state = SESSION_LISTING;
memset(&req, 0, sizeof(req));
req.subscribed_only = s->list_subscribed_only;
- /* see cmd_create()'s comment on this failure path */
if (!send_mbox_request(s, IMSG_MBOX_LIST, cmdname, "IMSG_MBOX_LIST",
&req, sizeof(req), NULL, 0, 0)) {
s->state = s->mbox_op_prev_state;
return list_dispatch(s, tag, args, 1);
}
-/* RFC 9051 SS6.3.10 NAMESPACE, answered locally: Personal NS, "/" delimiter */
+/* RFC 9051 SS6.3.10 NAMESPACE */
int
cmd_namespace(struct session *s, const char *tag, char *args)
{
return (1);
}
-/* RFC 9051 SS6.3.11 STATUS; doesn't change mbox, STATUSING is transient wait */
+/* RFC 9051 SS6.3.11 STATUS */
int
cmd_status(struct session *s, const char *tag, char *args)
{
else if (strcasecmp(tok, "HIGHESTMODSEQ") == 0)
attrs |= STATUS_ATT_HIGHESTMODSEQ;
else if (strcasecmp(tok, "RECENT") == 0)
- /*
- * IMAP4rev2 dropped RECENT (RFC 9051 SS2.3.2), but real
- * clients (Canary Mail) still ask for it -- accept it
- * and answer 0 rather than BAD-failing the whole
- * command.
- */
+ /* RFC 9051 SS2.3.2 dropped it; clients ask */
attrs |= STATUS_ATT_RECENT;
else {
session_reply(s, tag, "BAD", "unknown status-att");
}
}
- /*
- * RFC 9051 SS9: status-att-list needs >=1 status-att, unlike response
- * side
- */
+ /* RFC 9051 SS9: at least one status-att */
if (attrs == 0) {
session_reply(s, tag, "BAD",
"STATUS requires at least one status-att");
return (1);
}
- /*
- * RFC 7162 SS3.1: STATUS HIGHESTMODSEQ is CONDSTORE-enabling;
- * pre-overwrite
- */
+ /* RFC 7162 SS3.1: STATUS HIGHESTMODSEQ enables CONDSTORE */
if (attrs & STATUS_ATT_HIGHESTMODSEQ)
session_condstore_enable(s);
s->status_prev_state = s->state;
s->state = SESSION_STATUSING;
- /* see cmd_create(); restores status_prev_state, not mbox_op */
if (!send_mbox_request(s, IMSG_MBOX_STATUS, "STATUS",
"IMSG_MBOX_STATUS", &req, sizeof(req), NULL, 0, 0)) {
s->state = s->status_prev_state;
blob - ca9384ac7b6f9e505d2c2c19bd7e1ca7ccd0231a
blob + 07161abf280fd522a6ce6a00dc337ee613c5a0b9
--- src/main.c
+++ src/main.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* main.c: imapd(8) entry point, dispatches to the role named by "-x". */
-
#include <sys/types.h>
#include <err.h>
{ "auth", PROC_AUTH },
{ "store", PROC_STORE },
{ "keymgr", PROC_KEYMGR },
- { "search", PROC_SEARCH },
+ { "parser", PROC_PARSER },
};
const char *
return ("store");
case PROC_KEYMGR:
return ("keymgr");
- case PROC_SEARCH:
- return ("search");
+ case PROC_PARSER:
+ return ("parser");
default:
return ("?");
}
__dead static void
usage(void)
{
- /*
- * -x is absent from getopt (imapd.8): names a re-exec'd child's role
- * only.
- */
fprintf(stderr,
"usage: %s [-dVv] [-D macro=value] [-f file]\n",
getprogname());
memset(&conf, 0, sizeof(conf));
- /*
- * Set before any fork(2) so SIG_IGN survives into every role
- * (parent/listener/auth/store) via fork+execve -- this is the one place
- * that reliably reaches all of them.
- */
+ /* before any fork(2), so every role inherits SIG_IGN */
signal(SIGPIPE, SIG_IGN);
while ((ch = getopt(argc, argv, "D:df:Vvx:")) != -1) {
switch (ch) {
case 'V':
- /*
- * exits before log_init()/config_load(); works with no
- * config/privsep
- */
printf("%s %s\n", getprogname(), IMAPD_VERSION);
return (0);
case 'D':
- /*
- * "-D name=value" macro, applied to parse.y's symtab
- * pre-config_load()
- */
if (cmdline_symset(optarg) == -1)
fatalx("could not parse macro definition %s",
optarg);
log_procinit(log_procname(role));
log_init(debug, verbose);
- /*
- * re-exec'd children get config via fd 3 (IMSG_*_INIT); no conf arg
- * needed
- */
switch (role) {
case PROC_PARENT:
/* before config_load(): imapd.conf is root-only */
- /* else a non-root start reads as a config error */
if (geteuid() != 0)
fatalx("parent must start as root (running as "
"uid %u)", (unsigned int)geteuid());
store_main();
case PROC_KEYMGR:
keymgr_main();
- case PROC_SEARCH:
- search_oracle_main();
+ case PROC_PARSER:
+ parser_main();
}
fatalx("unhandled role %d", role);
blob - 8eed34ba60988e1609e365ec12afb7c28e189a5c
blob + 03da0e60a5cf8a9436a9f42d69541fb632f44545
--- src/mbox_copy.c
+++ src/mbox_copy.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* mbox_copy.c: COPY and MOVE handling, same-mailbox and cross-mailbox. */
-
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
return (-1);
}
-/* Pass 1 of COPY/MOVE: stages read-only; 0 means partial failure (SS6.4.7) */
-
static int
-stage_copy_messages(int dfd, struct mbox_index *idx,
+stage_copy_messages(struct store_session *ss, struct mbox_index *idx,
struct imsg_mbox_copy *req, const struct seq_range *ranges,
- uint32_t nranges, struct copy_staged **staged_out,
- size_t *nstaged_out)
+ uint32_t nranges, struct copy_staged **staged_out, size_t *nstaged_out)
{
struct copy_staged *staged = NULL;
size_t nstaged = 0, stagedcap = 0, i;
- /* bytes staged so far */
- uint64_t staged_total = 0;
struct seq_range resolved[SEQSET_MAX_RANGES];
uint32_t nresolved, max_hi;
- /*
- * "*" and backwards-range swaps (RFC 9051 SS9) are handled by
- * seqset_resolve(): seqno-space hi is clamped to idx->nlines, UID-space
- * hi is left alone since an out-of-range UID just matches nothing
- * extra.
- */
nresolved = seqset_resolve(ranges, nranges, req->by_uid ?
index_max_uid(idx) : (uint32_t)idx->nlines, !req->by_uid,
resolved);
char suffix[64];
off_t size;
char path[600];
- int srcfd;
struct copy_staged cs;
if (index_parse_line(idx->lines[i], &rec) == -1)
continue;
- /*
- * same check as handle_mbox_fetch()/_store(); 0-based loop,
- * seqno is i+1
- */
pos = seqset_position(resolved, nresolved, max_hi, req->by_uid,
rec.uid, (uint32_t)(i + 1));
if (pos == SEQSET_PAST_END)
if (pos == SEQSET_SKIP)
continue;
- if (locate_message_file(mailbox_dir_fd, rec.basename,
- &size, suffix, sizeof(suffix)) == -1) {
+ if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+ rec.basename, &size, suffix, sizeof(suffix)) == -1) {
log_warnx("session %u: COPY: message %s indexed but "
"missing on disk, failing whole COPY (partial "
"copy not permitted, RFC 9051 SS6.4.7)",
goto fail;
}
- /*
- * refuse before allocating if message/running total exceeds
- * 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",
- 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);
- goto fail;
- }
- staged_total += (uint64_t)size;
-
memset(&cs, 0, sizeof(cs));
cs.src_uid = rec.uid;
if (strlcpy(cs.keywords, rec.keywords, sizeof(cs.keywords)) >=
"%s, failing COPY", session_id, rec.basename);
goto fail;
}
- /*
- * Moved here from commit_copy_messages()'s second loop, which
- * used to fail a bad ':'/newline in keywords only after files
- * were already renamed; checking before anything is written
- * matches every other refusal in this function, and
- * commit_copy_messages() keeps its own copy as defence in
- * depth.
- */
+ /* refuse before any file is written */
if (!index_field_valid(cs.keywords)) {
log_warnx("session %u: COPY: unsafe keywords field on "
"%s, failing COPY", session_id, rec.basename);
"long", session_id);
goto fail;
}
- /*
- * Same reasoning as the keywords check above; a failure here
- * means a locally generated hostname or timestamp carries a ':'
- * or newline.
- */
if (!index_basename_valid(cs.basename)) {
log_warnx("session %u: COPY: generated basename is "
"unsafe for the index, failing COPY", session_id);
"for %s", session_id, rec.basename);
goto fail;
}
- if ((srcfd = openat(dfd, path, O_RDONLY)) == -1) {
- log_warn("session %u: COPY: open %s", session_id,
- path);
+ if ((cs.srcpath = strdup(path)) == NULL) {
+ log_warn("session %u: COPY: strdup source path",
+ session_id);
goto fail;
}
- cs.bodylen = (size_t)size;
- if (cs.bodylen > 0 && (cs.body = malloc(cs.bodylen)) == NULL) {
- log_warn("session %u: COPY: malloc %zu bytes",
- session_id, cs.bodylen);
- close(srcfd);
- goto fail;
- }
- {
- size_t rd = 0;
+ cs.srcsize = size;
- while (rd < cs.bodylen) {
- ssize_t n = read(srcfd,
- (char *)cs.body + rd, cs.bodylen - rd);
- if (n == -1) {
- if (errno == EINTR)
- continue;
- log_warn("session %u: COPY: read %s",
- session_id, path);
- close(srcfd);
- free(cs.body);
- goto fail;
- }
- if (n == 0) {
- /*
- * The file shrank between
- * locate_message_file()'s stat and this
- * read; copying the truncated prefix
- * would answer OK for a partial
- * message, violating RFC 9051 SS6.4.7's
- * all-or-nothing COPY, so fail the
- * whole operation like every other
- * integrity failure here.
- */
- log_warnx("session %u: COPY: %s shrank "
- "during staging (%zu of %zu bytes "
- "read), failing COPY", session_id,
- path, rd, cs.bodylen);
- close(srcfd);
- free(cs.body);
- goto fail;
- }
- rd += (size_t)n;
- }
- }
- close(srcfd);
-
if (nstaged == stagedcap) {
size_t newcap = (stagedcap == 0) ? 8 :
stagedcap * 2;
if (newstaged == NULL) {
log_warn("session %u: COPY: reallocarray "
"staged", session_id);
- free(cs.body);
+ free(cs.srcpath);
goto fail;
}
staged = newstaged;
fail:
for (i = 0; i < nstaged; i++)
- free(staged[i].body);
+ free(staged[i].srcpath);
free(staged);
*staged_out = NULL;
*nstaged_out = 0;
return (0);
}
-/* Pass 2+3: writes tmp/, renames to cur/, index_append(); no rollback. */
+/* linkat(2) failed: copy through tmp/, as APPEND does */
+static int
+copy_message_file(struct store_session *ss, const struct copy_staged *cs,
+ int dfd, const char *curpath)
+{
+ static char buf[65536];
+ char tmppath[300];
+ off_t left = cs->srcsize;
+ int srcfd, tmpfd;
+ if (snprintf(tmppath, sizeof(tmppath), "tmp/%s", cs->basename) >=
+ (int)sizeof(tmppath)) {
+ log_warnx("session %u: COPY: tmp path too long", session_id);
+ return (-1);
+ }
+ if ((srcfd = openat(ss->mailbox_dir_fd, cs->srcpath, O_RDONLY)) == -1) {
+ log_warn("session %u: COPY: open %s", session_id, cs->srcpath);
+ return (-1);
+ }
+ if ((tmpfd = openat(dfd, tmppath, O_WRONLY | O_CREAT | O_EXCL,
+ 0600)) == -1) {
+ log_warn("session %u: COPY: open %s", session_id, tmppath);
+ close(srcfd);
+ return (-1);
+ }
+ while (left > 0) {
+ size_t want, written = 0;
+ ssize_t n, w;
+
+ want = left < (off_t)sizeof(buf) ? (size_t)left : sizeof(buf);
+ if ((n = read(srcfd, buf, want)) == -1) {
+ if (errno == EINTR)
+ continue;
+ log_warn("session %u: COPY: read %s", session_id,
+ cs->srcpath);
+ goto fail;
+ }
+ /* shrank since its stat(2); a copy now would be partial */
+ if (n == 0) {
+ log_warnx("session %u: COPY: %s shrank during copy, "
+ "failing COPY", session_id, cs->srcpath);
+ goto fail;
+ }
+ while (written < (size_t)n) {
+ if ((w = write(tmpfd, buf + written,
+ (size_t)n - written)) == -1) {
+ if (errno == EINTR)
+ continue;
+ log_warn("session %u: COPY: write %s",
+ session_id, tmppath);
+ goto fail;
+ }
+ written += (size_t)w;
+ }
+ left -= n;
+ }
+ if (fsync(tmpfd) == -1)
+ log_warn("session %u: COPY: fsync %s (continuing)", session_id,
+ tmppath);
+ close(tmpfd);
+ close(srcfd);
+ if (renameat(dfd, tmppath, dfd, curpath) == -1) {
+ log_warn("session %u: COPY: rename %s -> %s", session_id,
+ tmppath, curpath);
+ unlinkat(dfd, tmppath, 0);
+ return (-1);
+ }
+ return (0);
+
+fail:
+ close(tmpfd);
+ close(srcfd);
+ unlinkat(dfd, tmppath, 0);
+ return (-1);
+}
+
static int
-commit_copy_messages(int dfd, struct mbox_index *destidx,
- struct copy_staged *staged, size_t nstaged, struct imsgev *iev)
+commit_copy_messages(struct store_session *ss, int dfd,
+ struct mbox_index *destidx, struct copy_staged *staged, size_t nstaged)
{
size_t i;
size_t ncommitted = 0; /* entries whose file is already in cur/ */
for (i = 0; i < nstaged; i++) {
- char tmppath[300], curpath[320];
+ char curpath[320];
char letters[8];
- int tmpfd;
- if (snprintf(tmppath, sizeof(tmppath), "tmp/%s",
- staged[i].basename) >= (int)sizeof(tmppath)) {
- log_warnx("session %u: COPY: tmp path too long",
- session_id);
- goto rollback;
- }
- if ((tmpfd = openat(dfd, tmppath, O_WRONLY | O_CREAT | O_EXCL,
- 0600)) == -1) {
- log_warn("session %u: COPY: open %s", session_id,
- tmppath);
- goto rollback;
- }
- {
- size_t written = 0;
-
- while (written < staged[i].bodylen) {
- ssize_t n = write(tmpfd,
- (char *)staged[i].body + written,
- staged[i].bodylen - written);
- if (n == -1) {
- if (errno == EINTR)
- continue;
- log_warn("session %u: COPY: write %s",
- session_id, tmppath);
- close(tmpfd);
- unlinkat(dfd, tmppath, 0);
- goto rollback;
- }
- written += (size_t)n;
- }
- }
- if (fsync(tmpfd) == -1)
- log_warn("session %u: COPY: fsync %s (continuing)",
- session_id, tmppath);
- close(tmpfd);
-
sysflags_to_letters(staged[i].sysflags, letters,
sizeof(letters));
if (snprintf(curpath, sizeof(curpath), "cur/%s:2,%s",
staged[i].basename, letters) >= (int)sizeof(curpath)) {
log_warnx("session %u: COPY: cur path too long",
session_id);
- unlinkat(dfd, tmppath, 0);
goto rollback;
}
- if (renameat(dfd, tmppath, dfd, curpath) == -1) {
- log_warn("session %u: COPY: rename %s -> %s",
- session_id, tmppath, curpath);
- unlinkat(dfd, tmppath, 0);
- goto rollback;
+ /* same bytes, new name: a maildir message is never rewritten */
+ if (linkat(ss->mailbox_dir_fd, staged[i].srcpath, dfd, curpath,
+ AT_SYMLINK_FOLLOW) == -1) {
+ /* not copied: the copy's rename(2) would replace it */
+ if (errno == EEXIST) {
+ log_warn("session %u: COPY: link %s -> %s",
+ session_id, staged[i].srcpath, curpath);
+ goto rollback;
+ }
+ log_debug("session %u: COPY: link %s: %s, copying "
+ "instead", session_id, staged[i].srcpath,
+ strerror(errno));
+ if (copy_message_file(ss, &staged[i], dfd,
+ curpath) == -1)
+ goto rollback;
}
ncommitted = i + 1;
}
for (i = 0; i < nstaged; i++) {
uint32_t dest_uid = destidx->uidnext;
- /*
- * stage_copy_messages() already refused these; kept as defence
- * in depth
- */
+ /* refused already; defence in depth */
if (!index_basename_valid(staged[i].basename) ||
!index_field_valid(staged[i].keywords)) {
log_warnx("session %u: COPY: unsafe field in staged "
memset(&mapping, 0, sizeof(mapping));
mapping.src_uid = staged[i].src_uid;
mapping.dest_uid = staged[i].dest_uid;
- if (imsg_compose(&iev->ibuf, IMSG_MBOX_COPY_MAPPING, 0, 0, -1,
- &mapping, sizeof(mapping)) == -1)
+ if (imsg_compose(&ss->iev.ibuf, IMSG_MBOX_COPY_MAPPING, 0, 0,
+ -1, &mapping, sizeof(mapping)) == -1)
log_warn("session %u: imsg_compose "
"IMSG_MBOX_COPY_MAPPING", session_id);
}
return (1);
rollback:
- /*
- * RFC 9051 SS6.4.7 forbids partial copy: a failure before index_save()
- * leaves observable state untouched but orphans unlinked files
- * (invisible but never reclaimed, leaking on retry), so undo the
- * renames; an unlink failure here is logged and stepped over rather
- * than aborting cleanup.
- */
+ /* RFC 9051 SS6.4.7: no partial copy */
while (ncommitted > 0) {
char curpath[320];
char letters[8];
return (0);
}
-/*
- * Shared COPY/MOVE prefix: resolves dest, locks the SELECTed mailbox's index
- * (and, cross-mailbox, the dest's too, in strcmp() order to dodge AB-BA
- * deadlock), loads both. Returns 1 locked and loaded; 0 failed, with any
- * descriptor it opened closed; or 2 when a lock is busy, holding nothing,
- * since the command is run again from the top (store.c).
- */
static int
-lock_copy_move_mailboxes(const char *what, const char *destname,
- int *destfd_out, struct mbox_index *idx_a, struct mbox_index *idx_b,
- struct mbox_index **srcidx_out, struct mbox_index **destidx_out,
- char *desttarget, size_t desttargetlen, int *cross_mailbox_out,
- struct index_lock *il_a, struct index_lock *il_b,
+lock_copy_move_mailboxes(struct store_session *ss, const char *what,
+ const char *destname, int *destfd_out, struct mbox_index *idx_a,
+ struct mbox_index *idx_b, struct mbox_index **srcidx_out,
+ struct mbox_index **destidx_out, char *desttarget, size_t desttargetlen,
+ int *cross_mailbox_out, struct index_lock *il_a, struct index_lock *il_b,
struct imsg_mbox_result *result)
{
int firstfd, secondfd, first_is_dest, locked;
result->error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
return (0);
}
- *cross_mailbox_out = (strcmp(selected_mailbox, desttarget) != 0);
+ *cross_mailbox_out = (strcmp(ss->selected_mailbox, desttarget) != 0);
if (!*cross_mailbox_out) {
- locked = index_lock_acquire(mailbox_dir_fd, il_a,
+ locked = index_lock_acquire(ss->mailbox_dir_fd, il_a,
LOCK_EX | LOCK_NB);
if (locked == 1)
return (2);
return (0);
}
- /*
- * Both indexes are locked at once, so take them in name order:
- * two sessions copying in opposite directions would otherwise
- * deadlock.
- */
- first_is_dest = strcmp(selected_mailbox, desttarget) > 0;
- firstfd = first_is_dest ? *destfd_out : mailbox_dir_fd;
- secondfd = first_is_dest ? mailbox_dir_fd : *destfd_out;
+ /* both locked at once: take them in name order, against AB-BA */
+ first_is_dest = strcmp(ss->selected_mailbox, desttarget) > 0;
+ firstfd = first_is_dest ? *destfd_out : ss->mailbox_dir_fd;
+ secondfd = first_is_dest ? ss->mailbox_dir_fd : *destfd_out;
locked = index_lock_acquire(firstfd, il_a, LOCK_EX | LOCK_NB);
if (locked != 0)
return (0);
}
-/* Common COPY/MOVE teardown: unlocks/closes index fd(s), frees index(es) */
static void
finish_copy_move(struct mbox_index *idx_a, struct mbox_index *idx_b,
struct index_lock *il_a, struct index_lock *il_b, int ok,
struct imsg_mbox_result *result, struct imsgev *iev)
{
- /* both idempotent, safe on an INDEX_LOCK_INIT struct never acquired */
index_lock_release(il_a);
index_lock_release(il_b);
session_id);
}
-/* RFC 9051 SS6.4.7 COPY; if dest != SELECTed, indexes lock in strcmp() order */
+/* RFC 9051 SS6.4.7 COPY */
int
handle_mbox_copy(struct imsg_mbox_copy *req, const struct seq_range *ranges,
- uint32_t nranges, struct imsgev *iev)
+ uint32_t nranges, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct mbox_index idx_a, idx_b;
struct mbox_index *srcidx = NULL, *destidx = NULL;
struct imsg_mbox_result result;
memset(&idx_b, 0, sizeof(idx_b));
memset(&result, 0, sizeof(result));
- got = lock_copy_move_mailboxes("COPY", req->destname, &destfd,
+ got = lock_copy_move_mailboxes(ss, "COPY", req->destname, &destfd,
&idx_a, &idx_b, &srcidx, &destidx, desttarget,
sizeof(desttarget), &cross_mailbox, &il_a, &il_b, &result);
if (got == 2)
ok = 0;
goto done;
}
- dfd = cross_mailbox ? destfd : mailbox_dir_fd;
+ dfd = cross_mailbox ? destfd : ss->mailbox_dir_fd;
- if (!stage_copy_messages(mailbox_dir_fd, srcidx, req, ranges,
- nranges, &staged, &nstaged)) {
+ if (!stage_copy_messages(ss, srcidx, req, ranges, nranges, &staged,
+ &nstaged)) {
ok = 0;
goto close_dest;
}
ok = 0;
goto close_dest;
}
- if (!commit_copy_messages(dfd, destidx, staged, nstaged, iev))
+ if (!commit_copy_messages(ss, dfd, destidx, staged, nstaged))
ok = 0;
}
done:
for (i = 0; i < nstaged; i++)
- free(staged[i].body);
+ free(staged[i].srcpath);
free(staged);
finish_copy_move(&idx_a, &idx_b, &il_a, &il_b, ok, &result, iev);
return (0);
}
-/*
- * Repairs move_same_mailbox()'s in-place compaction when it abandons partway:
- * slides the unvisited tail down over the stale duplicate region left by the
- * partial compaction and returns the new length, so index_free() doesn't
- * double-free; the on-disk index is untouched since the caller never reaches
- * index_save() on this path.
- */
static size_t
compaction_bail(struct mbox_index *idx, size_t dropped, size_t out)
{
return (out);
}
-/*
- * MOVE within same mailbox: range membership below must test "in" (read pos),
- * not "out" (compaction write index), "out" under-consumed the range.
- */
+/* test the read position, not the write index */
static int
move_same_mailbox(struct imsg_mbox_copy *req, const struct seq_range *ranges,
uint32_t nranges, struct mbox_index *idx, uint32_t *nmoved_out,
- struct imsgev *iev)
+ struct store_session *ss)
{
struct moved {
uint32_t old_uid;
char keywords[512];
};
+ struct imsgev *iev = &ss->iev;
struct moved *moved = NULL;
size_t nmoved = 0, movedcap = 0, in, out, i;
struct seq_range resolved[SEQSET_MAX_RANGES];
*nmoved_out = 0;
- /*
- * "*"/backwards-range swaps (RFC 9051 SS9) handled by seqset_resolve()
- * now
- */
nresolved = seqset_resolve(ranges, nranges, req->by_uid ?
index_max_uid(idx) : (uint32_t)idx->nlines, !req->by_uid,
resolved);
}
}
- if (index_save(mailbox_dir_fd, idx) == -1) {
+ if (index_save(ss->mailbox_dir_fd, idx) == -1) {
ok = 0;
goto done;
}
- /* COPYUID mapping data first... */
+ /* COPYUID mapping first */
for (i = 0; i < nmoved; i++) {
struct imsg_mbox_copy_mapping mapping;
log_warn("session %u: imsg_compose "
"IMSG_MBOX_COPY_MAPPING", session_id);
}
- /* ...then EXPUNGE notices for old UIDs/seqnos (RFC 9051 SS6.4.8) */
+ /* then EXPUNGE (RFC 9051 SS6.4.8) */
for (i = 0; i < nmoved; i++) {
struct imsg_mbox_expunged exp;
return (ok);
}
-/* Cross-mailbox MOVE (SS6.4.8): dest commits before removal, no lost mail */
+/* RFC 9051 SS6.4.8: dest commits before removal */
static int
move_cross_mailbox(struct imsg_mbox_copy *req, const struct seq_range *ranges,
uint32_t nranges, struct mbox_index *srcidx, struct mbox_index *destidx,
- int destfd, uint32_t *nmoved_out,
- struct imsgev *iev)
+ int destfd, uint32_t *nmoved_out, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct copy_staged *staged = NULL;
size_t nstaged = 0, i;
int ok = 1, any_removed = 0;
*nmoved_out = 0;
- if (!stage_copy_messages(mailbox_dir_fd, srcidx, req, ranges,
- nranges, &staged, &nstaged))
+ if (!stage_copy_messages(ss, srcidx, req, ranges, nranges, &staged,
+ &nstaged))
return (0);
if (nstaged == 0)
ok = 0;
goto cleanup;
}
- if (!commit_copy_messages(destfd, destidx, staged, nstaged,
- iev)) {
+ if (!commit_copy_messages(ss, destfd, destidx, staged, nstaged)) {
ok = 0;
goto cleanup;
}
off_t size;
char suffix[64], path[600];
- /*
- * fresh scan per message so old_seqno reflects state after each
- * removal
- */
for (j = 0, out = 0; j < srcidx->nlines; j++) {
if (!found && index_parse_line(srcidx->lines[j],
&rec) == 0 && rec.uid == staged[i].src_uid) {
}
any_removed = 1;
- if (locate_message_file(mailbox_dir_fd, rec.basename,
- &size, suffix, sizeof(suffix)) == 0) {
+ if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+ rec.basename, &size, suffix, sizeof(suffix)) == 0) {
if (snprintf(path, sizeof(path), "%s/%s%s",
suffix[0] == '\0' ? "new" : "cur", rec.basename,
suffix) < (int)sizeof(path))
- unlinkat(mailbox_dir_fd, path, 0);
+ unlinkat(ss->mailbox_dir_fd, path, 0);
}
{
}
}
- /* RFC 7162 SS3.1: HIGHESTMODSEQ bump per op; see handle_mbox_expunge */
+ /* RFC 7162 SS3.1 */
if (any_removed)
srcidx->highestmodseq++;
- if (index_save(mailbox_dir_fd, srcidx) == -1) {
+ if (index_save(ss->mailbox_dir_fd, srcidx) == -1) {
log_warnx("session %u: MOVE: destination commit succeeded "
"but saving the source's compacted index failed, "
"message(s) may remain duplicated", session_id);
cleanup:
for (i = 0; i < nstaged; i++)
- free(staged[i].body);
+ free(staged[i].srcpath);
free(staged);
return (ok);
}
-/* RFC 9051 SS6.4.8 MOVE dispatcher: locks index(es), hands off to helpers */
+/* RFC 9051 SS6.4.8 MOVE */
int
handle_mbox_move(struct imsg_mbox_copy *req, const struct seq_range *ranges,
- uint32_t nranges, struct imsgev *iev)
+ uint32_t nranges, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct mbox_index idx_a, idx_b;
struct mbox_index *srcidx = NULL, *destidx = NULL;
struct imsg_mbox_result result;
memset(&idx_b, 0, sizeof(idx_b));
memset(&result, 0, sizeof(result));
- got = lock_copy_move_mailboxes("MOVE", req->destname, &destfd,
+ got = lock_copy_move_mailboxes(ss, "MOVE", req->destname, &destfd,
&idx_a, &idx_b, &srcidx, &destidx, desttarget,
sizeof(desttarget), &cross_mailbox, &il_a, &il_b, &result);
if (got == 2)
if (!cross_mailbox) {
if (!move_same_mailbox(req, ranges, nranges, srcidx, &nmoved,
- iev))
+ ss))
ok = 0;
} else {
if (!move_cross_mailbox(req, ranges, nranges, srcidx, destidx,
- destfd, &nmoved, iev))
+ destfd, &nmoved, ss))
ok = 0;
}
blob - e7b9051e620318760b38289f0fe6823b5a4cea7e
blob + a0ae81162de65bd2b4a9878669c2f7cdd5292754
--- src/mbox_fetch.c
+++ src/mbox_fetch.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* FETCH and STATUS request handling. */
-
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include "log.h"
#include "store_internal.h"
-/*
- * A FETCH walk that can stop and be resumed, so the store composes a
- * bounded amount and lets it drain rather than building the whole reply
- * first. Only FETCH is paced: it is the one walk carrying message
- * bodies, where a large mailbox queues hundreds of megabytes, against
- * single-digit megabytes of fixed-size records for SEARCH, STORE,
- * EXPUNGE and COPY, whose loops are deliberately left as they are.
- */
-static struct {
- int active;
- struct mbox_index idx;
- struct imsg_mbox_fetch req;
- struct seq_range resolved[SEQSET_MAX_RANGES];
- uint32_t nresolved;
- uint32_t max_hi;
- uint32_t next; /* index line to resume at */
- uint32_t sent;
- struct imsgev *iev;
-} fetch_walk;
+static void fetch_walk_step(struct store_session *);
+static void fetch_walk_finish(struct store_session *, int);
-/* Bytes and descriptors composed since the queue was last empty. */
-static size_t fetch_composed;
-static int fetch_fds;
-
-static void fetch_walk_step(void);
-static void fetch_walk_finish(int);
-
-/* Shared compose-and-send helper for the four IMSG_MBOX_FETCH_* messages */
static void
-fetch_send_part(struct imsgev *iev, int imsg_type, const char *what,
+fetch_send_part(struct store_session *ss, int imsg_type, const char *what,
const void *meta, size_t metalen, int found, const void *buf,
uint32_t buflen)
{
memcpy(combined, meta, metalen);
if (found && buflen > 0)
memcpy((char *)combined + metalen, buf, buflen);
- if (imsg_compose(&iev->ibuf, imsg_type, 0, 0, -1, combined,
+ if (imsg_compose(&ss->iev.ibuf, imsg_type, 0, 0, -1, combined,
combined_len) == -1)
log_warn("session %u: imsg_compose %s", session_id, what);
else
- fetch_composed += combined_len;
+ ss->fetch.composed += combined_len;
free(combined);
}
-/*
- * Returns 0 having answered or started the walk, or 1 without answering
- * because another session holds the index lock; store.c runs it again.
- */
+/* returns 1, unanswered, if the index lock is busy */
int
handle_mbox_fetch(struct imsg_mbox_fetch *req, const struct seq_range *ranges,
- uint32_t nranges, struct imsgev *iev)
+ uint32_t nranges, struct store_session *ss)
{
- struct mbox_index *idx = &fetch_walk.idx;
+ struct store_fetch_walk *fw = &ss->fetch;
+ struct imsgev *iev = &ss->iev;
+ struct mbox_index *idx = &fw->idx;
struct index_lock il = INDEX_LOCK_INIT;
uint32_t i;
int locked;
/* Before the reset below, so that a busy return changes nothing. */
- locked = index_lock_acquire(mailbox_dir_fd, &il, LOCK_SH | LOCK_NB);
+ locked = index_lock_acquire(ss->mailbox_dir_fd, &il, LOCK_SH | LOCK_NB);
if (locked == 1)
return (1);
- if (fetch_walk.active) {
- /*
- * Unreachable: the listener holds a command until the one
- * in flight finishes (session_is_busy(), listener.c).
- * Abandoning the earlier walk rather than leaking its
- * snapshot is the defined answer if that ever changes.
- */
+ if (fw->active) {
log_warnx("session %u: FETCH arrived during a paused FETCH, "
"abandoning the earlier walk", session_id);
- index_free(&fetch_walk.idx);
+ index_free(&fw->idx);
}
- memset(&fetch_walk, 0, sizeof(fetch_walk));
- fetch_walk.iev = iev;
- fetch_walk.req = *req;
- fetch_walk.next = 1;
- fetch_composed = 0;
- fetch_fds = 0;
+ memset(fw, 0, sizeof(*fw));
+ fw->req = *req;
+ fw->next = 1;
if (locked == -1) {
- fetch_walk_finish(0);
+ fetch_walk_finish(ss, 0);
return (0);
}
if (index_load(il.fd, idx) == -1) {
index_lock_release(&il);
- fetch_walk_finish(0);
+ fetch_walk_finish(ss, 0);
return (0);
}
index_lock_release(&il);
- /* RFC 9051 SS6.4.9: UID FETCH set is UIDs; see index_max_uid() */
- fetch_walk.nresolved = seqset_resolve(ranges, nranges, req->by_uid ?
+ /* RFC 9051 SS6.4.9 */
+ fw->nresolved = seqset_resolve(ranges, nranges, req->by_uid ?
index_max_uid(idx) : (uint32_t)idx->nlines, !req->by_uid,
- fetch_walk.resolved);
- fetch_walk.max_hi = seqset_max_hi(fetch_walk.resolved,
- fetch_walk.nresolved);
+ fw->resolved);
+ fw->max_hi = seqset_max_hi(fw->resolved, fw->nresolved);
- /*
- * RFC 7162 SS3.2.6: VANISHED (EARLIER) precedes FETCH, per send order
- * here
- */
+ /* RFC 7162 SS3.2.6: VANISHED (EARLIER) first */
if (req->by_uid && req->want_vanished) {
- for (i = 0; i < fetch_walk.nresolved; i++)
- send_vanished_range(idx, fetch_walk.resolved[i].lo,
- fetch_walk.resolved[i].hi, iev);
+ for (i = 0; i < fw->nresolved; i++)
+ send_vanished_range(idx, fw->resolved[i].lo,
+ fw->resolved[i].hi, iev);
}
- fetch_walk.active = 1;
- fetch_walk_step();
+ fw->active = 1;
+ fetch_walk_step(ss);
return (0);
}
-/*
- * Composes replies from fetch_walk.next and returns with the walk still
- * active once FETCH_BATCH_MAX bytes are queued. Pausing is safe for the
- * reason the walk was already safe: it runs on a private snapshot taken
- * under the lock and released before the walk began, and it already
- * handles a message that vanishes underneath. Pausing lengthens that
- * window rather than opening it.
- */
+static int
+fetch_needs_parser(const struct imsg_mbox_fetch *req)
+{
+ if (req->attrs & (MBOX_FETCH_ENVELOPE | MBOX_FETCH_BODYSTRUCTURE))
+ return (1);
+ if ((req->attrs & MBOX_FETCH_HEADER_FIELDS) &&
+ !(req->attrs & MBOX_FETCH_BODY_HEADER))
+ return (1);
+ return ((req->attrs & MBOX_FETCH_BODY_PART) &&
+ !(req->attrs & (MBOX_FETCH_BODY_WHOLE | MBOX_FETCH_BODY_TEXT)));
+}
+
+/* returns, still active, once FETCH_BATCH_MAX is queued */
static void
-fetch_walk_step(void)
+fetch_walk_step(struct store_session *ss)
{
- struct mbox_index *idx = &fetch_walk.idx;
- struct imsg_mbox_fetch *req = &fetch_walk.req;
- struct imsgev *iev = fetch_walk.iev;
- struct seq_range *resolved = fetch_walk.resolved;
- uint32_t nresolved = fetch_walk.nresolved;
- uint32_t max_hi = fetch_walk.max_hi;
+ struct store_fetch_walk *fw = &ss->fetch;
+ struct mbox_index *idx = &fw->idx;
+ struct imsg_mbox_fetch *req = &fw->req;
+ struct imsgev *iev = &ss->iev;
+ struct seq_range *resolved = fw->resolved;
+ uint32_t nresolved = fw->nresolved;
+ uint32_t max_hi = fw->max_hi;
uint32_t i;
- for (i = fetch_walk.next; i <= (uint32_t)idx->nlines; i++) {
+ for (i = fw->next; i <= (uint32_t)idx->nlines; i++) {
struct imsg_mbox_fetch_meta meta;
struct index_rec rec;
enum seqset_pos pos;
if (index_parse_line(idx->lines[i - 1], &rec) == -1)
continue;
- /*
- * position/UID-space range check, per by_uid; asc pass,
- * PAST_END stops it
- */
pos = seqset_position(resolved, nresolved, max_hi, req->by_uid,
rec.uid, i);
if (pos == SEQSET_PAST_END)
if (req->has_changedsince && rec.modseq <= req->changedsince)
continue;
+ if (!fw->no_parser && fetch_needs_parser(req)) {
+ int ready = parser_ready();
+
+ if (ready == 0) {
+ fw->next = i;
+ fw->wait_parser = 1;
+ return;
+ }
+ if (ready == -1)
+ fw->no_parser = 1;
+ }
+
memset(&meta, 0, sizeof(meta));
meta.seqno = i;
meta.uid = rec.uid;
meta.modseq = rec.modseq;
if (req->attrs & (MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_FLAGS)) {
- if (locate_message_file(mailbox_dir_fd, rec.basename,
- &size, suffix, sizeof(suffix)) == -1) {
+ if (locate_message_file(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename, &size, suffix,
+ sizeof(suffix)) == -1) {
log_warnx("session %u: message %s (uid %u) "
"indexed but missing on disk, skipped",
session_id, rec.basename, meta.uid);
meta.internaldate = parse_maildir_timestamp(
rec.basename);
- /*
- * FETCH_HEADER precedes FETCH_META (listener order); always
- * sent, found=0
- */
+ /* FETCH_HEADER precedes FETCH_META */
if (req->attrs & (MBOX_FETCH_BODY_HEADER |
MBOX_FETCH_HEADER_FIELDS)) {
struct imsg_mbox_fetch_header hdrmeta;
+ struct imsg_parser_req preq;
+ struct imsg_parser_rep prep;
char *hdrbuf = NULL;
uint32_t hdrlen = 0;
- int rc;
+ int mfd, rc;
memset(&hdrmeta, 0, sizeof(hdrmeta));
hdrmeta.seqno = i;
hdrmeta.uid = rec.uid;
if (req->attrs & MBOX_FETCH_BODY_HEADER)
- rc = read_message_header(mailbox_dir_fd,
- rec.basename, &hdrbuf, &hdrlen);
- else
- rc = read_message_header_fields(mailbox_dir_fd,
- rec.basename, req->header_fields,
- req->header_fields_not, &hdrbuf, &hdrlen);
+ rc = read_message_header(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename, &hdrbuf,
+ &hdrlen);
+ else if ((mfd = open_message_file(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename)) == -1)
+ rc = -1;
+ else {
+ memset(&preq, 0, sizeof(preq));
+ /* same size, and terminated on arrival */
+ (void)strlcpy(preq.fields, req->header_fields,
+ sizeof(preq.fields));
+ preq.fields_not = req->header_fields_not;
+ rc = parser_request(IMSG_PARSER_HEADER_FIELDS,
+ "HEADER.FIELDS", mfd, rec.basename, &preq,
+ NULL, 0, FETCH_HEADER_MAX, &prep, &hdrbuf);
+ hdrlen = prep.len;
+ close(mfd);
+ }
if (rc == 0) {
hdrmeta.found = 1;
hdrmeta.hdrlen = hdrlen;
}
- fetch_send_part(iev, IMSG_MBOX_FETCH_HEADER,
+ fetch_send_part(ss, IMSG_MBOX_FETCH_HEADER,
"IMSG_MBOX_FETCH_HEADER", &hdrmeta, sizeof(hdrmeta),
hdrmeta.found, hdrbuf, hdrlen);
free(hdrbuf);
}
- /*
- * IMSG_MBOX_FETCH_BODY, same contract as HEADER; WHOLE wins
- * over TEXT/PART. It carries a descriptor and a range.
- */
+ /* WHOLE wins over TEXT and PART */
if (req->attrs & (MBOX_FETCH_BODY_WHOLE | MBOX_FETCH_BODY_TEXT |
MBOX_FETCH_BODY_PART)) {
struct imsg_mbox_fetch_body bodymeta;
bodymeta.uid = rec.uid;
if (want_part) {
- int path[MIME_MAX_DEPTH];
- int pathlen;
+ struct imsg_parser_req preq;
+ struct imsg_parser_rep prep;
- pathlen = parse_section_part(req->section_part,
- path, MIME_MAX_DEPTH);
- if (pathlen != -1 &&
- extract_mime_part(mailbox_dir_fd,
- rec.basename, path, pathlen, &off,
- &len) == 0)
- bodymeta.found = 1;
- if (bodymeta.found && len > 0 &&
- (fd = open_message_file(mailbox_dir_fd,
- rec.basename)) == -1) {
+ memset(&preq, 0, sizeof(preq));
+ preq.pathlen = parse_section_part(
+ req->section_part, preq.path,
+ MIME_MAX_DEPTH);
+ if (preq.pathlen != -1 &&
+ (fd = open_message_file(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename)) == -1)
fdbusy = errno == EMFILE ||
errno == ENFILE;
- bodymeta.found = 0;
+ else if (preq.pathlen != -1 &&
+ parser_request(IMSG_PARSER_PART,
+ "BODY[<part>]", fd, rec.basename, &preq,
+ NULL, 0, 0, &prep, NULL) == 0) {
+ bodymeta.found = 1;
+ off = prep.part_off;
+ len = prep.part_len;
}
+ /* the fd the extent was checked on */
+ if (fd != -1 && (!bodymeta.found || len == 0)) {
+ close(fd);
+ fd = -1;
+ }
} else {
- fd = message_body_range(mailbox_dir_fd,
- rec.basename, want_text, &off, &len,
- &fdbusy);
+ fd = message_body_range(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename, want_text,
+ &off, &len, &fdbusy);
if (fd != -1)
bodymeta.found = 1;
}
- /*
- * Out of descriptors with some of this walk's in
- * flight: retry this message once they are sent,
- * rather than answering NO for a message that is
- * there. A resent HEADER replaces the pending one.
- */
- if (fdbusy && fetch_fds > 0) {
- fetch_walk.next = i;
+ /* out of descriptors: retry once sent */
+ if (fdbusy && fw->fds > 0) {
+ fw->next = i;
return;
}
if (fd != -1)
close(fd);
} else {
- fetch_composed += sizeof(bodymeta);
+ fw->composed += sizeof(bodymeta);
if (fd != -1)
- fetch_fds++;
+ fw->fds++;
}
}
- /*
- * FETCH_ENVELOPE, same contract as HEADER/BODY; bytes are
- * formatted text
- */
if (req->attrs & MBOX_FETCH_ENVELOPE) {
struct imsg_mbox_fetch_envelope envmeta;
+ struct imsg_parser_rep prep;
char *envbuf = NULL;
- uint32_t envlen = 0;
+ int mfd;
memset(&envmeta, 0, sizeof(envmeta));
envmeta.seqno = i;
envmeta.uid = rec.uid;
- if (build_envelope(mailbox_dir_fd, rec.basename,
- &envbuf, &envlen) == 0) {
- envmeta.found = 1;
- envmeta.envlen = envlen;
+ if ((mfd = open_message_file(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename)) != -1) {
+ if (parser_request(IMSG_PARSER_ENVELOPE,
+ "ENVELOPE", mfd, rec.basename, NULL, NULL,
+ 0, ENVELOPE_MAX, &prep, &envbuf) == 0) {
+ envmeta.found = 1;
+ envmeta.envlen = prep.len;
+ }
+ close(mfd);
}
- fetch_send_part(iev, IMSG_MBOX_FETCH_ENVELOPE,
+ fetch_send_part(ss, IMSG_MBOX_FETCH_ENVELOPE,
"IMSG_MBOX_FETCH_ENVELOPE", &envmeta,
sizeof(envmeta),
- envmeta.found, envbuf, envlen);
+ envmeta.found, envbuf, envmeta.envlen);
free(envbuf);
}
- /*
- * IMSG_MBOX_FETCH_BODYSTRUCTURE, same contract/shape as
- * ENVELOPE above
- */
if (req->attrs & MBOX_FETCH_BODYSTRUCTURE) {
struct imsg_mbox_fetch_bodystructure bsmeta;
+ struct imsg_parser_rep prep;
char *bsbuf = NULL;
- uint32_t bslen = 0;
+ int mfd;
memset(&bsmeta, 0, sizeof(bsmeta));
bsmeta.seqno = i;
bsmeta.uid = rec.uid;
- if (build_bodystructure(mailbox_dir_fd,
- rec.basename, &bsbuf, &bslen) == 0) {
- bsmeta.found = 1;
- bsmeta.bslen = bslen;
+ if ((mfd = open_message_file(&ss->cur_snap,
+ ss->mailbox_dir_fd, rec.basename)) != -1) {
+ if (parser_request(IMSG_PARSER_BODYSTRUCTURE,
+ "BODYSTRUCTURE", mfd, rec.basename, NULL,
+ NULL, 0, BODYSTRUCTURE_MAX, &prep,
+ &bsbuf) == 0) {
+ bsmeta.found = 1;
+ bsmeta.bslen = prep.len;
+ }
+ close(mfd);
}
- fetch_send_part(iev, IMSG_MBOX_FETCH_BODYSTRUCTURE,
+ fetch_send_part(ss, IMSG_MBOX_FETCH_BODYSTRUCTURE,
"IMSG_MBOX_FETCH_BODYSTRUCTURE", &bsmeta,
- sizeof(bsmeta), bsmeta.found, bsbuf, bslen);
+ sizeof(bsmeta), bsmeta.found, bsbuf, bsmeta.bslen);
free(bsbuf);
}
log_warn("session %u: imsg_compose "
"IMSG_MBOX_FETCH_META", session_id);
else {
- fetch_walk.sent++;
- fetch_composed += sizeof(meta);
+ fw->sent++;
+ fw->composed += sizeof(meta);
}
- /*
- * Enough queued: stop and let it drain. The client reads
- * while the rest is still being built, and the store holds
- * FETCH_BATCH_MAX of a reply and FETCH_FD_MAX descriptors
- * rather than all of them.
- */
- if (fetch_composed >= FETCH_BATCH_MAX ||
- fetch_fds >= FETCH_FD_MAX) {
- fetch_walk.next = i + 1;
+ /* enough queued: let it drain */
+ if (fw->composed >= FETCH_BATCH_MAX ||
+ fw->fds >= FETCH_FD_MAX) {
+ fw->next = i + 1;
return;
}
}
- fetch_walk_finish(1);
+ fetch_walk_finish(ss, 1);
}
-/* Sends the terminal result and drops the walk's snapshots. */
static void
-fetch_walk_finish(int ok)
+fetch_walk_finish(struct store_session *ss, int ok)
{
+ struct store_fetch_walk *fw = &ss->fetch;
struct imsg_mbox_result result;
- struct imsgev *iev = fetch_walk.iev;
+ struct imsgev *iev = &ss->iev;
- index_free(&fetch_walk.idx);
- cur_snapshot_discard();
+ index_free(&fw->idx);
+ cur_snapshot_discard(&ss->cur_snap);
memset(&result, 0, sizeof(result));
result.error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
- result.count = fetch_walk.sent;
+ result.count = fw->sent;
if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result,
sizeof(result)) == -1)
log_warn("session %u: imsg_compose IMSG_MBOX_RESULT",
session_id);
- fetch_walk.active = 0;
- fetch_walk.iev = NULL;
+ fw->active = 0;
}
-/* 1 while a FETCH walk is part way through; store.c keeps its state. */
int
-fetch_walk_paused(void)
+fetch_walk_paused(struct store_session *ss)
{
- return (fetch_walk.active);
+ return (ss->fetch.active);
}
-/*
- * Continues a paused walk once everything it composed has gone out.
- * Called from the store's EV_WRITE handler, the only thing that drains
- * the queue. A paused walk always has a write pending to wake it, since
- * imsg_compose() arms EV_WRITE while anything is queued (imsgev.c), so
- * this cannot leave a client waiting for a reply that never resumes.
- */
void
-fetch_walk_resume(struct imsgev *iev)
+fetch_walk_resume(struct store_session *ss)
{
- if (!fetch_walk.active || fetch_walk.iev != iev)
+ struct store_fetch_walk *fw = &ss->fetch;
+
+ if (!fw->active || fw->wait_parser)
return;
- if (imsgbuf_queuelen(&iev->ibuf) > 0)
+ if (imsgbuf_queuelen(&ss->iev.ibuf) > 0)
return;
- fetch_composed = 0;
- fetch_fds = 0;
- fetch_walk_step();
+ fw->composed = 0;
+ fw->fds = 0;
+ fetch_walk_step(ss);
}
-/* Drops a paused walk's snapshots when the child is told to exit. */
void
-fetch_walk_abort(void)
+fetch_walk_parser(struct store_session *ss, int ok)
{
- if (!fetch_walk.active)
+ struct store_fetch_walk *fw = &ss->fetch;
+
+ if (!fw->active || !fw->wait_parser)
return;
- index_free(&fetch_walk.idx);
- fetch_walk.active = 0;
- fetch_walk.iev = NULL;
+ fw->wait_parser = 0;
+ if (!ok)
+ fw->no_parser = 1;
+ fetch_walk_step(ss);
}
-/* Inverse of build_flags_string()'s table: flag letters back to MBOX_FLAG_* */
+void
+fetch_walk_abort(struct store_session *ss)
+{
+ struct store_fetch_walk *fw = &ss->fetch;
+
+ if (!fw->active)
+ return;
+ index_free(&fw->idx);
+ fw->active = 0;
+}
+
uint32_t
letters_to_sysflags(const char *letters)
{
return (f);
}
-/* Renders sysflags into maildir(5)'s ASCII letter order: D, F, R, S, T. */
+/* maildir(5) order: D, F, R, S, T */
void
sysflags_to_letters(uint32_t sysflags, char *out, size_t outsize)
{
out[i] = '\0';
}
-/* RFC 9051 SS6.3.11 STATUS; reuses SELECT's scan; extras scan if requested */
+/* RFC 9051 SS6.3.11 STATUS */
int
-handle_mbox_status(struct imsg_mbox_status *req, struct imsgev *iev)
+handle_mbox_status(struct imsg_mbox_status *req, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct mbox_index idx;
struct imsg_mbox_status_result reply;
struct index_lock il = INDEX_LOCK_INIT;
memset(&reply, 0, sizeof(reply));
- /* RFC 9051 SS6.3.11: STATUS names a mailbox, not the selection */
if (mailbox_name_is_inbox(req->mailbox))
target = "";
else if (mailbox_name_valid(req->mailbox))
goto send;
}
- /*
- * STATUS can be the first command to touch a mailbox without a
- * prior SELECT, so it must persist any header index_load() just
- * invented -- otherwise the UIDVALIDITY reported here won't
- * match what the next caller sees.
- */
+ /* STATUS may be first to touch a mailbox: persist a fresh header */
if (idx.fresh && index_save(tfd, &idx) == -1)
log_warnx("session %u: STATUS: could not persist the new "
"index header", session_id);
if (index_parse_line(idx.lines[i], &rec) == -1)
continue;
- if (locate_message_file(tfd, rec.basename,
- &size, suffix, sizeof(suffix)) == -1) {
+ if (locate_message_file(&ss->cur_snap, tfd,
+ rec.basename, &size, suffix,
+ sizeof(suffix)) == -1) {
log_warnx("session %u: message %s indexed but "
"missing on disk, skipped for STATUS "
"UNSEEN/DELETED/SIZE", session_id,
blob - b29930cfcfcfffe8151e681d4f505d879b046a93
blob + 1dc72a9e2541dae6be222c72dbf7351f2615bd57
--- src/mbox_manage.c
+++ src/mbox_manage.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* mbox_manage.c: CREATE/DELETE/RENAME/LIST/APPEND/SELECT request handling. */
#include <sys/types.h>
#include <sys/file.h>
int
handle_mbox_select(struct imsg_mbox_select *req,
- const struct seq_range *ranges, uint32_t nranges, struct imsgev *iev)
+ const struct seq_range *ranges, uint32_t nranges, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct mbox_index idx;
struct imsg_mbox_selected reply;
struct index_lock il = INDEX_LOCK_INIT;
memset(&reply, 0, sizeof(reply));
- /*
- * Invalidate index.c's IDLE state up front: a later poll would
- * otherwise report "unchanged" for a directory it never looked at,
- * and diff a new mailbox against the old one's UID list. The SS6.2
- * gate is cleared by the caller, which dispatches nothing else that
- * could depend on it in between (store.c).
- */
- idle_probe_reset();
- idle_baseline_reset();
+ /* invalidate the IDLE state first */
+ idle_probe_reset(ss);
+ idle_baseline_reset(ss);
if (mailbox_name_is_inbox(req->mailbox))
target = "";
goto send;
}
- /*
- * multiple store children/user; r-m-w needs cross-process mutual
- * exclusion
- */
locked = index_lock_acquire(dfd, &il, LOCK_EX | LOCK_NB);
if (locked == 1) {
/* only the IDLE resets have run, and a re-run repeats them */
goto send;
}
- /*
- * Commit point: everything above leaves this session's own state
- * alone, so only a selection that has already succeeded moves the
- * descriptor, the name and the gate.
- */
- if (mailbox_dir_fd != -1)
- close(mailbox_dir_fd);
- mailbox_dir_fd = dfd;
+ /* commit point: nothing above touched this session */
+ if (ss->mailbox_dir_fd != -1)
+ close(ss->mailbox_dir_fd);
+ ss->mailbox_dir_fd = dfd;
- if (strlcpy(selected_mailbox, target, sizeof(selected_mailbox)) >=
- sizeof(selected_mailbox)) {
+ if (strlcpy(ss->selected_mailbox, target,
+ sizeof(ss->selected_mailbox)) >= sizeof(ss->selected_mailbox)) {
log_warnx("session %u: SELECT %s: name too long to "
"record, can't happen (validated above, same-size "
"buffers)", session_id, req->mailbox);
}
reply.error = MBOX_OP_OK;
- mailbox_selected = 1; /* the store's gate; see store_internal.h */
+ ss->mailbox_selected = 1;
reply.exists = (uint32_t)idx.nlines;
reply.uidvalidity = idx.uidvalidity;
reply.uidnext = idx.uidnext;
reply.highestmodseq = idx.highestmodseq;
if (req->qresync && req->qresync_uidvalidity == idx.uidvalidity)
- qresync_send_resync(req, ranges, nranges, &idx, iev);
+ qresync_send_resync(req, ranges, nranges, &idx, ss);
index_free(&idx);
index_lock_release(&il);
}
-/* IMSG_MBOX_CREATE (RFC 9051 SS6.3.4); mkdir EEXIST means already exists */
+/* RFC 9051 SS6.3.4 CREATE */
void
handle_mbox_create(struct imsg_mbox_create *req, struct imsgev *iev)
} else {
log_debug("session %u: CREATE %s: already exists",
session_id, req->mailbox);
- /*
- * RFC 5530 SS3 ALREADYEXISTS: this is its own worked
- * example
- */
+ /* RFC 5530 SS3 ALREADYEXISTS */
result.error = MBOX_OP_ERR_ALREADY_EXISTS;
}
goto send;
if (snprintf(prefix, sizeof(prefix), "%s/", req->mailbox) >=
(int)sizeof(prefix) ||
ensure_maildir_dirs(maildir_root_fd, prefix) == -1) {
- /*
- * best-effort cleanup so a half-init mailbox isn't stuck
- * (EEXIST later)
- */
unlinkat(maildir_root_fd, req->mailbox, AT_REMOVEDIR);
result.error = MBOX_OP_ERR_GENERIC;
goto send;
session_id);
}
-/* bounded removal of a mailbox's tmp/new/cur + index; refuses non-regulars */
static int
remove_maildir_subtree(int dfd, const char *prefix)
{
static const char *dirs[] = { "tmp", "new", "cur" };
- /*
- * +32 must cover STORE_INDEX_NAME on a near-maximal mailbox name.
- */
+ /* room for STORE_INDEX_NAME after the longest name */
char path[MBOX_NAME_MAX + 32];
size_t i;
}
}
- /*
- * Removes the index, its lock, and any leftover index_save() temp --
- * rmdir(2) follows, so every file this daemon creates here must be
- * named or a crash mid-save leaves DELETE failing with ENOTEMPTY.
- */
+ /* every file created here must go before rmdir(2) */
{
static const char *files[] = {
STORE_INDEX_NAME,
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.
- */
+/* RFC 9051 SS6.3.5 DELETE */
void
-handle_mbox_delete(struct imsg_mbox_delete *req, struct imsgev *iev)
+handle_mbox_delete(struct imsg_mbox_delete *req, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct imsg_mbox_result result;
struct stat st;
char prefix[MBOX_NAME_MAX + 1];
AT_SYMLINK_NOFOLLOW) == -1 || !S_ISDIR(st.st_mode)) {
log_debug("session %u: DELETE %s: no such mailbox",
session_id, req->mailbox);
- /*
- * RFC 5530 SS3 NONEXISTENT: its own worked example is exactly
- * this
- */
+ /* RFC 5530 SS3 NONEXISTENT */
result.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
goto send;
}
result.error = MBOX_OP_OK;
send:
- /*
- * Deleting this session's own selection clears the store's gate:
- * a later FETCH/STORE/EXPUNGE would otherwise still work on the
- * descriptor of a directory that no longer has a name.
- */
+ /* deleting the selection clears the store's gate */
if (result.error == MBOX_OP_OK &&
- strcmp(selected_mailbox, req->mailbox) == 0)
- mailbox_selected = 0;
+ strcmp(ss->selected_mailbox, req->mailbox) == 0)
+ ss->mailbox_selected = 0;
if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result,
sizeof(result)) == -1)
log_warn("session %u: imsg_compose IMSG_MBOX_RESULT",
session_id);
}
-/*
- * IMSG_MBOX_RENAME (RFC 9051 SS6.3.6); INBOX refused as source since its dir
- * can't be renamed away (store.c's chroot assumes it's always root).
- */
+/* RFC 9051 SS6.3.6 RENAME; INBOX is the root and cannot move */
void
-handle_mbox_rename(struct imsg_mbox_rename *req, struct imsgev *iev)
+handle_mbox_rename(struct imsg_mbox_rename *req, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct imsg_mbox_result result;
struct stat st;
AT_SYMLINK_NOFOLLOW) == -1 || !S_ISDIR(st.st_mode)) {
log_debug("session %u: RENAME %s: no such mailbox",
session_id, req->oldname);
- /*
- * RFC 5530 SS3 NONEXISTENT: example is RENAME failing on
- * missing source
- */
+ /* RFC 5530 SS3 NONEXISTENT */
result.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
goto send;
}
AT_SYMLINK_NOFOLLOW) == 0 || errno != ENOENT) {
log_debug("session %u: RENAME %s -> %s: destination exists",
session_id, req->oldname, req->newname);
- /*
- * RFC 5530 SS3 ALREADYEXISTS: its worked example is this exact
- * RENAME case
- */
+ /* RFC 5530 SS3 ALREADYEXISTS */
result.error = MBOX_OP_ERR_ALREADY_EXISTS;
goto send;
}
result.error = MBOX_OP_OK;
send:
- /*
- * A rename of this session's selection follows the mailbox: the
- * descriptor still names the same directory, so only the recorded
- * name changes.
- */
if (result.error == MBOX_OP_OK &&
- strcmp(selected_mailbox, req->oldname) == 0) {
- if (strlcpy(selected_mailbox, req->newname,
- sizeof(selected_mailbox)) >= sizeof(selected_mailbox))
+ strcmp(ss->selected_mailbox, req->oldname) == 0) {
+ if (strlcpy(ss->selected_mailbox, req->newname,
+ sizeof(ss->selected_mailbox)) >=
+ sizeof(ss->selected_mailbox))
log_warnx("session %u: RENAME %s -> %s: new name too "
"long to record, can't happen (validated)",
session_id, req->oldname, req->newname);
session_id);
}
-/*
- * 1 if this maildir-root directory entry names a mailbox. Shared by
- * handle_mbox_list() and subs_materialise(), which have to agree exactly: a
- * name the two judged differently would be listed but never subscribable, or
- * the reverse. *badname is set when a real directory was refused by the name
- * rule alone, which the caller may report; every other refusal is silent.
- */
+/* LIST and the subscription code must agree on this */
static int
mbox_root_entry_is_mailbox(const char *name, int *badname)
{
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
return (0);
- /*
- * Check directory-ness first so the name rule below only judges real
- * mailbox candidates. lstat(2) is safe pre-validation since a
- * readdir(2) d_name can't contain "/".
- */
if (fstatat(maildir_root_fd, name, &st, AT_SYMLINK_NOFOLLOW) ==
-1 || !S_ISDIR(st.st_mode))
return (0);
- /*
- * Directories that aren't mailboxes: tmp/new/cur are INBOX's own
- * maildir subdirs, and a directory named after a reserved file is
- * skipped quietly so such a name is never reported as lost mail.
- */
+ /* tmp, new and cur are INBOX's own */
if (strcmp(name, "tmp") == 0 || strcmp(name, "new") == 0 ||
strcmp(name, "cur") == 0 || mailbox_name_reserved(name))
return (0);
return (1);
}
-/*
- * The subscription list in memory. `present` is 0 when the file does not
- * exist, which means every mailbox is subscribed; see store_internal.h.
- */
+/* present 0: no file, so every mailbox is subscribed */
struct sublist {
char **names;
size_t n;
return (0);
}
-/* order is preserved so the file stays readable across rewrites */
static void
sublist_remove(struct sublist *sl, const char *name)
{
}
}
-/*
- * Reads the subscription file into *sl; cwd must be the maildir root. No
- * file is not an error: it leaves sl->present 0, which every caller reads as
- * "everything is subscribed". Takes no lock -- sublist_save() commits by
- * rename(2), so a reader gets a whole file either way.
- */
static int
sublist_load(struct sublist *sl)
{
sl->present = 1;
while (fgets(line, sizeof(line), fp) != NULL) {
- /*
- * fgets(3) silently splits an over-long line; peek at the next
- * byte to tell a legal max-length name (next byte is '\n' or
- * EOF) from an actual split record.
- */
+ /* fgets(3) splits an over-long line */
if (strchr(line, '\n') == NULL &&
strlen(line) == sizeof(line) - 1) {
int c = fgetc(fp);
if (line[0] == '\0')
continue;
- /*
- * SUBSCRIBE can't have written a name this server refuses, so
- * the file has been edited by hand. Refusing the whole file
- * rather than the line keeps the two halves of an edit from
- * being applied separately.
- */
+ /* a name SUBSCRIBE would refuse: the file was hand-edited */
if (!mailbox_name_syntax_ok(line)) {
log_warnx("session %u: unusable name in %s, refusing "
"to parse it", session_id,
return (-1);
}
-/*
- * Writes *sl over the subscription file, following index_save()'s discipline:
- * O_EXCL on the temp so a pre-planted symlink can't be followed, fsync(2)
- * before the rename(2) that commits it, and an fsync of the directory entry
- * the rename repointed. The caller holds the lock.
- */
+/* as index_save(): O_EXCL temp, fsync(2), rename(2) */
static int
sublist_save(const struct sublist *sl)
{
return (0);
}
-/*
- * The subscription lock, taken on a file of its own rather than on the list,
- * for the reason index_lock_acquire() takes the index's lock on one: flock(2)
- * locks an inode, and sublist_save() replaces the list's inode every time.
- */
static int
subs_lock_acquire(int *lockfd)
{
close(lockfd);
}
-/*
- * Fills *sl with every mailbox now on disk -- the list an absent file stands
- * for. UNSUBSCRIBE calls this before removing its name, because the first
- * UNSUBSCRIBE is what has to write that meaning down. INBOX is not among them
- * and never is: it is the maildir root, not an entry in it. cwd must be the
- * maildir root.
- */
+/* the list an absent file stands for */
static int
subs_materialise(struct sublist *sl)
{
return (0);
}
-/*
- * Emits one IMSG_MBOX_LIST_ITEM; returns 1 if it went out, 0 if it did not,
- * so a caller can add the result straight to its count.
- */
static int
list_item_send(struct imsgev *iev, const char *name, int exists)
{
return (1);
}
-/* IMSG_MBOX_LIST (RFC 9051 SS6.3.9); streams LIST_ITEM/mailbox, no INBOX */
+/* RFC 9051 SS6.3.9 LIST; INBOX is not sent */
void
handle_mbox_list(struct imsg_mbox_list *req, struct imsgev *iev)
struct dirent *de;
size_t i;
int badname;
- static int skip_warned; /* see the loop below */
+ static int skip_warned;
memset(&result, 0, sizeof(result));
- /*
- * Only a subscribed-only request reads the file, and a read that
- * fails is not fatal to the LIST: sl.present 0 means every mailbox is
- * subscribed, so the answer becomes every mailbox rather than none.
- */
if (req->subscribed_only && sublist_load(&sl) == -1)
sl.present = 0;
while ((de = readdir(dp)) != NULL) {
if (!mbox_root_entry_is_mailbox(de->d_name, &badname)) {
- /*
- * Logged (not silent): anything rejected by the name
- * rule is real mail made invisible over IMAP, likely
- * by the later SS5.1 UTF-8 rule, and the operator's
- * log is the only place to see why -- logged once per
- * store child, not once per LIST, so frequent Apple
- * Mail LISTs don't bury it.
- */
+ /* a rejected name hides real mail: say so */
if (badname && !skip_warned) {
skip_warned = 1;
log_warnx("session %u: LIST: skipping %s: "
}
closedir(dp);
- /*
- * Subscribed names with no mailbox left on disk. sl.n is 0 unless
- * this was a subscribed-only request, so the loop is skipped
- * entirely on a plain LIST.
- */
+ /* RFC 9051 SS6.3.9.6: subscribed names with no mailbox */
for (i = 0; i < sl.n; i++) {
struct stat st;
session_id);
}
-/* IMSG_MBOX_SUBSCRIBE (RFC 9051 SS6.3.7); idempotent, name must exist */
+/* RFC 9051 SS6.3.7 SUBSCRIBE; idempotent */
void
handle_mbox_subscribe(struct imsg_mbox_subscribe *req, struct imsgev *iev)
memset(&result, 0, sizeof(result));
- /*
- * INBOX is permanently subscribed and is never named in the file, so
- * this succeeds having written nothing (SS6.3.7: subscribing what is
- * already subscribed returns OK).
- */
if (mailbox_name_is_inbox(req->mailbox)) {
result.error = MBOX_OP_OK;
goto send;
}
- /*
- * SS6.3.7 leaves validating the name a MAY. Taking that MAY: this
- * namespace is flat and single-user, so a name that isn't there is a
- * typo rather than a mailbox that comes and goes, and saying so now
- * beats a permanent entry only an exact UNSUBSCRIBE can remove.
- */
+ /* RFC 9051 SS6.3.7 lets a server require the mailbox exist */
if (fstatat(maildir_root_fd, req->mailbox, &st,
AT_SYMLINK_NOFOLLOW) == -1 || !S_ISDIR(st.st_mode)) {
log_debug("session %u: SUBSCRIBE %s: no such mailbox",
session_id);
}
-/* IMSG_MBOX_UNSUBSCRIBE (RFC 9051 SS6.3.8); idempotent, no existence test */
+/* RFC 9051 SS6.3.8 UNSUBSCRIBE; idempotent */
void
handle_mbox_unsubscribe(struct imsg_mbox_subscribe *req, struct imsgev *iev)
memset(&result, 0, sizeof(result));
- /* the one name SS6.3.8's result table lets a server refuse outright */
if (mailbox_name_is_inbox(req->mailbox)) {
log_debug("session %u: UNSUBSCRIBE INBOX: refused",
session_id);
result.error = MBOX_OP_ERR_GENERIC;
goto send;
}
- /*
- * SS6.3.8 says a name may stay subscribed after its mailbox is gone,
- * so this does not ask whether the mailbox exists -- only whether the
- * name is one this server could ever have written down.
- */
+ /* RFC 9051 SS6.3.8: no existence test */
if (!mailbox_name_valid(req->mailbox)) {
log_debug("session %u: UNSUBSCRIBE %s: invalid name",
session_id, req->mailbox);
goto send;
}
- /*
- * First UNSUBSCRIBE on this account: write out what an absent file
- * stood for, minus this name, or the file would arrive claiming
- * nothing else is subscribed either.
- */
+ /* first UNSUBSCRIBE: write out what the absent file meant */
if (!sl.present && subs_materialise(&sl) == -1) {
result.error = MBOX_OP_ERR_GENERIC;
goto send;
session_id);
}
-/* host for maildir basename uniquer; cached, falls back to "imapd" */
const char *
append_hostname(void)
{
return (hostbuf);
}
-/*
- * The one APPEND in flight. IMSG_MBOX_APPEND opens a tmp/ file,
- * IMSG_MBOX_APPEND_DATA fills it, and IMSG_MBOX_APPEND_END commits it
- * once the listener has seen the command's closing CRLF (RFC 9051
- * SS6.3.12). A failure part way through is held until END, since the
- * rest of the literal is still arriving.
- */
-static struct {
- int active;
- int failed;
- enum mbox_op_error error;
- struct imsg_mbox_append req;
- uint64_t remaining;
- int tfd;
- int tmpfd;
- char basename[256];
- char tmppath[300];
-} ap;
-
-/* Closes what the in-flight APPEND holds and removes its tmp/ file. */
void
-append_abort(void)
+append_abort(struct store_session *ss)
{
- if (!ap.active)
+ struct store_append *ap = &ss->append;
+
+ if (!ap->active)
return;
- if (ap.tmpfd != -1)
- close(ap.tmpfd);
- if (ap.tmppath[0] != '\0' && ap.tfd != -1)
- unlinkat(ap.tfd, ap.tmppath, 0);
- if (ap.tfd != -1)
- close(ap.tfd);
- memset(&ap, 0, sizeof(ap));
+ if (ap->tmpfd != -1)
+ close(ap->tmpfd);
+ if (ap->tmppath[0] != '\0' && ap->tfd != -1)
+ unlinkat(ap->tfd, ap->tmppath, 0);
+ if (ap->tfd != -1)
+ close(ap->tfd);
+ memset(ap, 0, sizeof(*ap));
}
static void
session_id);
}
-/* Checks the target and opens its tmp/ file; the reply waits for END. */
void
-handle_mbox_append_begin(const struct imsg_mbox_append *req)
+handle_mbox_append_begin(struct store_session *ss,
+ const struct imsg_mbox_append *req)
{
+ struct store_append *ap = &ss->append;
char target[MBOX_NAME_MAX];
int64_t delivery_ts;
- if (ap.active) {
+ if (ap->active) {
log_warnx("session %u: APPEND begun with another in flight, "
"abandoning the first", session_id);
- append_abort();
+ append_abort(ss);
}
- memset(&ap, 0, sizeof(ap));
- ap.active = 1;
- ap.tfd = -1;
- ap.tmpfd = -1;
- ap.req = *req;
- ap.remaining = req->msglen;
+ memset(ap, 0, sizeof(*ap));
+ ap->active = 1;
+ ap->tfd = -1;
+ ap->tmpfd = -1;
+ ap->req = *req;
+ ap->remaining = req->msglen;
/* failed until the tmp/ file is open */
- ap.failed = 1;
- ap.error = MBOX_OP_ERR_GENERIC;
+ ap->failed = 1;
+ ap->error = MBOX_OP_ERR_GENERIC;
if (req->msglen > append_max) {
log_warnx("session %u: APPEND of %llu octets is over the %llu "
strlcpy(target, req->mailbox, sizeof(target)) >= sizeof(target)) {
log_debug("session %u: APPEND: invalid mailbox name",
session_id);
- ap.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
+ ap->error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
return;
}
- if ((ap.tfd = mailbox_open_dir(target)) == -1) {
+ if ((ap->tfd = mailbox_open_dir(target)) == -1) {
log_debug("session %u: APPEND %s: no such mailbox",
session_id, req->mailbox);
- ap.error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
+ ap->error = MBOX_OP_ERR_NO_SUCH_MAILBOX;
return;
}
- if (ensure_maildir_dirs(ap.tfd, "") == -1)
+ if (ensure_maildir_dirs(ap->tfd, "") == -1)
return;
delivery_ts = req->has_date ? req->date : (int64_t)time(NULL);
- if (snprintf(ap.basename, sizeof(ap.basename), "%lld.%d_%u.%s",
+ if (snprintf(ap->basename, sizeof(ap->basename), "%lld.%d_%u.%s",
(long long)delivery_ts, (int)getpid(), append_counter++,
- append_hostname()) >= (int)sizeof(ap.basename)) {
+ append_hostname()) >= (int)sizeof(ap->basename)) {
log_warnx("session %u: generated basename too long",
session_id);
return;
}
- if (snprintf(ap.tmppath, sizeof(ap.tmppath), "tmp/%s",
- ap.basename) >= (int)sizeof(ap.tmppath)) {
+ if (snprintf(ap->tmppath, sizeof(ap->tmppath), "tmp/%s",
+ ap->basename) >= (int)sizeof(ap->tmppath)) {
log_warnx("session %u: tmp path too long", session_id);
- ap.tmppath[0] = '\0';
+ ap->tmppath[0] = '\0';
return;
}
- if ((ap.tmpfd = openat(ap.tfd, ap.tmppath,
+ if ((ap->tmpfd = openat(ap->tfd, ap->tmppath,
O_WRONLY | O_CREAT | O_EXCL, 0600)) == -1) {
- log_warn("session %u: open %s", session_id, ap.tmppath);
+ log_warn("session %u: open %s", session_id, ap->tmppath);
/* not ours to remove, O_EXCL may have found another file */
- ap.tmppath[0] = '\0';
+ ap->tmppath[0] = '\0';
return;
}
- ap.failed = 0;
+ ap->failed = 0;
}
-/* Writes one piece of the literal to the tmp/ file. */
void
-handle_mbox_append_data(const char *buf, size_t len)
+handle_mbox_append_data(struct store_session *ss, const char *buf, size_t len)
{
+ struct store_append *ap = &ss->append;
size_t written = 0;
- if (!ap.active) {
+ if (!ap->active) {
log_warnx("session %u: APPEND data with no APPEND in flight, "
"ignoring", session_id);
return;
}
- if (len > ap.remaining) {
+ if (len > ap->remaining) {
log_warnx("session %u: APPEND data past the announced length",
session_id);
- ap.failed = 1;
- ap.error = MBOX_OP_ERR_GENERIC;
- ap.remaining = 0;
+ ap->failed = 1;
+ ap->error = MBOX_OP_ERR_GENERIC;
+ ap->remaining = 0;
return;
}
- ap.remaining -= len;
- if (ap.failed)
+ ap->remaining -= len;
+ if (ap->failed)
return;
while (written < len) {
ssize_t n;
- n = write(ap.tmpfd, buf + written, len - written);
+ n = write(ap->tmpfd, buf + written, len - written);
if (n == -1) {
if (errno == EINTR)
continue;
log_warn("session %u: write %s", session_id,
- ap.tmppath);
- ap.failed = 1;
- ap.error = MBOX_OP_ERR_GENERIC;
+ ap->tmppath);
+ ap->failed = 1;
+ ap->error = MBOX_OP_ERR_GENERIC;
return;
}
written += (size_t)n;
}
}
-/*
- * Commits the in-flight APPEND and replies. The index is updated before
- * the tmp/ to cur/ rename, so a rename failure leaves an indexed entry
- * missing on disk rather than a file nothing indexes.
- *
- * Returns 1 without replying when another session holds the index lock,
- * leaving ap as it was so that store.c can run this again.
- */
+/* the index is updated before the tmp/ to cur/ rename */
int
-handle_mbox_append_end(struct imsgev *iev)
+handle_mbox_append_end(struct store_session *ss)
{
+ struct store_append *ap = &ss->append;
+ struct imsgev *iev = &ss->iev;
struct imsg_mbox_appended reply;
struct mbox_index idx;
struct index_lock il = INDEX_LOCK_INIT;
memset(&reply, 0, sizeof(reply));
reply.error = MBOX_OP_ERR_GENERIC;
- if (!ap.active) {
+ if (!ap->active) {
log_warnx("session %u: APPEND end with no APPEND in flight",
session_id);
append_reply(iev, &reply);
return (0);
}
- if (!ap.failed && ap.remaining != 0) {
+ if (!ap->failed && ap->remaining != 0) {
log_warnx("session %u: APPEND ended %llu octets short",
- session_id, (unsigned long long)ap.remaining);
- ap.failed = 1;
- ap.error = MBOX_OP_ERR_GENERIC;
+ session_id, (unsigned long long)ap->remaining);
+ ap->failed = 1;
+ ap->error = MBOX_OP_ERR_GENERIC;
}
- if (ap.failed) {
- reply.error = ap.error;
+ if (ap->failed) {
+ reply.error = ap->error;
goto done;
}
/* A re-run after a busy lock finds this already done. */
- if (ap.tmpfd != -1) {
- if (fsync(ap.tmpfd) == -1)
+ if (ap->tmpfd != -1) {
+ if (fsync(ap->tmpfd) == -1)
log_warn("session %u: fsync %s (continuing)",
- session_id, ap.tmppath);
- close(ap.tmpfd);
- ap.tmpfd = -1;
+ session_id, ap->tmppath);
+ close(ap->tmpfd);
+ ap->tmpfd = -1;
}
- locked = index_lock_acquire(ap.tfd, &il, LOCK_EX | LOCK_NB);
+ locked = index_lock_acquire(ap->tfd, &il, LOCK_EX | LOCK_NB);
if (locked == 1)
return (1);
if (locked == -1)
goto done_index;
reply.uid = idx.uidnext;
- if (index_append(&idx, reply.uid, ap.basename) == -1)
+ if (index_append(&idx, reply.uid, ap->basename) == -1)
goto done_index;
idx.uidnext++;
- if (ap.req.keywords[0] != '\0') {
+ if (ap->req.keywords[0] != '\0') {
int n;
- /* carry forward the modseq index_append() assigned */
n = snprintf(line, sizeof(line), "%u:%s:%s:%llu", reply.uid,
- ap.basename, ap.req.keywords,
+ ap->basename, ap->req.keywords,
(unsigned long long)idx.highestmodseq);
if (n < 0 || (size_t)n >= sizeof(line)) {
log_warnx("session %u: index line too long for %s",
- session_id, ap.basename);
+ session_id, ap->basename);
goto done_index;
}
free(idx.lines[idx.nlines - 1]);
}
}
- if (index_save(ap.tfd, &idx) == -1)
+ if (index_save(ap->tfd, &idx) == -1)
goto done_index;
reply.uidvalidity = idx.uidvalidity;
index_lock_release(&il);
index_free(&idx);
- /* index committed, now rename; index-then-rename fails safer partway */
- sysflags_to_letters(ap.req.sysflags, letters, sizeof(letters));
- if (snprintf(curpath, sizeof(curpath), "cur/%s:2,%s", ap.basename,
+ sysflags_to_letters(ap->req.sysflags, letters, sizeof(letters));
+ if (snprintf(curpath, sizeof(curpath), "cur/%s:2,%s", ap->basename,
letters) >= (int)sizeof(curpath)) {
log_warnx("session %u: cur path too long for %s", session_id,
- ap.basename);
+ ap->basename);
goto done;
}
- if (renameat(ap.tfd, ap.tmppath, ap.tfd, curpath) == -1) {
- log_warn("session %u: rename %s -> %s", session_id, ap.tmppath,
+ if (renameat(ap->tfd, ap->tmppath, ap->tfd, curpath) == -1) {
+ log_warn("session %u: rename %s -> %s", session_id, ap->tmppath,
curpath);
goto done;
}
- ap.tmppath[0] = '\0'; /* now in cur/, append_abort() must keep it */
+ ap->tmppath[0] = '\0'; /* now in cur/, append_abort() must keep it */
reply.error = MBOX_OP_OK;
goto done;
done_index:
- /* both idempotent */
index_lock_release(&il);
index_free(&idx);
done:
- append_abort();
+ append_abort(ss);
append_reply(iev, &reply);
return (0);
}
blob - 1cc1977f0e44ec4adf3ef66db1f58f3b6faed340
blob + 77c3a0fc50453fb1d0601bbdf426d29413f81417
--- src/mbox_search.c
+++ src/mbox_search.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* SEARCH key evaluation. */
-
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include "log.h"
#include "store_internal.h"
-/* True if comma-list has kw as a whole token; used by merge_keywords(). */
static int
kw_list_contains(const char *list, const char *kw)
{
return (0);
}
-/*
- * Appends tok to out (comma-joined, tracking *firstp); shared
- * overflow-check body for merge_keywords()'s three tokenize loops below.
- */
static int
append_kw_token(char *out, size_t outsize, int *firstp, const char *tok)
{
- /*
- * Can happen on the ADD path: out and both inputs are
- * MBOX_FLAGS_MAX but ADD concatenates them, and strlcat(3) has
- * already written a truncated prefix by the time it reports
- * failure, so the caller must discard *out rather than store it.
- */
if (!*firstp && strlcat(out, ",", outsize) >= outsize) {
log_warnx("session %u: merge_keywords: keyword set does not "
"fit in %zu bytes", session_id, outsize);
return (1);
}
-/*
- * Applies mode/new_kws to old_kws into *out (SET ignores old_kws per RFC
- * 9051 SS6.4.6, dedup always); returns -1 if ADD's concatenation overflows
- * MBOX_FLAGS_MAX, since silently truncating used to cut a keyword
- * mid-token and drop the others under a tagged OK.
- */
+/* RFC 9051 SS6.4.6: SET ignores the old keywords */
int
merge_keywords(int mode, const char *old_kws, const char *new_kws,
char *out, size_t outsize)
}
}
- /*
- * 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, "
"can't happen (same-size MBOX_FLAGS_MAX buffers); "
return (0);
}
-/* Per-message context for search_eval()/search_eval_leaf() in this file. */
+/* A content key's value before the parser-worker has answered */
+#define SEARCH_UNKNOWN 2
+
+/* Parser requests a SEARCH makes before it lets other sessions run */
+#define SEARCH_YIELD_REQUESTS 64
+
+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 *);
+
struct search_msg_ctx {
uint32_t seqno;
uint32_t uid;
uint32_t sysflags;
- /* comma-separated, as elsewhere in file */
const char *keywords;
int64_t internaldate;
- uint64_t size; /* 64-bit for SEARCH LARGER/SMALLER */
+ uint64_t size;
uint64_t modseq; /* RFC 7162 SS3.1.5 MODSEQ search key */
};
-/*
- * One postfix node's evaluation against a message; SEARCH_OP_AND/OR/NOT
- * never reach here, handled by search_eval()'s stack machine.
- */
static int
search_eval_leaf(const struct search_node *n, const struct search_msg_ctx *m)
{
}
}
-/* Evaluates nodes[0..nnodes), postfix from listener.c's parse_search_key(). */
+/* 1 for the RFC 9051 SS6.4.4 keys the parser-worker answers */
+int
+search_op_content(int op)
+{
+ return (op == SEARCH_OP_SUBJECT || op == SEARCH_OP_HEADER ||
+ op == SEARCH_OP_SENTBEFORE || op == SEARCH_OP_SENTON ||
+ op == SEARCH_OP_SENTSINCE || op == SEARCH_OP_FROM ||
+ op == SEARCH_OP_TO || op == SEARCH_OP_CC || op == SEARCH_OP_BCC);
+}
+
static int
search_eval(const struct search_node *nodes, uint32_t nnodes,
- const struct search_msg_ctx *m)
+ const struct search_msg_ctx *m, const uint32_t *leafidx,
+ const char *bits)
{
int stack[SEARCH_PROGRAM_MAX_NODES];
uint32_t sp = 0, i;
+ int a, b;
for (i = 0; i < nnodes; i++) {
const struct search_node *n = &nodes[i];
case SEARCH_OP_AND:
if (sp < 2)
return (0);
- sp--;
- stack[sp - 1] = stack[sp - 1] && stack[sp];
+ b = stack[--sp];
+ a = stack[sp - 1];
+ stack[sp - 1] = (a == 0 || b == 0) ? 0 :
+ (a == 1 && b == 1) ? 1 : SEARCH_UNKNOWN;
break;
case SEARCH_OP_OR:
if (sp < 2)
return (0);
- sp--;
- stack[sp - 1] = stack[sp - 1] || stack[sp];
+ b = stack[--sp];
+ a = stack[sp - 1];
+ stack[sp - 1] = (a == 1 || b == 1) ? 1 :
+ (a == 0 && b == 0) ? 0 : SEARCH_UNKNOWN;
break;
case SEARCH_OP_NOT:
if (sp < 1)
return (0);
- stack[sp - 1] = !stack[sp - 1];
+ if (stack[sp - 1] != SEARCH_UNKNOWN)
+ stack[sp - 1] = !stack[sp - 1];
break;
default:
if (sp >= SEARCH_PROGRAM_MAX_NODES)
return (0);
- stack[sp++] = search_eval_leaf(n, m);
+ if (!search_op_content(n->op))
+ stack[sp++] = search_eval_leaf(n, m);
+ else if (bits == NULL)
+ stack[sp++] = SEARCH_UNKNOWN;
+ else
+ stack[sp++] = bits[leafidx[i]];
break;
}
}
return (sp == 1 ? stack[0] : 0);
}
-/*
- * IMSG_MBOX_SEARCH: LOCK_SH, iterates every message (no shortcut range),
- * evaluating search_eval() for each; streams matches, then
- * IMSG_MBOX_RESULT. Returns 0 having answered, or 1 without answering
- * because another session holds the index lock; store.c runs it again.
- */
int
handle_mbox_search(struct imsg_mbox_search *req, struct search_node *nodes,
- uint32_t nnodes, struct imsgev *iev)
+ uint32_t nnodes, struct store_session *ss)
{
- struct mbox_index idx;
- struct imsg_mbox_result result;
- struct index_lock il = INDEX_LOCK_INIT;
- int ok = 1, locked;
- uint32_t sent = 0;
- uint32_t max_uid = 0;
- uint32_t i;
+ struct store_search_walk *sw = &ss->search;
+ struct index_lock il = INDEX_LOCK_INIT;
+ struct imsg_parser_leaf leaf;
+ uint32_t max_uid, i;
+ int locked;
- /* nnodes, passed separately, is currently its only field */
- (void)req;
-
- memset(&idx, 0, sizeof(idx));
-
- /* Nodes are only rewritten after the load; busy leaves them intact. */
- locked = index_lock_acquire(mailbox_dir_fd, &il, LOCK_SH | LOCK_NB);
+ /* Before the reset below, so that a busy return changes nothing. */
+ locked = index_lock_acquire(ss->mailbox_dir_fd, &il, LOCK_SH | LOCK_NB);
if (locked == 1)
return (1);
+
+ if (sw->active) {
+ /* unreachable while the listener holds the next command */
+ log_warnx("session %u: SEARCH arrived during a SEARCH, "
+ "abandoning the earlier walk", session_id);
+ search_walk_abort(ss);
+ }
+ memset(sw, 0, sizeof(*sw));
+ evtimer_set(&sw->yield_ev, search_walk_yield, ss);
+
if (locked == -1) {
- ok = 0;
- goto done;
+ search_walk_finish(ss, 0);
+ return (0);
}
- if (index_load(il.fd, &idx) == -1) {
+ if (index_load(il.fd, &sw->idx) == -1) {
index_lock_release(&il);
- ok = 0;
- goto done;
+ search_walk_finish(ss, 0);
+ return (0);
}
index_lock_release(&il);
- /* shared with UID FETCH/STORE/EXPUNGE's own "*" resolution */
- max_uid = index_max_uid(&idx);
+ if (nnodes > 0)
+ memcpy(sw->nodes, nodes, nnodes * sizeof(*nodes));
+ sw->nnodes = nnodes;
- /*
- * Resolves each SEQSET/UIDSET node's "*" and normalizes backwards
- * ranges one at a time via the shared seqset_resolve() (SEARCH's
- * nodes sit scattered in a postfix AND/OR/NOT tree, so they can't
- * be batched like a single sequence-set); SEQSET clamps hi to
- * idx.nlines like FETCH/STORE, UIDSET doesn't.
- */
+ max_uid = index_max_uid(&sw->idx);
+
for (i = 0; i < nnodes; i++) {
- struct search_node *n = &nodes[i];
+ struct search_node *n = &sw->nodes[i];
struct seq_range in, out;
uint32_t max;
+ if (search_op_content(n->op)) {
+ memset(&leaf, 0, sizeof(leaf));
+ leaf.op = n->op;
+ leaf.str_off = n->str_off;
+ leaf.str_len = n->str_len;
+ leaf.name_off = n->name_off;
+ leaf.name_len = n->name_len;
+ leaf.num = n->num;
+ memcpy(sw->extra + sw->nleaves * sizeof(leaf), &leaf,
+ sizeof(leaf));
+ sw->leafidx[i] = sw->nleaves++;
+ continue;
+ }
if (n->op != SEARCH_OP_SEQSET && n->op != SEARCH_OP_UIDSET)
continue;
- max = (n->op == SEARCH_OP_SEQSET) ? (uint32_t)idx.nlines :
- max_uid;
+ max = (n->op == SEARCH_OP_SEQSET) ?
+ (uint32_t)sw->idx.nlines : max_uid;
in.lo = n->seq_lo;
in.hi = n->seq_hi;
in.lo_is_star = n->lo_is_star;
n->seq_hi = out.hi;
n->lo_is_star = n->hi_is_star = 0;
}
+ /* store.c has bounded poollen by the pool */
+ sw->poollen = req->poollen;
+ sw->extralen = sw->nleaves * sizeof(leaf);
+ memcpy(sw->extra + sw->extralen, req->pool, req->poollen);
+ sw->extralen += req->poollen;
- for (i = 1; i <= (uint32_t)idx.nlines; i++) {
+ sw->next = 1;
+ sw->active = 1;
+ search_walk_step(ss);
+ return (0);
+}
+
+/* answers 0 or 1, or -1 if it could not be checked */
+static int
+search_walk_ask(struct store_session *ss, const struct search_msg_ctx *m,
+ const char *basename)
+{
+ struct store_search_walk *sw = &ss->search;
+ struct imsg_parser_req preq;
+ struct imsg_parser_rep prep;
+ char *bits = NULL;
+ int mfd, ready, rc;
+
+ if ((ready = parser_ready()) == 0) {
+ sw->wait_parser = 1;
+ return (-2);
+ }
+ if (ready == -1) {
+ log_warnx("session %u: SEARCH needs the parser-worker and "
+ "none can be had", session_id);
+ return (-1);
+ }
+ if ((mfd = open_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+ basename)) == -1) {
+ log_warnx("session %u: message %s cannot be opened, SEARCH "
+ "cannot check it", session_id, basename);
+ return (-1);
+ }
+ memset(&preq, 0, sizeof(preq));
+ preq.nleaves = sw->nleaves;
+ preq.poollen = sw->poollen;
+ rc = parser_request(IMSG_PARSER_SEARCH, "SEARCH", mfd, basename,
+ &preq, sw->extra, sw->extralen, sw->nleaves, &prep, &bits);
+ close(mfd);
+ sw->asked++;
+ if (rc == -1) {
+ log_warnx("session %u: message %s could not be checked, "
+ "SEARCH fails", session_id, basename);
+ return (-1);
+ }
+ rc = search_eval(sw->nodes, sw->nnodes, m, sw->leafidx, bits);
+ free(bits);
+ return (rc == 1);
+}
+
+static void
+search_walk_step(struct store_session *ss)
+{
+ struct store_search_walk *sw = &ss->search;
+ struct imsgev *iev = &ss->iev;
+ struct timeval tv;
+ uint32_t i;
+
+ for (i = sw->next; i <= (uint32_t)sw->idx.nlines; i++) {
struct search_msg_ctx m;
struct index_rec rec;
const char *letters;
off_t size = 0;
char suffix[64];
+ int r;
- if (index_parse_line(idx.lines[i - 1], &rec) == -1)
+ if (index_parse_line(sw->idx.lines[i - 1], &rec) == -1)
continue;
memset(&m, 0, sizeof(m));
m.uid = rec.uid;
m.modseq = rec.modseq;
- if (locate_message_file(mailbox_dir_fd, rec.basename,
- &size, suffix, sizeof(suffix)) == -1) {
+ if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+ rec.basename, &size, suffix, sizeof(suffix)) == -1) {
log_warnx("session %u: message %s (uid %u) indexed "
"but missing on disk, skipped", session_id,
rec.basename, m.uid);
m.internaldate = parse_maildir_timestamp(rec.basename);
m.size = (uint64_t)size;
- if (search_eval(nodes, nnodes, &m)) {
+ r = search_eval(sw->nodes, sw->nnodes, &m, sw->leafidx, NULL);
+ if (r == SEARCH_UNKNOWN) {
+ if (sw->asked >= SEARCH_YIELD_REQUESTS) {
+ sw->asked = 0;
+ sw->next = i;
+ tv.tv_sec = 0;
+ tv.tv_usec = 0;
+ evtimer_add(&sw->yield_ev, &tv);
+ return;
+ }
+ r = search_walk_ask(ss, &m, rec.basename);
+ if (r == -2) {
+ sw->next = i;
+ return;
+ }
+ if (r == -1) {
+ search_walk_finish(ss, 0);
+ return;
+ }
+ }
+
+ if (r == 1) {
struct imsg_mbox_search_match match;
memset(&match, 0, sizeof(match));
log_warn("session %u: imsg_compose "
"IMSG_MBOX_SEARCH_MATCH", session_id);
else
- sent++;
+ sw->sent++;
}
}
+ search_walk_finish(ss, 1);
+}
-done:
- index_free(&idx);
+static void
+search_walk_finish(struct store_session *ss, int ok)
+{
+ struct store_search_walk *sw = &ss->search;
+ struct imsg_mbox_result result;
+ index_free(&sw->idx);
+ cur_snapshot_discard(&ss->cur_snap);
+
memset(&result, 0, sizeof(result));
result.error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC;
- result.count = sent;
- if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result,
+ result.count = sw->sent;
+ if (imsg_compose(&ss->iev.ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result,
sizeof(result)) == -1)
log_warn("session %u: imsg_compose IMSG_MBOX_RESULT",
session_id);
- return (0);
+ sw->active = 0;
}
+static void
+search_walk_yield(int fd, short event, void *arg)
+{
+ struct store_session *ss = arg;
+
+ (void)fd;
+ (void)event;
+ session_id = ss->id;
+ if (ss->search.active)
+ search_walk_step(ss);
+}
+
+int
+search_walk_paused(struct store_session *ss)
+{
+ return (ss->search.active);
+}
+
+void
+search_walk_abort(struct store_session *ss)
+{
+ struct store_search_walk *sw = &ss->search;
+
+ if (!sw->active)
+ return;
+ evtimer_del(&sw->yield_ev);
+ index_free(&sw->idx);
+ sw->active = 0;
+}
+
+void
+search_walk_parser(struct store_session *ss, int ok)
+{
+ struct store_search_walk *sw = &ss->search;
+
+ if (!sw->active || !sw->wait_parser)
+ return;
+ sw->wait_parser = 0;
+ if (!ok) {
+ log_warnx("session %u: no parser-worker, SEARCH fails",
+ session_id);
+ search_walk_finish(ss, 0);
+ return;
+ }
+ search_walk_step(ss);
+}
+
blob - 60e14a41865a3ce1ab8609c51f88be1aaff823e0
blob + 281b003d5dc7a3ec06075dc3f18e345f7cf3e01a
--- src/mbox_store.c
+++ src/mbox_store.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* mbox_store.c, STORE and EXPUNGE request handling. */
-
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include "log.h"
#include "store_internal.h"
-/*
- * What STORE does to one matched message, worked out before anything is
- * changed and kept so the renames can be undone and the responses sent
- * once the index is saved.
- */
struct store_step {
uint32_t seqno;
uint64_t modseq;
int modified; /* RFC 7162 UNCHANGEDSINCE miss */
- int changed; /* flags or keywords differ */
- int rename; /* the file's name changes */
+ int changed;
+ int rename;
int in_new;
char oldsuffix[64];
char letters[8];
};
-/*
- * Plans one message: its new letters, keywords and index line. Returns
- * 1 to store it, 0 to skip it (missing on disk, as before), -1 to refuse
- * the whole STORE.
- */
static int
-store_plan(const struct imsg_mbox_store *req, const struct index_rec *rec,
- uint64_t new_modseq, struct store_step *st, char *newline,
- size_t linesize)
+store_plan(struct store_session *ss, const struct imsg_mbox_store *req,
+ const struct index_rec *rec, uint64_t new_modseq, struct store_step *st,
+ char *newline, size_t linesize)
{
char suffix[64], newkeywords[MBOX_FLAGS_MAX];
const char *lp;
uint32_t old_sysflags, new_sysflags;
int len;
- if (locate_message_file(mailbox_dir_fd, rec->basename, &size, suffix,
- sizeof(suffix)) == -1) {
+ if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+ rec->basename, &size, suffix, sizeof(suffix)) == -1) {
log_warnx("session %u: message %s (uid %u) indexed but missing "
"on disk, skipped", session_id, rec->basename, rec->uid);
return (0);
break;
}
- /*
- * A merged keyword set that does not fit refuses the STORE rather
- * than storing a truncation under a tagged OK (RFC 9051 SS6.4.6).
- */
+ /* RFC 9051 SS6.4.6: refuse rather than store a truncation */
if (merge_keywords(req->mode, rec->keywords, req->keywords,
newkeywords, sizeof(newkeywords)) == -1) {
log_warnx("session %u: STORE: merged keyword set for uid %u "
return (1);
}
-/* The file's name before and after; -1 if either does not fit. */
static int
store_paths(const char *basename, const struct store_step *st,
char *oldpath, size_t oldsize, char *newpath, size_t newsize)
return (0);
}
-/*
- * Puts back the renames a failed STORE made, newest first. A rename
- * that cannot be undone is logged and stepped over, as COPY's rollback
- * does, since nothing better is available.
- */
static void
-store_undo(const struct mbox_index *idx, const struct store_step *steps,
- size_t nsteps)
+store_undo(struct store_session *ss, const struct mbox_index *idx,
+ const struct store_step *steps, size_t nsteps)
{
struct index_rec rec;
char oldpath[600], newpath[600];
store_paths(rec.basename, st, oldpath, sizeof(oldpath),
newpath, sizeof(newpath)) == -1)
continue;
- if (renameat(mailbox_dir_fd, newpath, mailbox_dir_fd,
+ if (renameat(ss->mailbox_dir_fd, newpath, ss->mailbox_dir_fd,
oldpath) == -1)
log_warn("session %u: STORE rollback: rename %s -> %s, "
"left with the new flags", session_id, newpath,
}
}
-/*
- * IMSG_MBOX_STORE (RFC 9051 SS6.4.6) under LOCK_EX. A first pass plans
- * every matched message and changes nothing, so a STORE refused for its
- * input touches no file. The second renames and edits the in-memory
- * index; if a rename or the save then fails, the renames are undone. The
- * FETCH echoes and RFC 7162 MODIFIED reports go out only once the index
- * is saved, so a NO means nothing changed.
- *
- * Returns 0 having answered the listener, or 1 without answering because
- * another session holds the index lock; store.c runs it again for that.
- */
+/* RFC 9051 SS6.4.6 STORE: plan every message, then change */
int
handle_mbox_store(struct imsg_mbox_store *req, const struct seq_range *ranges,
- uint32_t nranges, struct imsgev *iev)
+ uint32_t nranges, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
int locked;
struct mbox_index idx;
struct imsg_mbox_result result;
ok = 0;
goto done;
}
- /*
- * Nothing above this point has touched the mailbox or this session,
- * so returning here is free and the command can simply be run again.
- */
- locked = index_lock_acquire(mailbox_dir_fd, &il, LOCK_EX | LOCK_NB);
+ /* nothing touched yet: a busy return is free */
+ locked = index_lock_acquire(ss->mailbox_dir_fd, &il, LOCK_EX | LOCK_NB);
if (locked == 1)
return (1);
if (locked == -1) {
reported = idx.highestmodseq;
new_modseq = idx.highestmodseq + 1;
- /* RFC 9051 SS6.4.9: UID STORE's set is UIDs, as handle_mbox_fetch() */
+ /* RFC 9051 SS6.4.9 */
nresolved = seqset_resolve(ranges, nranges, req->by_uid ?
index_max_uid(&idx) : (uint32_t)idx.nlines, !req->by_uid,
resolved);
st.modified = 1;
st.modseq = rec.modseq;
} else {
- rc = store_plan(req, &rec, new_modseq, &st,
+ rc = store_plan(ss, req, &rec, new_modseq, &st,
newline, sizeof(newline));
if (rc == 0)
continue;
ok = 0;
break;
}
- if (st.rename && renameat(mailbox_dir_fd, oldpath,
- mailbox_dir_fd, newpath) == -1) {
+ if (st.rename && renameat(ss->mailbox_dir_fd, oldpath,
+ ss->mailbox_dir_fd, newpath) == -1) {
log_warn("session %u: rename %s -> %s",
session_id, oldpath, newpath);
free(dup);
if (ok && changed) {
idx.highestmodseq = new_modseq;
- if (index_save(mailbox_dir_fd, &idx) == -1)
+ if (index_save(ss->mailbox_dir_fd, &idx) == -1)
ok = 0;
}
if (!ok) {
- store_undo(&idx, steps, nsteps);
+ store_undo(ss, &idx, steps, nsteps);
goto done;
}
reported = idx.highestmodseq;
return (0);
}
-/*
- * IMSG_MBOX_EXPUNGE (also used, silent=1, by CLOSE) under LOCK_EX. The
- * compacted index is saved before any file is removed and before any
- * EXPUNGE response is composed. A failed save removes nothing and
- * reports nothing; a file that cannot be removed after the save is left
- * in cur/ with no index entry, which nothing reads, rather than an index
- * entry with no file, which every later command would trip over. The
- * decrement rule of RFC 9051 SS7.5.1 makes this order matter: a client
- * renumbers on each response it is sent.
- *
- * Returns 0 having answered the listener, or 1 without answering because
- * another session holds the index lock; store.c runs it again for that.
- */
+/* the compacted index is saved before any file is removed */
int
handle_mbox_expunge(struct imsg_mbox_expunge *req,
- const struct seq_range *ranges, uint32_t nranges, struct imsgev *iev)
+ const struct seq_range *ranges, uint32_t nranges, struct store_session *ss)
{
struct expunged {
char *line;
char suffix[64];
uint32_t seqno;
};
+ struct imsgev *iev = &ss->iev;
struct mbox_index idx;
struct imsg_mbox_result result;
struct index_lock il = INDEX_LOCK_INIT;
memset(&idx, 0, sizeof(idx));
/* Nothing above has touched the mailbox, so a busy return is free. */
- locked = index_lock_acquire(mailbox_dir_fd, &il, LOCK_EX | LOCK_NB);
+ locked = index_lock_acquire(ss->mailbox_dir_fd, &il, LOCK_EX | LOCK_NB);
if (locked == 1)
return (1);
if (locked == -1) {
}
reported = idx.highestmodseq;
- /*
- * UID EXPUNGE: resolve the UID ranges once, same seqset_resolve() "*"
- * resolution as UID FETCH/STORE; plain EXPUNGE and CLOSE send no
- * ranges and never consult them
- */
if (req->by_uid)
nresolved = seqset_resolve(ranges, nranges,
index_max_uid(&idx), 0, resolved);
idx.lines[out++] = idx.lines[in];
continue;
}
- if (locate_message_file(mailbox_dir_fd, rec.basename,
- &size, suffix, sizeof(suffix)) == -1) {
+ if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd,
+ rec.basename, &size, suffix, sizeof(suffix)) == -1) {
log_warnx("session %u: message %s indexed but missing "
"on disk, kept in index, not counted as "
"expunged", session_id, rec.basename);
idx.lines[out++] = idx.lines[in];
continue;
}
- /* SS6.4.9: \Deleted outside UID EXPUNGE's ranges is kept */
+ /* RFC 9051 SS6.4.9: \Deleted outside the ranges is kept */
if (req->by_uid &&
!seqset_contains(resolved, nresolved, rec.uid)) {
idx.lines[out++] = idx.lines[in];
if (ok && ngone > 0) {
idx.highestmodseq++;
- if (index_save(mailbox_dir_fd, &idx) == -1)
+ if (index_save(ss->mailbox_dir_fd, &idx) == -1)
ok = 0;
}
if (!ok) {
gone[k].suffix) >= (int)sizeof(path))
log_warnx("session %u: path too long for %s, left on "
"disk", session_id, rec.basename);
- else if (unlinkat(mailbox_dir_fd, path, 0) == -1 &&
+ else if (unlinkat(ss->mailbox_dir_fd, path, 0) == -1 &&
errno != ENOENT)
log_warn("session %u: unlink %s, left on disk with no "
"index entry", session_id, path);
done:
/* RFC 9051 SS6.4.1: CLOSE deselects only once its expunge is saved. */
if (req->silent && ok)
- mailbox_selected = 0;
+ ss->mailbox_selected = 0;
index_lock_release(&il);
blob - 1816ffbc3688578bbecb0cdbe54a822939add354
blob + f0673170737641f25ea55077ef7a880eec3800ad
--- src/mboxname.c
+++ src/mboxname.c
#include "mboxname.h"
#include "utf8.h"
-/*
- * RFC 9051 SS5.1: "INBOX" names this user's primary mailbox,
- * case-insensitively, in every command that takes a mailbox name. Callers peel
- * it off before mailbox_name_syntax_ok(), which would accept it as an ordinary
- * name and let a CREATE or DELETE through.
- */
+/* RFC 9051 SS5.1: INBOX is case-insensitive */
int
mailbox_name_is_inbox(const char *name)
{
return (strcasecmp(name, "INBOX") == 0);
}
-/*
- * Returns 1 if `name` is acceptable as a mailbox name on this server, 0
- * otherwise. RFC 9051 SS5.1/SS5.1.1 plus this implementation's own storage
- * rules; the caller decides what a refusal means on the wire, and INBOX is not
- * handled here (it is a name this predicate accepts, and
- * mailbox_name_is_inbox() peels it off before anyone asks). The store
- * additionally checks that the name arrived NUL-terminated before calling this,
- * since its copy comes off the imsg wire rather than out of its own parser --
- * see mailbox_name_valid() in store.c.
- */
int
mailbox_name_syntax_ok(const char *name)
{
if (len == 0 || len >= MBOX_NAME_MAX)
return (0);
- /*
- * RFC 9051 SS5.1: 8-bit mailbox names must comply with Net-Unicode; the
- * encoding test is its own shared predicate again (see utf8.c).
- */
if (!utf8_mailbox_ok(name))
return (0);
/* RFC 9051 SS5.1.1: "/" is the hierarchy delimiter */
if (c == '/')
return (0);
- /*
- * SS5.1 pt 2: MAY refuse CTL/non-graphic names; taking that MAY
- * for C0/DEL
- */
+ /* RFC 9051 SS5.1 lets a server refuse CTL names */
if (c < 0x20 || c == 0x7f)
return (0);
}
blob - 307e23dad0ae85a3d8ded4a0742f9850fa89960c
blob + 66731709f1033ef153c187bd122bfb804719994d
--- src/mboxname.h
+++ src/mboxname.h
#ifndef IMAPD_MBOXNAME_H
#define IMAPD_MBOXNAME_H
-/* The one mailbox-name syntax rule, shared by the listener's */
-/* listener_mailbox_name_valid() and the store's mailbox_name_valid(). The */
-/* two validators stay separate because the store does not trust the */
-/* listener, and re-checking across the imsg boundary is the point. What */
-/* they must not do is disagree about the rule, which they twice did. */
-/* The reserved filenames below are part of the rule: a mailbox may not be */
-/* named after one. What each file is FOR is documented in */
-/* store_internal.h, which includes this header. */
-/* Like utf8.h, this header depends on nothing, so either side can include */
-/* it without dragging in listener.h or store_internal.h. */
-
+/*
+ * Reserved names, refused as mailbox names. Locks are taken on the .lock
+ * files, since index_save() rename(2)s the index and flock(2) binds an
+ * inode; the uidvalidity file is rewritten in place and is its own lock.
+ * An index lock may be held while taking the uidvalidity lock, never the
+ * reverse. The subscriptions lock is taken alone, and an absent
+ * subscriptions file means every mailbox is subscribed.
+ */
#define STORE_INDEX_NAME "imapd.index"
#define STORE_INDEX_TMP_NAME "imapd.index.tmp"
#define STORE_INDEX_LOCK_NAME "imapd.index.lock"
#define STORE_SUBSCRIPTIONS_TMP_NAME "imapd.subscriptions.tmp"
#define STORE_SUBSCRIPTIONS_LOCK_NAME "imapd.subscriptions.lock"
+/* the one mailbox-name rule; the listener and the store must agree */
int mailbox_name_syntax_ok(const char *);
-/*
- * 1 if `name` is one of the reserved filenames above. mailbox_name_syntax_ok()
- * refuses such a name; the store also asks directly, to skip a directory named
- * after one quietly while walking the maildir root rather than reporting it as
- * a mailbox it could not list.
- */
int mailbox_name_reserved(const char *);
-/*
- * RFC 9051 SS5.1: INBOX is case-insensitive and always exists, so it is not a
- * name either side validates -- it is peeled off first and mapped to the
- * maildir root. One definition rather than two: the store and the listener
- * each used to carry an identical one-liner under a different name.
- */
+/* RFC 9051 SS5.1: INBOX is case-insensitive and always exists */
int mailbox_name_is_inbox(const char *);
#endif /* IMAPD_MBOXNAME_H */
blob - a3167586e86a8c6202f5d55df4ea81a7e7614998
blob + fa845ff835f6518c9e1430ea20524343597c0f2b
--- src/mime.c
+++ src/mime.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * mime.c: header and MIME parsing shared across FETCH, ENVELOPE, and
- * BODYSTRUCTURE -- content-type, multipart splitting, and per-part location.
- */
-
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include "log.h"
#include "store_internal.h"
-/*
- * One command's view of cur/. A walk over N messages used to read the
- * directory once per message, which is N reads of N entries; this reads it
- * once. Measured on premio before the change: 88% of a FETCH over 10,000
- * messages, and 99% over 100,000.
- *
- * A name absent from the snapshot falls through to a real scan below, so a
- * file created after the snapshot was taken is still found and the snapshot
- * can only ever cost a lookup, never change its answer.
- * cur_snapshot_discard() drops it at the end of every request (store.c),
- * the same lifetime as the index snapshot a walk already works from.
- */
-static struct {
- int dfd; /* directory described, -1 when empty */
- int failed; /* a build that failed is not retried */
- char **names; /* entry names, sorted by strcmp(3) */
- size_t n;
-} cur_snap = { -1, 0, NULL, 0 };
-
static int
cur_snap_cmp(const void *a, const void *b)
{
}
void
-cur_snapshot_discard(void)
+cur_snapshot_discard(struct cur_snapshot *snap)
{
size_t i;
- for (i = 0; i < cur_snap.n; i++)
- free(cur_snap.names[i]);
- free(cur_snap.names);
- cur_snap.names = NULL;
- cur_snap.n = 0;
- cur_snap.dfd = -1;
- cur_snap.failed = 0;
+ for (i = 0; i < snap->n; i++)
+ free(snap->names[i]);
+ free(snap->names);
+ snap->names = NULL;
+ snap->n = 0;
+ snap->dfd = -1;
+ snap->failed = 0;
}
-/* Reads cur/ into the snapshot; -1 having left it empty and not retryable. */
static int
-cur_snapshot_build(int dfd)
+cur_snapshot_build(struct cur_snapshot *snap, int dfd)
{
DIR *dp = NULL;
const struct dirent *de;
size_t n = 0, cap = 0;
int curfd;
- cur_snapshot_discard();
+ cur_snapshot_discard(snap);
curfd = openat(dfd, "cur", O_RDONLY | O_DIRECTORY);
if (curfd != -1 && (dp = fdopendir(curfd)) == NULL)
close(curfd);
if (dp == NULL) {
- cur_snap.failed = 1;
+ snap->failed = 1;
return (-1);
}
while ((de = readdir(dp)) != NULL) {
closedir(dp);
if (n > 1)
qsort(names, n, sizeof(*names), cur_snap_cmp);
- cur_snap.names = names;
- cur_snap.n = n;
- cur_snap.dfd = dfd;
+ snap->names = names;
+ snap->n = n;
+ snap->dfd = dfd;
return (0);
fail:
free(names[--n]);
free(names);
closedir(dp);
- cur_snap.failed = 1;
+ snap->failed = 1;
return (-1);
}
-/*
- * The "<basename>:*" entry in the snapshot, or NULL. Lower bound by binary
- * search then one prefix test: a maildir basename is unique, so at most one
- * entry can match.
- */
static const char *
-cur_snapshot_find(int dfd, const char *basename, size_t baselen)
+cur_snapshot_find(struct cur_snapshot *snap, int dfd, const char *basename,
+ size_t baselen)
{
size_t lo = 0, hi, mid;
- if (cur_snap.dfd != dfd) {
- if (cur_snap.failed)
+ if (snap->dfd != dfd) {
+ if (snap->failed)
return (NULL);
- if (cur_snapshot_build(dfd) == -1)
+ if (cur_snapshot_build(snap, dfd) == -1)
return (NULL);
}
- hi = cur_snap.n;
+ hi = snap->n;
while (lo < hi) {
mid = lo + (hi - lo) / 2;
- if (strncmp(cur_snap.names[mid], basename, baselen) < 0)
+ if (strncmp(snap->names[mid], basename, baselen) < 0)
lo = mid + 1;
else
hi = mid;
}
- if (lo < cur_snap.n &&
- strncmp(cur_snap.names[lo], basename, baselen) == 0 &&
- cur_snap.names[lo][baselen] == ':')
- return (cur_snap.names[lo]);
+ if (lo < snap->n &&
+ strncmp(snap->names[lo], basename, baselen) == 0 &&
+ snap->names[lo][baselen] == ':')
+ return (snap->names[lo]);
return (NULL);
}
-/* Writes one matched cur/ entry out as a path and a flag suffix. */
static int
cur_entry_take(const char *name, size_t baselen, char *path, size_t pathsize,
char *suffix_out, size_t suffix_out_size)
return (0);
}
-/*
- * Shared cur/ fallback lookup for locate_message_file()/open_message_file():
- * finds the "<basename>:*" entry, writes its "cur/<name>" path to path[] and
- * optionally its suffix to suffix_out; returns 0 on match, -1 on failure or
- * no match.
- */
static int
-scan_cur_for_basename(int dfd, const char *basename, char *path,
- size_t pathsize,
+scan_cur_for_basename(struct cur_snapshot *snap, int dfd,
+ const char *basename, char *path, size_t pathsize,
char *suffix_out, size_t suffix_out_size)
{
DIR *dp;
size_t baselen = strlen(basename);
int rv = -1;
- if ((hit = cur_snapshot_find(dfd, basename, baselen)) != NULL)
+ if ((hit = cur_snapshot_find(snap, dfd, basename, baselen)) != NULL)
return (cur_entry_take(hit, baselen, path, pathsize,
suffix_out, suffix_out_size));
}
int
-locate_message_file(int dfd, const char *basename, off_t *size_out,
- char *suffix_out, size_t suffix_out_size)
+locate_message_file(struct cur_snapshot *snap, int dfd, const char *basename,
+ off_t *size_out, char *suffix_out, size_t suffix_out_size)
{
struct stat st;
char path[600];
return (-1);
}
- if (scan_cur_for_basename(dfd, basename, path, sizeof(path),
+ if (scan_cur_for_basename(snap, dfd, basename, path, sizeof(path),
suffix_out, suffix_out_size) == -1)
return (-1);
if (fstatat(dfd, path, &st, 0) == -1)
return (0);
}
-/*
- * Same new/ -> cur/ fallback lookup as locate_message_file(), but opens the
- * file and returns a readable fd instead of stat()'ing it.
- */
int
-open_message_file(int dfd, const char *basename)
+open_message_file(struct cur_snapshot *snap, int dfd, const char *basename)
{
char path[600];
int fd;
return (-1);
}
- if (scan_cur_for_basename(dfd, basename, path, sizeof(path), NULL,
- 0) == -1)
+ if (scan_cur_for_basename(snap, dfd, basename, path, sizeof(path),
+ NULL, 0) == -1)
return (-1);
return (openat(dfd, path, O_RDONLY));
}
-/*
- * Returns basename's raw RFC 5322 header block through the blank-line separator
- * ("\r\n\r\n" or "\n\n"); -1 past FETCH_HEADER_MAX or on NUL.
- */
int
-read_message_header(int dfd, const char *basename, char **buf_out,
+read_message_header(struct cur_snapshot *snap, int dfd, const char *basename,
+ char **buf_out, uint32_t *len_out)
+{
+ int fd, rc;
+
+ if ((fd = open_message_file(snap, dfd, basename)) == -1)
+ return (-1);
+ rc = read_header_from_fd(fd, basename, buf_out, len_out);
+ close(fd);
+ return (rc);
+}
+
+/* for the parser-worker, whose pledge(2) lacks "rpath" */
+int
+read_header_from_fd(int fd, const char *basename, char **buf_out,
uint32_t *len_out)
{
char readbuf[FETCH_HEADER_MAX + 1];
- int fd;
ssize_t n, total = 0;
size_t i, hdrend = 0, limit, sepindex = 0;
int found_sep = 0;
- if ((fd = open_message_file(dfd, basename)) == -1)
- return (-1);
-
while (total < (ssize_t)sizeof(readbuf)) {
n = read(fd, readbuf + total, sizeof(readbuf) - total);
if (n == -1) {
continue;
log_warn("session %u: read message header (%s)",
session_id, basename);
- close(fd);
return (-1);
}
if (n == 0)
break;
total += n;
}
- close(fd);
if (find_header_body_split(readbuf, (size_t)total, &hdrend) == 0) {
sepindex = hdrend;
found_sep = 1;
}
- /*
- * NUL check is bounded to where the separator was found; the "i + 1 <
- * total" term looks like an off-by-one but isn't -- when limit == total
- * the unexamined final byte is always the separator's trailing '\n',
- * never a NUL.
- */
limit = found_sep ? sepindex : (size_t)total;
for (i = 0; i < limit && i + 1 < (size_t)total; i++) {
if (readbuf[i] == '\0') {
return (0);
}
-/*
- * Reads the whole message (text_only=0) or just past the header separator
- * (text_only=1, SS6.4.5.1 TEXT); maxlen/label vary per call site.
- */
int
-read_message_body(int dfd, const char *basename, int text_only,
+read_body_from_fd(int fd, const char *basename, int text_only,
size_t maxlen, const char *label, char **buf_out,
uint32_t *len_out)
{
char *readbuf;
size_t readbuf_size;
struct stat st;
- int fd;
ssize_t n, total = 0;
- /*
- * sepindex is size_t, not int: readbuf's size comes from the
- * configurable maxlen (bodystructure_read_max), and narrowing to int
- * was only safe because parse.y caps it at 1GB -- past 2GB it would go
- * negative, pointing before the allocation and wrapping the memcpy
- * length.
- */
size_t i, hdrend, sepindex = 0;
*buf_out = NULL;
*len_out = 0;
- if ((fd = open_message_file(dfd, basename)) == -1)
- return (-1);
-
- /*
- * Size the buffer to the message, not the configured ceiling --
- * allocating the full cap (up to 1GB) per message made "FETCH 1:*
- * BODYSTRUCTURE" do one huge malloc(3)+read(2) per message; maxlen+1
- * slack is kept at/over the cap so the "exceeds maxlen" check still
- * fires.
- */
+ /* sized to the message, not the configured ceiling */
readbuf_size = maxlen + 1;
if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_size >= 0 &&
(uint64_t)st.st_size < (uint64_t)maxlen)
if ((readbuf = malloc(readbuf_size)) == NULL) {
log_warn("session %u: malloc message body readbuf (%s)",
session_id, basename);
- close(fd);
return (-1);
}
continue;
log_warn("session %u: read message body (%s)",
session_id, basename);
- close(fd);
free(readbuf);
return (-1);
}
break;
total += n;
}
- close(fd);
if (total > (ssize_t)maxlen) {
log_warnx("session %u: message %s exceeds %zu bytes, "
return (0);
}
-/*
- * name[0..namelen) matches a space-separated name in list, ASCII-range
- * case-insensitively (RFC 9051 SS6.4.5.1); re-tokenized each call.
- */
+/* RFC 9051 SS6.4.5.1: ASCII case-insensitive */
int
header_field_name_matches(const char *name, size_t namelen, const char *list)
{
return (0);
}
-/*
- * One logical header field: the byte range [start, end) covering its first line
- * and every RFC 5322 SS2.2.3 obs-fold continuation that belongs to it, plus
- * line_end (the first line's end, before CR/LF) and colon (the ':' index, or
- * line_end when the line has none).
- */
+/* one field plus its RFC 5322 SS2.2.3 obs-fold lines */
struct hdr_field {
size_t start;
size_t colon;
size_t end;
};
-/*
- * Walks one field forward from *off. Returns 1 for a field, 0 for the blank
- * line that ends the header (with [start, end) covering that line, since a
- * HEADER.FIELDS response has to include it), and -1 for a header that ran out
- * without one. Both callers used to write this out themselves; the -1/0 split
- * in particular was expressed at each site as two different breaks setting two
- * different values, which is exactly the distinction that is easy to get wrong.
- */
static int
hdr_next_field(const char *hdr, size_t hdrlen, size_t *off,
struct hdr_field *f)
break;
}
- /*
- * obs-fold continuation lines start with SP/HTAB and belong to this
- * same field, so *off must land past them either way -- a caller that
- * ignored them would resume mid-field
- */
while (*off < hdrlen && (hdr[*off] == ' ' || hdr[*off] == '\t')) {
j = *off;
while (j < hdrlen && hdr[j] != '\n')
return (1);
}
-/*
- * Implements BODY.PEEK[HEADER.FIELDS[.NOT] (fields_spec)] (RFC 9051 SS6.4.5.1):
- * splits the raw header on RFC 5322 obs-fold lines and copies matching fields
- * verbatim.
- */
+/* RFC 9051 SS6.4.5.1 HEADER.FIELDS[.NOT] */
int
-read_message_header_fields(int dfd, const char *basename,
+filter_header_fields(const char *hdr, size_t hdrlen, const char *fields_spec,
+ int want_not, char *out, size_t *outlen_out)
+{
+ size_t outlen = 0;
+ size_t off = 0;
+
+ *outlen_out = 0;
+
+ for (;;) {
+ struct hdr_field f;
+ int matched, include, r;
+
+ r = hdr_next_field(hdr, hdrlen, &off, &f);
+ if (r == -1)
+ /* no terminating blank line */
+ return (-1);
+ if (r == 0) {
+ /* RFC 9051 SS6.4.5: the blank line is included */
+ memcpy(out + outlen, hdr + f.start, f.end - f.start);
+ outlen += f.end - f.start;
+ *outlen_out = outlen;
+ return (0);
+ }
+
+ matched = header_field_name_matches(hdr + f.start,
+ f.colon - f.start, fields_spec);
+ include = want_not ? !matched : matched;
+
+ if (include) {
+ memcpy(out + outlen, hdr + f.start, f.end - f.start);
+ outlen += f.end - f.start;
+ }
+ }
+}
+
+/* for the parser-worker, whose pledge(2) lacks "rpath" */
+int
+read_message_header_fields(int fd, const char *basename,
const char *fields_spec, int want_not, char **buf_out,
uint32_t *len_out)
{
uint32_t hdrlen = 0;
char *out;
size_t outlen = 0;
- size_t off = 0;
- int rc = -1;
*buf_out = NULL;
*len_out = 0;
- if (read_message_header(dfd, basename, &hdrbuf, &hdrlen) == -1)
+ if (read_header_from_fd(fd, basename, &hdrbuf, &hdrlen) == -1)
return (-1);
- /*
- * 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);
return (-1);
}
- for (;;) {
- struct hdr_field f;
- int matched, include, r;
-
- r = hdr_next_field(hdrbuf, hdrlen, &off, &f);
- if (r == -1)
- /* rc stays -1: no terminating blank line */
- break;
- if (r == 0) {
- /*
- * the blank line is copied too: SS6.4.5's HEADER.FIELDS
- * data is a header block, and a header block ends with
- * one
- */
- memcpy(out + outlen, hdrbuf + f.start,
- f.end - f.start);
- outlen += f.end - f.start;
- rc = 0;
- break;
- }
-
- matched = header_field_name_matches(hdrbuf + f.start,
- f.colon - f.start, fields_spec);
- include = want_not ? !matched : matched;
-
- if (include) {
- memcpy(out + outlen, hdrbuf + f.start,
- f.end - f.start);
- outlen += f.end - f.start;
- }
- }
-
- free(hdrbuf);
-
- if (rc == -1) {
+ if (filter_header_fields(hdrbuf, hdrlen, fields_spec, want_not, out,
+ &outlen) == -1) {
+ free(hdrbuf);
free(out);
return (-1);
}
+ free(hdrbuf);
*buf_out = out;
*len_out = (uint32_t)outlen;
return (0);
}
-/*
- * Finds the first field named `name`, returns its unfolded value (RFC 5322
- * SS2.2.3: CRLF+WSP -> WSP kept); NIL vs "" per SS7.5.2.
- */
+/* RFC 5322 SS2.2.3 unfolding; NIL vs "" per RFC 9051 SS7.5.2 */
int
extract_header_field(const char *hdr, size_t hdrlen, const char *name,
char **val_out, size_t *vallen_out)
char *out;
size_t outlen = 0;
- /*
- * a blank line (header ended, name never seen) and a header
- * that ran out both mean "no value", which is what this
- * returned for either before the walk was shared
- */
if (hdr_next_field(hdr, hdrlen, &off, &f) != 1)
return (-1);
strncasecmp(hdr + f.start, name, namelen) != 0)
continue;
- /*
- * RFC 5322 SS2.2.3: the value runs from the colon to the end of
- * the last fold line, with CRLF/LF dropped and leading WSP
- * trimmed -- f.end already covers the folds
- */
vstart = f.colon + 1;
vend = f.end;
if (vend > vstart && hdr[vend - 1] == '\n')
}
int
+header_next_field(const char *hdr, size_t hdrlen, size_t *off,
+ const char **name, size_t *namelen, const char **val, size_t *vallen)
+{
+ struct hdr_field f;
+ size_t vstart;
+
+ for (;;) {
+ if (hdr_next_field(hdr, hdrlen, off, &f) != 1)
+ return (0);
+ /* a line with no colon is not a field (RFC 5322 SS2.2) */
+ if (f.colon == f.line_end)
+ continue;
+ vstart = f.colon + 1;
+ while (vstart < f.end &&
+ (hdr[vstart] == ' ' || hdr[vstart] == '\t'))
+ vstart++;
+ *name = hdr + f.start;
+ *namelen = f.colon - f.start;
+ *val = hdr + vstart;
+ *vallen = f.end - vstart;
+ return (1);
+ }
+}
+
+int
find_header_body_split(const char *buf, size_t len, size_t *hdrend_out)
{
size_t i;
+ /* a part with no header fields starts with CRLF */
+ if (len >= 2 && buf[0] == '\r' && buf[1] == '\n') {
+ *hdrend_out = 2;
+ return (0);
+ }
+ if (len >= 1 && buf[0] == '\n') {
+ *hdrend_out = 1;
+ return (0);
+ }
+
for (i = 0; i + 1 < len; i++) {
if (i + 3 < len && buf[i] == '\r' && buf[i + 1] == '\n' &&
buf[i + 2] == '\r' && buf[i + 3] == '\n') {
return (strchr("()<>@,;:\\\"/[]?=", c) != NULL);
}
-/*
- * reads one RFC 2045 token/quoted-string at s[*pos], advancing *pos; undoes RFC
- * 822 quoted-pair escaping ("\" + one CHAR) in quotes
- */
+/* RFC 2045 token or quoted-string */
int
mime_read_token_or_qstring(const char *s, size_t len, size_t *pos,
char *out, size_t outsize)
(*pos)++;
c = s[*pos];
}
- /*
- * Unlike the unquoted-token branch below, this branch
- * applied no character class, letting a lone CR that
- * extract_header_field() didn't unfold ride into the
- * BODYSTRUCTURE; parse_content_type() degrades to its
- * RFC 2045 SS5.2 default on -1.
- */
+ /* no CR, LF or NUL inside a quoted-string either */
if (c == '\0' || c == '\r' || c == '\n')
return (-1);
if (outlen + 1 >= outsize)
return (0);
}
-/*
- * in-place ASCII-range uppercase, to canonicalize MIME type/subtype/attribute
- * names (RFC 9051 SS7.5.2 examples); values left as-is
- */
void
mime_str_upper(char *s)
{
}
}
-/*
- * RFC 2045 SS5.1 Content-Type parse into type/subtype (uppercased) +
- * params_fmt_out (RFC 9051 body-fld-param); SS5.2 default on failure
- */
+/* RFC 2045 SS5.1 Content-Type; SS5.2 default on failure */
int
parse_content_type(const char *hdr, size_t hdrlen, char *type_out,
size_t typesize, char *subtype_out, size_t subtypesize,
nparams++;
if (strcasecmp(attr, "BOUNDARY") == 0) {
- /*
- * boundary_out is RFC 2046 SS5.1.1-sized (70+1); an
- * oversized value is "no BOUNDARY found", not truncated
- */
+ /* RFC 2046 SS5.1.1: at most 70 characters */
if (strlcpy(boundary_out, value, boundary_outsize) <
boundary_outsize)
*has_boundary_out = 1;
return (0);
}
-/*
- * RFC 2046 SS5.1.1: splits multipart body into body-part spans (not yet
- * header/body split, caller uses find_header_body_split())
- */
+/* RFC 2046 SS5.1.1 */
int
split_multipart(const char *body, size_t bodylen, const char *boundary,
size_t *part_starts, size_t *part_ends, int *nparts_out, int maxparts)
after++;
if (after < bodylen && body[after] != '\r' &&
body[after] != '\n') {
- /*
- * not CRLF/LF/EOF: coincidental match, not a real
- * delimiter
- */
+ /* a coincidental match, not a delimiter */
pos++;
continue;
}
size_t content_end = pos;
size_t part_start = part_starts[n - 1];
- /*
- * bound strip at part_start: else an empty part
- * underflows the unsigned length into a huge
- * memcpy/scan bound
- */
+ /* an empty part must not underflow the length */
if (content_end >= part_start + 2 &&
body[content_end - 2] == '\r' &&
body[content_end - 1] == '\n')
else if (after < bodylen && body[after] == '\n')
after += 1;
else
- /*
- * delimiter runs to EOF with no CRLF, no body-part can
- * follow
- */
break;
if (n >= maxparts)
return (0);
}
-/*
- * parses a dotted section-part string (e.g. "1.2.3") into path[]; RFC 9051
- * SS6.4.5 section-part := nz-number *("." nz-number)
- */
+/* RFC 9051 SS6.4.5 section-part */
int
parse_section_part(const char *s, int *path, int maxpath)
{
return (n);
}
-/*
- * recursive descent once path[0] establishes MULTIPART; walks exactly one child
- * per level (the one path[0] names), mirrors build_body_structure()
- */
int
find_mime_part(int depth, const char *hdr, size_t hdrlen, const char *body,
size_t bodylen, const int *path, int pathlen, const char **part_out,
pbuf = body + part_starts[want - 1];
plen = part_ends[want - 1] - part_starts[want - 1];
- /* same empty-part handling as build_body_structure() */
if (plen == 0)
phdrend = 0;
else if (find_header_body_split(pbuf, plen, &phdrend) == -1)
return (-1);
if (pathlen == 1) {
- /*
- * path consumed; must be a genuine leaf (not
- * MULTIPART/MESSAGE-RFC822|GLOBAL), no "combined children"
- * concept exists
- */
+ /* a leaf only: no "combined children" of a multipart */
char ctype[64], csub[64], cparams[600];
char cboundary[70 + 1];
int chb;
plen - phdrend, path + 1, pathlen - 1, part_out, partlen_out));
}
-/*
- * locates a leaf MIME part by dotted path; handles RFC 9051 SS6.4.5.1's
- * non-multipart top-level case (own body = section-part "1")
- */
+/* RFC 9051 SS6.4.5.1: a non-multipart message is its own part 1 */
int
locate_mime_part(const char *hdr, size_t hdrlen, const char *body,
size_t bodylen, const int *path, int pathlen, const char **part_out,
*len = count;
}
-/*
- * BODY.PEEK[<section-part>]: reads the whole message (bodystructure_read_max
- * cap) to find the part, and returns where the part lies in the file.
- */
int
-extract_mime_part(int dfd, const char *basename, const int *path,
+extract_mime_part(int fd, const char *basename, const int *path,
int pathlen, uint64_t *off_out, uint64_t *len_out)
{
char *wholebuf = NULL;
*off_out = 0;
*len_out = 0;
- if (read_message_body(dfd, basename, 0, bodystructure_read_max,
+ if (read_body_from_fd(fd, basename, 0, bodystructure_read_max,
"BODY[<part>]", &wholebuf, &wholelen) == -1)
return (-1);
if (wholelen == 0 ||
return (-1);
}
- /*
- * RFC 9051 SS6.4.5: a section-part the message does not have is an
- * empty item, not a failure. found=0 means the server could not
- * produce what was asked for, which this is not; SS6.4.5.1 leaves
- * the nonexistent case unspecified. The out parameters keep the
- * empty values set at entry.
- */
+ /* RFC 9051 SS6.4.5: a missing part is an empty item */
if (locate_mime_part(wholebuf, hdrend, wholebuf + hdrend,
wholelen - hdrend, path, pathlen, &part, &partlen) == 0) {
- /* wholebuf was read from the file's first octet */
*off_out = (uint64_t)(part - wholebuf);
*len_out = (uint64_t)partlen;
}
return (0);
}
-/*
- * Finds BODY[] (text_only 0) or BODY[TEXT] (text_only 1) in basename by
- * reading it in blocks, and returns the open descriptor with the range. A
- * NUL is refused, since a literal carries CHAR8, %x01-ff (RFC 9051 SS9).
- * *fdbusy_out is set when the open failed for want of a descriptor.
- */
int
-message_body_range(int dfd, const char *basename, int text_only,
- uint64_t *off_out, uint64_t *len_out, int *fdbusy_out)
+message_body_range(struct cur_snapshot *snap, int dfd, const char *basename,
+ int text_only, uint64_t *off_out, uint64_t *len_out, int *fdbusy_out)
{
char blk[65536];
unsigned char c, b1 = 0, b2 = 0, b3 = 0;
*len_out = 0;
*fdbusy_out = 0;
- if ((fd = open_message_file(dfd, basename)) == -1) {
+ if ((fd = open_message_file(snap, dfd, basename)) == -1) {
if (errno == EMFILE || errno == ENFILE)
*fdbusy_out = 1;
return (-1);
return (fd);
}
-/*
- * builds the space-separated IMAP flag-atom list from maildir flag-suffix
- * letters + index keywords; letters per Courier's maildir(5)
- */
+/* flag letters as in Courier's maildir(5) */
void
build_flags_string(const char *maildir_suffix, const char *keywords,
char *out, size_t outsize)
}
}
-/*
- * RFC 9051 SS2.3.1.1 INTERNALDATE: uses the maildir basename's leading
- * timestamp field, not mtime; falls back to now if non-conforming
- */
+/* RFC 9051 SS2.3.3: from the basename, not mtime */
int64_t
parse_maildir_timestamp(const char *basename)
{
blob - a1ad40f9dd931b15cc09fee8a43afe409432650c
blob + 419823c5e4586ed3d978ce3412b312479e50daa4
--- src/parent.c
+++ src/parent.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * parent.c: privileged supervisor -- owns listen sockets, forks a paired
- * listener-worker/auth-worker per accepted connection (spawn_connection()) so a
- * compromise's blast radius is one connection; keymgr remains the sole
- * boot-time child. Retires the old single-listener IMSG_SESSION_OPEN/CLOSE and
- * listener-side MaxStartups machinery now that parent tracks sessions and
- * throttling itself.
- */
-
#include <sys/types.h>
#include <sys/queue.h>
#include <sys/resource.h>
/* l2tpd and pptpd both wait 5s for the same job */
#define SHUTDOWN_TIMEOUT_SEC 5
-/* caps children vs fork-flood PID/fd exhaustion */
-#define STORE_CHILD_MAX 64
-
-/*
- * Caps open_sessions tracking as a generous secondary backstop above
- * STORE_CHILD_MAX; count_startups()/startups_should_drop() below are the real
- * admission throttle, and past this cap spawn_connection() refuses the
- * connection outright.
- */
+/* backstop for "connections max", whose range it bounds */
#define OPEN_SESSION_MAX 4096
+/* how long accept(2) waits when out of descriptors, as in httpd */
+#define ACCEPT_PAUSE_SEC 1
+
struct child {
pid_t pid;
enum openimap_proc_type type;
TAILQ_ENTRY(child) entry;
};
-/*
- * per-session store child; "pending" until IMSG_SETUP_DONE arrives or times
- * out; one struct, never copied (embeds struct event)
- */
+/* one store child per account, shared by its sessions */
struct store_child {
uint32_t session_id;
+ uid_t uid;
+ gid_t gid;
+ char maildir[STORE_MAILDIR_MAX];
+ unsigned int nsessions;
+ int exiting;
pid_t pid;
- struct imsgev iev; /* parent<->this store child */
+ pid_t parser_pid;
+ struct imsgev iev;
int pending;
struct event timeout_ev;
- /* notify on failure, see store_child_fail() */
- struct imsgev *listener_iev;
TAILQ_ENTRY(store_child) entry;
};
-/*
- * Tracks one spawn_connection()-forked session from fork to reap_child()'s exit
- * notice; authenticated flips true on a validated IMSG_AUTH_CRED (rejecting
- * duplicates), and listener_iev/auth_iev are this session's own paired
- * channels, cleared to NULL when reap_child() sees that worker exit.
- */
struct open_session {
uint32_t session_id;
int authenticated;
struct imsgev *listener_iev;
struct imsgev *auth_iev;
pid_t listener_pid;
- /* 0 if no auth-worker was spawned/is left */
pid_t auth_pid;
- /* 0 if no search-oracle spawned/left */
- pid_t search_pid;
+ struct store_child *store;
+ struct sockaddr_storage peer;
TAILQ_ENTRY(open_session) entry;
};
static struct openimap_config *gconf;
static char progpath[PATH_MAX];
static char **saved_argv;
-/* set in parent_main(), reloaded on SIGHUP */
static const char *conf_path;
static struct event ev_sighup, ev_sigterm, ev_sigchld;
-/* the accept-loop events parent now owns, see this file's header comment */
static struct event ev_accept_cleartext[LISTENER_MAX_ADDRS];
static struct event ev_accept_tls[LISTENER_MAX_ADDRS];
-/* from listener.c; spawn_connection() */
static uint32_t next_session_id = 1;
-/* shutdown bookkeeping; set and read by sigterm_handler() */
static int shutting_down;
static struct event ev_shutdown;
-/* static so sigterm_handler() can unhook the accept events too */
static int n_cleartext, n_tls;
+/* accept(2) paused for descriptors, and "connections max" reached */
+static struct event ev_accept_pause;
+static int connections_full;
static __dead void exec_self_as(const char *);
static pid_t fork_child(enum openimap_proc_type, struct imsgev **,
static struct open_session *open_session_find(uint32_t);
static unsigned int count_startups(void);
static int startups_should_drop(unsigned int);
+static void accept_pause(void);
+static void accept_resume(int, short, void *);
+static unsigned int count_sessions(void);
+static int same_peer(const struct sockaddr_storage *,
+ const struct sockaddr_storage *);
+static unsigned int count_startups_from(const struct sockaddr_storage *);
+static void refuse_connection(int, int);
static void parent_accept(int, short, void *);
static void spawn_connection(int, int, const struct sockaddr_storage *,
socklen_t);
static void parent_handle_store_fork(struct open_session *, uid_t, gid_t,
const char *);
-static void store_fork_failed(struct open_session *);
+static void store_fork_failed(struct open_session *, int);
+static int store_child_attach(struct store_child *,
+ struct open_session *);
+static void store_child_session_gone(struct store_child *);
+static void store_child_parser(struct store_child *);
static void store_child_dispatch(int, short, void *);
static void store_child_timeout(int, short, void *);
static void store_child_teardown(struct store_child *, int);
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 */
__dead void
parent_main(const char *conffile, int argc, char *argv[],
struct openimap_config *conf)
int i;
struct rlimit rl;
- /* fork_child() et al. walk saved_argv for a NULL terminator */
(void)argc;
- /* main.c checks this first; kept so parent_main() stands alone */
if (geteuid() != 0)
fatalx("parent must start as root (running as uid %u)",
(unsigned int)geteuid());
- /*
- * Raise the descriptor soft limit to the hard limit (root, pre-pledge,
- * pre-bind/fork): the default "daemon" login class caps descriptors
- * well below what MaxStartups' default concurrency needs, at 3-4 fds
- * per session.
- */
+ /* the "daemon" login class caps descriptors below what startups need */
if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
log_warn("getrlimit RLIMIT_NOFILE");
else if (rl.rlim_cur < rl.rlim_max) {
if (realpath(argv[0], progpath) == NULL)
fatal("realpath");
- /*
- * bind(2) on <1024 needs root, done here, before any privdrop. Kept
- * open for the daemon's whole lifetime now -- see this file's header
- * comment -- not handed off and closed.
- */
+ /* bind(2) below port 1024 needs root */
n_cleartext = conf->port_cleartext == 0 ? 0 :
bind_listen_socket(conf->listen_addr, conf->port_cleartext,
cleartext_fds);
event_init();
- /*
- * keymgr is the sole remaining boot-time child (per-connection
- * listener/auth are spawned later by spawn_connection()); send
- * IMSG_KEYMGR_INIT before the peer handshake so real key material is in
- * place before its permission gate opens.
- */
fork_child(PROC_KEYMGR, &iev_keymgr, parent_dispatch_child);
- /*
- * Boot: a keymgr that cannot be initialized is not a daemon worth
- * starting, so this one caller keeps the old fatal() behavior.
- */
if (send_keymgr_init(iev_keymgr, conf) == -1)
fatalx("send_keymgr_init: could not initialize keymgr at boot");
- /*
- * keymgr gets no peer at boot (its first peer is wired later by
- * spawn_connection()), so this IMSG_SETUP_DONE with zero preceding
- * IMSG_SETUP_PEER is just the same boot-failure-detection handshake
- * every boot-time child gets.
- */
setup_done_send(iev_keymgr);
signal_set(&ev_sighup, SIGHUP, sighup_handler, NULL);
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 keymgr, which is already spawned above
- */
for (i = 0; i < n_cleartext; i++) {
event_set(&ev_accept_cleartext[i], cleartext_fds[i],
/* no setproctitle() here: rc.d matches the parent's command line */
- /*
- * proc/exec/sendfd stay for the process lifetime: spawn_connection()
- * forks a fresh pair, fd-passing the client_fd, for as long as the
- * daemon runs, not just at boot
- */
+ /* proc exec sendfd: every connection forks and fd-passes */
#ifdef __OpenBSD__
if (pledge("stdio rpath inet proc exec sendfd", NULL) == -1)
fatal("pledge");
fatalx("parent: exited event loop");
}
-/*
- * shared re-exec argv builder for fork_child()/parent_handle_store_fork()'s
- * child-side fork: progpath, "-x", role, then saved_argv with any pre-existing
- * "-x <role>" pair skipped
- */
static __dead void
exec_self_as(const char *role)
{
n = 0;
nargv[n++] = progpath;
nargv[n++] = "-x";
- /*
- * Casting away const on role for argv is the standard, unavoidable
- * idiom: execv(3) requires char *const argv[] for historical reasons
- * and never writes through the pointers.
- */
+ /* execv(3) takes char *const argv[] but never writes through it */
nargv[n++] = (char *)(uintptr_t)role;
for (i = 1; saved_argv[i] != NULL; i++) {
- /* skip any pre-existing -x <role>; the rest passes through */
if (strcmp(saved_argv[i], "-x") == 0) {
- /*
- * A trailing -x with no value would step the index past
- * argv's NULL terminator and read one element beyond
- * the array (in practice environ[0]); getopt(3) can't
- * catch this because it fires on an operand after "--",
- * which main() never checks via optind.
- */
+ /* a trailing -x would read past argv's NULL */
if (saved_argv[i + 1] == NULL)
break;
i++;
continue;
}
if (n >= (int)(sizeof(nargv) / sizeof(nargv[0])) - 1) {
- /*
- * Refuse rather than truncate an overlong argv:
- * silently dropping the tail could split an option from
- * its value, and the child's own getopt(3) would then
- * fail with a misleading usage() in the freshly forked
- * child.
- */
+ /* truncating could split an option from its value */
log_warnx("exec_self_as: argv too long to pass to the "
"%s child, refusing to exec a truncated command "
"line", role);
}
nargv[n] = NULL;
- /*
- * Use execv(3) not execvp(3): progpath is always absolute (realpath(3))
- * so PATH search was never needed, and execv() makes that property true
- * by construction rather than by relying on a caller's care (see
- * smtpd.c:856 for the same non-bug).
- */
execv(nargv[0], nargv);
_exit(1);
}
-/*
- * Re-exec mechanism (per smtpd.c's start_child()): socketpair/fork/dup2 onto fd
- * 3/closefrom/execv -x <role>; fatal()s on failure since keymgr, its only
- * caller, is boot-time-required, unlike fork_child_nonfatal() below for
- * per-connection forks.
- */
+/* as smtpd.c's start_child() */
static pid_t
fork_child(enum openimap_proc_type type, struct imsgev **ievp,
void (*handler)(int, short, void *))
return (pid);
}
-/*
- * fork_child()'s non-fatal twin; see fork_child()'s own comment. Returns -1
- * (nothing forked, *ievp untouched) on failure, logging via log_warn(x) rather
- * than fatal(ing) the daemon.
- */
static pid_t
fork_child_nonfatal(enum openimap_proc_type type, struct imsgev **ievp,
void (*handler)(int, short, void *))
_exit(1);
closefrom(4);
+ /* setrlimit(2) needs "proc" or "id" (sys/kern/kern_pledge.c) */
+ if (type == PROC_PARSER) {
+ struct rlimit rl;
+
+ rl.rlim_cur = PARSER_CPU_SOFT_SEC;
+ rl.rlim_max = PARSER_CPU_HARD_SEC;
+ if (setrlimit(RLIMIT_CPU, &rl) == -1)
+ _exit(1);
+ }
+
exec_self_as(log_procname(type));
}
}
c->pid = pid;
c->type = type;
- /* NULL, not "c": handlers expect &iev; imsgev_init() self-refs it */
imsgev_init(&c->iev, sp[0], handler, NULL);
TAILQ_INSERT_TAIL(&children, c, entry);
return (pid);
}
-/*
- * IMSG_SETUP_PEER / IMSG_SETUP_SEARCH_PEER (smtpd.c's setup_peers()): fresh
- * socketpair, fd-passed to "a"/"b". id is 0 at boot, or when the receiving side
- * has exactly one peer for its whole life (both search-oracle wirings and the
- * listener/auth pair), else session_id (keymgr's multi-peer case, store's own
- * case below). imsgname rides alongside imsg_type purely so a failure names the
- * wiring that failed, the same pairing send_mbox_request() uses in listener.c.
- */
+/* as smtpd.c's setup_peers(); id 0 for a child with one peer */
static int
setup_peer_send(struct imsgev *a, struct imsgev *b, int imsg_type,
const char *imsgname, uint32_t id)
{
int sp[2];
- /*
- * Returns -1 instead of fatal()ing since every caller is now
- * per-connection ("fail the session, not the daemon"); on the error
- * paths, an fd is closed here only if imsg_compose() did NOT already
- * take ownership of it.
- */
if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) {
log_warn("setup_peer_send %s: socketpair", imsgname);
return (-1);
if (imsg_compose(&a->ibuf, imsg_type, id, 0, sp[0], NULL, 0) == -1) {
log_warn("setup_peer_send %s: imsg_compose (a)", imsgname);
- close(sp[0]); /* neither end was taken */
+ close(sp[0]);
close(sp[1]);
return (-1);
}
if (imsg_compose(&b->ibuf, imsg_type, id, 0, sp[1], NULL, 0) == -1) {
log_warn("setup_peer_send %s: imsg_compose (b)", imsgname);
- close(sp[1]); /* sp[0] now belongs to a's msgbuf */
+ close(sp[1]);
return (-1);
}
return (0);
}
-/*
- * IMSG_SETUP_DONE: tell a child no more peers are coming, block for its ack
- * (smtpd.c's setup_done()); boot-time only -- see this file's header comment on
- * why spawn_connection() does not use this for per-connection peer wiring
- */
+/* as smtpd.c's setup_done(); boot only */
static void
setup_done_send(struct imsgev *iev)
{
imsg_free(&imsg);
}
-/*
- * Dispatches every non-boot child channel (keymgr plus each
- * spawn_connection()-forked listener/auth-worker) by imsg type alone, except
- * IMSG_AUTH_CRED below, which also verifies sender identity per session_id.
- */
static void
parent_dispatch_child(int fd, short event, void *arg)
{
struct imsg imsg;
ssize_t n;
- /*
- * EV_WRITE: imsg_compose() 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 */
event_del(&iev->ev);
return;
}
log_warnx("bad IMSG_AUTH_CRED");
break;
}
- /*
- * imsg_get_data() guarantees size but not NUL
- * termination, so an unterminated field here would be
- * an unbounded read past a stack array in the root
- * process, driven by the one child privsep exists to
- * contain -- so force it, like auth.c does for its own
- * inbound imsgs.
- */
+ /* imsg_get_data() does not NUL-terminate */
req.maildir[sizeof(req.maildir) - 1] = '\0';
- /*
- * Verify the claimed session_id against a session
- * parent independently knows is open, rather than
- * trusting whatever auth claims.
- */
+ /* check against sessions the parent knows */
if ((os = open_session_find(req.session_id)) == NULL) {
log_warnx("refusing IMSG_AUTH_CRED for session "
"%u: not a session parent knows is open "
req.session_id);
break;
}
- /*
- * Per-connection auth-workers replace the old single
- * global iev_auth sender check: each open_session
- * remembers which auth-worker channel
- * spawn_connection() paired it with, and only that
- * channel's IMSG_AUTH_CRED is honored.
- */
+ /* only this session's own auth-worker may grant it */
if (iev != os->auth_iev) {
log_warnx("refusing IMSG_AUTH_CRED for "
"session %u: not from that session's own "
log_warnx("refusing IMSG_AUTH_CRED for session "
"%u: already authenticated, refusing "
"duplicate grant", req.session_id);
- /*
- * Refuse the grant but still reply: that
- * session's listener-worker is waiting in
- * SESSION_STORE_PENDING with no timeout, a
- * state reachable via an ordinary retry after a
- * failed store spawn, not just misbehavior.
- */
- store_fork_failed(os);
+ /* reply: the listener waits with no timeout */
+ store_fork_failed(os, 0);
break;
}
os->authenticated = 1;
+ if (imsg_compose(&iev->ibuf, IMSG_AUTH_EXIT, 0, 0, -1,
+ NULL, 0) == -1)
+ log_warn("session %u: imsg_compose "
+ "IMSG_AUTH_EXIT", req.session_id);
+
parent_handle_store_fork(os, req.uid, req.gid,
req.maildir);
break;
(void)fd;
}
-/* The open_sessions lookup; see struct open_session's own comment. */
static struct open_session *
open_session_find(uint32_t session_id)
{
return (NULL);
}
-/*
- * Counts not-yet-authenticated open_sessions entries (imapd's analog of
- * sshd's concurrent-unauthenticated-connections), moved here from listener.c
- * now that parent tracks every session itself; O(n) is fine since n is bounded
- * by max_startups_full.
- */
static unsigned int
count_startups(void)
{
return (n);
}
-/*
- * sshd_config(5)'s MaxStartups algorithm verbatim (accept below begin,
- * ramp refusal probability linearly to full, always refuse at/above full, 0
- * disables it), moved from listener.c to read gconf->max_startups_* directly
- * since SIGHUP updates gconf in place.
- */
+/* sshd_config(5)'s MaxStartups */
static int
startups_should_drop(unsigned int nstartups)
{
return (1);
if (gconf->max_startups_rate >= 100)
return (1);
- /*
- * Misconfigured (full <= begin, which parse.y should already prevent):
- * treat as no ramp region and fail toward refusing rather than silently
- * accepting past the configured full.
- */
if (gconf->max_startups_full <= gconf->max_startups_begin)
return (1);
return (arc4random_uniform(100) < (uint32_t)p);
}
-/*
- * parent's own accept loop (moved from listener.c's listener_accept());
- * arg selects cleartext(0)/implicit-TLS(1) socket; MaxStartups is checked
- * before any fork so a refused connection costs almost nothing.
- */
+/* out of descriptors: pause accept(2), as httpd does */
static void
+accept_pause(void)
+{
+ struct timeval tv = { ACCEPT_PAUSE_SEC, 0 };
+ int i;
+
+ for (i = 0; i < n_cleartext; i++)
+ event_del(&ev_accept_cleartext[i]);
+ for (i = 0; i < n_tls; i++)
+ event_del(&ev_accept_tls[i]);
+ evtimer_set(&ev_accept_pause, accept_resume, NULL);
+ evtimer_add(&ev_accept_pause, &tv);
+}
+
+static void
+accept_resume(int fd, short event, void *arg)
+{
+ int i;
+
+ (void)fd; (void)event; (void)arg;
+
+ if (shutting_down)
+ return;
+ for (i = 0; i < n_cleartext; i++)
+ event_add(&ev_accept_cleartext[i], NULL);
+ for (i = 0; i < n_tls; i++)
+ event_add(&ev_accept_tls[i], NULL);
+}
+
+static unsigned int
+count_sessions(void)
+{
+ struct open_session *os;
+ unsigned int n = 0;
+
+ TAILQ_FOREACH(os, &open_sessions, entry)
+ n++;
+ return (n);
+}
+
+static int
+same_peer(const struct sockaddr_storage *a, const struct sockaddr_storage *b)
+{
+ if (a->ss_family != b->ss_family)
+ return (0);
+ if (a->ss_family == AF_INET)
+ return (memcmp(&((const struct sockaddr_in *)a)->sin_addr,
+ &((const struct sockaddr_in *)b)->sin_addr,
+ sizeof(struct in_addr)) == 0);
+ if (a->ss_family == AF_INET6)
+ return (memcmp(&((const struct sockaddr_in6 *)a)->sin6_addr,
+ &((const struct sockaddr_in6 *)b)->sin6_addr,
+ sizeof(struct in6_addr)) == 0);
+ return (0);
+}
+
+/* as sshd's srclimit_check_allow() */
+static unsigned int
+count_startups_from(const struct sockaddr_storage *ss)
+{
+ struct open_session *os;
+ unsigned int n = 0;
+
+ TAILQ_FOREACH(os, &open_sessions, entry) {
+ if (!os->authenticated && same_peer(&os->peer, ss))
+ n++;
+ }
+ return (n);
+}
+
+/* RFC 9051 SS7.1.5 BYE, on the cleartext port only */
+static void
+refuse_connection(int fd, int cleartext)
+{
+ static const char bye[] = "* BYE [UNAVAILABLE] too many "
+ "connections, try again later\r\n";
+
+ if (cleartext)
+ (void)write(fd, bye, sizeof(bye) - 1);
+ close(fd);
+}
+
+static void
parent_accept(int fd, short event, void *arg)
{
struct sockaddr_storage ss;
socklen_t sslen = sizeof(ss);
- int client_fd, flags, implicit_tls;
- unsigned int nstartups;
+ int client_fd, flags, implicit_tls, on = 1;
+ unsigned int nstartups, nopen, nsource;
(void)event;
if ((client_fd = accept(fd, (struct sockaddr *)&ss, &sslen)) == -1) {
- log_warn("accept");
+ /* else the listen event fires again at once, and spins */
+ if (errno == EMFILE || errno == ENFILE) {
+ log_warn("accept, pausing %ds", ACCEPT_PAUSE_SEC);
+ accept_pause();
+ } else
+ log_warn("accept");
return;
}
+ nopen = count_sessions();
+ if (nopen >= gconf->connections_max) {
+ if (!connections_full)
+ log_warnx("connections max reached (%u open), "
+ "refusing new connections", nopen);
+ connections_full = 1;
+ refuse_connection(client_fd, arg == (void *)0);
+ return;
+ }
+ if (connections_full) {
+ log_info("below connections max again (%u open), "
+ "accepting", nopen);
+ connections_full = 0;
+ }
+
+ if (gconf->max_startups_per_source != 0) {
+ nsource = count_startups_from(&ss);
+ if (nsource >= gconf->max_startups_per_source) {
+ log_debug("startups per-source: refusing connection "
+ "(%u unauthenticated already open from its "
+ "address)", nsource);
+ refuse_connection(client_fd, arg == (void *)0);
+ return;
+ }
+ }
+
nstartups = count_startups();
if (startups_should_drop(nstartups)) {
log_debug("MaxStartups: refusing connection (%u "
"unauthenticated already open)", nstartups);
- close(client_fd);
+ refuse_connection(client_fd, arg == (void *)0);
return;
}
- /*
- * accept(2) doesn't inherit O_NONBLOCK from the listening socket, and
- * the listener-worker this fd is handed to assumes non-blocking
- * throughout, so set it here before the fd-pass.
- */
+ /* accept(2) does not inherit O_NONBLOCK */
if ((flags = fcntl(client_fd, F_GETFL)) == -1 ||
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK) == -1) {
log_warn("fcntl O_NONBLOCK");
return;
}
- /* (void *)1 == port-993 listener */
+ /* as sshd's TCPKeepAlive: a session whose client vanished ends */
+ if (setsockopt(client_fd, SOL_SOCKET, SO_KEEPALIVE, &on,
+ sizeof(on)) == -1)
+ log_warn("setsockopt SO_KEEPALIVE");
+
implicit_tls = (arg != (void *)0);
spawn_connection(client_fd, implicit_tls, &ss, sslen);
}
-/*
- * The replicated-listener model: forks a paired listener-worker(+auth-worker)
- * for one accepted connection and wires them to each other and keymgr; never
- * fatal()s -- failures degrade only this connection/session, and client_fd is
- * always either consumed or closed before returning.
- */
static void
spawn_connection(int client_fd, int implicit_tls,
const struct sockaddr_storage *ss, socklen_t sslen)
struct open_session *os, *it;
struct imsgev *new_listener_iev;
struct imsgev *new_auth_iev;
- struct imsgev *new_search_iev;
struct imsg_listener_session_init init;
uint32_t session_id;
pid_t listener_pid, auth_pid;
- pid_t search_pid;
unsigned int nopen = 0;
session_id = next_session_id++;
- /*
- * session_id 0 is reserved: listener.c's boot-drain loop treats peer id
- * 0 as the auth peer and anything else as keymgr, so a session_id that
- * wrapped to 0 would misfile its keymgr descriptor as its auth peer and
- * hang.
- */
+ /* to listener.c, peer id 0 is the auth peer */
if (next_session_id == 0)
next_session_id = 1;
return;
}
os->session_id = session_id;
+ os->peer = *ss;
if ((listener_pid = fork_child_nonfatal(PROC_LISTENER,
&new_listener_iev, parent_dispatch_child)) == -1) {
}
os->listener_pid = listener_pid;
os->listener_iev = new_listener_iev;
- /*
- * Inserted as soon as the listener-worker exists, even though this
- * function can still fail below, so reap_child() always has a matching
- * open_session to find and clean up regardless of how incompletely
- * spawning finished.
- */
+ /* inserted early so reap_child() always finds it */
TAILQ_INSERT_TAIL(&open_sessions, os, entry);
if ((auth_pid = fork_child_nonfatal(PROC_AUTH, &new_auth_iev,
"connection itself (listener-worker detects a missing "
"auth peer the same way it detects one dying later)",
session_id);
- /* os->auth_iev/auth_pid stay 0/NULL; nothing to wire below. */
} else {
os->auth_pid = auth_pid;
os->auth_iev = new_auth_iev;
- /*
- * id 0: listener-worker and auth-worker have exactly one peer
- * each for their whole (short) life, no discriminator needed on
- * either side -- see setup_peer_send()'s own comment
- */
if (send_auth_init(new_auth_iev, gconf) == -1 ||
setup_peer_send(new_listener_iev, new_auth_iev,
IMSG_SETUP_PEER, "IMSG_SETUP_PEER", 0) == -1)
goto fail_close;
}
- /*
- * search-oracle is forked and wired the same fail-soft way as
- * auth-worker above; a missing oracle just degrades this session's
- * SEARCH to NO [UNAVAILABLE], and it needs no config push unlike
- * send_auth_init().
- */
- if ((search_pid = fork_child_nonfatal(PROC_SEARCH, &new_search_iev,
- parent_dispatch_child)) == -1) {
- log_warnx("session %u: no search-oracle available, this "
- "session's SEARCH will fail until a new connection gets "
- "one (listener-worker detects a missing search peer the "
- "same way it detects one dying later)", session_id);
- /* os->search_pid stays 0; nothing to wire below. */
- } else {
- os->search_pid = search_pid;
- /* session_id rides the imsg id field, unread on this type */
- /* not for IMSG_SETUP_PEER: listener.c reads id 0 as "auth" */
- if (setup_peer_send(new_listener_iev, new_search_iev,
- IMSG_SETUP_SEARCH_PEER, "IMSG_SETUP_SEARCH_PEER",
- session_id) == -1)
- goto fail_close;
- }
-
- /*
- * id session_id: keymgr is a long-lived, multi-peer process now
- * (keymgr.c), and needs it to know which peer entry this is
- */
if (setup_peer_send(new_listener_iev, iev_keymgr, IMSG_SETUP_PEER,
"IMSG_SETUP_PEER", session_id) == -1)
goto fail_close;
init.implicit_tls = implicit_tls;
init.remote_ss = *ss;
init.remote_sslen = sslen;
- /*
- * Read fresh from gconf per connection so a SIGHUP-changed "idle poll"
- * reaches every later connection -- see sighup_handler().
- */
init.idle_poll_secs = gconf->idle_poll_secs;
init.login_grace_secs = gconf->login_grace_secs;
init.append_max = gconf->append_max;
close(client_fd);
goto fail_workers;
}
- /*
- * From here on client_fd belongs to imsg (ownership taken by the
- * successful ibuf_fd_set(3) compose, closed by libutil after
- * sendmsg(2)), so the unwind below must not close it -- hence two
- * separate labels.
- */
+ /* client_fd now belongs to imsg; do not close it */
if (send_tls_cert(new_listener_iev, gconf) == -1)
goto fail_workers;
return;
fail_close:
close(client_fd); /* not yet handed to imsg; still ours */
fail_workers:
- /*
- * Fail this connection, not the daemon: kill the workers forked above
- * and leave the rest to reap_child(), which frees this open_session on
- * the listener-worker's exit -- so do NOT free(os) here.
- */
+ /* reap_child() frees os; do not free it here */
log_warnx("session %u: refusing connection: worker wiring failed",
session_id);
kill(os->listener_pid, SIGKILL);
if (os->auth_pid != 0)
kill(os->auth_pid, SIGKILL);
- if (os->search_pid != 0)
- kill(os->search_pid, SIGKILL);
}
/* rejects a maildir that could escape the spool: empty/absolute/".." */
return (1);
}
-/*
- * per-session store spawn triggered directly by auth's IMSG_AUTH_CRED; mirrors
- * fork_child_nonfatal() but with a timeout, not a bare failure return
- */
static void
parent_handle_store_fork(struct open_session *os, uid_t uid, gid_t gid,
const char *maildir)
struct timeval tv;
struct imsg_store_init init_payload;
struct store_child *it;
- unsigned int nchildren = 0;
uint32_t session_id = os->session_id;
- /*
- * os->listener_iev is who the fail: path reports to and who the new
- * child's fd is passed to; it can legitimately be NULL if the
- * listener-worker died between auth accepting credentials and this
- * call, since it's never restarted.
- */
if (os->listener_iev == NULL) {
log_warnx("refusing store spawn for session %u: "
"listener-worker is gone", session_id);
goto fail;
}
+ /* a second store for one session would orphan the first */
+ if (os->store != NULL) {
+ log_warnx("refusing store spawn: session %u already "
+ "has a store child", session_id);
+ goto fail;
+ }
+
TAILQ_FOREACH(it, &store_children, entry) {
- /*
- * A second spawn for a live session would overwrite
- * s->store_iev in listener.c's handler with a fresh calloc,
- * leaking the old struct/fd and orphaning a store child; only a
- * broken or compromised auth can trigger this.
- */
- if (it->session_id == session_id) {
- log_warnx("refusing store spawn: session %u already "
- "has a store child", session_id);
- goto fail;
+ if (!it->exiting && it->uid == uid && it->gid == gid &&
+ strcmp(it->maildir, maildir) == 0) {
+ if (it->nsessions >= gconf->account_sessions) {
+ log_info("session %u: refusing login: uid %u "
+ "already has %u sessions (account "
+ "sessions)", session_id, (unsigned int)uid,
+ it->nsessions);
+ store_fork_failed(os, 1);
+ return;
+ }
+ if (store_child_attach(it, os) == -1)
+ goto fail;
+ log_debug("session %u: joined the store child of "
+ "session %u", session_id, it->session_id);
+ return;
}
- nchildren++;
}
- if (nchildren >= STORE_CHILD_MAX) {
- log_warnx("refusing store spawn: %u store children active "
- "(session %u)", nchildren, session_id);
- goto fail;
- }
if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pair) == -1) {
log_warn("socketpair");
close(pair[1]);
- /* allocated once in place; sc->iev is never copied */
sc = calloc(1, sizeof(*sc));
if (sc == NULL) {
- /*
- * Fail this session, not the daemon: every other failure here
- * degrades to one NO reply, and a transient allocation failure
- * shouldn't be the exception that takes down the whole service.
- */
log_warn("calloc store child (session %u)", session_id);
kill(pid, SIGKILL);
close(pair[0]);
goto fail;
}
sc->session_id = session_id;
+ sc->uid = uid;
+ sc->gid = gid;
+ (void)strlcpy(sc->maildir, maildir, sizeof(sc->maildir));
sc->pid = pid;
sc->pending = 1;
- sc->listener_iev = os->listener_iev;
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
- */
- /*
- * memset first: without it, strlcpy(3)'s unused tail bytes and
- * inter-field padding would leak roughly a kilobyte of root's stack
- * contents to the store child, unlike every other imsg payload in this
- * file which is already zeroed.
- */
+ /* zeroed so no stack bytes of root reach the store child */
memset(&init_payload, 0, sizeof(init_payload));
init_payload.session_id = session_id;
init_payload.uid = uid;
goto fail_kill;
}
- /*
- * session_id rides as the imsg "id" field so listener.c can tell which
- * in-flight handshake this fd belongs to
- */
- if (setup_peer_send(&sc->iev, os->listener_iev, IMSG_SETUP_PEER,
- "IMSG_SETUP_PEER", session_id) == -1)
- goto fail_kill;
-
if (imsg_compose(&sc->iev.ibuf, IMSG_SETUP_DONE, 0, 0, -1, NULL, 0)
== -1) {
log_warn("imsg_compose IMSG_SETUP_DONE");
goto fail_kill;
}
+ if (store_child_attach(sc, os) == -1)
+ goto fail_kill;
+
evtimer_set(&sc->timeout_ev, store_child_timeout, sc);
tv.tv_sec = STORE_SETUP_TIMEOUT_SEC;
tv.tv_usec = 0;
fail_kill:
kill(pid, SIGKILL);
event_del(&sc->iev.ev);
- /* mirrors store_child_teardown(), else fd leaks */
close(sc->iev.ibuf.fd);
/* imsgbuf_init() allocs, close(2) leaks it */
imsgbuf_clear(&sc->iev.ibuf);
free(sc);
fail:
- store_fork_failed(os);
+ store_fork_failed(os, 0);
}
-/*
- * Tells a session's listener-worker no store child is coming, so it answers its
- * client instead of hanging forever in SESSION_STORE_PENDING; lifted out of
- * parent_handle_store_fork()'s fail: tail so the duplicate-IMSG_AUTH_CRED path
- * can reach it too.
- */
static void
-store_fork_failed(struct open_session *os)
+store_fork_failed(struct open_session *os, int limit)
{
struct imsg_store_fork fail_payload;
- /*
- * fail only this one session: IMSG_STORE_FORK is now solely this
- * failure-reply type, to the session's own listener-worker
- */
if (os->listener_iev == NULL)
return;
memset(&fail_payload, 0, sizeof(fail_payload));
fail_payload.session_id = os->session_id;
+ fail_payload.limit = limit;
if (imsg_compose(&os->listener_iev->ibuf, IMSG_STORE_FORK, 0, 0, -1,
&fail_payload, sizeof(fail_payload)) == -1)
log_warn("imsg_compose IMSG_STORE_FORK (failure reply)");
}
-/* the trailing imsgev_rearm_read() is not optional; see its definition */
+static int
+store_child_attach(struct store_child *sc, struct open_session *os)
+{
+ if (setup_peer_send(&sc->iev, os->listener_iev, IMSG_SETUP_PEER,
+ "IMSG_SETUP_PEER", os->session_id) == -1)
+ return (-1);
+ os->store = sc;
+ sc->nsessions++;
+ return (0);
+}
+
+/* the parent, not the store child, decides when it is done */
static void
+store_child_session_gone(struct store_child *sc)
+{
+ if (sc->nsessions > 0)
+ sc->nsessions--;
+ if (sc->nsessions > 0 || sc->exiting)
+ return;
+ sc->exiting = 1;
+ if (imsg_compose(&sc->iev.ibuf, IMSG_STORE_EXIT, 0, 0, -1, NULL,
+ 0) == -1) {
+ log_warn("session %u: imsg_compose IMSG_STORE_EXIT",
+ sc->session_id);
+ store_child_fail(sc);
+ }
+}
+
+/* the store child cannot fork: its pledge has no "proc exec" */
+static void
+store_child_parser(struct store_child *sc)
+{
+ struct imsg_parser_init payload;
+ struct imsgev *parser_iev;
+ pid_t pid;
+
+ if (sc->parser_pid != 0) {
+ kill(sc->parser_pid, SIGKILL);
+ sc->parser_pid = 0;
+ }
+ if ((pid = fork_child_nonfatal(PROC_PARSER, &parser_iev,
+ parent_dispatch_child)) == -1) {
+ log_warnx("session %u: could not spawn a parser-worker",
+ sc->session_id);
+ goto none;
+ }
+ memset(&payload, 0, sizeof(payload));
+ payload.session_id = sc->session_id;
+ payload.uid = sc->uid;
+ payload.gid = sc->gid;
+ payload.bodystructure_read_max = gconf->bodystructure_read_max;
+ if (imsg_compose(&parser_iev->ibuf, IMSG_PARSER_INIT, 0, 0, -1,
+ &payload, sizeof(payload)) == -1) {
+ log_warn("imsg_compose IMSG_PARSER_INIT");
+ goto kill;
+ }
+ if (imsgbuf_flush(&parser_iev->ibuf) == -1) {
+ log_warn("imsgbuf_flush IMSG_PARSER_INIT");
+ goto kill;
+ }
+ /* next_session_id skips 0, so id 0 is never a session */
+ if (setup_peer_send(&sc->iev, parser_iev, IMSG_SETUP_PEER,
+ "IMSG_SETUP_PEER", 0) == -1)
+ goto kill;
+ sc->parser_pid = pid;
+ return;
+
+kill:
+ kill(pid, SIGKILL);
+none:
+ if (imsg_compose(&sc->iev.ibuf, IMSG_PARSER_NONE, 0, 0, -1, NULL,
+ 0) == -1)
+ log_warn("session %u: imsg_compose IMSG_PARSER_NONE",
+ sc->session_id);
+}
+
+static void
store_child_dispatch(int fd, short event, void *arg)
{
struct store_child *sc = arg;
struct imsg imsg;
ssize_t n;
- /* EV_WRITE: as parent_dispatch_child(), imsg_compose() only queues */
if (event & EV_WRITE) {
if (imsgbuf_write(&sc->iev.ibuf) == -1) {
store_child_fail(sc);
imsg_free(&imsg);
continue;
}
+ if (imsg_get_type(&imsg) == IMSG_PARSER_WANT) {
+ imsg_free(&imsg);
+ store_child_parser(sc);
+ continue;
+ }
log_debug("store_child_dispatch: unhandled %d (session %u)",
imsg_get_type(&imsg), sc->session_id);
store_child_fail(sc);
}
-/*
- * shared teardown for a store_child, alive or already SIGCHLD-reaped; if
- * sc->pending, tells the store child's own session's listener-worker the fork
- * failed
- */
static void
store_child_teardown(struct store_child *sc, int already_dead)
{
- if (sc->pending && sc->listener_iev != NULL) {
- struct imsg_store_fork fail_payload;
+ struct open_session *os;
- memset(&fail_payload, 0, sizeof(fail_payload));
- fail_payload.session_id = sc->session_id;
- if (imsg_compose(&sc->listener_iev->ibuf, IMSG_STORE_FORK,
- 0, 0, -1, &fail_payload, sizeof(fail_payload)) == -1)
- log_warn("imsg_compose IMSG_STORE_FORK "
- "(failure reply)");
+ TAILQ_FOREACH(os, &open_sessions, entry) {
+ if (os->store != sc)
+ continue;
+ os->store = NULL;
+ if (sc->pending)
+ store_fork_failed(os, 0);
}
- /*
- * Unconditional: though pending is already cleared in
- * store_child_dispatch(), disarming here too prevents a future
- * clear-without-del from arming a timer on freed memory, regardless of
- * whether the listener-worker is still reachable.
- */
evtimer_del(&sc->timeout_ev);
if (!already_dead)
kill(sc->pid, SIGKILL);
store_child_teardown(sc, 0);
}
-/* binds, sets SO_REUSEADDR, listen(2)s; shared tail of bind_listen_socket() */
static int
bind_one(int family, const struct sockaddr *sa, socklen_t salen,
uint16_t port)
return (fd);
}
-/* resolves+binds "addr"/"port"; "*" or literal IPv4/IPv6 only, no DNS */
+/* "*" or a literal address; no DNS */
static int
bind_listen_socket(const char *addr, uint16_t port, int fds[LISTENER_MAX_ADDRS])
{
"IPv4/IPv6 address", addr);
}
-/*
- * Reads at most bufsize bytes of an already-open file into the CALLER's buffer;
- * feof(3) alone can't tell "exactly bufsize bytes" from "more to come", so on
- * an exactly-full read one extra fgetc(3) probes for a byte past the cap, and a
- * short read that isn't a clean EOF is refused for the same reason. Returns 0
- * with *n_out set, or -1 (already logged) for an oversized file or an unclean
- * short read -- and leaves *n_out 0 on -1, so a caller that composes anyway
- * sends nothing rather than a silent truncation.
- */
-/*
- * Takes an open FILE * rather than a path so the key file's ownership and
- * permission policy stays at its own call site, and never allocates, so the
- * bytes live only in the caller's buffer where a caller holding key material
- * can explicit_bzero() them.
- */
+/* an oversized file or a short read is refused, never truncated */
static int
read_file_capped(FILE *fp, char *buf, size_t bufsize, size_t *n_out,
const char *path, const char *what)
return (0);
}
-/*
- * Sends IMSG_TLS_CERT to both a fresh listener-worker (for its tls_config) and
- * keymgr (to compute the cert's pubkey hash); only the listener-worker case
- * reads a fresh file per spawn, so keymgr needs a separate reload push.
- */
static int
send_tls_cert(struct imsgev *iev, struct openimap_config *conf)
{
if ((fp = fopen(conf->tls_cert_file, "r")) == NULL) {
log_warn("fopen %s", conf->tls_cert_file);
} else {
- /*
- * A short buffer used to silently be sent as if it were the
- * whole file, failing later in the listener with a misleading
- * error; read_file_capped() carries that check, shared with
- * send_keymgr_init()'s key half.
- */
+ /* never send a truncated file */
if (read_file_capped(fp, buf, sizeof(buf), &n,
conf->tls_cert_file, "certificate") == -1)
n = 0; /* send an empty cert, never a truncated one */
fclose(fp);
}
- /* Non-fatal: spawn_connection() calls this per connection. */
if (imsg_compose(&iev->ibuf, IMSG_TLS_CERT, 0, 0, -1, buf, n) == -1) {
log_warn("imsg_compose IMSG_TLS_CERT");
return (-1);
return (0);
}
-/*
- * Sends keymgr its IMSG_TLS_CERT then IMSG_KEYMGR_INIT as two separate composes
- * (each capped at 8192 bytes) rather than one combined blob, to leave headroom
- * under imsg's 16384-byte MAX_IMSGSIZE; always sends both, even empty on
- * failure, so keymgr never hangs.
- */
+/* two composes, each under MAX_IMSGSIZE */
static int
send_keymgr_init(struct imsgev *iev, struct openimap_config *conf)
{
}
/* imsg_compose() copies buf at once; safe to scrub our stack copy */
- /*
- * Non-fatal here: parent_main() turns a boot-time -1 into a fatalx()
- * itself, but a failed sighup_handler() reload push must not kill an
- * otherwise-healthy daemon.
- */
if (imsg_compose(&iev->ibuf, IMSG_KEYMGR_INIT, 0, 0, -1, buf,
n) == -1) {
explicit_bzero(buf, sizeof(buf));
return (0);
}
-/*
- * IMSG_AUTH_INIT: auth's slice of config, just cred_file, to derive its chroot
- * dir and unveil() path; sent once per auth-worker spawn now
- * (spawn_connection()), not just once at boot -- cred_file doesn't vary per
- * connection, so the payload itself is unchanged
- */
static int
send_auth_init(struct imsgev *iev, struct openimap_config *conf)
{
struct imsg_auth_init init;
- /*
- * Non-fatal, since spawn_connection() is now the only caller (once per
- * connection); the truncation check below can't fire today since both
- * fields are 1024 bytes, but it stays as the wire-struct invariant it
- * documents.
- */
memset(&init, 0, sizeof(init));
if (strlcpy(init.cred_file, conf->cred_file, sizeof(init.cred_file))
>= sizeof(init.cred_file)) {
return (0);
}
-/*
- * SIGHUP: reloads imapd.conf; listen_addr/port/cred_file can't be swapped live
- * (sockets bound, auth chrooted per-connection but chroot dir is still derived
- * from cred_file) and warn instead
- */
+/* listen_addr, port and cred_file need a restart to change */
static void
sighup_handler(int fd, short event, void *arg)
{
"restart is required for this to take effect", conf_path);
}
- /*
- * checked before writing gconf: overwritten in place, no saved old
- * value
- */
if (strlen(newconf.spool_root) >= sizeof(gconf->spool_root) ||
strlen(newconf.tls_cert_file) >= sizeof(gconf->tls_cert_file) ||
strlen(newconf.tls_key_file) >= sizeof(gconf->tls_key_file)) {
return;
}
- /* 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;
gconf->idle_poll_secs = newconf.idle_poll_secs;
gconf->login_grace_secs = newconf.login_grace_secs;
gconf->lock_timeout_secs = newconf.lock_timeout_secs;
- /*
- * No corresponding push needed for max_startups_* or tls_cert_file:
- * startups_should_drop() and send_tls_cert() already read gconf fresh
- * on every accept/spawn, so they pick up a SIGHUP change on the very
- * next connection.
- */
+ gconf->account_sessions = newconf.account_sessions;
+ gconf->connections_max = newconf.connections_max;
+ /* startups and tls_cert_file are read afresh per connection */
gconf->max_startups_begin = newconf.max_startups_begin;
gconf->max_startups_rate = newconf.max_startups_rate;
gconf->max_startups_full = newconf.max_startups_full;
+ gconf->max_startups_per_source = newconf.max_startups_per_source;
(void)strlcpy(gconf->tls_cert_file, newconf.tls_cert_file,
sizeof(gconf->tls_cert_file));
(void)strlcpy(gconf->tls_key_file, newconf.tls_key_file,
sizeof(gconf->tls_key_file));
- /*
- * keymgr is still the one long-lived child that needs an explicit
- * reload push -- it holds the loaded private key for every existing and
- * future connection's private-key ops, unlike a listener-worker's
- * one-shot cert read above.
- */
+ /* keymgr holds the key for every connection, so it needs a push */
if (iev_keymgr != NULL) {
if (send_keymgr_init(iev_keymgr, gconf) == -1)
log_warnx("SIGHUP: pushing the reloaded certificate "
log_info("SIGHUP: reload complete");
}
-/* children still to reap before the parent may exit */
static int
shutdown_children_left(void)
{
exit(0);
}
-/*
- * Closes each listener worker's channel instead of signalling it: the
- * worker tears its own session down on EOF, which asks its store child
- * to exit and lets auth and search follow.
- */
+/* EOF, not a signal: the worker tears its own session down */
static void
sigterm_handler(int fd, short event, void *arg)
{
shutting_down = 1;
log_info("SIGTERM: shutting down");
- /* no new session may start while draining */
for (i = 0; i < n_cleartext; i++)
event_del(&ev_accept_cleartext[i]);
for (i = 0; i < n_tls; i++)
-1, NULL, 0) == -1 || imsgbuf_flush(&iev_keymgr->ibuf) == -1))
log_warn("shutdown: could not tell keymgr to exit");
- /* arm the timer only if there is anything to wait for */
if (shutdown_children_left() == 0) {
log_info("shutdown complete");
exit(0);
while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
reap_child(pid, status);
- /* the last child out ends the shutdown */
if (shutting_down && shutdown_children_left() == 0) {
evtimer_del(&ev_shutdown);
log_info("shutdown complete");
}
}
-/*
- * An unexpected child exit: keymgr (the sole boot-time child) is deliberately
- * not auto-restarted and degrades the whole daemon; a per-connection
- * listener/auth-worker or search-oracle exiting is the ordinary, expected way
- * one session's resources get reclaimed, logged at debug level.
- */
+/* keymgr is not restarted; a worker exiting is routine */
static void
reap_child(pid_t pid, int status)
{
TAILQ_REMOVE(&children, c, entry);
event_del(&c->iev.ev);
- /* sigterm_handler() may have closed this */
if (c->iev.ibuf.fd != -1)
close(c->iev.ibuf.fd);
imsgbuf_clear(&c->iev.ibuf);
return;
}
- /*
- * PROC_LISTENER, PROC_AUTH, or PROC_SEARCH: one of
- * spawn_connection()'s per-connection group,
- * search-oracle included. Find the open_session it
- * belonged to, if it's still tracked.
- */
+ if (c->type == PROC_PARSER) {
+ log_debug("parser[%d] exited (status %d)",
+ pid, status);
+ TAILQ_FOREACH(sc, &store_children, entry) {
+ if (sc->parser_pid == pid)
+ sc->parser_pid = 0;
+ }
+ free(c);
+ return;
+ }
+
TAILQ_FOREACH(os, &open_sessions, entry) {
if ((c->type == PROC_LISTENER &&
os->listener_pid == pid) ||
(c->type == PROC_AUTH &&
- os->auth_pid == pid) ||
- (c->type == PROC_SEARCH &&
- os->search_pid == pid))
+ os->auth_pid == pid))
break;
}
if (os == NULL) {
log_debug("session %u: listener-worker[%d] "
"exited (status %d), session closed",
os->session_id, pid, status);
- /*
- * The paired auth-worker and search-oracle, if
- * still alive, notice their own peer-channel
- * EOF and exit on their own -- parent isn't in
- * that data path (they're wired directly to
- * listener-worker).
- */
os->listener_iev = NULL;
- TAILQ_FOREACH(s, &store_children, entry) {
- if (s->session_id == os->session_id)
- s->listener_iev = NULL;
+ if ((s = os->store) != NULL) {
+ os->store = NULL;
+ store_child_session_gone(s);
}
TAILQ_REMOVE(&open_sessions, os, entry);
free(os);
- } else if (c->type == PROC_AUTH) {
+ } else {
log_debug("session %u: auth-worker[%d] "
"exited (status %d); this session's "
"listener-worker will detect this on "
pid, status);
os->auth_iev = NULL;
os->auth_pid = 0;
- } else {
- log_debug("session %u: search-oracle[%d] "
- "exited (status %d); this session's "
- "listener-worker will detect this on "
- "its own search channel", os->session_id,
- pid, status);
- os->search_pid = 0;
}
free(c);
blob - 9c564ba78b6053da3d754665b6ae523b9de5bda0
blob + cfad15bf92d2bb0d9cfdf97b9ce1d6cd053156ae
--- src/parse.y
+++ src/parse.y
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* parse.y, imapd.conf grammar. */
-
%{
#include <sys/types.h>
#include <sys/stat.h>
%token LISTEN ON TLS PORT
%token SPOOL CREDENTIALS CERTIFICATE KEY
%token APPEND ATTACHMENT MAX
-%token STARTUPS BEGIN RATE FULL
+%token STARTUPS BEGIN RATE FULL PERSOURCE NONE
%token IDLE POLL
%token LOGIN GRACE
%token LOCK TIMEOUT
+%token ACCOUNT SESSIONS
+%token CONNECTIONS
%token INCLUDE
%token ERROR
%token <v.string> STRING
include : INCLUDE STRING {
struct file *nfile;
- /*
- * secret=1, matching config_load()'s own pushfile():
- * an included file can set "tls key" and
- * "credentials", so it gets the same ownership and
- * permission check the main config gets. iked and
- * ldapd -- the two base parsers whose configs also
- * hold key material -- pass 1 here for the same
- * reason; httpd and smtpd check neither file.
- */
+ /* secret=1: an included file may hold "tls key" */
if ((nfile = pushfile($2, 1)) == NULL) {
yyerror("failed to include file %s", $2);
free($2);
free($3);
}
| IDLE POLL NUMBER {
- /*
- * 0 is legal and means "do not poll": that restores
- * the behaviour this directive replaced, where an
- * IDLEing session saw nothing until it sent DONE. It
- * is here so an operator who dislikes the polling can
- * turn it off without patching, not because it is a
- * sensible thing to want.
- */
+ /* 0 disables polling */
if ($3 < 0 || $3 > IDLE_POLL_MAX) {
yyerror("idle poll out of range "
"(0 to disable, otherwise 1-%d "
conf->idle_poll_secs = (uint32_t)$3;
}
| LOGIN GRACE NUMBER {
- /*
- * 0 is legal and disables the timer, matching
- * sshd_config(5)'s LoginGraceTime and "idle poll"
- * above. It reopens the denial of service the timer
- * exists to stop, so it is an escape hatch rather
- * than a tuning knob.
- */
+ /* 0 disables it, as sshd_config(5)'s LoginGraceTime */
if ($3 < 0 || $3 > LOGIN_GRACE_MAX) {
yyerror("login grace out of range "
"(0 to disable, otherwise 1-%d "
conf->login_grace_secs = (uint32_t)$3;
}
| LOCK TIMEOUT NUMBER {
- /*
- * 0 is legal and disables the bound, matching
- * "idle poll" and "login grace" above: a command
- * then waits for the index lock as long as the
- * holder takes, with nothing to interrupt it.
- */
+ /* 0 disables the bound */
if ($3 < 0 || $3 > LOCK_TIMEOUT_MAX) {
yyerror("lock timeout out of range "
"(0 to disable, otherwise 1-%d "
}
conf->lock_timeout_secs = (uint32_t)$3;
}
+ | ACCOUNT SESSIONS NUMBER {
+ if ($3 < 1 || $3 > ACCOUNT_SESSIONS_MAX) {
+ yyerror("account sessions out of range "
+ "(1-%d): %lld", ACCOUNT_SESSIONS_MAX,
+ (long long)$3);
+ YYERROR;
+ }
+ conf->account_sessions = (uint32_t)$3;
+ }
+ | CONNECTIONS MAX NUMBER {
+ if ($3 < 1 || $3 > CONNECTIONS_MAX_MAX) {
+ yyerror("connections max out of range "
+ "(1-%d): %lld", CONNECTIONS_MAX_MAX,
+ (long long)$3);
+ YYERROR;
+ }
+ conf->connections_max = (uint32_t)$3;
+ }
| APPEND MAX NUMBER {
if ($3 < 1 || $3 > APPEND_MAX_MAX) {
yyerror("append max out of range "
conf->append_max = (uint64_t)$3;
}
| ATTACHMENT MAX NUMBER {
- /*
- * Range-validated
- */
if ($3 < BODYSTRUCTURE_MAX || $3 > 1073741824) {
yyerror("attachment max out of range "
"(%d-1073741824 bytes): %lld",
}
conf->bodystructure_read_max = (uint32_t)$3;
}
+ | STARTUPS PERSOURCE NUMBER {
+ if ($3 < 1 || $3 > 1000000) {
+ yyerror("startups per-source out of range "
+ "(1-1000000, or none): %lld",
+ (long long)$3);
+ YYERROR;
+ }
+ conf->max_startups_per_source = (uint32_t)$3;
+ }
+ | STARTUPS PERSOURCE NONE {
+ conf->max_startups_per_source = 0;
+ }
| STARTUPS BEGIN NUMBER RATE NUMBER FULL NUMBER {
- /*
- * The admission-control throttle, modeled on
- * sshd_config(5)'s MaxStartups: below "begin" open
- * connections, every new one is accepted; between
- * "begin" and "full", new ones are refused with
- * linearly increasing probability starting at
- * "rate" percent; at or above "full", every new one
- * is refused. See parent.c's startups_should_drop()
- * (moved there with the accept loop; see parent.c's
- * header comment).
- */
+ /* as sshd_config(5)'s MaxStartups */
if ($3 < 0 || $3 > 1000000) {
yyerror("startups begin out of range "
"(0-1000000): %lld", (long long)$3);
{
/* this has to be sorted always */
static const struct keywords keywords[] = {
+ {"account", ACCOUNT},
{"append", APPEND},
{"attachment", ATTACHMENT},
{"begin", BEGIN},
{"certificate", CERTIFICATE},
+ {"connections", CONNECTIONS},
{"credentials", CREDENTIALS},
{"full", FULL},
{"grace", GRACE},
{"lock", LOCK},
{"login", LOGIN},
{"max", MAX},
+ {"none", NONE},
{"on", ON},
+ {"per-source", PERSOURCE},
{"poll", POLL},
{"port", PORT},
{"rate", RATE},
+ {"sessions", SESSIONS},
{"spool", SPOOL},
{"startups", STARTUPS},
{"timeout", TIMEOUT},
return (NUMBER);
} else {
nodigits:
- /*
- * This un-reads the whole token so the string scanner
- * below can re-lex it, but lungetc() drops characters
- * silently once pushback_buffer fills (MAXPUSHBACK is
- * 128, buf is sizeof(buf)) -- and pushback is LIFO, so
- * the loss excises the MIDDLE of the token and the
- * lexer proceeds on input the file does not contain.
- * Refuse it instead, with the same error the two
- * length checks above use. (Newer revisions of the
- * shared OpenBSD parse.y skeleton grow a per-file
- * ungetbuf instead of dropping; this file predates
- * that.)
- *
- * The loop below pushes (p - buf) - 1 characters and
- * lungetc() accepts MAXPUSHBACK - 1 of them, so the
- * exact bound is "p - buf > MAXPUSHBACK"; this is
- * deliberately one stricter, which costs nothing and
- * removes an off-by-one to get wrong.
- */
+ /* lungetc() silently drops past MAXPUSHBACK */
if ((size_t)(p - buf) >= MAXPUSHBACK) {
yyerror("string too long");
return (findeol());
x != '!' && x != '=' && x != '#' && \
x != ','))
- /*
- * '*' added alongside the pre-existing ':'
- */
if (isalnum(c) || c == ':' || c == '_' || c == '*') {
do {
*p++ = c;
return (c);
}
-/*
- * Same root-owned-or-current-user, not-group-or-world-writable check this
- * project already applies to the TLS private key file
- */
+/* owned by root or this user, not group- or world-writable */
static int
check_file_secrecy(int fd, const char *fname)
{
return (file ? 0 : EOF);
}
-/*
- * config_load(): imapd.h's public entry point.
- */
int
config_load(const char *path, struct openimap_config *xconf)
{
conf->idle_poll_secs = IDLE_POLL_DEFAULT;
conf->login_grace_secs = LOGIN_GRACE_DEFAULT;
conf->lock_timeout_secs = LOCK_TIMEOUT_DEFAULT;
+ conf->account_sessions = ACCOUNT_SESSIONS_DEFAULT;
+ conf->connections_max = CONNECTIONS_MAX_DEFAULT;
(void)strlcpy(conf->spool_root, "/var/mail/imapd",
sizeof(conf->spool_root));
(void)strlcpy(conf->cred_file, "/etc/imapd/credentials",
conf->max_startups_begin = 10;
conf->max_startups_rate = 30;
conf->max_startups_full = 100;
+ conf->max_startups_per_source = STARTUPS_PER_SOURCE_DEFAULT;
have_cleartext = 0;
have_tls_listen = 0;
errors = file->errors;
popfile();
- /*
- * Carry the listener selection into the config. Both ports were
- * seeded with their defaults above; clear the one this config did not
- * ask for, so parent.c does not bind it.
- *
- * The guard matters: a config with NO "listen" line at all keeps both
- * defaults, which is what such a config has always produced. Only a
- * config that names a listener is taken to be selecting listeners --
- * which is what imapd.conf's syntax reads as, and previously was not.
- */
if (have_cleartext || have_tls_listen) {
if (!have_cleartext)
conf->port_cleartext = 0;
conf->port_implicit_tls = 0;
}
- /*
- * Checked here, not in either rule, since the two directives may
- * come in either order. BODYSTRUCTURE and BODY[<part>] read a
- * message whole, up to "attachment max", so an upload larger than
- * that could be stored but never described.
- */
+ /* here, since the two directives may come in either order */
if (conf->append_max > conf->bodystructure_read_max) {
log_warnx("%s: append max %llu exceeds attachment max %u",
path, (unsigned long long)conf->append_max,
return (0);
}
-/*
- * cmdline_symset(): "-D name=value" command-line macro definitions.
- */
int
cmdline_symset(char *s)
{
blob - /dev/null
blob + 89cadd6d7d8b8435bffef0913ac0d978bd4afb4e (mode 644)
--- /dev/null
+++ src/parser.c
+/* $OpenIMAPD$ */
+
+/*
+ * Copyright (c) 2026 David Williams <dhw@openimapd.dev>
+ *
+ * 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.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+
+#include <event.h>
+#include <grp.h>
+#include <imsg.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "imapd.h"
+#include "log.h"
+#include "store_internal.h"
+
+static struct imsgev iev_store;
+
+static void parser_dispatch_store(int, short, void *);
+static int parser_spent(void);
+static void parser_handle_request(struct imsgev *, struct imsg *,
+ uint32_t);
+static int parser_search(struct imsg *, const struct imsg_parser_req *,
+ int, char **, uint32_t *);
+
+__dead void
+parser_main(void)
+{
+ struct imsgbuf ibuf3;
+ struct imsg imsg;
+ struct imsg_parser_init init;
+ ssize_t n;
+ gid_t gid;
+ int store_fd = -1;
+ int got_init = 0;
+
+ memset(&init, 0, sizeof(init));
+
+ imsgev_ibuf_init(&ibuf3, 3);
+
+ while (!got_init || store_fd == -1) {
+ if ((n = imsgbuf_get(&ibuf3, &imsg)) == -1)
+ fatal("imsgbuf_get");
+ if (n == 0) {
+ if ((n = imsgbuf_read(&ibuf3)) == -1)
+ fatal("imsgbuf_read");
+ if (n == 0)
+ fatalx("parent closed channel before boot "
+ "finished");
+ continue;
+ }
+
+ switch (imsg_get_type(&imsg)) {
+ case IMSG_PARSER_INIT:
+ if (imsg_get_data(&imsg, &init, sizeof(init)) == -1)
+ fatalx("bad IMSG_PARSER_INIT payload");
+ session_id = init.session_id;
+ bodystructure_read_max = init.bodystructure_read_max;
+ got_init = 1;
+ break;
+ case IMSG_SETUP_PEER:
+ if (store_fd != -1) {
+ log_warnx("duplicate IMSG_SETUP_PEER, "
+ "ignoring");
+ break;
+ }
+ if ((store_fd = imsg_get_fd(&imsg)) == -1)
+ fatalx("IMSG_SETUP_PEER carried no fd");
+ break;
+ default:
+ log_debug("parser boot: unhandled %d",
+ imsg_get_type(&imsg));
+ break;
+ }
+ imsg_free(&imsg);
+ }
+
+ /* same refusal the parent applies before spawning a store child */
+ if (init.uid == 0 || init.gid == 0)
+ fatalx("session %u: refusing to run as uid %u gid %u",
+ session_id, (unsigned int)init.uid,
+ (unsigned int)init.gid);
+
+ /* the chroot is a second line behind the pledge */
+ if (chroot("/var/empty") == -1)
+ fatal("chroot /var/empty");
+ if (chdir("/") == -1)
+ fatal("chdir /");
+
+ /* the account's own uid, so a parse bug is confined to that account */
+ gid = init.gid;
+ if (setgroups(1, &gid) == -1 ||
+ setresgid(init.gid, init.gid, init.gid) == -1 ||
+ setresuid(init.uid, init.uid, init.uid) == -1)
+ fatal("session %u: cannot drop privileges to uid %u gid %u",
+ session_id, (unsigned int)init.uid,
+ (unsigned int)init.gid);
+
+ setproctitle("parser uid %u", (unsigned int)init.uid);
+
+ event_init();
+ imsgev_init(&iev_store, store_fd, parser_dispatch_store, NULL);
+
+ /* no "rpath": the only bytes arrive on passed descriptors */
+#ifdef __OpenBSD__
+ if (pledge("stdio recvfd", NULL) == -1)
+ fatal("pledge");
+#endif
+
+ event_dispatch();
+ fatalx("parser: exited event loop");
+}
+
+static void
+parser_handle_request(struct imsgev *iev, struct imsg *imsg, uint32_t type)
+{
+ struct imsg_parser_req req;
+ struct imsg_parser_rep rep;
+ char reply[sizeof(rep) + BODYSTRUCTURE_MAX];
+ char *buf = NULL;
+ uint32_t len = 0, cap = 0;
+ size_t replylen = sizeof(rep);
+ int mfd, built = 0;
+
+ mfd = imsg_get_fd(imsg);
+ memset(&rep, 0, sizeof(rep));
+
+ if (imsg_get_buf(imsg, &req, sizeof(req)) == -1)
+ log_warnx("session %u: bad parser request %u", session_id,
+ type);
+ else if (mfd == -1)
+ log_warnx("session %u: parser request %u carried no "
+ "descriptor", session_id, type);
+ else {
+ /* the store composed it, but a copy must still terminate */
+ req.basename[sizeof(req.basename) - 1] = '\0';
+ req.fields[sizeof(req.fields) - 1] = '\0';
+ switch (type) {
+ case IMSG_PARSER_ENVELOPE:
+ cap = ENVELOPE_MAX;
+ built = build_envelope(mfd, req.basename, &buf,
+ &len) == 0;
+ break;
+ case IMSG_PARSER_BODYSTRUCTURE:
+ cap = BODYSTRUCTURE_MAX;
+ built = build_bodystructure(mfd, req.basename, &buf,
+ &len) == 0;
+ break;
+ case IMSG_PARSER_HEADER_FIELDS:
+ cap = FETCH_HEADER_MAX;
+ built = read_message_header_fields(mfd, req.basename,
+ req.fields, req.fields_not, &buf, &len) == 0;
+ break;
+ case IMSG_PARSER_SEARCH:
+ cap = SEARCH_PROGRAM_MAX_NODES;
+ built = parser_search(imsg, &req, mfd, &buf,
+ &len) == 0;
+ break;
+ case IMSG_PARSER_PART:
+ /* an extent, not the octets: a body never crosses */
+ if (req.pathlen >= 1 && req.pathlen <= MIME_MAX_DEPTH &&
+ extract_mime_part(mfd, req.basename, req.path,
+ req.pathlen, &rep.part_off, &rep.part_len) == 0)
+ rep.found = 1;
+ break;
+ }
+ if (built && len > 0 && len <= cap) {
+ rep.found = 1;
+ rep.len = len;
+ }
+ }
+ if (mfd != -1)
+ close(mfd);
+
+ rep.retiring = parser_spent();
+ memcpy(reply, &rep, sizeof(rep));
+ /* a found PART carries no bytes, and buf is NULL then */
+ if (rep.found && rep.len > 0) {
+ memcpy(reply + sizeof(rep), buf, rep.len);
+ replylen += rep.len;
+ }
+ free(buf);
+
+ if (imsg_compose(&iev->ibuf, type, imsg_get_id(imsg), 0, -1, reply,
+ replylen) == -1)
+ fatal("imsg_compose parser reply");
+
+ if (rep.retiring) {
+ if (imsgbuf_flush(&iev->ibuf) == -1)
+ fatal("imsgbuf_flush parser reply");
+ log_debug("session %u: parser retiring after %d seconds of "
+ "CPU", session_id, PARSER_CPU_RETIRE_SEC);
+ exit(0);
+ }
+}
+
+static int
+parser_search(struct imsg *imsg, const struct imsg_parser_req *req, int mfd,
+ char **buf, uint32_t *len)
+{
+ struct imsg_parser_leaf leaves[SEARCH_PROGRAM_MAX_NODES];
+ char pool[SEARCH_OPERANDS_MAX];
+ const struct imsg_parser_leaf *l;
+ uint32_t i;
+
+ if (req->nleaves == 0 || req->nleaves > SEARCH_PROGRAM_MAX_NODES ||
+ req->poollen > sizeof(pool) || imsg_get_len(imsg) !=
+ req->nleaves * sizeof(leaves[0]) + req->poollen ||
+ imsg_get_buf(imsg, leaves, req->nleaves * sizeof(leaves[0])) ==
+ -1 || imsg_get_buf(imsg, pool, req->poollen) == -1) {
+ log_warnx("session %u: bad SEARCH request", session_id);
+ return (-1);
+ }
+ for (i = 0; i < req->nleaves; i++) {
+ l = &leaves[i];
+ if (!search_op_content(l->op) || l->str_off > req->poollen ||
+ l->str_len > req->poollen - l->str_off ||
+ l->name_off > req->poollen ||
+ l->name_len > req->poollen - l->name_off) {
+ log_warnx("session %u: bad SEARCH key %u", session_id,
+ i);
+ return (-1);
+ }
+ }
+ return (search_match(mfd, req->basename, leaves, req->nleaves, pool,
+ buf, len));
+}
+
+/* getrusage(2) is allowed under "stdio" */
+static int
+parser_spent(void)
+{
+ struct rusage ru;
+ struct timeval tv;
+
+ if (getrusage(RUSAGE_SELF, &ru) == -1)
+ return (1); /* cannot tell: go, a fresh one is cheap */
+ timeradd(&ru.ru_utime, &ru.ru_stime, &tv);
+ return (tv.tv_sec >= PARSER_CPU_RETIRE_SEC);
+}
+
+static void
+parser_dispatch_store(int fd, short event, void *arg)
+{
+ struct imsgev *iev = arg;
+ struct imsg imsg;
+ ssize_t n;
+
+ (void)fd;
+
+ if (event & EV_WRITE) {
+ if (imsgbuf_write(&iev->ibuf) == -1)
+ fatal("imsgbuf_write");
+ }
+ if (event & EV_READ) {
+ if ((n = imsgbuf_read(&iev->ibuf)) == -1)
+ fatal("imsgbuf_read");
+ if (n == 0) {
+ log_debug("session %u: store closed channel, exiting",
+ session_id);
+ exit(0);
+ }
+ }
+
+ for (;;) {
+ if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1)
+ fatal("imsgbuf_get");
+ if (n == 0)
+ break;
+
+ switch (imsg_get_type(&imsg)) {
+ case IMSG_PARSER_ENVELOPE:
+ case IMSG_PARSER_BODYSTRUCTURE:
+ case IMSG_PARSER_HEADER_FIELDS:
+ case IMSG_PARSER_PART:
+ case IMSG_PARSER_SEARCH:
+ parser_handle_request(iev, &imsg,
+ imsg_get_type(&imsg));
+ break;
+ default:
+ log_debug("session %u: unhandled request %d",
+ session_id, imsg_get_type(&imsg));
+ break;
+ }
+ imsg_free(&imsg);
+ }
+
+ imsgev_rearm_read(iev);
+}
blob - 8bed78212415bc8cc45642b8d4ddc3428619f9f9
blob + 1ae52cd9d5eeda56251857220f67e5fde5d6bb59
--- src/search_cmd.c
+++ src/search_cmd.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* search_cmd.c: SEARCH key parsing, dispatch, async completion path */
#include <sys/types.h>
#include <sys/queue.h>
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 */
if ((t = timegm(&tm)) == (time_t)-1)
return (-1);
return (0);
}
-/* accumulator for parse_search_key()'s postfix program (wire: imapd.h) */
#define SEARCH_MAX_DEPTH 64 /* max parse_search_key() recursion */
struct search_parse_ctx {
struct search_node nodes[SEARCH_PROGRAM_MAX_NODES];
uint32_t n;
- int depth; /* current recursion depth */
- /* set when a MODSEQ key is pushed; see cmd_search() */
+ int depth;
int uses_modseq;
+ /* octets of every string operand, counted past the cap too */
+ size_t operand_len;
+ int uses_content;
+ char pool[SEARCH_OPERANDS_MAX];
};
return (0);
}
-/*
- * Reads one sequence-set token (RFC 9051 SS9's full comma-separated
- * grammar) and pushes it as OR'd SEARCH_OP_SEQSET/SEARCH_OP_UIDSET
- * leaf nodes; copied out first (not NUL-terminated in place) since
- * a trailing ')' must remain visible to the caller, into a buffer
- * sized off SESSION_INBUF_MAX so it can never truncate.
- */
+/* RFC 9051 SS9 sequence-set, pushed as ORed ranges */
static int
parse_search_seqset(char **pp, struct search_parse_ctx *ctx, int op,
const char **errmsg)
return (0);
}
-/* RFC 9051 SS6.4.4 search-key grammar, recursive-descent: ok=0 BAD=-1 NO=-2 */
+/* RFC 9051 SS6.4.4 search-key: 0 ok, -1 BAD, -2 NO */
int
parse_search_key(char **pp, struct search_parse_ctx *ctx, const char **errmsg)
{
int rc;
- /*
- * every nesting level (parens, NOT, OR) re-enters here; bounds deep
- * nesting
- */
+ /* bounds nesting */
if (++ctx->depth > SEARCH_MAX_DEPTH) {
ctx->depth--;
*errmsg = "search criteria nested too deeply";
}
-/*
- * Skips leading spaces, then reads one space/')'-terminated token
- * from *pp into buf (bounded, NUL-terminated); shared "empty or
- * too long" bounds check for KEYWORD/UNKEYWORD, BEFORE/ON/SINCE's
- * unquoted date, LARGER/SMALLER, and MODSEQ's value below. Returns
- * 0 on success, -1 (with *errmsg set to errtext) if the token is
- * empty or doesn't fit in buf.
- */
static int
read_search_token(char **pp, char *buf, size_t bufsize, const char *errtext,
const char **errmsg)
return (0);
}
+/* RFC 9051 SS9 date, as the UTC midnight that begins it */
+static int
+read_search_date(char **pp, int64_t *out, const char **errmsg)
+{
+ char *p = *pp;
+ char datebuf[32];
+
+ while (*p == ' ')
+ p++;
+ if (*p == '"') {
+ const char *start = p + 1;
+ char *end = strchr(start, '"');
+ size_t len;
+
+ if (end == NULL) {
+ *errmsg = "unterminated date string";
+ return (-1);
+ }
+ len = (size_t)(end - start);
+ if (len >= sizeof(datebuf)) {
+ *errmsg = "malformed date";
+ return (-1);
+ }
+ memcpy(datebuf, start, len);
+ datebuf[len] = '\0';
+ p = end + 1;
+ } else {
+ if (read_search_token(&p, datebuf, sizeof(datebuf),
+ "malformed date", errmsg) == -1)
+ return (-1);
+ }
+
+ if (parse_search_date(datebuf, out) == -1) {
+ *errmsg = "malformed date";
+ return (-1);
+ }
+ *pp = p;
+ return (0);
+}
+
+static void
+search_pool_add(struct search_parse_ctx *ctx, char c)
+{
+ if (ctx->operand_len < sizeof(ctx->pool))
+ ctx->pool[ctx->operand_len] = c;
+ ctx->operand_len++;
+}
+
+/* RFC 9051 SS9 astring operand */
+static int
+read_search_astring(char **pp, struct search_parse_ctx *ctx,
+ uint32_t *offp, uint32_t *lenp, const char **errmsg)
+{
+ char *p = *pp;
+ size_t start = ctx->operand_len;
+
+ while (*p == ' ')
+ p++;
+ if (*p == '{') {
+ *errmsg = "literals are not supported in SEARCH, send a "
+ "quoted string";
+ return (-1);
+ }
+ if (*p == '"') {
+ for (p++; *p != '"'; p++) {
+ if (*p == '\0') {
+ *errmsg = "unterminated quoted string";
+ return (-1);
+ }
+ /* RFC 9051 SS9: quoted-specials are the only escapes */
+ if (*p == '\\' && *++p != '"' && *p != '\\') {
+ *errmsg = "malformed quoted string";
+ return (-1);
+ }
+ search_pool_add(ctx, *p);
+ }
+ p++;
+ } else {
+ /* RFC 9051 SS9 ASTRING-CHAR */
+ for (; *p != '\0' && *p != ' ' && *p != ')'; p++) {
+ if ((unsigned char)*p <= 0x1f ||
+ (unsigned char)*p >= 0x7f ||
+ strchr("({%*\"\\", *p) != NULL) {
+ *errmsg = "malformed search string";
+ return (-1);
+ }
+ search_pool_add(ctx, *p);
+ }
+ if (ctx->operand_len == start) {
+ *errmsg = "missing search string";
+ return (-1);
+ }
+ }
+ /* the offsets mean something only once the total is under the cap */
+ *offp = (uint32_t)start;
+ *lenp = (uint32_t)(ctx->operand_len - start);
+ *pp = p;
+ return (0);
+}
+
int
parse_search_key_inner(char **pp, struct search_parse_ctx *ctx,
const char **errmsg)
if (strcasecmp(word, "BEFORE") == 0 || strcasecmp(word, "ON") == 0 ||
strcasecmp(word, "SINCE") == 0) {
struct search_node node;
- char datebuf[32];
memset(&node, 0, sizeof(node));
if (strcasecmp(word, "BEFORE") == 0)
else
node.op = SEARCH_OP_SINCE;
- while (*p == ' ')
- p++;
- if (*p == '"') {
- const char *start = p + 1;
- char *end = strchr(start, '"');
- size_t len;
-
- if (end == NULL) {
- *errmsg = "unterminated date string";
- return (-1);
- }
- len = (size_t)(end - start);
- if (len >= sizeof(datebuf)) {
- *errmsg = "malformed date";
- return (-1);
- }
- memcpy(datebuf, start, len);
- datebuf[len] = '\0';
- p = end + 1;
- } else {
- if (read_search_token(&p, datebuf, sizeof(datebuf),
- "malformed date", errmsg) == -1)
- return (-1);
- }
-
- if (parse_search_date(datebuf, &node.num) == -1) {
- *errmsg = "malformed date";
+ if (read_search_date(&p, &node.num, errmsg) == -1)
return (-1);
- }
if (search_push(ctx, &node, errmsg) == -1)
return (-1);
*errmsg = "malformed octet count";
return (-1);
}
- /*
- * node.num is int64_t compared signed by mbox_search.c, so an
- * unchecked cast turns "LARGER 18446744073709551615" into
- * "size > -1" (matches everything) -- reachable since numbuf
- * holds 23 digits and ULLONG_MAX is 20.
- */
+ /* node.num is signed: refuse above INT64_MAX */
if (v > (unsigned long long)INT64_MAX) {
*errmsg = "octet count out of range";
return (-1);
while (*p == ' ')
p++;
- /*
- * RFC 7162 SS3.1.5: optional entry-name/type-req
- * (unimplemented) skipped
- */
+ /* RFC 7162 SS3.1.5 entry-name and type-req are skipped */
if (*p != '\0' && !isdigit((unsigned char)*p)) {
if (*p == '"') {
char *end = strchr(p + 1, '"');
return (0);
}
+ /* RFC 9051 SS6.4.4 content keys, answered from the message */
+ if (strcasecmp(word, "SENTBEFORE") == 0 ||
+ strcasecmp(word, "SENTON") == 0 ||
+ strcasecmp(word, "SENTSINCE") == 0) {
+ struct search_node node;
+
+ memset(&node, 0, sizeof(node));
+ if (strcasecmp(word, "SENTBEFORE") == 0)
+ node.op = SEARCH_OP_SENTBEFORE;
+ else if (strcasecmp(word, "SENTON") == 0)
+ node.op = SEARCH_OP_SENTON;
+ else
+ node.op = SEARCH_OP_SENTSINCE;
+
+ if (read_search_date(&p, &node.num, errmsg) == -1)
+ return (-1);
+
+ if (search_push(ctx, &node, errmsg) == -1)
+ return (-1);
+ *pp = p;
+ return (0);
+ }
+
{
- static const char *content_keys[] = {
- "BCC", "BODY", "CC", "FROM", "HEADER", "SENTBEFORE",
- "SENTON", "SENTSINCE", "SUBJECT", "TEXT", "TO",
+ static const struct {
+ const char *name;
+ int op;
+ } string_keys[] = {
+ { "BCC", SEARCH_OP_BCC },
+ { "CC", SEARCH_OP_CC },
+ { "FROM", SEARCH_OP_FROM },
+ { "HEADER", SEARCH_OP_HEADER },
+ { "SUBJECT", SEARCH_OP_SUBJECT },
+ { "TO", SEARCH_OP_TO },
};
- size_t i;
+ struct search_node node;
+ size_t i;
- for (i = 0; i < sizeof(content_keys) / sizeof(content_keys[0]);
+ for (i = 0; i < sizeof(string_keys) / sizeof(string_keys[0]);
i++) {
- if (strcasecmp(word, content_keys[i]) == 0) {
- *errmsg = "search keys that require message "
- "content/header access are not "
- "supported in this pass";
- return (-2);
- }
+ if (strcasecmp(word, string_keys[i].name) != 0)
+ continue;
+ memset(&node, 0, sizeof(node));
+ node.op = string_keys[i].op;
+ /* HEADER's field-name is an astring too */
+ if (node.op == SEARCH_OP_HEADER &&
+ read_search_astring(&p, ctx, &node.name_off,
+ &node.name_len, errmsg) == -1)
+ return (-1);
+ if (read_search_astring(&p, ctx, &node.str_off,
+ &node.str_len, errmsg) == -1)
+ return (-1);
+
+ if (search_push(ctx, &node, errmsg) == -1)
+ return (-1);
+ *pp = p;
+ return (0);
}
}
+ /* BODY and TEXT: parsed, but not yet answered */
+ if (strcasecmp(word, "BODY") == 0 || strcasecmp(word, "TEXT") == 0) {
+ uint32_t off, len;
+
+ if (read_search_astring(&p, ctx, &off, &len, errmsg) == -1)
+ return (-1);
+ ctx->uses_content = 1;
+ *pp = p;
+ return (0);
+ }
+
*errmsg = "unknown search key";
return (-1);
}
-/* search-key *(SP search-key), ANDed left-right; in_parens stops at ')' */
+/* RFC 9051 SS6.4.4 search-key list, ANDed */
int
parse_search_key_list(char **pp, struct search_parse_ctx *ctx,
const char **errmsg, int in_parens)
}
}
-/*
- * search_oracle.c's one entry point into this file's private
- * struct search_parse_ctx: parses already-stripped SEARCH argument
- * text into nodes_out (a caller-supplied SEARCH_PROGRAM_MAX_NODES
- * buffer), returning parse_search_key_list()'s rc (0/-1/-2) with
- * errmsg copied out on failure; the empty-criteria check moved
- * here too as grammar validation.
- */
-int
-search_oracle_parse(char *args, struct search_node *nodes_out,
- uint32_t *nnodes_out, int *uses_modseq_out, char *errmsg_out,
- size_t errmsg_outsize)
+static int
+search_program_parse(char *args, struct search_node *nodes_out,
+ uint32_t *nnodes_out, int *uses_modseq_out, char *pool_out,
+ uint32_t *poollen_out, char *errmsg_out, size_t errmsg_outsize)
{
struct search_parse_ctx ctx;
char *p = args;
memset(&ctx, 0, sizeof(ctx));
rc = parse_search_key_list(&p, &ctx, &errmsg, 0);
- if (rc == 0 && ctx.n == 0) {
+ /* RFC 9051 SS7.1: LIMIT, an implementation limit was reached */
+ if (rc == 0 && ctx.operand_len > SEARCH_OPERANDS_MAX) {
+ errmsg = "[LIMIT] search strings exceed 4096 octets in all";
+ rc = -2;
+ } else if (rc == 0 && ctx.uses_content) {
+ errmsg = "search keys that require message content/header "
+ "access are not supported in this pass";
+ rc = -2;
+ } else if (rc == 0 && ctx.n == 0) {
errmsg = "SEARCH requires search criteria";
rc = -1;
}
memcpy(nodes_out, ctx.nodes, ctx.n * sizeof(*nodes_out));
*nnodes_out = ctx.n;
*uses_modseq_out = ctx.uses_modseq;
+ memcpy(pool_out, ctx.pool, ctx.operand_len);
+ *poollen_out = (uint32_t)ctx.operand_len;
return (0);
}
-/* RFC 9051 SS6.4.4 search-return-opts; SAVE recognized but rejected -2/NO */
+/* RFC 9051 SS6.4.4 search-return-opts; SAVE is refused */
int
parse_search_return_opts(char **pp, uint32_t *opts_out, const char **errmsg)
{
char *p = *pp;
*opts_out = 0;
- p++; /* skip the '(' the caller already confirmed is there */
+ p++;
while (*p == ' ')
p++;
}
}
-/* RFC 9051 SS6.4.4 SEARCH; CHARSET US-ASCII/UTF-8 only, else unsupported */
+static void search_dispatch_finish(struct session *,
+ const struct search_parse_result *, struct search_node *);
+
+/* RFC 9051 SS6.4.4 SEARCH; US-ASCII and UTF-8 only */
int
cmd_search(struct session *s, const char *tag, char *args)
{
return search_dispatch(s, tag, args, 0);
}
-/* shared by cmd_search()/UID SEARCH; by_uid changes reporting, not parsing */
int
search_dispatch(struct session *s, const char *tag, char *args, int by_uid)
{
}
if (return_opts == 0)
- return_opts = SEARCH_RETURN_ALL; /* SS6.4.4's default */
+ return_opts = SEARCH_RETURN_ALL; /* RFC 9051 SS6.4.4 */
if (strncasecmp(p, "CHARSET", 7) == 0 &&
(p[7] == ' ' || p[7] == '\0')) {
if (strcasecmp(charset, "US-ASCII") != 0 &&
strcasecmp(charset, "UTF-8") != 0) {
- /*
- * RFC 9051 SS9 ABNF requires parens around charset
- * list; over SS6.4.4.4
- */
+ /* RFC 9051 SS9 puts the charset list in parens */
session_reply(s, tag, "NO",
"[BADCHARSET (US-ASCII UTF-8)] unsupported "
"CHARSET");
}
if (s->store_iev == NULL) {
- /* same invariant check as cmd_fetch()/cmd_store_cmd() */
log_warnx("session %u: %s with no store channel wired",
s->id, by_uid ? "UID SEARCH" : "SEARCH");
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return (1);
}
- /*
- * Grammar parsing no longer happens in this process -- the
- * highest-risk remainder is handed to the per-connection
- * search-oracle (same fail-soft channel check sasl_plain_finish()
- * uses); the old inline parsing and its aftermath now live in
- * search_dispatch_finish(), invoked once IMSG_SEARCH_PARSE_RESULT
- * arrives.
- */
- if (strlen(p) >= SEARCH_ORACLE_ARGS_MAX) {
+ if (strlen(p) >= SEARCH_ARGS_MAX) {
session_reply(s, tag, "BAD", "SEARCH criteria too long");
return (1);
}
- if (iev_search.ibuf.fd == -1) {
- session_reply(s, tag, "NO",
- "[UNAVAILABLE] search temporarily unavailable");
- return (1);
- }
-
- /* defensive cleanup of a previous SEARCH; should not find any */
free(s->search_matches);
s->search_matches = NULL;
s->search_nmatches = 0;
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return (1);
}
- s->state = SESSION_SEARCH_PARSING;
+ {
+ struct search_parse_result res;
+ struct search_node nodes[SEARCH_PROGRAM_MAX_NODES];
- if (imsg_compose(&iev_search.ibuf, IMSG_SEARCH_PARSE_REQUEST, 0, 0,
- -1, p, strlen(p)) == -1) {
- /*
- * The oracle never received this request, so without an
- * explicit reply here the session would wait forever in
- * SESSION_SEARCH_PARSING (no inactivity timeout), queueing
- * commands until SESSION_CMD_QUEUE_MAX drops the connection.
- */
- log_warn("session %u: imsg_compose IMSG_SEARCH_PARSE_REQUEST",
- s->id);
- session_reply(s, tag, "NO",
- "[UNAVAILABLE] search temporarily unavailable");
- s->state = SESSION_SELECTED;
- return (1);
+ memset(&res, 0, sizeof(res));
+ res.rc = search_program_parse(p, nodes, &res.nnodes,
+ &res.uses_modseq, res.pool, &res.poollen, res.errmsg,
+ sizeof(res.errmsg));
+ search_dispatch_finish(s, &res,
+ res.nnodes > 0 ? nodes : NULL);
}
return (1);
}
-/*
- * Completes search_dispatch() once listener_dispatch_search()
- * gets this session's IMSG_SEARCH_PARSE_RESULT -- replies BAD/NO
- * from the oracle's (rc, errmsg) or, on rc==0, does what a
- * successful parse_search_key_list() call used to; nodes is
- * non-NULL only when rc==0 and nnodes>0, and the caller owns
- * freeing it.
- */
-void
+static void
search_dispatch_finish(struct session *s,
- const struct imsg_search_parse_result *res, struct search_node *nodes)
+ const struct search_parse_result *res, struct search_node *nodes)
{
if (res->rc == -1) {
session_reply(s, s->pending_tag, "BAD", res->errmsg);
s->search_used_modseq = res->uses_modseq;
s->search_max_modseq = 0;
- /*
- * RFC 7162 SS3.1: a SEARCH with MODSEQ item is a CONDSTORE enabling
- * command
- */
+ /* RFC 7162 SS3.1: MODSEQ enables CONDSTORE */
if (res->uses_modseq)
session_condstore_enable(s);
memset(&req, 0, sizeof(req));
req.nnodes = res->nnodes;
+ req.poollen = res->poollen;
+ 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,
s->search_matches_cap = newcap;
}
- /*
- * RFC 9051 SS6.4.9: UID SEARCH reports UIDs, not seqnos, in ESEARCH
- * data
- */
+ /* RFC 9051 SS6.4.9: UID SEARCH reports UIDs */
s->search_matches[s->search_nmatches++] = s->cmd_by_uid ?
m->uid : m->seqno;
- /* RFC 7162 SS3.1.6: running max modseq; printed only if MODSEQ used */
+ /* RFC 7162 SS3.1.6 */
if (m->modseq > s->search_max_modseq)
s->search_max_modseq = m->modseq;
}
-/*
- * Fixed part of an ESEARCH line (correlator + tag + " UID" +
- * MIN/MAX/COUNT/MODSEQ + CRLF) with headroom; the variable ALL
- * list is budgeted separately via SEARCH_ALL_PER_MATCH below.
- */
#define SEARCH_RESP_PREFIX_MAX 256
#define SEARCH_ALL_PER_MATCH 11 /* "4294967295" + one separator */
-/* builds/sends ESEARCH response once store's SEARCH pass completes (SS6.4.4) */
+/* RFC 9051 SS7.3.4 ESEARCH response */
void
session_finish_search(struct session *s, struct imsg_mbox_result *res)
{
s->state = SESSION_SELECTED;
- /*
- * res->error is enum mbox_op_error, not a boolean (MBOX_OP_OK is
- * 1, MBOX_ERR_UNSET is 0), so print it as an OK/error label
- * rather than a raw number that would misread success as a fault.
- */
log_debug("session %u: SEARCH done, status=%s, %u match(es)", s->id,
res->error == MBOX_OP_OK ? "OK" : "ERROR", res->count);
if (res->error != MBOX_OP_OK || s->search_alloc_failed)
goto fail;
- /*
- * Heap-allocated for the worst case rather than a fixed 8KB stack
- * buffer -- format_seq_list() truncates on a token boundary, so
- * an over-long ALL list used to silently produce a short-but-valid
- * ESEARCH, making a bulk MOVE/STORE/EXPUNGE quietly operate on a
- * subset.
- */
bufsize = SEARCH_RESP_PREFIX_MAX +
(size_t)s->search_nmatches * SEARCH_ALL_PER_MATCH + 1;
if ((buf = malloc(bufsize)) == NULL) {
n = snprintf(buf, bufsize, "* ESEARCH (TAG \"%s\")", s->pending_tag);
if (n < 0 || (size_t)n >= bufsize) {
- /*
- * pending_tag is IMAP_TAG_MAX-bounded and
- * tag_is_valid()-checked by session_handle_line(), so it can't
- * hold a quote or backslash that would break the quoting above
- * -- checked rather than assumed.
- */
log_warnx("session %u: SEARCH response prefix did not fit",
s->id);
goto fail;
len += (size_t)snprintf(buf + len, bufsize - len,
" COUNT %u", s->search_nmatches);
- /*
- * RFC 7162 SS3.1.10: non-empty MODSEQ result gets "MODSEQ n"
- * appended with the highest mod-sequence among matches.
- */
+ /* RFC 7162 SS3.1.10: MODSEQ in ESEARCH */
if (s->search_used_modseq && s->search_nmatches > 0 &&
len < bufsize)
len += (size_t)snprintf(buf + len, bufsize - len,
" MODSEQ %llu",
(unsigned long long)s->search_max_modseq);
- /*
- * len holds snprintf(3)'s "would-be" length; bufsize is sized
- * for the worst case so this can't actually fire, but keep the
- * defensive clamp anyway rather than trust that reasoning blindly.
- */
if (len >= bufsize - 1)
len = bufsize - 2;
session_write(s, buf, len);
free(buf);
- /*
- * "UID SEARCH completed" follows SS6.4.9's "UID <cmd> completed"
- * pattern
- */
+ /* 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");
goto cleanup;
blob - a67fc4d0d6d609010a94600dcbce2e6c785531b5 (mode 644)
blob + /dev/null
--- src/search_oracle.c
+++ /dev/null
-/* $OpenIMAPD$ */
-
-/*
- * Copyright (c) 2026 David Williams <dhw@openimapd.dev>
- *
- * 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.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-/*
- * search_oracle.c: per-connection SEARCH-grammar parsing process, forked
- * by parent.c, holding only its one peer channel and no TLS/creds/other
- * channels, so a grammar memory-safety bug reaches nothing else; exits on peer
- * EOF like auth.c.
- */
-
-#include <sys/types.h>
-
-#include <event.h>
-#include <grp.h>
-#include <imsg.h>
-#include <pwd.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "imapd.h"
-#include "log.h"
-
-static struct imsgev iev_listener;
-/* fd 3 -- nothing else arrives on it; see dispatch_parent()'s own comment */
-static struct imsgev iev_parent;
-
-static void search_oracle_dispatch(int, short, void *);
-static void search_oracle_dispatch_parent(int, short, void *);
-static void search_oracle_fail(struct imsgev *, const char *);
-
-__dead void
-search_oracle_main(void)
-{
- struct imsgbuf ibuf3;
- struct imsg imsg;
- struct passwd *pw;
- int peer_fd;
- ssize_t n;
- uint32_t session_id;
-
- /*
- * fd-passing is allowed on this channel for the IMSG_SETUP_SEARCH_PEER
- * peer fd below; see imsgev_ibuf_init()'s own comment
- */
- imsgev_ibuf_init(&ibuf3, 3);
-
- /*
- * Unlike auth.c or listener.c, this process needs no config at all
- * beyond the one peer fd -- its whole boot sequence is draining this
- * one message.
- */
- for (;;) {
- if ((n = imsgbuf_get(&ibuf3, &imsg)) == -1)
- fatal("imsgbuf_get");
- if (n != 0)
- break;
- if ((n = imsgbuf_read(&ibuf3)) == -1)
- fatal("imsgbuf_read");
- if (n == 0)
- fatalx("search-oracle: parent closed channel before "
- "IMSG_SETUP_SEARCH_PEER");
- }
- if (imsg_get_type(&imsg) != IMSG_SETUP_SEARCH_PEER)
- fatalx("search-oracle: expected IMSG_SETUP_SEARCH_PEER, got "
- "%d", imsg_get_type(&imsg));
- peer_fd = imsg_get_fd(&imsg);
- /* the id field carries this worker's session; see parent.c */
- session_id = imsg_get_id(&imsg);
- imsg_free(&imsg);
- if (peer_fd == -1)
- fatalx("search-oracle: IMSG_SETUP_SEARCH_PEER carried no fd");
-
- /*
- * search-oracle's own daemon-user identity, distinct from
- * listener's/auth's -- it holds no secrets, but a dedicated uid still
- * keeps its compromise domain separate, per project convention.
- */
- if ((pw = getpwnam("_imapsearch")) == NULL)
- fatalx("getpwnam _imapsearch: no such user "
- "(expected, not yet provisioned by an install script)");
-
- if (chroot("/var/empty") == -1)
- fatal("chroot /var/empty");
- if (chdir("/") == -1)
- fatal("chdir /");
-
- if (setgroups(1, &pw->pw_gid) == -1 ||
- setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 ||
- setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
- fatal("cannot drop privileges to _imapsearch");
-
- setproctitle("session %u search", session_id);
-
- event_init();
- imsgev_init(&iev_listener, peer_fd, search_oracle_dispatch, NULL);
- imsgev_init_from_ibuf(&iev_parent, &ibuf3,
- search_oracle_dispatch_parent,
- NULL);
-
- /*
- * No rpath (touches no file, ever), no inet (only its one already-open
- * peer channel), no recvfd/sendfd (never receives or attaches a
- * descriptor beyond the one peer fd drained at boot) -- leaving just
- * "stdio", matching auth.c minus rpath.
- */
-#ifdef __OpenBSD__
- if (pledge("stdio", NULL) == -1)
- fatal("pledge");
-#endif
-
- event_dispatch();
- fatalx("search-oracle: exited event loop");
-}
-
-/* EV_WRITE must be handled: imsg_compose() queues, imsgbuf_write() sends it */
-static void
-search_oracle_dispatch(int fd, short event, void *arg)
-{
- struct imsgev *iev = arg;
- struct imsg imsg;
- ssize_t n;
-
- if (event & EV_WRITE) {
- if (imsgbuf_write(&iev->ibuf) == -1)
- fatal("imsgbuf_write");
- }
-
- if (event & EV_READ) {
- if ((n = imsgbuf_read(&iev->ibuf)) == -1)
- fatal("imsgbuf_read");
- if (n == 0) {
- /*
- * This process serves exactly one connection and exits
- * when it's done rather than idling in
- * event_dispatch(), matching auth.c/listener.c;
- * parent.c's reap_child() treats this as expected, not
- * a warning.
- */
- log_debug("search-oracle: listener closed channel, "
- "exiting");
- exit(0);
- }
- }
-
- for (;;) {
- if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1)
- fatal("imsgbuf_get");
- if (n == 0)
- break;
-
- switch (imsg_get_type(&imsg)) {
- case IMSG_SEARCH_PARSE_REQUEST: {
- char args[SEARCH_ORACLE_ARGS_MAX + 1];
- struct search_node nodes[SEARCH_PROGRAM_MAX_NODES];
- struct imsg_search_parse_result res;
- size_t len, bodylen;
- void *combined;
-
- len = imsg_get_len(&imsg);
- if (len > (size_t)SEARCH_ORACLE_ARGS_MAX) {
- log_warnx("IMSG_SEARCH_PARSE_REQUEST too "
- "large (%zu > %u)", len,
- (unsigned int)SEARCH_ORACLE_ARGS_MAX);
- search_oracle_fail(iev,
- "SEARCH criteria too long");
- break;
- }
- if (imsg_get_data(&imsg, args, len) == -1) {
- log_warnx("bad IMSG_SEARCH_PARSE_REQUEST");
- search_oracle_fail(iev,
- "malformed SEARCH request");
- break;
- }
- /*
- * imsg_get_data() guarantees size, not NUL term, same
- * as auth.c's imsgs
- */
- args[len] = '\0';
-
- memset(&res, 0, sizeof(res));
- res.rc = search_oracle_parse(args, nodes, &res.nnodes,
- &res.uses_modseq, res.errmsg, sizeof(res.errmsg));
-
- bodylen = (res.rc == 0) ?
- (size_t)res.nnodes * sizeof(nodes[0]) : 0;
- if ((combined = malloc(sizeof(res) + bodylen)) ==
- NULL) {
- log_warn("malloc IMSG_SEARCH_PARSE_RESULT "
- "buffer");
- search_oracle_fail(iev,
- "SEARCH temporarily unavailable");
- break;
- }
- memcpy(combined, &res, sizeof(res));
- if (bodylen > 0)
- memcpy((char *)combined + sizeof(res), nodes,
- bodylen);
-
- if (imsg_compose(&iev->ibuf, IMSG_SEARCH_PARSE_RESULT,
- 0, 0, -1, combined, sizeof(res) + bodylen) == -1) {
- log_warn("imsg_compose "
- "IMSG_SEARCH_PARSE_RESULT");
- /*
- * the small reply may still fit where the full
- * one did not
- */
- search_oracle_fail(iev,
- "SEARCH temporarily unavailable");
- }
- free(combined);
- break;
- }
- default:
- log_debug("search_oracle_dispatch: unhandled %d",
- imsg_get_type(&imsg));
- break;
- }
- imsg_free(&imsg);
- }
- imsgev_rearm_read(iev);
- (void)fd;
-}
-
-/*
- * Answers a parse request this process couldn't carry out so the
- * listener-worker is never left waiting -- every handler path replies, even
- * "cannot happen" ones, since a missing reply wedges the session forever; rc =
- * -2 (NO) since these are local failures, not bad search criteria.
- */
-static void
-search_oracle_fail(struct imsgev *iev, const char *errmsg)
-{
- struct imsg_search_parse_result res;
-
- memset(&res, 0, sizeof(res));
- res.rc = -2;
- (void)strlcpy(res.errmsg, errmsg, sizeof(res.errmsg));
-
- if (imsg_compose(&iev->ibuf, IMSG_SEARCH_PARSE_RESULT, 0, 0, -1,
- &res, sizeof(res)) == -1)
- log_warn("imsg_compose IMSG_SEARCH_PARSE_RESULT (failure)");
-}
-
-/*
- * parent never sends search-oracle anything post-boot, so this only notices if
- * parent's end closes, same as auth.c's auth_dispatch_parent().
- */
-static void
-search_oracle_dispatch_parent(int fd, short event, void *arg)
-{
- struct imsgev *iev = arg;
- struct imsg imsg;
- ssize_t n;
-
- if (event & EV_WRITE) {
- if (imsgbuf_write(&iev->ibuf) == -1)
- fatal("imsgbuf_write");
- }
-
- if (event & EV_READ) {
- if ((n = imsgbuf_read(&iev->ibuf)) == -1)
- fatal("imsgbuf_read");
- if (n == 0) {
- log_warnx("parent closed channel");
- event_del(&iev->ev);
- return;
- }
- }
-
- for (;;) {
- if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1)
- fatal("imsgbuf_get");
- if (n == 0)
- break;
-
- log_debug("search_oracle_dispatch_parent: unhandled %d",
- imsg_get_type(&imsg));
- imsg_free(&imsg);
- }
- imsgev_rearm_read(iev);
- (void)fd;
-}
blob - 913b498731d9da804ddf2018bad6702f97c313af
blob + 62c0f546829cd6a07af029b8851ddef5fb6c0385
--- src/store.c
+++ src/store.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/* store.c: privilege-dropped mailbox-store process; handles IMSG_MBOX_*. */
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
+#include <sys/uio.h>
#include <dirent.h>
#include <errno.h>
#include <grp.h>
#include <imsg.h>
#include <limits.h>
+#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "mboxname.h"
#include "store_internal.h"
-static struct imsgev iev_listener;
+static TAILQ_HEAD(, store_session) sessions =
+ TAILQ_HEAD_INITIALIZER(sessions);
-/*
- * Defined below store_main(); declared here so boot can apply the
- * same NUL-termination rule to IMSG_STORE_INIT as every runtime
- * message.
- */
+static struct imsgev parent_iev;
+
+static int parent_gone;
+
+static void store_parent_dispatch(int, short, void *);
+static void store_attach(uint32_t, int);
+static void store_detach(struct store_session *);
+static void store_end(struct store_session *);
+static __dead void store_exit(void);
+
+/* a bare imsgbuf: every exchange with the parser is synchronous */
+static struct imsgbuf parser_ibuf;
+static int parser_up; /* parser_ibuf holds a channel */
+static int parser_wanted; /* asked the parent, no answer yet */
+static struct event parser_idle_ev;
+static struct event parser_wait_ev;
+
+/* kept here, not in the parser, so a parser cannot clear a strike */
+#define PARSER_STRIKE_SLOTS 32
+static struct {
+ char basename[sizeof(((struct imsg_parser_req *)NULL)->basename)];
+ int count;
+} parser_strikes[PARSER_STRIKE_SLOTS];
+static size_t parser_strike_next;
+
+static int parser_alive(void);
+static void parser_drop(void);
+static void parser_fail(const char *);
+static void parser_install(int);
+static void parser_give_up(void);
+static void parser_walks(int);
+static void parser_idle(int, short, void *);
+static void parser_wait_expired(int, short, void *);
+static int parser_struck_out(const char *);
+
+static int parser_reply_safe(const char *, size_t);
+static int parser_reply_valid(uint32_t, const char *, size_t,
+ uint32_t);
+static int parser_literal_safe(const char *, size_t);
+static int parser_extent_safe(uint64_t, uint64_t, uint64_t);
+static int extent_has_nul(int, uint64_t, uint64_t);
+
static int imsg_field_valid(const char *, size_t, const char *);
-/* definitions for store_internal.h's extern globals, shared across store_*.c */
uint32_t session_id;
uint32_t append_counter;
-char selected_mailbox[MBOX_NAME_MAX];
-int mailbox_selected;
uint32_t bodystructure_read_max;
uint64_t append_max;
uint32_t lock_timeout_secs;
int maildir_root_fd = -1;
-int mailbox_dir_fd = -1;
__dead void
store_main(void)
struct imsg imsg;
ssize_t n;
struct imsg_store_init init;
- int peer_fd;
gid_t gid;
- /* store children take no imapd.conf; uid/gid/spool_root via INIT */
- /*
- * fd-passing allowed on channel for SETUP_PEER fd; see
- * imsgev_ibuf_init()
- */
imsgev_ibuf_init(&ibuf3, 3);
- /* IMSG_STORE_INIT first: privilege target is runtime, pre-chroot() */
for (;;) {
if ((n = imsgbuf_get(&ibuf3, &imsg)) == -1)
fatal("imsgbuf_get");
append_max = init.append_max;
lock_timeout_secs = init.lock_timeout_secs;
- /*
- * imsg_get_data() guarantees payload size, not NUL-termination,
- * and these fields feed chroot(2)/snprintf("%s") directly;
- * fatalx() here since this is trusted boot-time data, not a
- * runtime message that gets a graceful refusal.
- */
+ /* imsg_get_data() does not NUL-terminate; these reach chroot(2) */
if (!imsg_field_valid(init.spool_root, sizeof(init.spool_root),
"IMSG_STORE_INIT.spool_root") ||
!imsg_field_valid(init.maildir, sizeof(init.maildir),
if (chdir("/") == -1)
fatal("chdir /");
- /*
- * drop to authenticated user's uid/gid, confining bugs to that user's
- * files
- */
gid = init.gid;
if (setgroups(1, &gid) == -1 ||
setresgid(init.gid, init.gid, init.gid) == -1 ||
fatal("session %u: cannot drop privileges to uid %u gid %u",
session_id, init.uid, init.gid);
- setproctitle("session %u store", session_id);
+ setproctitle("store uid %u", (unsigned int)init.uid);
- /*
- * Confinement happens before the handshake ack, not after:
- * acking first would leave a later filesystem failure with no
- * reply, while acking last lets a bad maildir trip parent's
- * pending-timeout and the designed failure path.
- */
+ /* confine before the ack, so a failure can be reported */
- /* unveil() scoped to the session's mailbox subdir, past chroot */
{
char unveil_path[sizeof(init.maildir) + 1];
if (unveil(unveil_path, "rwc") == -1)
fatal("unveil %s", unveil_path);
- /*
- * chdir once so handlers can use bare relative paths under
- * unveiled dir
- */
if (chdir(unveil_path) == -1)
fatal("chdir %s", unveil_path);
}
- /*
- * Descriptors for the maildir root and for the directory this
- * child stands in. unveil(2) covers a lookup relative to a
- * descriptor exactly as it covers one relative to the cwd: namei()
- * calls unveil_start_relative() on the starting vnode in both
- * cases (sys/kern/vfs_lookup.c).
- */
+ /* unveil(2) covers lookups relative to these (sys/kern/vfs_lookup.c) */
if ((maildir_root_fd = open(".", O_RDONLY | O_DIRECTORY)) == -1)
fatal("open maildir root");
- if ((mailbox_dir_fd = open(".", O_RDONLY | O_DIRECTORY)) == -1)
- fatal("open maildir root as the selected mailbox");
if (unveil(NULL, NULL) == -1)
fatal("unveil lock");
- /*
- * privilege-dropped, confined; finish handshake like boot child (peer,
- * ack)
- */
- peer_fd = setup_recv_one_peer(&ibuf3);
setup_recv_done_and_ack(&ibuf3);
event_init();
- imsgev_init(&iev_listener, peer_fd, store_dispatch, NULL);
+ evtimer_set(&parser_idle_ev, parser_idle, NULL);
+ evtimer_set(&parser_wait_ev, parser_wait_expired, NULL);
+ imsgev_init_from_ibuf(&parent_iev, &ibuf3, store_parent_dispatch,
+ NULL);
- /*
- * flock/rpath/wpath/cpath for index and delivery, no fattr;
- * sendfd for FETCH, which hands the listener a read-only
- * descriptor on a message rather than its octets. No recvfd:
- * this process's one peer fd has already arrived.
- */
+ /* the handshake may already have read the first attach: take it */
+ store_parent_dispatch(parent_iev.ibuf.fd, 0, &parent_iev);
+
+ /* sendfd: FETCH hands the listener a read-only descriptor */
#ifdef __OpenBSD__
- if (pledge("stdio rpath wpath cpath flock sendfd", NULL) == -1)
+ if (pledge("stdio rpath wpath cpath flock recvfd sendfd", NULL) == -1)
fatal("pledge");
#endif
fatalx("store: exited event loop");
}
+static void
+store_parent_dispatch(int fd, short event, void *arg)
+{
+ struct imsgev *iev = arg;
+ struct imsg imsg;
+ ssize_t n;
+
+ if (event & EV_WRITE) {
+ if (imsgbuf_write(&iev->ibuf) == -1)
+ fatal("imsgbuf_write");
+ }
+ if (event & EV_READ) {
+ if ((n = imsgbuf_read(&iev->ibuf)) == -1)
+ fatal("imsgbuf_read");
+ if (n == 0) {
+ /* no session can arrive now; serve those here out */
+ log_debug("store: parent closed channel");
+ event_del(&iev->ev);
+ parent_gone = 1;
+ if (TAILQ_EMPTY(&sessions))
+ store_exit();
+ return;
+ }
+ }
+
+ for (;;) {
+ if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1)
+ fatal("imsgbuf_get");
+ if (n == 0)
+ break;
+
+ switch (imsg_get_type(&imsg)) {
+ case IMSG_SETUP_PEER:
+ if (imsg_get_id(&imsg) == 0)
+ parser_install(imsg_get_fd(&imsg));
+ else
+ store_attach(imsg_get_id(&imsg),
+ imsg_get_fd(&imsg));
+ break;
+ case IMSG_PARSER_NONE:
+ if (parser_wanted) {
+ log_warnx("store: no parser-worker could be "
+ "started");
+ parser_give_up();
+ }
+ break;
+ case IMSG_STORE_EXIT:
+ imsg_free(&imsg);
+ store_exit();
+ default:
+ log_debug("store_parent_dispatch: unhandled %d "
+ "(session %u)", imsg_get_type(&imsg), session_id);
+ break;
+ }
+ imsg_free(&imsg);
+ }
+ imsgev_rearm_read(iev);
+ (void)fd;
+}
+
+static void
+store_attach(uint32_t id, int fd)
+{
+ struct store_session *ss;
+
+ if (fd == -1) {
+ log_warnx("session %u: IMSG_SETUP_PEER carried no fd", id);
+ return;
+ }
+ TAILQ_FOREACH(ss, &sessions, entry) {
+ if (ss->id == id)
+ break;
+ }
+ if (id == 0 || ss != NULL) {
+ log_warnx("session %u: refusing a session that is 0 or "
+ "already attached", id);
+ close(fd);
+ return;
+ }
+ if ((ss = calloc(1, sizeof(*ss))) == NULL) {
+ log_warn("session %u: calloc", id);
+ close(fd);
+ return;
+ }
+ ss->id = id;
+ ss->cur_snap.dfd = -1; /* nothing read from cur/ yet */
+ if ((ss->mailbox_dir_fd = mailbox_open_dir("")) == -1) {
+ log_warn("session %u: open maildir root", id);
+ free(ss);
+ close(fd);
+ return;
+ }
+ imsgev_init(&ss->iev, fd, store_dispatch, ss);
+ TAILQ_INSERT_TAIL(&sessions, ss, entry);
+ log_debug("session %u: attached", id);
+}
+
+/* the parser is assumed attackable: check its reply before use */
+static int
+parser_reply_safe(const char *buf, size_t len)
+{
+ size_t i;
+ int depth = 0, inquote = 0, escaped = 0;
+
+ if (len < 2 || buf[0] != '(' || buf[len - 1] != ')')
+ return (0);
+
+ /* no byte may end the line, quoted or not */
+ for (i = 0; i < len; i++) {
+ if (buf[i] == '\r' || buf[i] == '\n' || buf[i] == '\0')
+ return (0);
+ }
+
+ /* then framing, so a reply cannot swallow what follows it */
+ for (i = 0; i < len; i++) {
+ char c = buf[i];
+
+ if (escaped) {
+ escaped = 0;
+ continue;
+ }
+ if (inquote) {
+ if (c == '\\')
+ escaped = 1;
+ else if (c == '"')
+ inquote = 0;
+ continue;
+ }
+ if (c == '"')
+ inquote = 1;
+ else if (c == '(')
+ depth++;
+ else if (c == ')' && --depth < 0)
+ return (0);
+ }
+
+ return (!inquote && !escaped && depth == 0);
+}
+
+/* RFC 9051 SS9: a literal may not hold NUL */
+static int
+parser_literal_safe(const char *buf, size_t len)
+{
+ return (len > 0 && len <= FETCH_HEADER_MAX &&
+ memchr(buf, '\0', len) == NULL);
+}
+
+static int
+parser_reply_valid(uint32_t type, const char *buf, size_t len,
+ uint32_t maxlen)
+{
+ size_t i;
+
+ switch (type) {
+ case IMSG_PARSER_HEADER_FIELDS:
+ return (parser_literal_safe(buf, len));
+ case IMSG_PARSER_SEARCH:
+ if (len != maxlen)
+ return (0);
+ for (i = 0; i < len; i++) {
+ if (buf[i] != 0 && buf[i] != 1)
+ return (0);
+ }
+ return (1);
+ default:
+ return (parser_reply_safe(buf, len));
+ }
+}
+
+static int
+parser_extent_safe(uint64_t off, uint64_t len, uint64_t size)
+{
+ return (off <= size && len <= size - off);
+}
+
+static int
+extent_has_nul(int fd, uint64_t off, uint64_t len)
+{
+ char buf[16384];
+ size_t want;
+ ssize_t n;
+
+ while (len > 0) {
+ want = len < sizeof(buf) ? (size_t)len : sizeof(buf);
+ if ((n = pread(fd, buf, want, (off_t)off)) == -1) {
+ if (errno == EINTR)
+ continue;
+ return (1);
+ }
+ if (n == 0 || memchr(buf, '\0', (size_t)n) != NULL)
+ return (1);
+ off += (uint64_t)n;
+ len -= (uint64_t)n;
+ }
+ return (0);
+}
+
+/* blocks until a reply, the deadline, or the parser dies */
int
+parser_request(uint32_t type, const char *label, int fd,
+ const char *basename, struct imsg_parser_req *req, void *extra,
+ size_t extralen, uint32_t maxlen, struct imsg_parser_rep *rep_out,
+ char **buf_out)
+{
+ struct imsg_parser_req none;
+ struct imsg_parser_rep rep;
+ struct imsg imsg;
+ struct iovec iov[2];
+ struct pollfd pfd;
+ struct stat st;
+ struct timespec start, now;
+ struct timeval tv;
+ static uint32_t reqid;
+ uint32_t id;
+ ssize_t n;
+ int dupfd, ms, rc = -1;
+
+ memset(rep_out, 0, sizeof(*rep_out));
+ if (buf_out != NULL)
+ *buf_out = NULL;
+
+ if (!parser_up || parser_struck_out(basename))
+ return (-1);
+
+ if (req == NULL) {
+ memset(&none, 0, sizeof(none));
+ req = &none;
+ }
+ if (strlcpy(req->basename, basename, sizeof(req->basename)) >=
+ sizeof(req->basename)) {
+ log_warnx("session %u: message name too long for the parser",
+ session_id);
+ return (-1);
+ }
+
+ if ((dupfd = dup(fd)) == -1) {
+ log_warn("session %u: dup %s request", session_id, label);
+ return (-1);
+ }
+
+ id = ++reqid;
+ iov[0].iov_base = req;
+ iov[0].iov_len = sizeof(*req);
+ iov[1].iov_base = extra;
+ iov[1].iov_len = extralen;
+ /* imsg_composev() owns dupfd only once it succeeds */
+ if (imsg_composev(&parser_ibuf, type, id, 0, dupfd, iov,
+ extra != NULL ? 2 : 1) == -1) {
+ log_warn("session %u: imsg_composev %s request", session_id,
+ label);
+ close(dupfd);
+ return (-1);
+ }
+ if (imsgbuf_flush(&parser_ibuf) == -1) {
+ /* it never had the request: let it go, count nothing */
+ log_warn("session %u: imsgbuf_flush %s request", session_id,
+ label);
+ parser_drop();
+ return (-1);
+ }
+
+ if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
+ fatal("clock_gettime");
+
+ for (;;) {
+ if ((n = imsgbuf_get(&parser_ibuf, &imsg)) == -1) {
+ log_warn("session %u: imsgbuf_get (parser)",
+ session_id);
+ parser_fail(basename);
+ return (-1);
+ }
+ if (n == 0) {
+ if (clock_gettime(CLOCK_MONOTONIC, &now) == -1)
+ fatal("clock_gettime");
+ /* one budget for the whole exchange */
+ ms = PARSER_REPLY_TIMEOUT_SEC * 1000 -
+ (int)((now.tv_sec - start.tv_sec) * 1000 +
+ (now.tv_nsec - start.tv_nsec) / 1000000);
+ if (ms <= 0) {
+ log_warnx("session %u: parser-worker did not "
+ "answer within %d seconds, message %s",
+ session_id, PARSER_REPLY_TIMEOUT_SEC,
+ basename);
+ parser_fail(basename);
+ return (-1);
+ }
+ pfd.fd = parser_ibuf.fd;
+ pfd.events = POLLIN;
+ if ((n = poll(&pfd, 1, ms)) == -1) {
+ if (errno == EINTR)
+ continue;
+ log_warn("session %u: poll (parser)",
+ session_id);
+ parser_drop();
+ return (-1);
+ }
+ if (n == 0)
+ continue; /* deadline rechecked above */
+ if ((n = imsgbuf_read(&parser_ibuf)) == -1) {
+ log_warn("session %u: imsgbuf_read (parser)",
+ session_id);
+ parser_fail(basename);
+ return (-1);
+ }
+ if (n == 0) {
+ log_warnx("session %u: parser-worker closed "
+ "its channel, message %s", session_id,
+ basename);
+ parser_fail(basename);
+ return (-1);
+ }
+ continue;
+ }
+ if (imsg_get_type(&imsg) != type ||
+ imsg_get_id(&imsg) != id) {
+ log_warnx("session %u: unexpected parser reply type "
+ "%u id %u (wanted %u/%u)", session_id,
+ imsg_get_type(&imsg), imsg_get_id(&imsg), type,
+ id);
+ imsg_free(&imsg);
+ continue;
+ }
+ if (imsg_get_buf(&imsg, &rep, sizeof(rep)) == -1) {
+ log_warnx("session %u: bad parser reply header",
+ session_id);
+ imsg_free(&imsg);
+ break;
+ }
+ if (rep.found && type == IMSG_PARSER_PART) {
+ /* checked against the file, not the parser's word */
+ if (imsg_get_len(&imsg) == 0 && fstat(fd, &st) == 0 &&
+ parser_extent_safe(rep.part_off, rep.part_len,
+ (uint64_t)st.st_size) &&
+ !extent_has_nul(fd, rep.part_off, rep.part_len)) {
+ *rep_out = rep;
+ rc = 0;
+ } else
+ log_warnx("session %u: parser-worker returned "
+ "a %s that cannot be sent, message %s "
+ "skipped", session_id, label, basename);
+ } else if (rep.found && rep.len > 0 && rep.len <= maxlen &&
+ imsg_get_len(&imsg) == rep.len) {
+ if ((*buf_out = malloc(rep.len)) == NULL)
+ log_warn("session %u: malloc %s", session_id,
+ label);
+ else if (imsg_get_buf(&imsg, *buf_out, rep.len) ==
+ -1) {
+ log_warnx("session %u: bad parser reply data",
+ session_id);
+ free(*buf_out);
+ *buf_out = NULL;
+ } else if (!parser_reply_valid(type, *buf_out,
+ rep.len, maxlen)) {
+ log_warnx("session %u: parser-worker returned "
+ "a %s that cannot be sent, message %s "
+ "skipped", session_id, label, basename);
+ free(*buf_out);
+ *buf_out = NULL;
+ } else {
+ *rep_out = rep;
+ rc = 0;
+ }
+ }
+ /* it exits once this reply is sent: let it go now */
+ if (rep.retiring) {
+ log_debug("session %u: parser-worker retiring",
+ session_id);
+ imsg_free(&imsg);
+ parser_drop();
+ return (rc);
+ }
+ imsg_free(&imsg);
+ break;
+ }
+
+ evtimer_del(&parser_idle_ev);
+ tv.tv_sec = PARSER_IDLE_SEC;
+ tv.tv_usec = 0;
+ evtimer_add(&parser_idle_ev, &tv);
+ return (rc);
+}
+
+int
+parser_ready(void)
+{
+ struct timeval tv;
+
+ if (parser_up && !parser_alive())
+ parser_drop();
+ if (parser_up)
+ return (1);
+ if (parser_wanted)
+ return (0);
+ if (parent_gone)
+ return (-1);
+ if (imsg_compose(&parent_iev.ibuf, IMSG_PARSER_WANT, 0, 0, -1, NULL,
+ 0) == -1) {
+ log_warn("session %u: imsg_compose IMSG_PARSER_WANT",
+ session_id);
+ return (-1);
+ }
+ parser_wanted = 1;
+ tv.tv_sec = PARSER_REPLY_TIMEOUT_SEC;
+ tv.tv_usec = 0;
+ evtimer_add(&parser_wait_ev, &tv);
+ return (0);
+}
+
+/* Between requests nothing may arrive: anything readable is its end. */
+static int
+parser_alive(void)
+{
+ struct pollfd pfd;
+
+ pfd.fd = parser_ibuf.fd;
+ pfd.events = POLLIN;
+ pfd.revents = 0;
+ if (poll(&pfd, 1, 0) == -1)
+ return (errno == EINTR);
+ return (pfd.revents == 0);
+}
+
+/* Lets the parser go; closing its channel is what ends it. */
+static void
+parser_drop(void)
+{
+ if (!parser_up)
+ return;
+ evtimer_del(&parser_idle_ev);
+ close(parser_ibuf.fd);
+ imsgbuf_clear(&parser_ibuf);
+ parser_up = 0;
+}
+
+static void
+parser_fail(const char *basename)
+{
+ size_t i;
+
+ parser_drop();
+ for (i = 0; i < PARSER_STRIKE_SLOTS; i++) {
+ if (parser_strikes[i].count > 0 &&
+ strcmp(parser_strikes[i].basename, basename) == 0)
+ break;
+ }
+ if (i == PARSER_STRIKE_SLOTS) {
+ i = parser_strike_next++ % PARSER_STRIKE_SLOTS;
+ (void)strlcpy(parser_strikes[i].basename, basename,
+ sizeof(parser_strikes[i].basename));
+ parser_strikes[i].count = 0;
+ }
+ if (++parser_strikes[i].count == PARSER_STRIKES)
+ log_warnx("session %u: message %s has failed the "
+ "parser-worker %d times, not sending it again",
+ session_id, basename, PARSER_STRIKES);
+}
+
+static int
+parser_struck_out(const char *basename)
+{
+ size_t i;
+
+ for (i = 0; i < PARSER_STRIKE_SLOTS; i++) {
+ if (parser_strikes[i].count >= PARSER_STRIKES &&
+ strcmp(parser_strikes[i].basename, basename) == 0)
+ return (1);
+ }
+ return (0);
+}
+
+static void
+parser_install(int fd)
+{
+ struct timeval tv;
+
+ if (fd == -1) {
+ log_warnx("store: parser IMSG_SETUP_PEER carried no fd");
+ return;
+ }
+ if (parser_up) {
+ log_warnx("store: a second parser-worker, refusing it");
+ close(fd);
+ return;
+ }
+ imsgev_ibuf_init(&parser_ibuf, fd);
+ parser_up = 1;
+ parser_wanted = 0;
+ evtimer_del(&parser_wait_ev);
+ tv.tv_sec = PARSER_IDLE_SEC;
+ tv.tv_usec = 0;
+ evtimer_add(&parser_idle_ev, &tv);
+ parser_walks(1);
+}
+
+static void
+parser_give_up(void)
+{
+ parser_wanted = 0;
+ evtimer_del(&parser_wait_ev);
+ parser_walks(0);
+}
+
+static void
+parser_walks(int ok)
+{
+ struct store_session *ss;
+
+ TAILQ_FOREACH(ss, &sessions, entry) {
+ session_id = ss->id;
+ fetch_walk_parser(ss, ok);
+ search_walk_parser(ss, ok);
+ }
+}
+
+static void
+parser_idle(int fd, short event, void *arg)
+{
+ (void)fd;
+ (void)event;
+ (void)arg;
+ log_debug("store: parser-worker idle, letting it go");
+ parser_drop();
+}
+
+static void
+parser_wait_expired(int fd, short event, void *arg)
+{
+ (void)fd;
+ (void)event;
+ (void)arg;
+ log_warnx("store: no parser-worker within %d seconds",
+ PARSER_REPLY_TIMEOUT_SEC);
+ parser_give_up();
+}
+
+int
mailbox_name_valid(const char *name)
{
- /*
- * This name came off the imsg wire as a fixed-size field, so
- * before treating it as a C string at all: imsg_get_data()
- * guarantees the payload's SIZE, never that the field inside
- * it is terminated. The listener's own copy has no equivalent
- * check because its names come out of its own parser already
- * terminated. Everything after this is the shared rule.
- */
+ /* not NUL-terminated by imsg_get_data() */
if (memchr(name, '\0', MBOX_NAME_MAX) == NULL) {
log_warnx("session %u: mailbox name field is not "
"NUL-terminated, refusing", session_id);
-/*
- * Opens a mailbox directory by name, "" being the maildir root
- * (INBOX). Returns -1 with errno set when it is absent or is not a
- * directory, and the caller closes what it gets. Nothing chdir(2)s
- * after boot: a mailbox is named by a descriptor, never by where this
- * process happens to be standing.
- */
int
mailbox_open_dir(const char *target)
{
O_RDONLY | O_DIRECTORY));
}
-/* NUL-termination check for an imsg-carried field that isn't a mailbox name. */
static int
imsg_field_valid(const char *field, size_t size, const char *what)
{
return (0);
}
-/* Same check applied to every node's keyword field of a SEARCH program. */
static int
-search_nodes_valid(const struct search_node *nodes, uint32_t nnodes)
+search_nodes_valid(const struct search_node *nodes, uint32_t nnodes,
+ uint32_t poollen)
{
- uint32_t i;
+ const struct search_node *n;
+ uint32_t i;
for (i = 0; i < nnodes; i++) {
- if (memchr(nodes[i].keyword, '\0', sizeof(nodes[i].keyword))
- == NULL) {
+ n = &nodes[i];
+ if (memchr(n->keyword, '\0', sizeof(n->keyword)) == NULL) {
log_warnx("session %u: SEARCH node %u keyword is not "
"NUL-terminated, refusing the request",
session_id, i);
return (0);
}
+ if (search_op_content(n->op) && (n->str_off > poollen ||
+ n->str_len > poollen - n->str_off ||
+ n->name_off > poollen ||
+ n->name_len > poollen - n->name_off)) {
+ log_warnx("session %u: SEARCH node %u string lies "
+ "outside its pool, refusing the request",
+ session_id, i);
+ return (0);
+ }
}
return (1);
}
-/*
- * Shared receive-side unpack for store_dispatch()'s "fixed header +
- * trailing array of `count` `elemsize`-sized elements" imsg shape
- * (SELECT/FETCH/STORE/EXPUNGE/COPY/MOVE's seq_range[], SEARCH's
- * search_node[], APPEND's raw body bytes with elemsize 1); *ok_out
- * tells a legitimate 0-element buffer (NULL) apart from failure
- * (also NULL, already logged).
- */
static void *
recv_trailing_array(struct imsg *imsg, const char *what, uint32_t count,
uint32_t maxcount, size_t elemsize, int *ok_out)
return (out);
}
-/*
- * A command that could not take a mailbox's index lock, kept until it can
- * or until "lock timeout" expires. One slot is enough: the listener holds
- * a session's next command while one is in flight (session_is_busy(),
- * listener.c), so a store child has at most one command outstanding.
- *
- * The command is re-run from the top rather than resumed, so a handler
- * that defers must not have touched the mailbox or this session first.
- */
-static struct {
- int active;
- uint32_t type;
- struct imsgev *iev;
- struct event ev;
- struct timespec give_up_at;
- unsigned int wait_ms;
- union {
- struct imsg_mbox_store store;
- struct imsg_mbox_expunge expunge;
- struct imsg_mbox_fetch fetch;
- struct imsg_mbox_search search;
- struct imsg_mbox_select select;
- struct imsg_mbox_status status;
- struct imsg_mbox_copy copy;
- } req;
- union {
- struct seq_range ranges[SEQSET_MAX_RANGES];
- struct search_node nodes[SEARCH_PROGRAM_MAX_NODES];
- } elts;
- uint32_t nelts;
-} deferred;
-
-/*
- * Doubling from the first to the cap keeps a long wait cheap in wakeups,
- * and the cap is also the worst-case delay between the lock coming free
- * and this command noticing. Measured on a 100,000 message mailbox, a
- * command blocked behind a 46 second STORE costs about 190 wakeups at
- * this cap, which is four a second; raising it would buy fewer wakeups
- * than the machine will notice and pay for them in latency.
- */
+/* doubling keeps a long wait cheap; the cap bounds the latency */
#define LOCK_RETRY_FIRST_MS 50
#define LOCK_RETRY_CAP_MS 250
static void deferred_retry(int, short, void *);
-static void deferred_answer_busy_for(uint32_t, struct imsgev *);
+static void deferred_answer_busy_for(uint32_t, struct store_session *);
-/* True once "lock timeout" has passed. 0 disables the bound entirely. */
static int
-deferred_expired(void)
+deferred_expired(struct store_session *ss)
{
- struct timespec now;
+ struct store_deferred *d = &ss->deferred;
+ struct timespec now;
if (lock_timeout_secs == 0)
return (0);
log_warn("session %u: clock_gettime", session_id);
return (1); /* cannot tell how long: stop waiting */
}
- if (now.tv_sec != deferred.give_up_at.tv_sec)
- return (now.tv_sec > deferred.give_up_at.tv_sec);
- return (now.tv_nsec >= deferred.give_up_at.tv_nsec);
+ if (now.tv_sec != d->give_up_at.tv_sec)
+ return (now.tv_sec > d->give_up_at.tv_sec);
+ return (now.tv_nsec >= d->give_up_at.tv_nsec);
}
/* RFC 9051 SS7.1 INUSE: answers one command, having changed nothing. */
static void
-deferred_answer_busy_for(uint32_t type, struct imsgev *iev)
+deferred_answer_busy_for(uint32_t type, struct store_session *ss)
{
+ struct imsgev *iev = &ss->iev;
struct imsg_mbox_result result;
struct imsg_mbox_selected selected;
struct imsg_mbox_status_result status;
break;
case IMSG_MBOX_APPEND_END:
/* the tmp/ file and its descriptors go with the command */
- append_abort();
+ append_abort(ss);
memset(&appended, 0, sizeof(appended));
appended.error = MBOX_OP_ERR_BUSY;
rtype = IMSG_MBOX_APPENDED;
imsgev_rearm_read(iev);
}
-/* The deferred command has run out of time. */
static void
-deferred_give_up(void)
+deferred_give_up(struct store_session *ss)
{
log_debug("session %u: gave up waiting for the index lock after "
"%u seconds", session_id, lock_timeout_secs);
- deferred_answer_busy_for(deferred.type, deferred.iev);
- deferred.active = 0;
+ deferred_answer_busy_for(ss->deferred.type, ss);
+ ss->deferred.active = 0;
}
static void
-deferred_arm(void)
+deferred_arm(struct store_session *ss)
{
- struct timeval tv;
+ struct store_deferred *d = &ss->deferred;
+ struct timeval tv;
- tv.tv_sec = deferred.wait_ms / 1000;
- tv.tv_usec = (deferred.wait_ms % 1000) * 1000;
- evtimer_set(&deferred.ev, deferred_retry, NULL);
- if (evtimer_add(&deferred.ev, &tv) == -1) {
+ tv.tv_sec = d->wait_ms / 1000;
+ tv.tv_usec = (d->wait_ms % 1000) * 1000;
+ evtimer_set(&d->ev, deferred_retry, ss);
+ if (evtimer_add(&d->ev, &tv) == -1) {
log_warnx("session %u: no timer for the index lock retry; "
"answering busy now rather than waiting for ever",
session_id);
- deferred_give_up();
+ deferred_give_up(ss);
return;
}
- if (deferred.wait_ms < LOCK_RETRY_CAP_MS)
- deferred.wait_ms *= 2;
+ if (d->wait_ms < LOCK_RETRY_CAP_MS)
+ d->wait_ms *= 2;
}
-/* Runs the saved command again; gives up once the deadline has passed. */
static void
deferred_retry(int fd, short event, void *arg)
{
- int again;
+ struct store_session *ss = arg;
+ struct store_deferred *d = &ss->deferred;
+ int again;
(void)fd;
(void)event;
- (void)arg;
+ session_id = ss->id;
- switch (deferred.type) {
+ switch (d->type) {
case IMSG_MBOX_STORE:
- again = handle_mbox_store(&deferred.req.store,
- deferred.elts.ranges, deferred.nelts, deferred.iev);
+ again = handle_mbox_store(&d->req.store, d->elts.ranges,
+ d->nelts, ss);
break;
case IMSG_MBOX_EXPUNGE:
- again = handle_mbox_expunge(&deferred.req.expunge,
- deferred.nelts > 0 ? deferred.elts.ranges : NULL,
- deferred.nelts, deferred.iev);
+ again = handle_mbox_expunge(&d->req.expunge,
+ d->nelts > 0 ? d->elts.ranges : NULL, d->nelts, ss);
break;
case IMSG_MBOX_FETCH:
- again = handle_mbox_fetch(&deferred.req.fetch,
- deferred.elts.ranges, deferred.nelts, deferred.iev);
+ again = handle_mbox_fetch(&d->req.fetch, d->elts.ranges,
+ d->nelts, ss);
break;
case IMSG_MBOX_SEARCH:
- again = handle_mbox_search(&deferred.req.search,
- deferred.nelts > 0 ? deferred.elts.nodes : NULL,
- deferred.nelts, deferred.iev);
+ again = handle_mbox_search(&d->req.search,
+ d->nelts > 0 ? d->elts.nodes : NULL, d->nelts, ss);
break;
case IMSG_MBOX_SELECT:
- again = handle_mbox_select(&deferred.req.select,
- deferred.nelts > 0 ? deferred.elts.ranges : NULL,
- deferred.nelts, deferred.iev);
+ again = handle_mbox_select(&d->req.select,
+ d->nelts > 0 ? d->elts.ranges : NULL, d->nelts, ss);
break;
case IMSG_MBOX_STATUS:
- again = handle_mbox_status(&deferred.req.status,
- deferred.iev);
+ again = handle_mbox_status(&d->req.status, ss);
break;
case IMSG_MBOX_COPY:
- again = handle_mbox_copy(&deferred.req.copy,
- deferred.elts.ranges, deferred.nelts, deferred.iev);
+ again = handle_mbox_copy(&d->req.copy, d->elts.ranges,
+ d->nelts, ss);
break;
case IMSG_MBOX_MOVE:
- again = handle_mbox_move(&deferred.req.copy,
- deferred.elts.ranges, deferred.nelts, deferred.iev);
+ again = handle_mbox_move(&d->req.copy, d->elts.ranges,
+ d->nelts, ss);
break;
case IMSG_MBOX_APPEND_END:
/* its state is the in-flight APPEND, not a saved request */
- again = handle_mbox_append_end(deferred.iev);
+ again = handle_mbox_append_end(ss);
break;
default:
- /*
- * Nothing has answered the listener, so give up rather than
- * drop the command and leave the session waiting for ever.
- */
log_warnx("session %u: cannot re-run imsg %u, can't happen",
- session_id, deferred.type);
- deferred_give_up();
+ session_id, d->type);
+ deferred_give_up(ss);
return;
}
if (!again) {
- deferred.active = 0;
- imsgev_rearm_read(deferred.iev);
+ d->active = 0;
+ if (!fetch_walk_paused(ss) && !search_walk_paused(ss))
+ cur_snapshot_discard(&ss->cur_snap);
+ imsgev_rearm_read(&ss->iev);
return;
}
- if (deferred_expired())
- deferred_give_up();
+ if (deferred_expired(ss))
+ deferred_give_up(ss);
else
- deferred_arm();
+ deferred_arm(ss);
}
-/*
- * Takes over a command whose handler could not lock: copies the request
- * and its trailing array, sets the deadline on the first deferral only,
- * and starts retrying. nelts was bounded by maxelts on receipt.
- */
static void
defer_command(uint32_t type, const void *req, size_t reqlen,
const void *elts, uint32_t nelts, uint32_t maxelts, size_t eltlen,
- struct imsgev *iev)
+ struct store_session *ss)
{
- struct timespec now;
+ struct store_deferred *d = &ss->deferred;
+ struct timespec now;
- if (deferred.active) {
- /*
- * The listener holds a session's next command while one is
- * in flight, so this cannot arrive in the ordinary way; do
- * not overwrite the command already waiting.
- */
+ if (d->active) {
log_warnx("session %u: a second command to defer while one "
"waits, refusing the new one", session_id);
- deferred_answer_busy_for(type, iev);
+ deferred_answer_busy_for(type, ss);
return;
}
- if (reqlen > sizeof(deferred.req) || nelts > maxelts ||
- (size_t)nelts * eltlen > sizeof(deferred.elts)) {
+ if (reqlen > sizeof(d->req) || nelts > maxelts ||
+ (size_t)nelts * eltlen > sizeof(d->elts)) {
/* the handler answered nothing, so something must */
log_warnx("session %u: imsg %u too large to defer, can't "
"happen, it is checked on receipt", session_id, type);
- deferred_answer_busy_for(type, iev);
+ deferred_answer_busy_for(type, ss);
return;
}
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
now.tv_sec = 0;
now.tv_nsec = 0;
}
- deferred.active = 1;
- deferred.type = type;
- deferred.iev = iev;
+ d->active = 1;
+ d->type = type;
if (reqlen > 0)
- memcpy(&deferred.req, req, reqlen);
+ memcpy(&d->req, req, reqlen);
if (nelts > 0)
- memcpy(&deferred.elts, elts, (size_t)nelts * eltlen);
- deferred.nelts = nelts;
- deferred.give_up_at = now;
- deferred.give_up_at.tv_sec += lock_timeout_secs;
- deferred.wait_ms = LOCK_RETRY_FIRST_MS;
+ memcpy(&d->elts, elts, (size_t)nelts * eltlen);
+ d->nelts = nelts;
+ d->give_up_at = now;
+ d->give_up_at.tv_sec += lock_timeout_secs;
+ d->wait_ms = LOCK_RETRY_FIRST_MS;
log_debug("session %u: index lock held elsewhere, waiting up to "
"%u seconds", session_id, lock_timeout_secs);
- deferred_arm();
+ deferred_arm(ss);
}
-/*
- * The store's own check: mailbox_selected verifies independently, in this
- * process's own state, that MBOX_SELECT succeeded before
- * FETCH/STORE/EXPUNGE/SEARCH/COPY/MOVE/IDLE_REFRESH, rather than
- * trusting listener.c's gating -- a compromised listener could
- * send these out of order.
- */
+/* checked here, never trusted from the listener */
static int
-require_mailbox_selected(const char *what)
+require_mailbox_selected(struct store_session *ss, const char *what)
{
- if (mailbox_selected)
+ if (ss->mailbox_selected)
return (1);
log_warnx("session %u: %s before a successful IMSG_MBOX_SELECT, "
"refusing", session_id, what);
void
store_dispatch(int fd, short event, void *arg)
{
- struct imsgev *iev = arg;
+ struct store_session *ss = arg;
+ struct imsgev *iev = &ss->iev;
struct imsg imsg;
ssize_t n;
- /* queued IMSG_MBOX_*_RESULT needs an imsgbuf_write() once writable */
+ /* the account's other sessions share this process: label the log */
+ session_id = ss->id;
+
+ /* a failure here ends this session, not the process */
if (event & EV_WRITE) {
- if (imsgbuf_write(&iev->ibuf) == -1)
- fatal("imsgbuf_write");
+ if (imsgbuf_write(&iev->ibuf) == -1) {
+ log_warn("session %u: imsgbuf_write", ss->id);
+ store_end(ss);
+ return;
+ }
/* a FETCH paused for its queue to drain carries on here */
- fetch_walk_resume(iev);
+ fetch_walk_resume(ss);
}
if (event & EV_READ) {
- if ((n = imsgbuf_read(&iev->ibuf)) == -1)
- fatal("imsgbuf_read");
+ if ((n = imsgbuf_read(&iev->ibuf)) == -1) {
+ log_warn("session %u: imsgbuf_read", ss->id);
+ store_end(ss);
+ return;
+ }
if (n == 0) {
- /*
- * listener's end closed; treat like
- * IMSG_STORE_SHUTDOWN: nothing to serve
- */
- store_shutdown();
+ /* the listener's end closed, as IMSG_STORE_SHUTDOWN */
+ store_end(ss);
+ return;
}
}
for (;;) {
- if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1)
- fatal("imsgbuf_get");
+ if ((n = imsgbuf_get(&iev->ibuf, &imsg)) == -1) {
+ log_warn("session %u: imsgbuf_get", ss->id);
+ store_end(ss);
+ return;
+ }
if (n == 0)
break;
switch (imsg_get_type(&imsg)) {
case IMSG_STORE_SHUTDOWN:
imsg_free(&imsg);
- store_shutdown();
- break;
+ store_end(ss);
+ return;
case IMSG_MBOX_SELECT: {
struct imsg_mbox_select req;
struct seq_range *ranges;
int ok;
- /*
- * Same header-plus-variable-body shape as
- * STORE/FETCH/SEARCH, but nranges may legitimately be 0
- * here: plain SELECT/EXAMINE or QRESYNC without
- * known-uids carry no trailing sequence-set.
- */
if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
log_warnx("bad IMSG_MBOX_SELECT (header)");
break;
sizeof(struct seq_range), &ok);
if (!ok)
break;
- /*
- * RFC 9051 SS6.3.2: a failed SELECT leaves no mailbox
- * selected, matching listener's own
- * SESSION_AUTHENTICATED fallback -- clear the SS6.2
- * gate before dispatching so a failed re-SELECT doesn't
- * leave this process still answering "selected".
- */
- mailbox_selected = 0;
+ /* RFC 9051 SS6.3.2: failure deselects */
+ ss->mailbox_selected = 0;
- /*
- * composes own reply like handle_mbox_*(); EV_WRITE via
- * imsgev_on_compose
- */
if (handle_mbox_select(&req, ranges,
- req.qresync_nranges, iev) == 1)
+ req.qresync_nranges, ss) == 1)
defer_command(IMSG_MBOX_SELECT, &req,
sizeof(req), ranges, req.qresync_nranges,
- SEQSET_MAX_RANGES, sizeof(*ranges), iev);
+ SEQSET_MAX_RANGES, sizeof(*ranges), ss);
free(ranges);
break;
}
struct seq_range *ranges;
int ok;
- if (!require_mailbox_selected("IMSG_MBOX_FETCH"))
+ if (!require_mailbox_selected(ss, "IMSG_MBOX_FETCH"))
break;
- /*
- * same header+variable-body as SEARCH, trailing
- * seq_range[] not raw bytes
- */
if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
log_warnx("bad IMSG_MBOX_FETCH (header)");
break;
if (!ok)
break;
if (handle_mbox_fetch(&req, ranges, req.nranges,
- iev) == 1)
+ ss) == 1)
defer_command(IMSG_MBOX_FETCH, &req,
sizeof(req), ranges, req.nranges,
- SEQSET_MAX_RANGES, sizeof(*ranges), iev);
+ SEQSET_MAX_RANGES, sizeof(*ranges), ss);
free(ranges);
break;
}
struct seq_range *ranges;
int ok;
- if (!require_mailbox_selected("IMSG_MBOX_STORE"))
+ if (!require_mailbox_selected(ss, "IMSG_MBOX_STORE"))
break;
- /*
- * same header+variable-body as SEARCH/FETCH, trailing
- * seq_range[] not raw
- */
if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
log_warnx("bad IMSG_MBOX_STORE (header)");
break;
if (!ok)
break;
if (handle_mbox_store(&req, ranges, req.nranges,
- iev) == 1)
+ ss) == 1)
defer_command(IMSG_MBOX_STORE, &req,
sizeof(req), ranges, req.nranges,
- SEQSET_MAX_RANGES, sizeof(*ranges), iev);
+ SEQSET_MAX_RANGES, sizeof(*ranges), ss);
free(ranges);
break;
}
struct seq_range *ranges;
int ok;
- /*
- * Same header-plus-variable-body shape as
- * STORE/FETCH/SEARCH, but nranges may legitimately be
- * 0: plain EXPUNGE and CLOSE (silent=1) carry no
- * sequence-set, only UID EXPUNGE does.
- */
- if (!require_mailbox_selected("IMSG_MBOX_EXPUNGE"))
+ if (!require_mailbox_selected(ss, "IMSG_MBOX_EXPUNGE"))
break;
if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
if (!ok)
break;
if (handle_mbox_expunge(&req, ranges, req.nranges,
- iev) == 1)
+ ss) == 1)
defer_command(IMSG_MBOX_EXPUNGE, &req,
sizeof(req), ranges, req.nranges,
- SEQSET_MAX_RANGES, sizeof(*ranges), iev);
+ SEQSET_MAX_RANGES, sizeof(*ranges), ss);
free(ranges);
break;
}
log_warnx("bad IMSG_MBOX_IDLE_REFRESH");
break;
}
- if (!require_mailbox_selected("IMSG_MBOX_IDLE_REFRESH"))
+ if (!require_mailbox_selected(ss,
+ "IMSG_MBOX_IDLE_REFRESH"))
break;
- handle_mbox_idle_refresh(&req, iev);
+ handle_mbox_idle_refresh(&req, ss);
break;
}
case IMSG_MBOX_APPEND: {
struct imsg_mbox_append req;
- /*
- * No reply here even when refused: the literal follows
- * regardless; handle_mbox_append_end() answers.
- */
+ /* no reply: the literal follows regardless */
if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) {
log_warnx("bad IMSG_MBOX_APPEND");
break;
if (!imsg_field_valid(req.keywords,
sizeof(req.keywords), "IMSG_MBOX_APPEND.keywords"))
break;
- handle_mbox_append_begin(&req);
+ handle_mbox_append_begin(ss, &req);
break;
}
case IMSG_MBOX_APPEND_DATA: {
log_warnx("bad IMSG_MBOX_APPEND_DATA");
break;
}
- handle_mbox_append_data(buf, len);
+ handle_mbox_append_data(ss, buf, len);
break;
}
case IMSG_MBOX_APPEND_END:
- if (handle_mbox_append_end(iev) == 1)
+ if (handle_mbox_append_end(ss) == 1)
defer_command(IMSG_MBOX_APPEND_END, NULL, 0,
- NULL, 0, 0, 0, iev);
+ NULL, 0, 0, 0, ss);
break;
case IMSG_MBOX_SEARCH: {
struct imsg_mbox_search req;
struct search_node *nodes;
int ok;
- if (!require_mailbox_selected("IMSG_MBOX_SEARCH"))
+ if (!require_mailbox_selected(ss, "IMSG_MBOX_SEARCH"))
break;
- /*
- * same header+variable-body as APPEND, trailing
- * search_node[] not raw
- */
- if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
+ if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1 ||
+ req.poollen > sizeof(req.pool)) {
log_warnx("bad IMSG_MBOX_SEARCH (header)");
break;
}
if (!ok)
break;
if (nodes != NULL &&
- !search_nodes_valid(nodes, req.nnodes)) {
+ !search_nodes_valid(nodes, req.nnodes,
+ req.poollen)) {
free(nodes);
break;
}
if (handle_mbox_search(&req, nodes, req.nnodes,
- iev) == 1)
+ ss) == 1)
defer_command(IMSG_MBOX_SEARCH, &req,
sizeof(req), nodes, req.nnodes,
SEARCH_PROGRAM_MAX_NODES, sizeof(*nodes),
- iev);
+ ss);
free(nodes);
break;
}
log_warnx("bad IMSG_MBOX_STATUS");
break;
}
- if (handle_mbox_status(&req, iev) == 1)
+ if (handle_mbox_status(&req, ss) == 1)
defer_command(IMSG_MBOX_STATUS, &req,
- sizeof(req), NULL, 0, 0, 0, iev);
+ sizeof(req), NULL, 0, 0, 0, ss);
break;
}
case IMSG_MBOX_COPY: {
struct seq_range *ranges;
int ok;
- if (!require_mailbox_selected("IMSG_MBOX_COPY"))
+ if (!require_mailbox_selected(ss, "IMSG_MBOX_COPY"))
break;
- /*
- * same header+variable-body as STORE/FETCH/SEARCH,
- * trailing seq_range[]
- */
if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
log_warnx("bad IMSG_MBOX_COPY (header)");
break;
if (!ok)
break;
if (handle_mbox_copy(&req, ranges, req.nranges,
- iev) == 1)
+ ss) == 1)
defer_command(IMSG_MBOX_COPY, &req,
sizeof(req), ranges, req.nranges,
- SEQSET_MAX_RANGES, sizeof(*ranges), iev);
+ SEQSET_MAX_RANGES, sizeof(*ranges), ss);
free(ranges);
break;
}
struct seq_range *ranges;
int ok;
- if (!require_mailbox_selected("IMSG_MBOX_MOVE"))
+ if (!require_mailbox_selected(ss, "IMSG_MBOX_MOVE"))
break;
- /* same header-plus-variable-body shape as COPY above */
if (imsg_get_buf(&imsg, &req, sizeof(req)) == -1) {
log_warnx("bad IMSG_MBOX_MOVE (header)");
break;
if (!ok)
break;
if (handle_mbox_move(&req, ranges, req.nranges,
- iev) == 1)
+ ss) == 1)
defer_command(IMSG_MBOX_MOVE, &req,
sizeof(req), ranges, req.nranges,
- SEQSET_MAX_RANGES, sizeof(*ranges), iev);
+ SEQSET_MAX_RANGES, sizeof(*ranges), ss);
free(ranges);
break;
}
log_warnx("bad IMSG_MBOX_DELETE");
break;
}
- handle_mbox_delete(&req, iev);
+ handle_mbox_delete(&req, ss);
break;
}
case IMSG_MBOX_RENAME: {
log_warnx("bad IMSG_MBOX_RENAME");
break;
}
- handle_mbox_rename(&req, iev);
+ handle_mbox_rename(&req, ss);
break;
}
case IMSG_MBOX_SUBSCRIBE: {
}
imsg_free(&imsg);
- /*
- * One command's view of cur/ does not outlive the command;
- * see mime.c. The next request reads the directory again
- * rather than trusting what this one saw. A paused FETCH
- * keeps its snapshot: its command has not finished, and
- * rebuilding it per batch would undo what it is for.
- */
- if (!fetch_walk_paused())
- cur_snapshot_discard();
+ /* cur/'s snapshot does not outlive the command */
+ if (!fetch_walk_paused(ss) && !search_walk_paused(ss))
+ cur_snapshot_discard(&ss->cur_snap);
}
imsgev_rearm_read(iev);
(void)fd;
}
-/* IMSG_STORE_SHUTDOWN arrives directly from listener; no round-trip parent */
-__dead void
-store_shutdown(void)
+static void
+store_detach(struct store_session *ss)
{
- log_debug("session %u: store shutting down", session_id);
- if (deferred.active)
- evtimer_del(&deferred.ev);
- fetch_walk_abort();
- append_abort();
+ log_debug("session %u: detached", ss->id);
+ if (ss->deferred.active)
+ evtimer_del(&ss->deferred.ev);
+ fetch_walk_abort(ss);
+ search_walk_abort(ss);
+ append_abort(ss);
+ cur_snapshot_discard(&ss->cur_snap);
+ idle_baseline_reset(ss);
+ if (ss->mailbox_dir_fd != -1)
+ close(ss->mailbox_dir_fd);
+ event_del(&ss->iev.ev);
+ close(ss->iev.ibuf.fd);
+ imsgbuf_clear(&ss->iev.ibuf);
+ TAILQ_REMOVE(&sessions, ss, entry);
+ free(ss);
+}
+
+static void
+store_end(struct store_session *ss)
+{
+ store_detach(ss);
+ if (parent_gone && TAILQ_EMPTY(&sessions))
+ store_exit();
+}
+
+static __dead void
+store_exit(void)
+{
+ struct store_session *ss;
+
+ while ((ss = TAILQ_FIRST(&sessions)) != NULL)
+ store_detach(ss);
+ log_debug("store: shutting down");
exit(0);
}
blob - /dev/null
blob + f514c63455866a48bef14a150964c849578eced7 (mode 644)
--- /dev/null
+++ src/search_match.c
+/* $OpenIMAPD$ */
+
+/*
+ * Copyright (c) 2026 David Williams <dhw@openimapd.dev>
+ *
+ * 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.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+
+#include <errno.h>
+#include <event.h>
+#include <imsg.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "imapd.h"
+#include "log.h"
+#include "store_internal.h"
+
+#define SEARCH_READ_BLOCK 65536
+
+static const char *const month_names[12] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+};
+
+static int ascii_lower(int);
+static int ci_contains(const char *, size_t, const char *, size_t);
+static int b64_value(int);
+static int hex_value(int);
+static size_t decode_word(const char *, size_t, char *);
+static size_t encoded_word_len(const char *, size_t);
+static char *decode_value(const char *, size_t, size_t *);
+static int date_fws(int);
+static int date_field_day(const char *, size_t, int64_t *);
+static int read_header(int, const char *, char **, size_t *);
+static char *unescape_name(const char *, size_t, size_t *);
+static int match_addresses(const char *, size_t, const char *, size_t);
+static int match_leaf(const char *, size_t,
+ const struct imsg_parser_leaf *, const char *);
+
+/* RFC 9051 SS6.4.4 folds the ASCII range only, byte by byte */
+static int
+ascii_lower(int c)
+{
+ return ((c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c);
+}
+
+static int
+ci_contains(const char *hay, size_t haylen, const char *needle,
+ size_t needlelen)
+{
+ size_t i, j;
+
+ if (needlelen == 0)
+ return (1);
+ if (needlelen > haylen)
+ return (0);
+ for (i = 0; i + needlelen <= haylen; i++) {
+ for (j = 0; j < needlelen; j++) {
+ if (ascii_lower((unsigned char)hay[i + j]) !=
+ ascii_lower((unsigned char)needle[j]))
+ break;
+ }
+ if (j == needlelen)
+ return (1);
+ }
+ return (0);
+}
+
+static int
+b64_value(int c)
+{
+ if (c >= 'A' && c <= 'Z')
+ return (c - 'A');
+ if (c >= 'a' && c <= 'z')
+ return (c - 'a' + 26);
+ if (c >= '0' && c <= '9')
+ return (c - '0' + 52);
+ if (c == '+')
+ return (62);
+ if (c == '/')
+ return (63);
+ return (-1);
+}
+
+static int
+hex_value(int c)
+{
+ if (c >= '0' && c <= '9')
+ return (c - '0');
+ if (c >= 'A' && c <= 'F')
+ return (c - 'A' + 10);
+ if (c >= 'a' && c <= 'f')
+ return (c - 'a' + 10);
+ return (-1);
+}
+
+/* RFC 2047 SS2 encoded-word */
+static size_t
+decode_word(const char *w, size_t len, char *out)
+{
+ const char *p, *end = w + len - 2;
+ size_t n = 0;
+ unsigned int bits = 0;
+ int enc, nbits = 0, v, hi, lo;
+
+ if (len < 8 || w[0] != '=' || w[1] != '?' || w[len - 2] != '?' ||
+ w[len - 1] != '=')
+ return (0);
+ if ((p = memchr(w + 2, '?', len - 4)) == NULL || p == w + 2)
+ return (0);
+ if (p + 3 > end || p[2] != '?')
+ return (0);
+ enc = ascii_lower((unsigned char)p[1]);
+ for (p += 3; p < end; p++) {
+ if (enc == 'b') {
+ if (*p == '=')
+ break;
+ if ((v = b64_value((unsigned char)*p)) == -1)
+ return (0);
+ /* 12 bits hold what one character can leave */
+ bits = ((bits << 6) | (unsigned int)v) & 0xfff;
+ if ((nbits += 6) >= 8) {
+ nbits -= 8;
+ out[n++] = (char)((bits >> nbits) & 0xff);
+ }
+ } else if (enc == 'q') {
+ if (*p == '_')
+ out[n++] = ' ';
+ else if (*p == '=') {
+ if (p + 2 >= end ||
+ (hi = hex_value((unsigned char)p[1])) ==
+ -1 ||
+ (lo = hex_value((unsigned char)p[2])) == -1)
+ return (0);
+ out[n++] = (char)(hi << 4 | lo);
+ p += 2;
+ } else
+ out[n++] = *p;
+ } else
+ return (0);
+ }
+ return (n);
+}
+
+static size_t
+encoded_word_len(const char *s, size_t len)
+{
+ size_t i, cs;
+
+ if (len < 8 || s[0] != '=' || s[1] != '?')
+ return (0);
+ for (i = 2; i < len && s[i] != '?'; i++) {
+ if (s[i] == ' ' || s[i] == '\t')
+ return (0);
+ }
+ cs = i;
+ if (cs == 2 || cs + 3 >= len || s[cs + 2] != '?')
+ return (0);
+ for (i = cs + 3; i + 1 < len; i++) {
+ if (s[i] == ' ' || s[i] == '\t')
+ return (0);
+ if (s[i] == '?' && s[i + 1] == '=')
+ return (i + 2);
+ }
+ return (0);
+}
+
+/* RFC 5322 SS2.2.3 unfolding and RFC 2047 SS6.1 decoding */
+static char *
+decode_value(const char *v, size_t vlen, size_t *outlen)
+{
+ char *out, *unf;
+ size_t i, n = 0, ulen = 0, wlen, dn, gap = 0;
+ int after_word = 0;
+
+ if ((unf = malloc(vlen + 1)) == NULL)
+ return (NULL);
+ for (i = 0; i < vlen; i++) {
+ if (v[i] != '\r' && v[i] != '\n')
+ unf[ulen++] = v[i];
+ }
+ /* decoding never lengthens a word, so ulen bounds the output */
+ if ((out = malloc(ulen + 1)) == NULL) {
+ free(unf);
+ return (NULL);
+ }
+ for (i = 0; i < ulen; ) {
+ if (unf[i] == ' ' || unf[i] == '\t') {
+ out[n++] = unf[i++];
+ gap++;
+ continue;
+ }
+ /* a word that fails leaves octets past n, copied over below */
+ if ((wlen = encoded_word_len(unf + i, ulen - i)) > 0 &&
+ (dn = decode_word(unf + i, wlen, out + n)) > 0) {
+ /* between two encoded-words, the gap goes */
+ if (after_word) {
+ memmove(out + n - gap, out + n, dn);
+ n -= gap;
+ }
+ n += dn;
+ i += wlen;
+ after_word = 1;
+ gap = 0;
+ continue;
+ }
+ out[n++] = unf[i++];
+ after_word = 0;
+ gap = 0;
+ }
+ free(unf);
+ *outlen = n;
+ return (out);
+}
+
+/* RFC 5322 SS3.3 allows FWS, folds included, between a date's tokens */
+static int
+date_fws(int c)
+{
+ return (c == ' ' || c == '\t' || c == '\r' || c == '\n');
+}
+
+/* RFC 9051 SS6.4.4: the day only, ignoring time and zone */
+static int
+date_field_day(const char *v, size_t vlen, int64_t *out)
+{
+ struct tm tm;
+ const char *p = v, *end = v + vlen;
+ int day = 0, year = 0, mon, nd = 0, ny = 0;
+ time_t t;
+
+ while (p < end && date_fws((unsigned char)*p))
+ p++;
+ /* RFC 5322 SS3.3: day-of-week is optional */
+ if (p < end && ascii_lower((unsigned char)*p) >= 'a' &&
+ ascii_lower((unsigned char)*p) <= 'z') {
+ while (p < end && *p != ',')
+ p++;
+ if (p == end)
+ return (-1);
+ p++;
+ while (p < end && date_fws((unsigned char)*p))
+ p++;
+ }
+ for (; p < end && *p >= '0' && *p <= '9' && nd < 2; p++, nd++)
+ day = day * 10 + (*p - '0');
+ if (nd == 0 || p == end || !date_fws((unsigned char)*p))
+ return (-1);
+ while (p < end && date_fws((unsigned char)*p))
+ p++;
+ if (end - p < 3)
+ return (-1);
+ for (mon = 0; mon < 12; mon++) {
+ if (strncasecmp(p, month_names[mon], 3) == 0)
+ break;
+ }
+ if (mon == 12)
+ return (-1);
+ p += 3;
+ if (p == end || !date_fws((unsigned char)*p))
+ return (-1);
+ while (p < end && date_fws((unsigned char)*p))
+ p++;
+ for (; p < end && *p >= '0' && *p <= '9' && ny < 4; p++, ny++)
+ year = year * 10 + (*p - '0');
+ if (ny < 2 || day < 1 || day > 31)
+ return (-1);
+ if (ny == 2)
+ year += (year < 50) ? 2000 : 1900;
+ else if (ny == 3)
+ year += 1900;
+ if (year < 1970)
+ return (-1);
+
+ memset(&tm, 0, sizeof(tm));
+ tm.tm_mday = day;
+ tm.tm_mon = mon;
+ tm.tm_year = year - 1900;
+ if ((t = timegm(&tm)) == (time_t)-1)
+ return (-1);
+ *out = (int64_t)t;
+ return (0);
+}
+
+static int
+read_header(int fd, const char *basename, char **buf_out, size_t *len_out)
+{
+ char *buf = NULL, *nbuf;
+ size_t len = 0, size = 0, hdrend, limit;
+ ssize_t n;
+
+ /* one octet past the cap tells a header at it from one over it */
+ limit = (size_t)bodystructure_read_max + 1;
+ for (;;) {
+ if (len == size) {
+ if (size == limit)
+ break;
+ size = (limit - size > SEARCH_READ_BLOCK) ?
+ size + SEARCH_READ_BLOCK : limit;
+ if ((nbuf = realloc(buf, size)) == NULL) {
+ log_warn("session %u: realloc SEARCH header",
+ session_id);
+ free(buf);
+ return (-1);
+ }
+ buf = nbuf;
+ }
+ if ((n = read(fd, buf + len, size - len)) == -1) {
+ if (errno == EINTR)
+ continue;
+ log_warn("session %u: read message header (%s)",
+ session_id, basename);
+ free(buf);
+ return (-1);
+ }
+ if (n == 0)
+ break;
+ len += (size_t)n;
+ if (find_header_body_split(buf, len, &hdrend) == 0) {
+ len = hdrend;
+ break;
+ }
+ }
+ if (len > bodystructure_read_max) {
+ log_warnx("session %u: message %s has no header end within "
+ "%u bytes, SEARCH cannot check it", session_id, basename,
+ bodystructure_read_max);
+ free(buf);
+ return (-1);
+ }
+ *buf_out = buf;
+ *len_out = len;
+ return (0);
+}
+
+/* An RFC 5322 SS3.2.4 quoted string's content with its quoted-pairs undone */
+static char *
+unescape_name(const char *v, size_t vlen, size_t *outlen)
+{
+ char *out;
+ size_t i, n = 0;
+
+ if ((out = malloc(vlen + 1)) == NULL)
+ return (NULL);
+ for (i = 0; i < vlen; i++) {
+ if (v[i] == '\\' && i + 1 < vlen)
+ i++;
+ out[n++] = v[i];
+ }
+ *outlen = n;
+ return (out);
+}
+
+/* RFC 9051 SS6.4.4: addresses as ENVELOPE shows them */
+static int
+match_addresses(const char *v, size_t vlen, const char *needle,
+ size_t needlelen)
+{
+ const char *tok, *name, *mbox, *host;
+ char *unf, *spec, *raw, *dec;
+ size_t i, ulen = 0, pos = 0, toklen, namelen, mboxlen;
+ size_t hostlen, rawlen, declen;
+ int hit = 0;
+
+ /* RFC 5322 SS2.2.3: unfolding drops CR and LF */
+ if ((unf = malloc(vlen + 1)) == NULL)
+ return (-1);
+ for (i = 0; i < vlen; i++) {
+ if (v[i] != '\r' && v[i] != '\n')
+ unf[ulen++] = v[i];
+ }
+
+ while (hit == 0 && address_list_next(unf, ulen, &pos, &tok,
+ &toklen) == 1) {
+ if (address_split(tok, toklen, &name, &namelen, &mbox,
+ &mboxlen, &host, &hostlen) == -1)
+ continue;
+ if ((spec = malloc(mboxlen + 1 + hostlen)) == NULL) {
+ hit = -1;
+ break;
+ }
+ memcpy(spec, mbox, mboxlen);
+ spec[mboxlen] = '@';
+ memcpy(spec + mboxlen + 1, host, hostlen);
+ hit = ci_contains(spec, mboxlen + 1 + hostlen, needle,
+ needlelen);
+ free(spec);
+ if (hit || name == NULL)
+ continue;
+ if ((raw = unescape_name(name, namelen, &rawlen)) == NULL) {
+ hit = -1;
+ break;
+ }
+ dec = decode_value(raw, rawlen, &declen);
+ free(raw);
+ if (dec == NULL) {
+ hit = -1;
+ break;
+ }
+ hit = ci_contains(dec, declen, needle, needlelen);
+ free(dec);
+ }
+ free(unf);
+ return (hit);
+}
+
+static int
+match_leaf(const char *hdr, size_t hdrlen,
+ const struct imsg_parser_leaf *l, const char *pool)
+{
+ const char *name, *val, *want;
+ size_t namelen, vallen, wantlen, off = 0, dlen;
+ char *dec;
+ int64_t day;
+ int hit = 0, date = 0, addr = 0;
+
+ switch (l->op) {
+ case SEARCH_OP_SUBJECT:
+ want = "Subject";
+ wantlen = 7;
+ break;
+ case SEARCH_OP_HEADER:
+ want = pool + l->name_off;
+ wantlen = l->name_len;
+ break;
+ case SEARCH_OP_SENTBEFORE:
+ case SEARCH_OP_SENTON:
+ case SEARCH_OP_SENTSINCE:
+ want = "Date";
+ wantlen = 4;
+ date = 1;
+ break;
+ case SEARCH_OP_FROM:
+ want = "From";
+ wantlen = 4;
+ addr = 1;
+ break;
+ case SEARCH_OP_TO:
+ want = "To";
+ wantlen = 2;
+ addr = 1;
+ break;
+ case SEARCH_OP_CC:
+ want = "Cc";
+ wantlen = 2;
+ addr = 1;
+ break;
+ case SEARCH_OP_BCC:
+ want = "Bcc";
+ wantlen = 3;
+ addr = 1;
+ break;
+ default:
+ return (0);
+ }
+
+ while (!hit && header_next_field(hdr, hdrlen, &off, &name, &namelen,
+ &val, &vallen) == 1) {
+ if (namelen != wantlen || strncasecmp(name, want, wantlen) != 0)
+ continue;
+ if (date) {
+ /* the first Date: decides; RFC 5322 SS3.6 allows one */
+ if (date_field_day(val, vallen, &day) == -1)
+ return (0);
+ if (l->op == SEARCH_OP_SENTBEFORE)
+ return (day < l->num);
+ if (l->op == SEARCH_OP_SENTON)
+ return (day == l->num);
+ return (day >= l->num);
+ }
+ if (addr) {
+ hit = match_addresses(val, vallen, pool + l->str_off,
+ l->str_len);
+ if (hit == -1)
+ return (-1);
+ continue;
+ }
+ if ((dec = decode_value(val, vallen, &dlen)) == NULL)
+ return (-1);
+ hit = ci_contains(dec, dlen, pool + l->str_off, l->str_len);
+ free(dec);
+ }
+ return (hit);
+}
+
+int
+search_match(int fd, const char *basename,
+ const struct imsg_parser_leaf *leaves, uint32_t nleaves,
+ const char *pool, char **buf_out, uint32_t *len_out)
+{
+ char *hdr, *out;
+ size_t hdrlen;
+ uint32_t i;
+ int m;
+
+ *buf_out = NULL;
+ *len_out = 0;
+ if (nleaves == 0 || read_header(fd, basename, &hdr, &hdrlen) == -1)
+ return (-1);
+ if ((out = malloc(nleaves)) == NULL) {
+ free(hdr);
+ return (-1);
+ }
+ for (i = 0; i < nleaves; i++) {
+ if ((m = match_leaf(hdr, hdrlen, &leaves[i], pool)) == -1) {
+ free(out);
+ free(hdr);
+ return (-1);
+ }
+ out[i] = (char)m;
+ }
+ free(hdr);
+ *buf_out = out;
+ *len_out = nleaves;
+ return (0);
+}
blob - 9631a04f8feb7ca94034ebea550d884825269adc
blob + 7e705ce2ffaab5a8a2440376b5d57698a08f9620
--- src/store_cmd.c
+++ src/store_cmd.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * store_cmd.c: STORE/EXPUNGE/CLOSE/UNSELECT/IDLE/COPY/MOVE/UID command-select
- * handlers and their async completion paths.
- */
#include <sys/types.h>
#include <sys/queue.h>
int
cmd_idle(struct session *s, const char *tag, char *args)
{
- /* RFC 2177 takes no args, like other zero-arg commands */
+ /* RFC 9051 SS6.3.13: IDLE takes no arguments */
(void)args;
if (strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >=
if (s->state == SESSION_SELECTED) {
session_request_idle_refresh(s, 1); /* seeds the baseline */
- /* and keeps it current */
session_idle_poll_arm(s);
}
return (1);
}
-/* RFC 9051 SS6.4.1: CLOSE removes \Deleted, no untagged EXPUNGE (silent=1). */
+/* RFC 9051 SS6.4.1 CLOSE: a silent EXPUNGE */
int
cmd_close(struct session *s, const char *tag, char *args)
{
return session_request_expunge(s, tag, 1, 0, NULL, 0);
}
-/* RFC 9051 SS6.4.2: like CLOSE, but removes nothing; a local state change. */
+/* RFC 9051 SS6.4.2 UNSELECT */
int
cmd_unselect(struct session *s, const char *tag, char *args)
{
return (1);
}
-/* SS6.4.3: sends real IMSG_MBOX_EXPUNGE via session_request_expunge(). */
+/* RFC 9051 SS6.4.3 EXPUNGE */
int
cmd_expunge(struct session *s, const char *tag, char *args)
{
return session_request_expunge(s, tag, 0, 0, NULL, 0);
}
-/* Parses store-att-flags (paren or bare) into a sysflags bitmap + keywords. */
int
parse_store_flags(char *flagspec, uint32_t *sysflags_out, char *keywords_out,
size_t keywords_out_size, const char **errmsg)
p[len - 1] = '\0';
p++;
}
- /*
- * Empty flag-list is valid ABNF; SET (bare) may use it
- * (cmd_store_cmd()).
- */
+ /* an empty flag-list is valid */
if (*p == '\0')
return (0);
return (-2);
}
- /*
- * No RFC flag-list cap; checked explicitly, not via strlcat(3)
- * truncation.
- */
+ /* no flag-list cap in the RFC; checked, not truncated */
if (!first) {
if (strlcat(keywords_out, ",", keywords_out_size) >=
keywords_out_size) {
return (0);
}
-/*
- * RFC 7162 SS3.1.3: only UNCHANGEDSINCE implemented; value may be 0, so
- * has_unchangedsince flags it, not != 0.
- */
+/* RFC 7162 SS3.1.3 UNCHANGEDSINCE; 0 is legal */
int
parse_store_modifiers(char *modspec, struct imsg_mbox_store *req,
const char **errmsg)
"mod-sequence value";
return (-1);
}
- /*
- * RFC 7162 SS7: mod-sequence-valzer allows 0 here, but
- * strtoull(3) accepts a leading sign, so
- * "UNCHANGEDSINCE -1" became ULLONG_MAX and silently
- * turned a conditional STORE unconditional -- must be
- * rejected per RFC 7162 SS3.1.3.
- */
+ /* strtoull(3) accepts a sign */
if (*valtok < '0' || *valtok > '9') {
*errmsg = "invalid UNCHANGEDSINCE mod-sequence";
return (-1);
return (0);
}
-/* SS6.4.6: store-modifiers *lead* here, unlike FETCH's trailing ones. */
+/* RFC 9051 SS6.4.6: store-modifiers lead */
int
cmd_store_cmd(struct session *s, const char *tag, char *args)
{
return store_do(s, tag, args, 0);
}
-/*
- * Shared body for cmd_store_cmd()/cmd_uid()'s STORE branch; s->cmd_by_uid set
- * so the STORE echo includes UID (SS6.4.9).
- */
int
store_do(struct session *s, const char *tag, char *args, int by_uid)
{
}
p++;
}
- /* p points at the matching ')' for modspec (== args) */
modspec = args;
p++; /* just past ')' */
if (*p == '\0') {
}
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: EXAMINE is read-only */
session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
"(selected via EXAMINE)");
return (1);
}
if (s->store_iev == NULL) {
- /* ST_SELECTED requires store_iev to already be wired. */
log_warnx("session %u: %s with no store channel wired",
s->id, cmdname);
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return (1);
}
- /*
- * req already memset(3)'d; has_unchangedsince set earlier, kept as is
- * here.
- */
req.nranges = nranges;
req.mode = mode;
req.silent = silent;
return (1);
}
- /* RFC 7162 SS3.1: UNCHANGEDSINCE is a CONDSTORE-enabling command. */
+ /* RFC 7162 SS3.1: UNCHANGEDSINCE enables CONDSTORE */
if (req.has_unchangedsince)
session_condstore_enable(s);
- /*
- * defensive cleanup of a previous STORE's leftovers; shouldn't actually
- * find anything here, same reasoning (and same four fields plus flag)
- * as search_dispatch()'s own pre-command reset
- */
free(s->store_modified);
s->store_modified = NULL;
s->store_modified_n = 0;
return (1);
}
-/*
- * RFC 9051 SS6.4.7/SS6.4.8: invalid mailbox name is BAD; nonexistent is
- * TRYCREATE via res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX.
- */
+/* RFC 9051 SS6.4.7 COPY and SS6.4.8 MOVE */
int
copy_move_dispatch(struct session *s, const char *tag, char *args,
int by_uid, int is_move)
}
if (is_move && s->mbox_readonly) {
- /*
- * MOVE removes msgs (SS6.4.8), forbidden by SS6.3.3 on
- * EXAMINE'd; COPY OK.
- */
+ /* RFC 9051 SS6.3.3: MOVE removes messages; COPY may proceed */
session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
"(selected via EXAMINE)");
return (1);
}
if (s->store_iev == NULL) {
- /* ST_SELECTED requires store_iev to already be wired. */
log_warnx("session %u: %s with no store channel wired",
s->id, cmdname);
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return copy_move_dispatch(s, tag, args, 0, 1);
}
-/* SS6.4.9 UID; dispatches to the same *_dispatch() bodies, by_uid=1. */
+/* RFC 9051 SS6.4.9 UID */
int
cmd_uid(struct session *s, const char *tag, char *args)
{
return (1);
}
-/* SS7.5.1 untagged EXPUNGE; RFC7162 SS3.2.10.2 sends VANISHED <uid> instead. */
+/* RFC 9051 SS7.5.1 EXPUNGE, or RFC 7162 SS3.2.10.2 VANISHED */
void
session_send_expunge_response(struct session *s,
const struct imsg_mbox_expunged *exp)
session_untagged(s, buf);
}
-/* RFC 7162 SS3.2.6: VANISHED (EARLIER) range written now; SELECT buffers */
+/* RFC 7162 SS3.2.6 VANISHED (EARLIER) */
void
session_handle_fetch_vanished(struct session *s,
const struct imsg_mbox_select_vanished *v)
session_untagged(s, buf);
}
-/* Shared by expunge/close/UID EXPUNGE: '*' is always a seqno, even for UID. */
+/* '*' is always a sequence number here */
int
session_request_expunge(struct session *s, const char *tag, int is_close,
int by_uid, const struct seq_range *ranges, uint32_t nranges)
if (s->mbox_readonly) {
if (is_close) {
- /*
- * SS6.4.1: EXAMINE'd, skip round trip; just deselect
- * and reply OK.
- */
+ /* RFC 9051 SS6.4.1: read-only, so just deselect */
s->state = SESSION_AUTHENTICATED;
session_reply(s, tag, "OK", "CLOSE completed");
return (1);
}
- /*
- * Unlike CLOSE, EXPUNGE has no read-only exception; SS6.3.3
- * applies here.
- */
+ /* RFC 9051 SS6.3.3: no read-only exception for EXPUNGE */
session_reply(s, tag, "NO", "[CANNOT] Mailbox is read-only "
"(selected via EXAMINE)");
return (1);
}
if (s->store_iev == NULL) {
- /* ST_SELECTED requires store_iev to already be wired. */
log_warnx("session %u: %s with no store channel wired",
s->id, cmdname);
session_reply(s, tag, "NO", "[SERVERBUG] internal error");
return (1);
}
-/*
- * cmd_uid()'s EXPUNGE branch: requires a sequence set (plain EXPUNGE takes
- * none); never reached with is_close set.
- */
int
uid_expunge_dispatch(struct session *s, const char *tag, const char *args)
{
return session_request_expunge(s, tag, 0, 1, ranges, nranges);
}
-/*
- * Appends one MODIFIED entry (RFC 7162 SS3.1.3) to s->store_modified, growing
- * by doubling from 16; a failed grow sets s->store_modified_alloc_failed and
- * stops collecting, since SS3.1.3's set must list every message that failed
- * UNCHANGEDSINCE and session_handle_mbox_result() refuses to send a short one
- * (the early return below then keeps one failure from logging once per
- * remaining message, as session_handle_mbox_search_match() does).
- */
+/* RFC 7162 SS3.1.3 MODIFIED entry */
void
session_handle_store_modified(struct session *s,
struct imsg_mbox_store_modified *m)
s->store_modified_cap = newcap;
}
- /*
- * SS3.1.3: MODIFIED lists UIDs for UID STORE, else seqnos
- * (s->cmd_by_uid).
- */
s->store_modified[s->store_modified_n++] =
s->cmd_by_uid ? m->uid : m->seqno;
}
-/*
- * Appends one "lo" or "lo:hi" token, with a separating comma unless it is the
- * first, and refuses rather than truncates when it would not fit.
- * format_seq_list() and format_range_list() differ only in where lo and hi come
- * from; this is everything else they used to spell out twice, including the
- * budget arithmetic that decides whether a list is complete -- the thing a
- * caller must get right, and the reason both are fuzzed. Returns the new
- * written length, or (size_t)-1 if the token did not fit, leaving buf as it
- * was. *truncated is set only for a budget refusal, not for a snprintf(3)
- * failure, which is what both callers did before.
- */
+/* refuses rather than truncates when it would not fit */
static size_t
append_range_token(char *buf, size_t bufsize, size_t written, int *first,
uint32_t lo, uint32_t hi, int *truncated)
return (written);
}
-/* Formats nums as comma-sep bare/lo:hi ranges (SS7.3.4 ESEARCH); pre-sorted. */
+/* RFC 9051 SS7.3.4 sequence list; input sorted */
size_t
format_seq_list(char *buf, size_t bufsize, const uint32_t *nums, uint32_t n,
int *truncated)
uint32_t j = i + 1;
size_t w;
- /*
- * this function's compaction: collapse an ascending run into a
- * lo:hi token
- */
while (j < n && nums[j] == end + 1) {
end = nums[j];
j++;
return (written);
}
-/* RFC7162 sibling of format_seq_list() for VANISHED; ranges pre-compacted. */
+/* RFC 7162 VANISHED ranges, already compacted */
size_t
format_range_list(char *buf, size_t bufsize,
const struct vanished_range *ranges,
*truncated = 0;
buf[0] = '\0';
- /*
- * no compaction here, unlike format_seq_list(): store.c precompacts
- * these
- */
for (i = 0; i < n; i++) {
size_t w;
return (written);
}
-/*
- * Worst-case sizing for COPYUID's two UID sets ("4294967295" plus separator,
- * matching store_ipc.c/search_cmd.c), plus the tag/"OK [COPYUID
- * "/uidvalidity/cmdname/CRLF wrapper.
- */
#define COPYUID_PER_ENTRY 11
#define COPYUID_WRAPPER_MAX 160
-/*
- * Terminal COPY/MOVE reply; COPYUID via format_seq_list() (SS7.1); MOVE emits
- * EXPUNGE/VANISHED before the tagged OK.
- */
+/* RFC 9051 SS6.4.7/SS6.4.8 terminal reply */
void
session_finish_copy_or_move(struct session *s,
const struct imsg_mbox_result *res)
if (res->error != MBOX_OP_OK || s->copy_alloc_failed) {
char text[48];
- /*
- * SS6.4.7/8: destname valid, not found: TRYCREATE (as
- * imsg_mbox_appended).
- */
+ /* RFC 9051 SS6.4.7: TRYCREATE */
if (!s->copy_alloc_failed &&
res->error == MBOX_OP_ERR_NO_SUCH_MAILBOX)
snprintf(text, sizeof(text), "[TRYCREATE] no such "
goto cleanup;
}
- /*
- * RFC 7162 SS3.1.2.1: cache post-op HIGHESTMODSEQ (as for
- * STORE/EXPUNGE) for session_condstore_enable()'s later use.
- */
s->mbox_highestmodseq = res->highestmodseq;
size_t listsize, textsize;
int truncated, n, sent = 0;
- /*
- * Heap-allocated and worst-case sized (like
- * session_finish_search()'s ESEARCH ALL list): fixed buffers
- * here previously truncated independently, misaligning RFC 9051
- * SS7.1's COPYUID UID mapping and risking overflow-truncated
- * responses.
- */
listsize = (size_t)s->copy_n * COPYUID_PER_ENTRY + 1;
textsize = 2 * listsize + COPYUID_WRAPPER_MAX;
log_warnx("session %u: COPYUID dest list "
"truncated", s->id);
- /*
- * MOVE's COPYUID untagged (OK follows EXPUNGEs); COPY
- * rides its own OK.
- */
+ /* MOVE sends COPYUID untagged, before the EXPUNGEs */
if (s->cmd_is_move)
n = snprintf(text, textsize,
"* OK [COPYUID %u %s %s]\r\n",
session_reply(s, s->pending_tag, "OK", "Done");
} else if (!sent) {
- /*
- * SS6.4.7: COPYUID is a SHOULD; omitting is legal,
- * truncating is not.
- */
+ /* RFC 9051 SS6.4.7: a SHOULD; never truncated */
char fallback[64];
snprintf(fallback, sizeof(fallback), "%s completed",
blob - 5ae316a2795414fad66a9ceb21cd3dbf5f43c4dc
blob + 09d189f53097622242549e88eafd6fa569c58af5
--- src/store_internal.h
+++ src/store_internal.h
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * store_internal.h, internal, store-process-only shared declarations.
- */
-
#ifndef STORE_INTERNAL_H
#define STORE_INTERNAL_H
#include <sys/types.h>
+#include <sys/queue.h>
#include <stdint.h>
struct mbox_index {
uint32_t uidvalidity;
uint32_t uidnext;
- 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()). Always
- * supported, so NOMODSEQ (RFC 7162
- * SS3.1.2.2) is unreachable here. */
- /* Set by index_load() when it found no index and invented a header: */
- /* this UIDVALIDITY is issued but not yet written down, so every */
- /* caller holding an exclusive lock must persist it before returning. */
+ uint64_t highestmodseq;
+ /* no index was found: persist this UIDVALIDITY before unlocking */
int fresh;
- /* raw "UID:basename:keywords:MODSEQ" lines, no trailing newline, */
- /* one malloc(3) each; see index_parse_line() */
+ /* raw "UID:basename:keywords:MODSEQ" lines */
char **lines;
size_t nlines;
size_t cap;
};
-/* One index line, parsed, so each caller need not hand-roll the strchr() */
-/* chain. basename and keywords are copies, so the line may be mutated or */
-/* discarded afterwards. A three-field pre-CONDSTORE line parses with */
-/* modseq defaulted to 1. */
+/* a three-field pre-CONDSTORE line parses with modseq 1 */
struct index_rec {
uint32_t uid;
char basename[512];
uint64_t modseq;
};
-/* One message staged by stage_copy_messages() for COPY/MOVE: read fully */
-/* into memory from the source mailbox, not yet written. */
struct copy_staged {
uint32_t src_uid;
uint32_t dest_uid;
char basename[256];
char keywords[512];
uint32_t sysflags;
- void *body;
- size_t bodylen;
+ char *srcpath;
+ off_t srcsize;
};
-/* per message */
-#define COPY_STAGE_MSG_MAX ((uint64_t)64 * 1024 * 1024)
-/* per operation */
-#define COPY_STAGE_TOTAL_MAX ((uint64_t)512 * 1024 * 1024)
+/* a FETCH walk, paused while its output drains */
+struct store_fetch_walk {
+ int active;
+ struct mbox_index idx;
+ struct imsg_mbox_fetch req;
+ struct seq_range resolved[SEQSET_MAX_RANGES];
+ uint32_t nresolved;
+ uint32_t max_hi;
+ uint32_t next; /* index line to resume at */
+ uint32_t sent;
+ size_t composed;
+ int fds;
+ int wait_parser; /* paused until one comes */
+ int no_parser; /* none to be had: go without */
+};
-/* Globals shared across the files this process's source is split into
- * (definitions live in store.c's core).
- */
+/* RFC 9051 SS6.4.4 SEARCH, yielding every SEARCH_YIELD_REQUESTS */
+struct store_search_walk {
+ int active;
+ int wait_parser; /* paused until one comes */
+ struct event yield_ev;
+ struct mbox_index idx;
+ struct search_node nodes[SEARCH_PROGRAM_MAX_NODES];
+ uint32_t nnodes;
+ uint32_t leafidx[SEARCH_PROGRAM_MAX_NODES];
+ uint32_t nleaves;
+ uint32_t poollen;
+ char extra[SEARCH_PROGRAM_MAX_NODES *
+ sizeof(struct imsg_parser_leaf) +
+ SEARCH_OPERANDS_MAX];
+ size_t extralen;
+ uint32_t next; /* index line to resume at */
+ uint32_t sent;
+ uint32_t asked;
+};
+
+/* two stat(2) calls, no lock: "." and "new" */
+struct store_idle_probe {
+ int valid;
+ ino_t dir_ino;
+ ino_t new_ino;
+ struct timespec dir_mtim;
+ struct timespec new_mtim;
+};
+
+/* UIDs last reported, so a change costs one imsg, not one per message */
+struct store_idle_baseline {
+ uint32_t *uids;
+ size_t n;
+ uint64_t modseq; /* highest reported; above it is news */
+ int valid;
+};
+
+/* RFC 9051 SS6.3.12 APPEND; a failure is held until END */
+struct store_append {
+ int active;
+ int failed;
+ enum mbox_op_error error;
+ struct imsg_mbox_append req;
+ uint64_t remaining;
+ int tfd;
+ int tmpfd;
+ char basename[256];
+ char tmppath[300];
+};
+
+/* cur/ read once per command; discarded when the command is done */
+struct cur_snapshot {
+ int dfd; /* directory described, -1 when empty */
+ int failed;
+ char **names;
+ size_t n;
+};
+
+/* one command waiting on the index lock; re-run from the top */
+struct store_deferred {
+ int active;
+ uint32_t type;
+ struct event ev;
+ struct timespec give_up_at;
+ unsigned int wait_ms;
+ union {
+ struct imsg_mbox_store store;
+ struct imsg_mbox_expunge expunge;
+ struct imsg_mbox_fetch fetch;
+ struct imsg_mbox_search search;
+ struct imsg_mbox_select select;
+ struct imsg_mbox_status status;
+ struct imsg_mbox_copy copy;
+ } req;
+ union {
+ struct seq_range ranges[SEQSET_MAX_RANGES];
+ struct search_node nodes[SEARCH_PROGRAM_MAX_NODES];
+ } elts;
+ uint32_t nelts;
+};
+
+struct store_session {
+ uint32_t id;
+ TAILQ_ENTRY(store_session) entry;
+ struct imsgev iev;
+
+ /* a failed SELECT leaves nothing selected (RFC 9051 SS6.3.2) */
+ int mailbox_selected;
+
+ /* "" for INBOX */
+ char selected_mailbox[MBOX_NAME_MAX];
+
+ /* lookups relative to it are unveiled (sys/kern/vfs_lookup.c) */
+ int mailbox_dir_fd;
+
+ struct cur_snapshot cur_snap;
+ struct store_deferred deferred;
+ struct store_fetch_walk fetch;
+ struct store_search_walk search;
+ struct store_idle_probe idle_probe;
+ struct store_idle_baseline idle_baseline;
+ struct store_append append;
+};
+
extern uint32_t session_id;
-extern uint32_t append_counter; /* per-store-child monotonic counter
- * feeding the maildir basename uniquer
- * (see handle_mbox_append_begin()); shared with
- * handle_mbox_copy() so a copy's minted
- * basename can't collide with an APPEND's */
-/*
- * The store's own gate: 1 iff this session's most recent SELECT succeeded
- * and no CLOSE has followed. Needed separately from selected_mailbox,
- * whose "" cannot distinguish "nothing selected" from "INBOX selected".
- * store_dispatch() clears it before each SELECT, so a failed one leaves
- * nothing selected (RFC 9051 SS6.3.2); CLOSE and a DELETE of the selected
- * mailbox clear it too, since cwd is back at the maildir root by then and
- * anything still "selected" would silently be INBOX.
- */
-extern int mailbox_selected;
-
-/*
- * The name of the mailbox this session has SELECTed, "" for INBOX,
- * written by handle_mbox_select() and followed by a RENAME of that
- * mailbox. Distinct from where the process happens to be standing:
- * COPY needs the name to tell a cross-mailbox copy from a same-mailbox
- * one, and DELETE needs it to know it is deleting the selection.
- */
-extern char selected_mailbox[MBOX_NAME_MAX];
-
-/*
- * maildir_root_fd names the account's maildir root for the life of this
- * child; mailbox_dir_fd names the SELECTed mailbox and is replaced by
- * handle_mbox_select(). Both are opened once the unveil is in place,
- * and a lookup relative to either is unveiled exactly as a cwd-relative
- * one is (namei(), sys/kern/vfs_lookup.c).
- */
+extern uint32_t append_counter; /* unique across APPEND and COPY */
extern int maildir_root_fd;
-extern int mailbox_dir_fd;
-/* 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 imsg_store_init and
- * BODYSTRUCTURE_READ_DEFAULT
- * comments */
-
-extern uint64_t append_max; /* from IMSG_STORE_INIT, "append max" */
-/* from IMSG_STORE_INIT, "lock timeout"; 0 disables the bound */
+extern uint32_t bodystructure_read_max;
+extern uint64_t append_max;
extern uint32_t lock_timeout_secs;
void store_dispatch(int, short, void *);
-void store_shutdown(void);
-/* The int handlers return 0 answered; 1 lock busy, nothing answered */
int handle_mbox_select(struct imsg_mbox_select *,
- const struct seq_range *, uint32_t, struct imsgev *);
+ const struct seq_range *, uint32_t, struct store_session *);
int handle_mbox_fetch(struct imsg_mbox_fetch *,
- const struct seq_range *, uint32_t, struct imsgev *);
+ const struct seq_range *, uint32_t, struct store_session *);
int handle_mbox_store(struct imsg_mbox_store *,
- const struct seq_range *, uint32_t, struct imsgev *);
+ const struct seq_range *, uint32_t, struct store_session *);
int handle_mbox_expunge(struct imsg_mbox_expunge *,
- const struct seq_range *, uint32_t, struct imsgev *);
-void handle_mbox_append_begin(const struct imsg_mbox_append *);
-void handle_mbox_append_data(const char *, size_t);
-int handle_mbox_append_end(struct imsgev *);
-void append_abort(void);
+ const struct seq_range *, uint32_t, struct store_session *);
+void handle_mbox_append_begin(struct store_session *,
+ const struct imsg_mbox_append *);
+void handle_mbox_append_data(struct store_session *, const char *,
+ size_t);
+int handle_mbox_append_end(struct store_session *);
+void append_abort(struct store_session *);
int handle_mbox_search(struct imsg_mbox_search *,
- struct search_node *, uint32_t, struct imsgev *);
-int handle_mbox_status(struct imsg_mbox_status *, struct imsgev *);
+ struct search_node *, uint32_t, struct store_session *);
+int handle_mbox_status(struct imsg_mbox_status *, struct store_session *);
int handle_mbox_copy(struct imsg_mbox_copy *,
- const struct seq_range *, uint32_t, struct imsgev *);
+ const struct seq_range *, uint32_t, struct store_session *);
int handle_mbox_move(struct imsg_mbox_copy *,
- const struct seq_range *, uint32_t, struct imsgev *);
+ const struct seq_range *, uint32_t, struct store_session *);
void handle_mbox_create(struct imsg_mbox_create *, struct imsgev *);
-void handle_mbox_delete(struct imsg_mbox_delete *, struct imsgev *);
-void handle_mbox_rename(struct imsg_mbox_rename *, struct imsgev *);
+void handle_mbox_delete(struct imsg_mbox_delete *, struct store_session *);
+void handle_mbox_rename(struct imsg_mbox_rename *, struct store_session *);
void handle_mbox_list(struct imsg_mbox_list *, struct imsgev *);
void handle_mbox_subscribe(struct imsg_mbox_subscribe *,
struct imsgev *);
struct imsgev *);
int mailbox_name_valid(const char *);
int mailbox_open_dir(const char *);
-void cur_snapshot_discard(void);
-int fetch_walk_paused(void);
-void fetch_walk_resume(struct imsgev *);
-void fetch_walk_abort(void);
-int locate_message_file(int, const char *, off_t *, char *,
- size_t);
-int open_message_file(int, const char *);
-int read_message_header(int, const char *, char **, uint32_t *);
-int read_message_body(int, const char *, int, size_t,
+void cur_snapshot_discard(struct cur_snapshot *);
+int fetch_walk_paused(struct store_session *);
+void fetch_walk_resume(struct store_session *);
+void fetch_walk_abort(struct store_session *);
+void fetch_walk_parser(struct store_session *, int);
+int search_walk_paused(struct store_session *);
+void search_walk_abort(struct store_session *);
+void search_walk_parser(struct store_session *, int);
+int search_op_content(int);
+int search_match(int, const char *, const struct imsg_parser_leaf *,
+ uint32_t, const char *, char **, uint32_t *);
+int header_next_field(const char *, size_t, size_t *, const char **,
+ size_t *, const char **, size_t *);
+int address_split(const char *, size_t, const char **, size_t *,
+ const char **, size_t *, const char **, size_t *);
+int address_list_next(const char *, size_t, size_t *, const char **,
+ size_t *);
+int parser_ready(void);
+int locate_message_file(struct cur_snapshot *, int, const char *,
+ off_t *, char *, size_t);
+int open_message_file(struct cur_snapshot *, int, const char *);
+int read_message_header(struct cur_snapshot *, int, const char *,
+ char **, uint32_t *);
+int read_header_from_fd(int, const char *, char **, uint32_t *);
+int read_body_from_fd(int, const char *, int, size_t,
const char *,
char **, uint32_t *);
int header_field_name_matches(const char *, size_t, const char *);
+int filter_header_fields(const char *, size_t, const char *, int,
+ char *, size_t *);
int read_message_header_fields(int, const char *, const char *,
int,
char **, uint32_t *);
int append_field_nstring(char *, size_t, size_t *, const char *,
uint32_t, const char *);
int build_envelope(int, const char *, char **, uint32_t *);
+int parser_request(uint32_t, const char *, int, const char *,
+ struct imsg_parser_req *, void *, size_t, uint32_t,
+ struct imsg_parser_rep *, char **);
int find_header_body_split(const char *, size_t, size_t *);
int mime_is_tspecial(char);
int mime_read_token_or_qstring(const char *, size_t, size_t *,
void partial_range(int, uint32_t, uint32_t, uint64_t *, uint64_t *);
int extract_mime_part(int, const char *, const int *, int,
uint64_t *, uint64_t *);
-int message_body_range(int, const char *, int, uint64_t *,
- uint64_t *, int *);
+int message_body_range(struct cur_snapshot *, int, const char *, int,
+ uint64_t *, uint64_t *, int *);
-/*
- * The maildir+index format: stock maildir (tmp/new/cur) for bodies, plus
- * one line-oriented text index per mailbox holding UIDVALIDITY/UIDNEXT and
- * the UID to basename and keywords map, in that mailbox's maildir root.
- * The reserved filenames live in mboxname.h, since the listener must
- * refuse them as mailbox names without including this header; what each
- * file is FOR is documented below, per constant.
- */
-
-/* STORE_INDEX_LOCK_NAME: the index's lock is taken on this file, not on */
-/* the index. index_save() commits by rename(2), so the index's inode is */
-/* replaced on every save, while flock(2) binds to one inode. This file is */
-/* never replaced, so its inode is stable. It holds no content. */
-/* mailbox_name_valid() refuses it as a mailbox name, and */
-/* remove_maildir_subtree() unlinks it with the mailbox. */
-
#define STORE_INDEX_LINE_MAX 1024
-/* STORE_UIDVALIDITY_NAME: per-user floor, the highest UIDVALIDITY ever */
-/* issued to this user, as decimal digits. One file at the maildir root. */
-/* RFC 9051 SS2.3.1.1 wants a value that always increases, and a time(NULL) */
-/* seed is not unique at one-second granularity, so a mailbox deleted and */
-/* recreated quickly could reuse one. Unlike the index this file is its own */
-/* lock: it is rewritten in place, so its inode never changes. */
-/* LOCK ORDERING, which a later edit must not break: this lock is an */
-/* INNERMOST LEAF. uidvalidity_next() runs with a mailbox index lock held, */
-/* and must never acquire a mailbox lock itself. */
-
-/* STORE_SUBSCRIPTIONS_NAME: per-account subscribed-mailbox list, one flat */
-/* name per line, at the maildir root. INBOX is never named in it. */
-/* An ABSENT file means every mailbox is subscribed; only UNSUBSCRIBE makes */
-/* one, and it writes out every other mailbox as it goes. */
-/* Its lock is a separate file for STORE_INDEX_LOCK_NAME's reason, and is */
-/* taken ALONE. Nothing holds another lock while holding it. */
-
-/* A held index lock: the flock(2)ed lock file, plus the index descriptor, */
-/* which is opened only AFTER the lock is held so it cannot name an inode */
-/* a concurrent save has replaced. Both are -1 when nothing is held, so */
-/* declare with INDEX_LOCK_INIT: an all-zero struct would name fd 0. */
+/* the index is opened after the lock; init with INDEX_LOCK_INIT */
struct index_lock {
int lockfd;
int fd;
#define INDEX_LOCK_INIT { -1, -1 }
-/* In-memory copy of one mailbox's index for the duration of one mutating */
-/* op: built from disk, mutated, rewritten in full, discarded. Never held */
-/* across 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 *);
int index_save(int, const struct mbox_index *);
void index_free(struct mbox_index *);
int index_parse_line(const char *, struct index_rec *);
-int index_field_valid(const char *); /* no ':', CR or LF --
- * safe to write into a
- * colon-delimited index line */
-int index_basename_valid(const char *); /* additionally no '/', no
- * leading '.', no control bytes --
- * safe to paste into "new/%s" */
+int index_field_valid(const char *); /* no ':', CR or LF */
+int index_basename_valid(const char *); /* safe in "new/%s" */
uint32_t index_max_uid(struct mbox_index *);
void send_vanished_range(const struct mbox_index *, uint32_t, uint32_t,
struct imsgev *);
int seqset_contains(const struct seq_range *, uint32_t, uint32_t);
uint32_t seqset_max_hi(const struct seq_range *, uint32_t);
-/* Where one message sits relative to a resolved sequence-set, for the */
-/* ascending single-pass scans. PAST_END means the scan may stop, not */
-/* merely that this message is unmatched, which is sound only because */
-/* those loops walk idx->lines in ascending order. */
+/* PAST_END holds only because the scans walk in ascending order */
enum seqset_pos {
SEQSET_PAST_END,
SEQSET_SKIP,
int, uint32_t, uint32_t);
int refresh_index(int, struct mbox_index *, int);
uint32_t uidvalidity_next(void);
-void idle_probe_reset(void);
-void idle_baseline_reset(void);
+void idle_probe_reset(struct store_session *);
+void idle_baseline_reset(struct store_session *);
int index_lock_acquire(int, struct index_lock *, int);
void index_lock_release(struct index_lock *);
void handle_mbox_idle_refresh(const struct imsg_mbox_idle_refresh *,
- struct imsgev *);
+ struct store_session *);
void qresync_send_resync(const struct imsg_mbox_select *,
const struct seq_range *, uint32_t, struct mbox_index *,
- struct imsgev *);
+ struct store_session *);
-/*
- * The remaining six are single-purpose helpers that happen to be called
- * from more than one of the split-out files.
- */
const char *append_hostname(void);
int ensure_maildir_dirs(int, const char *);
uint32_t letters_to_sysflags(const char *);
blob - 68036f9bd0c0512b3cf54cf574d492051f023cb2
blob + 760d3f6ac69ae23a6461fef09899118f0e047e3d
--- src/store_ipc.c
+++ src/store_ipc.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * store_ipc.c: listener-side dispatch of the store process's async
- * imsg protocol (session_handle_mbox_*()/session_finish_*() reply
- * handlers).
- */
#include <sys/types.h>
#include <sys/queue.h>
#include "log.h"
#include "listener.h"
-/* Shared receive-side handling for the four IMSG_MBOX_FETCH_* messages. */
static void
fetch_part_recv(struct session *s, struct imsg *imsg, const char *what,
int found, uint32_t declared_len, char **bufp, uint32_t *lenp,
return;
}
if (wirelen == 0) {
- *foundp = 1; /* found but empty is legitimate, see callers */
+ *foundp = 1;
return;
}
if ((*bufp = malloc(wirelen)) == NULL) {
struct imsg imsg;
ssize_t n;
- /*
- * EV_WRITE: queued IMSG_MBOX_* requests need imsgbuf_write() once
- * writable.
- */
if (event & EV_WRITE) {
if (imsgbuf_write(&s->store_iev->ibuf) == -1) {
log_warnx("session %u: imsgbuf_write (store)", s->id);
case IMSG_MBOX_FETCH_HEADER: {
struct imsg_mbox_fetch_header hdr;
- /*
- * Fixed header plus trailing bytes, both read with
- * imsg_get_buf().
- */
if (imsg_get_buf(&imsg, &hdr, sizeof(hdr)) == -1) {
log_warnx("bad IMSG_MBOX_FETCH_HEADER "
"(header)");
struct imsg_mbox_fetch_body bodyhdr;
int bodyfd;
- /*
- * No octets on this imsg: a found body is a read-only
- * descriptor and a range, read when the response is
- * written. The label, has_partial and partial_origin
- * are set once per FETCH in fetch_dispatch().
- */
bodyfd = imsg_get_fd(&imsg);
if (s->pending_body_fd != -1)
close(s->pending_body_fd);
case IMSG_MBOX_FETCH_ENVELOPE: {
struct imsg_mbox_fetch_envelope envhdr;
- /*
- * Same technique as FETCH_HEADER/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 "
case IMSG_MBOX_FETCH_BODYSTRUCTURE: {
struct imsg_mbox_fetch_bodystructure bshdr;
- /*
- * Same technique as FETCH_HEADER/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 "
log_warnx("bad IMSG_MBOX_FETCH_META");
break;
}
- /*
- * imsg_get_data() doesn't guarantee NUL termination,
- * and flags is formatted with "%s" into client-visible
- * replies, so an unterminated field from the
- * (untrusted) store child would leak stack memory onto
- * the wire -- same trust boundary as auth.c, parent.c,
- * keymgr.c, and store.c's imsg_field_valid().
- */
+ /* imsg_get_data() does not NUL-terminate */
meta.flags[sizeof(meta.flags) - 1] = '\0';
- /*
- * Shared reply type for FETCH/STORE/QRESYNC resync;
- * SELECTING buffers per RFC 7162 SS3.2.6 order.
- */
+ /* SELECTING buffers, for RFC 7162 SS3.2.6 order */
if (s->state == SESSION_SELECTING)
session_handle_select_fetch(s, &meta);
else if (s->state == SESSION_STORING)
log_warnx("bad IMSG_MBOX_EXPUNGED");
break;
}
- /*
- * During a MOVE, buffered here, flushed after COPYUID
- * by session_finish_copy_or_move() (SS6.4.8).
- */
+ /* buffered until after COPYUID (RFC 9051 SS6.4.8) */
if (s->state == SESSION_COPYING) {
if (s->move_expunged_n ==
s->move_expunged_cap) {
log_warnx("bad IMSG_MBOX_SELECT_VANISHED");
break;
}
- /*
- * Also reused for RFC 7162 SS3.2.6's VANISHED modifier;
- * SELECTING buffers, FETCHING writes now.
- */
if (s->state == SESSION_SELECTING)
session_handle_select_vanished(s, &v);
else
log_warnx("bad IMSG_MBOX_LIST_ITEM");
break;
}
- /*
- * Same NUL-termination risk as IMSG_MBOX_FETCH_META's
- * flags: mailbox is walked as a C string by
- * list_pattern_match() and formatted with "%s" into the
- * untagged LIST response.
- */
+ /* not NUL-terminated by imsg_get_data() */
item.mailbox[sizeof(item.mailbox) - 1] = '\0';
session_handle_mbox_list_item(s, &item);
break;
imsgev_rearm_read(s->store_iev);
(void)fd;
- /*
- * If the reply just processed was terminal, 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 */
}
-/*
- * Finishes SELECT (SS6.3.2); flushes QRESYNC resync in RFC order: VANISHED,
- * FETCH, tagged OK.
- */
+/* RFC 9051 SS6.3.2; for QRESYNC: VANISHED, FETCH, tagged OK */
void
session_handle_mbox_selected(struct session *s,
const struct imsg_mbox_selected *res)
char buf[128];
if (res->error != MBOX_OP_OK || s->qresync_alloc_failed) {
- /*
- * RFC 9051 SS6.3.2 failure -> authenticated (RFC 5530
- * NONEXISTENT); a dropped QRESYNC resync also fails SELECT
- * here on purpose, before any HIGHESTMODSEQ is advertised,
- * using RFC 5530 SS3 UNAVAILABLE for the transient condition.
- */
+ /* RFC 5530 NONEXISTENT */
s->state = SESSION_AUTHENTICATED;
session_reply(s, s->pending_tag, "NO",
s->qresync_alloc_failed ? "[UNAVAILABLE] SELECT failed" :
res->error == MBOX_OP_ERR_BUSY ? IMAP_BUSY_TEXT :
"[NONEXISTENT] no such mailbox");
- /*
- * An honest store child never streams resync data before a
- * failing IMSG_MBOX_SELECTED, but a compromised one could,
- * and leftover data here would be replayed into the next
- * SELECT's response.
- */
+ /* a compromised store child may have streamed resync data */
free(s->vanished_ranges);
s->vanished_ranges = NULL;
s->vanished_nranges = 0;
s->state = SESSION_SELECTED;
s->mbox_highestmodseq = res->highestmodseq;
- /*
- * Order matches RFC 9051 SS6.3.2's worked example; that section says
- * the order isn't significant.
- */
snprintf(buf, sizeof(buf), "%u EXISTS", res->exists);
session_untagged(s, buf);
session_untagged(s, buf);
}
- /*
- * PERMANENTFLAGS: EXAMINE gets none (SS6.3.3); SELECT adds "\*" since
- * keywords may be created (SS6.3.2).
- */
+ /* RFC 9051 SS6.3.3: EXAMINE gets no PERMANENTFLAGS */
session_untagged(s,
"FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)");
if (s->mbox_readonly)
"OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen "
"\\Draft \\*)] System flags and keywords allowed");
- /*
- * RFC 9051 SS6.3.2 LIST; name goes through quote_mailbox() (may
- * contain a space or SS4.3 quoted-special) and is written with
- * session_write() since the escaped form can exceed
- * session_untagged()'s 512-byte limit, same as VANISHED
- * (EARLIER).
- */
{
char qname[MBOX_QUOTED_MAX];
char listbuf[MBOX_QUOTED_MAX + 64];
size_t vlen;
int flen;
- /*
- * session_untagged()'s 512-byte buffer is too small here, same
- * bypass used for ESEARCH.
- */
vlen = format_range_list(vbuf, sizeof(vbuf),
s->vanished_ranges, s->vanished_nranges, &truncated);
if (truncated)
s->qresync_fetches_cap = 0;
s->qresync_alloc_failed = 0;
- /*
- * RFC 9051 SS6.3.2/SS6.3.3: READ-WRITE for SELECT, READ-ONLY for
- * EXAMINE, keyed off s->mbox_readonly.
- */
if (s->mbox_readonly)
session_reply(s, s->pending_tag, "OK",
"[READ-ONLY] EXAMINE completed");
"[READ-WRITE] SELECT completed");
}
-/*
- * Finishes STATUS (SS6.3.11); attrs emitted in fixed canonical order, not
- * request order.
- */
+/* RFC 9051 SS6.3.11; attributes in a fixed order */
void
session_handle_mbox_status_result(struct session *s,
const struct imsg_mbox_status_result *res)
{
char qname[MBOX_QUOTED_MAX];
- /*
- * F2 fix: buf[256] was too small for a near-max mailbox name
- * plus STATUS attrs; now sized for the SS4.3-escaped name,
- * leading "* " and trailing CRLF since this line bypasses
- * session_untagged()'s 512-byte buffer.
- */
+ /* RFC 9051 SS4.3-escaped name plus attributes */
char buf[MBOX_QUOTED_MAX + 384];
size_t len;
int n, first = 1;
s->state = s->status_prev_state;
if (res->error != MBOX_OP_OK) {
- /*
- * Valid-but-missing name or an open/flock/index_load failure --
- * indistinguishable, so NONEXISTENT covers both.
- */
+ /* a missing mailbox and an I/O failure look alike */
session_reply(s, s->pending_tag, "NO",
res->error == MBOX_OP_ERR_BUSY ? IMAP_BUSY_TEXT :
"[NONEXISTENT] no such mailbox");
return;
}
- /*
- * Quoted, not bare, since mailbox names can contain spaces;
- * quote_mailbox() now renders proper SS4.3 quoting and
- * parse_mailbox_arg() decodes it on input, closing both the
- * output and input escaping gaps from the mailbox_cmd.c review.
- */
if (quote_mailbox(qname, sizeof(qname), s->status_mailbox) == -1)
log_warnx("session %u: STATUS mailbox name truncated", s->id);
len = (size_t)snprintf(buf, sizeof(buf), "* STATUS %s (", qname);
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 server
- * tracks no such state so always answers 0, kept here only
- * to mirror IMAP4rev1's MESSAGES/RECENT ordering.
- */
+ /* RFC 9051 SS2.3.2 dropped \Recent; always 0 */
STATUS_APPEND("RECENT %u", 0U);
if (s->status_attrs & STATUS_ATT_UIDNEXT)
STATUS_APPEND("UIDNEXT %u", res->uidnext);
#undef STATUS_APPEND
- /*
- * Closing paren and CRLF are written here (not via
- * session_untagged(), whose 512 bytes the escaped name can
- * exceed); buf is sized to always fit, and the else is an
- * unreachable guard answering NO.
- */
if (len + 3 < sizeof(buf)) {
buf[len++] = ')';
buf[len++] = '\r';
session_reply(s, s->pending_tag, "OK", "STATUS completed");
}
-/*
- * Terminal CREATE/DELETE/RENAME/SUBSCRIBE/UNSUBSCRIBE reply; s->state (which
- * command) is read before overwritten. Only RENAME and DELETE act on
- * s->mbox_op_name below; the other three have no effect on what is selected.
- */
void
session_finish_mbox_op(struct session *s, const struct imsg_mbox_result *res)
{
case MBOX_OP_OK:
break;
case MBOX_OP_ERR_NO_SUCH_MAILBOX:
- /*
- * RFC 5530 SS3 NONEXISTENT: DELETE of a missing mailbox, or
- * RENAME missing its source
- */
+ /* RFC 5530 SS3 NONEXISTENT */
session_reply(s, s->pending_tag, "NO",
"[NONEXISTENT] no such mailbox");
return;
case MBOX_OP_ERR_ALREADY_EXISTS:
- /*
- * RFC 5530 SS3 ALREADYEXISTS: CREATE of an existing mailbox, or
- * RENAME to an existing dest
- */
+ /* RFC 5530 SS3 ALREADYEXISTS */
session_reply(s, s->pending_tag, "NO",
"[ALREADYEXISTS] mailbox already exists");
return;
default:
- /*
- * 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;
}
- /*
- * The op succeeded on this session's own selected mailbox, so
- * selection must follow; s->state (restored to
- * mbox_op_prev_state above) gates the check since
- * s->selected_mailbox is meaningful only while SESSION_SELECTED.
- */
+ /* the selection follows a RENAME */
if (s->state == SESSION_SELECTED &&
strcmp(s->selected_mailbox, s->mbox_op_name) == 0) {
if (strcmp(cmdname, "RENAME") == 0) {
"truncated after RENAME, can't happen "
"(both same size)", s->id);
} else if (strcmp(cmdname, "DELETE") == 0) {
- /*
- * Selected mailbox is gone, so clear selection here too
- * (like CLOSE does): staying SESSION_SELECTED would let
- * further FETCH/STORE/SEARCH/COPY/MOVE/EXPUNGE silently
- * hit INBOX (the store child's cwd after delete),
- * risking INBOX data loss under a name the client
- * thinks it deleted.
- */
+ /* the mailbox is gone: deselect, as CLOSE does */
s->state = SESSION_AUTHENTICATED;
s->selected_mailbox[0] = '\0';
}
session_reply(s, s->pending_tag, "OK", text);
}
-/*
- * One LIST_ITEM entry, matched case-sensitively vs s->list_pattern; streamed
- * through, unbuffered.
- */
void
session_handle_mbox_list_item(struct session *s,
const struct imsg_mbox_list_item *item)
if (!list_pattern_match(s->list_pattern, item->mailbox, 0))
return;
- /*
- * Mailbox attributes, RFC 9051 SS7.3.1; "()" is none. A plain LIST
- * reports nothing about subscription state, so it keeps the empty
- * list it has always sent. Otherwise the two commands say the same
- * thing in their own dialects: LIST (SUBSCRIBED) marks a subscribed
- * name whose mailbox is gone "\Subscribed \NonExistent"
- * (SS6.3.9.6's Table 3), LSUB marks it "\Noselect" (RFC 3501
- * SS6.3.9), and RFC 9051's Table 2 makes \NonExistent imply
- * \NoSelect.
- */
+ /* RFC 9051 SS7.3.1 attributes */
if (!s->list_subscribed_only)
attrs = "";
else if (s->list_is_lsub)
attrs = item->exists ? "\\Subscribed" :
"\\Subscribed \\NonExistent";
- /*
- * Quoted, not bare, same reasoning as
- * session_handle_mbox_status_result(). item->mailbox comes straight
- * from readdir(2) or the subscription file in the store child, so
- * this is the emission that made the escaping gap client-visible.
- */
if (quote_mailbox(qname, sizeof(qname), item->mailbox) == -1)
log_warnx("session %u: LIST mailbox name truncated", s->id);
n = snprintf(buf, sizeof(buf), "* %s (%s) \"/\" %s\r\n", kw, attrs,
log_warnx("session %u: LIST response did not fit", s->id);
}
-/*
- * Terminal LIST/LSUB reply; a non-OK error means a real I/O error -- mismatches
- * already silently produce no item.
- */
void
session_finish_list(struct session *s, const struct imsg_mbox_result *res)
{
session_reply(s, s->pending_tag, "OK", text);
}
-/*
- * Appends one COPYUID UID pair, growing both arrays together (doubling from
- * 16); OOM sets copy_alloc_failed.
- */
void
session_handle_mbox_copy_mapping(struct session *s,
const struct imsg_mbox_copy_mapping *m)
s->copy_n++;
}
-/*
- * Appends one SELECT_VANISHED range; a dropped range would leave
- * the client holding a phantom UID it can never be told about, so
- * it fails the SELECT (see s->qresync_alloc_failed).
- */
+/* a dropped range would hide a UID from the client for ever */
void
session_handle_select_vanished(struct session *s,
const struct imsg_mbox_select_vanished *v)
s->vanished_nranges++;
}
-/*
- * Appends one QRESYNC resync FETCH_META; held back for RFC 7162 SS3.2.6's
- * ordering.
- */
void
session_handle_select_fetch(struct session *s,
const struct imsg_mbox_fetch_meta *m)
s->qresync_fetches[s->qresync_nfetches++] = *m;
}
-/* One untagged EXPUNGE the store child says to print, RFC 9051 SS7.5.1. */
void
session_handle_idle_expunge(struct session *s,
const struct imsg_mbox_idle_expunge *item)
session_untagged(s, buf);
}
-/*
- * One untagged FETCH for a message whose flags changed elsewhere, RFC 9051
- * SS6.3.13. Any unsolicited FETCH must carry UID, per SS6.3.13 and SS7.5.2.
- * RFC 7162 SS3.1 adds that once a CONDSTORE enabling command has been
- * issued, mod-sequence data belongs in every later untagged FETCH, whatever
- * caused it; s->condstore_enabled is that state.
- */
+/* RFC 9051 SS6.3.13: an unsolicited FETCH carries UID */
void
session_handle_idle_fetch(struct session *s,
const struct imsg_mbox_fetch_meta *meta)
session_untagged(s, buf);
}
-/*
- * Terminal IDLE_REFRESH reply; any EXPUNGE lines have already been printed
- * by session_handle_idle_expunge() and any FETCH lines by
- * session_handle_idle_fetch(), and RFC 9051 SS7.5.1 wants the EXPUNGEs
- * before the new count, which the imsg order gives.
- */
void
session_handle_idle_refreshed(struct session *s,
const struct imsg_mbox_idle_refreshed *res)
s->idle_refresh_pending = 0;
- /*
- * The store child keeps the baseline, so a failed refresh needs
- * nothing undone here: it reported no change and kept what it had.
- */
if (!res->ok && res->busy)
log_debug("session %u: IDLE refresh skipped, index lock busy",
s->id);
}
}
-/*
- * Sends IMSG_MBOX_IDLE_REFRESH; coalesces via s->idle_refresh_again instead of
- * overlapping in-flight requests. seed asks the store child to adopt what it
- * finds rather than report it, and outlives coalescing: a seed folded into a
- * pending request must still seed, or the client would be told about changes
- * made while it was not idling.
- */
void
session_request_idle_refresh(struct session *s, int seed)
{
s->id);
}
-/*
- * The IDLE poll (RFC 9051 SS6.3.13) that drives IDLE pushes;
- * replaces the old per-session notification walk (broken under
- * SS7's one-session-per-process model and blind to MTA deliveries)
- * with a poll that's cheap when nothing changed
- * (index.c's idle_probe_unchanged(), two stat(2) calls).
- */
static void
session_idle_poll(int fd, short event, void *arg)
{
(void)fd;
(void)event;
- /*
- * Both IDLE exits disarm this timer so these checks should never
- * fail; they guard against a refresh being asked for after the
- * session already left IDLE, which would advance the store child's
- * baseline past what this client has been told.
- */
if (!s->idling || s->state != SESSION_SELECTED) {
- /*
- * Should be unreachable; logged rather than silently ignored
- * so a wrong-baseline bug (reply arriving after the session
- * left IDLE) leaves evidence instead of just corrupting
- * state invisibly.
- */
log_debug("session %u: idle poll fired while not idling "
"(idling=%d state=%d), ignored", s->id, s->idling,
(int)s->state);
session_idle_poll_arm(s); /* an evtimer is one-shot */
}
-/* Once per session, early enough that session_idle_poll_disarm() is safe. */
void
session_idle_poll_init(struct session *s)
{
evtimer_del(&s->idle_ev);
}
-/*
- * QRESYNC resync FETCH: always UID+FLAGS+MODSEQ (RFC 7162 SS3.2.5.1); no
- * s->fetch_attrs, RFC fixes content.
- */
+/* RFC 7162 SS3.2.5.1: always UID, FLAGS and MODSEQ */
void
session_send_qresync_fetch_response(struct session *s,
const struct imsg_mbox_fetch_meta *meta)
session_untagged(s, buf);
}
-/*
- * Worst-case sizing for RFC 7162 SS3.1.3's MODIFIED list,
- * mirroring search_cmd.c's SEARCH_ALL_PER_MATCH/
- * SEARCH_RESP_PREFIX_MAX pair: each entry is at most
- * "4294967295" plus separator, plus tag, fixed words, cmdname
- * and CRLF.
- */
+/* RFC 7162 SS3.1.3 MODIFIED, worst case per entry */
#define MODIFIED_PER_ENTRY 11
#define MODIFIED_WRAPPER_MAX 160
-/*
- * Terminal reply hub; SEARCH/COPY/MOVE/CREATE/DELETE/RENAME/LIST peel off
- * first; FETCH/STORE/EXPUNGE share rest.
- */
void
session_handle_mbox_result(struct session *s, struct imsg_mbox_result *res)
{
if (s->state == SESSION_CREATING || s->state == SESSION_DELETING ||
s->state == SESSION_RENAMING || s->state == SESSION_SUBSCRIBING ||
s->state == SESSION_UNSUBSCRIBING) {
- /*
- * RFC 9051 SS6.3.4-SS6.3.8: same peel-off as SEARCH/COPY; reply
- * text doesn't fit this cmdname table.
- */
session_finish_mbox_op(s, res);
return;
}
if (s->state == SESSION_LISTING) {
- /*
- * RFC 9051 SS6.3.9: same peel-off reasoning; LIST/LSUB text
- * keyed off s->list_is_lsub.
- */
session_finish_list(s, res);
return;
}
- /*
- * RFC 9051 SS6.4.9: becomes "UID <CMD>" when s->cmd_by_uid is set;
- * CLOSE is the exception.
- */
+ /* RFC 9051 SS6.4.9: "UID <CMD>" */
if (s->state == SESSION_STORING) {
cmdname = s->cmd_by_uid ? "UID STORE" : "STORE";
was_storing = 1;
} else
cmdname = s->cmd_by_uid ? "UID FETCH" : "FETCH";
- /*
- * res->error is enum mbox_op_error where MBOX_OP_OK==1 (0 is
- * MBOX_ERR_UNSET), so logging it raw made every success read
- * "error=1"; same two-way label session_finish_search() already
- * uses.
- */
log_debug("session %u: %s done, status=%s, %u response(s) sent",
s->id, cmdname, res->error == MBOX_OP_OK ? "OK" : "ERROR",
res->count);
- /*
- * RFC 9051 SS6.4.1: only a CLOSE that succeeded leaves the selected
- * state; a failed one removed nothing (handle_mbox_expunge()).
- */
+ /* RFC 9051 SS6.4.1: only a successful CLOSE deselects */
s->state = (was_close && res->error == MBOX_OP_OK) ?
SESSION_AUTHENTICATED : SESSION_SELECTED;
if (res->error != MBOX_OP_OK) {
- /*
- * An I/O error or a keyword set that does not fit; RFC 9051
- * has no code for either, so plain NO is honest. The store
- * changed nothing, so HIGHESTMODSEQ is not adopted.
- */
char text[32];
free(s->store_modified);
return;
}
- /* RFC 7162: post-op HIGHESTMODSEQ for session_condstore_enable() */
if (was_storing || was_expunging)
s->mbox_highestmodseq = res->highestmodseq;
- /*
- * RFC 7162 SS3.1.3: a failed-conditional STORE gets MODIFIED on
- * its tagged OK. The alloc_failed arm of this test matters: a
- * grow that failed on the very first entry leaves
- * store_modified_n at 0, which would otherwise fall through to
- * the unqualified "STORE completed" below and tell the client
- * every message passed UNCHANGEDSINCE.
- */
+ /* RFC 7162 SS3.1.3 MODIFIED */
if (was_storing && (s->store_modified_n > 0 ||
s->store_modified_alloc_failed)) {
char *rbuf = NULL, *text = NULL;
size_t rbufsize, textsize;
int truncated, n, incomplete;
- /*
- * Every way the set can come up short lands here: a failed
- * grow in session_handle_store_modified(), a formatter
- * truncation, a response that doesn't fit, or buffers that
- * don't allocate.
- */
incomplete = s->store_modified_alloc_failed;
- /*
- * Heap-allocated and worst-case-sized like
- * session_finish_search()'s ESEARCH ALL list, since the old
- * fixed 2048-byte buffers weren't the real bound:
- * session_reply()'s 512-byte overflow handling amputates the
- * closing "]" into a malformed resp-text-code; composed in
- * full and sent via session_write(), same as ESEARCH and
- * VANISHED (EARLIER).
- */
rbufsize = (size_t)s->store_modified_n * MODIFIED_PER_ENTRY + 1;
textsize = rbufsize + MODIFIED_WRAPPER_MAX;
format_seq_list(rbuf, rbufsize,
s->store_modified, s->store_modified_n,
&truncated);
- /*
- * Can't fire today (MODIFIED_PER_ENTRY sizes
- * rbuf for the worst case, as
- * SEARCH_ALL_PER_MATCH does for ESEARCH), but a
- * short list is indistinguishable from a
- * complete one to the client, so it is refused
- * rather than sent.
- */
if (truncated) {
log_warnx("session %u: MODIFIED list "
"truncated", s->id);
free(rbuf);
free(text);
- /*
- * SS3.1.3's set MUST list every message that failed
- * UNCHANGEDSINCE, and per SS3.1.3's client guidance a message
- * absent from it is one the client believes was stored and
- * will never retry -- so a set known to be short is never
- * sent, and dropping the code entirely would say the same
- * thing (no MODIFIED means nothing failed). Refusing the
- * command is what every other variable-length list here does
- * on a failed grow; RFC 5530 SS3 UNAVAILABLE marks it
- * transient, the same code
- * session_handle_mbox_selected() uses for a dropped QRESYNC
- * range, so a client retries rather than treating the STORE
- * as rejected.
- */
+ /* RFC 7162 SS3.1.3: an incomplete MODIFIED is refused */
if (incomplete) {
char fail[64];
s->store_modified_alloc_failed = 0;
- /*
- * RFC 7162 SS3.2.7: real EXPUNGE (not CLOSE, forbidden by SS3.2.8) with
- * count>0 gets HIGHESTMODSEQ if CONDSTORE.
- */
+ /* RFC 7162 SS3.2.7, but never on CLOSE (SS3.2.8) */
if (was_expunging && !was_close && s->condstore_enabled &&
res->count > 0) {
char text[64];
return;
}
- /*
- * RFC 9051 SS6.4.5: "NO, fetch error: can't fetch that data". An
- * item the store could not produce was left out of the untagged
- * replies, so the command did not do what was asked, and SS7.1.1
- * makes a tagged OK a claim that it did. Untagged data already
- * sent stays valid, so the messages that worked are not withdrawn.
- * No RFC 5530 code fits every cause, so plain NO is honest.
- */
+ /* RFC 9051 SS6.4.5 */
if (s->fetch_incomplete) {
char text[96];
blob - 9adfc3af86fd43a4edbbee8c3ee5b0cacca4be55
blob + 591d448412ee231a8ee8bc46eda9c51afb1b6fdf
--- src/utf8.c
+++ src/utf8.c
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-/*
- * utf8_mailbox_ok() enforces RFC 5198 SS2's UTF-8/C1-control/no-BOM
- * requirements (3 of its 6 Net-Unicode items; NFC normalization and
- * unassigned-code-point checks are deliberately unenforced, per imapd.8) via
- * raw byte-range checks rather than decoding, since the well-formed-sequence
- * ranges alone are what RFC 3629 SS4 requires.
- */
+/* RFC 5198 SS2: UTF-8, no C1 controls, no BOM */
#include "utf8.h"
-/*
- * Returns 1 if `name` is a NUL-terminated string whose encoding this server
- * accepts as a mailbox name, 0 otherwise; other rules ("/", ".", "..", reserved
- * imapd.* names, length) are each caller's own responsibility.
- */
int
utf8_mailbox_ok(const char *name)
{
while (*p != '\0') {
unsigned int need, lo, hi, i;
- if (p[0] < 0x80) { /* ASCII, C0 and DEL included */
+ if (p[0] < 0x80) {
p++;
continue;
}
} else if (p[0] == 0xf4) {
need = 3; lo = 0x80; hi = 0x8f; /* <= U+10FFFF */
} else {
- /*
- * 0x80-0xc1 (continuation or overlong lead) and
- * 0xf5-0xff
- */
return (0);
}
- /*
- * Reading p[1..3] can't run past the terminator: each byte is
- * only reached after its predecessor tested inside 0x80-0xbf,
- * so the walk always stops at the first bad byte, terminator
- * included.
- */
if (p[1] < lo || p[1] > hi)
return (0);
for (i = 2; i <= need; i++)
if (p[0] == 0xc2 && p[1] <= 0x9f)
return (0);
- /*
- * RFC 5198 SS2 item 5 bans a leading BOM; U+FEFF is refused
- * anywhere in the name (stricter than the RFC, and imapd.8 says
- * so) since a zero-width no-break space mid-name would make two
- * mailboxes indistinguishable.
- */
+ /* RFC 5198 SS2 item 5 bans a leading BOM; refused anywhere */
if (p[0] == 0xef && p[1] == 0xbb && p[2] == 0xbf)
return (0);
blob - 55a0a1c34bc5f8d5174d4880637852c0cc9f5ec2
blob + 3c0922b2fc3e0d67ea1b498ff1a940617c49afc4
--- src/utf8.h
+++ src/utf8.h
#ifndef IMAPD_UTF8_H
#define IMAPD_UTF8_H
-/* The one UTF-8 predicate, shared by the listener's and the store's */
-/* mailbox-name validators. Those stay separate because the store does */
-/* not trust the listener, but well-formedness carries no policy, so both */
-/* call this rather than keeping a copy each. Depends on nothing, so */
-/* either side can include it. */
-
int utf8_mailbox_ok(const char *);
#endif /* IMAPD_UTF8_H */