imap_codec/
codec.rs

1pub mod decode;
2pub mod encode;
3
4/// Codec for greetings.
5#[derive(Debug, Default)]
6// We use `#[non_exhaustive]` to prevent users from using struct literal syntax.
7//
8// This allows to add configuration options later. For example, the
9// codec could transparently replace all literals with non-sync literals.
10#[non_exhaustive]
11pub struct GreetingCodec;
12
13/// Codec for commands.
14#[derive(Debug, Default)]
15#[non_exhaustive]
16pub struct CommandCodec;
17
18/// Codec for authenticate data lines.
19#[derive(Debug, Default)]
20#[non_exhaustive]
21pub struct AuthenticateDataCodec;
22
23/// Codec for responses.
24#[derive(Debug, Default)]
25#[non_exhaustive]
26pub struct ResponseCodec;
27
28/// Codec for idle dones.
29#[derive(Debug, Default)]
30#[non_exhaustive]
31pub struct IdleDoneCodec;
32
33macro_rules! impl_codec_new {
34    ($codec:ty) => {
35        impl $codec {
36            /// Create codec with default configuration.
37            pub fn new() -> Self {
38                Self::default()
39            }
40        }
41    };
42}
43
44impl_codec_new!(GreetingCodec);
45impl_codec_new!(CommandCodec);
46impl_codec_new!(AuthenticateDataCodec);
47impl_codec_new!(ResponseCodec);
48impl_codec_new!(IdleDoneCodec);
49
50#[cfg(test)]
51mod tests {
52    use std::num::NonZeroU32;
53
54    use imap_types::{
55        auth::AuthenticateData,
56        command::{Command, CommandBody},
57        core::{IString, Literal, LiteralMode, NString, NonEmptyVec, Tag},
58        fetch::MessageDataItem,
59        mailbox::Mailbox,
60        response::{Data, Greeting, GreetingKind, Response},
61        secret::Secret,
62    };
63
64    use super::*;
65    use crate::{
66        decode::{CommandDecodeError, Decoder, GreetingDecodeError, ResponseDecodeError},
67        testing::{
68            kat_inverse_authenticate_data, kat_inverse_command, kat_inverse_greeting,
69            kat_inverse_response,
70        },
71    };
72
73    #[test]
74    fn test_kat_inverse_greeting() {
75        kat_inverse_greeting(&[
76            (
77                b"* OK ...\r\n".as_ref(),
78                b"".as_ref(),
79                Greeting::new(GreetingKind::Ok, None, "...").unwrap(),
80            ),
81            (
82                b"* ByE .\r\n???",
83                b"???",
84                Greeting::new(GreetingKind::Bye, None, ".").unwrap(),
85            ),
86            (
87                b"* preaUth x\r\n?",
88                b"?",
89                Greeting::new(GreetingKind::PreAuth, None, "x").unwrap(),
90            ),
91        ]);
92    }
93
94    #[test]
95    fn test_kat_inverse_command() {
96        kat_inverse_command(&[
97            (
98                b"a nOOP\r\n".as_ref(),
99                b"".as_ref(),
100                Command::new("a", CommandBody::Noop).unwrap(),
101            ),
102            (
103                b"a NooP\r\n???",
104                b"???",
105                Command::new("a", CommandBody::Noop).unwrap(),
106            ),
107            (
108                b"a SeLECT {5}\r\ninbox\r\n",
109                b"",
110                Command::new(
111                    "a",
112                    CommandBody::Select {
113                        mailbox: Mailbox::Inbox,
114                    },
115                )
116                .unwrap(),
117            ),
118            (
119                b"a SElECT {5}\r\ninbox\r\nxxx",
120                b"xxx",
121                Command::new(
122                    "a",
123                    CommandBody::Select {
124                        mailbox: Mailbox::Inbox,
125                    },
126                )
127                .unwrap(),
128            ),
129        ]);
130    }
131
132    #[test]
133    fn test_kat_inverse_response() {
134        kat_inverse_response(&[
135            (
136                b"* SEARCH 1\r\n".as_ref(),
137                b"".as_ref(),
138                Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])),
139            ),
140            (
141                b"* SEARCH 1\r\n???",
142                b"???",
143                Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])),
144            ),
145            (
146                b"* 1 FETCH (RFC822 {5}\r\nhello)\r\n",
147                b"",
148                Response::Data(Data::Fetch {
149                    seq: NonZeroU32::new(1).unwrap(),
150                    items: NonEmptyVec::from(MessageDataItem::Rfc822(NString(Some(
151                        IString::Literal(Literal::try_from(b"hello".as_ref()).unwrap()),
152                    )))),
153                }),
154            ),
155        ]);
156    }
157
158    #[test]
159    fn test_kat_inverse_authenticate_data() {
160        kat_inverse_authenticate_data(&[(
161            b"VGVzdA==\r\n".as_ref(),
162            b"".as_ref(),
163            AuthenticateData(Secret::new(b"Test".to_vec())),
164        )]);
165    }
166
167    #[test]
168    fn test_greeting_incomplete_failed() {
169        let tests = [
170            // Incomplete
171            (b"*".as_ref(), Err(GreetingDecodeError::Incomplete)),
172            (b"* ".as_ref(), Err(GreetingDecodeError::Incomplete)),
173            (b"* O".as_ref(), Err(GreetingDecodeError::Incomplete)),
174            (b"* OK".as_ref(), Err(GreetingDecodeError::Incomplete)),
175            (b"* OK ".as_ref(), Err(GreetingDecodeError::Incomplete)),
176            (b"* OK .".as_ref(), Err(GreetingDecodeError::Incomplete)),
177            (b"* OK .\r".as_ref(), Err(GreetingDecodeError::Incomplete)),
178            // Failed
179            (b"**".as_ref(), Err(GreetingDecodeError::Failed)),
180            (b"* NO x\r\n".as_ref(), Err(GreetingDecodeError::Failed)),
181        ];
182
183        for (test, expected) in tests {
184            let got = GreetingCodec::default().decode(test);
185            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
186            assert_eq!(expected, got);
187
188            #[cfg(feature = "bounded-static")]
189            {
190                let got = GreetingCodec::default().decode_static(test);
191                assert_eq!(expected, got);
192            }
193        }
194    }
195
196    #[test]
197    fn test_command_incomplete_failed() {
198        let tests = [
199            // Incomplete
200            (b"a".as_ref(), Err(CommandDecodeError::Incomplete)),
201            (b"a ".as_ref(), Err(CommandDecodeError::Incomplete)),
202            (b"a n".as_ref(), Err(CommandDecodeError::Incomplete)),
203            (b"a no".as_ref(), Err(CommandDecodeError::Incomplete)),
204            (b"a noo".as_ref(), Err(CommandDecodeError::Incomplete)),
205            (b"a noop".as_ref(), Err(CommandDecodeError::Incomplete)),
206            (b"a noop\r".as_ref(), Err(CommandDecodeError::Incomplete)),
207            // LiteralAckRequired
208            (
209                b"a select {5}\r\n".as_ref(),
210                Err(CommandDecodeError::LiteralFound {
211                    tag: Tag::try_from("a").unwrap(),
212                    length: 5,
213                    mode: LiteralMode::Sync,
214                }),
215            ),
216            (
217                b"a select {5+}\r\n".as_ref(),
218                Err(CommandDecodeError::LiteralFound {
219                    tag: Tag::try_from("a").unwrap(),
220                    length: 5,
221                    mode: LiteralMode::NonSync,
222                }),
223            ),
224            // Incomplete (after literal)
225            (
226                b"a select {5}\r\nxxx".as_ref(),
227                Err(CommandDecodeError::Incomplete),
228            ),
229            // Failed
230            (b"* noop\r\n".as_ref(), Err(CommandDecodeError::Failed)),
231            (b"A  noop\r\n".as_ref(), Err(CommandDecodeError::Failed)),
232        ];
233
234        for (test, expected) in tests {
235            let got = CommandCodec::default().decode(test);
236            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
237            assert_eq!(expected, got);
238
239            #[cfg(feature = "bounded-static")]
240            {
241                let got = CommandCodec::default().decode_static(test);
242                assert_eq!(expected, got);
243            }
244        }
245    }
246
247    #[test]
248    fn test_response_incomplete_failed() {
249        let tests = [
250            // Incomplete
251            (b"".as_ref(), Err(ResponseDecodeError::Incomplete)),
252            (b"*".as_ref(), Err(ResponseDecodeError::Incomplete)),
253            (b"* ".as_ref(), Err(ResponseDecodeError::Incomplete)),
254            (b"* S".as_ref(), Err(ResponseDecodeError::Incomplete)),
255            (b"* SE".as_ref(), Err(ResponseDecodeError::Incomplete)),
256            (b"* SEA".as_ref(), Err(ResponseDecodeError::Incomplete)),
257            (b"* SEAR".as_ref(), Err(ResponseDecodeError::Incomplete)),
258            (b"* SEARC".as_ref(), Err(ResponseDecodeError::Incomplete)),
259            (b"* SEARCH".as_ref(), Err(ResponseDecodeError::Incomplete)),
260            (b"* SEARCH ".as_ref(), Err(ResponseDecodeError::Incomplete)),
261            (b"* SEARCH 1".as_ref(), Err(ResponseDecodeError::Incomplete)),
262            (
263                b"* SEARCH 1\r".as_ref(),
264                Err(ResponseDecodeError::Incomplete),
265            ),
266            // LiteralAck treated as Incomplete
267            (
268                b"* 1 FETCH (RFC822 {5}\r\n".as_ref(),
269                Err(ResponseDecodeError::LiteralFound { length: 5 }),
270            ),
271            // Failed
272            (
273                b"*  search 1 2 3\r\n".as_ref(),
274                Err(ResponseDecodeError::Failed),
275            ),
276            (b"A search\r\n".as_ref(), Err(ResponseDecodeError::Failed)),
277        ];
278
279        for (test, expected) in tests {
280            let got = ResponseCodec::default().decode(test);
281            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
282            assert_eq!(expected, got);
283
284            #[cfg(feature = "bounded-static")]
285            {
286                let got = ResponseCodec::default().decode_static(test);
287                assert_eq!(expected, got);
288            }
289        }
290    }
291}