1use std::marker::PhantomData;
21use std::time::Duration;
22
23use base64::Engine as _;
24use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
25use tokio::sync::broadcast;
26
27use crate::capabilities::Capabilities;
28use crate::client::RawClient;
29use crate::credentials::{Password, imap_quoted};
30use crate::error::ClientError;
31use crate::flags::{Flag, StoreAction};
32use crate::idle::IdleHandle;
33use crate::search::SearchQuery;
34
35use imap_core::ast::{DataResponse, FetchAttribute, Response};
36use imap_core::parser::parse_response;
37
38pub struct Unauthenticated;
41pub struct Authenticated;
42pub struct Selected;
43
44pub struct PlainText;
47pub struct Tls;
48
49pub struct Session<State, Transport> {
51 raw: RawClient,
52 pub capabilities: Capabilities,
53 _state: PhantomData<State>,
54 _transport: PhantomData<Transport>,
55}
56
57#[derive(Debug, Clone)]
58pub struct FetchResult {
59 pub seq: u32,
60 pub uid: Option<u32>,
61 pub body: Option<Vec<u8>>,
62}
63
64impl<S, T> Session<S, T> {
67 fn transition_state<NewState>(self) -> Session<NewState, T> {
68 Session {
69 raw: self.raw,
70 capabilities: self.capabilities,
71 _state: PhantomData,
72 _transport: PhantomData,
73 }
74 }
75
76 pub fn transition_transport<NewTransport>(self) -> Session<S, NewTransport> {
77 Session {
78 raw: self.raw,
79 capabilities: self.capabilities,
80 _state: PhantomData,
81 _transport: PhantomData,
82 }
83 }
84
85 pub fn events(&self) -> broadcast::Receiver<Vec<u8>> {
86 self.raw.events()
87 }
88
89 pub async fn refresh_capabilities(&mut self) -> Result<(), ClientError> {
93 let mut events = self.raw.events();
94 let _resp = self.raw.execute_command("CAPABILITY").await?;
95 while let Ok(event) = events.try_recv() {
97 if let Ok((_, response)) = parse_response(&event)
98 && self.capabilities.try_update_from(&response)
99 {
100 return Ok(());
101 }
102 }
103 Ok(())
104 }
105
106 async fn refresh_capabilities_from_frame(&mut self, frame: &[u8]) -> Result<(), ClientError> {
109 if let Ok((_, response)) = parse_response(frame)
110 && self.capabilities.try_update_from(&response)
111 {
112 return Ok(());
113 }
114 self.refresh_capabilities().await
115 }
116}
117
118impl<T> Session<Unauthenticated, T> {
119 pub fn new(raw: RawClient, capabilities: Capabilities) -> Self {
120 Self::new_in_state(raw, capabilities)
121 }
122}
123
124impl<S, T> Session<S, T> {
125 pub(crate) fn new_in_state(raw: RawClient, capabilities: Capabilities) -> Self {
126 Self {
127 raw,
128 capabilities,
129 _state: PhantomData,
130 _transport: PhantomData,
131 }
132 }
133
134 pub async fn logout(mut self) -> Result<(), ClientError> {
137 match self.raw.execute_command("LOGOUT").await {
140 Ok(_) | Err(ClientError::ConnectionClosed) => Ok(()),
141 Err(e) => Err(e),
142 }
143 }
144
145 pub async fn noop(&mut self) -> Result<(), ClientError> {
147 self.raw.execute_command("NOOP").await.map(|_| ())
148 }
149}
150
151impl Session<Unauthenticated, Tls> {
154 pub async fn login(
161 mut self,
162 user: &str,
163 pass: Password,
164 ) -> Result<Session<Authenticated, Tls>, ClientError> {
165 let user_q = imap_quoted(user)?;
166 let pass_q = pass.as_imap_quoted()?;
167 let cmd = format!("LOGIN {} {}", user_q, pass_q);
168
169 let frame = self.raw.execute_command(&cmd).await?;
170 self.refresh_capabilities_from_frame(&frame).await?;
171 Ok(self.transition_state())
172 }
173
174 pub async fn authenticate_plain(
177 mut self,
178 user: &str,
179 pass: &Password,
180 ) -> Result<Session<Authenticated, Tls>, ClientError> {
181 if user.as_bytes().contains(&0) {
182 return Err(ClientError::CommandFailed(
183 "username must not contain NUL".into(),
184 ));
185 }
186 if pass.as_str().as_bytes().contains(&0) {
187 return Err(ClientError::CommandFailed(
188 "password must not contain NUL".into(),
189 ));
190 }
191
192 let mut events = self.raw.events();
193 let (_tag, reply_rx) = self.raw.send_command_async("AUTHENTICATE PLAIN").await?;
194
195 wait_for_continuation(&mut events, Duration::from_secs(30)).await?;
197
198 let mut sasl = Vec::with_capacity(2 + user.len() + pass.as_str().len());
200 sasl.push(0);
201 sasl.extend_from_slice(user.as_bytes());
202 sasl.push(0);
203 sasl.extend_from_slice(pass.as_str().as_bytes());
204 let mut payload = BASE64_STANDARD.encode(&sasl).into_bytes();
205 payload.extend_from_slice(b"\r\n");
206 self.raw.send_raw(payload).await?;
207
208 let frame = match reply_rx.await {
209 Ok(r) => r?,
210 Err(_) => return Err(ClientError::ConnectionClosed),
211 };
212 self.refresh_capabilities_from_frame(&frame).await?;
213 Ok(self.transition_state())
214 }
215}
216
217impl<T> Session<Authenticated, T> {
220 pub async fn select(mut self, mailbox: &str) -> Result<Session<Selected, T>, ClientError> {
222 let mb = imap_quoted(mailbox)?;
223 let cmd = format!("SELECT {}", mb);
224 self.raw.execute_command(&cmd).await?;
225 Ok(self.transition_state())
226 }
227
228 pub async fn examine(mut self, mailbox: &str) -> Result<Session<Selected, T>, ClientError> {
230 let mb = imap_quoted(mailbox)?;
231 let cmd = format!("EXAMINE {}", mb);
232 self.raw.execute_command(&cmd).await?;
233 Ok(self.transition_state())
234 }
235
236 pub async fn list(
238 &mut self,
239 reference: &str,
240 mailbox_mask: &str,
241 ) -> Result<Vec<u8>, ClientError> {
242 let cmd = format!(
243 "LIST {} {}",
244 imap_quoted(reference)?,
245 imap_quoted(mailbox_mask)?
246 );
247 self.raw.execute_command(&cmd).await
248 }
249}
250
251impl<T> Session<Selected, T> {
254 pub async fn fetch_raw(
256 &mut self,
257 sequence_set: &str,
258 items: &str,
259 ) -> Result<Vec<u8>, ClientError> {
260 let cmd = format!("FETCH {} {}", sequence_set, items);
261 self.raw.execute_command(&cmd).await
262 }
263
264 pub async fn fetch(
267 &mut self,
268 sequence_set: &str,
269 items: &str,
270 ) -> Result<Vec<FetchResult>, ClientError> {
271 let mut events = self.raw.events();
272 let cmd = format!("FETCH {} {}", sequence_set, items);
273 let _resp = self.raw.execute_command(&cmd).await?;
274
275 let mut results = Vec::new();
276 while let Ok(event) = events.try_recv() {
277 if let Ok((_, Response::Data(DataResponse::Fetch { seq, attributes }))) =
278 parse_response(&event)
279 {
280 let mut uid = None;
281 let mut body = None;
282 for attr in attributes {
283 match attr {
284 FetchAttribute::Uid(u) => uid = Some(u),
285 FetchAttribute::BodySection { data: Some(d), .. } => {
286 body = Some(d.to_vec())
287 }
288 FetchAttribute::Body(b) => body = Some(b.to_vec()),
289 FetchAttribute::Rfc822(b) => body = Some(b.to_vec()),
290 _ => {}
291 }
292 }
293 results.push(FetchResult { seq, uid, body });
294 }
295 }
296 Ok(results)
297 }
298
299 pub async fn fetch_body(&mut self, sequence_set: &str) -> Result<Option<String>, ClientError> {
301 let results = self.fetch(sequence_set, "BODY[]").await?;
302 if let Some(res) = results.first()
303 && let Some(body) = &res.body
304 {
305 return Ok(Some(String::from_utf8_lossy(body).into_owned()));
306 }
307 Ok(None)
308 }
309
310 pub async fn search(&mut self, query: SearchQuery) -> Result<Vec<u32>, ClientError> {
311 self.run_search(&format!("SEARCH {}", query.build())).await
312 }
313
314 pub async fn uid_search(&mut self, query: SearchQuery) -> Result<Vec<u32>, ClientError> {
315 self.run_search(&format!("UID SEARCH {}", query.build()))
316 .await
317 }
318
319 async fn run_search(&mut self, cmd: &str) -> Result<Vec<u32>, ClientError> {
320 let mut events = self.raw.events();
321 let _resp = self.raw.execute_command(cmd).await?;
322
323 let mut all_ids = Vec::new();
324 while let Ok(event) = events.try_recv() {
325 if let Ok((_, Response::Data(DataResponse::Search(ids)))) = parse_response(&event) {
326 all_ids.extend(ids);
327 }
328 }
329 Ok(all_ids)
330 }
331
332 pub async fn store(
333 &mut self,
334 sequence_set: &str,
335 action: StoreAction,
336 flags: &[Flag],
337 ) -> Result<Vec<u8>, ClientError> {
338 let flags_str = flags
339 .iter()
340 .map(|f| f.to_string())
341 .collect::<Vec<_>>()
342 .join(" ");
343 let cmd = format!(
344 "STORE {} {} ({})",
345 sequence_set,
346 action.to_imap_prefix(false),
347 flags_str
348 );
349 self.raw.execute_command(&cmd).await
350 }
351
352 pub async fn uid_store(
353 &mut self,
354 uid_set: &str,
355 action: StoreAction,
356 flags: &[Flag],
357 ) -> Result<Vec<u8>, ClientError> {
358 let flags_str = flags
359 .iter()
360 .map(|f| f.to_string())
361 .collect::<Vec<_>>()
362 .join(" ");
363 let cmd = format!(
364 "UID STORE {} {} ({})",
365 uid_set,
366 action.to_imap_prefix(false),
367 flags_str
368 );
369 self.raw.execute_command(&cmd).await
370 }
371
372 pub async fn expunge(&mut self) -> Result<Vec<u8>, ClientError> {
373 self.raw.execute_command("EXPUNGE").await
374 }
375
376 pub async fn close_mailbox(mut self) -> Result<Session<Authenticated, T>, ClientError> {
379 self.raw.execute_command("CLOSE").await?;
380 Ok(self.transition_state())
381 }
382
383 pub async fn unselect(mut self) -> Result<Session<Authenticated, T>, ClientError> {
386 if !self.capabilities.unselect {
387 return Err(ClientError::UnsupportedCapability("UNSELECT"));
388 }
389 self.raw.execute_command("UNSELECT").await?;
390 Ok(self.transition_state())
391 }
392
393 pub async fn check(&mut self) -> Result<(), ClientError> {
395 self.raw.execute_command("CHECK").await.map(|_| ())
396 }
397
398 pub async fn idle(&mut self) -> Result<IdleHandle, ClientError> {
402 if !self.capabilities.idle {
403 return Err(ClientError::UnsupportedCapability("IDLE"));
404 }
405 let mut events = self.raw.events();
406 let writer = self.raw.writer();
407 let (_tag, reply_rx) = self.raw.send_command_async("IDLE").await?;
408 wait_for_continuation(&mut events, Duration::from_secs(30)).await?;
409 Ok(IdleHandle::new(writer, reply_rx))
410 }
411
412 pub async fn move_messages(
414 &mut self,
415 sequence_set: &str,
416 mailbox: &str,
417 ) -> Result<Vec<u8>, ClientError> {
418 if !self.capabilities.move_ext {
419 return Err(ClientError::UnsupportedCapability("MOVE"));
420 }
421 let cmd = format!("MOVE {} {}", sequence_set, imap_quoted(mailbox)?);
422 self.raw.execute_command(&cmd).await
423 }
424}
425
426async fn wait_for_continuation(
431 events: &mut broadcast::Receiver<Vec<u8>>,
432 timeout: Duration,
433) -> Result<(), ClientError> {
434 let deadline = tokio::time::Instant::now() + timeout;
435 loop {
436 let now = tokio::time::Instant::now();
437 if now >= deadline {
438 return Err(ClientError::Timeout);
439 }
440 match tokio::time::timeout(deadline - now, events.recv()).await {
441 Ok(Ok(frame)) => {
442 if frame.starts_with(b"+") {
443 return Ok(());
444 }
445 }
447 Ok(Err(broadcast::error::RecvError::Closed)) => {
448 return Err(ClientError::ConnectionClosed);
449 }
450 Ok(Err(broadcast::error::RecvError::Lagged(_))) => {
451 return Err(ClientError::CommandFailed(
455 "broadcast lagged; continuation may have been missed".into(),
456 ));
457 }
458 Err(_) => return Err(ClientError::Timeout),
459 }
460 }
461}
462
463#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::{Flag, SearchQuery, StoreAction, Tls};
469 use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
470
471 async fn read_cmd_tag(server: &mut tokio::io::DuplexStream) -> String {
472 let mut buf = [0u8; 1024];
473 let n = server.read(&mut buf).await.unwrap();
474 String::from_utf8_lossy(&buf[..n])
475 .split_whitespace()
476 .next()
477 .unwrap()
478 .to_owned()
479 }
480
481 fn unauth_session(client_io: tokio::io::DuplexStream) -> Session<Unauthenticated, Tls> {
482 let raw = RawClient::new(client_io);
483 Session::<Unauthenticated, Tls>::new_in_state(raw, Capabilities::default())
484 }
485
486 fn auth_session(client_io: tokio::io::DuplexStream) -> Session<Authenticated, Tls> {
487 let raw = RawClient::new(client_io);
488 Session::<Authenticated, Tls>::new_in_state(raw, Capabilities::default())
489 }
490
491 fn selected_session(client_io: tokio::io::DuplexStream) -> Session<Selected, Tls> {
492 let raw = RawClient::new(client_io);
493 Session::<Selected, Tls>::new_in_state(raw, Capabilities::default())
494 }
495
496 fn selected_session_with_caps(
497 client_io: tokio::io::DuplexStream,
498 caps: Capabilities,
499 ) -> Session<Selected, Tls> {
500 let raw = RawClient::new(client_io);
501 Session::<Selected, Tls>::new_in_state(raw, caps)
502 }
503
504 #[tokio::test]
505 async fn test_session_login() {
506 let (client_io, mut server_io) = duplex(1024);
507 let session = unauth_session(client_io);
508
509 let login_task =
510 tokio::spawn(async move { session.login("user", Password::new("pass")).await });
511
512 let mut buf = [0u8; 1024];
514 let n = server_io.read(&mut buf).await.unwrap();
515 let cmd = String::from_utf8_lossy(&buf[..n]).into_owned();
516 assert!(cmd.contains("LOGIN \"user\" \"pass\""));
517 let tag = cmd.split_whitespace().next().unwrap().to_owned();
518
519 server_io
520 .write_all(
521 format!("{} OK [CAPABILITY IMAP4rev2 IDLE] LOGIN completed\r\n", tag).as_bytes(),
522 )
523 .await
524 .unwrap();
525
526 let auth = login_task.await.unwrap().unwrap();
527 assert!(auth.capabilities.imap4rev2);
529 assert!(auth.capabilities.idle);
530 }
531
532 #[tokio::test]
533 async fn test_session_login_falls_back_to_capability_round_trip() {
534 let (client_io, mut server_io) = duplex(1024);
535 let session = unauth_session(client_io);
536
537 let login_task =
538 tokio::spawn(async move { session.login("user", Password::new("pass")).await });
539
540 let tag = read_cmd_tag(&mut server_io).await;
541 server_io
542 .write_all(format!("{} OK LOGIN completed\r\n", tag).as_bytes())
543 .await
544 .unwrap();
545
546 let tag2 = read_cmd_tag(&mut server_io).await;
548 server_io
549 .write_all(b"* CAPABILITY IMAP4rev2 STARTTLS UNSELECT\r\n")
550 .await
551 .unwrap();
552 server_io
553 .write_all(format!("{} OK CAPABILITY done\r\n", tag2).as_bytes())
554 .await
555 .unwrap();
556
557 let auth = login_task.await.unwrap().unwrap();
558 assert!(auth.capabilities.imap4rev2);
559 assert!(auth.capabilities.unselect);
560 }
561
562 #[tokio::test]
563 async fn test_login_escapes_quote_and_backslash() {
564 let (client_io, mut server_io) = duplex(1024);
565 let session = unauth_session(client_io);
566
567 let login_task = tokio::spawn(async move {
568 session
569 .login("user\"with\\specials", Password::new("p@ss\"\\"))
570 .await
571 });
572
573 let mut buf = [0u8; 1024];
574 let n = server_io.read(&mut buf).await.unwrap();
575 let cmd = String::from_utf8_lossy(&buf[..n]).into_owned();
576 assert!(cmd.contains(r#"LOGIN "user\"with\\specials" "p@ss\"\\""#));
578 let tag = cmd.split_whitespace().next().unwrap().to_owned();
579 server_io
581 .write_all(format!("{} OK [CAPABILITY IMAP4rev2] done\r\n", tag).as_bytes())
582 .await
583 .unwrap();
584 let _ = login_task.await.unwrap().unwrap();
585 }
586
587 #[tokio::test]
588 async fn test_login_rejects_8bit_password() {
589 let (client_io, _server_io) = duplex(1024);
590 let session = unauth_session(client_io);
591 let r = session.login("user", Password::new("café")).await;
592 assert!(matches!(r, Err(ClientError::CommandFailed(_))));
593 }
594
595 #[tokio::test]
596 async fn test_login_failure() {
597 let (client_io, mut server_io) = duplex(1024);
598 let session = unauth_session(client_io);
599 let task = tokio::spawn(async move { session.login("user", Password::new("pass")).await });
600 let tag = read_cmd_tag(&mut server_io).await;
601 server_io
602 .write_all(format!("{} NO authentication failed\r\n", tag).as_bytes())
603 .await
604 .unwrap();
605 match task.await.unwrap() {
606 Err(ClientError::CommandFailed(t)) => assert!(t.contains("authentication")),
607 _ => panic!("unexpected variant"),
608 }
609 }
610
611 #[tokio::test]
612 async fn test_authenticate_plain() {
613 let (client_io, mut server_io) = duplex(1024);
614 let session = unauth_session(client_io);
615 let task = tokio::spawn(async move {
616 session
617 .authenticate_plain("user", &Password::new("pass"))
618 .await
619 });
620
621 let mut buf = [0u8; 1024];
623 let n = server_io.read(&mut buf).await.unwrap();
624 let cmd = String::from_utf8_lossy(&buf[..n]).into_owned();
625 assert!(cmd.contains("AUTHENTICATE PLAIN"));
626 let tag = cmd.split_whitespace().next().unwrap().to_owned();
627
628 server_io.write_all(b"+ \r\n").await.unwrap();
630
631 let n = server_io.read(&mut buf).await.unwrap();
633 let payload_b64 = String::from_utf8_lossy(&buf[..n]).trim_end().to_string();
634 let payload = base64::engine::general_purpose::STANDARD
635 .decode(payload_b64)
636 .unwrap();
637 assert_eq!(payload, b"\0user\0pass");
638
639 server_io
640 .write_all(format!("{} OK [CAPABILITY IMAP4rev2] auth done\r\n", tag).as_bytes())
641 .await
642 .unwrap();
643
644 let _auth = task.await.unwrap().unwrap();
645 }
646
647 #[tokio::test]
648 async fn test_authenticate_plain_failure() {
649 let (client_io, mut server_io) = duplex(1024);
650 let session = unauth_session(client_io);
651 let task = tokio::spawn(async move {
652 session
653 .authenticate_plain("user", &Password::new("pass"))
654 .await
655 });
656
657 let tag = read_cmd_tag(&mut server_io).await;
658 server_io.write_all(b"+ \r\n").await.unwrap();
659 let mut buf = [0u8; 1024];
660 let _ = server_io.read(&mut buf).await.unwrap();
661 server_io
662 .write_all(format!("{} NO bad creds\r\n", tag).as_bytes())
663 .await
664 .unwrap();
665
666 assert!(matches!(
667 task.await.unwrap(),
668 Err(ClientError::CommandFailed(_))
669 ));
670 }
671
672 #[tokio::test]
673 async fn test_session_select() {
674 let (client_io, mut server_io) = duplex(1024);
675 let session = auth_session(client_io);
676 let task = tokio::spawn(async move { session.select("INBOX").await });
677 let tag = read_cmd_tag(&mut server_io).await;
678 server_io
679 .write_all(format!("{} OK SELECT completed\r\n", tag).as_bytes())
680 .await
681 .unwrap();
682 let _selected = task.await.unwrap().unwrap();
683 }
684
685 #[tokio::test]
686 async fn test_session_examine() {
687 let (client_io, mut server_io) = duplex(1024);
688 let session = auth_session(client_io);
689 let task = tokio::spawn(async move { session.examine("INBOX").await });
690 let mut buf = [0u8; 1024];
691 let n = server_io.read(&mut buf).await.unwrap();
692 let cmd = String::from_utf8_lossy(&buf[..n]).into_owned();
693 assert!(cmd.contains("EXAMINE \"INBOX\""));
694 let tag = cmd.split_whitespace().next().unwrap();
695 server_io
696 .write_all(format!("{} OK EXAMINE completed\r\n", tag).as_bytes())
697 .await
698 .unwrap();
699 let _ = task.await.unwrap().unwrap();
700 }
701
702 #[tokio::test]
703 async fn test_session_select_failure() {
704 let (client_io, mut server_io) = duplex(1024);
705 let session = auth_session(client_io);
706 let task = tokio::spawn(async move { session.select("BadBox").await });
707 let tag = read_cmd_tag(&mut server_io).await;
708 server_io
709 .write_all(format!("{} NO no such mailbox\r\n", tag).as_bytes())
710 .await
711 .unwrap();
712 assert!(matches!(
713 task.await.unwrap(),
714 Err(ClientError::CommandFailed(_))
715 ));
716 }
717
718 #[tokio::test]
719 async fn test_session_logout() {
720 let (client_io, mut server_io) = duplex(1024);
721 let session = auth_session(client_io);
722 let task = tokio::spawn(async move { session.logout().await });
723 let tag = read_cmd_tag(&mut server_io).await;
724 server_io.write_all(b"* BYE goodbye\r\n").await.unwrap();
725 server_io
726 .write_all(format!("{} OK LOGOUT completed\r\n", tag).as_bytes())
727 .await
728 .unwrap();
729 task.await.unwrap().unwrap();
730 }
731
732 #[tokio::test]
733 async fn test_session_noop() {
734 let (client_io, mut server_io) = duplex(1024);
735 let mut session = auth_session(client_io);
736 let task = tokio::spawn(async move {
737 let r = session.noop().await;
738 (session, r)
739 });
740 let tag = read_cmd_tag(&mut server_io).await;
741 server_io
742 .write_all(format!("{} OK NOOP done\r\n", tag).as_bytes())
743 .await
744 .unwrap();
745 let (_session, r) = task.await.unwrap();
746 r.unwrap();
747 }
748
749 #[tokio::test]
750 async fn test_session_close() {
751 let (client_io, mut server_io) = duplex(1024);
752 let session = selected_session(client_io);
753 let task = tokio::spawn(async move { session.close_mailbox().await });
754 let tag = read_cmd_tag(&mut server_io).await;
755 server_io
756 .write_all(format!("{} OK CLOSE done\r\n", tag).as_bytes())
757 .await
758 .unwrap();
759 let _ = task.await.unwrap().unwrap();
760 }
761
762 #[tokio::test]
763 async fn test_session_unselect_unsupported() {
764 let (client_io, _server_io) = duplex(1024);
765 let session = selected_session(client_io);
766 match session.unselect().await {
768 Err(ClientError::UnsupportedCapability("UNSELECT")) => {}
769 _ => panic!("unexpected variant"),
770 }
771 }
772
773 #[tokio::test]
774 async fn test_session_unselect_supported() {
775 let (client_io, mut server_io) = duplex(1024);
776 let caps = Capabilities {
777 unselect: true,
778 ..Default::default()
779 };
780 let session = selected_session_with_caps(client_io, caps);
781 let task = tokio::spawn(async move { session.unselect().await });
782 let tag = read_cmd_tag(&mut server_io).await;
783 server_io
784 .write_all(format!("{} OK UNSELECT done\r\n", tag).as_bytes())
785 .await
786 .unwrap();
787 let _ = task.await.unwrap().unwrap();
788 }
789
790 #[tokio::test]
791 async fn test_session_check() {
792 let (client_io, mut server_io) = duplex(1024);
793 let mut session = selected_session(client_io);
794 let task = tokio::spawn(async move {
795 let r = session.check().await;
796 (session, r)
797 });
798 let tag = read_cmd_tag(&mut server_io).await;
799 server_io
800 .write_all(format!("{} OK CHECK done\r\n", tag).as_bytes())
801 .await
802 .unwrap();
803 let (_, r) = task.await.unwrap();
804 r.unwrap();
805 }
806
807 #[tokio::test]
808 async fn test_session_idle_unsupported() {
809 let (client_io, _server_io) = duplex(1024);
810 let mut session = selected_session(client_io);
811 match session.idle().await {
812 Err(ClientError::UnsupportedCapability("IDLE")) => {}
813 _ => panic!("unexpected variant"),
814 }
815 }
816
817 #[tokio::test]
818 async fn test_session_idle_flow() {
819 let (client_io, mut server_io) = duplex(1024);
820 let caps = Capabilities {
821 idle: true,
822 ..Default::default()
823 };
824 let mut session = selected_session_with_caps(client_io, caps);
825 let task = tokio::spawn(async move {
826 let h = session.idle().await.unwrap();
827 (session, h)
828 });
829
830 let tag = read_cmd_tag(&mut server_io).await;
831 server_io.write_all(b"+ idling\r\n").await.unwrap();
832
833 let (_session, handle) = task.await.unwrap();
834
835 let stop_task = tokio::spawn(async move { handle.stop().await });
836 let mut buf = [0u8; 1024];
837 let n = server_io.read(&mut buf).await.unwrap();
838 assert_eq!(&buf[..n], b"DONE\r\n");
839 server_io
840 .write_all(format!("{} OK IDLE done\r\n", tag).as_bytes())
841 .await
842 .unwrap();
843 stop_task.await.unwrap().unwrap();
844 }
845
846 #[tokio::test]
847 async fn test_session_search() {
848 let (client_io, mut server_io) = duplex(1024);
849 let mut session = selected_session(client_io);
850 let task = tokio::spawn(async move { session.search(SearchQuery::subject("test")).await });
851 let tag = read_cmd_tag(&mut server_io).await;
852 server_io.write_all(b"* SEARCH 1 2 3\r\n").await.unwrap();
853 server_io
854 .write_all(format!("{} OK SEARCH completed\r\n", tag).as_bytes())
855 .await
856 .unwrap();
857 assert_eq!(task.await.unwrap().unwrap(), vec![1, 2, 3]);
858 }
859
860 #[tokio::test]
861 async fn test_session_uid_search() {
862 let (client_io, mut server_io) = duplex(1024);
863 let mut session = selected_session(client_io);
864 let task = tokio::spawn(async move { session.uid_search(SearchQuery::subject("t")).await });
865 let mut buf = [0u8; 1024];
866 let n = server_io.read(&mut buf).await.unwrap();
867 let cmd = String::from_utf8_lossy(&buf[..n]).into_owned();
868 assert!(cmd.contains("UID SEARCH"));
869 let tag = cmd.split_whitespace().next().unwrap();
870 server_io.write_all(b"* SEARCH 4 5 6\r\n").await.unwrap();
871 server_io
872 .write_all(format!("{} OK done\r\n", tag).as_bytes())
873 .await
874 .unwrap();
875 assert_eq!(task.await.unwrap().unwrap(), vec![4, 5, 6]);
876 }
877
878 #[tokio::test]
879 async fn test_session_search_failure() {
880 let (client_io, mut server_io) = duplex(1024);
881 let mut session = selected_session(client_io);
882 let task = tokio::spawn(async move { session.search(SearchQuery::subject("t")).await });
883 let tag = read_cmd_tag(&mut server_io).await;
884 server_io
885 .write_all(format!("{} NO failed\r\n", tag).as_bytes())
886 .await
887 .unwrap();
888 assert!(task.await.unwrap().is_err());
889 }
890
891 #[tokio::test]
892 async fn test_session_run_search_multiple_events() {
893 let (client_io, mut server_io) = duplex(1024);
894 let mut session = selected_session(client_io);
895 let task = tokio::spawn(async move { session.search(SearchQuery::subject("t")).await });
896 let tag = read_cmd_tag(&mut server_io).await;
897 server_io.write_all(b"* SEARCH 1 2\r\n").await.unwrap();
898 server_io.write_all(b"* SEARCH 3 4\r\n").await.unwrap();
899 server_io
900 .write_all(format!("{} OK done\r\n", tag).as_bytes())
901 .await
902 .unwrap();
903 assert_eq!(task.await.unwrap().unwrap(), vec![1, 2, 3, 4]);
904 }
905
906 #[tokio::test]
907 async fn test_session_store_and_uid_store() {
908 for (kind, expected_prefix) in [("STORE", "STORE"), ("UID STORE", "UID STORE")] {
909 let (client_io, mut server_io) = duplex(1024);
910 let mut session = selected_session(client_io);
911 let task = tokio::spawn(async move {
912 if kind == "STORE" {
913 session.store("1", StoreAction::Add, &[Flag::Seen]).await
914 } else {
915 session
916 .uid_store("1", StoreAction::Add, &[Flag::Seen])
917 .await
918 }
919 });
920 let mut buf = [0u8; 1024];
921 let n = server_io.read(&mut buf).await.unwrap();
922 assert!(String::from_utf8_lossy(&buf[..n]).contains(expected_prefix));
923 let tag = String::from_utf8_lossy(&buf[..n])
924 .split_whitespace()
925 .next()
926 .unwrap()
927 .to_owned();
928 server_io
929 .write_all(format!("{} OK done\r\n", tag).as_bytes())
930 .await
931 .unwrap();
932 let _ = task.await.unwrap().unwrap();
933 }
934 }
935
936 #[tokio::test]
937 async fn test_session_expunge() {
938 let (client_io, mut server_io) = duplex(1024);
939 let mut session = selected_session(client_io);
940 let task = tokio::spawn(async move { session.expunge().await });
941 let tag = read_cmd_tag(&mut server_io).await;
942 server_io
943 .write_all(format!("{} OK done\r\n", tag).as_bytes())
944 .await
945 .unwrap();
946 let _ = task.await.unwrap().unwrap();
947 }
948
949 #[tokio::test]
950 async fn test_session_move_unsupported() {
951 let (client_io, _server_io) = duplex(1024);
952 let mut session = selected_session(client_io);
953 match session.move_messages("1", "Archive").await {
954 Err(ClientError::UnsupportedCapability("MOVE")) => {}
955 _ => panic!("unexpected variant"),
956 }
957 }
958
959 #[tokio::test]
960 async fn test_session_move_supported() {
961 let (client_io, mut server_io) = duplex(1024);
962 let caps = Capabilities {
963 move_ext: true,
964 ..Default::default()
965 };
966 let mut session = selected_session_with_caps(client_io, caps);
967 let task = tokio::spawn(async move { session.move_messages("1", "Archive").await });
968 let mut buf = [0u8; 1024];
969 let n = server_io.read(&mut buf).await.unwrap();
970 let cmd = String::from_utf8_lossy(&buf[..n]).into_owned();
971 assert!(cmd.contains(r#"MOVE 1 "Archive""#));
972 let tag = cmd.split_whitespace().next().unwrap();
973 server_io
974 .write_all(format!("{} OK done\r\n", tag).as_bytes())
975 .await
976 .unwrap();
977 let _ = task.await.unwrap().unwrap();
978 }
979
980 #[tokio::test]
981 async fn test_session_list() {
982 let (client_io, mut server_io) = duplex(1024);
983 let mut session = auth_session(client_io);
984 let task = tokio::spawn(async move { session.list("", "*").await });
985 let tag = read_cmd_tag(&mut server_io).await;
986 server_io
987 .write_all(format!("{} OK done\r\n", tag).as_bytes())
988 .await
989 .unwrap();
990 let _ = task.await.unwrap().unwrap();
991 }
992
993 #[tokio::test]
994 async fn test_session_list_failure() {
995 let (client_io, mut server_io) = duplex(1024);
996 let mut session = auth_session(client_io);
997 let task = tokio::spawn(async move { session.list("", "*").await });
998 let tag = read_cmd_tag(&mut server_io).await;
999 server_io
1000 .write_all(format!("{} NO failed\r\n", tag).as_bytes())
1001 .await
1002 .unwrap();
1003 assert!(task.await.unwrap().is_err());
1004 }
1005
1006 #[tokio::test]
1007 async fn test_session_fetch_raw() {
1008 let (client_io, mut server_io) = duplex(1024);
1009 let mut session = selected_session(client_io);
1010 let task = tokio::spawn(async move { session.fetch_raw("1", "ALL").await });
1011 let tag = read_cmd_tag(&mut server_io).await;
1012 server_io
1013 .write_all(format!("{} OK done\r\n", tag).as_bytes())
1014 .await
1015 .unwrap();
1016 let _ = task.await.unwrap().unwrap();
1017 }
1018
1019 #[tokio::test]
1020 async fn test_session_fetch_structured() {
1021 let (client_io, mut server_io) = duplex(1024);
1022 let mut session = selected_session(client_io);
1023 let task = tokio::spawn(async move { session.fetch("1", "BODY[]").await });
1024 let tag = read_cmd_tag(&mut server_io).await;
1025 server_io
1026 .write_all(b"* 1 FETCH (BODY[] {10}\r\n0123456789 UID 123)\r\n")
1027 .await
1028 .unwrap();
1029 server_io
1030 .write_all(format!("{} OK done\r\n", tag).as_bytes())
1031 .await
1032 .unwrap();
1033 let results = task.await.unwrap().unwrap();
1034 assert_eq!(results.len(), 1);
1035 assert_eq!(results[0].seq, 1);
1036 assert_eq!(results[0].uid, Some(123));
1037 assert_eq!(results[0].body.as_deref(), Some(&b"0123456789"[..]));
1038 }
1039
1040 #[tokio::test]
1041 async fn test_session_fetch_body_helper() {
1042 let (client_io, mut server_io) = duplex(1024);
1043 let mut session = selected_session(client_io);
1044 let task = tokio::spawn(async move { session.fetch_body("1").await });
1045 let tag = read_cmd_tag(&mut server_io).await;
1046 server_io
1047 .write_all(b"* 1 FETCH (BODY[] {10}\r\n0123456789)\r\n")
1048 .await
1049 .unwrap();
1050 server_io
1051 .write_all(format!("{} OK done\r\n", tag).as_bytes())
1052 .await
1053 .unwrap();
1054 assert_eq!(task.await.unwrap().unwrap(), Some("0123456789".to_string()));
1055 }
1056
1057 #[tokio::test]
1058 async fn test_session_transition_transport() {
1059 let (client_io, _server_io) = duplex(1024);
1060 let session = unauth_session(client_io);
1061 let _ = session.transition_transport::<crate::PlainText>();
1062 }
1063
1064 #[tokio::test]
1065 async fn test_session_events() {
1066 let (client_io, _server_io) = duplex(1024);
1067 let session = unauth_session(client_io);
1068 let _ = session.events();
1069 }
1070
1071 #[tokio::test]
1072 async fn test_refresh_capabilities_explicit() {
1073 let (client_io, mut server_io) = duplex(1024);
1074 let mut session = unauth_session(client_io);
1075 let task = tokio::spawn(async move {
1076 let r = session.refresh_capabilities().await;
1077 (session, r)
1078 });
1079 let tag = read_cmd_tag(&mut server_io).await;
1080 server_io
1081 .write_all(b"* CAPABILITY IMAP4rev2 IDLE\r\n")
1082 .await
1083 .unwrap();
1084 server_io
1085 .write_all(format!("{} OK done\r\n", tag).as_bytes())
1086 .await
1087 .unwrap();
1088 let (session, r) = task.await.unwrap();
1089 r.unwrap();
1090 assert!(session.capabilities.imap4rev2);
1091 assert!(session.capabilities.idle);
1092 }
1093}