Repository: duesee/imap-codec (language: rust)

Issue:
feat: Implement `CONDSTORE` and `QRESYNC`.
### Context

* https://www.rfc-editor.org/rfc/rfc7162
* #221
* #629
* #631
* #417
* #439

---

### TODO

* [ ] #632
* [ ] #633

---

### ABNF

* [x] Core

```abnf
mod-sequence-valzer = "0" / mod-sequence-value

mod-sequence-value  = 1*DIGIT
                       ;; Positive unsigned 63-bit integer
                       ;; (mod-sequence)
                       ;; (1 <= n <= 9,223,372,036,854,775,807).
```

- [x] Capability

```abnf
capability          =/ "CONDSTORE" / "QRESYNC"
```

* [x] Code

```abnf
resp-text-code      =/ "HIGHESTMODSEQ" SP mod-sequence-value /
                       "NOMODSEQ" /
                       "MODIFIED" SP sequence-set
		       ;; Errata (?) * in sequence-set is not allowed.

resp-text-code      =/ "CLOSED"
```

* [x] Status Attributes

```abnf
status-att          =/ "HIGHESTMODSEQ"
                       ;; Extends non-terminal defined in [RFC3501].

status-att-val      =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
                       ;; Extends non-terminal defined in [RFC4466].
                       ;; Value 0 denotes that the mailbox doesn't
                       ;; support persistent mod-sequences
                       ;; as described in Section 3.1.2.2.
```

* [x] Modifier (Store)

```abnf
store-modifier      =/ "UNCHANGEDSINCE" SP mod-sequence-valzer
                       ;; Only a single "UNCHANGEDSINCE" may be
                       ;; specified in a STORE operation.
```

* [x] Modifier (Fetch)

```abnf
fetch-modifier      =/ chgsince-fetch-mod
                       ;; Conforms to the generic "fetch-modifier"
                       ;; syntax defined in [RFC4466].
                       ;; TODO: Submit errata? Add rexpunges-fetch-mod?

chgsince-fetch-mod  = "CHANGEDSINCE" SP mod-sequence-value
                       ;; CHANGEDSINCE FETCH modifier conforms to
                       ;; the fetch-modifier syntax.

rexpunges-fetch-mod =  "VANISHED"
                    ;; VANISHED UID FETCH modifier conforms
                    ;; to the fetch-modifier syntax
                    ;; defined in [RFC4466].  It is only
                    ;; allowed in the UID FETCH command.
```

* [x] Modifier (Select)

```abnf
select-param        =/ condstore-param
                       ;; Conforms to the generic "select-param"
                       ;; non-terminal syntax defined in [RFC4466].

condstore-param     = "CONDSTORE"
```

```abnf
select-param        =/  "QRESYNC" SP "(" uidvalidity SP
                    mod-sequence-value [SP known-uids]
                    [SP seq-match-data] ")"
                    ;; Conforms to the generic select-param
                    ;; syntax defined in [RFC4466].

uidvalidity         =  nz-number

known-uids          =  sequence-set
                    ;; Sequence of UIDs; "*" is not allowed.

seq-match-data      =  "(" known-sequence-set SP known-uid-set ")"

known-sequence-set  =  sequence-set
                    ;; Set of message numbers corresponding to
                    ;; the UIDs in known-uid-set, in ascending order.
                    ;; * is not allowed.

known-uid-set       =  sequence-set
                    ;; Set of UIDs corresponding to the messages in
                    ;; known-sequence-set, in ascending order.
                    ;; * is not allowed.
```

* [x] Fetch Attribute

```abnf
fetch-att           =/ fetch-mod-sequence
                       ;; Modifies original IMAP4 fetch-att.

fetch-mod-sequence  = "MODSEQ"

msg-att-dynamic     =/ fetch-mod-resp

fetch-mod-resp      = "MODSEQ" SP "(" permsg-modsequence ")"

permsg-modsequence  = mod-sequence-value
                       ;; Per-message mod-sequence.
```

* [x] Search

```abnf
search-key          =/ search-modsequence
                       ;; Modifies original IMAP4 search-key.
                       ;;
                       ;; This change applies to all commands
                       ;; referencing this non-terminal -- in
                       ;; particular, SEARCH, SORT, and THREAD.

search-modsequence  = "MODSEQ" [search-modseq-ext] SP
                      mod-sequence-valzer

search-modseq-ext   = SP entry-name SP entry-type-req

entry-name          = entry-flag-name

entry-flag-name     = DQUOTE "/flags/" attr-flag DQUOTE
                       ;; Each system or user-defined flag <flag>
                       ;; is mapped to "/flags/<flag>".
                       ;;
                       ;; <entry-flag-name> follows the escape rules
                       ;; used by "quoted" string as described in
                       ;; Section 4.3 of [RFC3501]; e.g., for the
                       ;; flag \Seen, the corresponding <entry-name>
                       ;; is "/flags/\\seen", and for the flag
                       ;; $MDNSent, the corresponding <entry-name>
                       ;; is "/flags/$mdnsent".

attr-flag           = "\\Answered" / "\\Flagged" / "\\Deleted" /
                      "\\Seen" / "\\Draft" / attr-flag-keyword /
                      attr-flag-extension
                       ;; Does not include "\\Recent".

attr-flag-keyword   = atom

attr-flag-extension = "\\" atom
                       ;; Future expansion.  Client implementations
                       ;; MUST accept flag-extension flags.  Server
                       ;; implementations MUST NOT generate
                       ;; flag-extension flags, except as defined by
                       ;; future standards or Standards Track
                       ;; revisions of [RFC3501].

entry-type-req      = entry-type-resp / "all"
                       ;; Perform SEARCH operation on a private
                       ;; metadata item, shared metadata item,
                       ;; or both.

entry-type-resp     = "priv" / "shared"
                       ;; Metadata item type.
```

* [x] Search / Sort

```abnf
search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
```

```abnf
mailbox-data        =/ "SEARCH" [1*(SP nz-number) SP
                       search-sort-mod-seq]
```

```abnf
sort-data           = "SORT" [1*(SP nz-number) SP
                       search-sort-mod-seq]
                       ; Updates the SORT response from RFC 5256.
```

* [x] Vanished

```abnf
message-data        =/ expunged-resp

expunged-resp       =  "VANISHED" [SP "(EARLIER)"] SP known-uids
```

---

- [X] CONDSTORE activation
   - [X] **ENABLE COMMAND:** CONDSTORE (already added by you some times ago)
- [x] Response
   - [X] **SELECT/EXAMINE COMMAND:** HIGHESTMODSEQ Status Code *(RFC g. add.)*
   - [X] **SELECT/EXAMINE COMMAND:** NOMODSEQ Status Code *(RFC g. add.)*
   - [x] **STATUS COMMAND** HIGHESTMODSEQ Response parameter to the STATUS Response *(RFC h. add.)* 
   - [x] **FETCH COMMAND** MODSEQ data item *(RFC c. add)*
   - [X] **STORE COMMAND** MODIFIED STATUS RESPONSE CODE *(RFC b. add.)*
   - [x] **SEARCH COMMAND** Extend search response syntax *(RFC f. add.)*
-  [x] Modifiers
   - [x] **SELECT/EXAMINE**: CONDSTORE MODIFIER *(RFC h. add.)*
   - [x] **FETCH COMMAND**: CHANGEDSINCE modifier *(RFC d. add.)*
   - [x] **STORE COMMAND**: UNCHANGEDSINCE modifier *(RFC a. add.)*
- [x] Query
   - [x] **FETCH COMMAND**: MODSEQ data item name *(RFC c. add.)*
   - [x] **SEARCH COMMAND**: MODSEQ *(RFC e. add.)*


Discussion / hints:

Fix patch:
diff --git a/imap-codec/src/codec.rs b/imap-codec/src/codec.rs
index 9b058bab..8c129e45 100644
--- a/imap-codec/src/codec.rs
+++ b/imap-codec/src/codec.rs
@@ -111,6 +111,8 @@ mod tests {
                     "a",
                     CommandBody::Select {
                         mailbox: Mailbox::Inbox,
+                        #[cfg(feature = "ext_condstore_qresync")]
+                        parameters: Vec::default(),
                     },
                 )
                 .unwrap(),
@@ -122,6 +124,8 @@ mod tests {
                     "a",
                     CommandBody::Select {
                         mailbox: Mailbox::Inbox,
+                        #[cfg(feature = "ext_condstore_qresync")]
+                        parameters: Vec::default(),
                     },
                 )
                 .unwrap(),
@@ -135,12 +139,20 @@ mod tests {
             (
                 b"* SEARCH 1\r\n".as_ref(),
                 b"".as_ref(),
-                Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])),
+                Response::Data(Data::Search(
+                    vec![NonZeroU32::new(1).unwrap()],
+                    #[cfg(feature = "ext_condstore_qresync")]
+                    None,
+                )),
             ),
             (
                 b"* SEARCH 1\r\n???",
                 b"???",
-                Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])),
+                Response::Data(Data::Search(
+                    vec![NonZeroU32::new(1).unwrap()],
+                    #[cfg(feature = "ext_condstore_qresync")]
+                    None,
+                )),
             ),
             (
                 b"* 1 FETCH (RFC822 {5}\r\nhello)\r\n",
diff --git a/imap-codec/src/codec/decode.rs b/imap-codec/src/codec/decode.rs
index 1028cb19..ac76d4e9 100644
--- a/imap-codec/src/codec/decode.rs
+++ b/imap-codec/src/codec/decode.rs
@@ -474,6 +474,8 @@ mod tests {
                         "a",
                         CommandBody::Select {
                             mailbox: Mailbox::Inbox,
+                            #[cfg(feature = "ext_condstore_qresync")]
+                            parameters: Vec::default(),
                         },
                     )
                     .unwrap(),
@@ -487,6 +489,8 @@ mod tests {
                         "a",
                         CommandBody::Select {
                             mailbox: Mailbox::Inbox,
+                            #[cfg(feature = "ext_condstore_qresync")]
+                            parameters: Vec::default(),
                         },
                     )
                     .unwrap(),
@@ -675,14 +679,22 @@ mod tests {
                 b"* SEARCH 1\r\n".as_ref(),
                 Ok((
                     b"".as_ref(),
-                    Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])),
+                    Response::Data(Data::Search(
+                        vec![NonZeroU32::new(1).unwrap()],
+                        #[cfg(feature = "ext_condstore_qresync")]
+                        None,
+                    )),
                 )),
             ),
             (
                 b"* SEARCH 1\r\n???".as_ref(),
                 Ok((
                     b"???".as_ref(),
-                    Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])),
+                    Response::Data(Data::Search(
+                        vec![NonZeroU32::new(1).unwrap()],
+                        #[cfg(feature = "ext_condstore_qresync")]
+                        None,
+                    )),
                 )),
             ),
             (
diff --git a/imap-codec/src/codec/encode.rs b/imap-codec/src/codec/encode.rs
index 79e5e6ee..b166ddaa 100644
--- a/imap-codec/src/codec/encode.rs
+++ b/imap-codec/src/codec/encode.rs
@@ -51,6 +51,8 @@ use std::{borrow::Borrow, collections::VecDeque, io::Write, num::NonZeroU32};
 
 use base64::{engine::general_purpose::STANDARD as base64, Engine};
 use chrono::{DateTime as ChronoDateTime, FixedOffset};
+#[cfg(feature = "ext_condstore_qresync")]
+use imap_types::command::{FetchModifier, SelectParameter, StoreModifier};
 use imap_types::{
     auth::{AuthMechanism, AuthenticateData},
     body::{
@@ -323,16 +325,42 @@ impl EncodeIntoContext for CommandBody<'_> {
                 ctx.write_all(b" ")?;
                 password.declassify().encode_ctx(ctx)
             }
-            CommandBody::Select { mailbox } => {
+            CommandBody::Select {
+                mailbox,
+                #[cfg(feature = "ext_condstore_qresync")]
+                parameters,
+            } => {
                 ctx.write_all(b"SELECT")?;
                 ctx.write_all(b" ")?;
-                mailbox.encode_ctx(ctx)
+                mailbox.encode_ctx(ctx)?;
+
+                #[cfg(feature = "ext_condstore_qresync")]
+                if !parameters.is_empty() {
+                    ctx.write_all(b" (")?;
+                    join_serializable(parameters, b" ", ctx)?;
+                    ctx.write_all(b")")?;
+                }
+
+                Ok(())
             }
             CommandBody::Unselect => ctx.write_all(b"UNSELECT"),
-            CommandBody::Examine { mailbox } => {
+            CommandBody::Examine {
+                mailbox,
+                #[cfg(feature = "ext_condstore_qresync")]
+                parameters,
+            } => {
                 ctx.write_all(b"EXAMINE")?;
                 ctx.write_all(b" ")?;
-                mailbox.encode_ctx(ctx)
+                mailbox.encode_ctx(ctx)?;
+
+                #[cfg(feature = "ext_condstore_qresync")]
+                if !parameters.is_empty() {
+                    ctx.write_all(b" (")?;
+                    join_serializable(parameters, b" ", ctx)?;
+                    ctx.write_all(b")")?;
+                }
+
+                Ok(())
             }
             CommandBody::Create { mailbox } => {
                 ctx.write_all(b"CREATE")?;
@@ -484,7 +512,7 @@ impl EncodeIntoContext for CommandBody<'_> {
                 macro_or_item_names,
                 uid,
                 #[cfg(feature = "ext_condstore_qresync")]
-                changed_since,
+                modifiers,
             } => {
                 if *uid {
                     ctx.write_all(b"UID FETCH ")?;
@@ -497,10 +525,10 @@ impl EncodeIntoContext for CommandBody<'_> {
                 macro_or_item_names.encode_ctx(ctx)?;
 
                 #[cfg(feature = "ext_condstore_qresync")]
-                if let Some(changed_since) = changed_since {
-                    ctx.write_all(b" (CHANGEDSINCE ")?;
-                    changed_since.encode_ctx(ctx)?;
-                    ctx.write_all(b" ")?;
+                if !modifiers.is_empty() {
+                    ctx.write_all(b" (")?;
+                    join_serializable(modifiers, b" ", ctx)?;
+                    ctx.write_all(b")")?;
                 }
 
                 Ok(())
@@ -512,7 +540,7 @@ impl EncodeIntoContext for CommandBody<'_> {
                 flags,
                 uid,
                 #[cfg(feature = "ext_condstore_qresync")]
-                unchanged_since,
+                modifiers,
             } => {
                 if *uid {
                     ctx.write_all(b"UID STORE ")?;
@@ -524,9 +552,9 @@ impl EncodeIntoContext for CommandBody<'_> {
                 ctx.write_all(b" ")?;
 
                 #[cfg(feature = "ext_condstore_qresync")]
-                if let Some(unchanged_since) = unchanged_since {
-                    ctx.write_all(b"(UNCHANGEDSINCE ")?;
-                    unchanged_since.encode_ctx(ctx)?;
+                if !modifiers.is_empty() {
+                    ctx.write_all(b"(")?;
+                    join_serializable(modifiers, b" ", ctx)?;
                     ctx.write_all(b") ")?;
                 }
 
@@ -676,6 +704,60 @@ impl EncodeIntoContext for CommandBody<'_> {
     }
 }
 
+#[cfg(feature = "ext_condstore_qresync")]
+impl EncodeIntoContext for FetchModifier {
+    fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
+        match self {
+            FetchModifier::ChangedSince(since) => write!(ctx, "CHANGEDSINCE {since}"),
+            FetchModifier::Vanished => write!(ctx, "VANISHED"),
+        }
+    }
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+impl EncodeIntoContext for StoreModifier {
+    fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
+        match self {
+            StoreModifier::UnchangedSince(since) => write!(ctx, "UNCHANGEDSINCE {since}"),
+        }
+    }
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+impl EncodeIntoContext for SelectParameter {
+    fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
+        match self {
+            SelectParameter::CondStore => write!(ctx, "CONDSTORE"),
+            SelectParameter::QResync {
+                uid_validity,
+                mod_sequence_value,
+                known_uids,
+                seq_match_data,
+            } => {
+                write!(ctx, "QRESYNC (")?;
+                uid_validity.encode_ctx(ctx)?;
+                write!(ctx, " ")?;
+                mod_sequence_value.encode_ctx(ctx)?;
+
+                if let Some(known_uids) = known_uids {
+                    write!(ctx, " ")?;
+                    known_uids.encode_ctx(ctx)?;
+                }
+
+                if let Some((known_sequence_set, known_uid_set)) = seq_match_data {
+                    write!(ctx, " (")?;
+                    known_sequence_set.encode_ctx(ctx)?;
+                    write!(ctx, " ")?;
+                    known_uid_set.encode_ctx(ctx)?;
+                    write!(ctx, ")")?;
+                }
+
+                write!(ctx, ")")
+            }
+        }
+    }
+}
+
 impl EncodeIntoContext for AuthMechanism<'_> {
     fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
         write!(ctx, "{}", self)
@@ -930,6 +1012,16 @@ impl EncodeIntoContext for SearchKey<'_> {
                 sequence_set.encode_ctx(ctx)
             }
             SearchKey::Undraft => ctx.write_all(b"UNDRAFT"),
+            #[cfg(feature = "ext_condstore_qresync")]
+            SearchKey::ModSequence { entry, modseq } => {
+                ctx.write_all(b"MODSEQ")?;
+                if let Some((attribute_flag, entry_type_req)) = entry {
+                    write!(ctx, " \"/flags/{attribute_flag}\"")?;
+                    write!(ctx, " {entry_type_req}")?;
+                }
+                ctx.write_all(b" ")?;
+                modseq.encode_ctx(ctx)
+            }
             SearchKey::SequenceSet(sequence_set) => sequence_set.encode_ctx(ctx),
             SearchKey::And(search_keys) => {
                 ctx.write_all(b"(")?;
@@ -1381,6 +1473,8 @@ impl EncodeIntoContext for Data<'_> {
                 join_serializable(items, b" ", ctx)?;
                 ctx.write_all(b")")?;
             }
+            // TODO: Exclude pattern via cfg?
+            #[cfg(not(feature = "ext_condstore_qresync"))]
             Data::Search(seqs) => {
                 if seqs.is_empty() {
                     ctx.write_all(b"* SEARCH")?;
@@ -1389,6 +1483,24 @@ impl EncodeIntoContext for Data<'_> {
                     join_serializable(seqs, b" ", ctx)?;
                 }
             }
+            // TODO: Exclude pattern via cfg?
+            #[cfg(feature = "ext_condstore_qresync")]
+            Data::Search(seqs, modseq) => {
+                if seqs.is_empty() {
+                    ctx.write_all(b"* SEARCH")?;
+                } else {
+                    ctx.write_all(b"* SEARCH ")?;
+                    join_serializable(seqs, b" ", ctx)?;
+                }
+
+                if let Some(modseq) = modseq {
+                    ctx.write_all(b" (MODSEQ ")?;
+                    modseq.encode_ctx(ctx)?;
+                    ctx.write_all(b")")?;
+                }
+            }
+            // TODO: Exclude pattern via cfg?
+            #[cfg(not(feature = "ext_condstore_qresync"))]
             Data::Sort(seqs) => {
                 if seqs.is_empty() {
                     ctx.write_all(b"* SORT")?;
@@ -1397,6 +1509,22 @@ impl EncodeIntoContext for Data<'_> {
                     join_serializable(seqs, b" ", ctx)?;
                 }
             }
+            // TODO: Exclude pattern via cfg?
+            #[cfg(feature = "ext_condstore_qresync")]
+            Data::Sort(seqs, modseq) => {
+                if seqs.is_empty() {
+                    ctx.write_all(b"* SORT")?;
+                } else {
+                    ctx.write_all(b"* SORT ")?;
+                    join_serializable(seqs, b" ", ctx)?;
+                }
+
+                if let Some(modseq) = modseq {
+                    ctx.write_all(b" (MODSEQ ")?;
+                    modseq.encode_ctx(ctx)?;
+                    ctx.write_all(b")")?;
+                }
+            }
             Data::Thread(threads) => {
                 if threads.is_empty() {
                     ctx.write_all(b"* THREAD")?;
@@ -1487,6 +1615,18 @@ impl EncodeIntoContext for Data<'_> {
                 ctx.write_all(b" ")?;
                 items.encode_ctx(ctx)?;
             }
+            #[cfg(feature = "ext_condstore_qresync")]
+            Data::Vanished {
+                earlier,
+                known_uids,
+            } => {
+                ctx.write_all(b"* VANISHED")?;
+                if *earlier {
+                    ctx.write_all(b" (EARLIER)")?;
+                }
+                ctx.write_all(b" ")?;
+                known_uids.encode_ctx(ctx)?;
+            }
         }
 
         ctx.write_all(b"\r\n")
diff --git a/imap-codec/src/command.rs b/imap-codec/src/command.rs
index d9a6f1d8..ed1ed6a6 100644
--- a/imap-codec/src/command.rs
+++ b/imap-codec/src/command.rs
@@ -5,6 +5,8 @@ use abnf_core::streaming::crlf;
 #[cfg(feature = "quirk_crlf_relaxed")]
 use abnf_core::streaming::crlf_relaxed as crlf;
 use abnf_core::streaming::sp;
+#[cfg(feature = "ext_condstore_qresync")]
+use imap_types::command::{FetchModifier, SelectParameter, StoreModifier};
 use imap_types::{
     auth::AuthMechanism,
     command::{Command, CommandBody},
@@ -16,6 +18,8 @@ use imap_types::{
 };
 #[cfg(feature = "ext_condstore_qresync")]
 use nom::character::streaming::char;
+#[cfg(feature = "ext_condstore_qresync")]
+use nom::sequence::separated_pair;
 use nom::{
     branch::alt,
     bytes::streaming::{tag, tag_no_case},
@@ -24,6 +28,8 @@ use nom::{
     sequence::{delimited, preceded, terminated, tuple},
 };
 
+#[cfg(feature = "ext_condstore_qresync")]
+use crate::core::nz_number;
 #[cfg(feature = "ext_condstore_qresync")]
 use crate::extensions::condstore_qresync::mod_sequence_value;
 #[cfg(feature = "ext_condstore_qresync")]
@@ -214,13 +220,29 @@ pub(crate) fn delete(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     Ok((remaining, CommandBody::Delete { mailbox }))
 }
 
-/// `examine = "EXAMINE" SP mailbox`
+/// ```abnf
+/// examine = "EXAMINE" SP mailbox [select-params]
+///                                ^^^^^^^^^^^^^^^
+///                                |
+///                                RFC 4466: modifies the original IMAP EXAMINE command to accept optional parameters
+/// ```
 pub(crate) fn examine(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     let mut parser = preceded(tag_no_case(b"EXAMINE "), mailbox);
 
     let (remaining, mailbox) = parser(input)?;
 
-    Ok((remaining, CommandBody::Examine { mailbox }))
+    #[cfg(feature = "ext_condstore_qresync")]
+    let (remaining, parameters) =
+        map(opt(select_params), |params| params.unwrap_or_default())(remaining)?;
+
+    Ok((
+        remaining,
+        CommandBody::Examine {
+            mailbox,
+            #[cfg(feature = "ext_condstore_qresync")]
+            parameters,
+        },
+    ))
 }
 
 /// `list = "LIST" SP mailbox SP list-mailbox`
@@ -270,13 +292,110 @@ pub(crate) fn rename(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     ))
 }
 
-/// `select = "SELECT" SP mailbox`
+/// ```abnf
+/// select = "SELECT" SP mailbox [select-params]
+///                              ^^^^^^^^^^^^^^^
+///                              |
+///                              RFC 4466: modifies the original IMAP SELECT command to accept optional parameters
+/// ```
 pub(crate) fn select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     let mut parser = preceded(tag_no_case(b"SELECT "), mailbox);
 
     let (remaining, mailbox) = parser(input)?;
 
-    Ok((remaining, CommandBody::Select { mailbox }))
+    #[cfg(feature = "ext_condstore_qresync")]
+    let (remaining, parameters) =
+        map(opt(select_params), |params| params.unwrap_or_default())(remaining)?;
+
+    Ok((
+        remaining,
+        CommandBody::Select {
+            mailbox,
+            #[cfg(feature = "ext_condstore_qresync")]
+            parameters,
+        },
+    ))
+}
+
+/// FROM RFC4466:
+///
+/// ```abnf
+/// select-params = SP "(" select-param *(SP select-param) ")"
+/// ```
+#[cfg(feature = "ext_condstore_qresync")]
+pub(crate) fn select_params(input: &[u8]) -> IMAPResult<&[u8], Vec<SelectParameter>> {
+    delimited(tag(" ("), separated_list1(sp, select_param), tag(")"))(input)
+}
+
+/// FROM RFC4466:
+///
+/// ```abnf
+/// select-param = select-param-name [SP select-param-value]
+///                ;; a parameter to SELECT may contain one or more atoms and/or strings and/or lists.
+///
+/// select-param-name = tagged-ext-label
+///
+/// select-param-value = tagged-ext-val
+///                      ;; This non-terminal shows recommended syntax for future extensions.
+/// ```
+///
+/// FROM RFC 7162 (CONDSTORE/QRESYNC):
+///
+/// ```abnf
+/// select-param =/ condstore-param
+///              ;; Conforms to the generic "select-param" non-terminal syntax defined in [RFC4466].
+///
+/// condstore-param = "CONDSTORE"
+///
+/// select-param =/ "QRESYNC" SP "("
+///                   uidvalidity SP
+///                   mod-sequence-value [SP known-uids]
+///                   [SP seq-match-data]
+///                 ")"
+///                 ;; Conforms to the generic select-param syntax defined in [RFC4466].
+///
+/// uidvalidity = nz-number
+///
+/// known-uids = sequence-set
+///              ;; Sequence of UIDs; "*" is not allowed.
+///
+/// seq-match-data = "(" known-sequence-set SP known-uid-set ")"
+///
+/// known-sequence-set = sequence-set
+///                    ;; Set of message numbers corresponding to
+///                    ;; the UIDs in known-uid-set, in ascending order.
+///                    ;; * is not allowed.
+///
+/// known-uid-set = sequence-set
+///                 ;; Set of UIDs corresponding to the messages in
+///                 ;; known-sequence-set, in ascending order.
+///                 ;; * is not allowed.
+/// ```
+#[cfg(feature = "ext_condstore_qresync")]
+pub(crate) fn select_param(input: &[u8]) -> IMAPResult<&[u8], SelectParameter> {
+    alt((
+        value(SelectParameter::CondStore, tag_no_case("CONDSTORE")),
+        map(
+            delimited(
+                tag_no_case("QRESYNC ("),
+                tuple((
+                    terminated(nz_number, sp),
+                    mod_sequence_value,
+                    opt(preceded(sp, sequence_set)),
+                    opt(preceded(sp, separated_pair(sequence_set, sp, sequence_set))),
+                )),
+                char(')'),
+            ),
+            |(uid_validity, mod_sequence_value, known_uids, seq_match_data)| {
+                SelectParameter::QResync {
+                    uid_validity,
+                    mod_sequence_value,
+                    known_uids,
+                    seq_match_data,
+                }
+            },
+        ),
+    ))(input)
 }
 
 /// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"`
@@ -452,38 +571,7 @@ pub(crate) fn copy(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
 ///                                     "FULL" /
 ///                                     "FAST" /
 ///                                     fetch-att / "(" fetch-att *(SP fetch-att) ")")
-/// ```
-///
-/// From RFC 4466:
-///
-/// ```abnf
-/// fetch = "FETCH" SP sequence-set SP ("ALL" /
-///                                     "FULL" /
-///                                     "FAST" /
-///                                     fetch-att / "(" fetch-att *(SP fetch-att) ")")
-///                                     [fetch-modifiers]
-///                                     ;; modifies the original IMAP4 FETCH command to
-///                                     ;; accept optional modifiers
-///
-/// fetch-modifiers = SP "(" fetch-modifier *(SP fetch-modifier) ")"
-///
-/// fetch-modifier = fetch-modifier-name [ SP fetch-modif-params ]
-///
-/// fetch-modif-params = tagged-ext-val
-///                      ;; This non-terminal shows recommended syntax
-///                      ;; for future extensions.
-///
-/// fetch-modifier-name = tagged-ext-label
-/// ```
-///
-/// From RFC 7162 (CONDSTORE/QRESYNC):
-///
-/// ```abnf
-/// fetch-modifier =/ chgsince-fetch-mod
-///                   ;; Conforms to the generic "fetch-modifier" syntax defined in [RFC4466].
-///
-/// chgsince-fetch-mod = "CHANGEDSINCE" SP mod-sequence-value
-///                      ;; CHANGEDSINCE FETCH modifier conforms to the fetch-modifier syntax.
+///                                     [fetch-modifiers] ; FROM RFC 4466
 /// ```
 pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     let mut parser = tuple((
@@ -514,18 +602,14 @@ pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
             )),
         ),
         #[cfg(feature = "ext_condstore_qresync")]
-        opt(delimited(
-            tag(" ("),
-            preceded(tag_no_case("CHANGEDSINCE "), mod_sequence_value),
-            char(')'),
-        )),
+        map(opt(fetch_modifiers), Option::unwrap_or_default),
     ));
 
     #[cfg(not(feature = "ext_condstore_qresync"))]
     let (remaining, (_, sequence_set, macro_or_item_names)) = parser(input)?;
 
     #[cfg(feature = "ext_condstore_qresync")]
-    let (remaining, (_, sequence_set, macro_or_item_names, changed_since)) = parser(input)?;
+    let (remaining, (_, sequence_set, macro_or_item_names, modifiers)) = parser(input)?;
 
     Ok((
         remaining,
@@ -534,52 +618,69 @@ pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
             macro_or_item_names,
             uid: false,
             #[cfg(feature = "ext_condstore_qresync")]
-            changed_since,
+            modifiers,
         },
     ))
 }
 
+#[cfg(feature = "ext_condstore_qresync")]
+/// From RFC 4466:
+///
 /// ```abnf
-/// store = "STORE" SP sequence-set SP store-att-flags
+/// fetch-modifiers = SP "(" fetch-modifier *(SP fetch-modifier) ")"
 /// ```
-///
-/// With store-modifier from RFC 4466 (enabled by CONDSTORE/QRESYNC):
+pub(crate) fn fetch_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<FetchModifier>> {
+    delimited(tag(" ("), separated_list1(sp, fetch_modifier), char(')'))(input)
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+/// From RFC 4466:
 ///
 /// ```abnf
-/// store = "STORE" SP sequence-set [store-modifiers] SP store-att-flags
-///
-/// store-modifiers =  SP "(" store-modifier *(SP store-modifier) ")"
-///
-/// store-modifier = store-modifier-name [SP store-modif-params]
+/// fetch-modifier = fetch-modifier-name [ SP fetch-modif-params ]
 ///
-/// store-modif-params = tagged-ext-val
+/// fetch-modif-params = tagged-ext-val
+///                      ;; This non-terminal shows recommended syntax
+///                      ;; for future extensions.
 ///
-/// store-modifier-name = tagged-ext-label
+/// fetch-modifier-name = tagged-ext-label
 /// ```
 ///
 /// From RFC 7162 (CONDSTORE/QRESYNC):
 ///
 /// ```abnf
-/// store-modifier =/ "UNCHANGEDSINCE" SP mod-sequence-valzer
-///                ;; Only a single "UNCHANGEDSINCE" may be specified in a STORE operation.
+/// fetch-modifier =/ chgsince-fetch-mod
+///                   ;; Conforms to the generic "fetch-modifier" syntax defined in [RFC4466].
+///
+/// chgsince-fetch-mod = "CHANGEDSINCE" SP mod-sequence-value
+///                      ;; CHANGEDSINCE FETCH modifier conforms to the fetch-modifier syntax.
+///
+/// rexpunges-fetch-mod = "VANISHED"
+///                     ;; VANISHED UID FETCH modifier conforms to the fetch-modifier syntax defined in [RFC4466].
+///                     ;; It is only allowed in the UID FETCH command.
 /// ```
-pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
-    #[cfg(not(feature = "ext_condstore_qresync"))]
-    let mut parser = tuple((
-        tag_no_case(b"STORE"),
-        preceded(sp, sequence_set),
-        preceded(sp, store_att_flags),
-    ));
+pub(crate) fn fetch_modifier(input: &[u8]) -> IMAPResult<&[u8], FetchModifier> {
+    alt((
+        map(
+            preceded(tag_no_case("CHANGEDSINCE "), mod_sequence_value),
+            FetchModifier::ChangedSince,
+        ),
+        value(FetchModifier::Vanished, tag_no_case("VANISHED")),
+    ))(input)
+}
 
-    #[cfg(feature = "ext_condstore_qresync")]
+/// ```abnf
+/// store = "STORE" SP sequence-set [store-modifiers] SP store-att-flags
+///                                 ^^^^^^^^^^^^^^^^^
+///                                 |
+///                                 RFC 4466
+/// ```
+pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     let mut parser = tuple((
         tag_no_case(b"STORE"),
         preceded(sp, sequence_set),
-        opt(delimited(
-            tag(" ("),
-            preceded(tag_no_case("UNCHANGEDSINCE "), mod_sequence_valzer),
-            char(')'),
-        )),
+        #[cfg(feature = "ext_condstore_qresync")]
+        map(opt(store_modifiers), Option::unwrap_or_default),
         preceded(sp, store_att_flags),
     ));
 
@@ -587,7 +688,7 @@ pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     let (remaining, (_, sequence_set, (kind, response, flags))) = parser(input)?;
 
     #[cfg(feature = "ext_condstore_qresync")]
-    let (remaining, (_, sequence_set, unchanged_since, (kind, response, flags))) = parser(input)?;
+    let (remaining, (_, sequence_set, modifiers, (kind, response, flags))) = parser(input)?;
 
     Ok((
         remaining,
@@ -598,11 +699,45 @@ pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
             flags,
             uid: false,
             #[cfg(feature = "ext_condstore_qresync")]
-            unchanged_since,
+            modifiers,
         },
     ))
 }
 
+#[cfg(feature = "ext_condstore_qresync")]
+/// From RFC 4466:
+///
+/// ```abnf
+/// store-modifiers = SP "(" store-modifier *(SP store-modifier) ")"
+/// ```
+pub(crate) fn store_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<StoreModifier>> {
+    delimited(tag(" ("), separated_list1(sp, store_modifier), char(')'))(input)
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+/// From RFC 4466:
+///
+/// ```abnf
+/// store-modifier = store-modifier-name [SP store-modif-params]
+///
+/// store-modif-params = tagged-ext-val
+///
+/// store-modifier-name = tagged-ext-label
+/// ```
+///
+/// From RFC 7162 (CONDSTORE/QRESYNC):
+///
+/// ```abnf
+/// store-modifier =/ "UNCHANGEDSINCE" SP mod-sequence-valzer
+///                ;; Only a single "UNCHANGEDSINCE" may be specified in a STORE operation.
+/// ```
+pub(crate) fn store_modifier(input: &[u8]) -> IMAPResult<&[u8], StoreModifier> {
+    map(
+        preceded(tag_no_case(b"UNCHANGEDSINCE "), mod_sequence_valzer),
+        StoreModifier::UnchangedSince,
+    )(input)
+}
+
 /// `store-att-flags = (["+" / "-"] "FLAGS" [".SILENT"]) SP (flag-list / (flag *(SP flag)))`
 pub(crate) fn store_att_flags(
     input: &[u8],
diff --git a/imap-codec/src/extensions/condstore_qresync.rs b/imap-codec/src/extensions/condstore_qresync.rs
index 130242bd..fec7f9b3 100644
--- a/imap-codec/src/extensions/condstore_qresync.rs
+++ b/imap-codec/src/extensions/condstore_qresync.rs
@@ -1,8 +1,20 @@
 use std::num::NonZeroU64;
 
-use nom::combinator::map_res;
+use abnf_core::streaming::sp;
+#[cfg(feature = "ext_condstore_qresync")]
+use imap_types::extensions::condstore_qresync::{AttributeFlag, EntryTypeReq};
+use nom::{
+    branch::alt,
+    bytes::streaming::{tag, tag_no_case},
+    character::streaming::char,
+    combinator::{map, map_res, opt, value},
+    sequence::{delimited, preceded, tuple},
+};
 
-use crate::{core::number64, decode::IMAPResult};
+use crate::{
+    core::{atom, number64},
+    decode::IMAPResult,
+};
 
 /// ```abnf
 /// mod-sequence-valzer = "0" / mod-sequence-value
@@ -20,6 +32,112 @@ pub(crate) fn mod_sequence_value(input: &[u8]) -> IMAPResult<&[u8], NonZeroU64>
     map_res(number64, NonZeroU64::try_from)(input)
 }
 
+/// ```abnf
+/// search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
+/// ```
+pub(crate) fn search_sort_mod_seq(input: &[u8]) -> IMAPResult<&[u8], NonZeroU64> {
+    delimited(
+        char('('),
+        preceded(tag_no_case("MODSEQ "), mod_sequence_value),
+        char(')'),
+    )(input)
+}
+
+/// ```abnf
+/// search-modsequence = "MODSEQ" [search-modseq-ext] SP mod-sequence-valzer
+/// ```
+#[allow(clippy::type_complexity)]
+pub(crate) fn search_modsequence(
+    input: &[u8],
+) -> IMAPResult<&[u8], (Option<(AttributeFlag, EntryTypeReq)>, u64)> {
+    preceded(
+        tag_no_case("MODSEQ"),
+        tuple((opt(search_modseq_ext), preceded(sp, mod_sequence_valzer))),
+    )(input)
+}
+
+/// ```abnf
+/// search-modseq-ext = SP entry-name SP entry-type-req
+/// ```
+pub(crate) fn search_modseq_ext(input: &[u8]) -> IMAPResult<&[u8], (AttributeFlag, EntryTypeReq)> {
+    tuple((preceded(sp, entry_name), preceded(sp, entry_type_req)))(input)
+}
+
+/// ```abnf
+/// entry-name = entry-flag-name
+/// ```
+#[inline]
+pub(crate) fn entry_name(input: &[u8]) -> IMAPResult<&[u8], AttributeFlag> {
+    entry_flag_name(input)
+}
+
+/// Each system or user-defined flag \<flag\> is mapped to "/flags/\<flag\>".
+///
+/// \<entry-flag-name\> follows the escape rules used by "quoted" string as described in
+/// Section 4.3 of \[RFC3501\]; e.g., for the flag \Seen, the corresponding \<entry-name\>
+/// is "/flags/\\seen", and for the flag $MDNSent, the corresponding \<entry-name\>
+/// is "/flags/$mdnsent".
+///
+/// ```abnf
+/// entry-flag-name = DQUOTE "/flags/" attr-flag DQUOTE
+/// ```
+pub(crate) fn entry_flag_name(input: &[u8]) -> IMAPResult<&[u8], AttributeFlag> {
+    delimited(tag_no_case("\"/flags/"), attr_flag, char('"'))(input)
+}
+
+/// ```abnf
+/// attr-flag = "\\Answered" /
+///             "\\Flagged" /
+///             "\\Deleted" /
+///             "\\Seen" /
+///             "\\Draft" /
+///             attr-flag-keyword /
+///             attr-flag-extension
+///             ;; Does not include "\\Recent".
+/// ```
+pub(crate) fn attr_flag(input: &[u8]) -> IMAPResult<&[u8], AttributeFlag> {
+    alt((
+        map(preceded(tag("\\\\"), atom), AttributeFlag::system),
+        map(atom, AttributeFlag::Keyword),
+    ))(input)
+}
+
+// /// ```abnf
+// /// attr-flag-keyword = atom
+// /// ```
+// #[inline]
+// pub(crate) fn attr_flag_keyword(input: &[u8]) -> IMAPResult<&[u8], Atom> {
+//     atom(input)
+// }
+
+// /// Future expansion.
+// /// Client implementations MUST accept flag-extension flags.
+// /// Server implementations MUST NOT generate flag-extension flags, except as defined by future
+// /// standards or Standards Track revisions of [RFC3501].
+// ///
+// /// ```abnf
+// /// attr-flag-extension = "\\" atom
+// /// ```
+// pub(crate) fn attr_flag_extension(input: &[u8]) -> IMAPResult<&[u8], Atom> {
+//     preceded(tag("\\\\"), atom)(input)
+// }
+
+/// ```abnf
+/// ;; Perform SEARCH operation on a private metadata item,
+/// ;; shared metadata item, or both.
+/// entry-type-req = entry-type-resp / "all"
+///
+/// ;; Metadata item type.
+/// entry-type-resp = "priv" / "shared"
+/// ```
+pub(crate) fn entry_type_req(input: &[u8]) -> IMAPResult<&[u8], EntryTypeReq> {
+    alt((
+        value(EntryTypeReq::Private, tag_no_case("priv")),
+        value(EntryTypeReq::Shared, tag_no_case("shared")),
+        value(EntryTypeReq::All, tag_no_case("all")),
+    ))(input)
+}
+
 #[cfg(test)]
 mod tests {
     use crate::response::resp_text;
diff --git a/imap-codec/src/fetch.rs b/imap-codec/src/fetch.rs
index a975d43e..93dc7ec2 100644
--- a/imap-codec/src/fetch.rs
+++ b/imap-codec/src/fetch.rs
@@ -113,6 +113,8 @@ pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName>
         value(MessageDataItemName::Rfc822Size, tag_no_case(b"RFC822.SIZE")),
         value(MessageDataItemName::Rfc822Text, tag_no_case(b"RFC822.TEXT")),
         value(MessageDataItemName::Rfc822, tag_no_case(b"RFC822")),
+        #[cfg(feature = "ext_condstore_qresync")]
+        value(MessageDataItemName::ModSeq, tag_no_case(b"MODSEQ")),
     ))(input)
 }
 
diff --git a/imap-codec/src/mailbox.rs b/imap-codec/src/mailbox.rs
index d1b3f167..1c205147 100644
--- a/imap-codec/src/mailbox.rs
+++ b/imap-codec/src/mailbox.rs
@@ -6,6 +6,8 @@ use imap_types::{
     response::Data,
     utils::indicators::is_list_char,
 };
+#[cfg(feature = "ext_condstore_qresync")]
+use nom::character::streaming::char;
 use nom::{
     branch::alt,
     bytes::streaming::{tag, tag_no_case, take_while1},
@@ -14,6 +16,8 @@ use nom::{
     sequence::{delimited, preceded, terminated, tuple},
 };
 
+#[cfg(feature = "ext_condstore_qresync")]
+use crate::extensions::condstore_qresync::search_sort_mod_seq;
 #[cfg(feature = "ext_metadata")]
 use crate::extensions::metadata::metadata_resp;
 use crate::{
@@ -61,12 +65,23 @@ pub(crate) fn mailbox(input: &[u8]) -> IMAPResult<&[u8], Mailbox> {
 /// mailbox-data = "FLAGS" SP flag-list /
 ///                "LIST" SP mailbox-list /
 ///                "LSUB" SP mailbox-list /
-///                "SEARCH" *(SP nz-number) /
+///                "SEARCH" *(SP nz-number) [SP search-sort-mod-seq] /
+///                                         ^^^^^^^^^^^^^^^^^^^^^^^^
+///                                         |
+///                                         RFC 7162 (edited)
 ///                "STATUS" SP mailbox SP "(" [status-att-list] ")" /
 ///                "METADATA" SP mailbox SP (entry-values / entry-list) / ; RFC 5464
 ///                number SP "EXISTS" /
 ///                number SP "RECENT"
 /// ```
+///
+/// FROM RFC 7162 (CONDSTORE/QRESYNC):
+///
+/// ```abnf
+/// mailbox-data =/ "SEARCH" [1*(SP nz-number) SP search-sort-mod-seq]
+///
+/// search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
+/// ```
 pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
     alt((
         map(preceded(tag_no_case(b"FLAGS "), flag_list), Data::Flags),
@@ -86,14 +101,34 @@ pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
                 delimiter,
             },
         ),
+        #[cfg(not(feature = "ext_condstore_qresync"))]
         map(
             tuple((tag_no_case(b"SEARCH"), many0(preceded(sp, nz_number)))),
             |(_, nums)| Data::Search(nums),
         ),
+        #[cfg(feature = "ext_condstore_qresync")]
+        map(
+            tuple((
+                tag_no_case(b"SEARCH"),
+                many0(preceded(sp, nz_number)),
+                opt(preceded(char(' '), search_sort_mod_seq)),
+            )),
+            |(_, nums, modseq)| Data::Search(nums, modseq),
+        ),
+        #[cfg(not(feature = "ext_condstore_qresync"))]
         map(
             preceded(tag_no_case(b"SORT"), many0(preceded(sp, nz_number))),
             Data::Sort,
         ),
+        #[cfg(feature = "ext_condstore_qresync")]
+        map(
+            tuple((
+                tag_no_case(b"SORT"),
+                many0(preceded(sp, nz_number)),
+                opt(preceded(char(' '), search_sort_mod_seq)),
+            )),
+            |(_, nums, modseq)| Data::Sort(nums, modseq),
+        ),
         thread_data,
         map(
             tuple((
diff --git a/imap-codec/src/response.rs b/imap-codec/src/response.rs
index 3a83009b..1c40cc13 100644
--- a/imap-codec/src/response.rs
+++ b/imap-codec/src/response.rs
@@ -4,8 +4,11 @@ use abnf_core::streaming::crlf;
 use abnf_core::streaming::crlf_relaxed as crlf;
 use abnf_core::streaming::sp;
 use base64::{engine::general_purpose::STANDARD as _base64, Engine};
+#[cfg(feature = "ext_condstore_qresync")]
+use imap_types::sequence::SequenceSet;
 use imap_types::{
     core::{Text, Vec1},
+    fetch::MessageDataItem,
     response::{
         Bye, Capability, Code, CodeOther, CommandContinuationRequest, Data, Greeting, GreetingKind,
         Response, Status, StatusBody, StatusKind, Tagged,
@@ -385,16 +388,55 @@ pub(crate) fn response_fatal(input: &[u8]) -> IMAPResult<&[u8], Status> {
     Ok((remaining, Status::Bye(Bye { code, text })))
 }
 
-/// `message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))`
+/// ```abnf
+/// message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
+/// ```
+///
+/// From RFC 7162:
+///
+/// ```abnf
+/// message-data =/ expunged-resp
+///
+/// expunged-resp = "VANISHED" [SP "(EARLIER)"] SP known-uids
+/// ```
 pub(crate) fn message_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
-    let (remaining, seq) = terminated(nz_number, sp)(input)?;
+    #[derive(Clone)]
+    enum TmpData<'a> {
+        Expunge,
+        Fetch(Vec1<MessageDataItem<'a>>),
+        #[cfg(feature = "ext_condstore_qresync")]
+        Vanished(bool, SequenceSet),
+    }
 
-    alt((
-        map(tag_no_case(b"EXPUNGE"), move |_| Data::Expunge(seq)),
-        map(preceded(tag_no_case(b"FETCH "), msg_att), move |items| {
-            Data::Fetch { seq, items }
-        }),
-    ))(remaining)
+    let (remaining, (seq, tmp)) = tuple((
+        terminated(nz_number, sp),
+        alt((
+            value(TmpData::Expunge, tag_no_case(b"EXPUNGE")),
+            map(preceded(tag_no_case(b"FETCH "), msg_att), TmpData::Fetch),
+            #[cfg(feature = "ext_condstore_qresync")]
+            map(
+                tuple((
+                    tag_no_case("VANISHED"),
+                    opt(tag_no_case(" (EARLIER)")),
+                    preceded(sp, sequence_set),
+                )),
+                |(_, earlier, known_uids)| TmpData::Vanished(earlier.is_some(), known_uids),
+            ),
+        )),
+    ))(input)?;
+
+    Ok((
+        remaining,
+        match tmp {
+            TmpData::Expunge => Data::Expunge(seq),
+            TmpData::Fetch(items) => Data::Fetch { seq, items },
+            #[cfg(feature = "ext_condstore_qresync")]
+            TmpData::Vanished(earlier, known_uids) => Data::Vanished {
+                earlier,
+                known_uids,
+            },
+        },
+    ))
 }
 
 #[cfg(test)]
@@ -467,12 +509,16 @@ mod tests {
             (
                 b"* SEARCH 1 2 3 42\r\n",
                 b"",
-                Response::Data(Data::Search(vec![
-                    1.try_into().unwrap(),
-                    2.try_into().unwrap(),
-                    3.try_into().unwrap(),
-                    42.try_into().unwrap(),
-                ])),
+                Response::Data(Data::Search(
+                    vec![
+                        1.try_into().unwrap(),
+                        2.try_into().unwrap(),
+                        3.try_into().unwrap(),
+                        42.try_into().unwrap(),
+                    ],
+                    #[cfg(feature = "ext_condstore_qresync")]
+                    None,
+                )),
             ),
             (b"* 42 EXISTS\r\n", b"", Response::Data(Data::Exists(42))),
             (
@@ -675,12 +721,12 @@ mod tests {
                             disposition: None,
                             tail: Some(Language {
                                 language: vec![],
-                                tail: Some(Location{
+                                tail: Some(Location {
                                     location: NString(None),
                                     extensions: vec![BodyExtension::List(Vec1::from(BodyExtension::Number(1337)))],
-                                })
-                            })
-                        })
+                                }),
+                            }),
+                        }),
                     }),
                 },
                 b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14 \"AABB\" NIL NIL NIL (1337))",
diff --git a/imap-codec/src/search.rs b/imap-codec/src/search.rs
index e7a7ff81..b275684f 100644
--- a/imap-codec/src/search.rs
+++ b/imap-codec/src/search.rs
@@ -12,6 +12,8 @@ use nom::{
     sequence::{delimited, separated_pair, tuple},
 };
 
+#[cfg(feature = "ext_condstore_qresync")]
+use crate::extensions::condstore_qresync::search_modsequence;
 use crate::{
     core::{astring, atom, charset, number},
     datetime::date,
@@ -48,44 +50,47 @@ pub(crate) fn search(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
     ))
 }
 
-/// `search-key = "ALL" /
-///               "ANSWERED" /
-///               "BCC" SP astring /
-///               "BEFORE" SP date /
-///               "BODY" SP astring /
-///               "CC" SP astring /
-///               "DELETED" /
-///               "FLAGGED" /
-///               "FROM" SP astring /
-///               "KEYWORD" SP flag-keyword /
-///               "NEW" /
-///               "OLD" /
-///               "ON" SP date /
-///               "RECENT" /
-///               "SEEN" /
-///               "SINCE" SP date /
-///               "SUBJECT" SP astring /
-///               "TEXT" SP astring /
-///               "TO" SP astring /
-///               "UNANSWERED" /
-///               "UNDELETED" /
-///               "UNFLAGGED" /
-///               "UNKEYWORD" SP flag-keyword /
-///               "UNSEEN" /
-///                 ; Above this line were in [IMAP2]
-///               "DRAFT" /
-///               "HEADER" SP header-fld-name SP astring /
-///               "LARGER" SP number /
-///               "NOT" SP search-key /
-///               "OR" SP search-key SP search-key /
-///               "SENTBEFORE" SP date /
-///               "SENTON" SP date /
-///               "SENTSINCE" SP date /
-///               "SMALLER" SP number /
-///               "UID" SP sequence-set /
-///               "UNDRAFT" /
-///               sequence-set /
-///               "(" search-key *(SP search-key) ")"`
+/// ```abnf
+/// search-key = "ALL" /
+///              "ANSWERED" /
+///              "BCC" SP astring /
+///              "BEFORE" SP date /
+///              "BODY" SP astring /
+///              "CC" SP astring /
+///              "DELETED" /
+///              "FLAGGED" /
+///              "FROM" SP astring /
+///              "KEYWORD" SP flag-keyword /
+///              "NEW" /
+///              "OLD" /
+///              "ON" SP date /
+///              "RECENT" /
+///              "SEEN" /
+///              "SINCE" SP date /
+///              "SUBJECT" SP astring /
+///              "TEXT" SP astring /
+///              "TO" SP astring /
+///              "UNANSWERED" /
+///              "UNDELETED" /
+///              "UNFLAGGED" /
+///              "UNKEYWORD" SP flag-keyword /
+///              "UNSEEN" /
+///                ; Above this line were in [IMAP2]
+///              "DRAFT" /
+///              "HEADER" SP header-fld-name SP astring /
+///              "LARGER" SP number /
+///              "NOT" SP search-key /
+///              "OR" SP search-key SP search-key /
+///              "SENTBEFORE" SP date /
+///              "SENTON" SP date /
+///              "SENTSINCE" SP date /
+///              "SMALLER" SP number /
+///              "UID" SP sequence-set /
+///              "UNDRAFT" /
+///              search-modsequence / ; RFC 7162
+///              sequence-set /
+///              "(" search-key *(SP search-key) ")"
+/// ```
 ///
 /// This parser is recursively defined. Thus, in order to not overflow the stack,
 /// it is needed to limit how may recursions are allowed. (8 should suffice).
@@ -207,6 +212,10 @@ fn search_key_limited(input: &[u8], remaining_recursion: usize) -> IMAPResult<&[
                 |(_, _, val)| SearchKey::Uid(val),
             ),
             value(SearchKey::Undraft, tag_no_case(b"UNDRAFT")),
+            #[cfg(feature = "ext_condstore_qresync")]
+            map(search_modsequence, |(entry, modseq)| {
+                SearchKey::ModSequence { entry, modseq }
+            }),
             map(sequence_set, SearchKey::SequenceSet),
             map(
                 delimited(tag(b"("), separated_list1(sp, search_key), tag(b")")),
diff --git a/imap-types/src/arbitrary.rs b/imap-types/src/arbitrary.rs
index 006a223e..30266e3f 100644
--- a/imap-types/src/arbitrary.rs
+++ b/imap-types/src/arbitrary.rs
@@ -1,6 +1,8 @@
 use arbitrary::{Arbitrary, Unstructured};
 use chrono::{FixedOffset, TimeZone};
 
+#[cfg(feature = "ext_condstore_qresync")]
+use crate::extensions::condstore_qresync::AttributeFlag;
 use crate::{
     auth::AuthMechanism,
     body::{
@@ -67,6 +69,8 @@ impl_arbitrary_try_from! { QuotedChar, char }
 impl_arbitrary_try_from! { Mailbox<'a>, &str }
 impl_arbitrary_try_from! { Capability<'a>, Atom<'a> }
 impl_arbitrary_try_from! { Flag<'a>, &str }
+#[cfg(feature = "ext_condstore_qresync")]
+impl_arbitrary_try_from! { AttributeFlag<'a>, &str }
 impl_arbitrary_try_from! { FlagNameAttribute<'a>, Atom<'a> }
 impl_arbitrary_try_from! { MailboxOther<'a>, AString<'a> }
 impl_arbitrary_try_from! { CapabilityEnable<'a>, &str }
@@ -198,7 +202,13 @@ fn arbitrary_search_key_limited<'a>(
         return arbitrary_search_key_leaf(u);
     }
 
-    Ok(match u.int_in_range(0u8..=36)? {
+    let till = if cfg!(feature = "ext_condstore_qresync") {
+        37
+    } else {
+        36
+    };
+
+    Ok(match u.int_in_range(0u8..=till)? {
         0 => SearchKey::And({
             let keys = {
                 let len = u.arbitrary_len::<SearchKey>()?;
@@ -256,6 +266,11 @@ fn arbitrary_search_key_limited<'a>(
         34 => SearchKey::Unflagged,
         35 => SearchKey::Unkeyword(Atom::arbitrary(u)?),
         36 => SearchKey::Unseen,
+        #[cfg(feature = "ext_condstore_qresync")]
+        37 => SearchKey::ModSequence {
+            entry: Arbitrary::arbitrary(u)?,
+            modseq: Arbitrary::arbitrary(u)?,
+        },
         _ => unreachable!(),
     })
 }
diff --git a/imap-types/src/command.rs b/imap-types/src/command.rs
index b314a02b..f517789c 100644
--- a/imap-types/src/command.rs
+++ b/imap-types/src/command.rs
@@ -4,6 +4,8 @@
 
 use std::borrow::Cow;
 #[cfg(feature = "ext_condstore_qresync")]
+use std::num::NonZeroU32;
+#[cfg(feature = "ext_condstore_qresync")]
 use std::num::NonZeroU64;
 
 #[cfg(feature = "arbitrary")]
@@ -398,6 +400,8 @@ pub enum CommandBody<'a> {
     Select {
         /// Mailbox.
         mailbox: Mailbox<'a>,
+        #[cfg(feature = "ext_condstore_qresync")]
+        parameters: Vec<SelectParameter>,
     },
 
     /// Unselect a mailbox.
@@ -431,6 +435,8 @@ pub enum CommandBody<'a> {
     Examine {
         /// Mailbox.
         mailbox: Mailbox<'a>,
+        #[cfg(feature = "ext_condstore_qresync")]
+        parameters: Vec<SelectParameter>,
     },
 
     /// ### 6.3.3.  CREATE Command
@@ -1150,7 +1156,7 @@ pub enum CommandBody<'a> {
         /// Use UID variant.
         uid: bool,
         #[cfg(feature = "ext_condstore_qresync")]
-        changed_since: Option<NonZeroU64>,
+        modifiers: Vec<FetchModifier>,
     },
 
     /// ### 6.4.6.  STORE Command
@@ -1216,7 +1222,7 @@ pub enum CommandBody<'a> {
         uid: bool,
         /// --- Modifiers ---
         #[cfg(feature = "ext_condstore_qresync")]
-        unchanged_since: Option<u64>,
+        modifiers: Vec<StoreModifier>,
     },
 
     /// 6.4.7.  COPY Command
@@ -1591,6 +1597,8 @@ impl<'a> CommandBody<'a> {
     {
         Ok(CommandBody::Select {
             mailbox: mailbox.try_into()?,
+            #[cfg(feature = "ext_condstore_qresync")]
+            parameters: Vec::default(),
         })
     }
 
@@ -1601,6 +1609,8 @@ impl<'a> CommandBody<'a> {
     {
         Ok(CommandBody::Examine {
             mailbox: mailbox.try_into()?,
+            #[cfg(feature = "ext_condstore_qresync")]
+            parameters: Vec::default(),
         })
     }
 
@@ -1741,7 +1751,7 @@ impl<'a> CommandBody<'a> {
             macro_or_item_names: macro_or_item_names.into(),
             uid,
             #[cfg(feature = "ext_condstore_qresync")]
-            changed_since: None,
+            modifiers: Vec::default(),
         })
     }
 
@@ -1765,7 +1775,7 @@ impl<'a> CommandBody<'a> {
             flags,
             uid,
             #[cfg(feature = "ext_condstore_qresync")]
-            unchanged_since: None,
+            modifiers: Vec::default(),
         })
     }
 
@@ -1835,6 +1845,40 @@ impl<'a> CommandBody<'a> {
     }
 }
 
+#[cfg(feature = "ext_condstore_qresync")]
+#[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
+#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, ToStatic)]
+pub enum SelectParameter {
+    CondStore,
+    QResync {
+        uid_validity: NonZeroU32,
+        mod_sequence_value: NonZeroU64,
+        known_uids: Option<SequenceSet>, // TODO(misuse): "*" is not allowed.
+        seq_match_data: Option<(SequenceSet, SequenceSet)>, // TODO(misuse): ensure both have the same length?
+    },
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+#[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
+#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, ToStatic)]
+pub enum FetchModifier {
+    ChangedSince(NonZeroU64),
+    Vanished,
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+#[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
+#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, ToStatic)]
+pub enum StoreModifier {
+    UnchangedSince(u64),
+}
+
 /// Error-related types.
 pub mod error {
     use thiserror::Error;
@@ -2107,6 +2151,8 @@ mod tests {
             (
                 CommandBody::Select {
                     mailbox: Mailbox::Inbox,
+                    #[cfg(feature = "ext_condstore_qresync")]
+                    parameters: Vec::default(),
                 },
                 "SELECT",
             ),
@@ -2114,6 +2160,8 @@ mod tests {
             (
                 CommandBody::Examine {
                     mailbox: Mailbox::Inbox,
+                    #[cfg(feature = "ext_condstore_qresync")]
+                    parameters: Vec::default(),
                 },
                 "EXAMINE",
             ),
@@ -2207,7 +2255,7 @@ mod tests {
                     macro_or_item_names: MacroOrMessageDataItemNames::Macro(Macro::Full),
                     uid: true,
                     #[cfg(feature = "ext_condstore_qresync")]
-                    changed_since: None,
+                    modifiers: Vec::default(),
                 },
                 "FETCH",
             ),
@@ -2219,7 +2267,7 @@ mod tests {
                     kind: StoreType::Add,
                     uid: true,
                     #[cfg(feature = "ext_condstore_qresync")]
-                    unchanged_since: None,
+                    modifiers: Vec::default(),
                 },
                 "STORE",
             ),
diff --git a/imap-types/src/extensions.rs b/imap-types/src/extensions.rs
index fe64c111..83ed2433 100644
--- a/imap-types/src/extensions.rs
+++ b/imap-types/src/extensions.rs
@@ -2,6 +2,8 @@
 
 pub mod binary;
 pub mod compress;
+#[cfg(feature = "ext_condstore_qresync")]
+pub mod condstore_qresync;
 pub mod enable;
 pub mod idle;
 #[cfg(feature = "ext_metadata")]
diff --git a/imap-types/src/extensions/condstore_qresync.rs b/imap-types/src/extensions/condstore_qresync.rs
new file mode 100644
index 00000000..ccf9d679
--- /dev/null
+++ b/imap-types/src/extensions/condstore_qresync.rs
@@ -0,0 +1,88 @@
+use std::fmt::{Display, Formatter};
+
+#[cfg(feature = "arbitrary")]
+use arbitrary::Arbitrary;
+use bounded_static_derive::ToStatic;
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use crate::{core::Atom, error::ValidationError};
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, ToStatic)]
+pub enum AttributeFlag<'a> {
+    Answered,
+    Deleted,
+    Draft,
+    Flagged,
+    Seen,
+    Extension(AttributeFlagExtension<'a>),
+    Keyword(Atom<'a>),
+}
+
+impl<'a> AttributeFlag<'a> {
+    pub fn system(atom: Atom<'a>) -> Self {
+        match atom.as_ref().to_ascii_lowercase().as_ref() {
+            "answered" => Self::Answered,
+            "flagged" => Self::Flagged,
+            "deleted" => Self::Deleted,
+            "seen" => Self::Seen,
+            "draft" => Self::Draft,
+            _ => Self::Extension(AttributeFlagExtension(atom)),
+        }
+    }
+
+    pub fn keyword(atom: Atom<'a>) -> Self {
+        Self::Keyword(atom)
+    }
+}
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, ToStatic)]
+pub struct AttributeFlagExtension<'a>(Atom<'a>);
+
+impl<'a> TryFrom<&'a str> for AttributeFlag<'a> {
+    type Error = ValidationError;
+
+    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+        Ok(if let Some(value) = value.strip_prefix("\\\\") {
+            Self::system(Atom::try_from(value)?)
+        } else {
+            Self::keyword(Atom::try_from(value)?)
+        })
+    }
+}
+
+impl Display for AttributeFlag<'_> {
+    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+        match self {
+            AttributeFlag::Answered => f.write_str("\\\\Answered"),
+            AttributeFlag::Flagged => f.write_str("\\\\Flagged"),
+            AttributeFlag::Deleted => f.write_str("\\\\Deleted"),
+            AttributeFlag::Seen => f.write_str("\\\\Seen"),
+            AttributeFlag::Draft => f.write_str("\\\\Draft"),
+            AttributeFlag::Keyword(atom) => write!(f, "{}", atom),
+            AttributeFlag::Extension(other) => write!(f, "\\\\{}", other.0),
+        }
+    }
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, ToStatic)]
+pub enum EntryTypeReq {
+    Private,
+    Shared,
+    All,
+}
+
+#[cfg(feature = "ext_condstore_qresync")]
+impl Display for EntryTypeReq {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            EntryTypeReq::Private => write!(f, "priv"),
+            EntryTypeReq::Shared => write!(f, "shared"),
+            EntryTypeReq::All => write!(f, "all"),
+        }
+    }
+}
diff --git a/imap-types/src/fetch.rs b/imap-types/src/fetch.rs
index 0d19c28a..6c3cd189 100644
--- a/imap-types/src/fetch.rs
+++ b/imap-types/src/fetch.rs
@@ -379,6 +379,7 @@ pub enum MessageDataItem<'a> {
     },
 
     #[cfg(feature = "ext_condstore_qresync")]
+    #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
     ModSeq(NonZeroU64),
 }
 
diff --git a/imap-types/src/response.rs b/imap-types/src/response.rs
index c87c1858..f4ac9387 100644
--- a/imap-types/src/response.rs
+++ b/imap-types/src/response.rs
@@ -425,9 +425,20 @@ pub enum Data<'a> {
     /// search criteria.  For SEARCH, these are message sequence numbers;
     /// for UID SEARCH, these are unique identifiers.  Each number is
     /// delimited by a space.
-    Search(Vec<NonZeroU32>),
-
-    Sort(Vec<NonZeroU32>),
+    Search(
+        Vec<NonZeroU32>,
+        /// MODSEQ
+        #[cfg(feature = "ext_condstore_qresync")]
+        #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
+        Option<NonZeroU64>,
+    ),
+
+    Sort(
+        Vec<NonZeroU32>,
+        #[cfg(feature = "ext_condstore_qresync")]
+        #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
+        Option<NonZeroU64>,
+    ),
 
     Thread(Vec<Thread>),
 
@@ -572,6 +583,13 @@ pub enum Data<'a> {
         mailbox: Mailbox<'a>,
         items: MetadataResponse<'a>,
     },
+
+    #[cfg(feature = "ext_condstore_qresync")]
+    #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
+    Vanished {
+        earlier: bool,
+        known_uids: SequenceSet,
+    },
 }
 
 impl<'a> Data<'a> {
diff --git a/imap-types/src/search.rs b/imap-types/src/search.rs
index 426d4a2b..bb7dae32 100644
--- a/imap-types/src/search.rs
+++ b/imap-types/src/search.rs
@@ -4,6 +4,8 @@ use bounded_static_derive::ToStatic;
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
 
+#[cfg(feature = "ext_condstore_qresync")]
+use crate::extensions::condstore_qresync::{AttributeFlag, EntryTypeReq};
 use crate::{
     core::{AString, Atom, Vec1},
     datetime::NaiveDate,
@@ -158,6 +160,13 @@ pub enum SearchKey<'a> {
 
     /// Messages that do not have the \Seen flag set.
     Unseen,
+
+    #[cfg(feature = "ext_condstore_qresync")]
+    #[cfg_attr(docsrs, doc(cfg("ext_condstore_qresync")))]
+    ModSequence {
+        entry: Option<(AttributeFlag<'a>, EntryTypeReq)>,
+        modseq: u64,
+    },
 }
 
 impl SearchKey<'_> {


Test patch: