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

Issue:
Use `secrecy` for sensitive data.
See https://crates.io/crates/secrecy


Discussion / hints:
Changing `CommandBody::Login.password` to `Secret<AString>` has a whole string of consequences. We would need to work around `Arbitrary`, `PartialEq`, etc. Postpone.
Grrr... this bugs me. I'll add it to some milestone.

Fix patch:
diff --git a/README.md b/README.md
index 9bda7d6d..84e6eee7 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ Status(Ok { tag: None, code: None, text: Text("IMAP4rev1 Service Ready") })
 // * OK IMAP4rev1 Service Ready
 
 // a001 login mrc secret
-Command { tag: Tag("a001"), body: Login { username: Atom(Atom("mrc")), password: Atom(Atom("secret")) } }
+Command { tag: Tag("a001"), body: Login { username: Atom(AtomExt("mrc")), password: [[REDACTED]] } }
 // a001 LOGIN mrc secret
 
 // a001 OK LOGIN completed
@@ -117,11 +117,11 @@ Status(Ok { tag: Some(Tag("a002")), code: Some(ReadWrite), text: Text("SELECT co
 // a002 OK [READ-WRITE] SELECT completed
 
 // a003 fetch 12 full
-Command { tag: Tag("a003"), body: Fetch { sequence_set: SequenceSet([Single(Value(12))]), attributes: Macro(Full), uid: false } }
+Command { tag: Tag("a003"), body: Fetch { sequence_set: SequenceSet(NonEmptyVec([Single(Value(12))])), attributes: Macro(Full), uid: false } }
 // a003 FETCH 12 FULL
 
 // * 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US")("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92))
-Data(Fetch { seq_or_uid: 12, attributes: [Flags([Seen]), InternalDate(1996-07-17T02:44:25-07:00), Rfc822Size(4286), Envelope(Envelope { date: NString(Some(Quoted(Quoted("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)")))), subject: NString(Some(Quoted(Quoted("IMAP4rev1 WG mtg summary and minutes")))), from: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], sender: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], reply_to: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], to: [Address { name: NString(None), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("imap")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], cc: [Address { name: NString(None), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("minutes")))), host: NString(Some(Quoted(Quoted("CNRI.Reston.VA.US")))) }, Address { name: NString(Some(Quoted(Quoted("John Klensin")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("KLENSIN")))), host: NString(Some(Quoted(Quoted("MIT.EDU")))) }], bcc: [], in_reply_to: NString(None), message_id: NString(Some(Quoted(Quoted("<B27397-0100000@cac.washington.edu>")))) }), Body(Single { body: Body { basic: BasicFields { parameter_list: [(Quoted(Quoted("CHARSET")), Quoted(Quoted("US-ASCII")))], id: NString(None), description: NString(None), content_transfer_encoding: Quoted(Quoted("7BIT")), size: 3028 }, specific: Text { subtype: Quoted(Quoted("PLAIN")), number_of_lines: 92 } }, extension: None })] })
+Data(Fetch { seq_or_uid: 12, attributes: NonEmptyVec([Flags([Seen]), InternalDate(1996-07-17T02:44:25-07:00), Rfc822Size(4286), Envelope(Envelope { date: NString(Some(Quoted(Quoted("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)")))), subject: NString(Some(Quoted(Quoted("IMAP4rev1 WG mtg summary and minutes")))), from: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], sender: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], reply_to: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], to: [Address { name: NString(None), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("imap")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], cc: [Address { name: NString(None), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("minutes")))), host: NString(Some(Quoted(Quoted("CNRI.Reston.VA.US")))) }, Address { name: NString(Some(Quoted(Quoted("John Klensin")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("KLENSIN")))), host: NString(Some(Quoted(Quoted("MIT.EDU")))) }], bcc: [], in_reply_to: NString(None), message_id: NString(Some(Quoted(Quoted("<B27397-0100000@cac.washington.edu>")))) }), Body(Single { body: Body { basic: BasicFields { parameter_list: [(Quoted(Quoted("CHARSET")), Quoted(Quoted("US-ASCII")))], id: NString(None), description: NString(None), content_transfer_encoding: Quoted(Quoted("7BIT")), size: 3028 }, specific: Text { subtype: Quoted(Quoted("PLAIN")), number_of_lines: 92 } }, extension: None })]) })
 // * 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US")("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92))
 
 // a003 OK FETCH completed
@@ -129,19 +129,43 @@ Status(Ok { tag: Some(Tag("a003")), code: None, text: Text("FETCH completed") })
 // a003 OK FETCH completed
 
 // a004 fetch 12 body[header]
-Command { tag: Tag("a004"), body: Fetch { sequence_set: SequenceSet([Single(Value(12))]), attributes: FetchAttributes([BodyExt { section: Some(Header(None)), partial: None, peek: false }]), uid: false } }
+Command { tag: Tag("a004"), body: Fetch { sequence_set: SequenceSet(NonEmptyVec([Single(Value(12))])), attributes: FetchAttributes([BodyExt { section: Some(Header(None)), partial: None, peek: false }]), uid: false } }
 // a004 FETCH 12 BODY[HEADER]
 
+// * 12 FETCH (BODY[HEADER] {342}
+// Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)
+// From: Terry Gray <gray@cac.washington.edu>
+// Subject: IMAP4rev1 WG mtg summary and minutes
+// To: imap@cac.washington.edu
+// cc: minutes@CNRI.Reston.VA.US, John Klensin <KLENSIN@MIT.EDU>
+// Message-Id: <B27397-0100000@cac.washington.edu>
+// MIME-Version: 1.0
+// Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
+// 
+// )
+Data(Fetch { seq_or_uid: 12, attributes: NonEmptyVec([BodyExt { section: Some(Header(None)), origin: None, data: NString(Some(Literal(Literal([68, 97, 116, 101, 58, 32, 87, 101, 100, 44, 32, 49, 55, 32, 74, 117, 108, 32, 49, 57, 57, 54, 32, 48, 50, 58, 50, 51, 58, 50, 53, 32, 45, 48, 55, 48, 48, 32, 40, 80, 68, 84, 41, 13, 10, 70, 114, 111, 109, 58, 32, 84, 101, 114, 114, 121, 32, 71, 114, 97, 121, 32, 60, 103, 114, 97, 121, 64, 99, 97, 99, 46, 119, 97, 115, 104, 105, 110, 103, 116, 111, 110, 46, 101, 100, 117, 62, 13, 10, 83, 117, 98, 106, 101, 99, 116, 58, 32, 73, 77, 65, 80, 52, 114, 101, 118, 49, 32, 87, 71, 32, 109, 116, 103, 32, 115, 117, 109, 109, 97, 114, 121, 32, 97, 110, 100, 32, 109, 105, 110, 117, 116, 101, 115, 13, 10, 84, 111, 58, 32, 105, 109, 97, 112, 64, 99, 97, 99, 46, 119, 97, 115, 104, 105, 110, 103, 116, 111, 110, 46, 101, 100, 117, 13, 10, 99, 99, 58, 32, 109, 105, 110, 117, 116, 101, 115, 64, 67, 78, 82, 73, 46, 82, 101, 115, 116, 111, 110, 46, 86, 65, 46, 85, 83, 44, 32, 74, 111, 104, 110, 32, 75, 108, 101, 110, 115, 105, 110, 32, 60, 75, 76, 69, 78, 83, 73, 78, 64, 77, 73, 84, 46, 69, 68, 85, 62, 13, 10, 77, 101, 115, 115, 97, 103, 101, 45, 73, 100, 58, 32, 60, 66, 50, 55, 51, 57, 55, 45, 48, 49, 48, 48, 48, 48, 48, 64, 99, 97, 99, 46, 119, 97, 115, 104, 105, 110, 103, 116, 111, 110, 46, 101, 100, 117, 62, 13, 10, 77, 73, 77, 69, 45, 86, 101, 114, 115, 105, 111, 110, 58, 32, 49, 46, 48, 13, 10, 67, 111, 110, 116, 101, 110, 116, 45, 84, 121, 112, 101, 58, 32, 84, 69, 88, 84, 47, 80, 76, 65, 73, 78, 59, 32, 67, 72, 65, 82, 83, 69, 84, 61, 85, 83, 45, 65, 83, 67, 73, 73, 13, 10, 13, 10])))) }]) })
+// * 12 FETCH (BODY[HEADER] {342}
+// Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)
+// From: Terry Gray <gray@cac.washington.edu>
+// Subject: IMAP4rev1 WG mtg summary and minutes
+// To: imap@cac.washington.edu
+// cc: minutes@CNRI.Reston.VA.US, John Klensin <KLENSIN@MIT.EDU>
+// Message-Id: <B27397-0100000@cac.washington.edu>
+// MIME-Version: 1.0
+// Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
+// 
+// )
+
 // a004 OK FETCH completed
 Status(Ok { tag: Some(Tag("a004")), code: None, text: Text("FETCH completed") })
 // a004 OK FETCH completed
 
 // a005 store 12 +flags \deleted
-Command { tag: Tag("a005"), body: Store { sequence_set: SequenceSet([Single(Value(12))]), kind: Add, response: Answer, flags: [Deleted], uid: false } }
+Command { tag: Tag("a005"), body: Store { sequence_set: SequenceSet(NonEmptyVec([Single(Value(12))])), kind: Add, response: Answer, flags: [Deleted], uid: false } }
 // a005 STORE 12 +FLAGS (\Deleted)
 
 // * 12 FETCH (FLAGS (\Seen \Deleted))
-Data(Fetch { seq_or_uid: 12, attributes: [Flags([Seen, Deleted])] })
+Data(Fetch { seq_or_uid: 12, attributes: NonEmptyVec([Flags([Seen, Deleted])]) })
 // * 12 FETCH (FLAGS (\Seen \Deleted))
 
 // a005 OK +FLAGS completed
diff --git a/assets/demos/tokio_server/Cargo.toml b/assets/demos/tokio_server/Cargo.toml
index 84e3ea83..f5e070b5 100644
--- a/assets/demos/tokio_server/Cargo.toml
+++ b/assets/demos/tokio_server/Cargo.toml
@@ -8,6 +8,5 @@ edition = "2021"
 [dependencies]
 futures = "0.3"
 imap-codec = { path = "../../../", features = ["tokio_util_codec"] }
-subtle = "*"
 tokio = { version = "*", features = ["full"] }
 tokio-util = { version = "*", features = ["codec"] }
diff --git a/assets/demos/tokio_server/src/main.rs b/assets/demos/tokio_server/src/main.rs
index 678a07fa..6bcdb51a 100644
--- a/assets/demos/tokio_server/src/main.rs
+++ b/assets/demos/tokio_server/src/main.rs
@@ -4,7 +4,6 @@ use imap_codec::{
     response::{data::Capability, Continue, Data, Greeting, Response, Status},
     tokio_compat::server::{Action, ImapServerCodec, OutcomeServer},
 };
-use subtle::ConstantTimeEq;
 use tokio::{self, net::TcpListener};
 use tokio_util::codec::Decoder;
 
@@ -52,13 +51,8 @@ async fn main() {
                         println!("S: {rsp:?}");
                     }
                     (tag, CommandBody::Login { username, password }) => {
-                        // Convert `AString`s to `&[u8]` ...
-                        let (username, password) = (username.as_ref(), password.as_ref());
-
-                        // ... and compare them with something. (In this example we compare the
-                        // hard-codec username and password in constant time by using the "subtle" crate.)
-                        let rsp = if username.ct_eq(b"alice").unwrap_u8() == 1
-                            && password.ct_eq(b"password").unwrap_u8() == 1
+                        let rsp = if username.as_ref() == b"alice"
+                            && password.compare_ct("password")
                         {
                             Response::Status(
                                 Status::ok(Some(tag), None, "LOGIN succeeded").unwrap(),
diff --git a/benches/parse_command.rs b/benches/parse_command.rs
index b56e3e11..e4460313 100644
--- a/benches/parse_command.rs
+++ b/benches/parse_command.rs
@@ -1,7 +1,7 @@
 use criterion::{black_box, criterion_group, criterion_main, Criterion};
 use imap_codec::{codec::Decode, command::Command};
 
-fn parse_command<'a>(input: &'a [u8]) -> Command<'a> {
+fn parse_command(input: &[u8]) -> Command {
     let (_remaining, cmd) = Command::decode(input).unwrap();
 
     cmd
diff --git a/benches/parse_response.rs b/benches/parse_response.rs
index 781e02f1..415afce7 100644
--- a/benches/parse_response.rs
+++ b/benches/parse_response.rs
@@ -1,7 +1,7 @@
 use criterion::{black_box, criterion_group, criterion_main, Criterion};
 use imap_codec::{codec::Decode, response::Response};
 
-fn parse_response<'a>(input: &'a [u8]) -> Response<'a> {
+fn parse_response(input: &[u8]) -> Response {
     let (_remaining, rsp) = Response::decode(input).unwrap();
 
     rsp
diff --git a/deny.toml b/deny.toml
index e0e32634..8f77759c 100644
--- a/deny.toml
+++ b/deny.toml
@@ -3,4 +3,4 @@ unknown-registry = "deny"
 unknown-git      = "deny"
 
 [licenses]
-allow = [ "Apache-2.0", "MIT", "Unicode-DFS-2016" ]
+allow = [ "Apache-2.0", "MIT", "BSD-3-Clause", "Unicode-DFS-2016" ]
diff --git a/examples/parse_command.rs b/examples/parse_command.rs
index 02edf764..73682522 100644
--- a/examples/parse_command.rs
+++ b/examples/parse_command.rs
@@ -87,5 +87,5 @@ pub fn read_line(prompt: &str) -> String {
 
     print!("{}", ColorClient.suffix());
 
-    line.replace("\n", "\r\n")
+    line.replace('\n', "\r\n")
 }
diff --git a/examples/parse_greeting.rs b/examples/parse_greeting.rs
index c2269f93..e6572b22 100644
--- a/examples/parse_greeting.rs
+++ b/examples/parse_greeting.rs
@@ -10,7 +10,7 @@ fn main() -> std::io::Result<()> {
 
             let mut line = String::new();
             std::io::stdin().read_line(&mut line)?;
-            line.replace("\n", "\r\n")
+            line.replace('\n', "\r\n")
         };
 
         if line.trim() == "exit" {
diff --git a/examples/parse_response.rs b/examples/parse_response.rs
index 11d75de7..17ad7cd4 100644
--- a/examples/parse_response.rs
+++ b/examples/parse_response.rs
@@ -10,7 +10,7 @@ fn main() -> std::io::Result<()> {
 
             let mut line = String::new();
             std::io::stdin().read_line(&mut line)?;
-            line.replace("\n", "\r\n")
+            line.replace('\n', "\r\n")
         };
 
         if line.trim() == "exit" {
diff --git a/fuzz/fuzz_targets/greeting_to_bytes_and_back.rs b/fuzz/fuzz_targets/greeting_to_bytes_and_back.rs
index cbcf0926..f71d34ba 100644
--- a/fuzz/fuzz_targets/greeting_to_bytes_and_back.rs
+++ b/fuzz/fuzz_targets/greeting_to_bytes_and_back.rs
@@ -11,8 +11,8 @@ use libfuzzer_sys::fuzz_target;
 fuzz_target!(|test: Greeting| {
     // 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.code {
-        Some(ref code) => match code {
+    if let Some(ref code) = test.code {
+        match code {
             Code::PermanentFlags(_) => {
                 // FIXME(#30)
                 return;
@@ -27,8 +27,7 @@ fuzz_target!(|test: Greeting| {
                 return;
             }
             _ => {}
-        },
-        _ => {}
+        }
     }
 
     #[cfg(feature = "debug")]
diff --git a/imap-types/Cargo.toml b/imap-types/Cargo.toml
index 4373bc6d..d52226c3 100644
--- a/imap-types/Cargo.toml
+++ b/imap-types/Cargo.toml
@@ -32,6 +32,7 @@ base64 = "0.21"
 bounded-static = { version = "0.4", optional = true }
 chrono = { version = "0.4", default-features = false, features = ["alloc"] }
 serde = { version = "1", features = ["derive"], optional = true }
+subtle = "2.5.0"
 
 [dev-dependencies]
 criterion = "0.4"
diff --git a/imap-types/src/codec.rs b/imap-types/src/codec.rs
index 2797d250..97d59668 100644
--- a/imap-types/src/codec.rs
+++ b/imap-types/src/codec.rs
@@ -115,10 +115,10 @@ impl<'a> Encode for CommandBody<'a> {
                     // RFC 4959 (https://datatracker.ietf.org/doc/html/rfc4959#section-3)
                     // "To send a zero-length initial response, the client MUST send a single pad character ("=").
                     // This indicates that the response is present, but is a zero-length string."
-                    if ir.is_empty() {
+                    if ir.expose_secret().is_empty() {
                         writer.write_all(b"=")?;
                     } else {
-                        writer.write_all(base64.encode(ir).as_bytes())?;
+                        writer.write_all(base64.encode(ir.expose_secret()).as_bytes())?;
                     };
                 };
 
@@ -129,7 +129,7 @@ impl<'a> Encode for CommandBody<'a> {
                 writer.write_all(b" ")?;
                 username.encode(writer)?;
                 writer.write_all(b" ")?;
-                password.encode(writer)
+                password.expose_secret().encode(writer)
             }
             CommandBody::Select { mailbox } => {
                 writer.write_all(b"SELECT")?;
@@ -367,7 +367,7 @@ impl<'a> Encode for AuthMechanismOther<'a> {
 
 impl Encode for AuthenticateData {
     fn encode(&self, writer: &mut impl Write) -> std::io::Result<()> {
-        let encoded = base64.encode(&self.data);
+        let encoded = base64.encode(self.0.expose_secret());
         writer.write_all(encoded.as_bytes())
     }
 }
@@ -426,7 +426,7 @@ impl<'a> Encode for Mailbox<'a> {
 
 impl<'a> Encode for MailboxOther<'a> {
     fn encode(&self, writer: &mut impl Write) -> std::io::Result<()> {
-        self.inner.encode(writer)
+        self.0.encode(writer)
     }
 }
 
@@ -1188,7 +1188,7 @@ impl<'a> Encode for FetchAttributeValue<'a> {
 
 impl<'a> Encode for NString<'a> {
     fn encode(&self, writer: &mut impl Write) -> std::io::Result<()> {
-        match &self.inner {
+        match &self.0 {
             Some(imap_str) => imap_str.encode(writer),
             None => writer.write_all(b"NIL"),
         }
@@ -1211,7 +1211,7 @@ impl<'a> Encode for BodyStructure<'a> {
                 subtype,
                 extension_data,
             } => {
-                for body in &bodies.inner {
+                for body in &bodies.0 {
                     body.encode(writer)?;
                 }
                 writer.write_all(b" ")?;
diff --git a/imap-types/src/extensions/rfc4987.rs b/imap-types/src/extensions/rfc4987.rs
index 5a741d92..957e73fe 100644
--- a/imap-types/src/extensions/rfc4987.rs
+++ b/imap-types/src/extensions/rfc4987.rs
@@ -46,7 +46,7 @@ impl<'a> TryFrom<Atom<'a>> for CompressionAlgorithm {
     }
 }
 
-impl<'a> AsRef<str> for CompressionAlgorithm {
+impl AsRef<str> for CompressionAlgorithm {
     fn as_ref(&self) -> &str {
         match self {
             CompressionAlgorithm::Deflate => "deflate",
diff --git a/imap-types/src/extensions/rfc9208.rs b/imap-types/src/extensions/rfc9208.rs
index 382ce10e..54979e64 100644
--- a/imap-types/src/extensions/rfc9208.rs
+++ b/imap-types/src/extensions/rfc9208.rs
@@ -138,7 +138,7 @@ impl<'a> From<Atom<'a>> for Resource<'a> {
             "message" => Resource::Message,
             "mailbox" => Resource::Mailbox,
             "annotation-storage" => Resource::AnnotationStorage,
-            _ => Resource::Other(ResourceOther { inner: value }),
+            _ => Resource::Other(ResourceOther(value)),
         }
     }
 }
@@ -159,14 +159,12 @@ impl<'a> Encode for Resource<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct ResourceOther<'a> {
-    inner: Atom<'a>,
-}
+pub struct ResourceOther<'a>(Atom<'a>);
 
 impl<'a> ResourceOther<'a> {
     #[cfg(feature = "unchecked")]
     pub fn new_unchecked(atom: Atom<'a>) -> Self {
-        Self { inner: atom }
+        Self(atom)
     }
 }
 
@@ -180,16 +178,16 @@ impl<'a> TryFrom<Atom<'a>> for ResourceOther<'a> {
     type Error = ();
 
     fn try_from(atom: Atom<'a>) -> Result<Self, Self::Error> {
-        match atom.inner.to_lowercase().as_ref() {
+        match atom.0.to_lowercase().as_ref() {
             "storage" | "message" | "mailbox" | "annotation-storage" => Err(()),
-            _ => Ok(Self { inner: atom }),
+            _ => Ok(Self(atom)),
         }
     }
 }
 
 impl<'a> Encode for ResourceOther<'a> {
     fn encode(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
-        self.inner.encode(writer)
+        self.0.encode(writer)
     }
 }
 
diff --git a/imap-types/src/lib.rs b/imap-types/src/lib.rs
index 512f6e0d..fb202046 100644
--- a/imap-types/src/lib.rs
+++ b/imap-types/src/lib.rs
@@ -111,10 +111,10 @@
 //!                 body: Body {
 //!                     basic: BasicFields {
 //!                         parameter_list: vec![],
-//!                         id: NString { inner: None },
-//!                         description: NString {
-//!                             inner: Some(IString::try_from("Important message.").unwrap()),
-//!                         },
+//!                         id: NString(None),
+//!                         description: NString(Some(
+//!                             IString::try_from("Important message.").unwrap(),
+//!                         )),
 //!                         content_transfer_encoding: IString::try_from("base64").unwrap(),
 //!                         size: 512,
 //!                     },
@@ -124,7 +124,7 @@
 //!                     },
 //!                 },
 //!                 extension: Some(SinglePartExtensionData {
-//!                     md5: NString { inner: None },
+//!                     md5: NString(None),
 //!                     disposition: None,
 //!                     language: None,
 //!                     location: None,
@@ -152,6 +152,7 @@
 #[cfg(feature = "arbitrary")]
 mod arbitrary;
 mod rfc3501;
+pub mod security;
 mod utils;
 
 // -- API -----------------------------------------------------------------------------------
diff --git a/imap-types/src/rfc3501/command.rs b/imap-types/src/rfc3501/command.rs
index b62d49e1..ddef2cb5 100644
--- a/imap-types/src/rfc3501/command.rs
+++ b/imap-types/src/rfc3501/command.rs
@@ -28,6 +28,7 @@ use crate::{
     },
     core::{AString, Atom, Literal, NonEmptyVec},
     message::{AuthMechanism, Charset, Flag, Mailbox, MyDateTime, MyNaiveDate, Tag},
+    security::Secret,
 };
 
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
@@ -250,7 +251,7 @@ pub enum CommandBody<'a> {
         mechanism: AuthMechanism<'a>,
         /// Already base64-decoded
         #[cfg(feature = "ext_sasl_ir")]
-        initial_response: Option<Cow<'a, [u8]>>,
+        initial_response: Option<Secret<Cow<'a, [u8]>>>,
     },
 
     /// ### 6.2.3.  LOGIN Command
@@ -293,7 +294,7 @@ pub enum CommandBody<'a> {
     ///   LOGINDISABLED capability is advertised.
     Login {
         username: AString<'a>,
-        password: AString<'a>,
+        password: Secret<AString<'a>>,
     },
 
     // ----- Authenticated State (https://tools.ietf.org/html/rfc3501#section-6.3) -----
@@ -1315,7 +1316,7 @@ impl<'a> CommandBody<'a> {
     pub fn authenticate(mechanism: AuthMechanism<'a>, initial_response: Option<&'a [u8]>) -> Self {
         CommandBody::Authenticate {
             mechanism,
-            initial_response: initial_response.map(Cow::Borrowed),
+            initial_response: initial_response.map(|ir| Secret::new(Cow::Borrowed(ir))),
         }
     }
 
@@ -1326,7 +1327,7 @@ impl<'a> CommandBody<'a> {
     {
         Ok(CommandBody::Login {
             username: username.try_into().map_err(|_| ())?,
-            password: password.try_into().map_err(|_| ())?,
+            password: Secret::new(password.try_into().map_err(|_| ())?),
         })
     }
 
@@ -1594,9 +1595,7 @@ impl<'a> CommandBody<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct AuthenticateData {
-    pub data: Vec<u8>,
-}
+pub struct AuthenticateData(pub Secret<Vec<u8>>);
 
 /// The defined search keys.
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
@@ -1750,7 +1749,7 @@ pub enum SearchKey<'a> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     #[cfg(feature = "ext_sasl_ir")]
     use std::borrow::Cow;
     #[cfg(feature = "ext_sasl_ir")]
@@ -1778,7 +1777,7 @@ mod test {
         message::{AuthMechanism, Flag, MyDateTime, Part, Section},
     };
     #[cfg(feature = "ext_sasl_ir")]
-    use crate::{command::Command, message::Tag};
+    use crate::{command::Command, message::Tag, security::Secret};
 
     #[test]
     fn test_commandbody_new() {
@@ -1967,7 +1966,7 @@ mod test {
             Tag::try_from("A").unwrap(),
             CommandBody::Authenticate {
                 mechanism: AuthMechanism::Plain,
-                initial_response: Some(Cow::Borrowed(&b""[..])),
+                initial_response: Some(Secret::new(Cow::Borrowed(&b""[..]))),
             },
         )
         .unwrap();
diff --git a/imap-types/src/rfc3501/core.rs b/imap-types/src/rfc3501/core.rs
index 131b9384..ba488c22 100644
--- a/imap-types/src/rfc3501/core.rs
+++ b/imap-types/src/rfc3501/core.rs
@@ -48,9 +48,7 @@ pub(crate) use impl_try_from_try_from;
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Atom<'a> {
-    pub(crate) inner: Cow<'a, str>,
-}
+pub struct Atom<'a>(pub(crate) Cow<'a, str>);
 
 impl<'a> Atom<'a> {
     pub fn verify(value: &[u8]) -> bool {
@@ -58,11 +56,11 @@ impl<'a> Atom<'a> {
     }
 
     pub fn inner(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 
     pub fn into_inner(self) -> Cow<'a, str> {
-        self.inner
+        self.0
     }
 
     #[cfg(feature = "unchecked")]
@@ -70,7 +68,7 @@ impl<'a> Atom<'a> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(inner.as_bytes()));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -99,9 +97,7 @@ impl<'a> TryFrom<&'a str> for Atom<'a> {
 
     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Self {
-                inner: Cow::Borrowed(value),
-            })
+            Ok(Self(Cow::Borrowed(value)))
         } else {
             Err(())
         }
@@ -113,9 +109,7 @@ impl<'a> TryFrom<String> for Atom<'a> {
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Atom {
-                inner: Cow::Owned(value),
-            })
+            Ok(Atom(Cow::Owned(value)))
         } else {
             Err(())
         }
@@ -127,7 +121,7 @@ impl<'a> TryFrom<Cow<'a, str>> for Atom<'a> {
 
     fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Atom { inner: value })
+            Ok(Atom(value))
         } else {
             Err(())
         }
@@ -136,7 +130,7 @@ impl<'a> TryFrom<Cow<'a, str>> for Atom<'a> {
 
 impl<'a> AsRef<str> for Atom<'a> {
     fn as_ref(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 }
 
@@ -146,9 +140,7 @@ impl<'a> AsRef<str> for Atom<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct AtomExt<'a> {
-    pub(crate) inner: Cow<'a, str>,
-}
+pub struct AtomExt<'a>(pub(crate) Cow<'a, str>);
 
 impl<'a> AtomExt<'a> {
     pub fn verify(value: &[u8]) -> bool {
@@ -156,7 +148,7 @@ impl<'a> AtomExt<'a> {
     }
 
     pub fn inner(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 
     #[cfg(feature = "unchecked")]
@@ -164,7 +156,7 @@ impl<'a> AtomExt<'a> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(inner.as_bytes()));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -193,9 +185,7 @@ impl<'a> TryFrom<&'a str> for AtomExt<'a> {
 
     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Self {
-                inner: Cow::Borrowed(value),
-            })
+            Ok(Self(Cow::Borrowed(value)))
         } else {
             Err(())
         }
@@ -207,9 +197,7 @@ impl<'a> TryFrom<String> for AtomExt<'a> {
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Self {
-                inner: Cow::Owned(value),
-            })
+            Ok(Self(Cow::Owned(value)))
         } else {
             Err(())
         }
@@ -218,7 +206,7 @@ impl<'a> TryFrom<String> for AtomExt<'a> {
 
 impl<'a> AsRef<str> for AtomExt<'a> {
     fn as_ref(&self) -> &str {
-        &self.inner
+        &self.0
     }
 }
 
@@ -326,9 +314,7 @@ impl<'a> AsRef<[u8]> for IString<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Literal<'a> {
-    pub(crate) inner: Cow<'a, [u8]>,
-}
+pub struct Literal<'a>(pub(crate) Cow<'a, [u8]>);
 
 impl<'a> Literal<'a> {
     pub fn verify(bytes: &[u8]) -> bool {
@@ -336,7 +322,7 @@ impl<'a> Literal<'a> {
     }
 
     pub fn inner(&self) -> &[u8] {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 
     /// Create a literal from a byte sequence without checking
@@ -351,7 +337,7 @@ impl<'a> Literal<'a> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(&inner));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -360,9 +346,7 @@ impl<'a> TryFrom<&'a [u8]> for Literal<'a> {
 
     fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
         if Literal::verify(value) {
-            Ok(Literal {
-                inner: Cow::Borrowed(value),
-            })
+            Ok(Literal(Cow::Borrowed(value)))
         } else {
             Err(())
         }
@@ -374,9 +358,7 @@ impl<'a> TryFrom<Vec<u8>> for Literal<'a> {
 
     fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
         if Literal::verify(&value) {
-            Ok(Literal {
-                inner: Cow::Owned(value),
-            })
+            Ok(Literal(Cow::Owned(value)))
         } else {
             Err(())
         }
@@ -401,7 +383,7 @@ impl<'a> TryFrom<String> for Literal<'a> {
 
 impl<'a> AsRef<[u8]> for Literal<'a> {
     fn as_ref(&self) -> &[u8] {
-        &self.inner
+        &self.0
     }
 }
 
@@ -413,9 +395,7 @@ impl<'a> AsRef<[u8]> for Literal<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Quoted<'a> {
-    pub(crate) inner: Cow<'a, str>,
-}
+pub struct Quoted<'a>(pub(crate) Cow<'a, str>);
 
 impl<'a> Quoted<'a> {
     pub fn verify(value: &[u8]) -> bool {
@@ -423,7 +403,7 @@ impl<'a> Quoted<'a> {
     }
 
     pub fn inner(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 
     /// Create a quoted from a string without checking
@@ -438,7 +418,7 @@ impl<'a> Quoted<'a> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(inner.as_bytes()));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -467,9 +447,7 @@ impl<'a> TryFrom<&'a str> for Quoted<'a> {
 
     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
         if Quoted::verify(value.as_bytes()) {
-            Ok(Quoted {
-                inner: Cow::Borrowed(value),
-            })
+            Ok(Quoted(Cow::Borrowed(value)))
         } else {
             Err(())
         }
@@ -481,9 +459,7 @@ impl<'a> TryFrom<String> for Quoted<'a> {
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
         if Quoted::verify(value.as_bytes()) {
-            Ok(Quoted {
-                inner: Cow::Owned(value),
-            })
+            Ok(Quoted(Cow::Owned(value)))
         } else {
             Err(())
         }
@@ -492,7 +468,7 @@ impl<'a> TryFrom<String> for Quoted<'a> {
 
 impl<'a> AsRef<str> for Quoted<'a> {
     fn as_ref(&self) -> &str {
-        &self.inner
+        &self.0
     }
 }
 
@@ -503,11 +479,11 @@ impl<'a> AsRef<str> for Quoted<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct NString<'a> {
+pub struct NString<'a>(
     // This wrapper is merely used for formatting.
     // The inner value can be public.
-    pub inner: Option<IString<'a>>,
-}
+    pub Option<IString<'a>>,
+);
 
 /// Either an (extended) atom or a string.
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
@@ -637,9 +613,7 @@ impl<'a> AsRef<[u8]> for AString<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, PartialEq, Eq, Hash, Clone)]
-pub struct Tag<'a> {
-    pub(crate) inner: Cow<'a, str>,
-}
+pub struct Tag<'a>(pub(crate) Cow<'a, str>);
 
 impl<'a> Tag<'a> {
     pub fn verify(value: &[u8]) -> bool {
@@ -647,7 +621,7 @@ impl<'a> Tag<'a> {
     }
 
     pub fn inner(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 
     #[cfg(feature = "unchecked")]
@@ -655,7 +629,7 @@ impl<'a> Tag<'a> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(inner.as_bytes()));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -684,9 +658,7 @@ impl<'a> TryFrom<&'a str> for Tag<'a> {
 
     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Self {
-                inner: Cow::Borrowed(value),
-            })
+            Ok(Self(Cow::Borrowed(value)))
         } else {
             Err(())
         }
@@ -698,9 +670,7 @@ impl<'a> TryFrom<String> for Tag<'a> {
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Self {
-                inner: Cow::Owned(value),
-            })
+            Ok(Self(Cow::Owned(value)))
         } else {
             Err(())
         }
@@ -709,16 +679,14 @@ impl<'a> TryFrom<String> for Tag<'a> {
 
 impl<'a> AsRef<str> for Tag<'a> {
     fn as_ref(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 }
 
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, PartialEq, Eq, Hash, Clone)]
-pub struct Text<'a> {
-    pub(crate) inner: Cow<'a, str>,
-}
+pub struct Text<'a>(pub(crate) Cow<'a, str>);
 
 impl<'a> Text<'a> {
     pub fn verify(value: &[u8]) -> bool {
@@ -726,7 +694,7 @@ impl<'a> Text<'a> {
     }
 
     pub fn inner(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 
     #[cfg(feature = "unchecked")]
@@ -734,7 +702,7 @@ impl<'a> Text<'a> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(inner.as_bytes()));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -763,9 +731,7 @@ impl<'a> TryFrom<&'a str> for Text<'a> {
 
     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Self {
-                inner: Cow::Borrowed(value),
-            })
+            Ok(Self(Cow::Borrowed(value)))
         } else {
             Err(())
         }
@@ -777,9 +743,7 @@ impl<'a> TryFrom<String> for Text<'a> {
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
         if Self::verify(value.as_bytes()) {
-            Ok(Self {
-                inner: Cow::Owned(value),
-            })
+            Ok(Self(Cow::Owned(value)))
         } else {
             Err(())
         }
@@ -789,9 +753,7 @@ impl<'a> TryFrom<String> for Text<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Copy, Debug, PartialEq, Eq, Hash, Clone)]
-pub struct QuotedChar {
-    inner: char,
-}
+pub struct QuotedChar(char);
 
 impl QuotedChar {
     pub fn verify(input: char) -> bool {
@@ -802,7 +764,7 @@ impl QuotedChar {
     }
 
     pub fn inner(&self) -> char {
-        self.inner
+        self.0
     }
 
     #[cfg(feature = "unchecked")]
@@ -810,7 +772,7 @@ impl QuotedChar {
         #[cfg(debug_assertions)]
         assert!(Self::verify(inner));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -819,7 +781,7 @@ impl TryFrom<char> for QuotedChar {
 
     fn try_from(value: char) -> Result<Self, Self::Error> {
         if Self::verify(value) {
-            Ok(QuotedChar { inner: value })
+            Ok(QuotedChar(value))
         } else {
             Err(())
         }
@@ -901,9 +863,7 @@ impl<'a> AsRef<str> for Charset<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct NonEmptyVec<T> {
-    pub(crate) inner: Vec<T>,
-}
+pub struct NonEmptyVec<T>(pub(crate) Vec<T>);
 
 impl<T> NonEmptyVec<T> {
     pub fn verify(value: &[T]) -> bool {
@@ -915,7 +875,7 @@ impl<T> NonEmptyVec<T> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(&inner));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -924,7 +884,7 @@ impl<T> TryFrom<Vec<T>> for NonEmptyVec<T> {
 
     fn try_from(inner: Vec<T>) -> Result<Self, Self::Error> {
         if Self::verify(&inner) {
-            Ok(Self { inner })
+            Ok(Self(inner))
         } else {
             Err(())
         }
@@ -933,12 +893,12 @@ impl<T> TryFrom<Vec<T>> for NonEmptyVec<T> {
 
 impl<T> AsRef<[T]> for NonEmptyVec<T> {
     fn as_ref(&self) -> &[T] {
-        &self.inner
+        &self.0
     }
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::{convert::TryInto, str::from_utf8};
 
     use super::*;
@@ -950,23 +910,15 @@ mod test {
             (
                 b"A",
                 (
-                    Ok(Atom {
-                        inner: Cow::Borrowed("A"),
-                    }),
-                    Ok(Atom {
-                        inner: Cow::Owned("A".into()),
-                    }),
+                    Ok(Atom(Cow::Borrowed("A"))),
+                    Ok(Atom(Cow::Owned("A".into()))),
                 ),
             ),
             (
                 b"ABC",
                 (
-                    Ok(Atom {
-                        inner: Cow::Borrowed("ABC"),
-                    }),
-                    Ok(Atom {
-                        inner: Cow::Owned("ABC".into()),
-                    }),
+                    Ok(Atom(Cow::Borrowed("ABC"))),
+                    Ok(Atom(Cow::Owned("ABC".into()))),
                 ),
             ),
             (b" A", (Err(()), Err(()))),
@@ -1011,34 +963,22 @@ mod test {
             (
                 b"A",
                 (
-                    Ok(AtomExt {
-                        inner: Cow::Borrowed("A"),
-                    }),
-                    Ok(AtomExt {
-                        inner: Cow::Owned("A".into()),
-                    }),
+                    Ok(AtomExt(Cow::Borrowed("A"))),
+                    Ok(AtomExt(Cow::Owned("A".into()))),
                 ),
             ),
             (
                 b"ABC",
                 (
-                    Ok(AtomExt {
-                        inner: Cow::Borrowed("ABC"),
-                    }),
-                    Ok(AtomExt {
-                        inner: Cow::Owned("ABC".into()),
-                    }),
+                    Ok(AtomExt(Cow::Borrowed("ABC"))),
+                    Ok(AtomExt(Cow::Owned("ABC".into()))),
                 ),
             ),
             (
                 b"!partition/sda4",
                 (
-                    Ok(AtomExt {
-                        inner: Cow::Borrowed("!partition/sda4"),
-                    }),
-                    Ok(AtomExt {
-                        inner: Cow::Owned("!partition/sda4".into()),
-                    }),
+                    Ok(AtomExt(Cow::Borrowed("!partition/sda4"))),
+                    Ok(AtomExt(Cow::Owned("!partition/sda4".into()))),
                 ),
             ),
             (b" A", (Err(()), Err(()))),
@@ -1083,78 +1023,68 @@ mod test {
             (
                 b"A",
                 (
-                    Ok(AString::Atom(AtomExt {
-                        inner: Cow::Borrowed("A"),
-                    })),
-                    Ok(AString::Atom(AtomExt {
-                        inner: Cow::Owned("A".into()),
-                    })),
+                    Ok(AString::Atom(AtomExt(Cow::Borrowed("A")))),
+                    Ok(AString::Atom(AtomExt(Cow::Owned("A".into())))),
                 ),
             ),
             (
                 b"ABC",
                 (
-                    Ok(AString::Atom(AtomExt {
-                        inner: Cow::Borrowed("ABC"),
-                    })),
-                    Ok(AString::Atom(AtomExt {
-                        inner: Cow::Owned("ABC".into()),
-                    })),
+                    Ok(AString::Atom(AtomExt(Cow::Borrowed("ABC")))),
+                    Ok(AString::Atom(AtomExt(Cow::Owned("ABC".into())))),
                 ),
             ),
             (
                 b"",
                 (
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Borrowed(""),
-                    }))),
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Owned("".to_owned()),
-                    }))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed(""))))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Owned(
+                        "".to_owned(),
+                    ))))),
                 ),
             ),
             (
                 b" A",
                 (
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Borrowed(" A"),
-                    }))),
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Owned(" A".to_owned()),
-                    }))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed(
+                        " A",
+                    ))))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Owned(
+                        " A".to_owned(),
+                    ))))),
                 ),
             ),
             (
                 b"A ",
                 (
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Borrowed("A "),
-                    }))),
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Owned("A ".to_owned()),
-                    }))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed(
+                        "A ",
+                    ))))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Owned(
+                        "A ".to_owned(),
+                    ))))),
                 ),
             ),
             (
                 b"\"",
                 (
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Borrowed("\""),
-                    }))),
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Owned("\"".to_owned()),
-                    }))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed(
+                        "\"",
+                    ))))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Owned(
+                        "\"".to_owned(),
+                    ))))),
                 ),
             ),
             (
                 b"\\\"",
                 (
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Borrowed("\\\""),
-                    }))),
-                    Ok(AString::String(IString::Quoted(Quoted {
-                        inner: Cow::Owned("\\\"".to_owned()),
-                    }))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed(
+                        "\\\"",
+                    ))))),
+                    Ok(AString::String(IString::Quoted(Quoted(Cow::Owned(
+                        "\\\"".to_owned(),
+                    ))))),
                 ),
             ),
             (b"A\x00", (Err(()), Err(()))),
@@ -1194,16 +1124,16 @@ mod test {
     fn test_istring() {
         assert_eq!(
             IString::try_from("AAA").unwrap(),
-            IString::Quoted("AAA".try_into().unwrap()).into()
+            IString::Quoted("AAA".try_into().unwrap())
         );
         assert_eq!(
             IString::try_from("\"AAA").unwrap(),
-            IString::Quoted("\"AAA".try_into().unwrap()).into()
+            IString::Quoted("\"AAA".try_into().unwrap())
         );
 
         assert_ne!(
             IString::try_from("\"AAA").unwrap(),
-            IString::Quoted("\\\"AAA".try_into().unwrap()).into()
+            IString::Quoted("\\\"AAA".try_into().unwrap())
         );
     }
 
diff --git a/imap-types/src/rfc3501/mailbox.rs b/imap-types/src/rfc3501/mailbox.rs
index b43d399e..bb62b275 100644
--- a/imap-types/src/rfc3501/mailbox.rs
+++ b/imap-types/src/rfc3501/mailbox.rs
@@ -20,9 +20,7 @@ use crate::{
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct ListCharString<'a> {
-    pub(crate) inner: Cow<'a, str>,
-}
+pub struct ListCharString<'a>(pub(crate) Cow<'a, str>);
 
 impl<'a> ListCharString<'a> {
     pub fn verify(value: &str) -> bool {
@@ -34,7 +32,7 @@ impl<'a> ListCharString<'a> {
         #[cfg(debug_assertions)]
         assert!(Self::verify(&inner));
 
-        Self { inner }
+        Self(inner)
     }
 }
 
@@ -43,9 +41,7 @@ impl<'a> TryFrom<&'a str> for ListCharString<'a> {
 
     fn try_from(value: &'a str) -> Result<Self, Self::Error> {
         if Self::verify(value) {
-            Ok(Self {
-                inner: Cow::Borrowed(value),
-            })
+            Ok(Self(Cow::Borrowed(value)))
         } else {
             Err(())
         }
@@ -57,9 +53,7 @@ impl<'a> TryFrom<String> for ListCharString<'a> {
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
         if Self::verify(&value) {
-            Ok(Self {
-                inner: Cow::Owned(value),
-            })
+            Ok(Self(Cow::Owned(value)))
         } else {
             Err(())
         }
@@ -68,7 +62,7 @@ impl<'a> TryFrom<String> for ListCharString<'a> {
 
 impl<'a> AsRef<[u8]> for ListCharString<'a> {
     fn as_ref(&self) -> &[u8] {
-        self.inner.as_bytes()
+        self.0.as_bytes()
     }
 }
 
@@ -177,9 +171,7 @@ impl<'a> From<AString<'a>> for Mailbox<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct MailboxOther<'a> {
-    pub(crate) inner: AString<'a>,
-}
+pub struct MailboxOther<'a>(pub(crate) AString<'a>);
 
 impl_try_from_try_from!(AString, 'a, &'a [u8], MailboxOther<'a>);
 impl_try_from_try_from!(AString, 'a, Vec<u8>, MailboxOther<'a>);
@@ -196,18 +188,18 @@ impl<'a> TryFrom<AString<'a>> for MailboxOther<'a> {
             }
         }
 
-        Ok(MailboxOther { inner: mailbox })
+        Ok(MailboxOther(mailbox))
     }
 }
 
 impl<'a> AsRef<[u8]> for MailboxOther<'a> {
     fn as_ref(&self) -> &[u8] {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::{borrow::Cow, convert::TryFrom};
 
     use crate::{
@@ -225,11 +217,9 @@ mod test {
             ("INBOX", Mailbox::Inbox),
             (
                 "INBO²",
-                Mailbox::Other(MailboxOther {
-                    inner: AString::String(IString::Literal(Literal {
-                        inner: Cow::Borrowed("INBO²".as_bytes()),
-                    })),
-                }),
+                Mailbox::Other(MailboxOther(AString::String(IString::Literal(Literal(
+                    Cow::Borrowed("INBO²".as_bytes()),
+                ))))),
             ),
         ];
 
diff --git a/imap-types/src/rfc3501/mod.rs b/imap-types/src/rfc3501/mod.rs
index 791fecb4..56e7d574 100644
--- a/imap-types/src/rfc3501/mod.rs
+++ b/imap-types/src/rfc3501/mod.rs
@@ -50,7 +50,7 @@ impl<'a> From<Atom<'a>> for AuthMechanism<'a> {
         match inner.as_ref().to_ascii_lowercase().as_str() {
             "plain" => AuthMechanism::Plain,
             "login" => AuthMechanism::Login,
-            _ => AuthMechanism::Other(AuthMechanismOther { inner }),
+            _ => AuthMechanism::Other(AuthMechanismOther(inner)),
         }
     }
 }
@@ -58,13 +58,11 @@ impl<'a> From<Atom<'a>> for AuthMechanism<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct AuthMechanismOther<'a> {
-    inner: Atom<'a>,
-}
+pub struct AuthMechanismOther<'a>(Atom<'a>);
 
 impl<'a> AuthMechanismOther<'a> {
     pub fn inner(&self) -> &Atom<'a> {
-        &self.inner
+        &self.0
     }
 }
 
@@ -79,13 +77,13 @@ impl<'a> TryFrom<Atom<'a>> for AuthMechanismOther<'a> {
     fn try_from(atom: Atom<'a>) -> Result<Self, ()> {
         match atom.as_ref().to_ascii_lowercase().as_ref() {
             "plain" | "login" => Err(()),
-            _ => Ok(Self { inner: atom }),
+            _ => Ok(Self(atom)),
         }
     }
 }
 
 impl<'a> AsRef<str> for AuthMechanismOther<'a> {
     fn as_ref(&self) -> &str {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 }
diff --git a/imap-types/src/rfc3501/response.rs b/imap-types/src/rfc3501/response.rs
index 1b546a3d..be214d62 100644
--- a/imap-types/src/rfc3501/response.rs
+++ b/imap-types/src/rfc3501/response.rs
@@ -814,9 +814,7 @@ impl<'a> Code<'a> {
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct CodeOther<'a> {
-    inner: Cow<'a, [u8]>,
-}
+pub struct CodeOther<'a>(Cow<'a, [u8]>);
 
 impl<'a> CodeOther<'a> {
     #[cfg(feature = "unchecked")]
@@ -824,11 +822,11 @@ impl<'a> CodeOther<'a> {
     where
         D: Into<Cow<'a, [u8]>>,
     {
-        Self { inner: data.into() }
+        Self(data.into())
     }
 
     pub fn inner(&self) -> &[u8] {
-        self.inner.as_ref()
+        self.0.as_ref()
     }
 }
 
@@ -956,9 +954,7 @@ impl<'a> From<Atom<'a>> for Capability<'a> {
                     }
                 }
 
-                Self::Other(CapabilityOther {
-                    inner: Atom { inner: cow },
-                })
+                Self::Other(CapabilityOther { inner: Atom(cow) })
             }
         }
     }
@@ -983,7 +979,7 @@ impl<'a> CapabilityOther<'a> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::convert::TryFrom;
 
     use super::*;
diff --git a/imap-types/src/rfc3501/sequence.rs b/imap-types/src/rfc3501/sequence.rs
index abe8fad6..2ad4f7dd 100644
--- a/imap-types/src/rfc3501/sequence.rs
+++ b/imap-types/src/rfc3501/sequence.rs
@@ -256,7 +256,7 @@ impl<'a> Iterator for SequenceSetIterNaive<'a> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::{
         convert::{TryFrom, TryInto},
         num::NonZeroU32,
@@ -405,7 +405,7 @@ mod test {
             ),
         ];
 
-        for (test, expected) in tests.into_iter() {
+        for (test, expected) in tests.iter() {
             let got = SequenceSet::try_from(*test).unwrap();
             assert_eq!(*expected, got);
         }
diff --git a/imap-types/src/security.rs b/imap-types/src/security.rs
new file mode 100644
index 00000000..b86c723d
--- /dev/null
+++ b/imap-types/src/security.rs
@@ -0,0 +1,225 @@
+use std::fmt::{Debug, Formatter};
+
+#[cfg(feature = "arbitrary")]
+use arbitrary::Arbitrary;
+#[cfg(feature = "bounded-static")]
+use bounded_static::ToStatic;
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+use subtle::ConstantTimeEq;
+
+#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
+#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
+#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
+// Note: The implementation of these traits does agree:
+//       `PartialEq` is just a thin wrapper that ensures constant-time comparison.
+#[allow(clippy::derived_hash_with_manual_eq)]
+#[derive(Clone, Hash)]
+pub struct Secret<T>(T);
+
+impl<T> Secret<T> {
+    pub fn new(inner: T) -> Self {
+        Self(inner)
+    }
+
+    pub fn expose_secret(&self) -> &T {
+        &self.0
+    }
+}
+
+impl<S> Secret<S>
+where
+    S: AsRef<[u8]>,
+{
+    pub fn compare_ct<O>(&self, other: O) -> bool
+    where
+        O: AsRef<[u8]>,
+    {
+        self.0.as_ref().ct_eq(other.as_ref()).unwrap_u8() == 1
+    }
+}
+
+impl<T> Debug for Secret<T> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "[[REDACTED]]")
+    }
+}
+
+impl<T> Eq for Secret<T> where T: AsRef<[u8]> {}
+
+impl<T> PartialEq for Secret<T>
+where
+    T: AsRef<[u8]>,
+{
+    fn eq(&self, other: &Self) -> bool {
+        self.0.as_ref().ct_eq(other.0.as_ref()).unwrap_u8() == 1
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{command::CommandBody, security::Secret};
+
+    #[test]
+    #[allow(clippy::redundant_clone)]
+    fn test_that_secret_is_redacted() {
+        let secret = Secret("xyz123");
+        let got = format!("{:?}", secret);
+        println!("{}", got);
+        assert!(!got.contains("xyz123"));
+
+        let cmd = CommandBody::login("alice", "xyz123")
+            .unwrap()
+            .tag("A")
+            .unwrap();
+        let got = format!("{:?}", cmd);
+        println!("{}", got);
+        assert!(!got.contains("xyz123"));
+    }
+
+    /// A best effort test to ensure that constant-time comparison works.
+    #[test]
+    #[allow(clippy::redundant_clone)]
+    fn test_that_eq_is_constant_time() {
+        let took_constant = {
+            fn compare_eq(a: Secret<String>, b: Secret<String>) -> u128 {
+                let tik = std::time::Instant::now();
+                assert_eq!(a, b);
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            fn compare_ne(a: Secret<String>, b: Secret<String>) -> u128 {
+                let tik = std::time::Instant::now();
+                assert_ne!(a, b);
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            let a = Secret::new(str::repeat("A", 4096));
+            let b = Secret::new(str::repeat("B", 4096));
+
+            let took1 = compare_eq(a.clone(), a.clone());
+            println!("{}", took1);
+            let took2 = compare_ne(a.clone(), b.clone());
+            println!("{}", took2);
+            let took3 = compare_ne(b.clone(), a.clone());
+            println!("{}", took3);
+            let took4 = compare_eq(b.clone(), b.clone());
+            println!("{}", took4);
+
+            (took1 + took2 + took3 + took4) / 4
+        };
+
+        let took_variable = {
+            fn compare_eq(a: String, b: String) -> u128 {
+                let tik = std::time::Instant::now();
+                assert_eq!(a, b);
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            fn compare_ne(a: String, b: String) -> u128 {
+                let tik = std::time::Instant::now();
+                assert_ne!(a, b);
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            let a = str::repeat("A", 4096);
+            let b = str::repeat("B", 4096);
+
+            let took1 = compare_eq(a.clone(), a.clone());
+            println!("{}", took1);
+            let took2 = compare_ne(a.clone(), b.clone());
+            println!("{}", took2);
+            let took3 = compare_ne(b.clone(), a.clone());
+            println!("{}", took3);
+            let took4 = compare_eq(b.clone(), b.clone());
+            println!("{}", took4);
+
+            (took1 + took2 + took3 + took4) / 4
+        };
+
+        let times = took_constant / took_variable;
+        println!("{took_constant} vs {took_variable} ({times} times slower)");
+        assert!(times > 100);
+    }
+
+    /// A best effort test to ensure that constant-time comparison works.
+    #[test]
+    #[allow(clippy::redundant_clone)]
+    fn test_that_compare_ct_is_constant_time() {
+        let took_constant = {
+            fn compare_eq(a: Secret<String>, b: Secret<String>) -> u128 {
+                let tik = std::time::Instant::now();
+                assert!(a.compare_ct(b.expose_secret()));
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            fn compare_ne(a: Secret<String>, b: Secret<String>) -> u128 {
+                let tik = std::time::Instant::now();
+                assert!(!a.compare_ct(b.expose_secret()));
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            let a = Secret::new(str::repeat("A", 4096));
+            let b = Secret::new(str::repeat("B", 4096));
+
+            let took1 = compare_eq(a.clone(), a.clone());
+            println!("{}", took1);
+            let took2 = compare_ne(a.clone(), b.clone());
+            println!("{}", took2);
+            let took3 = compare_ne(b.clone(), a.clone());
+            println!("{}", took3);
+            let took4 = compare_eq(b.clone(), b.clone());
+            println!("{}", took4);
+
+            (took1 + took2 + took3 + took4) / 4
+        };
+
+        let took_variable = {
+            fn compare_eq(a: String, b: String) -> u128 {
+                let tik = std::time::Instant::now();
+                assert_eq!(a, b);
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            fn compare_ne(a: String, b: String) -> u128 {
+                let tik = std::time::Instant::now();
+                assert_ne!(a, b);
+                let tok = std::time::Instant::now();
+
+                tok.duration_since(tik).as_nanos()
+            }
+
+            let a = str::repeat("A", 4096);
+            let b = str::repeat("B", 4096);
+
+            let took1 = compare_eq(a.clone(), a.clone());
+            println!("{}", took1);
+            let took2 = compare_ne(a.clone(), b.clone());
+            println!("{}", took2);
+            let took3 = compare_ne(b.clone(), a.clone());
+            println!("{}", took3);
+            let took4 = compare_eq(b.clone(), b.clone());
+            println!("{}", took4);
+
+            (took1 + took2 + took3 + took4) / 4
+        };
+
+        let times = took_constant / took_variable;
+        println!("{took_constant} vs {took_variable} ({times} times slower)");
+        assert!(times > 100);
+    }
+}
diff --git a/imap-types/src/utils.rs b/imap-types/src/utils.rs
index 7bcf31d7..5fc9d436 100644
--- a/imap-types/src/utils.rs
+++ b/imap-types/src/utils.rs
@@ -113,7 +113,7 @@ pub fn unescape_quoted(escaped: &str) -> Cow<str> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::*;
 
     #[test]
diff --git a/src/codec.rs b/src/codec.rs
index 105904ce..b361b0d6 100644
--- a/src/codec.rs
+++ b/src/codec.rs
@@ -99,7 +99,7 @@ impl<'a> Decode<'a> for Response<'a> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::{convert::TryFrom, num::NonZeroU32};
 
     use imap_types::{
@@ -219,11 +219,9 @@ mod test {
                     Response::Data(Data::Fetch {
                         seq_or_uid: NonZeroU32::new(1).unwrap(),
                         attributes: NonEmptyVec::try_from(vec![FetchAttributeValue::Rfc822(
-                            NString {
-                                inner: Some(IString::Literal(
-                                    Literal::try_from(b"hello".as_ref()).unwrap(),
-                                )),
-                            },
+                            NString(Some(IString::Literal(
+                                Literal::try_from(b"hello".as_ref()).unwrap(),
+                            ))),
                         )])
                         .unwrap(),
                     }),
diff --git a/src/extensions/rfc9208.rs b/src/extensions/rfc9208.rs
index 0dce26a5..c82b64fd 100644
--- a/src/extensions/rfc9208.rs
+++ b/src/extensions/rfc9208.rs
@@ -191,7 +191,7 @@ pub fn setquota_resource(input: &[u8]) -> IResult<&[u8], QuotaSet> {
 // }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::convert::TryInto;
 
     use imap_types::{
diff --git a/src/rfc3501/address.rs b/src/rfc3501/address.rs
index ce8526b4..a5f5edb7 100644
--- a/src/rfc3501/address.rs
+++ b/src/rfc3501/address.rs
@@ -75,7 +75,7 @@ pub fn addr_host(input: &[u8]) -> IResult<&[u8], NString> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::convert::TryInto;
 
     use imap_types::core::{IString, NString};
@@ -88,14 +88,12 @@ mod test {
         assert_eq!(
             val,
             Address {
-                name: NString { inner: None },
-                adl: NString {
-                    inner: Some(IString::Literal(b"xxx".as_slice().try_into().unwrap()))
-                },
-                mailbox: NString {
-                    inner: Some(IString::Quoted("xxx".try_into().unwrap()))
-                },
-                host: NString { inner: None },
+                name: NString(None),
+                adl: NString(Some(IString::Literal(
+                    b"xxx".as_slice().try_into().unwrap()
+                ))),
+                mailbox: NString(Some(IString::Quoted("xxx".try_into().unwrap()))),
+                host: NString(None),
             }
         );
         assert_eq!(rem, b"");
diff --git a/src/rfc3501/body.rs b/src/rfc3501/body.rs
index db2bc2fe..d9e18e48 100644
--- a/src/rfc3501/body.rs
+++ b/src/rfc3501/body.rs
@@ -349,7 +349,7 @@ pub fn body_fld_dsp(input: &[u8]) -> IResult<&[u8], Option<(IString, Vec<(IStrin
 /// `body-fld-lang = nstring / "(" string *(SP string) ")"`
 pub fn body_fld_lang(input: &[u8]) -> IResult<&[u8], Vec<IString>> {
     alt((
-        map(nstring, |nstring| match nstring.inner {
+        map(nstring, |nstring| match nstring.0 {
             Some(item) => vec![item],
             None => vec![],
         }),
@@ -561,7 +561,7 @@ pub fn media_text(input: &[u8]) -> IResult<&[u8], IString> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::*;
 
     #[test]
diff --git a/src/rfc3501/command.rs b/src/rfc3501/command.rs
index ffc2486b..d520a77e 100644
--- a/src/rfc3501/command.rs
+++ b/src/rfc3501/command.rs
@@ -11,6 +11,7 @@ use imap_types::{
     },
     core::{AString, NonEmptyVec},
     message::{AuthMechanism, Flag},
+    security::Secret,
 };
 use nom::{
     branch::alt,
@@ -305,7 +306,13 @@ pub fn login(input: &[u8]) -> IResult<&[u8], CommandBody> {
 
     let (remaining, (_, _, username, _, password)) = parser(input)?;
 
-    Ok((remaining, CommandBody::Login { username, password }))
+    Ok((
+        remaining,
+        CommandBody::Login {
+            username,
+            password: Secret::new(password),
+        },
+    ))
 }
 
 #[inline]
@@ -353,15 +360,17 @@ pub fn authenticate(input: &[u8]) -> IResult<&[u8], AuthMechanism> {
 ///                CRLF is parsed by upper command parser.
 /// ```
 #[cfg(feature = "ext_sasl_ir")]
-pub fn authenticate_sasl_ir(input: &[u8]) -> IResult<&[u8], (AuthMechanism, Option<Cow<[u8]>>)> {
+pub fn authenticate_sasl_ir(
+    input: &[u8],
+) -> IResult<&[u8], (AuthMechanism, Option<Secret<Cow<[u8]>>>)> {
     let mut parser = tuple((
         tag_no_case(b"AUTHENTICATE "),
         auth_type,
         opt(preceded(
             SP,
             alt((
-                map(base64, Cow::Owned),
-                value(Cow::Borrowed(&b""[..]), tag("=")),
+                map(base64, |data| Secret::new(Cow::Owned(data))),
+                value(Secret::new(Cow::Borrowed(&b""[..])), tag("=")),
             )),
         )),
     ));
@@ -384,7 +393,9 @@ pub fn authenticate_sasl_ir(input: &[u8]) -> IResult<&[u8], (AuthMechanism, Opti
 ///                FIXME: Multiline base64 currently does not work.
 /// ```
 pub fn authenticate_data(input: &[u8]) -> IResult<&[u8], AuthenticateData> {
-    map(terminated(base64, CRLF), |data| AuthenticateData { data })(input) // FIXME: many0 deleted
+    map(terminated(base64, CRLF), |data| {
+        AuthenticateData(Secret::new(data))
+    })(input) // FIXME: many0 deleted
 }
 
 // # Command Select
@@ -747,7 +758,7 @@ fn search_key_limited<'a>(
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::{
         convert::{TryFrom, TryInto},
         num::NonZeroU32,
diff --git a/src/rfc3501/core.rs b/src/rfc3501/core.rs
index adbdac4d..7f8fa662 100644
--- a/src/rfc3501/core.rs
+++ b/src/rfc3501/core.rs
@@ -247,8 +247,8 @@ pub fn atom(input: &[u8]) -> IResult<&[u8], Atom> {
 /// `nstring = string / nil`
 pub fn nstring(input: &[u8]) -> IResult<&[u8], NString> {
     alt((
-        map(string, |item| NString { inner: Some(item) }),
-        map(nil, |_| NString { inner: None }),
+        map(string, |item| NString(Some(item))),
+        map(nil, |_| NString(None)),
     ))(input)
 }
 
@@ -324,7 +324,7 @@ pub fn tag_imap(input: &[u8]) -> IResult<&[u8], Tag> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::convert::TryInto;
 
     use imap_types::core::Quoted;
diff --git a/src/rfc3501/datetime.rs b/src/rfc3501/datetime.rs
index 493b3edf..5ddf92f2 100644
--- a/src/rfc3501/datetime.rs
+++ b/src/rfc3501/datetime.rs
@@ -207,7 +207,7 @@ fn digit_4(input: &[u8]) -> IResult<&[u8], u16> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::str::from_utf8;
 
     use super::*;
diff --git a/src/rfc3501/mailbox.rs b/src/rfc3501/mailbox.rs
index b26f35d4..5d771402 100644
--- a/src/rfc3501/mailbox.rs
+++ b/src/rfc3501/mailbox.rs
@@ -147,7 +147,7 @@ pub fn mailbox_list(
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::*;
 
     #[test]
diff --git a/src/rfc3501/mod.rs b/src/rfc3501/mod.rs
index c14ef306..7f51a52c 100644
--- a/src/rfc3501/mod.rs
+++ b/src/rfc3501/mod.rs
@@ -29,7 +29,7 @@ pub fn auth_type(input: &[u8]) -> IResult<&[u8], AuthMechanism> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::convert::TryFrom;
 
     use imap_types::message::{AuthMechanism, AuthMechanismOther};
diff --git a/src/rfc3501/response.rs b/src/rfc3501/response.rs
index 7c7f08aa..aa74a619 100644
--- a/src/rfc3501/response.rs
+++ b/src/rfc3501/response.rs
@@ -356,7 +356,7 @@ pub fn message_data(input: &[u8]) -> IResult<&[u8], Data> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::*;
 
     #[test]
diff --git a/src/rfc3501/sequence.rs b/src/rfc3501/sequence.rs
index 5a80032a..9756d298 100644
--- a/src/rfc3501/sequence.rs
+++ b/src/rfc3501/sequence.rs
@@ -83,7 +83,7 @@ pub fn seq_number(input: &[u8]) -> IResult<&[u8], SeqNo> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::convert::TryInto;
 
     use super::*;
diff --git a/src/tokio_compat/mod.rs b/src/tokio_compat/mod.rs
index 4e46ddd0..fd6bbd94 100644
--- a/src/tokio_compat/mod.rs
+++ b/src/tokio_compat/mod.rs
@@ -78,7 +78,7 @@ fn parse_literal_enclosing(line: &[u8]) -> Result<Option<&[u8]>, LiteralError> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::*;
 
     #[test]
diff --git a/src/tokio_compat/server.rs b/src/tokio_compat/server.rs
index 4b19e36a..0830ba5a 100644
--- a/src/tokio_compat/server.rs
+++ b/src/tokio_compat/server.rs
@@ -186,13 +186,14 @@ impl<'a> Encoder<&Response<'a>> for ImapServerCodec {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::convert::TryFrom;
 
     use bytes::BytesMut;
     use imap_types::{
         command::{Command, CommandBody},
         core::{AString, AtomExt, IString, Literal},
+        security::Secret,
     };
     use tokio_util::codec::Decoder;
 
@@ -256,7 +257,9 @@ mod test {
                             username: AString::String(IString::Literal(
                                 Literal::try_from(b"alice".as_ref()).unwrap(),
                             )),
-                            password: AString::Atom(AtomExt::try_from("password").unwrap()),
+                            password: Secret::new(AString::Atom(
+                                AtomExt::try_from("password").unwrap(),
+                            )),
                         },
                     )
                     .unwrap(),
diff --git a/src/utils.rs b/src/utils.rs
index e80797f2..a972e328 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -52,7 +52,7 @@ pub fn unescape_quoted(escaped: &str) -> Cow<str> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::*;
 
     #[test]


Test patch:
diff --git a/tests/trace.rs b/tests/trace.rs
index 83eb8a9a..63e585a7 100644
--- a/tests/trace.rs
+++ b/tests/trace.rs
@@ -13,7 +13,7 @@ use imap_codec::{
         Code, Data, Response, Status,
     },
 };
-use imap_types::response::Greeting;
+use imap_types::{response::Greeting, security::Secret};
 
 enum Who {
     Client,
@@ -106,28 +106,57 @@ fn test_lines_of_trace(trace: &[u8]) {
 
 fn test_trace_known_positive(tests: Vec<(&[u8], Message)>) {
     for (test, expected) in tests.into_iter() {
+        println!("// {}", std::str::from_utf8(test).unwrap().trim());
         match expected {
             Message::Command(expected) => {
                 let (rem, got) = Command::decode(test).unwrap();
                 assert!(rem.is_empty());
                 assert_eq!(expected, got);
+                println!("{:?}", got);
+                println!(
+                    "// {}",
+                    String::from_utf8(got.encode_detached().unwrap())
+                        .unwrap()
+                        .trim()
+                );
             }
             Message::Response(expected) => {
                 let (rem, got) = Response::decode(test).unwrap();
                 assert!(rem.is_empty());
                 assert_eq!(expected, got);
+                println!("{:?}", got);
+                println!(
+                    "// {}",
+                    String::from_utf8(got.encode_detached().unwrap())
+                        .unwrap()
+                        .trim()
+                );
             }
             Message::CommandSkip => {
                 let (rem, got) = Command::decode(test).unwrap();
                 assert!(rem.is_empty());
                 println!("{:?}", got);
+                println!(
+                    "// {}",
+                    String::from_utf8(got.encode_detached().unwrap())
+                        .unwrap()
+                        .trim()
+                );
             }
             Message::ResponseSkip => {
                 let (rem, got) = Response::decode(test).unwrap();
                 assert!(rem.is_empty());
                 println!("{:?}", got);
+                println!(
+                    "// {}",
+                    String::from_utf8(got.encode_detached().unwrap())
+                        .unwrap()
+                        .trim()
+                );
             }
-        }
+        };
+
+        println!();
     }
 }
 
@@ -381,7 +410,7 @@ fn test_from_login() {
                                 Quoted::try_from("SMITH").unwrap(),
                             )),
                             // ... and an atom (knowing that `AString::try_from(...)` will create it.
-                            password: AString::try_from("SESAME").unwrap(),
+                            password: Secret::new(AString::try_from("SESAME").unwrap()),
                         },
                     )
                     .unwrap(),
@@ -865,22 +894,22 @@ fn test_transcript_from_rfc() {
                 )),
             ),
             (
-                b"001 login mrc secret\r\n",
+                b"a001 login mrc secret\r\n",
                 Message::Command(
-                    Command::new("001", CommandBody::login("mrc", "secret").unwrap()).unwrap(),
+                    Command::new("a001", CommandBody::login("mrc", "secret").unwrap()).unwrap(),
                 ),
             ),
             (
-                b"001 OK LOGIN completed\r\n",
+                b"a001 OK LOGIN completed\r\n",
                 Message::Response(Response::Status(
-                    Status::ok(Some(Tag::try_from("001").unwrap()), None, "LOGIN completed")
+                    Status::ok(Some(Tag::try_from("a001").unwrap()), None, "LOGIN completed")
                         .unwrap(),
                 )),
             ),
             (
-                b"002 select inbox\r\n",
+                b"a002 select inbox\r\n",
                 Message::Command(
-                    Command::new("002", CommandBody::select("inbox").unwrap()).unwrap(),
+                    Command::new("a002", CommandBody::select("inbox").unwrap()).unwrap(),
                 ),
             ),
             (
@@ -922,34 +951,34 @@ fn test_transcript_from_rfc() {
                                  .unwrap(),
                          )),
             ),
-                        (b"002 OK [READ-WRITE] SELECT completed\r\n", Message::Response(Response::Status(Status::ok(Some(Tag::try_from("002").unwrap()), Some(Code::ReadWrite), "SELECT completed").unwrap()))),
-                        (b"003 fetch 12 full\r\n", Message::Command(Command::new("003", CommandBody::fetch("12", Macro::Full,false).unwrap()).unwrap())),
+                        (b"a002 OK [READ-WRITE] SELECT completed\r\n", Message::Response(Response::Status(Status::ok(Some(Tag::try_from("a002").unwrap()), Some(Code::ReadWrite), "SELECT completed").unwrap()))),
+                        (b"a003 fetch 12 full\r\n", Message::Command(Command::new("a003", CommandBody::fetch("12", Macro::Full,false).unwrap()).unwrap())),
                             // FIXME
                         (b"* 12 FETCH (FLAGS (\\Seen) INTERNALDATE \"17-Jul-1996 02:44:25 -0700\" RFC822.SIZE 4286 ENVELOPE (\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\")(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"<B27397-0100000@cac.washington.edu>\") BODY (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 3028 92))\r\n", Message::ResponseSkip),
-                        (b"003 OK FETCH completed\r\n", Message::Response(Response::Status(Status::ok(Some(Tag::try_from("003").unwrap()), None, "FETCH completed").unwrap()))),
-                        (b"004 fetch 12 body[header]\r\n", Message::Command(Command::new("004", CommandBody::fetch("12", vec![FetchAttribute::BodyExt {
+                        (b"a003 OK FETCH completed\r\n", Message::Response(Response::Status(Status::ok(Some(Tag::try_from("a003").unwrap()), None, "FETCH completed").unwrap()))),
+                        (b"a004 fetch 12 body[header]\r\n", Message::Command(Command::new("a004", CommandBody::fetch("12", vec![FetchAttribute::BodyExt {
                             section: Some(Section::Header(None)),
                             peek: false,
                             partial: None,
                         }], false).unwrap()).unwrap())),
-            //             (b"* 12 FETCH (BODY[HEADER] {342}\r
-            // Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\r
-            // From: Terry Gray <gray@cac.washington.edu>\r
-            // Subject: IMAP4rev1 WG mtg summary and minutes\r
-            // To: imap@cac.washington.edu\r
-            // cc: minutes@CNRI.Reston.VA.US, John Klensin <KLENSIN@MIT.EDU>\r
-            // Message-Id: <B27397-0100000@cac.washington.edu>\r
-            // MIME-Version: 1.0\r
-            // Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r
-            // \r
-            // )\r\n", Message::Response()),
-                        (b"004 OK FETCH completed\r\n", Message::ResponseSkip),
-                        (b"005 store 12 +flags \\deleted\r\n", Message::CommandSkip),
+                         (b"* 12 FETCH (BODY[HEADER] {342}\r
+Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\r
+From: Terry Gray <gray@cac.washington.edu>\r
+Subject: IMAP4rev1 WG mtg summary and minutes\r
+To: imap@cac.washington.edu\r
+cc: minutes@CNRI.Reston.VA.US, John Klensin <KLENSIN@MIT.EDU>\r
+Message-Id: <B27397-0100000@cac.washington.edu>\r
+MIME-Version: 1.0\r
+Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r
+\r
+)\r\n", Message::ResponseSkip),
+                        (b"a004 OK FETCH completed\r\n", Message::ResponseSkip),
+                        (b"a005 store 12 +flags \\deleted\r\n", Message::CommandSkip),
                         (b"* 12 FETCH (FLAGS (\\Seen \\Deleted))\r\n", Message::ResponseSkip),
-                        (b"005 OK +FLAGS completed\r\n", Message::ResponseSkip),
-                        (b"006 logout\r\n", Message::CommandSkip),
+                        (b"a005 OK +FLAGS completed\r\n", Message::ResponseSkip),
+                        (b"a006 logout\r\n", Message::CommandSkip),
                         (b"* BYE IMAP4rev1 server terminating connection\r\n", Message::ResponseSkip),
-                        (b"006 OK LOGOUT completed\r\n", Message::ResponseSkip),
+                        (b"a006 OK LOGOUT completed\r\n", Message::ResponseSkip),
         ]
     };