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
8pub struct ImapCodec {
18 literal_remaining: Option<u32>,
19}
20
21#[derive(Debug)]
23pub enum ImapInput {
24 Line(String),
29 LiteralData(Vec<u8>),
34}
35
36impl Default for ImapCodec {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl ImapCodec {
43 pub fn new() -> Self {
45 Self {
46 literal_remaining: None,
47 }
48 }
49
50 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}