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

Issue:
build: decide what features to inline in 2.0.0

Implement initial iteration of Python bindings for `imap-codec`
This adds the first iteration of Python bindings for `imap-codec`.

The first iteration aims to provide a first "usable" version wich a very rough API through `dict`/`tuple` types. Switching these types to provide a more Pythonic API should be achieved lateron:
- use Python classes for codec messages (e.g. `Command`, `Greeting`) -> #559
 This would make it more clear what the methods accept and return, even if these classes itself are constructed from dictionaries.
- use Python classes for `Fragment` variants (`LineFragment`, `LiteralFragment`) -> #560
 Note: This would remove the need for `Fragment` to implement `Serialize`, so it might make sense to do this before the next release of `imap-codec`.

Also, before releasing a first Python package to PyPI, these things should probably be considered:
- name of the package -> #556 
- package metadata -> #557
- some documentation on how and why to use the package -> #558



Discussion / hints:

## Pull Request Test Coverage Report for [Build 9645169959](https://coveralls.io/builds/68258565)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **92.678%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/68258565/badge)](https://coveralls.io/builds/68258565) |
| :-- | --: |
| Change from base [Build 9634738796](https://coveralls.io/builds/68246141): |  0.0% |
| Covered Lines: | 10190 |
| Relevant Lines: | 10995 |

---
##### 💛  - [Coveralls](https://coveralls.io)

## Pull Request Test Coverage Report for [Build 9669612084](https://coveralls.io/builds/68298205)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **92.678%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/68298205/badge)](https://coveralls.io/builds/68298205) |
| :-- | --: |
| Change from base [Build 9658279125](https://coveralls.io/builds/68279078): |  0.0% |
| Covered Lines: | 10190 |
| Relevant Lines: | 10995 |

---
##### 💛  - [Coveralls](https://coveralls.io)

## Pull Request Test Coverage Report for [Build 9669683508](https://coveralls.io/builds/68298326)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **92.678%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/68298326/badge)](https://coveralls.io/builds/68298326) |
| :-- | --: |
| Change from base [Build 9658279125](https://coveralls.io/builds/68279078): |  0.0% |
| Covered Lines: | 10190 |
| Relevant Lines: | 10995 |

---
##### 💛  - [Coveralls](https://coveralls.io)

## Pull Request Test Coverage Report for [Build 9700099017](https://coveralls.io/builds/68343831)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **92.678%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/68343831/badge)](https://coveralls.io/builds/68343831) |
| :-- | --: |
| Change from base [Build 9658279125](https://coveralls.io/builds/68279078): |  0.0% |
| Covered Lines: | 10190 |
| Relevant Lines: | 10995 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Fix patch:
diff --git a/assets/demos/tokio-support/Cargo.toml b/assets/demos/tokio-support/Cargo.toml
index 1245a397..c6ab5bf8 100644
--- a/assets/demos/tokio-support/Cargo.toml
+++ b/assets/demos/tokio-support/Cargo.toml
@@ -6,6 +6,9 @@ license = "MIT OR Apache-2.0"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+[features]
+quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
+
 [dependencies]
 bytes = "1.6.1"
 thiserror = "1.0.62"
diff --git a/assets/demos/tokio-support/src/server.rs b/assets/demos/tokio-support/src/server.rs
index 34ceba09..514cf5f3 100644
--- a/assets/demos/tokio-support/src/server.rs
+++ b/assets/demos/tokio-support/src/server.rs
@@ -197,13 +197,13 @@ impl TokioEncoder<&Response<'_>> for ImapServerCodec {
 #[cfg(test)]
 mod tests {
     use bytes::BytesMut;
+    #[cfg(feature = "quirk_crlf_relaxed")]
+    use imap_codec::imap_types::core::Tag;
     use imap_codec::imap_types::{
         command::{Command, CommandBody},
         core::{AString, AtomExt, IString, Literal},
         secret::Secret,
     };
-    #[cfg(feature = "quirk_crlf_relaxed")]
-    use imap_types::core::Tag;
     use tokio_util::codec::Decoder;
 
     use super::*;
diff --git a/bindings/imap-codec-python/Cargo.toml b/bindings/imap-codec-python/Cargo.toml
index d7ef4a16..0429e679 100644
--- a/bindings/imap-codec-python/Cargo.toml
+++ b/bindings/imap-codec-python/Cargo.toml
@@ -14,6 +14,19 @@ crate-type = ["cdylib"]
 doc = false
 
 [dependencies]
-imap-codec = { version = "2.0.0-alpha.2", path = "../../imap-codec", features = ["serde"] }
 pyo3 = "0.22.1"
 serde-pyobject = "0.4.0"
+
+[dependencies.imap-codec]
+version = "2.0.0-alpha.2"
+path = "../../imap-codec"
+features = [
+    "serde",
+    "quirk_crlf_relaxed",
+    "starttls",
+    #"ext_condstore_qresync",
+    "ext_id",
+    #"ext_login_referrals",
+    #"ext_mailbox_referrals",
+    "ext_metadata",
+]
diff --git a/imap-codec/Cargo.toml b/imap-codec/Cargo.toml
index d6265b46..1905da14 100644
--- a/imap-codec/Cargo.toml
+++ b/imap-codec/Cargo.toml
@@ -13,7 +13,7 @@ exclude = [
 ]
 
 [features]
-default = ["quirk_rectify_numbers", "quirk_missing_text", "quirk_trailing_space"]
+default = ["quirk"]
 # Expose internal parsers for fuzzing
 fuzz = []
 
@@ -25,35 +25,38 @@ starttls = ["imap-types/starttls"]
 
 # IMAP Extensions
 ext_condstore_qresync = ["imap-types/ext_condstore_qresync"]
+ext_id = ["imap-types/ext_id"]
 ext_login_referrals = ["imap-types/ext_login_referrals"]
 ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
-ext_id = ["imap-types/ext_id"]
-ext_sort_thread = ["imap-types/ext_sort_thread"]
-ext_binary = ["imap-types/ext_binary"]
-ext_metadata = ["ext_binary", "imap-types/ext_metadata"]
-ext_uidplus = ["imap-types/ext_uidplus"]
+ext_metadata = ["imap-types/ext_metadata"]
 # </Forward to imap-types>
 
 # IMAP quirks
 #
 # These features bypass interoperability issues to allow safe processing of *almost* correct message.
-# 
+quirk = [
+    #"quirk_crlf_relaxed",
+    #"quirk_id_empty_to_nil",
+    "quirk_missing_text",
+    "quirk_rectify_numbers",
+    "quirk_trailing_space"
+]
+
 # Make `\r` in `\r\n` optional.
 quirk_crlf_relaxed = []
-# 
-# Rectify (invalid) numbers.
+# Encode `A ID ()` as `A ID NIL`
 # Observed in ...
-# * Dovecot (`-1`)
-quirk_rectify_numbers = []
+# * GMX
+# * Microsoft Exchange
+quirk_id_empty_to_nil = []
 # Add missing `text` by adding [" "] "<missing text>".
 # Observed in ...
 # * Gmail `* OK [HIGHESTMODSEQ <n>]\r\n`
 quirk_missing_text = []
-# Encode `A ID ()` as `A ID NIL
+# Rectify (invalid) numbers.
 # Observed in ...
-# * GMX
-# * Microsoft Exchange
-quirk_id_empty_to_nil = []
+# * Dovecot (`-1`)
+quirk_rectify_numbers = []
 # Accept a trailing space in `STATUS` data response.
 # Observed in ...
 # * Microsoft Exchange `* STATUS INBOX (MESSAGES 100 UNSEEN 0) \n\n`
@@ -63,7 +66,7 @@ quirk_trailing_space = []
 abnf-core = "0.6.0"
 base64 = { version = "0.22", default-features = false, features = ["alloc"] }
 chrono = { version = "0.4", default-features = false, features = ["alloc"] }
-imap-types = { version = "2.0.0-alpha.2", path = "../imap-types", default-features = false, features = ["unvalidated"] }
+imap-types = { version = "2.0.0-alpha.2", path = "../imap-types", default-features = false }
 nom = { version = "7", default-features = false }
 log = { version = "0.4.22", default-features = false }
 serde = { version = "1.0.203", features = ["derive"], optional = true }
diff --git a/imap-codec/fuzz/Cargo.toml b/imap-codec/fuzz/Cargo.toml
index 7cc6f563..d344bdb1 100644
--- a/imap-codec/fuzz/Cargo.toml
+++ b/imap-codec/fuzz/Cargo.toml
@@ -20,13 +20,10 @@ starttls = ["imap-codec/starttls"]
 
 # IMAP Extensions
 ext_condstore_qresync = ["imap-codec/ext_condstore_qresync"]
+ext_id = ["imap-codec/ext_id"]
 ext_login_referrals = ["imap-codec/ext_login_referrals"]
 ext_mailbox_referrals = ["imap-codec/ext_mailbox_referrals"]
-ext_id = ["imap-codec/ext_id"]
-ext_sort_thread = ["imap-codec/ext_sort_thread"]
-ext_binary = ["imap-codec/ext_binary"]
-ext_metadata = ["ext_binary", "imap-codec/ext_metadata"]
-ext_uidplus = ["imap-codec/ext_uidplus"]
+ext_metadata = ["imap-codec/ext_metadata"]
 
 # IMAP quirks
 quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
@@ -36,13 +33,10 @@ quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
 ext = [
     "starttls",
     "ext_condstore_qresync",
+    "ext_id",
     #"ext_login_referrals",
     #"ext_mailbox_referrals",
-    "ext_id",
-    "ext_sort_thread",
-    "ext_binary",
     "ext_metadata",
-    "ext_uidplus",
 ]
 # Enable `Debug`-printing during parsing. This is useful to analyze crashes.
 debug = []
diff --git a/imap-codec/src/codec/encode.rs b/imap-codec/src/codec/encode.rs
index 0d5728df..c45795ac 100644
--- a/imap-codec/src/codec/encode.rs
+++ b/imap-codec/src/codec/encode.rs
@@ -49,8 +49,6 @@ 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(any(feature = "ext_binary", feature = "ext_metadata"))]
-use imap_types::core::NString8;
 use imap_types::{
     auth::{AuthMechanism, AuthenticateData},
     body::{
@@ -59,7 +57,7 @@ use imap_types::{
     },
     command::{Command, CommandBody},
     core::{
-        AString, Atom, AtomExt, Charset, IString, Literal, LiteralMode, NString, Quoted,
+        AString, Atom, AtomExt, Charset, IString, Literal, LiteralMode, NString, NString8, Quoted,
         QuotedChar, Tag, Text,
     },
     datetime::{DateTime, NaiveDate},
@@ -427,7 +425,6 @@ impl<'a> EncodeIntoContext for CommandBody<'a> {
             CommandBody::Check => ctx.write_all(b"CHECK"),
             CommandBody::Close => ctx.write_all(b"CLOSE"),
             CommandBody::Expunge => ctx.write_all(b"EXPUNGE"),
-            #[cfg(feature = "ext_uidplus")]
             CommandBody::ExpungeUid { sequence_set } => {
                 ctx.write_all(b"UID EXPUNGE ")?;
                 sequence_set.encode_ctx(ctx)
@@ -449,7 +446,6 @@ impl<'a> EncodeIntoContext for CommandBody<'a> {
                 ctx.write_all(b" ")?;
                 join_serializable(criteria.as_ref(), b" ", ctx)
             }
-            #[cfg(feature = "ext_sort_thread")]
             CommandBody::Sort {
                 sort_criteria,
                 charset,
@@ -467,7 +463,6 @@ impl<'a> EncodeIntoContext for CommandBody<'a> {
                 ctx.write_all(b" ")?;
                 join_serializable(search_criteria.as_ref(), b" ", ctx)
             }
-            #[cfg(feature = "ext_sort_thread")]
             CommandBody::Thread {
                 algorithm,
                 charset,
@@ -1016,7 +1011,6 @@ impl<'a> EncodeIntoContext for MessageDataItemName<'a> {
             Self::Rfc822Size => ctx.write_all(b"RFC822.SIZE"),
             Self::Rfc822Text => ctx.write_all(b"RFC822.TEXT"),
             Self::Uid => ctx.write_all(b"UID"),
-            #[cfg(feature = "ext_binary")]
             MessageDataItemName::Binary {
                 section,
                 partial,
@@ -1041,7 +1035,6 @@ impl<'a> EncodeIntoContext for MessageDataItemName<'a> {
 
                 Ok(())
             }
-            #[cfg(feature = "ext_binary")]
             MessageDataItemName::BinarySize { section } => {
                 ctx.write_all(b"BINARY.SIZE")?;
 
@@ -1256,16 +1249,13 @@ impl<'a> EncodeIntoContext for Code<'a> {
                 ctx.write_all(b"METADATA ")?;
                 code.encode_ctx(ctx)
             }
-            #[cfg(feature = "ext_binary")]
             Code::UnknownCte => ctx.write_all(b"UNKNOWN-CTE"),
-            #[cfg(feature = "ext_uidplus")]
             Code::AppendUid { uid_validity, uid } => {
                 ctx.write_all(b"APPENDUID ")?;
                 uid_validity.encode_ctx(ctx)?;
                 ctx.write_all(b" ")?;
                 uid.encode_ctx(ctx)
             }
-            #[cfg(feature = "ext_uidplus")]
             Code::CopyUid {
                 uid_validity,
                 source,
@@ -1278,7 +1268,6 @@ impl<'a> EncodeIntoContext for Code<'a> {
                 ctx.write_all(b" ")?;
                 destination.encode_ctx(ctx)
             }
-            #[cfg(feature = "ext_uidplus")]
             Code::UidNotSticky => ctx.write_all(b"UIDNOTSTICKY"),
             Code::Other(unknown) => unknown.encode_ctx(ctx),
         }
@@ -1357,7 +1346,6 @@ impl<'a> EncodeIntoContext for Data<'a> {
                     join_serializable(seqs, b" ", ctx)?;
                 }
             }
-            #[cfg(feature = "ext_sort_thread")]
             Data::Sort(seqs) => {
                 if seqs.is_empty() {
                     ctx.write_all(b"* SORT")?;
@@ -1366,7 +1354,6 @@ impl<'a> EncodeIntoContext for Data<'a> {
                     join_serializable(seqs, b" ", ctx)?;
                 }
             }
-            #[cfg(feature = "ext_sort_thread")]
             Data::Thread(threads) => {
                 if threads.is_empty() {
                     ctx.write_all(b"* THREAD")?;
@@ -1569,14 +1556,12 @@ impl<'a> EncodeIntoContext for MessageDataItem<'a> {
                 nstring.encode_ctx(ctx)
             }
             Self::Uid(uid) => write!(ctx, "UID {uid}"),
-            #[cfg(feature = "ext_binary")]
             Self::Binary { section, value } => {
                 ctx.write_all(b"BINARY[")?;
                 join_serializable(section, b".", ctx)?;
                 ctx.write_all(b"] ")?;
                 value.encode_ctx(ctx)
             }
-            #[cfg(feature = "ext_binary")]
             Self::BinarySize { section, size } => {
                 ctx.write_all(b"BINARY.SIZE[")?;
                 join_serializable(section, b".", ctx)?;
@@ -1596,7 +1581,6 @@ impl<'a> EncodeIntoContext for NString<'a> {
     }
 }
 
-#[cfg(any(feature = "ext_binary", feature = "ext_metadata"))]
 impl<'a> EncodeIntoContext for NString8<'a> {
     fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
         match self {
diff --git a/imap-codec/src/command.rs b/imap-codec/src/command.rs
index 2b43f80b..0817a9da 100644
--- a/imap-codec/src/command.rs
+++ b/imap-codec/src/command.rs
@@ -5,12 +5,11 @@ 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_binary")]
-use imap_types::extensions::binary::LiteralOrLiteral8;
 use imap_types::{
     auth::AuthMechanism,
     command::{Command, CommandBody},
     core::AString,
+    extensions::binary::LiteralOrLiteral8,
     fetch::{Macro, MacroOrMessageDataItemNames},
     flag::{Flag, StoreResponse, StoreType},
     secret::Secret,
@@ -23,27 +22,25 @@ use nom::{
     sequence::{delimited, preceded, terminated, tuple},
 };
 
-#[cfg(feature = "ext_binary")]
-use crate::extensions::binary::literal8;
 #[cfg(feature = "ext_id")]
 use crate::extensions::id::id;
 #[cfg(feature = "ext_metadata")]
 use crate::extensions::metadata::{getmetadata, setmetadata};
-#[cfg(feature = "ext_uidplus")]
-use crate::extensions::uidplus::uid_expunge;
-#[cfg(feature = "ext_sort_thread")]
-use crate::extensions::{sort::sort, thread::thread};
 use crate::{
     auth::auth_type,
     core::{astring, base64, literal, tag_imap},
     datetime::date_time,
     decode::{IMAPErrorKind, IMAPResult},
     extensions::{
+        binary::literal8,
         compress::compress,
         enable::enable,
         idle::idle,
         quota::{getquota, getquotaroot, setquota},
         r#move::r#move,
+        sort::sort,
+        thread::thread,
+        uidplus::uid_expunge,
     },
     fetch::fetch_att,
     flag::{flag, flag_list},
@@ -170,9 +167,6 @@ pub(crate) fn append(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
         opt(preceded(sp, flag_list)),
         opt(preceded(sp, date_time)),
         sp,
-        #[cfg(not(feature = "ext_binary"))]
-        literal,
-        #[cfg(feature = "ext_binary")]
         alt((
             map(literal, LiteralOrLiteral8::Literal),
             map(literal8, LiteralOrLiteral8::Literal8),
@@ -418,16 +412,13 @@ pub(crate) fn command_select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
         value(CommandBody::Check, tag_no_case(b"CHECK")),
         value(CommandBody::Close, tag_no_case(b"CLOSE")),
         value(CommandBody::Expunge, tag_no_case(b"EXPUNGE")),
-        #[cfg(feature = "ext_uidplus")]
         uid_expunge,
         copy,
         fetch,
         store,
         uid,
         search,
-        #[cfg(feature = "ext_sort_thread")]
         sort,
-        #[cfg(feature = "ext_sort_thread")]
         thread,
         value(CommandBody::Unselect, tag_no_case(b"UNSELECT")),
         r#move,
diff --git a/imap-codec/src/extensions.rs b/imap-codec/src/extensions.rs
index fa544d88..2788e7b9 100644
--- a/imap-codec/src/extensions.rs
+++ b/imap-codec/src/extensions.rs
@@ -1,4 +1,3 @@
-#[cfg(feature = "ext_binary")]
 pub mod binary;
 pub mod compress;
 pub mod enable;
@@ -10,10 +9,7 @@ pub mod literal;
 pub mod metadata;
 pub mod r#move;
 pub mod quota;
-#[cfg(feature = "ext_sort_thread")]
 pub mod sort;
-#[cfg(feature = "ext_sort_thread")]
 pub mod thread;
-#[cfg(feature = "ext_uidplus")]
 pub mod uidplus;
 pub mod unselect;
diff --git a/imap-codec/src/fetch.rs b/imap-codec/src/fetch.rs
index d2d4178d..c788d21e 100644
--- a/imap-codec/src/fetch.rs
+++ b/imap-codec/src/fetch.rs
@@ -1,30 +1,25 @@
 use std::num::NonZeroU32;
 
 use abnf_core::streaming::sp;
-#[cfg(feature = "ext_binary")]
-use imap_types::core::NString8;
 use imap_types::{
-    core::{AString, Vec1},
+    core::{AString, NString8, Vec1},
     fetch::{MessageDataItem, MessageDataItemName, Part, PartSpecifier, Section},
 };
-#[cfg(feature = "ext_binary")]
-use nom::sequence::preceded;
 use nom::{
     branch::alt,
     bytes::streaming::{tag, tag_no_case},
     combinator::{map, opt, value},
     multi::separated_list1,
-    sequence::{delimited, tuple},
+    sequence::{delimited, preceded, tuple},
 };
 
-#[cfg(feature = "ext_binary")]
-use crate::extensions::binary::{literal8, partial, section_binary};
 use crate::{
     body::body,
     core::{astring, nstring, number, nz_number},
     datetime::date_time,
     decode::IMAPResult,
     envelope::envelope,
+    extensions::binary::{literal8, partial, section_binary},
     flag::flag_fetch,
 };
 
@@ -85,7 +80,6 @@ pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName>
                 peek: false,
             },
         ),
-        #[cfg(feature = "ext_binary")]
         map(
             tuple((tag_no_case("BINARY.PEEK"), section_binary, opt(partial))),
             |(_, section, partial)| MessageDataItemName::Binary {
@@ -94,7 +88,6 @@ pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName>
                 peek: true,
             },
         ),
-        #[cfg(feature = "ext_binary")]
         map(
             tuple((tag_no_case("BINARY"), section_binary, opt(partial))),
             |(_, section, partial)| MessageDataItemName::Binary {
@@ -103,7 +96,6 @@ pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName>
                 peek: false,
             },
         ),
-        #[cfg(feature = "ext_binary")]
         map(
             preceded(tag_no_case("BINARY.SIZE"), section_binary),
             |section| MessageDataItemName::BinarySize { section },
@@ -213,7 +205,6 @@ pub(crate) fn msg_att_static(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem>
         map(tuple((tag_no_case(b"UID"), sp, uniqueid)), |(_, _, uid)| {
             MessageDataItem::Uid(uid)
         }),
-        #[cfg(feature = "ext_binary")]
         map(
             tuple((
                 tag_no_case(b"BINARY"),
@@ -226,7 +217,6 @@ pub(crate) fn msg_att_static(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem>
             )),
             |(_, section, _, value)| MessageDataItem::Binary { section, value },
         ),
-        #[cfg(feature = "ext_binary")]
         map(
             tuple((tag_no_case(b"BINARY.SIZE"), section_binary, sp, number)),
             |(_, section, _, size)| MessageDataItem::BinarySize { section, size },
diff --git a/imap-codec/src/mailbox.rs b/imap-codec/src/mailbox.rs
index b66f30f6..d1b3f167 100644
--- a/imap-codec/src/mailbox.rs
+++ b/imap-codec/src/mailbox.rs
@@ -16,12 +16,13 @@ use nom::{
 
 #[cfg(feature = "ext_metadata")]
 use crate::extensions::metadata::metadata_resp;
-#[cfg(feature = "ext_sort_thread")]
-use crate::extensions::thread::thread_data;
 use crate::{
     core::{astring, nil, number, nz_number, quoted_char, string},
     decode::IMAPResult,
-    extensions::quota::{quota_response, quotaroot_response},
+    extensions::{
+        quota::{quota_response, quotaroot_response},
+        thread::thread_data,
+    },
     flag::{flag_list, mbx_list_flags},
     status::status_att_list,
 };
@@ -89,12 +90,10 @@ pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
             tuple((tag_no_case(b"SEARCH"), many0(preceded(sp, nz_number)))),
             |(_, nums)| Data::Search(nums),
         ),
-        #[cfg(feature = "ext_sort_thread")]
         map(
             preceded(tag_no_case(b"SORT"), many0(preceded(sp, nz_number))),
             Data::Sort,
         ),
-        #[cfg(feature = "ext_sort_thread")]
         thread_data,
         map(
             tuple((
diff --git a/imap-codec/src/response.rs b/imap-codec/src/response.rs
index 2f14e662..5ccff6c7 100644
--- a/imap-codec/src/response.rs
+++ b/imap-codec/src/response.rs
@@ -25,12 +25,13 @@ use nom::{
 use crate::extensions::id::id_response;
 #[cfg(feature = "ext_metadata")]
 use crate::extensions::metadata::metadata_code;
-#[cfg(feature = "ext_uidplus")]
-use crate::extensions::uidplus::{resp_code_apnd, resp_code_copy};
 use crate::{
     core::{atom, charset, nz_number, tag_imap, text},
     decode::IMAPResult,
-    extensions::enable::enable_data,
+    extensions::{
+        enable::enable_data,
+        uidplus::{resp_code_apnd, resp_code_copy},
+    },
     fetch::msg_att,
     flag::flag_perm,
     mailbox::mailbox_data,
@@ -189,13 +190,9 @@ pub(crate) fn resp_text_code(input: &[u8]) -> IMAPResult<&[u8], Code> {
             preceded(tag_no_case("METADATA "), metadata_code),
             Code::Metadata,
         ),
-        #[cfg(feature = "ext_binary")]
         value(Code::UnknownCte, tag_no_case(b"UNKNOWN-CTE")),
-        #[cfg(feature = "ext_uidplus")]
         resp_code_apnd,
-        #[cfg(feature = "ext_uidplus")]
         resp_code_copy,
-        #[cfg(feature = "ext_uidplus")]
         value(Code::UidNotSticky, tag_no_case(b"UIDNOTSTICKY")),
     ))(input)
 }
diff --git a/imap-codec/src/search.rs b/imap-codec/src/search.rs
index 9ff392b8..c1345751 100644
--- a/imap-codec/src/search.rs
+++ b/imap-codec/src/search.rs
@@ -1,15 +1,15 @@
 use abnf_core::streaming::sp;
-#[cfg(feature = "ext_sort_thread")]
-use imap_types::core::Charset;
-use imap_types::{command::CommandBody, core::Vec1, search::SearchKey};
-#[cfg(feature = "ext_sort_thread")]
-use nom::sequence::separated_pair;
+use imap_types::{
+    command::CommandBody,
+    core::{Charset, Vec1},
+    search::SearchKey,
+};
 use nom::{
     branch::alt,
     bytes::streaming::{tag, tag_no_case},
     combinator::{map, map_opt, opt, value},
     multi::separated_list1,
-    sequence::{delimited, tuple},
+    sequence::{delimited, separated_pair, tuple},
 };
 
 use crate::{
@@ -220,8 +220,6 @@ fn search_key_limited<'a>(
     ))(input)
 }
 
-// Used by both, SORT and THREAD.
-#[cfg(feature = "ext_sort_thread")]
 /// ```abnf
 /// search-criteria = charset 1*(SP search-key)
 /// ```
diff --git a/imap-types/Cargo.toml b/imap-types/Cargo.toml
index 48166d13..5f57a951 100644
--- a/imap-types/Cargo.toml
+++ b/imap-types/Cargo.toml
@@ -13,7 +13,7 @@ exclude = [
 ]
 
 [features]
-arbitrary = ["dep:arbitrary", "unvalidated", "chrono/arbitrary", "chrono/std"]
+arbitrary = ["dep:arbitrary", "chrono/arbitrary", "chrono/std"]
 arbitrary_simplified = ["arbitrary"]
 serde = ["dep:serde", "chrono/serde"]
 
@@ -22,16 +22,10 @@ starttls = []
 
 # IMAP Extensions
 ext_condstore_qresync = []
+ext_id = []
 ext_login_referrals = []
 ext_mailbox_referrals = []
-ext_id = []
-ext_sort_thread = []
-ext_binary = []
-ext_metadata = ["ext_binary"]
-ext_uidplus = []
-
-# Unlock `unvalidated` constructors.
-unvalidated = []
+ext_metadata = []
 
 [dependencies]
 arbitrary = { version = "1.3.2", optional = true, default-features = false, features = ["derive"] }
diff --git a/imap-types/README.md b/imap-types/README.md
index fbc7d2bd..d388914a 100644
--- a/imap-types/README.md
+++ b/imap-types/README.md
@@ -124,35 +124,31 @@ alice password
 Also, you can use Rust literals and resort to `unvalidated` constructors when you are certain that your input is correct:
 
 ```rust
-// Note: "unvalidated" feature must be activated.
-#[cfg(feature = "unvalidated")]
-{
-  use imap_types::{
-      command::{Command, CommandBody},
-      core::{AString, Atom, Tag},
-      secret::Secret,
-  };
-
-  // This could be provided by the email application.
-  struct TagGenerator;
-
-  impl TagGenerator {
-      fn random() -> Tag<'static> {
-          // Make this random :-)
-          Tag::unvalidated("A1")
-      }
-  }
-
-  let tag = TagGenerator::random();
-
-  let cmd = Command {
-      tag,
-      body: CommandBody::Login {
-          username: AString::from(Atom::unvalidated("alice")),
-          password: Secret::new(AString::from(Atom::unvalidated("password"))),
-      },
-  };
+use imap_types::{
+    command::{Command, CommandBody},
+    core::{AString, Atom, Tag},
+    secret::Secret,
+};
+
+// This could be provided by the email application.
+struct TagGenerator;
+
+impl TagGenerator {
+    fn random() -> Tag<'static> {
+        // Make this random :-)
+        Tag::unvalidated("A1")
+    }
 }
+
+let tag = TagGenerator::random();
+
+let cmd = Command {
+    tag,
+    body: CommandBody::Login {
+        username: AString::from(Atom::unvalidated("alice")),
+        password: Secret::new(AString::from(Atom::unvalidated("password"))),
+    },
+};
 ```
 
 In this case, imap-codec won't stand in your way.
diff --git a/imap-types/fuzz/Cargo.toml b/imap-types/fuzz/Cargo.toml
index 902902c9..97b2532f 100644
--- a/imap-types/fuzz/Cargo.toml
+++ b/imap-types/fuzz/Cargo.toml
@@ -16,33 +16,27 @@ starttls = ["imap-types/starttls"]
 
 # IMAP Extensions
 ext_condstore_qresync = ["imap-types/ext_condstore_qresync"]
+ext_id = ["imap-types/ext_id"]
 ext_login_referrals = ["imap-types/ext_login_referrals"]
 ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
-ext_id = ["imap-types/ext_id"]
-ext_sort_thread = ["imap-types/ext_sort_thread"]
-ext_binary = ["imap-types/ext_binary"]
-ext_metadata = ["ext_binary", "imap-types/ext_metadata"]
-ext_uidplus = ["imap-types/ext_uidplus"]
+ext_metadata = ["imap-types/ext_metadata"]
 # </Forward to imap-types>
 
 # Use (most) IMAP extensions.
 ext = [
     "starttls",
     "ext_condstore_qresync",
+    "ext_id",
     #"ext_login_referrals",
     #"ext_mailbox_referrals",
-    "ext_id",
-    "ext_sort_thread",
-    "ext_binary",
     "ext_metadata",
-    "ext_uidplus",
 ]
 # Enable `Debug`-printing during parsing. This is useful to analyze crashes.
 debug = []
 
 [dependencies]
 libfuzzer-sys = "0.4"
-imap-types = { path = "..", default-features = false, features = ["arbitrary", "unvalidated"] }
+imap-types = { path = "..", default-features = false, features = ["arbitrary"] }
 
 [[bin]]
 name = "into_static"
diff --git a/imap-types/src/command.rs b/imap-types/src/command.rs
index 284e3e37..f5df2443 100644
--- a/imap-types/src/command.rs
+++ b/imap-types/src/command.rs
@@ -12,18 +12,17 @@ use serde::{Deserialize, Serialize};
 
 #[cfg(feature = "ext_id")]
 use crate::core::{IString, NString};
-#[cfg(feature = "ext_binary")]
-use crate::extensions::binary::LiteralOrLiteral8;
 #[cfg(feature = "ext_metadata")]
 use crate::extensions::metadata::{Entry, EntryValue, GetMetadataOption};
-#[cfg(feature = "ext_sort_thread")]
-use crate::extensions::{sort::SortCriterion, thread::ThreadingAlgorithm};
 use crate::{
     auth::AuthMechanism,
     command::error::{AppendError, CopyError, ListError, LoginError, RenameError},
     core::{AString, Charset, Literal, Tag, Vec1},
     datetime::DateTime,
-    extensions::{compress::CompressionAlgorithm, enable::CapabilityEnable, quota::QuotaSet},
+    extensions::{
+        binary::LiteralOrLiteral8, compress::CompressionAlgorithm, enable::CapabilityEnable,
+        quota::QuotaSet, sort::SortCriterion, thread::ThreadingAlgorithm,
+    },
     fetch::MacroOrMessageDataItemNames,
     flag::{Flag, StoreResponse, StoreType},
     mailbox::{ListMailbox, Mailbox},
@@ -900,10 +899,6 @@ pub enum CommandBody<'a> {
         flags: Vec<Flag<'a>>,
         /// Datetime.
         date: Option<DateTime>,
-        #[cfg(not(feature = "ext_binary"))]
-        /// Message to append.
-        message: Literal<'a>,
-        #[cfg(feature = "ext_binary")]
         /// Message to append.
         ///
         /// <div class="warning">
@@ -1006,7 +1001,6 @@ pub enum CommandBody<'a> {
     ///
     /// Alternatively, the client may fall back to using just the EXPUNGE
     /// command, risking the unintended removal of some messages.
-    #[cfg(feature = "ext_uidplus")]
     ExpungeUid { sequence_set: SequenceSet },
 
     /// ### 6.4.4.  SEARCH Command
@@ -1070,7 +1064,6 @@ pub enum CommandBody<'a> {
         uid: bool,
     },
 
-    #[cfg(feature = "ext_sort_thread")]
     /// SORT command.
     ///
     /// The SORT command is a variant of SEARCH with sorting semantics for the results.
@@ -1097,7 +1090,6 @@ pub enum CommandBody<'a> {
         uid: bool,
     },
 
-    #[cfg(feature = "ext_sort_thread")]
     /// THREAD command.
     ///
     /// The THREAD command is a variant of SEARCH with threading semantics for the results.
@@ -1716,9 +1708,6 @@ impl<'a> CommandBody<'a> {
             mailbox: mailbox.try_into().map_err(AppendError::Mailbox)?,
             flags,
             date,
-            #[cfg(not(feature = "ext_binary"))]
-            message: message.try_into().map_err(AppendError::Data)?,
-            #[cfg(feature = "ext_binary")]
             message: LiteralOrLiteral8::Literal(message.try_into().map_err(AppendError::Data)?),
         })
     }
@@ -1797,9 +1786,7 @@ impl<'a> CommandBody<'a> {
             Self::Authenticate { .. } => "AUTHENTICATE",
             Self::Login { .. } => "LOGIN",
             Self::Select { .. } => "SELECT",
-            #[cfg(feature = "ext_sort_thread")]
             Self::Sort { .. } => "SORT",
-            #[cfg(feature = "ext_sort_thread")]
             Self::Thread { .. } => "THREAD",
             Self::Unselect => "UNSELECT",
             Self::Examine { .. } => "EXAMINE",
@@ -1815,7 +1802,6 @@ impl<'a> CommandBody<'a> {
             Self::Check => "CHECK",
             Self::Close => "CLOSE",
             Self::Expunge => "EXPUNGE",
-            #[cfg(feature = "ext_uidplus")]
             Self::ExpungeUid { .. } => "EXPUNGE",
             Self::Search { .. } => "SEARCH",
             Self::Fetch { .. } => "FETCH",
@@ -1890,9 +1876,10 @@ mod tests {
     use super::*;
     use crate::{
         auth::AuthMechanism,
-        core::{AString, Charset, IString, Literal, Vec1},
+        core::{AString, Charset, IString, Literal, LiteralMode, Vec1},
         datetime::DateTime,
         extensions::{
+            binary::Literal8,
             compress::CompressionAlgorithm,
             enable::{CapabilityEnable, Utf8Kind},
         },
@@ -1904,8 +1891,6 @@ mod tests {
         sequence::{SeqOrUid, Sequence, SequenceSet},
         status::StatusDataItemName,
     };
-    #[cfg(feature = "ext_binary")]
-    use crate::{core::LiteralMode, extensions::binary::Literal8};
 
     #[test]
     fn test_conversion_command_body() {
@@ -2178,14 +2163,10 @@ mod tests {
                     mailbox: Mailbox::Inbox,
                     flags: vec![],
                     date: None,
-                    #[cfg(not(feature = "ext_binary"))]
-                    message: Literal::try_from("").unwrap(),
-                    #[cfg(feature = "ext_binary")]
                     message: LiteralOrLiteral8::Literal(Literal::try_from("").unwrap()),
                 },
                 "APPEND",
             ),
-            #[cfg(feature = "ext_binary")]
             (
                 CommandBody::Append {
                     mailbox: Mailbox::Inbox,
diff --git a/imap-types/src/core.rs b/imap-types/src/core.rs
index a03227a5..73c12e21 100644
--- a/imap-types/src/core.rs
+++ b/imap-types/src/core.rs
@@ -67,9 +67,10 @@ macro_rules! impl_try_from {
 
 pub(crate) use impl_try_from;
 
-use crate::error::{ValidationError, ValidationErrorKind};
-#[cfg(any(feature = "ext_binary", feature = "ext_metadata"))]
-use crate::extensions::binary::Literal8;
+use crate::{
+    error::{ValidationError, ValidationErrorKind},
+    extensions::binary::Literal8,
+};
 
 /// A string subset to model IMAP's `atom`s.
 ///
@@ -145,8 +146,6 @@ impl<'a> Atom<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<C>(inner: C) -> Self
     where
         C: Into<Cow<'a, str>>,
@@ -297,8 +296,6 @@ impl<'a> AtomExt<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<C>(inner: C) -> Self
     where
         C: Into<Cow<'a, str>>,
@@ -601,8 +598,6 @@ impl<'a> Literal<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<D>(data: D) -> Self
     where
         D: Into<Cow<'a, [u8]>>,
@@ -631,8 +626,6 @@ impl<'a> Literal<'a> {
     /// <div class="warning">
     /// This extension must only be used when the server advertised support for it sending the LITERAL+ or LITERAL- capability.
     /// </div>
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated_non_sync<D>(data: D) -> Self
     where
         D: Into<Cow<'a, [u8]>>,
@@ -786,8 +779,6 @@ impl<'a> Quoted<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<C>(inner: C) -> Self
     where
         C: Into<Cow<'a, str>>,
@@ -1078,8 +1069,6 @@ impl<'a> Tag<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<C>(inner: C) -> Self
     where
         C: Into<Cow<'a, str>>,
@@ -1214,8 +1203,6 @@ impl<'a> Text<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<C>(inner: C) -> Self
     where
         C: Into<Cow<'a, str>>,
@@ -1321,8 +1308,6 @@ impl QuotedChar {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated(inner: char) -> Self {
         #[cfg(debug_assertions)]
         Self::validate(inner).unwrap();
@@ -1445,7 +1430,6 @@ impl<'a> AsRef<str> for Charset<'a> {
     }
 }
 
-#[cfg(any(feature = "ext_binary", feature = "ext_metadata"))]
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Clone, Debug, Eq, Hash, PartialEq, ToStatic)]
@@ -1502,8 +1486,6 @@ impl<T, const N: usize> VecN<T, N> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated(inner: Vec<T>) -> Self {
         #[cfg(debug_assertions)]
         Self::validate(&inner).unwrap();
diff --git a/imap-types/src/datetime.rs b/imap-types/src/datetime.rs
index d6aa7521..f4baaa05 100644
--- a/imap-types/src/datetime.rs
+++ b/imap-types/src/datetime.rs
@@ -45,8 +45,6 @@ impl DateTime {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated(value: chrono::DateTime<FixedOffset>) -> Self {
         #[cfg(debug_assertions)]
         Self::validate(&value).unwrap();
@@ -117,8 +115,6 @@ impl NaiveDate {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated(value: chrono::NaiveDate) -> Self {
         #[cfg(debug_assertions)]
         Self::validate(&value).unwrap();
diff --git a/imap-types/src/extensions.rs b/imap-types/src/extensions.rs
index f0f06d00..fe64c111 100644
--- a/imap-types/src/extensions.rs
+++ b/imap-types/src/extensions.rs
@@ -1,6 +1,5 @@
 //! IMAP extensions.
 
-#[cfg(feature = "ext_binary")]
 pub mod binary;
 pub mod compress;
 pub mod enable;
@@ -9,10 +8,7 @@ pub mod idle;
 pub mod metadata;
 pub mod r#move;
 pub mod quota;
-#[cfg(feature = "ext_sort_thread")]
 pub mod sort;
-#[cfg(feature = "ext_sort_thread")]
 pub mod thread;
-#[cfg(feature = "ext_uidplus")]
 pub mod uidplus;
 pub mod unselect;
diff --git a/imap-types/src/extensions/thread.rs b/imap-types/src/extensions/thread.rs
index c75f9aef..3f3b4295 100644
--- a/imap-types/src/extensions/thread.rs
+++ b/imap-types/src/extensions/thread.rs
@@ -99,10 +99,7 @@ impl<'a> Arbitrary<'a> for Thread {
 }
 
 #[cfg(all(feature = "arbitrary", not(feature = "arbitrary_simplified")))]
-fn arbitrary_thread_limited<'a>(
-    u: &mut Unstructured<'a>,
-    depth: usize,
-) -> arbitrary::Result<Thread> {
+fn arbitrary_thread_limited(u: &mut Unstructured, depth: usize) -> arbitrary::Result<Thread> {
     // We cheat a bit: Start from a leaf ...
     let mut current = arbitrary_thread_leaf(u)?;
 
diff --git a/imap-types/src/fetch.rs b/imap-types/src/fetch.rs
index 6cf4c22b..e12dfe9a 100644
--- a/imap-types/src/fetch.rs
+++ b/imap-types/src/fetch.rs
@@ -11,11 +11,9 @@ use bounded_static_derive::ToStatic;
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
 
-#[cfg(feature = "ext_binary")]
-use crate::core::NString8;
 use crate::{
     body::BodyStructure,
-    core::{AString, NString, Vec1},
+    core::{AString, NString, NString8, Vec1},
     datetime::DateTime,
     envelope::Envelope,
     flag::FlagFetch,
@@ -228,15 +226,15 @@ pub enum MessageDataItemName<'a> {
     /// ```
     Uid,
 
-    #[cfg(feature = "ext_binary")]
     Binary {
         section: Vec<NonZeroU32>,
         partial: Option<(u32, NonZeroU32)>,
         peek: bool,
     },
 
-    #[cfg(feature = "ext_binary")]
-    BinarySize { section: Vec<NonZeroU32> },
+    BinarySize {
+        section: Vec<NonZeroU32>,
+    },
 }
 
 /// Message data item.
@@ -364,14 +362,15 @@ pub enum MessageDataItem<'a> {
     /// ```
     Uid(NonZeroU32),
 
-    #[cfg(feature = "ext_binary")]
     Binary {
         section: Vec<NonZeroU32>,
         value: NString8<'a>,
     },
 
-    #[cfg(feature = "ext_binary")]
-    BinarySize { section: Vec<NonZeroU32>, size: u32 },
+    BinarySize {
+        section: Vec<NonZeroU32>,
+        size: u32,
+    },
 }
 
 /// A part specifier is either a part number or one of the following:
diff --git a/imap-types/src/lib.rs b/imap-types/src/lib.rs
index 6f996d1e..57a666e1 100644
--- a/imap-types/src/lib.rs
+++ b/imap-types/src/lib.rs
@@ -109,6 +109,9 @@
 //! | The IMAP ENABLE Extension ([RFC 5161])                                                                  |
 //! | IMAP4 IDLE command ([RFC 2177])                                                                         |
 //! | IMAP QUOTA Extension ([RFC 9208])                                                                       |
+//! | IMAP4 UIDPLUS extension ([RFC 2359], [RFC 4315])                                                        |
+//! | IMAP4 Binary Content Extension ([RFC 3516])                                                             |
+//! | Internet Message Access Protocol - SORT and THREAD Extensions ([RFC 5256], [RFC 5957])                  |
 //!
 //! # Features
 //!
@@ -116,15 +119,12 @@
 //!
 //! | Feature               | Description                                                                                                                  | Status     |
 //! |-----------------------|------------------------------------------------------------------------------------------------------------------------------|------------|
-//! | ext_id                | IMAP4 ID extension ([RFC 2971])                                                                                              | Unfinished |
-//! | ext_sort_thread       | Internet Message Access Protocol - SORT and THREAD Extensions ([RFC 5256], [RFC 5957])                                       | Unfinished |
+//! | starttls              | IMAP4rev1 ([RFC 3501]; section 6.2.1)                                                                                        |            |
 //! | ext_condstore_qresync | IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC) ([RFC 7162]) | Unfinished |
+//! | ext_id                | IMAP4 ID extension ([RFC 2971])                                                                                              | Unfinished |
 //! | ext_login_referrals   | IMAP4 Login Referrals ([RFC 2221])                                                                                           | Unfinished |
 //! | ext_mailbox_referrals | IMAP4 Mailbox Referrals ([RFC 2193])                                                                                         | Unfinished |
-//! | ext_binary            | IMAP4 Binary Content Extension ([RFC 3516])                                                                                  | Unfinished |
 //! | ext_metadata          | The IMAP METADATA Extension ([RFC 5464])                                                                                     | Unfinished |
-//! | ext_uidplus           | IMAP4 UIDPLUS extension ([RFC 2359], [RFC 4315])                                                                             | Unfinished |
-//! | starttls              | IMAP4rev1 ([RFC 3501]; section 6.2.1)                                                                                        |            |
 //!
 //! STARTTLS is not an IMAP extension but feature-gated because it [should be avoided](https://nostarttls.secvuln.info/).
 //! For better performance and security, use "implicit TLS", i.e., IMAP-over-TLS on port 993, and don't use STARTTLS at all.
@@ -135,7 +135,6 @@
 //! |------------------|---------------------------------------------------------------|--------------------|
 //! | arbitrary        | Derive `Arbitrary` implementations                            | No                 |
 //! | serde            | Derive `serde`s `Serialize` and `Deserialize` implementations | No                 |
-//! | unvalidated      | Unlock `unvalidated` constructors                             | No                 |
 //!
 //! When using `arbitrary`, all types defined in imap-types implement the [Arbitrary] trait to ease testing.
 //! This is used, for example, to generate instances during fuzz-testing.
diff --git a/imap-types/src/mailbox.rs b/imap-types/src/mailbox.rs
index 6bd49252..dd0148d6 100644
--- a/imap-types/src/mailbox.rs
+++ b/imap-types/src/mailbox.rs
@@ -47,8 +47,6 @@ impl<'a> ListCharString<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<C>(inner: C) -> Self
     where
         C: Into<Cow<'a, str>>,
@@ -222,8 +220,6 @@ impl<'a> MailboxOther<'a> {
     /// Do not call this constructor with untrusted data.
     ///
     /// Note: This method will `panic!` on wrong input in debug builds.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated(value: AString<'a>) -> Self {
         #[cfg(debug_assertions)]
         Self::validate(&value).unwrap();
diff --git a/imap-types/src/response.rs b/imap-types/src/response.rs
index fa6e9642..d34dcbab 100644
--- a/imap-types/src/response.rs
+++ b/imap-types/src/response.rs
@@ -17,12 +17,6 @@ use serde::{Deserialize, Serialize};
 use crate::core::{IString, NString};
 #[cfg(feature = "ext_metadata")]
 use crate::extensions::metadata::{MetadataCode, MetadataResponse};
-#[cfg(feature = "ext_sort_thread")]
-use crate::extensions::sort::SortAlgorithm;
-#[cfg(feature = "ext_sort_thread")]
-use crate::extensions::thread::{Thread, ThreadingAlgorithm};
-#[cfg(feature = "ext_uidplus")]
-use crate::extensions::uidplus::UidSet;
 use crate::{
     auth::AuthMechanism,
     core::{impl_try_from, AString, Atom, Charset, QuotedChar, Tag, Text, Vec1},
@@ -31,6 +25,9 @@ use crate::{
         compress::CompressionAlgorithm,
         enable::CapabilityEnable,
         quota::{QuotaGet, Resource},
+        sort::SortAlgorithm,
+        thread::{Thread, ThreadingAlgorithm},
+        uidplus::UidSet,
     },
     fetch::MessageDataItem,
     flag::{Flag, FlagNameAttribute, FlagPerm},
@@ -426,10 +423,8 @@ pub enum Data<'a> {
     /// delimited by a space.
     Search(Vec<NonZeroU32>),
 
-    #[cfg(feature = "ext_sort_thread")]
     Sort(Vec<NonZeroU32>),
 
-    #[cfg(feature = "ext_sort_thread")]
     Thread(Vec<Thread>),
 
     /// ### 7.2.6.  FLAGS Response
@@ -858,11 +853,9 @@ pub enum Code<'a> {
     /// Metadata
     Metadata(MetadataCode),
 
-    #[cfg(feature = "ext_binary")]
     /// Server does not know how to decode the section's CTE.
     UnknownCte,
 
-    #[cfg(feature = "ext_uidplus")]
     /// Message has been appended to destination mailbox with that UID
     AppendUid {
         /// UIDVALIDITY of destination mailbox
@@ -871,7 +864,6 @@ pub enum Code<'a> {
         uid: NonZeroU32,
     },
 
-    #[cfg(feature = "ext_uidplus")]
     /// Message(s) have been copied to destination mailbox with stated UID(s)
     CopyUid {
         /// UIDVALIDITY of destination mailbox
@@ -882,7 +874,6 @@ pub enum Code<'a> {
         destination: UidSet,
     },
 
-    #[cfg(feature = "ext_uidplus")]
     UidNotSticky,
 
     /// Additional response codes defined by particular client or server
@@ -964,8 +955,6 @@ impl<'a> CodeOther<'a> {
     /// The caller must ensure that `data` is valid. Failing to do so may create invalid/unparsable
     /// IMAP messages, or even produce unintended protocol flows. Do not call this constructor with
     /// untrusted data.
-    #[cfg(feature = "unvalidated")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
     pub fn unvalidated<D>(data: D) -> Self
     where
         D: Into<Cow<'a, [u8]>>,
@@ -1020,9 +1009,7 @@ pub enum Capability<'a> {
     Id,
     /// See RFC 3691.
     Unselect,
-    #[cfg(feature = "ext_sort_thread")]
     Sort(Option<SortAlgorithm<'a>>),
-    #[cfg(feature = "ext_sort_thread")]
     Thread(ThreadingAlgorithm<'a>),
     #[cfg(feature = "ext_metadata")]
     /// Server supports (both) server annotations and mailbox annotations.
@@ -1030,10 +1017,8 @@ pub enum Capability<'a> {
     #[cfg(feature = "ext_metadata")]
     /// Server supports (only) server annotations.
     MetadataServer,
-    #[cfg(feature = "ext_binary")]
     /// IMAP4 Binary Content Extension
     Binary,
-    #[cfg(feature = "ext_uidplus")]
     /// UIDPLUS extension (RFC 4351)
     UidPlus,
     /// Other/Unknown
@@ -1065,19 +1050,14 @@ impl<'a> Display for Capability<'a> {
             #[cfg(feature = "ext_id")]
             Self::Id => write!(f, "ID"),
             Self::Unselect => write!(f, "UNSELECT"),
-            #[cfg(feature = "ext_sort_thread")]
             Self::Sort(None) => write!(f, "SORT"),
-            #[cfg(feature = "ext_sort_thread")]
             Self::Sort(Some(algorithm)) => write!(f, "SORT={}", algorithm),
-            #[cfg(feature = "ext_sort_thread")]
             Self::Thread(algorithm) => write!(f, "THREAD={}", algorithm),
             #[cfg(feature = "ext_metadata")]
             Self::Metadata => write!(f, "METADATA"),
             #[cfg(feature = "ext_metadata")]
             Self::MetadataServer => write!(f, "METADATA-SERVER"),
-            #[cfg(feature = "ext_binary")]
             Self::Binary => write!(f, "BINARY"),
-            #[cfg(feature = "ext_uidplus")]
             Self::UidPlus => write!(f, "UIDPLUS"),
             Self::Other(other) => write!(f, "{}", other.0),
         }
@@ -1135,16 +1115,13 @@ impl<'a> From<Atom<'a>> for Capability<'a> {
             "move" => Self::Move,
             #[cfg(feature = "ext_id")]
             "id" => Self::Id,
-            #[cfg(feature = "ext_sort_thread")]
             "sort" => Self::Sort(None),
             #[cfg(feature = "ext_metadata")]
             "metadata" => Self::Metadata,
             #[cfg(feature = "ext_metadata")]
             "metadata-server" => Self::MetadataServer,
-            #[cfg(feature = "ext_binary")]
             "binary" => Self::Binary,
             "unselect" => Self::Unselect,
-            #[cfg(feature = "ext_uidplus")]
             "uidplus" => Self::UidPlus,
             _ => {
                 // TODO(efficiency)
@@ -1172,13 +1149,11 @@ impl<'a> From<Atom<'a>> for Capability<'a> {
                                 }
                             }
                         }
-                        #[cfg(feature = "ext_sort_thread")]
                         "sort" => {
                             if let Ok(atom) = Atom::try_from(right) {
                                 return Self::Sort(Some(SortAlgorithm::from(atom)));
                             }
                         }
-                        #[cfg(feature = "ext_sort_thread")]
                         "thread" => {
                             if let Ok(atom) = Atom::try_from(right) {
                                 return Self::Thread(ThreadingAlgorithm::from(atom));
diff --git a/justfile b/justfile
index db3ea604..d125af8f 100644
--- a/justfile
+++ b/justfile
@@ -50,8 +50,6 @@ cargo_hack mode: install_cargo_hack
         ext_login_referrals,\
         ext_mailbox_referrals,\
         ext_id,\
-        ext_sort_thread,\
-        ext_binary,\
         ext_metadata \
         --group-features \
         quirk_crlf_relaxed,\
@@ -73,8 +71,6 @@ cargo_hack mode: install_cargo_hack
         ext_login_referrals,\
         ext_mailbox_referrals,\
         ext_id,\
-        ext_sort_thread,\
-        ext_binary,\
         ext_metadata\
         {{ mode }}
 	


Test patch:
diff --git a/imap-codec/tests/trace.rs b/imap-codec/tests/trace.rs
index 3bef1d98..4152e4d2 100644
--- a/imap-codec/tests/trace.rs
+++ b/imap-codec/tests/trace.rs
@@ -1402,7 +1402,6 @@ fn test_trace_rfc2088() {
     })
 }
 
-#[cfg(feature = "ext_sort_thread")]
 #[test]
 fn test_trace_sort() {
     let trace = br#"C: A282 SORT (SUBJECT) UTF-8 SINCE 1-Feb-1994
@@ -1419,7 +1418,6 @@ S: A284 OK SORT completed
     test_lines_of_trace(trace);
 }
 
-#[cfg(feature = "ext_sort_thread")]
 #[test]
 fn test_trace_thread() {
     let trace = br#"C: A283 THREAD ORDEREDSUBJECT UTF-8 SINCE 5-MAR-2000
diff --git a/imap-types/tests/api.rs b/imap-types/tests/api.rs
index 280f270b..93656b28 100644
--- a/imap-types/tests/api.rs
+++ b/imap-types/tests/api.rs
@@ -11,11 +11,8 @@ use imap_types::{
 macro_rules! test_conversions {
     // Unvalidated
     (y, $try_from:tt, $from:tt, $as_ref:tt, $object:ty, $sample:expr) => {{
-        #[cfg(feature = "unvalidated")]
-        {
-            let object = <$object>::unvalidated($sample);
-            let _ = object.as_ref();
-        }
+        let object = <$object>::unvalidated($sample);
+        let _ = object.as_ref();
 
         test_conversions!($try_from, $from, $as_ref, $object, $sample);
     }};
diff --git a/imap-types/tests/readme.rs b/imap-types/tests/readme.rs
index 1bf3b9d9..7847cf62 100644
--- a/imap-types/tests/readme.rs
+++ b/imap-types/tests/readme.rs
@@ -1,10 +1,6 @@
 use imap_types::{
     command::{Command, CommandBody},
-    core::Literal,
-};
-#[cfg(feature = "unvalidated")]
-use imap_types::{
-    core::{AString, Atom, Tag},
+    core::{AString, Atom, Literal, Tag},
     secret::Secret,
 };
 
@@ -22,18 +18,15 @@ fn test_readme() {
     )
     .unwrap();
 
-    #[cfg(feature = "unvalidated")]
-    {
-        let tag = Tag::try_from("A1").unwrap();
+    let tag = Tag::try_from("A1").unwrap();
 
-        let _ = Command {
-            tag,
-            body: CommandBody::Login {
-                username: AString::from(Atom::unvalidated("alice")),
-                password: Secret::new(AString::from(Atom::unvalidated("password"))),
-            },
-        };
-    }
+    let _ = Command {
+        tag,
+        body: CommandBody::Login {
+            username: AString::from(Atom::unvalidated("alice")),
+            password: Secret::new(AString::from(Atom::unvalidated("password"))),
+        },
+    };
 }
 
 #[test]