imap_types/
mailbox.rs

1//! Mailbox-related types.
2
3use std::{borrow::Cow, str::from_utf8};
4
5#[cfg(feature = "arbitrary")]
6use arbitrary::Arbitrary;
7#[cfg(feature = "bounded-static")]
8use bounded_static::ToStatic;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    core::{impl_try_from, AString, IString},
14    error::{ValidationError, ValidationErrorKind},
15    mailbox::error::MailboxOtherError,
16    utils::indicators::is_list_char,
17};
18
19#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct ListCharString<'a>(pub(crate) Cow<'a, str>);
23
24impl<'a> ListCharString<'a> {
25    pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> {
26        let value = value.as_ref();
27
28        if value.is_empty() {
29            return Err(ValidationError::new(ValidationErrorKind::Empty));
30        }
31
32        if let Some(at) = value.iter().position(|b| !is_list_char(*b)) {
33            return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt {
34                byte: value[at],
35                at,
36            }));
37        };
38
39        Ok(())
40    }
41
42    /// Constructs a list char string without validation.
43    ///
44    /// # Warning: IMAP conformance
45    ///
46    /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do
47    /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows.
48    /// Do not call this constructor with untrusted data.
49    #[cfg(feature = "unvalidated")]
50    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
51    pub fn unvalidated<C>(inner: C) -> Self
52    where
53        C: Into<Cow<'a, str>>,
54    {
55        let inner = inner.into();
56
57        #[cfg(debug_assertions)]
58        Self::validate(inner.as_bytes()).unwrap();
59
60        Self(inner)
61    }
62}
63
64impl<'a> TryFrom<&'a str> for ListCharString<'a> {
65    type Error = ValidationError;
66
67    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
68        Self::validate(value)?;
69
70        Ok(Self(Cow::Borrowed(value)))
71    }
72}
73
74impl<'a> TryFrom<String> for ListCharString<'a> {
75    type Error = ValidationError;
76
77    fn try_from(value: String) -> Result<Self, Self::Error> {
78        Self::validate(&value)?;
79
80        Ok(Self(Cow::Owned(value)))
81    }
82}
83
84impl<'a> AsRef<[u8]> for ListCharString<'a> {
85    fn as_ref(&self) -> &[u8] {
86        self.0.as_bytes()
87    }
88}
89
90#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
91#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
92#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub enum ListMailbox<'a> {
95    Token(ListCharString<'a>),
96    String(IString<'a>),
97}
98
99impl<'a> TryFrom<&'a str> for ListMailbox<'a> {
100    type Error = ValidationError;
101
102    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
103        if s.is_empty() {
104            // Safety: We know that an empty string can always be converted into a quoted string.
105            return Ok(ListMailbox::String(IString::Quoted(s.try_into().unwrap())));
106        }
107
108        if let Ok(lcs) = ListCharString::try_from(s) {
109            return Ok(ListMailbox::Token(lcs));
110        }
111
112        Ok(ListMailbox::String(s.try_into()?))
113    }
114}
115
116impl<'a> TryFrom<String> for ListMailbox<'a> {
117    type Error = ValidationError;
118
119    fn try_from(s: String) -> Result<Self, Self::Error> {
120        if s.is_empty() {
121            // Safety: We know that an empty string can always be converted into a quoted string.
122            return Ok(ListMailbox::String(IString::Quoted(s.try_into().unwrap())));
123        }
124
125        // TODO(efficiency)
126        if let Ok(lcs) = ListCharString::try_from(s.clone()) {
127            return Ok(ListMailbox::Token(lcs));
128        }
129
130        Ok(ListMailbox::String(s.try_into()?))
131    }
132}
133
134/// 5.1. Mailbox Naming
135///
136/// Mailbox names are 7-bit.  Client implementations MUST NOT attempt to
137/// create 8-bit mailbox names, and SHOULD interpret any 8-bit mailbox
138/// names returned by LIST or LSUB as UTF-8.  Server implementations
139/// SHOULD prohibit the creation of 8-bit mailbox names, and SHOULD NOT
140/// return 8-bit mailbox names in LIST or LSUB.  See section 5.1.3 for
141/// more information on how to represent non-ASCII mailbox names.
142///
143/// Note: 8-bit mailbox names were undefined in earlier
144/// versions of this protocol.  Some sites used a local 8-bit
145/// character set to represent non-ASCII mailbox names.  Such
146/// usage is not interoperable, and is now formally deprecated.
147///
148/// The case-insensitive mailbox name INBOX is a special name reserved to
149/// mean "the primary mailbox for this user on this server".  The
150/// interpretation of all other names is implementation-dependent.
151///
152/// In particular, this specification takes no position on case
153/// sensitivity in non-INBOX mailbox names.  Some server implementations
154/// are fully case-sensitive; others preserve case of a newly-created
155/// name but otherwise are case-insensitive; and yet others coerce names
156/// to a particular case.  Client implementations MUST interact with any
157/// of these.  If a server implementation interprets non-INBOX mailbox
158/// names as case-insensitive, it MUST treat names using the
159/// international naming convention specially as described in section 5.1.3.
160///
161/// There are certain client considerations when creating a new mailbox name:
162///
163/// 1) Any character which is one of the atom-specials (see the Formal Syntax) will require
164///    that the mailbox name be represented as a quoted string or literal.
165/// 2) CTL and other non-graphic characters are difficult to represent in a user interface
166///    and are best avoided.
167/// 3) Although the list-wildcard characters ("%" and "*") are valid in a mailbox name, it is
168///    difficult to use such mailbox names with the LIST and LSUB commands due to the conflict
169///    with wildcard interpretation.
170/// 4) Usually, a character (determined by the server implementation) is reserved to delimit
171///    levels of hierarchy.
172/// 5) Two characters, "#" and "&", have meanings by convention, and should be avoided except
173///    when used in that convention.
174#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
175#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
176#[derive(Debug, Clone, PartialEq, Eq, Hash)]
177pub enum Mailbox<'a> {
178    Inbox,
179    Other(MailboxOther<'a>),
180}
181
182impl_try_from!(AString<'a>, 'a, &'a [u8], Mailbox<'a>);
183impl_try_from!(AString<'a>, 'a, Vec<u8>, Mailbox<'a>);
184impl_try_from!(AString<'a>, 'a, &'a str, Mailbox<'a>);
185impl_try_from!(AString<'a>, 'a, String, Mailbox<'a>);
186
187impl<'a> From<AString<'a>> for Mailbox<'a> {
188    fn from(value: AString<'a>) -> Self {
189        match from_utf8(value.as_ref()) {
190            Ok(value) if value.to_ascii_lowercase() == "inbox" => Self::Inbox,
191            _ => Self::Other(MailboxOther::try_from(value).unwrap()),
192        }
193    }
194}
195
196// We do not implement `AsRef<...>` for `Mailbox` because we want to enforce that a consumer
197// `match`es on `Mailbox::Inbox`/`Mailbox::Other`.
198
199#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
200#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
201#[derive(Debug, Clone, PartialEq, Eq, Hash)]
202pub struct MailboxOther<'a>(pub(crate) AString<'a>);
203
204impl<'a> MailboxOther<'a> {
205    pub fn validate(value: impl AsRef<[u8]>) -> Result<(), MailboxOtherError> {
206        if value.as_ref().to_ascii_lowercase() == b"inbox" {
207            return Err(MailboxOtherError::Reserved);
208        }
209
210        Ok(())
211    }
212
213    pub fn inner(&self) -> &AString {
214        &self.0
215    }
216
217    /// Constructs a mailbox without validation.
218    ///
219    /// # Warning: IMAP conformance
220    ///
221    /// The caller must ensure that `value` is valid according to [`Self::validate`]. Failing to do
222    /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows.
223    /// Do not call this constructor with untrusted data.
224    #[cfg(feature = "unvalidated")]
225    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
226    pub fn unvalidated(value: AString<'a>) -> Self {
227        #[cfg(debug_assertions)]
228        Self::validate(&value).unwrap();
229
230        Self(value)
231    }
232}
233
234macro_rules! impl_try_from {
235    ($from:ty) => {
236        impl<'a> TryFrom<$from> for MailboxOther<'a> {
237            type Error = MailboxOtherError;
238
239            fn try_from(value: $from) -> Result<Self, Self::Error> {
240                let astring = AString::try_from(value)?;
241
242                Self::validate(&astring)?;
243
244                Ok(Self(astring))
245            }
246        }
247    };
248}
249
250impl_try_from!(&'a [u8]);
251impl_try_from!(Vec<u8>);
252impl_try_from!(&'a str);
253impl_try_from!(String);
254
255impl<'a> TryFrom<AString<'a>> for MailboxOther<'a> {
256    type Error = MailboxOtherError;
257
258    fn try_from(value: AString<'a>) -> Result<Self, Self::Error> {
259        Self::validate(&value)?;
260
261        Ok(Self(value))
262    }
263}
264
265impl<'a> AsRef<[u8]> for MailboxOther<'a> {
266    fn as_ref(&self) -> &[u8] {
267        self.0.as_ref()
268    }
269}
270
271/// Error-related types.
272pub mod error {
273    use thiserror::Error;
274
275    use crate::error::ValidationError;
276
277    #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
278    pub enum MailboxOtherError {
279        #[error(transparent)]
280        Literal(#[from] ValidationError),
281        #[error("Reserved: Please use one of the typed variants")]
282        Reserved,
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use std::borrow::Cow;
289
290    use super::*;
291    use crate::core::{AString, IString, Literal, LiteralMode};
292
293    #[test]
294    fn test_conversion_mailbox() {
295        let tests = [
296            ("inbox", Mailbox::Inbox),
297            ("inboX", Mailbox::Inbox),
298            ("Inbox", Mailbox::Inbox),
299            ("InboX", Mailbox::Inbox),
300            ("INBOX", Mailbox::Inbox),
301            (
302                "INBO²",
303                Mailbox::Other(MailboxOther(AString::String(IString::Literal(Literal {
304                    data: Cow::Borrowed("INBO²".as_bytes()),
305                    mode: LiteralMode::Sync,
306                })))),
307            ),
308        ];
309
310        for (test, expected) in tests {
311            let got = Mailbox::try_from(test).unwrap();
312            assert_eq!(expected, got);
313
314            let got = Mailbox::try_from(String::from(test)).unwrap();
315            assert_eq!(expected, got);
316        }
317    }
318
319    #[test]
320    fn test_conversion_mailbox_failing() {
321        let tests = ["\x00", "A\x00", "\x00A"];
322
323        for test in tests {
324            assert!(Mailbox::try_from(test).is_err());
325            assert!(Mailbox::try_from(String::from(test)).is_err());
326        }
327    }
328}