commit 65c7cf929f78f3280baaf04805e06a3d59b8b4f2 from: David Williams date: Tue Sep 29 01:08:18 2026 UTC NOOP reports changes; describe forwards; check accounts; bound keymgr; set \Seen Change 1 of 6: Report other agents' changes at NOOP, and to IDLE from SELECT on NOOP answered OK and nothing else, so a client that polls rather than idles never learned of new mail, flag changes or expunges made by another session or by smtpd. RFC 9051 section 5.2 requires a mailbox size update whenever a command observes one, and section 6.1.2 names NOOP as the poll. IDLE did report such changes, but it took the mailbox as it found it at IDLE as its starting point, so a change made between SELECT and IDLE, or between DONE and the next IDLE, was never reported at all. The account worker now keeps what it last told each session from SELECT on, rather than from IDLE on. NOOP in the selected state asks it for the same comparison IDLE makes, and completes once the answer is written: EXISTS for arrivals, EXPUNGE for removals, and a FETCH carrying UID and FLAGS for each flag change, with MODSEQ once CONDSTORE is enabled. IDLE compares against the same record instead of starting afresh. The account worker answers every such request, even one it refuses, since NOOP now waits for the answer. A message the session expunges or moves out itself leaves that record as its EXPUNGE is sent, so it is never reported twice. One function now sends that EXPUNGE for EXPUNGE, UID EXPUNGE and both kinds of MOVE, so none can skip the step. A message the session appends to its selected mailbox joins the record. Its own flag changes do not, so a later NOOP or IDLE may report them again. A refresh still in flight when DONE arrives is now answered before IDLE's tagged OK. Its EXPUNGE, FETCH and EXISTS lines used to be dropped, though the account worker counted them as told, so they were never reported. Two errors in IDLE's reporting are fixed on the way. A removal and an arrival seen in one refresh left the count unchanged, so no EXISTS was sent and the client was one message short; EXISTS now follows any EXPUNGE a refresh sends, as RFC 9051 section 6.3.13's example does. And a session that had enabled QRESYNC was sent EXPUNGE for another session's removals, where RFC 7162 section 3.2.10.2 requires VANISHED. Change 2 of 6: Read sequence numbers as the session was told them A session's sequence numbers were read against the mailbox as it stood, not as the session had been told it stood. After another session expunged a message, FETCH 2 returned the message the client called 3, STORE 2 flagged it, and an EXPUNGE that followed removed it. RFC 9051 section 7.5.1 forbids EXPUNGE responses during FETCH, STORE and SEARCH so that the numbers stay in step; imapd kept the rule and lost the step. APPEND's EXISTS could also lower the count, which section 5.2 forbids. The account worker now reads every sequence set through the session's view, the record the first change keeps: number n is the nth message the session was told of, found by its UID, and every response is numbered the same way. Each command that reads the index first tells the session what changed: EXISTS and flag FETCHes always, EXPUNGE only where section 7.5.1 allows it, so UID FETCH and UID STORE may carry one and FETCH, STORE and SEARCH may not. A message expunged elsewhere and not yet reported stays in the view until it is. A command naming such a message answers as RFC 2180 section 4 describes. FETCH returns the others and a tagged NO [EXPUNGEISSUED] (section 4.1.2), and so does STORE without .SILENT (sections 4.2.2 and 4.2.3); STORE .SILENT stores the others and answers OK (4.2.1); SEARCH never matches it (4.3); COPY and MOVE copy the others, send the pending EXPUNGEs and answer OK (4.4.2), as RFC 9051 section 6.4.9 already asks of a UID COPY naming a UID that is gone. APPEND to the selected mailbox takes its EXISTS from the view, after any pending EXPUNGEs. A session's own flag changes are no longer reported back to it: STORE holds the index lock from its report to its save, so the mod-sequence it assigns is its own. Change 3 of 6: Describe and fetch forwarded messages; refuse what FETCH cannot return BODYSTRUCTURE refused any message holding a MESSAGE/RFC822 or MESSAGE/GLOBAL part, at any depth, so fetching the structure of a message with a forwarded message attached ended NO. RFC 9051 section 7.5.2 describes such a part by the envelope, body structure and line count of the message it holds, and its grammar allows no other description. The builder now writes them. The envelope comes from the code that answers ENVELOPE, now split from the read of the header so that both can use it. A forwarded message counts against the depth and part limits as one level, as SEARCH already counts it. Part numbers now reach through such a part, as section 6.4.5.1 describes: BODY[2] of a forward is the whole forwarded message, and BODY[2.1] its first part. HEADER, TEXT and MIME after a part number are still not supported. A FETCH naming such an item beside one imapd supports, or plain BODY[...] or RFC822.TEXT, returned the rest and ended OK, as though the item had been sent. Section 6.4.5 gives NO for data that cannot be fetched, and imapd already answers NO when a supported item cannot be produced. It now does the same here, after sending what it could. A FETCH naming only such items was already refused. Change 4 of 6: Refuse to start without the daemon accounts; document creating them imapd drops privileges to three accounts, _imapd, _imapauth and _imapkey, but nothing created them and README.md never named them, so a fresh install that followed it failed. A missing _imapkey stopped the daemon at startup; a missing _imapd or _imapauth let it start and then failed every connection. Each lookup's message promised an install script that does not exist. The parent now looks up all three before reading its configuration and exits with "unknown user" if one is missing, as bgpd, ldapd, ospfd, ripd, dvmrpd and rad do. The children keep their own lookups, now with the same message, and the three names are defined once in imapd.h. README.md gains the three useradd(8) lines, placed before the first mailbox account since imapduser(8) needs the _imapauth group. Change 5 of 6: Bound the listener's wait on keymgr; keep keymgr from blocking on one A listener asked keymgr for each private-key operation and then waited for the answer with no bound. A keymgr that stopped answering left every new TLS handshake waiting for ever: the login grace timer could not fire, and each stuck connection held an unauthenticated slot until new connections were refused on every port. Its answer is now awaited with poll(2) for at most 10 seconds over the whole exchange, as the store already waits for the parser and as relayd bounds its wait on its ca; after that the operation fails and the handshake ends. A failed RSA operation returned 0. RSA_private_encrypt(3) returns -1 on error, and under TLS 1.3's PSS padding libcrypto took the 0 as a signature of length 0 and sent it. Every failure now returns -1, so the handshake fails at the server with an alert. keymgr's end of each listener channel was a blocking socket, so a listener that sent requests and never read the replies could stop keymgr for everyone. keymgr now sets it non-blocking, as smtpd and relayd make their channels. The two wait loops are now one function. Change 6 of 6: Answer a plain BODY[...] and set \Seen as RFC 9051 requires Only BODY.PEEK[...] was answered. A plain BODY[...], the form RFC 9051 section 6.4.5 defines as setting \Seen and the form its own example uses, was dropped when the FETCH was parsed, so the FETCH ended NO and the message stayed unread. A plain BODY[...] is now answered as its BODY.PEEK form is, for every section that form supports, and sets \Seen. The account worker sets it before the walk, under the exclusive index lock, with the code STORE uses, which now lives in one function both call: one rename per message, one index write per FETCH, no mod-sequence change for a message already \Seen (RFC 7162 section 3.1.11), and the session's own change is not reported back to it. A message whose \Seen the FETCH set is answered with FLAGS, and once CONDSTORE is enabled with UID and MODSEQ, as RFC 7162 section 3.2.4 requires. In a mailbox opened by EXAMINE nothing is set (RFC 9051 section 6.3.3). RFC822, RFC822.HEADER and RFC822.TEXT, which RFC 9051 removed from its grammar, still end NO. commit - f940a630e00423a0043b84fbe5affd05ea548d6d commit + 65c7cf929f78f3280baaf04805e06a3d59b8b4f2 blob - 8c9c9862d382bfe7df46a4cc864956499b97ce3a blob + 5705b36cfc88caba91ffb269193d7c04ba72f507 --- README.md +++ README.md @@ -53,6 +53,18 @@ doas install -o root -g wheel -m 600 \ Every directive is documented inline in the sample file; the full reference is in `imapd(8)`'s FILES section. +## Creating the daemon accounts + +imapd drops privileges to three accounts of its own and will not start until all three exist. Create them once, before the first mailbox account, since `imapduser(8)` gives the credentials file to the `_imapauth` group: + +``` +doas useradd -c "IMAP Daemon" -d /var/empty -s /sbin/nologin _imapd +doas useradd -c "IMAP Auth" -d /var/empty -s /sbin/nologin _imapauth +doas useradd -c "IMAP Key Manager" -d /var/empty -s /sbin/nologin _imapkey +``` + +Each runs in a group of its own name, which `useradd(8)` creates by default unless `/etc/usermgmt.conf` sets `group` otherwise. + ## Creating an account imapd's users aren't real system accounts, `imapduser(8)` manages a bespoke credentials file (`username:passwordhash:uid:gid:maildir`, bcrypt via `crypt_checkpass(3)`) and the matching maildir ownership together, since no combination of `useradd(8)`/`userdel(8)` can safely keep both in sync: blob - 3011ab2b68aa384025c3db089fbdca6bfd0a28ad blob + 38f2e42d3962909ce6815417a407d690a860f0f2 --- contrib/imapd-teardown +++ contrib/imapd-teardown @@ -102,12 +102,8 @@ do_userdel() { echo " system account $_acct ($_label)" else userdel "$_acct" 2>/dev/null || true - # useradd's own default (no -g given) creates a same- - # named group alongside the account, confirmed - # against the pre-rename _openimap/_openimapd - # accounts. userdel(8) itself makes no mention of - # touching groups, so that group is cleaned up - # separately here, guarded against already being gone. + # useradd(8) makes a same-named group by default and + # userdel(8) leaves a primary group behind. groupdel "$_acct" 2>/dev/null || true echo "${0##*/}: removed account $_acct (and its group, if any)" 1>&2 fi @@ -129,6 +125,7 @@ list_targets() { do_rm "TLS private key" "$TLS_KEY" f do_userdel "auth's privilege-drop account" "_imapauth" do_userdel "listener's privilege-drop account" "_imapd" + do_userdel "keymgr's privilege-drop account" "_imapkey" if [ "$WIPE_MAIL" -eq 1 ]; then do_rm "mail spool -- REAL MAIL DATA" "$SPOOL_ROOT" r fi blob - 60bed43482a76e1603b8d438342657969d72b840 blob + cd483dbb205734c6ecdc3591bf43d05bdd60f354 --- contrib/imapduser.8 +++ contrib/imapduser.8 @@ -2,7 +2,7 @@ .\" .\" Written for the OpenIMAPD project. Public domain / no rights reserved. .\" -.Dd $Mdocdate: September 27 2026 $ +.Dd $Mdocdate: September 28 2026 $ .Dt IMAPDUSER 8 .Os .Sh NAME blob - f7a13b2a677ece560279a205ca1d8a83d19722fd blob + 7e0c578730502999d1136d4258eab53efa0c9d4b --- src/append_cmd.c +++ src/append_cmd.c @@ -339,8 +339,6 @@ void session_handle_mbox_appended(struct session *s, const struct imsg_mbox_appended *res) { - int appended_to_selected; - s->state = s->append_prev_state; if (res->error != MBOX_OP_OK) { @@ -357,22 +355,6 @@ session_handle_mbox_appended(struct session *s, return; } - /* 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; - else - appended_to_selected = - (strcmp(s->append_mailbox, s->selected_mailbox) == 0); - - - if (s->append_prev_state == SESSION_SELECTED && appended_to_selected) { - char buf[32]; - - snprintf(buf, sizeof(buf), "%u EXISTS", res->exists); - session_untagged(s, buf); - } - { char buf[96]; blob - 15fc87a7f400101a1cce03f1c440bc849107d673 blob + a9edf8444f61eb110fa9d62bde920efa5aa13ed5 --- src/auth.c +++ src/auth.c @@ -119,9 +119,8 @@ auth_main(void) init.cred_file[sizeof(init.cred_file) - 1] = '\0'; imsg_free(&imsg); - if ((pw = getpwnam("_imapauth")) == NULL) - fatalx("getpwnam _imapauth: no such user " - "(expected, not yet provisioned by an install script)"); + if ((pw = getpwnam(IMAPD_AUTH_USER)) == NULL) + fatalx("unknown user %s", IMAPD_AUTH_USER); if (strlcpy(chrootdir, init.cred_file, sizeof(chrootdir)) >= sizeof(chrootdir)) blob - 9bdf08c3065723587c43f234b227be200861d536 blob + 295dbfe792a76217279f47edda320d56449332aa --- src/auth_cmd.c +++ src/auth_cmd.c @@ -64,7 +64,16 @@ int cmd_noop(struct session *s, const char *tag, char *args) { (void)args; - session_reply(s, tag, "OK", "NOOP completed"); + + /* RFC 9051 SS6.1.2: the poll; the reply waits for the refresh */ + if (s->state != SESSION_SELECTED || + strlcpy(s->pending_tag, tag, sizeof(s->pending_tag)) >= + sizeof(s->pending_tag) || session_request_idle_refresh(s) == -1) { + session_reply(s, tag, "OK", "NOOP completed"); + return (1); + } + s->refresh_status = "OK"; + s->refresh_text = "NOOP completed"; return (1); } @@ -277,16 +286,21 @@ session_handle_auth_continuation(struct session *s, co int session_handle_idle_continuation(struct session *s, const char *line) { + int done; + s->idling = 0; + s->idle_refresh_again = 0; session_idle_poll_disarm(s); - if (strcasecmp(line, "DONE") != 0) { - session_reply(s, s->pending_tag, "BAD", - "expected DONE"); - return (1); + done = strcasecmp(line, "DONE") == 0; + s->refresh_status = done ? "OK" : "BAD"; + s->refresh_text = done ? "IDLE terminated" : "expected DONE"; + /* RFC 9051 SS7.5.1: an EXPUNGE in flight goes before the reply */ + if (!s->idle_refresh_pending) { + session_reply(s, s->pending_tag, s->refresh_status, + s->refresh_text); + s->refresh_status = NULL; } - - session_reply(s, s->pending_tag, "OK", "IDLE terminated"); return (1); } blob - a5728b7e6970463d7e16080c594c7fb8719cb862 blob + 3156bf94c2560eef628e87b9365638328a43164e --- src/envelope.c +++ src/envelope.c @@ -39,6 +39,8 @@ static size_t quoted_end(const char *, size_t, size_t); static size_t unquoted_find(const char *, size_t, size_t, const char *); +static int envelope_from_header(const char *, size_t, char *, size_t, + size_t *); int envbuf_append(char *buf, size_t bufsize, size_t *outlen, const char *data, @@ -327,7 +329,7 @@ envbuf_append_address_list(char *buf, size_t bufsize, int append_field_nstring(char *out, size_t outsize, size_t *outlen, - const char *hdrbuf, uint32_t hdrlen, const char *name) + const char *hdrbuf, size_t hdrlen, const char *name) { char *val; size_t vallen; @@ -343,59 +345,49 @@ append_field_nstring(char *out, size_t outsize, size_t } /* RFC 9051 SS7.5.2 ENVELOPE; Sender and Reply-To default to From */ -int -build_envelope(int fd, const char *basename, char **buf_out, - uint32_t *len_out) +static int +envelope_from_header(const char *hdr, size_t hdrlen, char *out, + size_t outsize, size_t *outlen) { - char *hdrbuf = NULL; - uint32_t hdrlen = 0; - char out[ENVELOPE_MAX]; - size_t outlen = 0; char from_formatted[ENVELOPE_MAX]; size_t from_len = 0; - *buf_out = NULL; - *len_out = 0; - - if (read_header_from_fd(fd, basename, &hdrbuf, &hdrlen) == -1) + if (envbuf_append(out, outsize, outlen, "(", 1) == -1) return (-1); - if (envbuf_append(out, sizeof(out), &outlen, "(", 1) == -1) - goto fail; - - if (append_field_nstring(out, sizeof(out), &outlen, hdrbuf, hdrlen, + if (append_field_nstring(out, outsize, outlen, hdr, hdrlen, "Date") == -1) - goto fail; - if (envbuf_append(out, sizeof(out), &outlen, " ", 1) == -1) - goto fail; - if (append_field_nstring(out, sizeof(out), &outlen, hdrbuf, hdrlen, + return (-1); + if (envbuf_append(out, outsize, outlen, " ", 1) == -1) + return (-1); + if (append_field_nstring(out, outsize, outlen, hdr, hdrlen, "Subject") == -1) - goto fail; - if (envbuf_append(out, sizeof(out), &outlen, " ", 1) == -1) - goto fail; + return (-1); + if (envbuf_append(out, outsize, outlen, " ", 1) == -1) + return (-1); { char *val; size_t vallen; - if (extract_header_field(hdrbuf, hdrlen, "From", &val, + if (extract_header_field(hdr, hdrlen, "From", &val, &vallen) == 0) { int rc = envbuf_append_address_list(from_formatted, sizeof(from_formatted), &from_len, val, vallen); free(val); if (rc == -1) - goto fail; + return (-1); } else { if (envbuf_append_str(from_formatted, sizeof(from_formatted), &from_len, "NIL") == -1) - goto fail; + return (-1); } } - if (envbuf_append(out, sizeof(out), &outlen, from_formatted, + if (envbuf_append(out, outsize, outlen, from_formatted, from_len) == -1) - goto fail; - if (envbuf_append(out, sizeof(out), &outlen, " ", 1) == -1) - goto fail; + return (-1); + if (envbuf_append(out, outsize, outlen, " ", 1) == -1) + return (-1); { static const char *const fallback_fields[] = @@ -407,26 +399,26 @@ build_envelope(int fd, const char *basename, char **bu size_t vallen; int used_value = 0; - if (extract_header_field(hdrbuf, hdrlen, + if (extract_header_field(hdr, hdrlen, fallback_fields[fi], &val, &vallen) == 0) { if (vallen > 0) { int rc = envbuf_append_address_list( - out, sizeof(out), &outlen, val, + out, outsize, outlen, val, vallen); used_value = 1; free(val); if (rc == -1) - goto fail; + return (-1); } else free(val); } if (!used_value && - envbuf_append(out, sizeof(out), &outlen, + envbuf_append(out, outsize, outlen, from_formatted, from_len) == -1) - goto fail; - if (envbuf_append(out, sizeof(out), &outlen, + return (-1); + if (envbuf_append(out, outsize, outlen, " ", 1) == -1) - goto fail; + return (-1); } } @@ -438,36 +430,57 @@ build_envelope(int fd, const char *basename, char **bu char *val; size_t vallen; - if (extract_header_field(hdrbuf, hdrlen, + if (extract_header_field(hdr, hdrlen, addr_fields[fi], &val, &vallen) == 0) { int rc = envbuf_append_address_list(out, - sizeof(out), &outlen, val, vallen); + outsize, outlen, val, vallen); free(val); if (rc == -1) - goto fail; + return (-1); } else { - if (envbuf_append_str(out, sizeof(out), - &outlen, "NIL") == -1) - goto fail; + if (envbuf_append_str(out, outsize, outlen, + "NIL") == -1) + return (-1); } - if (envbuf_append(out, sizeof(out), &outlen, + if (envbuf_append(out, outsize, outlen, " ", 1) == -1) - goto fail; + return (-1); } } - if (append_field_nstring(out, sizeof(out), &outlen, hdrbuf, hdrlen, + if (append_field_nstring(out, outsize, outlen, hdr, hdrlen, "In-Reply-To") == -1) - goto fail; - if (envbuf_append(out, sizeof(out), &outlen, " ", 1) == -1) - goto fail; - if (append_field_nstring(out, sizeof(out), &outlen, hdrbuf, hdrlen, + return (-1); + if (envbuf_append(out, outsize, outlen, " ", 1) == -1) + return (-1); + if (append_field_nstring(out, outsize, outlen, hdr, hdrlen, "Message-Id") == -1) - goto fail; + return (-1); - if (envbuf_append(out, sizeof(out), &outlen, ")", 1) == -1) - goto fail; + return (envbuf_append(out, outsize, outlen, ")", 1)); +} +int +build_envelope(int fd, const char *basename, char **buf_out, + uint32_t *len_out) +{ + char *hdrbuf = NULL; + uint32_t hdrlen = 0; + char out[ENVELOPE_MAX]; + size_t outlen = 0; + + *buf_out = NULL; + *len_out = 0; + + if (read_header_from_fd(fd, basename, &hdrbuf, &hdrlen) == -1) + return (-1); + if (envelope_from_header(hdrbuf, hdrlen, out, sizeof(out), + &outlen) == -1) { + log_warnx("session %u: message %s: formatted ENVELOPE exceeds " + "ENVELOPE_MAX, ENVELOPE skipped", session_id, basename); + free(hdrbuf); + return (-1); + } free(hdrbuf); if ((*buf_out = malloc(outlen)) == NULL) { @@ -478,12 +491,6 @@ build_envelope(int fd, const char *basename, char **bu memcpy(*buf_out, out, outlen); *len_out = (uint32_t)outlen; return (0); - -fail: - log_warnx("session %u: message %s: formatted ENVELOPE exceeds " - "ENVELOPE_MAX, ENVELOPE skipped", session_id, basename); - free(hdrbuf); - return (-1); } /* RFC 9051 SS7.5.2 BODYSTRUCTURE; no extension data */ @@ -545,17 +552,14 @@ build_body_structure(int depth, int *nparts_used, cons return (envbuf_append(out, outsize, outlen, ")", 1)); } - if (strcasecmp(type, "MESSAGE") == 0 && - (strcasecmp(subtype, "RFC822") == 0 || - strcasecmp(subtype, "GLOBAL") == 0)) - /* MESSAGE/RFC822 and MESSAGE/GLOBAL are not supported */ - return (-1); - { char *idval = NULL, *descval = NULL, *encval = NULL; - size_t idlen = 0, desclen = 0, enclen = 0; + size_t idlen = 0, desclen = 0, enclen = 0, ihdr; char encstr[40]; int is_text = (strcasecmp(type, "TEXT") == 0); + int is_msg = strcasecmp(type, "MESSAGE") == 0 && + (strcasecmp(subtype, "RFC822") == 0 || + strcasecmp(subtype, "GLOBAL") == 0); int rc = 0; if (envbuf_append(out, outsize, outlen, "(", 1) == -1) @@ -641,7 +645,18 @@ build_body_structure(int depth, int *nparts_used, cons return (-1); } - if (is_text) { + /* RFC 9051 SS7.5.2 body-type-msg: envelope, body, lines */ + if (is_msg && (bodylen == 0 || + find_header_body_split(body, bodylen, &ihdr) == -1 || + envbuf_append(out, outsize, outlen, " ", 1) == -1 || + envelope_from_header(body, ihdr, out, outsize, + outlen) == -1 || + envbuf_append(out, outsize, outlen, " ", 1) == -1 || + build_body_structure(depth + 1, nparts_used, body, ihdr, + body + ihdr, bodylen - ihdr, out, outsize, outlen) == -1)) + return (-1); + + if (is_text || is_msg) { size_t lines = 0, li; char numbuf2[32]; blob - fa0815a7ea209ae6c3da6c3e18505aba39d017ca blob + d2ffc23c6531d1c39bc449b58c991c54b69674d8 --- src/fetch_cmd.c +++ src/fetch_cmd.c @@ -323,12 +323,11 @@ parse_partial_suffix(const char *s, int *has_partial_o } static int -parse_body_peek_section_tok(const char *tok, uint32_t *attrs_inout, +parse_body_peek_section_tok(const char *bracket_start, uint32_t *attrs_inout, char *section_part_out, size_t section_part_outsize, int *has_partial_out, uint32_t *partial_start_out, uint32_t *partial_count_out, int *degraded_out, const char **errmsg) { - const char *bracket_start = tok + strlen("BODY.PEEK["); char *close; char inner[SECTION_PART_MAX]; const char *suffix; @@ -375,15 +374,15 @@ parse_body_peek_section_tok(const char *tok, uint32_t } static int -parse_body_peek_header_fields_tok(const char *tok, uint32_t *attrs_inout, +parse_body_peek_header_fields_tok(const char *sec, uint32_t *attrs_inout, int *header_fields_not_out, char *header_fields_out, size_t header_fields_outsize, char *header_fields_label_out, size_t header_fields_label_outsize, const char **errmsg) { - size_t toklen = strlen(tok); + size_t len = strlen(sec); char inner[HEADER_FIELDS_LABEL_MAX]; - if (toklen < strlen("BODY.PEEK[") + 1 || tok[toklen - 1] != ']') { + if (len < 1 || sec[len - 1] != ']') { *errmsg = "malformed HEADER.FIELDS section"; return (-1); } @@ -391,13 +390,12 @@ parse_body_peek_header_fields_tok(const char *tok, uin /* already captured one, ignore any further duplicates */ return (0); - if (toklen - strlen("BODY.PEEK[") - 1 >= sizeof(inner)) { + if (len - 1 >= sizeof(inner)) { *errmsg = "HEADER.FIELDS section too long"; return (-1); } - memcpy(inner, tok + strlen("BODY.PEEK["), - toklen - strlen("BODY.PEEK[") - 1); - inner[toklen - strlen("BODY.PEEK[") - 1] = '\0'; + memcpy(inner, sec, len - 1); + inner[len - 1] = '\0'; if (parse_header_fields_att(inner, header_fields_not_out, header_fields_out, header_fields_outsize) == -1) { @@ -413,6 +411,17 @@ parse_body_peek_header_fields_tok(const char *tok, uin return (1); } +static const char * +body_section(const char *tok, int *plain) +{ + *plain = strncasecmp(tok, "BODY[", 5) == 0; + if (*plain) + return (tok + 5); + if (strncasecmp(tok, "BODY.PEEK[", 10) == 0) + return (tok + 10); + return (NULL); +} + /* RFC 9051 SS6.4.5 fetch-att; unsupported items are skipped */ int parse_fetch_atts(char *spec, uint32_t *attrs_out, int *degraded_out, @@ -424,9 +433,10 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int uint32_t *partial_count_out, const char **errmsg) { char *p, *tok, *save; + const char *sec; size_t len; uint32_t attrs = 0; - int degraded = 0; + int degraded = 0, plain, d; int has_partial = 0; uint32_t partial_start = 0, partial_count = 0; @@ -484,25 +494,27 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int } else if (strcasecmp(tok, "MODSEQ") == 0) { /* 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_BODY_HEADER; - } else if (strncasecmp(tok, "BODY.PEEK[", - strlen("BODY.PEEK[")) == 0 && - strncasecmp(tok, "BODY.PEEK[HEADER.FIELDS", - strlen("BODY.PEEK[HEADER.FIELDS")) != 0) { - if (parse_body_peek_section_tok(tok, &attrs, + } else if ((sec = body_section(tok, &plain)) != NULL) { + d = 0; + if (strcasecmp(sec, "HEADER]") == 0) + attrs |= MBOX_FETCH_BODY_HEADER; + else if (strncasecmp(sec, "HEADER.FIELDS", + strlen("HEADER.FIELDS")) == 0) { + if (parse_body_peek_header_fields_tok(sec, + &attrs, header_fields_not_out, + header_fields_out, header_fields_outsize, + header_fields_label_out, + header_fields_label_outsize, + errmsg) == -1) + return (-1); + } else if (parse_body_peek_section_tok(sec, &attrs, section_part_out, section_part_outsize, &has_partial, &partial_start, &partial_count, - °raded, errmsg) == -1) + &d, errmsg) == -1) return (-1); - } else if (strncasecmp(tok, "BODY.PEEK[HEADER.FIELDS", - strlen("BODY.PEEK[HEADER.FIELDS")) == 0) { - if (parse_body_peek_header_fields_tok(tok, &attrs, - header_fields_not_out, header_fields_out, - header_fields_outsize, header_fields_label_out, - header_fields_label_outsize, errmsg) == -1) - return (-1); + if (plain && !d) + attrs |= MBOX_FETCH_SET_SEEN; + degraded |= d; } else if (strcasecmp(tok, "ENVELOPE") == 0) { /* SS7.5.2; no .PEEK, no \Seen effect */ attrs |= MBOX_FETCH_ENVELOPE; @@ -526,7 +538,7 @@ parse_fetch_atts(char *spec, uint32_t *attrs_out, int if (attrs == 0) { *errmsg = "cannot fetch that message content yet, " "supported: FLAGS/UID/INTERNALDATE/RFC822.SIZE/MODSEQ/" - "ENVELOPE/(BODY|BODYSTRUCTURE)/BODY.PEEK[...]"; + "ENVELOPE/(BODY|BODYSTRUCTURE)/BODY[...]/BODY.PEEK[...]"; return (-2); } @@ -629,6 +641,7 @@ session_send_fetch_response(struct session *s, char date[40]; size_t len = 0; int need_sp = 0; + int cs = meta->seen_set && s->condstore_enabled; int have_header = (s->fetch_attrs & (MBOX_FETCH_BODY_HEADER | MBOX_FETCH_HEADER_FIELDS)) && s->pending_header_found; int have_body = (s->fetch_attrs & @@ -642,11 +655,12 @@ session_send_fetch_response(struct session *s, fetch_append(buf, sizeof(buf), &len, "%u FETCH (", meta->seqno); - if (s->fetch_attrs & MBOX_FETCH_FLAGS) { + /* RFC 9051 SS6.4.5, RFC 7162 SS3.2.4: report a \Seen this set */ + if ((s->fetch_attrs & MBOX_FETCH_FLAGS) || meta->seen_set) { fetch_append(buf, sizeof(buf), &len, "FLAGS (%s)", meta->flags); need_sp = 1; } - if (s->fetch_attrs & MBOX_FETCH_UID) { + if ((s->fetch_attrs & MBOX_FETCH_UID) || cs) { fetch_append(buf, sizeof(buf), &len, "%sUID %u", need_sp ? " " : "", meta->uid); need_sp = 1; @@ -662,7 +676,7 @@ session_send_fetch_response(struct session *s, need_sp ? " " : "", (unsigned long long)meta->size); need_sp = 1; } - if (s->fetch_attrs & MBOX_FETCH_MODSEQ) { + if ((s->fetch_attrs & MBOX_FETCH_MODSEQ) || cs) { /* RFC 7162 SS3.1.4.2 fetch-mod-resp */ fetch_append(buf, sizeof(buf), &len, "%sMODSEQ (%llu)", need_sp ? " " : "", (unsigned long long)meta->modseq); @@ -1022,12 +1036,14 @@ fetch_dispatch(struct session *s, const char *tag, cha session_reply(s, tag, "NO", errmsg); return (1); } + /* RFC 9051 SS6.3.3: EXAMINE permits no change, \Seen included */ + if (s->mbox_readonly) + attrs &= ~MBOX_FETCH_SET_SEEN; if (degraded) log_debug("session %u: %s: one or more unsupported message " - "data items silently dropped (plain BODY[...]/BODY[]" - "/BODY.PEEK[] with MESSAGE/RFC822|GLOBAL or MULTIPART" - " nested numbering/RFC822[.HEADER/.TEXT]), answering " - "with whatever was recognized", s->id, cmdname); + "data items not returned (a part number before " + "HEADER/TEXT/MIME, RFC822[.HEADER/.TEXT]), " + "answering the rest, then NO", s->id, cmdname); memset(&req, 0, sizeof(req)); req.attrs = attrs; @@ -1136,7 +1152,8 @@ fetch_dispatch(struct session *s, const char *tag, cha } s->fetch_attrs = req.attrs; s->cmd_by_uid = by_uid; - s->fetch_incomplete = 0; + /* RFC 9051 SS6.4.5: "NO - fetch error: can't fetch that data" */ + s->fetch_incomplete = degraded; s->state = SESSION_FETCHING; if (!send_mbox_request(s, IMSG_MBOX_FETCH, cmdname, "IMSG_MBOX_FETCH", blob - e17940a4005eba802bafef76c6836b6948719c4f blob + 89cc5d2c5094633ba2e50057c592a262ca6ef0ec --- src/imapd.8 +++ src/imapd.8 @@ -3,7 +3,7 @@ .\" Written for the OpenIMAPD project. Public domain / no rights reserved, .\" matching the project's ports-oriented, OpenBSD-base-inclusion goal. .\" -.Dd $Mdocdate: September 27 2026 $ +.Dd $Mdocdate: September 28 2026 $ .Dt IMAPD 8 .Os .Sh NAME blob - 8b8d979bbea17a86332be12e7642d65a0584705f blob + 6eb1a0411a611331b59b79c7139120c4135ecfc8 --- src/imapd.h +++ src/imapd.h @@ -28,8 +28,12 @@ #include #include -#define IMAPD_VERSION "0.1.7" +#define IMAPD_VERSION "0.1.8" +#define IMAPD_USER "_imapd" +#define IMAPD_AUTH_USER "_imapauth" +#define IMAPD_KEY_USER "_imapkey" + enum openimap_proc_type { PROC_PARENT, PROC_LISTENER, @@ -121,10 +125,11 @@ enum imsg_type { IMSG_MBOX_SELECT_VANISHED, IMSG_MBOX_STORE_MODIFIED, - /* RFC 9051 SS6.3.13 (IDLE) */ + /* RFC 9051 SS6.3.13 (IDLE); the VIEW ones go with any command */ IMSG_MBOX_IDLE_REFRESH, - IMSG_MBOX_IDLE_EXPUNGE, - IMSG_MBOX_IDLE_FETCH, + IMSG_MBOX_VIEW_EXPUNGE, + IMSG_MBOX_VIEW_FETCH, + IMSG_MBOX_VIEW_EXISTS, IMSG_MBOX_IDLE_REFRESHED, /* RFC 9051 SS6.3.9 (LIST) */ @@ -260,6 +265,7 @@ enum mbox_op_error { MBOX_OP_ERR_NO_SUCH_MAILBOX, MBOX_OP_ERR_ALREADY_EXISTS, MBOX_OP_ERR_BUSY, + MBOX_OP_ERR_EXPUNGEISSUED, }; /* QRESYNC select-param (RFC 7162 SS3.2.5). */ @@ -331,6 +337,7 @@ struct imsg_mbox_select_vanished { #define MBOX_FETCH_ENVELOPE (1U << 9) #define MBOX_FETCH_BODYSTRUCTURE (1U << 10) /* and bare BODY */ #define MBOX_FETCH_BODY_PART (1U << 11) +#define MBOX_FETCH_SET_SEEN (1U << 12) #define SECTION_PART_MAX 40 @@ -351,6 +358,7 @@ struct imsg_mbox_select_vanished { /* relayd's RELAY_TLS_PRIV_TIMEOUT bounds a similar wait */ #define PARSER_REPLY_TIMEOUT_SEC 10 +#define KEYMGR_REPLY_TIMEOUT_SEC 10 /* SIGXCPU at soft, SIGKILL at hard (sys/kern/kern_resource.c) */ #define PARSER_CPU_SOFT_SEC 8 @@ -462,6 +470,7 @@ struct imsg_mbox_fetch_meta { int64_t internaldate; /* from the basename, not mtime */ char flags[MBOX_FLAGS_MAX]; uint64_t modseq; + int seen_set; }; /* sent just before the same message's IMSG_MBOX_FETCH_META */ @@ -577,7 +586,6 @@ struct imsg_mbox_appended { enum mbox_op_error error; /* NO_SUCH_MAILBOX gets [TRYCREATE] */ uint32_t uidvalidity; uint32_t uid; /* APPENDUID, with uidvalidity */ - uint32_t exists; }; /* RFC 9051 SS6.4.4 search-program, as a postfix array of nodes */ @@ -662,25 +670,17 @@ struct imsg_mbox_search_match { uint64_t modseq; /* RFC 7162 SS3.1.6 */ }; -/* RFC 9051 SS6.3.13 IDLE: seeded once, then a diff per poll */ -struct imsg_mbox_idle_refresh { - /* adopt the current state as baseline, report nothing */ - int seed; -}; - -struct imsg_mbox_idle_expunge { - uint32_t seqno; -}; - +/* RFC 9051 SS6.3.13 IDLE, SS6.1.2 NOOP; EXPUNGE is imsg_mbox_expunged */ struct imsg_mbox_idle_refreshed { int ok; - /* the probe saw no change; the fields below are zero */ - int unchanged; - int exists_changed; - uint32_t exists; + int unchanged; /* the probe saw no change */ int busy; /* lock held elsewhere; ok says if it seeded */ }; +struct imsg_mbox_view_exists { + uint32_t exists; +}; + /* RFC 9051 SS6.3.4/SS6.3.5; store.c re-validates the name */ struct imsg_mbox_create { char mailbox[MBOX_NAME_MAX]; blob - cf71d95cf2cc0880c154fda71df483e955db9cae blob + fe42950141044244657ac834101c6d2d9009749c --- src/index.c +++ src/index.c @@ -873,53 +873,177 @@ idle_baseline_reset(struct store_session *ss) base->uids = NULL; base->n = 0; base->modseq = 0; + base->ghosts = 0; base->valid = 0; } +static size_t +uid_find(const uint32_t *list, size_t n, uint32_t uid) +{ + size_t lo = 0, hi = n, mid; + + while (lo < hi) { + mid = lo + (hi - lo) / 2; + if (list[mid] < uid) + lo = mid + 1; + else + hi = mid; + } + return ((lo < n && list[lo] == uid) ? lo : n); +} + +/* RFC 9051 SS7.5.1: once told, never again, so drop it from the baseline */ +int +send_expunged(struct store_session *ss, uint32_t seqno, uint32_t uid) +{ + struct store_idle_baseline *base = &ss->idle_baseline; + struct imsg_mbox_expunged exp; + size_t at; + + at = uid_find(base->uids, base->n, uid); + memset(&exp, 0, sizeof(exp)); + exp.seqno = at < base->n ? (uint32_t)at + 1 : seqno; + exp.uid = uid; + if (imsg_compose(&ss->iev.ibuf, IMSG_MBOX_EXPUNGED, 0, 0, -1, &exp, + sizeof(exp)) == -1) { + log_warn("session %u: imsg_compose IMSG_MBOX_EXPUNGED", + session_id); + return (-1); + } + if (at < base->n) { + memmove(&base->uids[at], &base->uids[at + 1], + (base->n - at - 1) * sizeof(*base->uids)); + base->n--; + } + return (0); +} + +uint32_t +view_seqno(const struct store_session *ss, uint32_t uid, uint32_t fallback) +{ + const struct store_idle_baseline *base = &ss->idle_baseline; + size_t at; + + if (!base->valid) + return (fallback); + at = uid_find(base->uids, base->n, uid); + return (at < base->n ? (uint32_t)at + 1 : fallback); +} + +/* a sequence set, read through the view, as UIDs; *by_uid then set */ +void +view_resolve(const struct store_session *ss, const struct seq_range *in, + uint32_t nin, struct seq_range *out, uint32_t *nout, int *by_uid) +{ + const struct store_idle_baseline *base = &ss->idle_baseline; + uint32_t i, lo, hi, tmp, n = 0; + + if (*by_uid || !base->valid) { + for (i = 0; i < nin; i++) + out[i] = in[i]; + *nout = nin; + return; + } + for (i = 0; i < nin; i++) { + lo = in[i].lo_is_star ? (uint32_t)base->n : in[i].lo; + hi = in[i].hi_is_star ? (uint32_t)base->n : in[i].hi; + if (lo > hi) { + tmp = lo; + lo = hi; + hi = tmp; + } + if (lo < 1) + lo = 1; + if (hi > base->n) + hi = (uint32_t)base->n; + if (lo > hi) + continue; + memset(&out[n], 0, sizeof(out[n])); + out[n].lo = base->uids[lo - 1]; + out[n].hi = base->uids[hi - 1]; + n++; + } + *nout = n; + *by_uid = 1; +} + +/* RFC 2180 SS4.1.2: does the set name a message expunged, not yet told? */ +int +view_names_ghost(const struct store_session *ss, const struct mbox_index *idx, + const struct seq_range *uids, uint32_t nuids) +{ + const struct store_idle_baseline *base = &ss->idle_baseline; + struct index_rec rec; + size_t i, j = 0; + + if (!base->valid || base->ghosts == 0) + return (0); + memset(&rec, 0, sizeof(rec)); + for (i = 0; i < base->n; i++) { + if (!seqset_contains(uids, nuids, base->uids[i])) + continue; + for (; j < idx->nlines; j++) + if (index_parse_line(idx->lines[j], &rec) == 0 && + rec.uid >= base->uids[i]) + break; + if (j == idx->nlines || rec.uid != base->uids[i]) + return (1); + } + return (0); +} + /* 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) +view_merge(const uint32_t *old, size_t oldn, const uint32_t *cur, + size_t curn, int expunge_ok, uint32_t *view, size_t *viewn, + struct imsgev *iev) { - struct imsg_mbox_idle_expunge item; - size_t i, j = 0, gone = 0; - uint32_t seqno = 1; + struct imsg_mbox_expunged item; + size_t i = 0, j = 0, n = 0, gone = 0; - for (i = 0; i < oldn; i++) { - while (j < curn && cur[j] < old[i]) - j++; + while (i < oldn || j < curn) { + if (j < curn && (i == oldn || cur[j] < old[i])) { + view[n++] = cur[j++]; + continue; + } if (j < curn && cur[j] == old[i]) { + view[n++] = old[i++]; j++; - seqno++; continue; } + if (!expunge_ok) { + view[n++] = old[i++]; + continue; + } memset(&item, 0, sizeof(item)); - item.seqno = seqno; + item.seqno = (uint32_t)n + 1; + item.uid = old[i++]; gone++; - if (imsg_compose(&iev->ibuf, IMSG_MBOX_IDLE_EXPUNGE, 0, 0, -1, + if (imsg_compose(&iev->ibuf, IMSG_MBOX_VIEW_EXPUNGE, 0, 0, -1, &item, sizeof(item)) == -1) log_warn("session %u: imsg_compose " - "IMSG_MBOX_IDLE_EXPUNGE", session_id); + "IMSG_MBOX_VIEW_EXPUNGE", session_id); } + *viewn = n; return (gone); } /* 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 store_session *ss) +view_send_flag_fetches(const struct mbox_index *idx, const uint32_t *old, + size_t oldn, uint64_t since, const uint32_t *view, size_t viewn, + struct store_session *ss) { struct imsgev *iev = &ss->iev; struct imsg_mbox_fetch_meta meta; struct index_rec rec; char suffix[64]; off_t size; - size_t i, j = 0, seqno = 0, sent = 0; + size_t i, j = 0, sent = 0; for (i = 0; i < idx->nlines; i++) { if (index_parse_line(idx->lines[i], &rec) == -1) continue; /* malformed, skipped as everywhere */ - seqno++; /* position after the EXPUNGEs above */ if (rec.modseq <= since) continue; while (j < oldn && old[j] < rec.uid) @@ -934,16 +1058,16 @@ idle_send_flag_fetches(const struct mbox_index *idx, c continue; } memset(&meta, 0, sizeof(meta)); - meta.seqno = (uint32_t)seqno; + meta.seqno = (uint32_t)uid_find(view, viewn, rec.uid) + 1; meta.uid = rec.uid; meta.modseq = rec.modseq; build_flags_string(suffix, rec.keywords, meta.flags, sizeof(meta.flags)); sent++; - if (imsg_compose(&iev->ibuf, IMSG_MBOX_IDLE_FETCH, 0, 0, -1, + if (imsg_compose(&iev->ibuf, IMSG_MBOX_VIEW_FETCH, 0, 0, -1, &meta, sizeof(meta)) == -1) log_warn("session %u: imsg_compose " - "IMSG_MBOX_IDLE_FETCH", session_id); + "IMSG_MBOX_VIEW_FETCH", session_id); } return (sent); } @@ -1015,10 +1139,75 @@ idle_uid_list(const struct mbox_index *idx, uint32_t * return (0); } +void +idle_baseline_seed(struct store_session *ss, const struct mbox_index *idx) +{ + struct store_idle_baseline *base = &ss->idle_baseline; + uint32_t *list; + uint64_t modseq; + size_t n; + + idle_baseline_reset(ss); + if (idle_uid_list(idx, &list, &n, &modseq) == -1) + return; + base->uids = list; + base->n = n; + base->modseq = modseq; + base->valid = 1; +} + +/* RFC 9051 SS5.2, SS7.5.1: what changed; EXPUNGE only if expunge_ok */ +int +view_sync(struct store_session *ss, const struct mbox_index *idx, + int expunge_ok) +{ + struct store_idle_baseline *base = &ss->idle_baseline; + struct imsg_mbox_view_exists ve; + uint32_t *cur, *view; + uint64_t seen; + size_t curn, viewn, gone; + + if (!base->valid) { + idle_baseline_seed(ss, idx); + return (base->valid ? 0 : -1); + } + /* index_append() and every removal raise HIGHESTMODSEQ */ + if (idx->nlines == base->n && idx->highestmodseq <= base->modseq) + return (0); + if (idle_uid_list(idx, &cur, &curn, &seen) == -1) + return (-1); + if ((view = reallocarray(NULL, base->n + curn + 1, + sizeof(*view))) == NULL) { + log_warn("session %u: view: reallocarray", session_id); + free(cur); + return (-1); + } + gone = view_merge(base->uids, base->n, cur, curn, expunge_ok, view, + &viewn, &ss->iev); + (void)view_send_flag_fetches(idx, base->uids, base->n, base->modseq, + view, viewn, ss); + /* after EXPUNGEs too, as RFC 9051 SS6.3.13's example does */ + if (gone > 0 || viewn != base->n) { + memset(&ve, 0, sizeof(ve)); + ve.exists = (uint32_t)viewn; + if (imsg_compose(&ss->iev.ibuf, IMSG_MBOX_VIEW_EXISTS, 0, 0, + -1, &ve, sizeof(ve)) == -1) + log_warn("session %u: imsg_compose " + "IMSG_MBOX_VIEW_EXISTS", session_id); + } + free(cur); + free(base->uids); + base->uids = view; + base->n = viewn; + base->ghosts = viewn - curn; + if (seen > base->modseq) + base->modseq = seen; + return (0); +} + /* lockless: index_save() only ever replaces the index by rename(2) */ static int -idle_seed_unlocked(struct store_session *ss, - struct imsg_mbox_idle_refreshed *reply) +idle_seed_unlocked(struct store_session *ss) { struct store_idle_baseline *base = &ss->idle_baseline; struct mbox_index idx; @@ -1049,28 +1238,26 @@ idle_seed_unlocked(struct store_session *ss, base->uids = list; base->n = n; base->modseq = modseq; + base->ghosts = 0; base->valid = 1; - reply->exists = (uint32_t)n; return (0); } void -handle_mbox_idle_refresh(const struct imsg_mbox_idle_refresh *req, - struct store_session *ss) +handle_mbox_idle_refresh(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; - size_t newn = 0, gone = 0, changed = 0; int pending = 0, seeded, locked; - uint32_t *newlist; - uint64_t seen_modseq = 0; memset(&reply, 0, sizeof(reply)); + if (!ss->mailbox_selected) + goto send; /* a seed adopts what it finds rather than reporting it */ - seeded = req->seed || !base->valid; + seeded = !base->valid; if (idle_probe_unchanged(ss)) { reply.ok = 1; @@ -1110,31 +1297,12 @@ handle_mbox_idle_refresh(const struct imsg_mbox_idle_r } } - if (idle_uid_list(&idx, &newlist, &newn, &seen_modseq) == -1) { - index_free(&idx); - index_lock_release(&il); - goto send; - } + if (view_sync(ss, &idx, 1) == 0) + reply.ok = 1; - if (!seeded) { - 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(base->uids); - base->uids = newlist; - base->n = newn; - base->modseq = seen_modseq; - base->valid = 1; - - reply.ok = 1; - reply.exists = (uint32_t)newn; - - log_debug("session %u: idle refresh: %zu uid(s), highestmodseq %llu, " - "%zu expunge(s), %zu flag change(s)%s%s", session_id, newn, - (unsigned long long)idx.highestmodseq, gone, changed, + log_debug("session %u: idle refresh: %zu uid(s) in view, " + "highestmodseq %llu%s%s", session_id, base->n, + (unsigned long long)idx.highestmodseq, seeded ? " (baseline seeded)" : "", pending ? " (escalated to LOCK_EX, indexed new delivery)" : ""); @@ -1145,7 +1313,7 @@ handle_mbox_idle_refresh(const struct imsg_mbox_idle_r busy: /* RFC 9051 SS7.5.1: a seed cannot skip, or EXPUNGEs repeat */ reply.busy = 1; - if (seeded && idle_seed_unlocked(ss, &reply) == 0) { + if (seeded && idle_seed_unlocked(ss) == 0) { reply.ok = 1; /* so the next poll reads the index, not the probe */ idle_probe_reset(ss); blob - 2c1312d3accc2cbb04903f56af503731f6550e94 blob + d1f56a65c1e211d40c9520058aff64700ffd3ce8 --- src/keymgr.c +++ src/keymgr.c @@ -54,6 +54,7 @@ #include #include +#include #include #include #include @@ -179,9 +180,8 @@ keymgr_main(void) explicit_bzero(key_buf, sizeof(key_buf)); keymgr_got_init = 1; - if ((pw = getpwnam("_imapkey")) == NULL) - fatalx("getpwnam _imapkey: no such user " - "(expected, not yet provisioned by an install script)"); + if ((pw = getpwnam(IMAPD_KEY_USER)) == NULL) + fatalx("unknown user %s", IMAPD_KEY_USER); /* the key arrives over imsg; nothing is read from disk */ if (chroot("/var/empty") == -1) @@ -367,11 +367,20 @@ keymgr_dispatch_parent(int fd, short event, void *arg) uint32_t sess_id = imsg_get_id(&imsg); int peer_fd = imsg_get_fd(&imsg); struct keymgr_peer *kp; + int flags; if (peer_fd == -1) { log_warnx("IMSG_SETUP_PEER carried no fd"); break; } + /* a listener that stops reading must not stop keymgr */ + if ((flags = fcntl(peer_fd, F_GETFL)) == -1 || + fcntl(peer_fd, F_SETFL, flags | O_NONBLOCK) == -1) { + log_warn("session %u: fcntl O_NONBLOCK", + sess_id); + close(peer_fd); + break; + } if ((kp = calloc(1, sizeof(*kp))) == NULL) { log_warn("calloc"); close(peer_fd); blob - b83f6daf82d8b6e6b1b0d41ecc8fb9295a45c899 blob + 3726d921cf0beb10b9d3633754337174daf18e4a --- src/listener.c +++ src/listener.c @@ -168,6 +168,57 @@ static const EC_KEY_METHOD *keymgr_ecdsa_default; static EC_KEY_METHOD *keymgr_ecdsae_method; static int +keymgr_wait_reply(uint32_t type, uint32_t id, const char *op, + struct imsg *imsg) +{ + struct timespec start, now; + struct pollfd pfd; + ssize_t n; + int ms; + + if (clock_gettime(CLOCK_MONOTONIC, &start) == -1) + fatal("%s: clock_gettime", op); + for (;;) { + if ((n = imsgbuf_get(&keymgr_ibuf, imsg)) == -1) + fatal("%s: imsg_get", op); + if (n != 0) { + if (imsg_get_type(imsg) == type && + imsg_get_id(imsg) == id) + return (0); + log_warnx("%s: unexpected reply type %u id %u " + "(wanted %u/%u)", op, imsg_get_type(imsg), + imsg_get_id(imsg), type, id); + imsg_free(imsg); + continue; + } + if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) + fatal("%s: clock_gettime", op); + ms = KEYMGR_REPLY_TIMEOUT_SEC * 1000 - + (int)((now.tv_sec - start.tv_sec) * 1000 + + (now.tv_nsec - start.tv_nsec) / 1000000); + if (ms <= 0) { + log_warnx("%s: keymgr did not answer within %d " + "seconds", op, KEYMGR_REPLY_TIMEOUT_SEC); + return (-1); + } + pfd.fd = keymgr_ibuf.fd; + pfd.events = POLLIN; + if ((n = poll(&pfd, 1, ms)) == -1) { + if (errno == EINTR) + continue; + fatal("%s: poll", op); + } + if (n == 0) + continue; + if ((n = imsgbuf_read(&keymgr_ibuf)) == -1) + fatal("%s: imsgbuf_read", op); + if (n == 0) + fatalx("%s: keymgr closed channel", op); + } +} + +/* RSA_private_encrypt(3): -1 on error */ +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) { @@ -177,19 +228,18 @@ keymgr_forward_rsa(uint32_t type, const char *hash, co struct imsg imsg; static uint32_t reqid; uint32_t id; - ssize_t n; - int done, ret; + int ret; if (fromlen < 0 || (size_t)fromlen > KEYMGR_DATA_MAX) { log_warnx("keymgr_forward_rsa: %d bytes over KEYMGR_DATA_MAX", fromlen); - return (0); + return (-1); } memset(&req, 0, sizeof(req)); if (strlcpy(req.hash, hash, sizeof(req.hash)) >= sizeof(req.hash)) { log_warnx("keymgr_forward_rsa: pubkey hash too long"); - return (0); + return (-1); } req.padding = (uint32_t)padding; req.fromlen = (uint32_t)fromlen; @@ -201,50 +251,27 @@ keymgr_forward_rsa(uint32_t type, const char *hash, co if (imsg_compose(&keymgr_ibuf, type, id, 0, -1, combined, sizeof(req) + (size_t)fromlen) == -1) { log_warnx("keymgr_forward_rsa: imsg_compose"); - return (0); + return (-1); } if (imsgbuf_flush(&keymgr_ibuf) == -1) fatal("keymgr_forward_rsa: imsgbuf_flush"); - ret = 0; - done = 0; - while (!done) { - if ((n = imsgbuf_get(&keymgr_ibuf, &imsg)) == -1) - fatal("keymgr_forward_rsa: imsg_get"); - if (n == 0) { - if ((n = imsgbuf_read(&keymgr_ibuf)) == -1) - fatal("keymgr_forward_rsa: imsgbuf_read"); - if (n == 0) - fatalx("keymgr_forward_rsa: keymgr closed " - "channel"); - continue; - } - if (imsg_get_type(&imsg) != type || - imsg_get_id(&imsg) != id) { - log_warnx("keymgr_forward_rsa: unexpected reply " - "type %u id %u (wanted %u/%u)", - 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("keymgr_forward_rsa: bad reply header"); - imsg_free(&imsg); - break; - } - /* 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) - log_warnx("keymgr_forward_rsa: bad reply " - "data"); - else - ret = (int)rep.tolen; - } + if (keymgr_wait_reply(type, id, "keymgr_forward_rsa", &imsg) == -1) + return (-1); + if (imsg_get_buf(&imsg, &rep, sizeof(rep)) == -1) { + log_warnx("keymgr_forward_rsa: bad reply header"); imsg_free(&imsg); - done = 1; + return (-1); } + ret = -1; + if (rep.ok && rep.tolen <= tosize && + imsg_get_len(&imsg) == rep.tolen) { + if (imsg_get_buf(&imsg, to, rep.tolen) == -1) + log_warnx("keymgr_forward_rsa: bad reply data"); + else + ret = (int)rep.tolen; + } + imsg_free(&imsg); return (ret); } @@ -261,8 +288,6 @@ keymgr_forward_ecdsa(const char *hash, const unsigned ECDSA_SIG *sig = NULL; static uint32_t reqid; uint32_t id; - ssize_t n; - int done; const unsigned char *sigp; if (dgst_len < 0 || (size_t)dgst_len > KEYMGR_DATA_MAX) { @@ -290,45 +315,24 @@ keymgr_forward_ecdsa(const char *hash, const unsigned if (imsgbuf_flush(&keymgr_ibuf) == -1) fatal("keymgr_forward_ecdsa: imsgbuf_flush"); - done = 0; - while (!done) { - if ((n = imsgbuf_get(&keymgr_ibuf, &imsg)) == -1) - fatal("keymgr_forward_ecdsa: imsg_get"); - if (n == 0) { - if ((n = imsgbuf_read(&keymgr_ibuf)) == -1) - fatal("keymgr_forward_ecdsa: imsgbuf_read"); - if (n == 0) - fatalx("keymgr_forward_ecdsa: keymgr closed " - "channel"); - continue; - } - if (imsg_get_type(&imsg) != IMSG_KEYMGR_ECDSA_SIGN || - imsg_get_id(&imsg) != id) { - log_warnx("keymgr_forward_ecdsa: unexpected reply " - "type %u id %u (wanted %u/%u)", - imsg_get_type(&imsg), imsg_get_id(&imsg), - IMSG_KEYMGR_ECDSA_SIGN, id); - imsg_free(&imsg); - continue; - } - if (imsg_get_buf(&imsg, &rep, sizeof(rep)) == -1) { - log_warnx("keymgr_forward_ecdsa: bad reply header"); - imsg_free(&imsg); - break; - } - if (rep.ok && rep.tolen <= KEYMGR_DATA_MAX && - imsg_get_len(&imsg) == rep.tolen) { - if (imsg_get_buf(&imsg, sigbuf, rep.tolen) == -1) - log_warnx("keymgr_forward_ecdsa: bad reply " - "data"); - else { - sigp = sigbuf; - d2i_ECDSA_SIG(&sig, &sigp, (long)rep.tolen); - } - } + if (keymgr_wait_reply(IMSG_KEYMGR_ECDSA_SIGN, id, + "keymgr_forward_ecdsa", &imsg) == -1) + return (NULL); + if (imsg_get_buf(&imsg, &rep, sizeof(rep)) == -1) { + log_warnx("keymgr_forward_ecdsa: bad reply header"); imsg_free(&imsg); - done = 1; + return (NULL); } + if (rep.ok && rep.tolen <= KEYMGR_DATA_MAX && + imsg_get_len(&imsg) == rep.tolen) { + if (imsg_get_buf(&imsg, sigbuf, rep.tolen) == -1) + log_warnx("keymgr_forward_ecdsa: bad reply data"); + else { + sigp = sigbuf; + d2i_ECDSA_SIG(&sig, &sigp, (long)rep.tolen); + } + } + imsg_free(&imsg); return (sig); } @@ -547,9 +551,8 @@ listener_main(void) fatalx("listener: IMSG_LISTENER_SESSION_INIT carried no " "client fd"); - if ((pw = getpwnam("_imapd")) == NULL) - fatalx("getpwnam _imapd: no such user " - "(expected, not yet provisioned by an install script)"); + if ((pw = getpwnam(IMAPD_USER)) == NULL) + fatalx("unknown user %s", IMAPD_USER); if (chroot("/var/empty") == -1) fatal("chroot /var/empty"); @@ -1504,6 +1507,8 @@ parse_command_line(char *line, char **tag, char **name static int session_is_busy(const struct session *s) { + if (s->refresh_status != NULL) + return (1); switch (s->state) { case SESSION_SELECTING: case SESSION_FETCHING: blob - f33cba5aa3d76a8ba44c5945fc1a086bc3970e2d blob + 272fe1eb376922247b102eb04ff8e4fc7e165ebc --- src/listener.h +++ src/listener.h @@ -186,7 +186,9 @@ struct session { /* 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; + /* the tagged reply IDLE or NOOP owes once the refresh is in */ + const char *refresh_status; + const char *refresh_text; struct event idle_ev; struct event grace_ev; @@ -312,13 +314,11 @@ void session_handle_select_fetch(struct session *, const struct imsg_mbox_fetch_meta *); void session_handle_store_modified(struct session *, struct imsg_mbox_store_modified *); -void session_handle_idle_expunge(struct session *, - const struct imsg_mbox_idle_expunge *); -void session_handle_idle_fetch(struct session *, +void session_handle_view_fetch(struct session *, const struct imsg_mbox_fetch_meta *); void session_handle_idle_refreshed(struct session *, const struct imsg_mbox_idle_refreshed *); -void session_request_idle_refresh(struct session *, int); +int session_request_idle_refresh(struct session *); void session_idle_poll_init(struct session *); void session_login_grace_init(struct session *); void session_login_grace_disarm(struct session *); blob - 07161abf280fd522a6ce6a00dc337ee613c5a0b9 blob + 328b47f988740d0a5fe9f0f6576e7df5ddcd1803 --- src/main.c +++ src/main.c @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -136,6 +137,12 @@ main(int argc, char *argv[]) if (geteuid() != 0) fatalx("parent must start as root (running as " "uid %u)", (unsigned int)geteuid()); + if (getpwnam(IMAPD_USER) == NULL) + fatalx("unknown user %s", IMAPD_USER); + if (getpwnam(IMAPD_AUTH_USER) == NULL) + fatalx("unknown user %s", IMAPD_AUTH_USER); + if (getpwnam(IMAPD_KEY_USER) == NULL) + fatalx("unknown user %s", IMAPD_KEY_USER); if (config_load(conffile, &conf) == -1) fatalx("config_load: %s", conffile); parent_main(conffile, argc, argv, &conf); blob - 458f5c3e1ebc8d703f72a9e02f98902b7b6a33c1 blob + c64b5e7d6cf102bfad49200a205fe00d009d82b6 --- src/mbox_copy.c +++ src/mbox_copy.c @@ -468,6 +468,8 @@ handle_mbox_copy(struct imsg_mbox_copy *req, const str size_t nstaged = 0, i; struct index_lock il_a = INDEX_LOCK_INIT; struct index_lock il_b = INDEX_LOCK_INIT; + struct seq_range vr[SEQSET_MAX_RANGES]; + uint32_t nvr; int ok = 1; char desttarget[MBOX_NAME_MAX]; int cross_mailbox; @@ -487,8 +489,11 @@ handle_mbox_copy(struct imsg_mbox_copy *req, const str goto done; } dfd = cross_mailbox ? destfd : ss->mailbox_dir_fd; + /* RFC 2180 SS4.4.2: the numbers are those before the EXPUNGEs */ + view_resolve(ss, ranges, nranges, vr, &nvr, &req->by_uid); + (void)view_sync(ss, srcidx, 1); - if (!stage_copy_messages(ss, srcidx, req, ranges, nranges, &staged, + if (!stage_copy_messages(ss, srcidx, req, vr, nvr, &staged, &nstaged)) { ok = 0; goto close_dest; @@ -684,16 +689,8 @@ move_same_mailbox(struct imsg_mbox_copy *req, const st } /* then EXPUNGE (RFC 9051 SS6.4.8) */ for (i = 0; i < nmoved; i++) { - struct imsg_mbox_expunged exp; - pcache_drop_uid(ss->selected_mailbox, moved[i].old_uid); - memset(&exp, 0, sizeof(exp)); - exp.seqno = moved[i].old_seqno; - exp.uid = moved[i].old_uid; - if (imsg_compose(&iev->ibuf, IMSG_MBOX_EXPUNGED, 0, 0, -1, - &exp, sizeof(exp)) == -1) - log_warn("session %u: imsg_compose IMSG_MBOX_EXPUNGED", - session_id); + (void)send_expunged(ss, moved[i].old_seqno, moved[i].old_uid); } done: @@ -709,7 +706,6 @@ move_cross_mailbox(struct imsg_mbox_copy *req, const s uint32_t nranges, struct mbox_index *srcidx, struct mbox_index *destidx, 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; @@ -767,17 +763,7 @@ move_cross_mailbox(struct imsg_mbox_copy *req, const s unlinkat(ss->mailbox_dir_fd, path, 0); } - { - struct imsg_mbox_expunged exp; - - memset(&exp, 0, sizeof(exp)); - exp.seqno = old_seqno; - exp.uid = staged[i].src_uid; - if (imsg_compose(&iev->ibuf, IMSG_MBOX_EXPUNGED, 0, 0, - -1, &exp, sizeof(exp)) == -1) - log_warn("session %u: imsg_compose " - "IMSG_MBOX_EXPUNGED", session_id); - } + (void)send_expunged(ss, old_seqno, staged[i].src_uid); } /* RFC 7162 SS3.1 */ @@ -818,6 +804,8 @@ handle_mbox_move(struct imsg_mbox_copy *req, const str char desttarget[MBOX_NAME_MAX]; int cross_mailbox; int destfd = -1, got; + struct seq_range vr[SEQSET_MAX_RANGES]; + uint32_t nvr; memset(&idx_a, 0, sizeof(idx_a)); memset(&idx_b, 0, sizeof(idx_b)); @@ -833,12 +821,13 @@ handle_mbox_move(struct imsg_mbox_copy *req, const str goto done; } + view_resolve(ss, ranges, nranges, vr, &nvr, &req->by_uid); + (void)view_sync(ss, srcidx, 1); if (!cross_mailbox) { - if (!move_same_mailbox(req, ranges, nranges, srcidx, &nmoved, - ss)) + if (!move_same_mailbox(req, vr, nvr, srcidx, &nmoved, ss)) ok = 0; } else { - if (!move_cross_mailbox(req, ranges, nranges, srcidx, destidx, + if (!move_cross_mailbox(req, vr, nvr, srcidx, destidx, destfd, &nmoved, ss)) ok = 0; } blob - 0b694205c1d879f3ca71a8c24865436b4b7da1e7 blob + 3088dce4bc163aa0100848cfe302e99edd45c2d5 --- src/mbox_fetch.c +++ src/mbox_fetch.c @@ -64,6 +64,34 @@ fetch_send_part(struct store_session *ss, int imsg_typ free(combined); } +/* RFC 9051 SS6.4.5: BODY[...] sets \Seen, with STORE's own code */ +static int +fetch_set_seen(struct store_session *ss, int synced) +{ + struct store_fetch_walk *fw = &ss->fetch; + struct imsg_mbox_store sreq; + struct store_step *steps = NULL; + size_t nsteps = 0; + uint64_t before = fw->idx.highestmodseq; + int ok; + + memset(&sreq, 0, sizeof(sreq)); + sreq.mode = MBOX_STORE_ADD; + sreq.sysflags = MBOX_FLAG_SEEN; + ok = store_apply(ss, &sreq, &fw->idx, fw->resolved, fw->nresolved, + fw->req.by_uid, synced, fw->req.has_changedsince ? + &fw->req.changedsince : NULL, &steps, &nsteps); + free(steps); + if (!ok) + return (-1); + if (fw->idx.highestmodseq != before) { + fw->seen_modseq = fw->idx.highestmodseq; + /* its renames left cur/'s snapshot naming the old files */ + cur_snapshot_discard(&ss->cur_snap); + } + return (0); +} + /* returns 1, unanswered, if the index lock is busy */ int handle_mbox_fetch(struct imsg_mbox_fetch *req, const struct seq_range *ranges, @@ -73,11 +101,14 @@ handle_mbox_fetch(struct imsg_mbox_fetch *req, const s struct imsgev *iev = &ss->iev; struct mbox_index *idx = &fw->idx; struct index_lock il = INDEX_LOCK_INIT; - uint32_t i; - int locked; + struct seq_range vr[SEQSET_MAX_RANGES]; + uint32_t i, nvr; + int locked, synced, rc, set_seen; + set_seen = (req->attrs & MBOX_FETCH_SET_SEEN) != 0; /* Before the reset below, so that a busy return changes nothing. */ - locked = index_lock_acquire(ss->mailbox_dir_fd, &il, LOCK_SH | LOCK_NB); + locked = index_lock_acquire(ss->mailbox_dir_fd, &il, + (set_seen ? LOCK_EX : LOCK_SH) | LOCK_NB); if (locked == 1) return (1); @@ -99,15 +130,29 @@ handle_mbox_fetch(struct imsg_mbox_fetch *req, const s fetch_walk_finish(ss, 0); return (0); } - index_lock_release(&il); + if (!set_seen) + index_lock_release(&il); pcache_check_mailbox(ss->selected_mailbox, idx->uidvalidity); + synced = view_sync(ss, idx, req->by_uid) == 0; + view_resolve(ss, ranges, nranges, vr, &nvr, &fw->req.by_uid); /* 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, + fw->nresolved = seqset_resolve(vr, nvr, fw->req.by_uid ? + index_max_uid(idx) : (uint32_t)idx->nlines, !fw->req.by_uid, fw->resolved); fw->max_hi = seqset_max_hi(fw->resolved, fw->nresolved); + fw->ghost = !req->by_uid && + view_names_ghost(ss, idx, fw->resolved, fw->nresolved); + if (set_seen) { + rc = fetch_set_seen(ss, synced); + index_lock_release(&il); + if (rc == -1) { + fetch_walk_finish(ss, 0); + return (0); + } + } + /* RFC 7162 SS3.2.6: VANISHED (EARLIER) first */ if (req->by_uid && req->want_vanished) { for (i = 0; i < fw->nresolved; i++) @@ -205,11 +250,14 @@ fetch_walk_step(struct store_session *ss) } memset(&meta, 0, sizeof(meta)); - meta.seqno = i; + meta.seqno = view_seqno(ss, rec.uid, i); meta.uid = rec.uid; meta.modseq = rec.modseq; + meta.seen_set = fw->seen_modseq != 0 && + rec.modseq == fw->seen_modseq; - if (req->attrs & (MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_FLAGS)) { + if (meta.seen_set || (req->attrs & + (MBOX_FETCH_RFC822_SIZE | MBOX_FETCH_FLAGS))) { if (locate_message_file(&ss->cur_snap, ss->mailbox_dir_fd, rec.basename, &size, suffix, sizeof(suffix)) == -1) { @@ -223,7 +271,7 @@ fetch_walk_step(struct store_session *ss) if (req->attrs & MBOX_FETCH_RFC822_SIZE) meta.size = (uint64_t)size; - if (req->attrs & MBOX_FETCH_FLAGS) + if ((req->attrs & MBOX_FETCH_FLAGS) || meta.seen_set) build_flags_string(have_file ? suffix : "", rec.keywords, meta.flags, sizeof(meta.flags)); if (req->attrs & MBOX_FETCH_INTERNALDATE) @@ -241,7 +289,7 @@ fetch_walk_step(struct store_session *ss) int mfd, rc; memset(&hdrmeta, 0, sizeof(hdrmeta)); - hdrmeta.seqno = i; + hdrmeta.seqno = view_seqno(ss, rec.uid, i); hdrmeta.uid = rec.uid; if (req->attrs & MBOX_FETCH_BODY_HEADER) rc = read_message_header(&ss->cur_snap, @@ -290,7 +338,7 @@ fetch_walk_step(struct store_session *ss) !want_part; memset(&bodymeta, 0, sizeof(bodymeta)); - bodymeta.seqno = i; + bodymeta.seqno = view_seqno(ss, rec.uid, i); bodymeta.uid = rec.uid; if (want_part) { @@ -367,7 +415,7 @@ fetch_walk_step(struct store_session *ss) int mfd; memset(&envmeta, 0, sizeof(envmeta)); - envmeta.seqno = i; + envmeta.seqno = view_seqno(ss, rec.uid, i); envmeta.uid = rec.uid; if ((env = fetch_cached(ss, &rec, PCACHE_ENVELOPE, &have_file, &envmeta.envlen)) != NULL) @@ -403,7 +451,7 @@ fetch_walk_step(struct store_session *ss) int mfd; memset(&bsmeta, 0, sizeof(bsmeta)); - bsmeta.seqno = i; + bsmeta.seqno = view_seqno(ss, rec.uid, i); bsmeta.uid = rec.uid; if ((bs = fetch_cached(ss, &rec, PCACHE_BODYSTRUCTURE, &have_file, &bsmeta.bslen)) != NULL) @@ -461,7 +509,8 @@ fetch_walk_finish(struct store_session *ss, int ok) cur_snapshot_discard(&ss->cur_snap); memset(&result, 0, sizeof(result)); - result.error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC; + result.error = !ok ? MBOX_OP_ERR_GENERIC : fw->ghost ? + MBOX_OP_ERR_EXPUNGEISSUED : MBOX_OP_OK; result.count = fw->sent; if (imsg_compose(&iev->ibuf, IMSG_MBOX_RESULT, 0, 0, -1, &result, sizeof(result)) == -1) blob - 63953515d197c7229551665b077361c892fc219a blob + db797c1b29abb06e90a0be290a8fd944fd711c7f --- src/mbox_manage.c +++ src/mbox_manage.c @@ -116,6 +116,7 @@ handle_mbox_select(struct imsg_mbox_select *req, if (req->qresync && req->qresync_uidvalidity == idx.uidvalidity) qresync_send_resync(req, ranges, nranges, &idx, ss); + idle_baseline_seed(ss, &idx); index_free(&idx); index_lock_release(&il); @@ -1198,7 +1199,9 @@ handle_mbox_append_end(struct store_session *ss) goto done_index; reply.uidvalidity = idx.uidvalidity; - reply.exists = (uint32_t)idx.nlines; + if (ss->mailbox_selected && strcmp(ss->selected_mailbox, + mailbox_name_is_inbox(ap->req.mailbox) ? "" : ap->req.mailbox) == 0) + (void)view_sync(ss, &idx, 1); index_lock_release(&il); index_free(&idx); blob - 3f440822a5f3f8e4528335336b76cdb09c52bb88 blob + fb8ba5603a5446fd95335c1e1ef8654a5f73daf2 --- src/mbox_search.c +++ src/mbox_search.c @@ -278,8 +278,8 @@ handle_mbox_search(struct imsg_mbox_search *req, struc 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; + uint32_t max_uid, i, nout; + int locked, by_uid; /* Before the reset below, so that a busy return changes nothing. */ locked = index_lock_acquire(ss->mailbox_dir_fd, &il, LOCK_SH | LOCK_NB); @@ -311,6 +311,7 @@ handle_mbox_search(struct imsg_mbox_search *req, struc sw->nnodes = nnodes; max_uid = index_max_uid(&sw->idx); + (void)view_sync(ss, &sw->idx, 0); for (i = 0; i < nnodes; i++) { struct search_node *n = &sw->nodes[i]; @@ -340,6 +341,15 @@ handle_mbox_search(struct imsg_mbox_search *req, struc in.lo_is_star = n->lo_is_star; in.hi_is_star = n->hi_is_star; + by_uid = n->op == SEARCH_OP_UIDSET; + view_resolve(ss, &in, 1, &out, &nout, &by_uid); + if (n->op == SEARCH_OP_SEQSET && by_uid) { + n->op = SEARCH_OP_UIDSET; + n->seq_lo = nout > 0 ? out.lo : 0; + n->seq_hi = nout > 0 ? out.hi : 0; + n->lo_is_star = n->hi_is_star = 0; + continue; + } (void)seqset_resolve(&in, 1, max, n->op == SEARCH_OP_SEQSET, &out); n->seq_lo = out.lo; @@ -421,7 +431,7 @@ search_walk_step(struct store_session *ss) continue; memset(&m, 0, sizeof(m)); - m.seqno = i; + m.seqno = view_seqno(ss, rec.uid, i); m.uid = rec.uid; m.modseq = rec.modseq; @@ -465,7 +475,7 @@ search_walk_step(struct store_session *ss) struct imsg_mbox_search_match match; memset(&match, 0, sizeof(match)); - match.seqno = i; + match.seqno = m.seqno; match.uid = m.uid; match.modseq = m.modseq; if (imsg_compose(&iev->ibuf, IMSG_MBOX_SEARCH_MATCH, blob - ffb4cf902c1fc490f7cdf4d038efe4efab3c46f3 blob + 4061bcf4257fbef8e964f7f25980a2576a5360c8 --- src/mbox_store.c +++ src/mbox_store.c @@ -150,55 +150,22 @@ store_undo(struct store_session *ss, const struct mbox } } -/* RFC 9051 SS6.4.6 STORE: plan every message, then change */ +/* RFC 9051 SS6.4.6 STORE: plan every message, then change; LOCK_EX held */ int -handle_mbox_store(struct imsg_mbox_store *req, const struct seq_range *ranges, - uint32_t nranges, struct store_session *ss) +store_apply(struct store_session *ss, const struct imsg_mbox_store *req, + struct mbox_index *idx, const struct seq_range *resolved, + uint32_t nresolved, int by_uid, int synced, const uint64_t *since, + struct store_step **stepsp, size_t *nstepsp) { - struct imsgev *iev = &ss->iev; - int locked; - struct mbox_index idx; - struct imsg_mbox_result result; - struct index_lock il = INDEX_LOCK_INIT; - struct seq_range resolved[SEQSET_MAX_RANGES]; struct store_step *steps = NULL, *grown; - size_t nsteps = 0, maxsteps = 0, k; - uint32_t i, nresolved, max_hi, sent = 0; - uint64_t new_modseq, reported = 0; + size_t nsteps = 0, maxsteps = 0; + uint32_t i, max_hi; + uint64_t new_modseq = idx->highestmodseq + 1; int ok = 1, changed = 0, pass; - memset(&idx, 0, sizeof(idx)); - - if (!index_field_valid(req->keywords)) { - log_warnx("session %u: STORE: refusing unsafe keywords field", - session_id); - ok = 0; - goto done; - } - /* 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) { - ok = 0; - goto done; - } - if (index_load(il.fd, &idx) == -1) { - ok = 0; - goto done; - } - - reported = idx.highestmodseq; - new_modseq = idx.highestmodseq + 1; - - /* 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); max_hi = seqset_max_hi(resolved, nresolved); - for (pass = 0; pass < 2 && ok; pass++) { - for (i = 1; i <= (uint32_t)idx.nlines; i++) { + for (i = 1; i <= (uint32_t)idx->nlines; i++) { struct index_rec rec; struct store_step st; enum seqset_pos pos; @@ -207,13 +174,14 @@ handle_mbox_store(struct imsg_mbox_store *req, const s char *dup; int rc; - if (index_parse_line(idx.lines[i - 1], &rec) == -1) + if (index_parse_line(idx->lines[i - 1], &rec) == -1) continue; pos = seqset_position(resolved, nresolved, max_hi, - req->by_uid, rec.uid, i); + by_uid, rec.uid, i); if (pos == SEQSET_PAST_END) break; - if (pos == SEQSET_SKIP) + if (pos == SEQSET_SKIP || + (since != NULL && rec.modseq <= *since)) continue; memset(&st, 0, sizeof(st)); @@ -270,24 +238,85 @@ handle_mbox_store(struct imsg_mbox_store *req, const s break; } steps[nsteps++] = st; - free(idx.lines[i - 1]); - idx.lines[i - 1] = dup; + free(idx->lines[i - 1]); + idx->lines[i - 1] = dup; if (st.changed) changed = 1; } } if (ok && changed) { - idx.highestmodseq = new_modseq; - if (index_save(ss->mailbox_dir_fd, &idx) == -1) + idx->highestmodseq = new_modseq; + if (index_save(ss->mailbox_dir_fd, idx) == -1) ok = 0; } - if (!ok) { - store_undo(ss, &idx, steps, nsteps); + if (!ok) + store_undo(ss, idx, steps, nsteps); + else if (synced && changed && new_modseq > ss->idle_baseline.modseq) + /* LOCK_EX held since the sync: new_modseq is ours alone */ + ss->idle_baseline.modseq = new_modseq; + *stepsp = steps; + *nstepsp = nsteps; + return (ok); +} + +int +handle_mbox_store(struct imsg_mbox_store *req, const struct seq_range *ranges, + uint32_t nranges, struct store_session *ss) +{ + struct imsgev *iev = &ss->iev; + int locked; + struct mbox_index idx; + struct imsg_mbox_result result; + struct index_lock il = INDEX_LOCK_INIT; + struct seq_range resolved[SEQSET_MAX_RANGES]; + struct seq_range vr[SEQSET_MAX_RANGES]; + struct store_step *steps = NULL; + size_t nsteps = 0, k; + uint32_t nresolved, nvr, sent = 0; + uint64_t reported = 0; + int ok = 1, by_uid, synced; + int ghost; + + memset(&idx, 0, sizeof(idx)); + + if (!index_field_valid(req->keywords)) { + log_warnx("session %u: STORE: refusing unsafe keywords field", + session_id); + ok = 0; goto done; } + /* 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) { + ok = 0; + goto done; + } + if (index_load(il.fd, &idx) == -1) { + ok = 0; + goto done; + } + + synced = view_sync(ss, &idx, req->by_uid) == 0; + by_uid = req->by_uid; + view_resolve(ss, ranges, nranges, vr, &nvr, &by_uid); + reported = idx.highestmodseq; + /* RFC 9051 SS6.4.9 */ + nresolved = seqset_resolve(vr, nvr, by_uid ? + index_max_uid(&idx) : (uint32_t)idx.nlines, !by_uid, resolved); + ghost = !req->by_uid && view_names_ghost(ss, &idx, resolved, + nresolved); + + ok = store_apply(ss, req, &idx, resolved, nresolved, by_uid, synced, + NULL, &steps, &nsteps); + if (!ok) + goto done; + reported = idx.highestmodseq; + for (k = 0; k < nsteps; k++) { const struct store_step *st = &steps[k]; struct index_rec rec; @@ -298,7 +327,7 @@ handle_mbox_store(struct imsg_mbox_store *req, const s struct imsg_mbox_store_modified mod; memset(&mod, 0, sizeof(mod)); - mod.seqno = st->seqno; + mod.seqno = view_seqno(ss, rec.uid, st->seqno); mod.uid = rec.uid; if (imsg_compose(&iev->ibuf, IMSG_MBOX_STORE_MODIFIED, 0, 0, -1, &mod, sizeof(mod)) == -1) @@ -313,7 +342,7 @@ handle_mbox_store(struct imsg_mbox_store *req, const s char newsuffix[16]; memset(&meta, 0, sizeof(meta)); - meta.seqno = st->seqno; + meta.seqno = view_seqno(ss, rec.uid, st->seqno); meta.uid = rec.uid; meta.modseq = st->modseq; (void)snprintf(newsuffix, sizeof(newsuffix), "2,%s", @@ -334,7 +363,9 @@ done: free(steps); memset(&result, 0, sizeof(result)); - result.error = ok ? MBOX_OP_OK : MBOX_OP_ERR_GENERIC; + /* RFC 2180 SS4.2.1-SS4.2.3 */ + result.error = !ok ? MBOX_OP_ERR_GENERIC : ghost && !req->silent ? + MBOX_OP_ERR_EXPUNGEISSUED : MBOX_OP_OK; result.count = sent; /* never a value the index does not hold */ result.highestmodseq = reported; @@ -382,6 +413,8 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req, goto done; } reported = idx.highestmodseq; + if (!req->silent) + (void)view_sync(ss, &idx, 1); if (req->by_uid) nresolved = seqset_resolve(ranges, nranges, @@ -471,19 +504,9 @@ handle_mbox_expunge(struct imsg_mbox_expunge *req, log_warn("session %u: unlink %s, left on disk with no " "index entry", session_id, path); - if (!req->silent) { - struct imsg_mbox_expunged exp; - - memset(&exp, 0, sizeof(exp)); - exp.seqno = gone[k].seqno; - exp.uid = rec.uid; - if (imsg_compose(&iev->ibuf, IMSG_MBOX_EXPUNGED, 0, 0, - -1, &exp, sizeof(exp)) == -1) - log_warn("session %u: imsg_compose " - "IMSG_MBOX_EXPUNGED", session_id); - else - sent++; - } + if (!req->silent && + send_expunged(ss, gone[k].seqno, rec.uid) == 0) + sent++; } done: blob - b27d4cefa382bca517e4f751af19673567f77a9d blob + 0efcbefdf39129fc1141c56af54fa50edb9a656c --- src/mime.c +++ src/mime.c @@ -1001,8 +1001,8 @@ find_mime_part(int depth, const char *hdr, size_t hdrl int has_boundary; size_t part_starts[MIME_MAX_PARTS], part_ends[MIME_MAX_PARTS]; int n, want; - const char *pbuf; - size_t plen, phdrend; + const char *pbuf, *pbody; + size_t plen, phdrend, pbodylen, ih; if (depth > MIME_MAX_DEPTH) return (-1); @@ -1029,44 +1029,52 @@ find_mime_part(int depth, const char *hdr, size_t hdrl else if (find_header_body_split(pbuf, plen, &phdrend) == -1) return (-1); + params_fmt[0] = '\0'; + if (parse_content_type(pbuf, phdrend, type, sizeof(type), subtype, + sizeof(subtype), params_fmt, sizeof(params_fmt), boundary, + sizeof(boundary), &has_boundary) == -1) + return (-1); + pbody = pbuf + phdrend; + pbodylen = plen - phdrend; + if (pathlen == 1) { - /* a leaf only: no "combined children" of a multipart */ - char ctype[64], csub[64], cparams[600]; - char cboundary[70 + 1]; - int chb; - - cparams[0] = '\0'; - if (parse_content_type(pbuf, phdrend, ctype, sizeof(ctype), - csub, sizeof(csub), cparams, sizeof(cparams), cboundary, - sizeof(cboundary), &chb) == -1) + /* a leaf or a message: no "combined children" of a multipart */ + if (strcasecmp(type, "MULTIPART") == 0) return (-1); - if (strcasecmp(ctype, "MULTIPART") == 0 || - (strcasecmp(ctype, "MESSAGE") == 0 && - (strcasecmp(csub, "RFC822") == 0 || - strcasecmp(csub, "GLOBAL") == 0))) - return (-1); - - *part_out = pbuf + phdrend; - *partlen_out = plen - phdrend; + *part_out = pbody; + *partlen_out = pbodylen; return (0); } - return (find_mime_part(depth + 1, pbuf, phdrend, pbuf + phdrend, - plen - phdrend, path + 1, pathlen - 1, part_out, partlen_out)); + /* RFC 9051 SS6.4.5.1: a message's parts are numbered within it */ + if (strcasecmp(type, "MESSAGE") == 0 && + (strcasecmp(subtype, "RFC822") == 0 || + strcasecmp(subtype, "GLOBAL") == 0)) { + if (pbodylen == 0 || + find_header_body_split(pbody, pbodylen, &ih) == -1) + return (-1); + return (locate_mime_part(depth + 1, pbody, ih, pbody + ih, + pbodylen - ih, path + 1, pathlen - 1, part_out, + partlen_out)); + } + + return (find_mime_part(depth + 1, pbuf, phdrend, pbody, pbodylen, + path + 1, pathlen - 1, part_out, partlen_out)); } /* 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, - size_t *partlen_out) +locate_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, size_t *partlen_out) { char type[64], subtype[64]; char params_fmt[600]; char boundary[70 + 1]; int has_boundary; + size_t ih; - if (pathlen < 1) + if (depth > MIME_MAX_DEPTH || pathlen < 1) return (-1); params_fmt[0] = '\0'; @@ -1076,14 +1084,23 @@ locate_mime_part(const char *hdr, size_t hdrlen, const return (-1); if (strcasecmp(type, "MULTIPART") == 0) - return (find_mime_part(1, hdr, hdrlen, body, bodylen, path, - pathlen, part_out, partlen_out)); + return (find_mime_part(depth + 1, hdr, hdrlen, body, bodylen, + path, pathlen, part_out, partlen_out)); - if (pathlen != 1 || path[0] != 1) + if (path[0] != 1) return (-1); - *part_out = body; - *partlen_out = bodylen; - return (0); + if (pathlen == 1) { + *part_out = body; + *partlen_out = bodylen; + return (0); + } + if (strcasecmp(type, "MESSAGE") != 0 || + (strcasecmp(subtype, "RFC822") != 0 && + strcasecmp(subtype, "GLOBAL") != 0) || bodylen == 0 || + find_header_body_split(body, bodylen, &ih) == -1) + return (-1); + return (locate_mime_part(depth + 1, body, ih, body + ih, + bodylen - ih, path + 1, pathlen - 1, part_out, partlen_out)); } /* RFC 9051 SS6.4.5 "" applied to a range; past its end is empty */ @@ -1127,7 +1144,7 @@ extract_mime_part(int fd, const char *basename, const } /* RFC 9051 SS6.4.5: a missing part is an empty item */ - if (locate_mime_part(wholebuf, hdrend, wholebuf + hdrend, + if (locate_mime_part(0, wholebuf, hdrend, wholebuf + hdrend, wholelen - hdrend, path, pathlen, &part, &partlen) == 0) { *off_out = (uint64_t)(part - wholebuf); *len_out = (uint64_t)partlen; blob - 108e7ceb1611d9fc797ea19eeae6596f60131974 blob + e861c44b599d3f715f3058d691d8bba81707ce65 --- src/store.c +++ src/store.c @@ -1236,19 +1236,16 @@ store_dispatch(int fd, short event, void *arg) free(ranges); break; } - case IMSG_MBOX_IDLE_REFRESH: { - struct imsg_mbox_idle_refresh req; - - if (imsg_get_data(&imsg, &req, sizeof(req)) == -1) { + case IMSG_MBOX_IDLE_REFRESH: + if (imsg_get_len(&imsg) != 0) { log_warnx("bad IMSG_MBOX_IDLE_REFRESH"); break; } - if (!require_mailbox_selected(ss, - "IMSG_MBOX_IDLE_REFRESH")) - break; - handle_mbox_idle_refresh(&req, ss); + /* answered even if refused: the listener waits */ + (void)require_mailbox_selected(ss, + "IMSG_MBOX_IDLE_REFRESH"); + handle_mbox_idle_refresh(ss); break; - } case IMSG_MBOX_APPEND: { struct imsg_mbox_append req; blob - bba0601daaaca5fdca6c7ef25b28c96d2e6f1a03 blob + 6ea1823a6c6737cef6a9d3176be27be5ab7ac21d --- src/store_cmd.c +++ src/store_cmd.c @@ -56,7 +56,7 @@ cmd_idle(struct session *s, const char *tag, char *arg session_write(s, "+ idling\r\n", 10); if (s->state == SESSION_SELECTED) { - session_request_idle_refresh(s, 1); /* seeds the baseline */ + (void)session_request_idle_refresh(s); session_idle_poll_arm(s); } blob - a37ae3887f2d08276ba8a3bbe156d22bf670f4e9 blob + 3c83389b2eb5b76d0b40c888d57458d13bb30233 --- src/store_internal.h +++ src/store_internal.h @@ -70,6 +70,8 @@ struct store_fetch_walk { int fds; int wait_parser; /* paused until one comes */ int no_parser; /* none to be had: go without */ + int ghost; + uint64_t seen_modseq; }; /* RFC 9051 SS6.4.4 SEARCH, yielding every SEARCH_YIELD_REQUESTS */ @@ -106,6 +108,7 @@ struct store_idle_baseline { uint32_t *uids; size_t n; uint64_t modseq; /* highest reported; above it is news */ + size_t ghosts; /* expunged, not yet told */ int valid; }; @@ -207,6 +210,11 @@ int handle_mbox_fetch(struct imsg_mbox_fetch *, const struct seq_range *, uint32_t, struct store_session *); int handle_mbox_store(struct imsg_mbox_store *, const struct seq_range *, uint32_t, struct store_session *); +struct store_step; +int store_apply(struct store_session *, + const struct imsg_mbox_store *, struct mbox_index *, + const struct seq_range *, uint32_t, int, int, + const uint64_t *, struct store_step **, size_t *); int handle_mbox_expunge(struct imsg_mbox_expunge *, const struct seq_range *, uint32_t, struct store_session *); void handle_mbox_append_begin(struct store_session *, @@ -279,7 +287,7 @@ int envbuf_append_one_address(char *, size_t, size_t int envbuf_append_address_list(char *, size_t, size_t *, char *, size_t); int append_field_nstring(char *, size_t, size_t *, const char *, - uint32_t, const char *); + size_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, @@ -299,7 +307,7 @@ int build_bodystructure(int, const char *, char **, u int parse_section_part(const char *, int *, int); int find_mime_part(int, const char *, size_t, const char *, size_t, const int *, int, const char **, size_t *); -int locate_mime_part(const char *, size_t, const char *, size_t, +int locate_mime_part(int, const char *, size_t, const char *, size_t, const int *, int, const char **, size_t *); void partial_range(int, uint32_t, uint32_t, uint64_t *, uint64_t *); int extract_mime_part(int, const char *, const int *, int, @@ -365,8 +373,16 @@ 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 store_session *); +void handle_mbox_idle_refresh(struct store_session *); +void idle_baseline_seed(struct store_session *, const struct mbox_index *); +int view_sync(struct store_session *, const struct mbox_index *, int); +void view_resolve(const struct store_session *, const struct seq_range *, + uint32_t, struct seq_range *, uint32_t *, int *); +uint32_t view_seqno(const struct store_session *, uint32_t, uint32_t); +int view_names_ghost(const struct store_session *, + const struct mbox_index *, const struct seq_range *, + uint32_t); +int send_expunged(struct store_session *, uint32_t, uint32_t); void qresync_send_resync(const struct imsg_mbox_select *, const struct seq_range *, uint32_t, struct mbox_index *, struct store_session *); blob - 760d3f6ac69ae23a6461fef09899118f0e047e3d blob + 9a98af9d87a28cfac11ea767354ca2ceeecbbafd --- src/store_ipc.c +++ src/store_ipc.c @@ -311,28 +311,40 @@ session_store_dispatch(int fd, short event, void *arg) session_handle_store_modified(s, &m); break; } - case IMSG_MBOX_IDLE_EXPUNGE: { - struct imsg_mbox_idle_expunge item; + case IMSG_MBOX_VIEW_EXPUNGE: { + struct imsg_mbox_expunged item; if (imsg_get_data(&imsg, &item, sizeof(item)) == -1) { - log_warnx("bad IMSG_MBOX_IDLE_EXPUNGE"); + log_warnx("bad IMSG_MBOX_VIEW_EXPUNGE"); break; } - session_handle_idle_expunge(s, &item); + session_send_expunge_response(s, &item); break; } - case IMSG_MBOX_IDLE_FETCH: { + case IMSG_MBOX_VIEW_FETCH: { struct imsg_mbox_fetch_meta meta; if (imsg_get_data(&imsg, &meta, sizeof(meta)) == -1) { - log_warnx("bad IMSG_MBOX_IDLE_FETCH"); + log_warnx("bad IMSG_MBOX_VIEW_FETCH"); break; } /* same risk as IMSG_MBOX_FETCH_META's flags above */ meta.flags[sizeof(meta.flags) - 1] = '\0'; - session_handle_idle_fetch(s, &meta); + session_handle_view_fetch(s, &meta); break; } + case IMSG_MBOX_VIEW_EXISTS: { + struct imsg_mbox_view_exists ve; + char buf[32]; + + if (imsg_get_data(&imsg, &ve, sizeof(ve)) == -1) { + log_warnx("bad IMSG_MBOX_VIEW_EXISTS"); + break; + } + snprintf(buf, sizeof(buf), "%u EXISTS", ve.exists); + session_untagged(s, buf); + break; + } case IMSG_MBOX_IDLE_REFRESHED: { struct imsg_mbox_idle_refreshed res; @@ -778,31 +790,13 @@ session_handle_select_fetch(struct session *s, s->qresync_fetches[s->qresync_nfetches++] = *m; } -void -session_handle_idle_expunge(struct session *s, - const struct imsg_mbox_idle_expunge *item) -{ - char buf[32]; - - /* DONE may have arrived while the refresh was in flight. */ - if (!s->idling || s->state != SESSION_SELECTED) - return; - - snprintf(buf, sizeof(buf), "%u EXPUNGE", item->seqno); - session_untagged(s, buf); -} - /* RFC 9051 SS6.3.13: an unsolicited FETCH carries UID */ void -session_handle_idle_fetch(struct session *s, +session_handle_view_fetch(struct session *s, const struct imsg_mbox_fetch_meta *meta) { char buf[MBOX_FLAGS_MAX + 96]; - /* DONE may have arrived while the refresh was in flight. */ - if (!s->idling || s->state != SESSION_SELECTED) - return; - if (s->condstore_enabled) snprintf(buf, sizeof(buf), "%u FETCH (UID %u FLAGS (%s) MODSEQ (%llu))", meta->seqno, @@ -817,8 +811,6 @@ void session_handle_idle_refreshed(struct session *s, const struct imsg_mbox_idle_refreshed *res) { - char buf[32]; - s->idle_refresh_pending = 0; if (!res->ok && res->busy) @@ -827,42 +819,35 @@ session_handle_idle_refreshed(struct session *s, else if (!res->ok) log_warnx("session %u: IDLE refresh failed, keeping last " "known state", s->id); - else if (res->exists_changed && s->idling && - s->state == SESSION_SELECTED) { - snprintf(buf, sizeof(buf), "%u EXISTS", res->exists); - session_untagged(s, buf); - } - if (s->idle_refresh_again) { - int seed = s->idle_refresh_again_seed; - + if (s->refresh_status != NULL) { + session_reply(s, s->pending_tag, s->refresh_status, + s->refresh_text); + s->refresh_status = NULL; + } else if (s->idle_refresh_again && s->idling) { s->idle_refresh_again = 0; - s->idle_refresh_again_seed = 0; - session_request_idle_refresh(s, seed); + (void)session_request_idle_refresh(s); } } -void -session_request_idle_refresh(struct session *s, int seed) +int +session_request_idle_refresh(struct session *s) { - struct imsg_mbox_idle_refresh req; - if (s->idle_refresh_pending) { s->idle_refresh_again = 1; - if (seed) - s->idle_refresh_again_seed = 1; - return; + return (0); } if (s->store_iev == NULL) - return; /* no store child wired, nothing to ask */ + return (-1); - memset(&req, 0, sizeof(req)); - req.seed = seed; - s->idle_refresh_pending = 1; if (imsg_compose(&s->store_iev->ibuf, IMSG_MBOX_IDLE_REFRESH, 0, 0, - -1, &req, sizeof(req)) == -1) + -1, NULL, 0) == -1) { log_warn("session %u: imsg_compose IMSG_MBOX_IDLE_REFRESH", s->id); + return (-1); + } + s->idle_refresh_pending = 1; + return (0); } static void @@ -880,7 +865,7 @@ session_idle_poll(int fd, short event, void *arg) return; } - session_request_idle_refresh(s, 0); + (void)session_request_idle_refresh(s); session_idle_poll_arm(s); /* an evtimer is one-shot */ } @@ -974,7 +959,7 @@ session_handle_mbox_result(struct session *s, struct i SESSION_AUTHENTICATED : SESSION_SELECTED; if (res->error != MBOX_OP_OK) { - char text[32]; + char text[64]; free(s->store_modified); s->store_modified = NULL; @@ -987,7 +972,9 @@ session_handle_mbox_result(struct session *s, struct i IMAP_BUSY_TEXT); return; } - snprintf(text, sizeof(text), "%s failed", cmdname); + snprintf(text, sizeof(text), "%s%s failed", + res->error == MBOX_OP_ERR_EXPUNGEISSUED ? + "[EXPUNGEISSUED] " : "", cmdname); session_reply(s, s->pending_tag, "NO", text); return; }