Skip to main content

mailrs_imap_codec/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3#![deny(rustdoc::broken_intra_doc_links)]
4
5use bytes::{Buf, BytesMut};
6use tokio_util::codec::{Decoder, Encoder};
7
8/// Tokio codec for IMAP. Switches between line mode (CRLF-terminated
9/// commands + responses) and literal mode (raw byte-counted payloads).
10///
11/// IMAP uses literals (e.g. `{12}\r\nHello world!`) for arbitrary
12/// binary content — passwords with special chars, APPEND payloads,
13/// FETCH BODY[…] data. The protocol layer parses the `{N}` marker,
14/// then calls [`expect_literal`](Self::expect_literal) to tell the
15/// codec to read the next N bytes as raw data instead of splitting
16/// on CRLF.
17pub struct ImapCodec {
18    literal_remaining: Option<u32>,
19}
20
21/// One decoded IMAP-session frame.
22#[derive(Debug)]
23pub enum ImapInput {
24    /// A line-mode frame (everything up to the CRLF, exclusive).
25    /// Returned for both client commands (`A001 LOGIN …`) and
26    /// continuation requests (`+ ready for literal`). Non-UTF-8
27    /// bytes are replaced with U+FFFD (lossy conversion).
28    Line(String),
29    /// A literal-mode frame: exactly N bytes as requested by the
30    /// most recent [`ImapCodec::expect_literal`] call. The trailing
31    /// CRLF that often follows a literal is consumed automatically
32    /// when present.
33    LiteralData(Vec<u8>),
34}
35
36impl Default for ImapCodec {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl ImapCodec {
43    /// New codec in line mode.
44    pub fn new() -> Self {
45        Self {
46            literal_remaining: None,
47        }
48    }
49
50    /// Switch the codec into literal mode for the next decode.
51    /// `size` is the byte count parsed from the IMAP `{N}` marker.
52    /// After exactly `size` bytes have been read, the codec
53    /// auto-switches back to line mode (consuming any trailing
54    /// CRLF if present).
55    pub fn expect_literal(&mut self, size: u32) {
56        self.literal_remaining = Some(size);
57    }
58}
59
60impl Decoder for ImapCodec {
61    type Item = ImapInput;
62    type Error = std::io::Error;
63
64    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
65        if let Some(remaining) = self.literal_remaining {
66            let needed = remaining as usize;
67            if src.len() >= needed {
68                let data = src.split_to(needed).to_vec();
69                self.literal_remaining = None;
70                if src.len() >= 2 && &src[..2] == b"\r\n" {
71                    src.advance(2);
72                }
73                return Ok(Some(ImapInput::LiteralData(data)));
74            }
75            return Ok(None);
76        }
77
78        if let Some(pos) = src.windows(2).position(|w| w == b"\r\n") {
79            let line = src.split_to(pos);
80            src.advance(2);
81            let s = String::from_utf8_lossy(&line).into_owned();
82            Ok(Some(ImapInput::Line(s)))
83        } else {
84            Ok(None)
85        }
86    }
87}
88
89impl Encoder<Vec<u8>> for ImapCodec {
90    type Error = std::io::Error;
91
92    fn encode(&mut self, item: Vec<u8>, dst: &mut BytesMut) -> Result<(), Self::Error> {
93        dst.extend_from_slice(&item);
94        Ok(())
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use bytes::BytesMut;
102    use tokio_util::codec::{Decoder, Encoder};
103
104    fn decode_once(
105        codec: &mut ImapCodec,
106        data: &[u8],
107    ) -> Result<Option<ImapInput>, std::io::Error> {
108        let mut buf = BytesMut::from(data);
109        codec.decode(&mut buf)
110    }
111
112    #[test]
113    fn decode_simple_line() {
114        let mut codec = ImapCodec::new();
115        let mut buf = BytesMut::from("A001 LOGIN user pass\r\n");
116        let result = codec.decode(&mut buf).unwrap();
117        match result {
118            Some(ImapInput::Line(s)) => assert_eq!(s, "A001 LOGIN user pass"),
119            other => panic!("expected Line, got {other:?}"),
120        }
121        assert!(buf.is_empty());
122    }
123
124    #[test]
125    fn decode_empty_line() {
126        let mut codec = ImapCodec::new();
127        let mut buf = BytesMut::from("\r\n");
128        match codec.decode(&mut buf).unwrap() {
129            Some(ImapInput::Line(s)) => assert_eq!(s, ""),
130            other => panic!("expected empty Line, got {other:?}"),
131        }
132    }
133
134    #[test]
135    fn decode_incomplete_line_returns_none() {
136        let mut codec = ImapCodec::new();
137        assert!(decode_once(&mut codec, b"A001 NOOP").unwrap().is_none());
138    }
139
140    #[test]
141    fn decode_line_with_bare_lf_not_matched() {
142        let mut codec = ImapCodec::new();
143        assert!(decode_once(&mut codec, b"A001 NOOP\n").unwrap().is_none());
144    }
145
146    #[test]
147    fn decode_two_lines_sequentially() {
148        let mut codec = ImapCodec::new();
149        let mut buf = BytesMut::from("A001 NOOP\r\nA002 LOGOUT\r\n");
150        match codec.decode(&mut buf).unwrap() {
151            Some(ImapInput::Line(s)) => assert_eq!(s, "A001 NOOP"),
152            other => panic!("first: {other:?}"),
153        }
154        match codec.decode(&mut buf).unwrap() {
155            Some(ImapInput::Line(s)) => assert_eq!(s, "A002 LOGOUT"),
156            other => panic!("second: {other:?}"),
157        }
158        assert!(buf.is_empty());
159    }
160
161    #[test]
162    fn decode_line_preserves_internal_cr_when_no_lf() {
163        let mut codec = ImapCodec::new();
164        let mut buf = BytesMut::from("hello\rworld\r\n");
165        match codec.decode(&mut buf).unwrap() {
166            Some(ImapInput::Line(s)) => assert_eq!(s, "hello\rworld"),
167            other => panic!("expected Line, got {other:?}"),
168        }
169    }
170
171    #[test]
172    fn decode_literal_exact_size() {
173        let mut codec = ImapCodec::new();
174        codec.expect_literal(5);
175        let mut buf = BytesMut::from("ABCDE\r\n");
176        match codec.decode(&mut buf).unwrap() {
177            Some(ImapInput::LiteralData(data)) => assert_eq!(data, b"ABCDE"),
178            other => panic!("expected LiteralData, got {other:?}"),
179        }
180        assert!(buf.is_empty());
181    }
182
183    #[test]
184    fn decode_literal_without_trailing_crlf() {
185        let mut codec = ImapCodec::new();
186        codec.expect_literal(3);
187        let mut buf = BytesMut::from("ABCnext");
188        match codec.decode(&mut buf).unwrap() {
189            Some(ImapInput::LiteralData(data)) => assert_eq!(data, b"ABC"),
190            other => panic!("expected LiteralData, got {other:?}"),
191        }
192        assert_eq!(&buf[..], b"next");
193    }
194
195    #[test]
196    fn decode_literal_incomplete_returns_none() {
197        let mut codec = ImapCodec::new();
198        codec.expect_literal(10);
199        assert!(decode_once(&mut codec, b"short").unwrap().is_none());
200    }
201
202    #[test]
203    fn decode_literal_zero_length() {
204        let mut codec = ImapCodec::new();
205        codec.expect_literal(0);
206        let mut buf = BytesMut::from("\r\n");
207        match codec.decode(&mut buf).unwrap() {
208            Some(ImapInput::LiteralData(data)) => assert!(data.is_empty()),
209            other => panic!("expected empty LiteralData, got {other:?}"),
210        }
211        assert!(buf.is_empty());
212    }
213
214    #[test]
215    fn decode_literal_then_line() {
216        let mut codec = ImapCodec::new();
217        codec.expect_literal(4);
218        let mut buf = BytesMut::from("data\r\nA003 OK\r\n");
219        match codec.decode(&mut buf).unwrap() {
220            Some(ImapInput::LiteralData(d)) => assert_eq!(d, b"data"),
221            other => panic!("expected LiteralData, got {other:?}"),
222        }
223        match codec.decode(&mut buf).unwrap() {
224            Some(ImapInput::Line(s)) => assert_eq!(s, "A003 OK"),
225            other => panic!("expected Line, got {other:?}"),
226        }
227    }
228
229    #[test]
230    fn decode_literal_containing_crlf() {
231        let mut codec = ImapCodec::new();
232        codec.expect_literal(6);
233        let mut buf = BytesMut::from("AB\r\nCD\r\n");
234        match codec.decode(&mut buf).unwrap() {
235            Some(ImapInput::LiteralData(data)) => assert_eq!(data, b"AB\r\nCD"),
236            other => panic!("expected LiteralData, got {other:?}"),
237        }
238        assert!(buf.is_empty());
239    }
240
241    #[test]
242    fn decode_literal_with_binary_data() {
243        let mut codec = ImapCodec::new();
244        let binary: Vec<u8> = (0u8..=255).collect();
245        let size = binary.len() as u32;
246        codec.expect_literal(size);
247        let mut buf = BytesMut::from(binary.as_slice());
248        buf.extend_from_slice(b"\r\n");
249        match codec.decode(&mut buf).unwrap() {
250            Some(ImapInput::LiteralData(data)) => assert_eq!(data, binary),
251            other => panic!("expected LiteralData, got {other:?}"),
252        }
253    }
254
255    #[test]
256    fn encode_copies_bytes_to_dst() {
257        let mut codec = ImapCodec::new();
258        let mut dst = BytesMut::new();
259        codec.encode(b"* OK ready\r\n".to_vec(), &mut dst).unwrap();
260        assert_eq!(&dst[..], b"* OK ready\r\n");
261    }
262
263    #[test]
264    fn encode_appends_to_existing_buffer() {
265        let mut codec = ImapCodec::new();
266        let mut dst = BytesMut::from("existing");
267        codec.encode(b"+more".to_vec(), &mut dst).unwrap();
268        assert_eq!(&dst[..], b"existing+more");
269    }
270
271    #[test]
272    fn encode_empty_vec() {
273        let mut codec = ImapCodec::new();
274        let mut dst = BytesMut::new();
275        codec.encode(vec![], &mut dst).unwrap();
276        assert!(dst.is_empty());
277    }
278
279    #[test]
280    fn new_codec_has_no_literal_pending() {
281        let mut codec = ImapCodec::new();
282        let mut buf = BytesMut::from("test\r\n");
283        assert!(matches!(
284            codec.decode(&mut buf).unwrap(),
285            Some(ImapInput::Line(_))
286        ));
287    }
288
289    #[test]
290    fn expect_literal_clears_after_read() {
291        let mut codec = ImapCodec::new();
292        codec.expect_literal(2);
293        let mut buf = BytesMut::from("OK\r\nA001 DONE\r\n");
294        let _ = codec.decode(&mut buf).unwrap();
295        match codec.decode(&mut buf).unwrap() {
296            Some(ImapInput::Line(s)) => assert_eq!(s, "A001 DONE"),
297            other => panic!("expected Line after literal, got {other:?}"),
298        }
299    }
300
301    #[test]
302    fn decode_non_utf8_line_uses_lossy_conversion() {
303        let mut codec = ImapCodec::new();
304        let mut buf = BytesMut::from(&b"hello \xff world\r\n"[..]);
305        match codec.decode(&mut buf).unwrap() {
306            Some(ImapInput::Line(s)) => {
307                assert!(s.contains("hello"));
308                assert!(s.contains("world"));
309                assert!(s.contains('\u{FFFD}'));
310            }
311            other => panic!("expected Line, got {other:?}"),
312        }
313    }
314
315    #[test]
316    fn decode_partial_crlf_at_buffer_end() {
317        let mut codec = ImapCodec::new();
318        assert!(decode_once(&mut codec, b"A001 NOOP\r").unwrap().is_none());
319    }
320
321    #[test]
322    fn default_constructs_same_as_new() {
323        let _c = ImapCodec::default();
324    }
325}