From 23c3f7e59beafc832e39c4d0ee44ea3aaff0e353 Mon Sep 17 00:00:00 2001
From: Bron Gondwana <brong@fastmailteam.com>
Date: Fri, 27 Dec 2024 22:51:19 +1100
Subject: [PATCH 1/2] index: parse '$' in vanished as well

---
 imap/index.c | 51 ++++++++++++++++++++++++---------------------------
 1 file changed, 24 insertions(+), 27 deletions(-)

diff --git a/imap/index.c b/imap/index.c
index 5516bdbd1f..fa65732019 100644
--- a/imap/index.c
+++ b/imap/index.c
@@ -426,7 +426,7 @@ EXPORTED int index_expunge(struct index_state *state, const char *sequence,
             continue; /* no \Deleted flag */
 
         /* if there is a sequence list, check it */
-        if (sequence && !seqset_ismember(seq, im->uid))
+        if (seq && !seqset_ismember(seq, im->uid))
             continue; /* not in the list */
 
         /* load first once we know we have to process this one */
@@ -469,7 +469,7 @@ EXPORTED int index_expunge(struct index_state *state, const char *sequence,
         mboxevent_extract_record(mboxevent, state->mailbox, &record);
     }
 
-    seqset_free(&seq);
+    if (seq != state->searchres) seqset_free(&seq);
 
     mboxevent_extract_mailbox(mboxevent, state->mailbox);
     mboxevent_set_access(mboxevent, NULL, NULL, state->userid, mailbox_name(state->mailbox), 1);
@@ -877,16 +877,16 @@ EXPORTED void index_select(struct index_state *state, struct index_init *init)
         }
 
         sequence = init->vanished.sequence;
-        if (sequence) seq = _parse_sequence(state, sequence, 1);
+        seq = _parse_sequence(state, sequence, 1);
         for (msgno = 1; msgno <= state->exists; msgno++) {
             im = &state->map[msgno-1];
-            if (sequence && !seqset_ismember(seq, im->uid))
+            if (seq && !seqset_ismember(seq, im->uid))
                 continue;
             if (im->modseq <= init->vanished.modseq)
                 continue;
             index_printflags(state, msgno, TELL_UID);
         }
-        seqset_free(&seq);
+        if (seq != state->searchres) seqset_free(&seq);
     }
 }
 
@@ -972,7 +972,7 @@ seqset_t *index_vanished(struct index_state *state,
             const struct index_record *record = msg_record(msg);
             if (!(record->internal_flags & FLAG_INTERNAL_EXPUNGED))
                 continue;
-            if (!params->sequence || seqset_ismember(seq, record->uid))
+            if (!seq || seqset_ismember(seq, record->uid))
                 seqset_add(outlist, record->uid, 1);
         }
         mailbox_iter_done(&iter);
@@ -1036,7 +1036,7 @@ seqset_t *index_vanished(struct index_state *state,
         }
     }
 
-    seqset_free(&seq);
+    if (seq != state->searchres) seqset_free(&seq);
 
     return outlist;
 }
@@ -1211,11 +1211,11 @@ EXPORTED int index_fetch(struct index_state *state,
     r = index_lock(state, readonly);
     if (r) return r;
 
-    if (!strcmp("$", sequence)) {
-        seq = state->searchres;
+    seq = _parse_sequence(state, sequence, usinguid);
+    if (!strcmpsafe("$", sequence)) {
         usinguid = 1;
 
-        if (!seqset_first(state->searchres)) {
+        if (!seqset_first(seq)) {
             /* RFC 5182: 2.1
              * Note that even if the "$" marker contains the empty list of
              * messages, it must be treated by all commands accepting message
@@ -1227,9 +1227,6 @@ EXPORTED int index_fetch(struct index_state *state,
             goto done;
         }
     }
-    else {
-        seq = _parse_sequence(state, sequence, usinguid);
-    }
 
     /* set the \Seen flag if necessary - while we still have the lock */
     if (!readonly) {
@@ -1365,11 +1362,11 @@ EXPORTED int index_store(struct index_state *state, const char *sequence,
 
     mailbox = state->mailbox;
 
+    seq = _parse_sequence(state, sequence, storeargs->usinguid);
     if (!strcmp("$", sequence)) {
-        seq = state->searchres;
         storeargs->usinguid = 1;
 
-        if (!seqset_first(state->searchres)) {
+        if (!seqset_first(seq)) {
             /* RFC 5182: 2.1
              * Note that even if the "$" marker contains the empty list of
              * messages, it must be treated by all commands accepting message
@@ -1378,9 +1375,6 @@ EXPORTED int index_store(struct index_state *state, const char *sequence,
             goto done;
         }
     }
-    else {
-        seq = _parse_sequence(state, sequence, storeargs->usinguid);
-    }
 
     for (i = 0; i < flags->count ; i++) {
         r = mailbox_user_flag(mailbox, flags->data[i], &userflag, 1);
@@ -1615,7 +1609,7 @@ EXPORTED int index_run_annotator(struct index_state *state,
     }
 
 out:
-    seqset_free(&seq);
+    if (seq != state->searchres) seqset_free(&seq);
 
     if (msgrec) msgrecord_unref(&msgrec);
     if (!r) {
@@ -3147,7 +3141,9 @@ EXPORTED int index_copy(struct index_state *state,
 
     srcmailbox = state->mailbox;
 
-    if (!strcmp("$", sequence)) {
+    seq = _parse_sequence(state, sequence, usinguid);
+    if (!strcmpsafe("$", sequence)) {
+        usinguid = 1;
         if (!seqset_first(state->searchres)) {
             /* RFC 5182: 2.1
              * Note that even if the "$" marker contains the empty list of
@@ -3156,12 +3152,6 @@ EXPORTED int index_copy(struct index_state *state,
              */
             return 0;
         }
-
-        seq = state->searchres;
-        usinguid = 1;
-    }
-    else {
-        seq = _parse_sequence(state, sequence, usinguid);
     }
 
     for (msgno = 1; msgno <= state->exists; msgno++) {
@@ -3369,7 +3359,7 @@ EXPORTED int index_copy_remote(struct index_state *state, const char *sequence,
         index_appendremote(state, msgno, pout);
     }
 
-    seqset_free(&seq);
+    if (seq != state->searchres) seqset_free(&seq);
 
     return 0;
 }
@@ -8249,6 +8239,13 @@ static seqset_t *_parse_sequence(struct index_state *state,
 {
     unsigned maxval;
 
+    // handle no sequence
+    if (!sequence) return NULL;
+
+    // handle saved sequences
+    if (!strcmpsafe("$", sequence))
+        return state->searchres;
+
     /* Per RFC 3501, seq-number ABNF:
        "*" represents the largest number in use.
        In the case of message sequence numbers,

From f27fa5afe80a764e7ee790396b45f6aacd59e613 Mon Sep 17 00:00:00 2001
From: Bron Gondwana <brong@fastmailteam.com>
Date: Fri, 3 Jan 2025 15:06:50 +1100
Subject: [PATCH 2/2] QResync: add a test that saved fetch returns correctly

---
 cassandane/Cassandane/Cyrus/QResync.pm | 32 ++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/cassandane/Cassandane/Cyrus/QResync.pm b/cassandane/Cassandane/Cyrus/QResync.pm
index b87465dfb1..813d11c170 100644
--- a/cassandane/Cassandane/Cyrus/QResync.pm
+++ b/cassandane/Cassandane/Cyrus/QResync.pm
@@ -104,4 +104,36 @@ sub test_qresync_simple
     $self->assert_equals("5:10,25:45", $vanished[0][1]);
 }
 
+sub test_qresync_saved_search
+{
+    my ($self) = @_;
+
+    xlog $self, "Make some messages";
+    my $uid = 1;
+    my %msgs;
+    for (1..3)
+    {
+        $msgs{$uid} = $self->make_message("Message $uid");
+        $msgs{$uid}->set_attribute('uid', $uid);
+        $uid++;
+    }
+
+    my $talk = $self->{store}->get_client();
+    $talk->uid(1);
+    $talk->enable("qresync");
+    $talk->select("INBOX");
+    my $since = $talk->get_response_code('highestmodseq');
+    for (4..6)
+    {
+        $msgs{$uid} = $self->make_message("Message $uid");
+        $msgs{$uid}->set_attribute('uid', $uid);
+        $uid++;
+    }
+    $talk->store('5', '+flags', '(\\Deleted)');
+    $talk->expunge();
+    $talk->search('RETURN', ['SAVE'], 'SINCE', '1-Feb-1994');
+    my $res = $talk->fetch('$', ['FLAGS'], ['CHANGEDSINCE', $since, 'VANISHED']);
+    $self->assert_str_equals("4,6", join(',', sort keys %$res));
+}
+
 1;