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