1use bytes::BytesMut;
20use std::collections::HashMap;
21use std::sync::Arc;
22use std::time::Duration;
23use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
24use tokio::sync::{Mutex, broadcast, mpsc, oneshot};
25
26use crate::error::ClientError;
27use imap_core::ast::{Response, Status};
28use imap_core::parser::{MAX_LITERAL_SIZE, parse_response};
29
30type TaggedReply = oneshot::Sender<Result<Vec<u8>, ClientError>>;
32type PendingCommands = Arc<Mutex<HashMap<String, TaggedReply>>>;
33
34const EVENT_CHANNEL_CAP: usize = 1024;
38
39const MAX_FRAME_SIZE: usize = MAX_LITERAL_SIZE + 64 * 1024;
47
48enum WriteRequest {
50 Command {
54 bytes: Vec<u8>,
55 tag: String,
56 reply_tx: TaggedReply,
57 },
58 Raw { bytes: Vec<u8> },
61}
62
63pub struct RawClient {
65 write_tx: mpsc::Sender<WriteRequest>,
66 event_tx: broadcast::Sender<Vec<u8>>,
67 tag_counter: u64,
68 pub default_timeout: Duration,
69}
70
71impl RawClient {
72 pub fn new<S>(stream: S) -> Self
73 where
74 S: AsyncRead + AsyncWrite + Send + 'static,
75 {
76 let (read_half, write_half) = tokio::io::split(stream);
77 let (write_tx, write_rx) = mpsc::channel(32);
78 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAP);
79
80 let pending_commands = Arc::new(Mutex::new(HashMap::new()));
81
82 tokio::spawn(read_loop(
83 read_half,
84 Arc::clone(&pending_commands),
85 event_tx.clone(),
86 MAX_FRAME_SIZE,
87 ));
88 tokio::spawn(write_loop(
89 write_half,
90 write_rx,
91 Arc::clone(&pending_commands),
92 ));
93
94 Self {
95 write_tx,
96 event_tx,
97 tag_counter: 1,
98 default_timeout: Duration::from_secs(30),
99 }
100 }
101
102 pub fn events(&self) -> broadcast::Receiver<Vec<u8>> {
103 self.event_tx.subscribe()
104 }
105
106 fn next_tag(&mut self) -> String {
109 let tag = format!("A{:04}", self.tag_counter);
110 self.tag_counter = self.tag_counter.wrapping_add(1);
111 tag
112 }
113
114 pub async fn execute_command(&mut self, cmd: &str) -> Result<Vec<u8>, ClientError> {
120 self.execute_command_with_timeout(cmd, self.default_timeout)
121 .await
122 }
123
124 pub async fn execute_command_with_timeout(
125 &mut self,
126 cmd: &str,
127 timeout: Duration,
128 ) -> Result<Vec<u8>, ClientError> {
129 let (_tag, rx) = self.send_command_async(cmd).await?;
130 match tokio::time::timeout(timeout, rx).await {
131 Ok(Ok(res)) => res,
132 Ok(Err(_)) => Err(ClientError::ConnectionClosed),
133 Err(_) => Err(ClientError::Timeout),
134 }
135 }
136
137 pub async fn send_command_async(
141 &mut self,
142 cmd: &str,
143 ) -> Result<(String, oneshot::Receiver<Result<Vec<u8>, ClientError>>), ClientError> {
144 let tag = self.next_tag();
145 let bytes = format!("{} {}\r\n", tag, cmd).into_bytes();
146 let (reply_tx, reply_rx) = oneshot::channel();
147 self.write_tx
148 .send(WriteRequest::Command {
149 bytes,
150 tag: tag.clone(),
151 reply_tx,
152 })
153 .await
154 .map_err(|_| ClientError::ConnectionClosed)?;
155 Ok((tag, reply_rx))
156 }
157
158 pub async fn send_raw(&mut self, bytes: Vec<u8>) -> Result<(), ClientError> {
161 self.write_tx
162 .send(WriteRequest::Raw { bytes })
163 .await
164 .map_err(|_| ClientError::ConnectionClosed)
165 }
166
167 pub fn writer(&self) -> WriterHandle {
171 WriterHandle {
172 write_tx: self.write_tx.clone(),
173 }
174 }
175}
176
177#[derive(Clone)]
181pub struct WriterHandle {
182 write_tx: mpsc::Sender<WriteRequest>,
183}
184
185impl WriterHandle {
186 pub async fn send_raw(&self, bytes: Vec<u8>) -> Result<(), ClientError> {
187 self.write_tx
188 .send(WriteRequest::Raw { bytes })
189 .await
190 .map_err(|_| ClientError::ConnectionClosed)
191 }
192}
193
194async fn write_loop<W>(
195 mut write_half: W,
196 mut rx: mpsc::Receiver<WriteRequest>,
197 pending_commands: PendingCommands,
198) where
199 W: AsyncWrite + Unpin,
200{
201 while let Some(req) = rx.recv().await {
202 match req {
203 WriteRequest::Command {
204 bytes,
205 tag,
206 reply_tx,
207 } => {
208 pending_commands.lock().await.insert(tag, reply_tx);
211 if write_half.write_all(&bytes).await.is_err() {
212 break;
213 }
214 }
215 WriteRequest::Raw { bytes } => {
216 if write_half.write_all(&bytes).await.is_err() {
217 break;
218 }
219 }
220 }
221 }
222}
223
224async fn fail_all_pending(pending: &PendingCommands, make_err: impl Fn() -> ClientError) {
228 let mut map = pending.lock().await;
229 for (_, tx) in map.drain() {
230 let _ = tx.send(Err(make_err()));
231 }
232}
233
234async fn read_loop<R>(
235 mut read_half: R,
236 pending_commands: PendingCommands,
237 event_tx: broadcast::Sender<Vec<u8>>,
238 max_frame_size: usize,
239) where
240 R: AsyncRead + Unpin,
241{
242 let mut buffer = BytesMut::with_capacity(8192);
243
244 loop {
245 match read_half.read_buf(&mut buffer).await {
246 Ok(0) => {
247 fail_all_pending(&pending_commands, || ClientError::ConnectionClosed).await;
249 break;
250 }
251 Ok(_) => {
252 while !buffer.is_empty() {
253 let routing = match parse_response(&buffer) {
256 Ok((remaining, response)) => {
257 let consumed = buffer.len() - remaining.len();
258 let routing = match &response {
259 Response::Status(s) => s
260 .tag
261 .map(|tag| (tag.to_string(), s.status, s.text.to_string())),
262 _ => None,
263 };
264 (consumed, routing)
265 }
266 Err(imap_core::error::ParseError::Incomplete) => break,
267 Err(_) => {
268 buffer.clear();
271 break;
272 }
273 };
274 let (consumed, routing) = routing;
275 let frame = buffer.split_to(consumed).to_vec();
276 dispatch_frame(routing, frame, &pending_commands, &event_tx).await;
277 }
278
279 if buffer.len() > max_frame_size {
284 fail_all_pending(&pending_commands, || ClientError::FrameTooLarge {
285 max: max_frame_size,
286 })
287 .await;
288 break;
289 }
290 }
291 Err(_) => {
292 fail_all_pending(&pending_commands, || ClientError::ConnectionClosed).await;
293 break;
294 }
295 }
296 }
297}
298
299async fn dispatch_frame(
303 routing: Option<(String, Status, String)>,
304 frame: Vec<u8>,
305 pending_commands: &PendingCommands,
306 event_tx: &broadcast::Sender<Vec<u8>>,
307) {
308 if let Some((tag, status, text)) = routing {
309 let mut map = pending_commands.lock().await;
310 if let Some(tx) = map.remove(&tag) {
311 let result = match status {
312 Status::Ok => Ok(frame),
313 Status::No | Status::Bad => Err(ClientError::CommandFailed(text)),
314 Status::Bye => Err(ClientError::ConnectionClosed),
315 Status::PreAuth => Err(ClientError::CommandFailed(text)),
318 };
319 let _ = tx.send(result);
320 return;
321 }
322 }
323 let _ = event_tx.send(frame);
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use std::time::Duration;
331 use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
332
333 #[tokio::test]
334 async fn test_tagged_response_matching() {
335 let (client_io, mut server_io) = duplex(1024);
336 let mut client = RawClient::new(client_io);
337
338 let command_task = tokio::spawn(async move { client.execute_command("NOOP").await });
339
340 let mut buf = [0u8; 1024];
341 let n = server_io.read(&mut buf).await.unwrap();
342 let cmd = String::from_utf8_lossy(&buf[..n]);
343 assert!(cmd.contains("NOOP"));
344 let tag = cmd.split_whitespace().next().unwrap();
345
346 server_io
347 .write_all(format!("{} OK NOOP completed\r\n", tag).as_bytes())
348 .await
349 .unwrap();
350
351 let result = command_task.await.unwrap().unwrap();
352 assert!(String::from_utf8_lossy(&result).contains("OK"));
353 }
354
355 #[tokio::test]
356 async fn test_no_response_becomes_error() {
357 let (client_io, mut server_io) = duplex(1024);
358 let mut client = RawClient::new(client_io);
359
360 let command_task = tokio::spawn(async move { client.execute_command("LOGIN x y").await });
361
362 let mut buf = [0u8; 1024];
363 let n = server_io.read(&mut buf).await.unwrap();
364 let tag = String::from_utf8_lossy(&buf[..n])
365 .split_whitespace()
366 .next()
367 .unwrap()
368 .to_owned();
369 server_io
370 .write_all(format!("{} NO authentication failed\r\n", tag).as_bytes())
371 .await
372 .unwrap();
373
374 let result = command_task.await.unwrap();
375 match result {
376 Err(ClientError::CommandFailed(text)) => {
377 assert_eq!(text, "authentication failed")
378 }
379 other => panic!("expected CommandFailed, got {:?}", other),
380 }
381 }
382
383 #[tokio::test]
384 async fn test_bad_response_becomes_error() {
385 let (client_io, mut server_io) = duplex(1024);
386 let mut client = RawClient::new(client_io);
387
388 let command_task = tokio::spawn(async move { client.execute_command("BOGUS").await });
389
390 let mut buf = [0u8; 1024];
391 let n = server_io.read(&mut buf).await.unwrap();
392 let tag = String::from_utf8_lossy(&buf[..n])
393 .split_whitespace()
394 .next()
395 .unwrap()
396 .to_owned();
397 server_io
398 .write_all(format!("{} BAD unknown command\r\n", tag).as_bytes())
399 .await
400 .unwrap();
401
402 let result = command_task.await.unwrap();
403 assert!(matches!(result, Err(ClientError::CommandFailed(_))));
404 }
405
406 #[tokio::test]
407 async fn test_untagged_event_broadcasting() {
408 let (client_io, mut server_io) = duplex(1024);
409 let client = RawClient::new(client_io);
410 let mut events = client.events();
411
412 server_io.write_all(b"* 5 EXISTS\r\n").await.unwrap();
413
414 let event = events.recv().await.unwrap();
415 assert_eq!(String::from_utf8_lossy(&event), "* 5 EXISTS\r\n");
416 }
417
418 #[tokio::test]
419 async fn test_partial_read_reassembly() {
420 let (client_io, mut server_io) = duplex(1024);
421 let mut client = RawClient::new(client_io);
422
423 let command_task = tokio::spawn(async move { client.execute_command("NOOP").await });
424
425 let mut buf = [0u8; 1024];
426 let n = server_io.read(&mut buf).await.unwrap();
427 let tag = String::from_utf8_lossy(&buf[..n])
428 .split_whitespace()
429 .next()
430 .unwrap()
431 .to_string();
432
433 let response = format!("{} OK NOOP completed\r\n", tag);
434 for byte in response.as_bytes() {
435 server_io.write_all(&[*byte]).await.unwrap();
436 tokio::task::yield_now().await;
437 }
438
439 let result = command_task.await.unwrap().unwrap();
440 assert!(String::from_utf8_lossy(&result).contains("OK"));
441 }
442
443 #[tokio::test]
444 async fn test_command_timeout() {
445 let (client_io, _server_io) = duplex(1024);
446 let mut client = RawClient::new(client_io);
447 client.default_timeout = Duration::from_millis(50);
448
449 let result = client.execute_command("NOOP").await;
450 assert!(matches!(result, Err(ClientError::Timeout)));
451 }
452
453 #[tokio::test]
454 async fn test_search_parsing() {
455 let (client_io, mut server_io) = duplex(1024);
456 let mut client = RawClient::new(client_io);
457 let mut events = client.events();
458
459 let command_task =
460 tokio::spawn(async move { client.execute_command("SEARCH FROM \"alice\"").await });
461
462 let mut buf = [0u8; 1024];
463 let n = server_io.read(&mut buf).await.unwrap();
464 let cmd = String::from_utf8_lossy(&buf[..n]);
465 let tag = cmd.split_whitespace().next().unwrap();
466
467 server_io.write_all(b"* SEARCH 1 2 3\r\n").await.unwrap();
468 server_io
469 .write_all(format!("{} OK SEARCH completed\r\n", tag).as_bytes())
470 .await
471 .unwrap();
472
473 let _result = command_task.await.unwrap().unwrap();
474
475 let event = events.recv().await.unwrap();
476 assert_eq!(String::from_utf8_lossy(&event), "* SEARCH 1 2 3\r\n");
477 }
478
479 #[tokio::test]
480 async fn test_send_raw() {
481 let (client_io, mut server_io) = duplex(1024);
482 let mut client = RawClient::new(client_io);
483
484 client.send_raw(b"DONE\r\n".to_vec()).await.unwrap();
485
486 let mut buf = [0u8; 1024];
487 let n = server_io.read(&mut buf).await.unwrap();
488 assert_eq!(&buf[..n], b"DONE\r\n");
489 }
490
491 #[tokio::test]
492 async fn test_connection_closed_on_eof() {
493 let (client_io, server_io) = duplex(1024);
494 let mut client = RawClient::new(client_io);
495 client.default_timeout = Duration::from_secs(1);
496
497 let task = tokio::spawn(async move { client.execute_command("NOOP").await });
498 tokio::time::sleep(Duration::from_millis(20)).await;
500 drop(server_io);
501
502 let result = task.await.unwrap();
503 assert!(matches!(
504 result,
505 Err(ClientError::ConnectionClosed) | Err(ClientError::Timeout)
506 ));
507 }
508
509 #[tokio::test]
510 async fn test_connection_closed_immediate() {
511 let (client_io, server_io) = duplex(1024);
512 let mut client = RawClient::new(client_io);
513 drop(server_io);
514
515 let result = client.execute_command("NOOP").await;
516 assert!(matches!(
517 result,
518 Err(ClientError::ConnectionClosed) | Err(ClientError::Timeout)
519 ));
520 }
521
522 #[tokio::test]
523 async fn test_unterminated_frame_is_bounded() {
524 let (client_io, mut server_io) = duplex(8192);
529
530 let pending: PendingCommands = Arc::new(Mutex::new(HashMap::new()));
531 let (event_tx, _event_rx) = broadcast::channel(EVENT_CHANNEL_CAP);
532
533 let (reply_tx, reply_rx) = oneshot::channel();
534 pending.lock().await.insert("A0001".to_string(), reply_tx);
535
536 let max_frame_size = 64;
537 let loop_task = tokio::spawn(read_loop(
538 client_io,
539 Arc::clone(&pending),
540 event_tx,
541 max_frame_size,
542 ));
543
544 server_io.write_all(b"* OK ").await.unwrap();
547 server_io
548 .write_all(&vec![b'a'; max_frame_size * 2])
549 .await
550 .unwrap();
551
552 let result = reply_rx.await.unwrap();
553 assert!(
554 matches!(result, Err(ClientError::FrameTooLarge { max }) if max == max_frame_size),
555 "expected FrameTooLarge, got {result:?}"
556 );
557 loop_task.await.unwrap();
559 }
560}