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

Issue:
Resolve test-related `TODO`s.
* [ ] Resolve `TODO`s in fuzz-targets, i.e., do not skip variants.
  * [ ] `response_to_bytes_and_back` remaining
    * [ ] `BodyStructure`
    * [ ] `Continue`
* [x] Resolve `TODO`s in `Arbitrary` implementations, i.e., do not generate static values.


Discussion / hints:

Fix patch:
diff --git a/fuzz/README.md b/fuzz/README.md
index a72fd3f4..92becdf0 100644
--- a/fuzz/README.md
+++ b/fuzz/README.md
@@ -38,14 +38,14 @@ cargo +nightly fuzz run <target>
 
 ... with `<target>` being ...
 
-| Name of the fuzz target `<target>` | Purpose                | Expectation                                |
-|------------------------------------|------------------------|--------------------------------------------|
-| `greeting`                         | Test parsing           | Must not fail.                             |
-| `command`                          | Test parsing           | Must not fail.                             |
-| `response`                         | Test parsing           | Must not fail.                             |
-| `greeting_to_bytes_and_back`       | Test misuse-resistance | Must not fail.                             |
-| `command_to_bytes_and_back`        | Test misuse-resistance | Must not fail.                             |
-| `response_to_bytes_and_back`       | Test misuse-resistance | Must not fail (but has remaining `TODO`s). |
+| Name of the fuzz target `<target>` | Purpose                | Expectation    |
+|------------------------------------|------------------------|----------------|
+| `greeting`                         | Test parsing           | Must not fail. |
+| `command`                          | Test parsing           | Must not fail. |
+| `response`                         | Test parsing           | Must not fail. |
+| `greeting_to_bytes_and_back`       | Test misuse-resistance | Must not fail. |
+| `command_to_bytes_and_back`        | Test misuse-resistance | Must not fail. |
+| `response_to_bytes_and_back`       | Test misuse-resistance | Must not fail. |
 
 Three first three fuzz targets are used to test the parsing routines.
 The fuzzers all do the same: try to parse the input from libFuzzer (and hope that the parsers don't crash), then,
@@ -74,7 +74,7 @@ cargo +nightly fuzz run <target> -j 32 -- -dict=terminals.dict -max_len=64 -only
 
 ## Structured fuzzing with `Arbitrary`
 
-This beautiful `Command`¹ ... 
+This beautiful `Command`¹ ...
 
 ```rust
 Command {
diff --git a/fuzz/fuzz_targets/response_to_bytes_and_back.rs b/fuzz/fuzz_targets/response_to_bytes_and_back.rs
index ec6c0b84..fa16ad38 100644
--- a/fuzz/fuzz_targets/response_to_bytes_and_back.rs
+++ b/fuzz/fuzz_targets/response_to_bytes_and_back.rs
@@ -1,44 +1,16 @@
 #![no_main]
 
-use base64::{engine::general_purpose::STANDARD as _base64, Engine};
 #[cfg(feature = "debug")]
 use imap_codec::utils::escape_byte_string;
 use imap_codec::{
     codec::{Decode, Encode},
-    response::{data::FetchAttributeValue, Continue, Data, Response},
+    response::Response,
 };
 use libfuzzer_sys::fuzz_target;
 
 fuzz_target!(|test: Response| {
-    // TODO(#30): Skip certain generations for now as we know they need to be fixed.
-    //            The goal is to not skip anything eventually.
-    match test {
-        Response::Data(Data::Fetch { ref attributes, .. }) => {
-            for attribute in attributes.as_ref().iter() {
-                match attribute {
-                    FetchAttributeValue::Body(_) | FetchAttributeValue::BodyStructure(_) => {
-                        // FIXME(#30): Body(Structure)
-                        return;
-                    }
-                    _ => {}
-                }
-            }
-        }
-        Response::Continue(Continue::Basic {
-            code: None,
-            ref text,
-        }) => {
-            // Oh, IMAP :-/
-            if _base64.decode(text.inner()).is_ok() {
-                // FIXME(#30)
-                return;
-            }
-        }
-        _ => {}
-    }
-
     #[cfg(feature = "debug")]
-    println!("[!] Input: {test:?}");
+    println!("[!] Input: {test:#?}");
 
     let buffer = test.encode_detached().unwrap();
 
diff --git a/imap-types/src/arbitrary.rs b/imap-types/src/arbitrary.rs
index 9270784d..40a4be91 100644
--- a/imap-types/src/arbitrary.rs
+++ b/imap-types/src/arbitrary.rs
@@ -9,11 +9,15 @@ use crate::extensions::enable::CapabilityEnableOther;
 use crate::extensions::quota::ResourceOther;
 use crate::{
     command::{search::SearchKey, ListCharString, SequenceSet},
-    core::{AString, Atom, AtomExt, Literal, NonEmptyVec, Quoted},
+    core::{AString, Atom, AtomExt, IString, Literal, NString, NonEmptyVec, Quoted},
+    imap4rev1::body::{BasicFields, SinglePartExtensionData},
     message::{AuthMechanismOther, DateTime, FlagExtension, Mailbox, MailboxOther, NaiveDate, Tag},
     response::{
-        data::{Capability, QuotedChar},
-        CodeOther, Text,
+        data::{
+            Body, BodyExtension, BodyStructure, Capability, Envelope, MultiPartExtensionData,
+            QuotedChar, SpecificFields,
+        },
+        Code, CodeOther, ContinueBasic, Text,
     },
 };
 
@@ -65,6 +69,13 @@ implement_tryfrom! { AuthMechanismOther<'a>, Atom<'a> }
 implement_tryfrom! { SequenceSet, &str }
 implement_tryfrom_t! { NonEmptyVec<T>, Vec<T> }
 
+impl<'a> Arbitrary<'a> for ContinueBasic<'a> {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        ContinueBasic::new(Option::<Code>::arbitrary(u)?, Text::arbitrary(u)?)
+            .map_err(|_| arbitrary::Error::IncorrectFormat)
+    }
+}
+
 impl<'a> Arbitrary<'a> for Literal<'a> {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         match Literal::try_from(<&[u8]>::arbitrary(u)?) {
@@ -203,6 +214,196 @@ impl<'a> Arbitrary<'a> for SearchKey<'a> {
     }
 }
 
+impl<'a> Arbitrary<'a> for BodyStructure<'a> {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        fn make_body_structure_terminator<'a>(
+            u: &mut Unstructured<'a>,
+        ) -> arbitrary::Result<BodyStructure<'a>> {
+            Ok(BodyStructure::Single {
+                body: Body {
+                    basic: BasicFields::arbitrary(u)?,
+                    specific: match u.int_in_range(1..=2)? {
+                        1 => SpecificFields::Basic {
+                            type_: IString::arbitrary(u)?,
+                            subtype: IString::arbitrary(u)?,
+                        },
+                        // No SpecificFields::Message because it would recurse.
+                        2 => SpecificFields::Text {
+                            subtype: IString::arbitrary(u)?,
+                            number_of_lines: u32::arbitrary(u)?,
+                        },
+                        _ => unreachable!(),
+                    },
+                },
+                extension_data: Option::<SinglePartExtensionData>::arbitrary(u)?,
+            })
+        }
+
+        fn make_body_structure_rec<'a>(
+            u: &mut Unstructured<'a>,
+            depth: u8,
+        ) -> arbitrary::Result<BodyStructure<'a>> {
+            if depth == 0 {
+                return make_body_structure_terminator(u);
+            }
+
+            Ok(match u.int_in_range(1..=2)? {
+                1 => BodyStructure::Single {
+                    body: Body {
+                        basic: BasicFields::arbitrary(u)?,
+                        specific: match u.int_in_range(1..=3)? {
+                            1 => SpecificFields::Basic {
+                                type_: IString::arbitrary(u)?,
+                                subtype: IString::arbitrary(u)?,
+                            },
+                            2 => SpecificFields::Message {
+                                envelope: Box::<Envelope>::arbitrary(u)?,
+                                body_structure: Box::new(make_body_structure_rec(u, depth - 1)?),
+                                number_of_lines: u32::arbitrary(u)?,
+                            },
+                            3 => SpecificFields::Text {
+                                subtype: IString::arbitrary(u)?,
+                                number_of_lines: u32::arbitrary(u)?,
+                            },
+                            _ => unreachable!(),
+                        },
+                    },
+                    extension_data: Option::<SinglePartExtensionData>::arbitrary(u)?,
+                },
+                2 => BodyStructure::Multi {
+                    bodies: {
+                        let bodies = {
+                            let len = u.arbitrary_len::<BodyStructure>()?;
+                            let mut tmp = Vec::with_capacity(len);
+
+                            for _ in 0..len {
+                                tmp.push(make_body_structure_rec(u, depth - 1)?);
+                            }
+
+                            tmp
+                        };
+
+                        if !bodies.is_empty() {
+                            NonEmptyVec::try_from(bodies).unwrap()
+                        } else {
+                            NonEmptyVec::from(make_body_structure_terminator(u)?)
+                        }
+                    },
+                    subtype: IString::arbitrary(u)?,
+                    extension_data: Option::<MultiPartExtensionData>::arbitrary(u)?,
+                },
+                _ => unreachable!(),
+            })
+        }
+
+        make_body_structure_rec(u, 3)
+    }
+}
+
+impl<'a> Arbitrary<'a> for SinglePartExtensionData<'a> {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        let disposition = Option::<Option<(IString, Vec<(IString, IString)>)>>::arbitrary(u)?;
+        let mut language = None;
+        let mut location = None;
+        let mut extensions = vec![];
+
+        if disposition.is_some() {
+            language = Option::<Vec<IString>>::arbitrary(u)?;
+            if language.is_some() {
+                location = Option::<NString>::arbitrary(u)?;
+                if location.is_some() {
+                    extensions = Vec::<BodyExtension>::arbitrary(u)?;
+                }
+            }
+        }
+
+        Ok(SinglePartExtensionData {
+            md5: NString::arbitrary(u)?,
+            disposition,
+            language,
+            location,
+            extensions,
+        })
+    }
+}
+
+impl<'a> Arbitrary<'a> for MultiPartExtensionData<'a> {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        let disposition = Option::<Option<(IString, Vec<(IString, IString)>)>>::arbitrary(u)?;
+        let mut language = None;
+        let mut location = None;
+        let mut extensions = vec![];
+
+        if disposition.is_some() {
+            language = Option::<Vec<IString>>::arbitrary(u)?;
+            if language.is_some() {
+                location = Option::<NString>::arbitrary(u)?;
+                if location.is_some() {
+                    extensions = Vec::<BodyExtension>::arbitrary(u)?;
+                }
+            }
+        }
+
+        Ok(MultiPartExtensionData {
+            parameter_list: Vec::<(IString, IString)>::arbitrary(u)?,
+            disposition,
+            language,
+            location,
+            extensions,
+        })
+    }
+}
+
+impl<'a> Arbitrary<'a> for BodyExtension<'a> {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        fn make_body_extension_terminator<'a>(
+            u: &mut Unstructured<'a>,
+        ) -> arbitrary::Result<BodyExtension<'a>> {
+            Ok(match u.int_in_range(1..=2)? {
+                1 => BodyExtension::NString(NString::arbitrary(u)?),
+                2 => BodyExtension::Number(u32::arbitrary(u)?),
+                // No `BodyExtension::List` because it could recurse.
+                _ => unreachable!(),
+            })
+        }
+
+        fn make_body_extension_rec<'a>(
+            u: &mut Unstructured<'a>,
+            depth: u8,
+        ) -> arbitrary::Result<BodyExtension<'a>> {
+            if depth == 0 {
+                return make_body_extension_terminator(u);
+            }
+
+            Ok(match u.int_in_range(1..=2)? {
+                1 => BodyExtension::NString(NString::arbitrary(u)?),
+                2 => BodyExtension::Number(u32::arbitrary(u)?),
+                3 => BodyExtension::List({
+                    let body_extensions = {
+                        let len = u.arbitrary_len::<BodyExtension>()?;
+                        let mut tmp = Vec::with_capacity(len);
+
+                        for _ in 0..len {
+                            tmp.push(make_body_extension_rec(u, depth - 1)?);
+                        }
+
+                        tmp
+                    };
+
+                    if !body_extensions.is_empty() {
+                        NonEmptyVec::try_from(body_extensions).unwrap()
+                    } else {
+                        NonEmptyVec::from(make_body_extension_terminator(u)?)
+                    }
+                }),
+                _ => unreachable!(),
+            })
+        }
+
+        make_body_extension_rec(u, 3)
+    }
+}
+
 impl<'a> Arbitrary<'a> for DateTime {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         // Note: `chrono`s `NaiveDate::arbitrary` may `panic!`.
diff --git a/imap-types/src/codec.rs b/imap-types/src/codec.rs
index 9fd1a110..3b76bfba 100644
--- a/imap-types/src/codec.rs
+++ b/imap-types/src/codec.rs
@@ -41,9 +41,9 @@ use crate::{
     },
     response::{
         data::{
-            Address, BasicFields, Body, BodyStructure, Capability, Envelope, FetchAttributeValue,
-            MultiPartExtensionData, QuotedChar, SinglePartExtensionData, SpecificFields,
-            StatusAttributeValue,
+            Address, BasicFields, Body, BodyExtension, BodyStructure, Capability, Envelope,
+            FetchAttributeValue, MultiPartExtensionData, QuotedChar, SinglePartExtensionData,
+            SpecificFields, StatusAttributeValue,
         },
         Code, CodeOther, Continue, Data, Greeting, GreetingKind, Response, Status, Text,
     },
@@ -1217,7 +1217,10 @@ impl<'a> Encode for BodyStructure<'a> {
     fn encode(&self, writer: &mut impl Write) -> std::io::Result<()> {
         writer.write_all(b"(")?;
         match self {
-            BodyStructure::Single { body, extension } => {
+            BodyStructure::Single {
+                body,
+                extension_data: extension,
+            } => {
                 body.encode(writer)?;
                 if let Some(extension) = extension {
                     writer.write_all(b" ")?;
@@ -1368,10 +1371,9 @@ impl<'a> Encode for SinglePartExtensionData<'a> {
                     writer.write_all(b" ")?;
                     loc.encode(writer)?;
 
-                    if !self.extension.is_empty() {
-                        // FIXME: Extension includes the SP for now, as it is unparsed.
-                        //writer.write_all(b" ")?;
-                        writer.write_all(&self.extension)?;
+                    for body_extension in &self.extensions {
+                        writer.write_all(b" ")?;
+                        body_extension.encode(writer)?;
                     }
                 }
             }
@@ -1407,10 +1409,9 @@ impl<'a> Encode for MultiPartExtensionData<'a> {
                     writer.write_all(b" ")?;
                     loc.encode(writer)?;
 
-                    if !self.extension.is_empty() {
-                        // FIXME: Extension includes the SP for now, as it is unparsed.
-                        //writer.write_all(b" ");
-                        writer.write_all(&self.extension)?;
+                    for body_extension in &self.extensions {
+                        writer.write_all(b" ")?;
+                        body_extension.encode(writer)?;
                     }
                 }
             }
@@ -1420,6 +1421,20 @@ impl<'a> Encode for MultiPartExtensionData<'a> {
     }
 }
 
+impl<'a> Encode for BodyExtension<'a> {
+    fn encode(&self, writer: &mut impl Write) -> std::io::Result<()> {
+        match self {
+            BodyExtension::NString(nstring) => nstring.encode(writer),
+            BodyExtension::Number(number) => number.encode(writer),
+            BodyExtension::List(list) => {
+                writer.write_all(b"(")?;
+                join_serializable(list.as_ref(), b" ", writer)?;
+                writer.write_all(b")")
+            }
+        }
+    }
+}
+
 impl Encode for ChronoDateTime<FixedOffset> {
     fn encode(&self, writer: &mut impl Write) -> std::io::Result<()> {
         write!(writer, "\"{}\"", self.format("%d-%b-%Y %H:%M:%S %z"))
@@ -1429,17 +1444,17 @@ impl Encode for ChronoDateTime<FixedOffset> {
 impl<'a> Encode for Continue<'a> {
     fn encode(&self, writer: &mut impl Write) -> std::io::Result<()> {
         match self {
-            Continue::Basic { code, text } => match code {
+            Continue::Basic(continue_basic) => match continue_basic.code() {
                 Some(ref code) => {
                     writer.write_all(b"+ [")?;
                     code.encode(writer)?;
                     writer.write_all(b"] ")?;
-                    text.encode(writer)?;
+                    continue_basic.text().encode(writer)?;
                     writer.write_all(b"\r\n")
                 }
                 None => {
                     writer.write_all(b"+ ")?;
-                    text.encode(writer)?;
+                    continue_basic.text().encode(writer)?;
                     writer.write_all(b"\r\n")
                 }
             },
diff --git a/imap-types/src/imap4rev1/body.rs b/imap-types/src/imap4rev1/body.rs
index f1fbe356..e3590b6e 100644
--- a/imap-types/src/imap4rev1/body.rs
+++ b/imap-types/src/imap4rev1/body.rs
@@ -1,5 +1,3 @@
-use std::borrow::Cow;
-
 #[cfg(feature = "arbitrary")]
 use arbitrary::Arbitrary;
 #[cfg(feature = "bounded-static")]
@@ -167,7 +165,6 @@ pub enum SpecificFields<'a> {
     },
 }
 
-#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -189,7 +186,7 @@ pub enum BodyStructure<'a> {
         /// Any following extension data are not yet defined in this
         /// version of the protocol, and would be as described above under
         /// multipart extension data.
-        extension: Option<SinglePartExtensionData<'a>>,
+        extension_data: Option<SinglePartExtensionData<'a>>,
     },
 
     /// Multiple parts are indicated by parenthesis nesting.  Instead
@@ -247,11 +244,9 @@ pub enum BodyStructure<'a> {
 }
 
 /// The extension data of a non-multipart body part are in the following order:
-#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-// TODO(misuse)
 pub struct SinglePartExtensionData<'a> {
     /// A string giving the body MD5 value as defined in [MD5].
     pub md5: NString<'a>,
@@ -268,7 +263,7 @@ pub struct SinglePartExtensionData<'a> {
     /// A string list giving the body content URI as defined in [LOCATION].
     pub location: Option<NString<'a>>,
 
-    pub extension: Cow<'a, [u8]>,
+    pub extensions: Vec<BodyExtension<'a>>,
 }
 
 /// The extension data of a multipart body part are in the following order:
@@ -285,7 +280,6 @@ pub struct SinglePartExtensionData<'a> {
 ///           | extension multipart data
 /// )
 /// ```
-#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -317,5 +311,40 @@ pub struct MultiPartExtensionData<'a> {
     /// [LOCATION].
     pub location: Option<NString<'a>>,
 
-    pub extension: Cow<'a, [u8]>,
+    pub extensions: Vec<BodyExtension<'a>>,
+}
+
+#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum BodyExtension<'a> {
+    NString(NString<'a>),
+    Number(u32),
+    List(NonEmptyVec<BodyExtension<'a>>),
+}
+
+#[cfg(test)]
+mod tests {
+    use std::convert::TryFrom;
+
+    use super::*;
+    use crate::{core::Quoted, testing::known_answer_test_encode};
+
+    #[test]
+    fn test_encode_single_part_extension_data() {
+        let tests = [(
+            SinglePartExtensionData {
+                md5: NString(None),
+                disposition: Some(None),
+                language: Some(vec![]),
+                location: Some(NString::from(Quoted::try_from("").unwrap())),
+                extensions: vec![],
+            },
+            b"NIL NIL NIL \"\"".as_ref(),
+        )];
+
+        for test in tests {
+            known_answer_test_encode(test);
+        }
+    }
 }
diff --git a/imap-types/src/imap4rev1/command/search.rs b/imap-types/src/imap4rev1/command/search.rs
index 51803267..63ab5c07 100644
--- a/imap-types/src/imap4rev1/command/search.rs
+++ b/imap-types/src/imap4rev1/command/search.rs
@@ -85,7 +85,7 @@ pub enum SearchKey<'a> {
     New,
 
     /// Messages that do not match the specified search key.
-    Not(Box<SearchKey<'a>>), // TODO(misuse): is this a Vec or a single SearchKey?
+    Not(Box<SearchKey<'a>>),
 
     /// Messages that do not have the \Recent flag set.  This is
     /// functionally equivalent to "NOT RECENT" (as opposed to "NOT
@@ -97,7 +97,7 @@ pub enum SearchKey<'a> {
     On(NaiveDate),
 
     /// Messages that match either search key.
-    Or(Box<SearchKey<'a>>, Box<SearchKey<'a>>), /* TODO(misuse): is this a Vec or a single SearchKey? */
+    Or(Box<SearchKey<'a>>, Box<SearchKey<'a>>),
 
     /// Messages that have the \Recent flag set.
     Recent,
diff --git a/imap-types/src/imap4rev1/core.rs b/imap-types/src/imap4rev1/core.rs
index 84000c1d..9c96f301 100644
--- a/imap-types/src/imap4rev1/core.rs
+++ b/imap-types/src/imap4rev1/core.rs
@@ -582,6 +582,14 @@ pub struct NString<'a>(
     pub Option<IString<'a>>,
 );
 
+impl<'a> TryFrom<&'a str> for NString<'a> {
+    type Error = LiteralError;
+
+    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+        Ok(Self(Some(IString::try_from(value)?)))
+    }
+}
+
 impl<'a> From<Literal<'a>> for NString<'a> {
     fn from(value: Literal<'a>) -> Self {
         Self(Some(IString::from(value)))
diff --git a/imap-types/src/imap4rev1/fetch_attributes.rs b/imap-types/src/imap4rev1/fetch_attributes.rs
index 4af2ea2d..5652a6a0 100644
--- a/imap-types/src/imap4rev1/fetch_attributes.rs
+++ b/imap-types/src/imap4rev1/fetch_attributes.rs
@@ -338,7 +338,7 @@ mod tests {
                             number_of_lines: 1337,
                         },
                     },
-                    extension: None,
+                    extension_data: None,
                 }),
                 b"BODY (\"TEXT\" \"foo\" NIL NIL NIL \"base64\" 42 1337)".as_ref(),
             ),
@@ -373,7 +373,7 @@ mod tests {
                             number_of_lines: 224,
                         },
                     },
-                    extension: None,
+                    extension_data: None,
                 }),
                 b"BODYSTRUCTURE (\"TEXT\" \"\" NIL NIL NIL \"base64\" 213 224)",
             ),
diff --git a/imap-types/src/imap4rev1/response.rs b/imap-types/src/imap4rev1/response.rs
index b032be6c..1b7fc189 100644
--- a/imap-types/src/imap4rev1/response.rs
+++ b/imap-types/src/imap4rev1/response.rs
@@ -8,6 +8,7 @@ use std::{
 
 #[cfg(feature = "arbitrary")]
 use arbitrary::Arbitrary;
+use base64::{engine::general_purpose::STANDARD as _base64, Engine};
 #[cfg(feature = "bounded-static")]
 use bounded_static::ToStatic;
 #[cfg(feature = "serde")]
@@ -617,22 +618,16 @@ pub enum FetchError<I, A> {
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum Continue<'a> {
-    Basic {
-        code: Option<Code<'a>>,
-        text: Text<'a>,
-    },
+    Basic(ContinueBasic<'a>),
     Base64(Cow<'a, [u8]>),
 }
 
 impl<'a> Continue<'a> {
-    pub fn basic<T>(code: Option<Code<'a>>, text: T) -> Result<Self, T::Error>
+    pub fn basic<T>(code: Option<Code<'a>>, text: T) -> Result<Self, ContinueError<T::Error>>
     where
         T: TryInto<Text<'a>>,
     {
-        Ok(Continue::Basic {
-            code,
-            text: text.try_into()?,
-        })
+        Ok(Continue::Basic(ContinueBasic::new(code, text)?))
     }
 
     pub fn base64<'data: 'a, D>(data: D) -> Self
@@ -643,6 +638,48 @@ impl<'a> Continue<'a> {
     }
 }
 
+#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ContinueBasic<'a> {
+    code: Option<Code<'a>>,
+    text: Text<'a>,
+}
+
+impl<'a> ContinueBasic<'a> {
+    pub fn new<T>(code: Option<Code<'a>>, text: T) -> Result<Self, ContinueError<T::Error>>
+    where
+        T: TryInto<Text<'a>>,
+    {
+        let text = text.try_into().map_err(ContinueError::Text)?;
+
+        // We need to work around an ambiguity in IMAP:
+        // When there is no `Code`, the `Text` must *not* be valid according to base64.
+        // Otherwise, we could send a `Continue::Basic` that is interpreted as `Continue::Base64`.
+        if code.is_none() && _base64.decode(text.inner()).is_ok() {
+            return Err(ContinueError::Ambiguity);
+        }
+
+        Ok(Self { code, text })
+    }
+
+    pub fn code(&self) -> Option<&Code<'a>> {
+        self.code.as_ref()
+    }
+
+    pub fn text(&self) -> &Text<'a> {
+        &self.text
+    }
+}
+
+#[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
+pub enum ContinueError<T> {
+    #[error("invalid text")]
+    Text(T),
+    #[error("using no code and valid base64 may result in ambiguities")]
+    Ambiguity,
+}
+
 /// A response code consists of data inside square brackets in the form of an atom,
 /// possibly followed by a space and arguments.  The response code
 /// contains additional information or status codes for client software
@@ -984,7 +1021,14 @@ mod tests {
     use std::convert::TryFrom;
 
     use super::*;
-    use crate::testing::known_answer_test_encode;
+    use crate::{
+        core::{IString, NString},
+        imap4rev1::body::BodyExtension,
+        response::data::{
+            BasicFields, Body, BodyStructure, SinglePartExtensionData, SpecificFields,
+        },
+        testing::known_answer_test_encode,
+    };
 
     #[test]
     fn test_encode_greeting() {
@@ -1145,85 +1189,74 @@ mod tests {
 
     #[test]
     fn test_body_structure() {
-        /*
-        let tests: Vec<(_, &[u8])> = vec![
+        let tests = [
             (
-                BodyStructure::Single(Body {
-                    parameter_list: vec![],
-                    id: NString::String(IString::try_from("ares").unwrap()),
-                    description: NString::Nil,
-                    content_transfer_encoding: IString::try_from("xxx").unwrap(),
-                    size: 123,
-                    specific: SpecificFields::Basic {
-                        type_: IString::try_from("application").unwrap(),
-                        subtype: IString::try_from("voodoo").unwrap(),
+                BodyStructure::Single {
+                    body: Body {
+                        basic: BasicFields {
+                            parameter_list: vec![],
+                            id: NString(None),
+                            description: NString::try_from("description").unwrap(),
+                            content_transfer_encoding: IString::try_from("cte").unwrap(),
+                            size: 123,
+                        },
+                        specific: SpecificFields::Basic {
+                            type_: IString::try_from("application").unwrap(),
+                            subtype: IString::try_from("voodoo").unwrap(),
+                        },
                     },
-                    extension: None,
-                }),
-                b"(\"application\" \"voodoo\" nil \"ares\" nil \"xxx\" 123)",
+                    extension_data: None,
+                },
+                b"(\"application\" \"voodoo\" NIL NIL \"description\" \"cte\" 123)".as_ref(),
             ),
             (
-                BodyStructure::Single(Body {
-                    parameter_list: vec![],
-                    id: NString::Nil,
-                    description: NString::Nil,
-                    content_transfer_encoding: IString::try_from("xxx").unwrap(),
-                    size: 123,
-                    specific: SpecificFields::Text {
-                        subtype: IString::try_from("plain").unwrap(),
-                        number_of_lines: 14,
+                BodyStructure::Single {
+                    body: Body {
+                        basic: BasicFields {
+                            parameter_list: vec![],
+                            id: NString(None),
+                            description: NString::try_from("description").unwrap(),
+                            content_transfer_encoding: IString::try_from("cte").unwrap(),
+                            size: 123,
+                        },
+                        specific: SpecificFields::Text {
+                            subtype: IString::try_from("plain").unwrap(),
+                            number_of_lines: 14,
+                        },
                     },
-                    extension: None,
-                }),
-                b"(\"text\" \"plain\" nil nil nil \"xxx\" 123 14)",
+                    extension_data: None,
+                },
+                b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14)",
             ),
             (
-                BodyStructure::Single(Body {
-                    parameter_list: vec![],
-                    id: NString::Nil,
-                    description: NString::Nil,
-                    content_transfer_encoding: IString::try_from("xxx").unwrap(),
-                    size: 123,
-                    specific: SpecificFields::MessageRfc822 {
-                        envelope: Envelope {
-                            date: IString::try_from("date").unwrap(),
-                            subject: IString::try_from("subject").unwrap(),
-                            from: vec![],
-                            sender: vec![],
-                            reply_to: vec![],
-                            to: vec![],
-                            cc: vec![],
-                            bcc: vec![],
-                            in_reply_to: IString::try_from("in-reply-to".to_string()).unwrap(),
-                            message_id: IString::try_from("message-id".to_string()).unwrap(),
-                        },
-                        body_structure: Box::new(BodyStructure::Single(Body {
+                BodyStructure::Single {
+                    body: Body {
+                        basic: BasicFields {
                             parameter_list: vec![],
-                            id: NString::Nil,
-                            description: NString::Nil,
-                            content_transfer_encoding: IString::try_from("xxx").unwrap(),
+                            id: NString(None),
+                            description: NString::try_from("description").unwrap(),
+                            content_transfer_encoding: IString::try_from("cte").unwrap(),
                             size: 123,
-                            specific: SpecificFields::Basic {
-                                type_: IString::try_from("application").unwrap(),
-                                subtype: IString::try_from("voodoo").unwrap(),
-                            },
-                            extension: None,
-                        })),
-                        number_of_lines: 14,
+                        },
+                        specific: SpecificFields::Text {
+                            subtype: IString::try_from("plain").unwrap(),
+                            number_of_lines: 14,
+                        },
                     },
-                    extension: None,
-                }),
-                b"(\"message\" \"rfc822\" nil nil nil \"xxx\" 123 ????????? (\"application\" \"voodoo\" nil nil nil \"xxx\" 123) 14)",
+                    extension_data: Some(SinglePartExtensionData {
+                        md5: NString::try_from("AABB".as_ref()).unwrap(),
+                        disposition: Some(None),
+                        language: Some(vec![]),
+                        location: Some(NString(None)),
+                        extensions: vec![BodyExtension::List(NonEmptyVec::from(BodyExtension::Number(1337)))],
+                    }),
+                },
+                b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14 \"AABB\" NIL NIL NIL (1337))",
             ),
         ];
 
-        for (parsed, serialized) in tests.into_iter() {
-            assert_eq!(
-                String::from_utf8(parsed.serialize()).unwrap(),
-                String::from_utf8(serialized.to_vec()).unwrap()
-            );
-            //assert_eq!(parsed, BodyStructure::deserialize(serialized).unwrap().1);
+        for test in tests {
+            known_answer_test_encode(test);
         }
-        */
     }
 }
diff --git a/imap-types/src/lib.rs b/imap-types/src/lib.rs
index b55ec98f..9dfda7aa 100644
--- a/imap-types/src/lib.rs
+++ b/imap-types/src/lib.rs
@@ -123,12 +123,12 @@
 //!                         subtype: IString::try_from("html").unwrap(),
 //!                     },
 //!                 },
-//!                 extension: Some(SinglePartExtensionData {
+//!                 extension_data: Some(SinglePartExtensionData {
 //!                     md5: NString(None),
 //!                     disposition: None,
 //!                     language: None,
 //!                     location: None,
-//!                     extension: Cow::Borrowed(b""),
+//!                     extensions: vec![],
 //!                 }),
 //!             }),
 //!         ])
@@ -153,11 +153,10 @@
 mod arbitrary;
 mod imap4rev1;
 pub mod security;
-mod utils;
+pub mod utils;
 
-#[cfg(test)]
 pub mod testing {
-    use crate::codec::Encode;
+    use crate::{codec::Encode, utils::escape_byte_string};
 
     pub fn known_answer_test_encode(
         (test_object, expected_bytes): (impl Encode, impl AsRef<[u8]>),
@@ -167,11 +166,11 @@ pub mod testing {
         let got_bytes = got_bytes.as_slice();
 
         if expected_bytes != got_bytes {
-            println!("# Debug (`from_utf8_lossy`, encapsulated by `<<<` and `>>>`)");
+            println!("# Debug (`escape_byte_string`, encapsulated by `<<<` and `>>>`)");
             println!(
                 "Left:  <<<{}>>>\nRight: <<<{}>>>",
-                String::from_utf8_lossy(expected_bytes),
-                String::from_utf8_lossy(got_bytes),
+                escape_byte_string(expected_bytes),
+                escape_byte_string(got_bytes),
             );
             println!("# Debug");
             panic!("Left:  {:02x?}\nRight: {:02x?}", expected_bytes, got_bytes);
@@ -288,15 +287,18 @@ pub mod response {
 
     pub use crate::imap4rev1::{
         core::Text,
-        response::{Code, CodeOther, Continue, Data, Greeting, GreetingKind, Response, Status},
+        response::{
+            Code, CodeOther, Continue, ContinueBasic, Data, Greeting, GreetingKind, Response,
+            Status,
+        },
     };
 
     pub mod data {
         pub use crate::imap4rev1::{
             address::Address,
             body::{
-                BasicFields, Body, BodyStructure, MultiPartExtensionData, SinglePartExtensionData,
-                SpecificFields,
+                BasicFields, Body, BodyExtension, BodyStructure, MultiPartExtensionData,
+                SinglePartExtensionData, SpecificFields,
             },
             core::QuotedChar,
             envelope::Envelope,
diff --git a/imap-types/src/utils.rs b/imap-types/src/utils.rs
index 5fc9d436..43a7352a 100644
--- a/imap-types/src/utils.rs
+++ b/imap-types/src/utils.rs
@@ -1,5 +1,28 @@
 use std::borrow::Cow;
 
+pub fn escape_byte_string(bytes: &[u8]) -> String {
+    bytes
+        .iter()
+        .map(|byte| match byte {
+            0x00..=0x08 => format!("\\x{:02x}", byte),
+            0x09 => String::from("\\t"),
+            0x0A => String::from("\\n"),
+            0x0B => format!("\\x{:02x}", byte),
+            0x0C => format!("\\x{:02x}", byte),
+            0x0D => String::from("\\r"),
+            0x0e..=0x1f => format!("\\x{:02x}", byte),
+            0x20..=0x21 => format!("{}", *byte as char),
+            0x22 => String::from("\\\""),
+            0x23..=0x5B => format!("{}", *byte as char),
+            0x5C => String::from("\\\\"),
+            0x5D..=0x7E => format!("{}", *byte as char),
+            0x7f => format!("\\x{:02x}", byte),
+            0x80..=0xff => format!("\\x{:02x}", byte),
+        })
+        .collect::<Vec<String>>()
+        .join("")
+}
+
 // FIXME(extract_types): copied from abnf-core and imap-codec
 pub mod indicators {
     /// Any 7-bit US-ASCII character, excluding NUL
@@ -154,8 +177,42 @@ mod tests {
 
     #[test]
     fn test_that_unescape_is_inverse_of_escape() {
-        let input = "\\\"\\¹²³abc_*:;059^$%§!\""; // FIXME(#30): use QuickCheck or something similar
+        let input = "\\\"\\¹²³abc_*:;059^$%§!\"";
 
         assert_eq!(input, unescape_quoted(escape_quoted(input).as_ref()));
     }
+
+    #[test]
+    fn test_escape_byte_string() {
+        for byte in 0u8..=255 {
+            let got = escape_byte_string(&[byte]);
+
+            if byte.is_ascii_alphanumeric() {
+                assert_eq!((byte as char).to_string(), got.to_string());
+            } else if byte.is_ascii_whitespace() {
+                if byte == b'\t' {
+                    assert_eq!(String::from("\\t"), got);
+                } else if byte == b'\n' {
+                    assert_eq!(String::from("\\n"), got);
+                }
+            } else if byte.is_ascii_punctuation() {
+                if byte == b'\\' {
+                    assert_eq!(String::from("\\\\"), got);
+                } else if byte == b'"' {
+                    assert_eq!(String::from("\\\""), got);
+                } else {
+                    assert_eq!((byte as char).to_string(), got);
+                }
+            } else {
+                assert_eq!(format!("\\x{:02x}", byte), got);
+            }
+        }
+
+        let tests = [(b"Hallo \"\\\x00", String::from(r#"Hallo \"\\\x00"#))];
+
+        for (test, expected) in tests {
+            let got = escape_byte_string(test);
+            assert_eq!(expected, got);
+        }
+    }
 }
diff --git a/src/imap4rev1/body.rs b/src/imap4rev1/body.rs
index d800570b..038aefb4 100644
--- a/src/imap4rev1/body.rs
+++ b/src/imap4rev1/body.rs
@@ -1,17 +1,17 @@
-use std::{borrow::Cow, convert::TryFrom};
+use std::convert::TryFrom;
 
 use abnf_core::streaming::SP;
 use imap_types::{
     core::{IString, NString, NonEmptyVec},
     response::data::{
-        BasicFields, Body, BodyStructure, MultiPartExtensionData, SinglePartExtensionData,
-        SpecificFields,
+        BasicFields, Body, BodyExtension, BodyStructure, MultiPartExtensionData,
+        SinglePartExtensionData, SpecificFields,
     },
 };
 use nom::{
     branch::alt,
     bytes::streaming::{tag, tag_no_case},
-    combinator::{map, opt, recognize},
+    combinator::{map, opt},
     multi::{many0, many1, separated_list1},
     sequence::{delimited, preceded, tuple},
     IResult,
@@ -75,21 +75,20 @@ fn body_type_1part_limited<'a>(
         )));
     }
 
-    let body_type_msg =
-        move |input: &'a [u8]| body_type_msg_limited(input, remaining_recursions.saturating_sub(1));
+    let body_type_msg = move |input: &'a [u8]| body_type_msg_limited(input, 8);
 
     let mut parser = tuple((
         alt((body_type_msg, body_type_text, body_type_basic)),
         opt(preceded(SP, body_ext_1part)),
     ));
 
-    let (remaining, ((basic, specific), maybe_extension)) = parser(input)?;
+    let (remaining, ((basic, specific), extension_data)) = parser(input)?;
 
     Ok((
         remaining,
         BodyStructure::Single {
             body: Body { basic, specific },
-            extension: maybe_extension,
+            extension_data,
         },
     ))
 }
@@ -273,12 +272,14 @@ pub fn body_fld_lines(input: &[u8]) -> IResult<&[u8], u32> {
     number(input)
 }
 
-/// `body-ext-1part = body-fld-md5
+/// ```abnf
+/// body-ext-1part = body-fld-md5
 ///                   [SP body-fld-dsp
 ///                     [SP body-fld-lang
 ///                       [SP body-fld-loc *(SP body-extension)]
 ///                     ]
-///                   ]`
+///                   ]
+/// ```
 ///
 /// MUST NOT be returned on non-extensible "BODY" fetch
 ///
@@ -287,7 +288,7 @@ pub fn body_ext_1part(input: &[u8]) -> IResult<&[u8], SinglePartExtensionData> {
     let mut disposition = None;
     let mut language = None;
     let mut location = None;
-    let mut extension = Cow::Borrowed(&b""[..]);
+    let mut extensions = vec![];
 
     let (mut rem, md5) = body_fld_md5(input)?;
 
@@ -306,9 +307,9 @@ pub fn body_ext_1part(input: &[u8]) -> IResult<&[u8], SinglePartExtensionData> {
                 rem = rem_;
                 location = Some(loc_);
 
-                let (rem_, ext_) = recognize(many0(preceded(SP, body_extension(8))))(rem)?;
+                let (rem_, exts) = many0(preceded(SP, body_extension(8)))(rem)?;
                 rem = rem_;
-                extension = Cow::Borrowed(ext_);
+                extensions = exts;
             }
         }
     }
@@ -320,7 +321,7 @@ pub fn body_ext_1part(input: &[u8]) -> IResult<&[u8], SinglePartExtensionData> {
             disposition,
             language,
             location,
-            extension,
+            extensions,
         },
     ))
 }
@@ -364,28 +365,30 @@ pub fn body_fld_loc(input: &[u8]) -> IResult<&[u8], NString> {
     nstring(input)
 }
 
-/// `body-extension = nstring /
-///                   number /
-///                   "(" body-extension *(SP body-extension) ")"`
-///
 /// Future expansion.
 ///
 /// Client implementations MUST accept body-extension fields.
 /// Server implementations MUST NOT generate body-extension fields except as defined by
 /// future standard or standards-track revisions of this specification.
 ///
+/// ```abnf
+/// body-extension = nstring /
+///                  number /
+///                  "(" body-extension *(SP body-extension) ")"
+/// ```
+///
 /// Note: 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).
-///
-/// FIXME: This recognizes extension data and returns &[u8].
-pub fn body_extension(remaining_recursions: usize) -> impl Fn(&[u8]) -> IResult<&[u8], &[u8]> {
+pub fn body_extension(
+    remaining_recursions: usize,
+) -> impl Fn(&[u8]) -> IResult<&[u8], BodyExtension> {
     move |input: &[u8]| body_extension_limited(input, remaining_recursions)
 }
 
 fn body_extension_limited<'a>(
     input: &'a [u8],
     remaining_recursion: usize,
-) -> IResult<&'a [u8], &[u8]> {
+) -> IResult<&'a [u8], BodyExtension> {
     if remaining_recursion == 0 {
         return Err(nom::Err::Failure(nom::error::make_error(
             input,
@@ -397,13 +400,12 @@ fn body_extension_limited<'a>(
         move |input: &'a [u8]| body_extension_limited(input, remaining_recursion.saturating_sub(1));
 
     alt((
-        recognize(nstring),
-        recognize(number),
-        recognize(delimited(
-            tag(b"("),
-            separated_list1(SP, body_extension),
-            tag(b")"),
-        )),
+        map(nstring, BodyExtension::NString),
+        map(number, BodyExtension::Number),
+        map(
+            delimited(tag(b"("), separated_list1(SP, body_extension), tag(b")")),
+            |body_extensions| BodyExtension::List(NonEmptyVec::new_unchecked(body_extensions)),
+        ),
     ))(input)
 }
 
@@ -444,12 +446,14 @@ fn body_type_mpart_limited(
     ))
 }
 
-/// `body-ext-mpart = body-fld-param
+/// ```abnf
+/// body-ext-mpart = body-fld-param
 ///                   [SP body-fld-dsp
 ///                     [SP body-fld-lang
 ///                       [SP body-fld-loc *(SP body-extension)]
 ///                     ]
-///                   ]`
+///                   ]
+/// ```
 ///
 /// MUST NOT be returned on non-extensible "BODY" fetch
 ///
@@ -458,7 +462,7 @@ pub fn body_ext_mpart(input: &[u8]) -> IResult<&[u8], MultiPartExtensionData> {
     let mut disposition = None;
     let mut language = None;
     let mut location = None;
-    let mut extension = Cow::Borrowed(&b""[..]);
+    let mut extensions = vec![];
 
     let (mut rem, parameter_list) = body_fld_param(input)?;
 
@@ -477,9 +481,9 @@ pub fn body_ext_mpart(input: &[u8]) -> IResult<&[u8], MultiPartExtensionData> {
                 rem = rem_;
                 location = Some(loc_);
 
-                let (rem_, ext_) = recognize(many0(preceded(SP, body_extension(8))))(rem)?;
+                let (rem_, exts) = many0(preceded(SP, body_extension(8)))(rem)?;
                 rem = rem_;
-                extension = Cow::Borrowed(ext_);
+                extensions = exts;
             }
         }
     }
@@ -491,7 +495,7 @@ pub fn body_ext_mpart(input: &[u8]) -> IResult<&[u8], MultiPartExtensionData> {
             disposition,
             language,
             location,
-            extension,
+            extensions,
         },
     ))
 }
@@ -563,7 +567,17 @@ pub fn media_text(input: &[u8]) -> IResult<&[u8], IString> {
 
 #[cfg(test)]
 mod tests {
+    use std::num::NonZeroU32;
+
     use super::*;
+    use crate::{
+        core::Literal,
+        response::{
+            data::{FetchAttributeValue, SpecificFields::Basic},
+            Data, Response,
+        },
+        testing::known_answer_test_encode,
+    };
 
     #[test]
     fn test_parse_media_basic() {
@@ -595,6 +609,10 @@ mod tests {
             b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") nil|xxx".as_ref(),
             b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\"|xxx".as_ref(),
             b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4))|xxx".as_ref(),
+            b"\"AABB\" NIL NIL NIL 1337|xxx",
+            b"\"AABB\" NIL NIL NIL (1337)|xxx",
+            b"\"AABB\" NIL NIL NIL (1337 1337)|xxx",
+            b"\"AABB\" NIL NIL NIL (1337 (1337 (1337 \"FOO\" {0}\r\n)))|xxx",
         ]
         .iter()
         {
@@ -622,6 +640,8 @@ mod tests {
             b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") nil|xxx".as_ref(),
             b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\"|xxx".as_ref(),
             b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4))|xxx".as_ref(),
+            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4) {0}\r\n)|xxx".as_ref(),
+            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" {0}\r\n {0}\r\n|xxx".as_ref(),
         ]
             .iter()
         {
@@ -630,4 +650,81 @@ mod tests {
             assert_eq!(rem, b"|xxx");
         }
     }
+
+    #[test]
+    fn test_parse_body() {
+        dbg!(body(9)(b"((((((({0}\r\n {0}\r\n NIL NIL NIL {0}\r\n 0 \"FOO\" NIL NIL \"LOCATION\" 1337) \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\")|xxx").unwrap());
+    }
+
+    #[test]
+    fn test_encode_decode() {
+        let tests = [(
+            Response::Data(Data::Fetch {
+                seq_or_uid: NonZeroU32::try_from(3372220415).unwrap(),
+                attributes: NonEmptyVec::from(FetchAttributeValue::BodyStructure(
+                    BodyStructure::Multi {
+                        bodies: NonEmptyVec::from(BodyStructure::Multi {
+                            bodies: NonEmptyVec::from(BodyStructure::Multi {
+                                bodies: NonEmptyVec::from(BodyStructure::Multi {
+                                    bodies: NonEmptyVec::from(BodyStructure::Multi {
+                                        bodies: NonEmptyVec::from(BodyStructure::Multi {
+                                            bodies: NonEmptyVec::from(BodyStructure::Single {
+                                                body: Body {
+                                                    basic: BasicFields {
+                                                        parameter_list: vec![],
+                                                        id: NString(None),
+                                                        description: NString(None),
+                                                        content_transfer_encoding: IString::from(
+                                                            Literal::try_from(b"".as_ref())
+                                                                .unwrap(),
+                                                        ),
+                                                        size: 0,
+                                                    },
+                                                    specific: Basic {
+                                                        type_: IString::from(
+                                                            Literal::try_from(b"".as_ref())
+                                                                .unwrap(),
+                                                        ),
+                                                        subtype: IString::from(
+                                                            Literal::try_from(b"".as_ref())
+                                                                .unwrap(),
+                                                        ),
+                                                    },
+                                                },
+                                                extension_data: Some(SinglePartExtensionData {
+                                                    md5: NString::try_from("FOO").unwrap(),
+                                                    disposition: Some(None),
+                                                    language: Some(vec![]),
+                                                    location: Some(NString::try_from("LOCATION").unwrap()),
+                                                    extensions: vec![BodyExtension::Number(1337)],
+                                                }),
+                                            }),
+                                            subtype: IString::try_from("mixed").unwrap(),
+                                            extension_data: None,
+                                        }),
+                                        subtype: IString::try_from("mixed").unwrap(),
+                                        extension_data: None,
+                                    }),
+                                    subtype: IString::try_from("mixed").unwrap(),
+                                    extension_data: None,
+                                }),
+                                subtype: IString::try_from("mixed").unwrap(),
+                                extension_data: None,
+                            }),
+                            subtype: IString::try_from("mixed").unwrap(),
+                            extension_data: None,
+                        }),
+                        subtype: IString::try_from("mixed").unwrap(),
+                        extension_data: None,
+                    },
+                )),
+            }),
+            b"* 3372220415 FETCH (BODYSTRUCTURE ((((((({0}\r\n {0}\r\n NIL NIL NIL {0}\r\n 0 \"FOO\" NIL NIL \"LOCATION\" 1337) \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\"))\r\n"
+            .as_ref(),
+        )];
+
+        for test in tests {
+            known_answer_test_encode(test);
+        }
+    }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 496fe4ad..55004d53 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -99,9 +99,8 @@ pub mod codec;
 ))]
 mod extensions;
 mod imap4rev1;
-pub mod utils;
 
-pub use imap_types::{command, core, message, response, security, state};
+pub use imap_types::{command, core, message, response, security, state, testing, utils};
 
 // ----------- Compatibility modules -----------
 
diff --git a/src/utils.rs b/src/utils.rs
deleted file mode 100644
index f93516ee..00000000
--- a/src/utils.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-use std::borrow::Cow;
-
-pub fn escape_byte_string(bytes: &[u8]) -> String {
-    bytes
-        .iter()
-        .map(|byte| match byte {
-            0x00..=0x08 => format!("\\x{:02x}", byte),
-            0x09 => String::from("\\t"),
-            0x0A => String::from("\\n"),
-            0x0B => format!("\\x{:02x}", byte),
-            0x0C => format!("\\x{:02x}", byte),
-            0x0D => String::from("\\r"),
-            0x0e..=0x1f => format!("\\x{:02x}", byte),
-            0x20..=0x21 => format!("{}", *byte as char),
-            0x22 => String::from("\\\""),
-            0x23..=0x5B => format!("{}", *byte as char),
-            0x5C => String::from("\\\\"),
-            0x5D..=0x7E => format!("{}", *byte as char),
-            0x7f => format!("\\x{:02x}", byte),
-            0x80..=0xff => format!("\\x{:02x}", byte),
-        })
-        .collect::<Vec<String>>()
-        .join("")
-}
-
-#[allow(unused)]
-pub fn escape_quoted(unescaped: &str) -> Cow<str> {
-    let mut escaped = Cow::Borrowed(unescaped);
-
-    if escaped.contains('\\') {
-        escaped = Cow::Owned(escaped.replace('\\', "\\\\"));
-    }
-
-    if escaped.contains('\"') {
-        escaped = Cow::Owned(escaped.replace('"', "\\\""));
-    }
-
-    escaped
-}
-
-pub fn unescape_quoted(escaped: &str) -> Cow<str> {
-    let mut unescaped = Cow::Borrowed(escaped);
-
-    if unescaped.contains("\\\\") {
-        unescaped = Cow::Owned(unescaped.replace("\\\\", "\\"));
-    }
-
-    if unescaped.contains("\\\"") {
-        unescaped = Cow::Owned(unescaped.replace("\\\"", "\""));
-    }
-
-    unescaped
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_escape_quoted() {
-        let tests = [
-            ("", ""),
-            ("\\", "\\\\"),
-            ("\"", "\\\""),
-            ("alice", "alice"),
-            ("\\alice\\", "\\\\alice\\\\"),
-            ("alice\"", "alice\\\""),
-            (r#"\alice\ ""#, r#"\\alice\\ \""#),
-        ];
-
-        for (test, expected) in tests {
-            let got = escape_quoted(test);
-            assert_eq!(expected, got);
-        }
-    }
-
-    #[test]
-    fn test_unescape_quoted() {
-        let tests = [
-            ("", ""),
-            ("\\\\", "\\"),
-            ("\\\"", "\""),
-            ("alice", "alice"),
-            ("\\\\alice\\\\", "\\alice\\"),
-            ("alice\\\"", "alice\""),
-            (r#"\\alice\\ \""#, r#"\alice\ ""#),
-        ];
-
-        for (test, expected) in tests {
-            let got = unescape_quoted(test);
-            assert_eq!(expected, got);
-        }
-    }
-
-    #[test]
-    fn test_that_unescape_is_inverse_of_escape() {
-        let input = "\\\"\\¹²³abc_*:;059^$%§!\""; // FIXME(#30): use QuickCheck or something similar
-
-        assert_eq!(input, unescape_quoted(escape_quoted(input).as_ref()));
-    }
-
-    #[test]
-    fn test_escape_byte_string() {
-        for byte in 0u8..=255 {
-            let got = escape_byte_string(&[byte]);
-
-            if byte.is_ascii_alphanumeric() {
-                assert_eq!((byte as char).to_string(), got.to_string());
-            } else if byte.is_ascii_whitespace() {
-                if byte == b'\t' {
-                    assert_eq!(String::from("\\t"), got);
-                } else if byte == b'\n' {
-                    assert_eq!(String::from("\\n"), got);
-                }
-            } else if byte.is_ascii_punctuation() {
-                if byte == b'\\' {
-                    assert_eq!(String::from("\\\\"), got);
-                } else if byte == b'"' {
-                    assert_eq!(String::from("\\\""), got);
-                } else {
-                    assert_eq!((byte as char).to_string(), got);
-                }
-            } else {
-                assert_eq!(format!("\\x{:02x}", byte), got);
-            }
-        }
-
-        let tests = [(b"Hallo \"\\\x00", String::from(r#"Hallo \"\\\x00"#))];
-
-        for (test, expected) in tests {
-            let got = escape_byte_string(test);
-            assert_eq!(expected, got);
-        }
-    }
-}


Test patch:
diff --git a/tests/trace.rs b/tests/trace.rs
index 28e1bde6..c6954f21 100644
--- a/tests/trace.rs
+++ b/tests/trace.rs
@@ -1056,7 +1056,7 @@ fn test_transcript_from_rfc() {
                                         number_of_lines: 92,
                                     },
                                 },
-                                extension: None,
+                                extension_data: None,
                             }),
                         ],
                     )