1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::ops::{Deref, DerefMut};
4use std::pin::Pin;
5use std::str;
6
7use async_channel::{self as channel, bounded};
8#[cfg(feature = "runtime-async-std")]
9use async_std::io::{Read, Write, WriteExt};
10use base64::Engine as _;
11use extensions::id::{format_identification, parse_id};
12use extensions::quota::parse_get_quota_root;
13use futures_util::{Stream, TryStreamExt, io};
14use imap_proto::rfc5464::Metadata;
15use imap_proto::{RequestId, Response};
16#[cfg(feature = "runtime-tokio")]
17use tokio::io::{AsyncRead as Read, AsyncWrite as Write, AsyncWriteExt};
18
19use super::authenticator::Authenticator;
20use super::error::{Error, ParseError, Result, ValidateError};
21use super::parse::*;
22use super::types::*;
23use crate::extensions::{self, quota::parse_get_quota};
24use crate::imap_stream::ImapStream;
25
26macro_rules! quote {
27 ($x:expr) => {
28 format!("\"{}\"", $x.replace(r"\", r"\\").replace("\"", "\\\""))
29 };
30}
31
32#[derive(Debug)]
42pub struct Session<T: Read + Write + Unpin + fmt::Debug> {
43 pub(crate) conn: Connection<T>,
44 pub(crate) unsolicited_responses_tx: channel::Sender<UnsolicitedResponse>,
45
46 pub unsolicited_responses: channel::Receiver<UnsolicitedResponse>,
49}
50
51impl<T: Read + Write + Unpin + fmt::Debug> Unpin for Session<T> {}
52impl<T: Read + Write + Unpin + fmt::Debug> Unpin for Client<T> {}
53impl<T: Read + Write + Unpin + fmt::Debug> Unpin for Connection<T> {}
54
55impl<T: Read + Write + Unpin + fmt::Debug> AsMut<T> for Session<T> {
58 fn as_mut(&mut self) -> &mut T {
59 self.conn.stream.as_mut()
60 }
61}
62
63#[derive(Debug)]
71pub struct Client<T: Read + Write + Unpin + fmt::Debug> {
72 conn: Connection<T>,
73}
74
75#[derive(Debug)]
78pub struct Connection<T: Read + Write + Unpin + fmt::Debug> {
79 pub(crate) stream: ImapStream<T>,
80
81 pub(crate) request_ids: IdGenerator,
83}
84
85impl<T: Read + Write + Unpin + fmt::Debug> Deref for Client<T> {
88 type Target = Connection<T>;
89
90 fn deref(&self) -> &Connection<T> {
91 &self.conn
92 }
93}
94
95impl<T: Read + Write + Unpin + fmt::Debug> DerefMut for Client<T> {
96 fn deref_mut(&mut self) -> &mut Connection<T> {
97 &mut self.conn
98 }
99}
100
101impl<T: Read + Write + Unpin + fmt::Debug> Deref for Session<T> {
102 type Target = Connection<T>;
103
104 fn deref(&self) -> &Connection<T> {
105 &self.conn
106 }
107}
108
109impl<T: Read + Write + Unpin + fmt::Debug> DerefMut for Session<T> {
110 fn deref_mut(&mut self) -> &mut Connection<T> {
111 &mut self.conn
112 }
113}
114
115macro_rules! ok_or_unauth_client_err {
123 ($r:expr, $self:expr) => {
124 match $r {
125 Ok(o) => o,
126 Err(e) => return Err((e.into(), $self)),
127 }
128 };
129}
130
131impl<T: Read + Write + Unpin + fmt::Debug + Send> Client<T> {
132 pub fn new(stream: T) -> Client<T> {
137 let stream = ImapStream::new(stream);
138
139 Client {
140 conn: Connection {
141 stream,
142 request_ids: IdGenerator::new(),
143 },
144 }
145 }
146
147 pub fn into_inner(self) -> T {
149 let Self { conn, .. } = self;
150 conn.into_inner()
151 }
152
153 pub async fn login<U: AsRef<str>, P: AsRef<str>>(
185 self,
186 username: U,
187 password: P,
188 ) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
189 let (session, _capabilities) = self.login_with_capabilities(username, password).await?;
190 Ok(session)
191 }
192
193 pub async fn login_with_capabilities<U: AsRef<str>, P: AsRef<str>>(
199 mut self,
200 username: U,
201 password: P,
202 ) -> ::std::result::Result<(Session<T>, Option<Capabilities>), (Error, Client<T>)> {
203 let u = ok_or_unauth_client_err!(validate_str(username.as_ref()), self);
204 let p = ok_or_unauth_client_err!(validate_str(password.as_ref()), self);
205
206 let id = ok_or_unauth_client_err!(self.run_command(&format!("LOGIN {u} {p}")).await, self);
207 loop {
208 let Some(res) = ok_or_unauth_client_err!(self.stream.try_next().await, self) else {
209 return Err((Error::ConnectionLost, self));
210 };
211
212 if let Response::Done {
213 status,
214 outcome,
215 tag,
216 } = res.parsed()
217 && *tag == id
218 {
219 ok_or_unauth_client_err!(
220 self.check_status_ok(
221 status,
222 outcome.code.as_ref(),
223 outcome.information.as_deref()
224 ),
225 self
226 );
227
228 let capabilities =
229 if let Some(imap_proto::ResponseCode::Capabilities(capabilities)) =
230 &outcome.code
231 {
232 use crate::types::{Capabilities, Capability};
233 let capability_set: HashSet<Capability> =
234 capabilities.iter().map(Capability::from).collect();
235 Some(Capabilities(capability_set))
236 } else {
237 None
238 };
239 return Ok((Session::new(self.conn), capabilities));
240 }
241 }
242 }
243
244 pub async fn authenticate<A: Authenticator, S: AsRef<str>>(
288 mut self,
289 auth_type: S,
290 authenticator: A,
291 ) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
292 let id = ok_or_unauth_client_err!(
293 self.run_command(&format!("AUTHENTICATE {}", auth_type.as_ref()))
294 .await,
295 self
296 );
297 let session = self.do_auth_handshake(id, authenticator).await?;
298 Ok(session)
299 }
300
301 async fn do_auth_handshake<A: Authenticator>(
303 mut self,
304 id: RequestId,
305 mut authenticator: A,
306 ) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
307 loop {
310 let Some(res) = ok_or_unauth_client_err!(self.read_response().await, self) else {
311 return Err((Error::ConnectionLost, self));
312 };
313 match res.parsed() {
314 Response::Continue(outcome) => {
315 let challenge = if let Some(text) = &outcome.information {
316 ok_or_unauth_client_err!(
317 base64::engine::general_purpose::STANDARD
318 .decode(text.as_ref())
319 .map_err(|e| Error::Parse(ParseError::Authentication(
320 (*text).to_string(),
321 Some(e)
322 ))),
323 self
324 )
325 } else {
326 Vec::new()
327 };
328 let raw_response = &mut authenticator.process(&challenge);
329 let auth_response =
330 base64::engine::general_purpose::STANDARD.encode(raw_response);
331
332 ok_or_unauth_client_err!(
333 self.conn.run_command_untagged(&auth_response).await,
334 self
335 );
336 }
337 _ => {
338 ok_or_unauth_client_err!(self.check_done_ok_from(&id, None, res).await, self);
339 return Ok(Session::new(self.conn));
340 }
341 }
342 }
343 }
344}
345
346impl<T: Read + Write + Unpin + fmt::Debug + Send> Session<T> {
347 unsafe_pinned!(conn: Connection<T>);
348
349 pub(crate) fn get_stream(self: Pin<&mut Self>) -> Pin<&mut ImapStream<T>> {
350 self.conn().stream()
351 }
352
353 fn new(conn: Connection<T>) -> Self {
355 let (tx, rx) = bounded(100);
356 Session {
357 conn,
358 unsolicited_responses: rx,
359 unsolicited_responses_tx: tx,
360 }
361 }
362
363 pub async fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
381 let id = self
383 .run_command(&format!("SELECT {}", validate_str(mailbox_name.as_ref())?))
384 .await?;
385 let mbox = parse_mailbox(
386 &mut self.conn.stream,
387 self.unsolicited_responses_tx.clone(),
388 id,
389 )
390 .await?;
391
392 Ok(mbox)
393 }
394
395 pub async fn select_condstore<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
398 let id = self
399 .run_command(&format!(
400 "SELECT {} (CONDSTORE)",
401 validate_str(mailbox_name.as_ref())?
402 ))
403 .await?;
404 let mbox = parse_mailbox(
405 &mut self.conn.stream,
406 self.unsolicited_responses_tx.clone(),
407 id,
408 )
409 .await?;
410
411 Ok(mbox)
412 }
413
414 pub async fn examine<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
419 let id = self
420 .run_command(&format!("EXAMINE {}", validate_str(mailbox_name.as_ref())?))
421 .await?;
422 let mbox = parse_mailbox(
423 &mut self.conn.stream,
424 self.unsolicited_responses_tx.clone(),
425 id,
426 )
427 .await?;
428
429 Ok(mbox)
430 }
431
432 pub async fn fetch<S1, S2>(
491 &mut self,
492 sequence_set: S1,
493 query: S2,
494 ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send>
495 where
496 S1: AsRef<str>,
497 S2: AsRef<str>,
498 {
499 let id = self
500 .run_command(&format!(
501 "FETCH {} {}",
502 sequence_set.as_ref(),
503 query.as_ref()
504 ))
505 .await?;
506 let res = parse_fetches(
507 &mut self.conn.stream,
508 self.unsolicited_responses_tx.clone(),
509 id,
510 );
511
512 Ok(res)
513 }
514
515 pub async fn uid_fetch<S1, S2>(
518 &mut self,
519 uid_set: S1,
520 query: S2,
521 ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send + Unpin>
522 where
523 S1: AsRef<str>,
524 S2: AsRef<str>,
525 {
526 let id = self
527 .run_command(&format!(
528 "UID FETCH {} {}",
529 uid_set.as_ref(),
530 query.as_ref()
531 ))
532 .await?;
533 let res = parse_fetches(
534 &mut self.conn.stream,
535 self.unsolicited_responses_tx.clone(),
536 id,
537 );
538 Ok(res)
539 }
540
541 pub async fn noop(&mut self) -> Result<()> {
543 let id = self.run_command("NOOP").await?;
544 parse_noop(
545 &mut self.conn.stream,
546 self.unsolicited_responses_tx.clone(),
547 id,
548 )
549 .await?;
550 Ok(())
551 }
552
553 pub async fn logout(&mut self) -> Result<()> {
555 self.run_command_and_check_ok("LOGOUT").await?;
556 Ok(())
557 }
558
559 pub async fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
582 self.run_command_and_check_ok(&format!("CREATE {}", validate_str(mailbox_name.as_ref())?))
583 .await?;
584
585 Ok(())
586 }
587
588 pub async fn delete<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
608 self.run_command_and_check_ok(&format!("DELETE {}", validate_str(mailbox_name.as_ref())?))
609 .await?;
610
611 Ok(())
612 }
613
614 pub async fn rename<S1: AsRef<str>, S2: AsRef<str>>(&mut self, from: S1, to: S2) -> Result<()> {
640 self.run_command_and_check_ok(&format!(
641 "RENAME {} {}",
642 validate_str(from.as_ref())?,
643 validate_str(to.as_ref())?
644 ))
645 .await?;
646
647 Ok(())
648 }
649
650 pub async fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
659 self.run_command_and_check_ok(&format!("SUBSCRIBE {}", validate_str(mailbox.as_ref())?))
660 .await?;
661 Ok(())
662 }
663
664 pub async fn unsubscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
669 self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", validate_str(mailbox.as_ref())?))
670 .await?;
671 Ok(())
672 }
673
674 pub async fn capabilities(&mut self) -> Result<Capabilities> {
678 let id = self.run_command("CAPABILITY").await?;
679 let c = parse_capabilities(
680 &mut self.conn.stream,
681 self.unsolicited_responses_tx.clone(),
682 id,
683 )
684 .await?;
685 Ok(c)
686 }
687
688 pub async fn expunge(&mut self) -> Result<impl Stream<Item = Result<Seq>> + '_ + Send> {
692 let id = self.run_command("EXPUNGE").await?;
693 let res = parse_expunge(
694 &mut self.conn.stream,
695 self.unsolicited_responses_tx.clone(),
696 id,
697 );
698 Ok(res)
699 }
700
701 pub async fn uid_expunge<S: AsRef<str>>(
724 &mut self,
725 uid_set: S,
726 ) -> Result<impl Stream<Item = Result<Uid>> + '_ + Send> {
727 let id = self
728 .run_command(&format!("UID EXPUNGE {}", uid_set.as_ref()))
729 .await?;
730 let res = parse_expunge(
731 &mut self.conn.stream,
732 self.unsolicited_responses_tx.clone(),
733 id,
734 );
735 Ok(res)
736 }
737
738 pub async fn check(&mut self) -> Result<()> {
749 self.run_command_and_check_ok("CHECK").await?;
750 Ok(())
751 }
752
753 pub async fn close(&mut self) -> Result<()> {
769 self.run_command_and_check_ok("CLOSE").await?;
770 Ok(())
771 }
772
773 pub async fn store<S1, S2>(
823 &mut self,
824 sequence_set: S1,
825 query: S2,
826 ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send>
827 where
828 S1: AsRef<str>,
829 S2: AsRef<str>,
830 {
831 let id = self
832 .run_command(&format!(
833 "STORE {} {}",
834 sequence_set.as_ref(),
835 query.as_ref()
836 ))
837 .await?;
838 let res = parse_fetches(
839 &mut self.conn.stream,
840 self.unsolicited_responses_tx.clone(),
841 id,
842 );
843 Ok(res)
844 }
845
846 pub async fn uid_store<S1, S2>(
849 &mut self,
850 uid_set: S1,
851 query: S2,
852 ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send>
853 where
854 S1: AsRef<str>,
855 S2: AsRef<str>,
856 {
857 let id = self
858 .run_command(&format!(
859 "UID STORE {} {}",
860 uid_set.as_ref(),
861 query.as_ref()
862 ))
863 .await?;
864 let res = parse_fetches(
865 &mut self.conn.stream,
866 self.unsolicited_responses_tx.clone(),
867 id,
868 );
869 Ok(res)
870 }
871
872 pub async fn copy<S1: AsRef<str>, S2: AsRef<str>>(
880 &mut self,
881 sequence_set: S1,
882 mailbox_name: S2,
883 ) -> Result<()> {
884 self.run_command_and_check_ok(&format!(
885 "COPY {} {}",
886 sequence_set.as_ref(),
887 validate_str(mailbox_name.as_ref())?
888 ))
889 .await?;
890
891 Ok(())
892 }
893
894 pub async fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
897 &mut self,
898 uid_set: S1,
899 mailbox_name: S2,
900 ) -> Result<()> {
901 self.run_command_and_check_ok(&format!(
902 "UID COPY {} {}",
903 uid_set.as_ref(),
904 validate_str(mailbox_name.as_ref())?
905 ))
906 .await?;
907
908 Ok(())
909 }
910
911 pub async fn mv<S1: AsRef<str>, S2: AsRef<str>>(
942 &mut self,
943 sequence_set: S1,
944 mailbox_name: S2,
945 ) -> Result<()> {
946 self.run_command_and_check_ok(&format!(
947 "MOVE {} {}",
948 sequence_set.as_ref(),
949 validate_str(mailbox_name.as_ref())?
950 ))
951 .await?;
952
953 Ok(())
954 }
955
956 pub async fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
961 &mut self,
962 uid_set: S1,
963 mailbox_name: S2,
964 ) -> Result<()> {
965 self.run_command_and_check_ok(&format!(
966 "UID MOVE {} {}",
967 uid_set.as_ref(),
968 validate_str(mailbox_name.as_ref())?
969 ))
970 .await?;
971
972 Ok(())
973 }
974
975 pub async fn list(
1007 &mut self,
1008 reference_name: Option<&str>,
1009 mailbox_pattern: Option<&str>,
1010 ) -> Result<impl Stream<Item = Result<Name>> + '_ + Send + use<'_, T>> {
1011 let id = self
1012 .run_command(&format!(
1013 "LIST {} {}",
1014 validate_str(reference_name.unwrap_or(""))?,
1015 mailbox_pattern.unwrap_or("\"\"")
1016 ))
1017 .await?;
1018 let names = parse_names(
1019 &mut self.conn.stream,
1020 self.unsolicited_responses_tx.clone(),
1021 id,
1022 );
1023
1024 Ok(names)
1025 }
1026
1027 pub async fn lsub(
1043 &mut self,
1044 reference_name: Option<&str>,
1045 mailbox_pattern: Option<&str>,
1046 ) -> Result<impl Stream<Item = Result<Name>> + '_ + Send + use<'_, T>> {
1047 let id = self
1048 .run_command(&format!(
1049 "LSUB {} {}",
1050 validate_str(reference_name.unwrap_or(""))?,
1051 validate_str(mailbox_pattern.unwrap_or(""))?
1052 ))
1053 .await?;
1054 let names = parse_names(
1055 &mut self.conn.stream,
1056 self.unsolicited_responses_tx.clone(),
1057 id,
1058 );
1059
1060 Ok(names)
1061 }
1062
1063 pub async fn status<S1: AsRef<str>, S2: AsRef<str>>(
1098 &mut self,
1099 mailbox_name: S1,
1100 data_items: S2,
1101 ) -> Result<Mailbox> {
1102 let id = self
1103 .run_command(&format!(
1104 "STATUS {} {}",
1105 validate_str(mailbox_name.as_ref())?,
1106 data_items.as_ref()
1107 ))
1108 .await?;
1109 let mbox = parse_status(
1110 &mut self.conn.stream,
1111 mailbox_name.as_ref(),
1112 self.unsolicited_responses_tx.clone(),
1113 id,
1114 )
1115 .await?;
1116 Ok(mbox)
1117 }
1118
1119 pub fn idle(self) -> extensions::idle::Handle<T> {
1138 extensions::idle::Handle::new(self)
1139 }
1140
1141 pub async fn append(
1161 &mut self,
1162 mailbox: impl AsRef<str>,
1163 flags: Option<&str>,
1164 internaldate: Option<&str>,
1165 content: impl AsRef<[u8]>,
1166 ) -> Result<()> {
1167 let content = content.as_ref();
1168 let id = self
1169 .run_command(&format!(
1170 "APPEND {}{}{}{}{} {{{}}}",
1171 validate_str(mailbox.as_ref())?,
1172 if flags.is_some() { " " } else { "" },
1173 flags.unwrap_or(""),
1174 if internaldate.is_some() { " " } else { "" },
1175 internaldate.unwrap_or(""),
1176 content.len()
1177 ))
1178 .await?;
1179
1180 let Some(res) = self.read_response().await? else {
1181 return Err(Error::Append);
1182 };
1183 let Response::Continue { .. } = res.parsed() else {
1184 return Err(Error::Append);
1185 };
1186
1187 self.stream.as_mut().write_all(content).await?;
1188 self.stream.as_mut().write_all(b"\r\n").await?;
1189 self.stream.flush().await?;
1190 self.conn
1191 .check_done_ok(&id, Some(self.unsolicited_responses_tx.clone()))
1192 .await?;
1193 Ok(())
1194 }
1195
1196 pub async fn search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Seq>> {
1241 let id = self
1242 .run_command(&format!("SEARCH {}", query.as_ref()))
1243 .await?;
1244 let seqs = parse_ids(
1245 &mut self.conn.stream,
1246 self.unsolicited_responses_tx.clone(),
1247 id,
1248 )
1249 .await?;
1250
1251 Ok(seqs)
1252 }
1253
1254 pub async fn uid_search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Uid>> {
1258 let id = self
1259 .run_command(&format!("UID SEARCH {}", query.as_ref()))
1260 .await?;
1261 let uids = parse_ids(
1262 &mut self.conn.stream,
1263 self.unsolicited_responses_tx.clone(),
1264 id,
1265 )
1266 .await?;
1267
1268 Ok(uids)
1269 }
1270
1271 pub async fn get_quota(&mut self, quota_root: &str) -> Result<Quota> {
1273 let id = self
1274 .run_command(format!("GETQUOTA {}", validate_str(quota_root)?))
1275 .await?;
1276 let c = parse_get_quota(
1277 &mut self.conn.stream,
1278 self.unsolicited_responses_tx.clone(),
1279 id,
1280 )
1281 .await?;
1282 Ok(c)
1283 }
1284
1285 pub async fn get_quota_root(
1287 &mut self,
1288 mailbox_name: &str,
1289 ) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
1290 let id = self
1291 .run_command(format!("GETQUOTAROOT {}", validate_str(mailbox_name)?))
1292 .await?;
1293 let c = parse_get_quota_root(
1294 &mut self.conn.stream,
1295 self.unsolicited_responses_tx.clone(),
1296 id,
1297 )
1298 .await?;
1299 Ok(c)
1300 }
1301
1302 pub async fn get_metadata(
1304 &mut self,
1305 mailbox_name: &str,
1306 options: &str,
1307 entry_specifier: &str,
1308 ) -> Result<Vec<Metadata>> {
1309 let options = if options.is_empty() {
1310 String::new()
1311 } else {
1312 format!(" {options}")
1313 };
1314 let id = self
1315 .run_command(format!(
1316 "GETMETADATA {} {}{}",
1317 validate_str(mailbox_name)?,
1318 options,
1319 entry_specifier
1320 ))
1321 .await?;
1322 let metadata = parse_metadata(
1323 &mut self.conn.stream,
1324 mailbox_name,
1325 self.unsolicited_responses_tx.clone(),
1326 id,
1327 )
1328 .await?;
1329 Ok(metadata)
1330 }
1331
1332 pub async fn id(
1336 &mut self,
1337 identification: impl IntoIterator<Item = (&str, Option<&str>)>,
1338 ) -> Result<Option<HashMap<String, String>>> {
1339 let id = self
1340 .run_command(format!("ID ({})", format_identification(identification)))
1341 .await?;
1342 let server_identification = parse_id(
1343 &mut self.conn.stream,
1344 self.unsolicited_responses_tx.clone(),
1345 id,
1346 )
1347 .await?;
1348 Ok(server_identification)
1349 }
1350
1351 pub async fn id_nil(&mut self) -> Result<Option<HashMap<String, String>>> {
1355 let id = self.run_command("ID NIL").await?;
1356 let server_identification = parse_id(
1357 &mut self.conn.stream,
1358 self.unsolicited_responses_tx.clone(),
1359 id,
1360 )
1361 .await?;
1362 Ok(server_identification)
1363 }
1364
1365 pub async fn run_command_and_check_ok<S: AsRef<str>>(&mut self, command: S) -> Result<()> {
1368 self.conn
1369 .run_command_and_check_ok(
1370 command.as_ref(),
1371 Some(self.unsolicited_responses_tx.clone()),
1372 )
1373 .await?;
1374
1375 Ok(())
1376 }
1377
1378 pub async fn run_command<S: AsRef<str>>(&mut self, command: S) -> Result<RequestId> {
1380 let id = self.conn.run_command(command.as_ref()).await?;
1381
1382 Ok(id)
1383 }
1384
1385 pub async fn run_command_untagged<S: AsRef<str>>(&mut self, command: S) -> Result<()> {
1387 self.conn.run_command_untagged(command.as_ref()).await?;
1388
1389 Ok(())
1390 }
1391
1392 pub async fn read_response(&mut self) -> io::Result<Option<ResponseData>> {
1394 self.conn.read_response().await
1395 }
1396}
1397
1398impl<T: Read + Write + Unpin + fmt::Debug> Connection<T> {
1399 unsafe_pinned!(stream: ImapStream<T>);
1400
1401 pub fn get_ref(&self) -> &T {
1403 self.stream.get_ref()
1404 }
1405
1406 pub fn get_mut(&mut self) -> &mut T {
1408 self.stream.get_mut()
1409 }
1410
1411 pub fn into_inner(self) -> T {
1413 let Self { stream, .. } = self;
1414 stream.into_inner()
1415 }
1416
1417 pub async fn read_response(&mut self) -> io::Result<Option<ResponseData>> {
1419 self.stream.try_next().await
1420 }
1421
1422 pub(crate) async fn run_command_untagged(&mut self, command: &str) -> Result<()> {
1423 self.stream
1424 .encode(Request(None, command.as_bytes().into()))
1425 .await?;
1426 self.stream.flush().await?;
1427 Ok(())
1428 }
1429
1430 pub(crate) async fn run_command(&mut self, command: &str) -> Result<RequestId> {
1431 let request_id = self.request_ids.next().unwrap(); self.stream
1433 .encode(Request(Some(request_id.clone()), command.as_bytes().into()))
1434 .await?;
1435 self.stream.flush().await?;
1436 Ok(request_id)
1437 }
1438
1439 pub async fn run_command_and_check_ok(
1441 &mut self,
1442 command: &str,
1443 unsolicited: Option<channel::Sender<UnsolicitedResponse>>,
1444 ) -> Result<()> {
1445 let id = self.run_command(command).await?;
1446 self.check_done_ok(&id, unsolicited).await?;
1447
1448 Ok(())
1449 }
1450
1451 pub(crate) async fn check_done_ok(
1452 &mut self,
1453 id: &RequestId,
1454 unsolicited: Option<channel::Sender<UnsolicitedResponse>>,
1455 ) -> Result<()> {
1456 match self.stream.try_next().await? {
1457 Some(first_res) => self.check_done_ok_from(id, unsolicited, first_res).await,
1458 _ => Err(Error::ConnectionLost),
1459 }
1460 }
1461
1462 pub(crate) async fn check_done_ok_from(
1463 &mut self,
1464 id: &RequestId,
1465 unsolicited: Option<channel::Sender<UnsolicitedResponse>>,
1466 mut response: ResponseData,
1467 ) -> Result<()> {
1468 loop {
1469 if let Response::Done {
1470 tag,
1471 status,
1472 outcome,
1473 } = response.parsed()
1474 {
1475 self.check_status_ok(
1476 status,
1477 outcome.code.as_ref(),
1478 outcome.information.as_deref(),
1479 )?;
1480
1481 if tag == id {
1482 return Ok(());
1483 }
1484 }
1485
1486 if let Some(unsolicited) = unsolicited.clone() {
1487 handle_unilateral(response, unsolicited);
1488 }
1489
1490 let Some(res) = self.stream.try_next().await? else {
1491 return Err(Error::ConnectionLost);
1492 };
1493 response = res;
1494 }
1495 }
1496
1497 pub(crate) fn check_status_ok(
1498 &self,
1499 status: &imap_proto::Status,
1500 code: Option<&imap_proto::ResponseCode<'_>>,
1501 information: Option<&str>,
1502 ) -> Result<()> {
1503 use imap_proto::Status;
1504 match status {
1505 Status::Ok => Ok(()),
1506 Status::Bad => Err(Error::Bad(format!("code: {code:?}, info: {information:?}"))),
1507 Status::No => Err(Error::No(format!("code: {code:?}, info: {information:?}"))),
1508 _ => Err(Error::Io(io::Error::other(format!(
1509 "status: {status:?}, code: {code:?}, information: {information:?}"
1510 )))),
1511 }
1512 }
1513}
1514
1515fn validate_str(value: &str) -> Result<String> {
1516 let quoted = quote!(value);
1517 if quoted.find('\n').is_some() {
1518 return Err(Error::Validate(ValidateError('\n')));
1519 }
1520 if quoted.find('\r').is_some() {
1521 return Err(Error::Validate(ValidateError('\r')));
1522 }
1523 Ok(quoted)
1524}
1525
1526#[cfg(test)]
1527mod tests {
1528 use pretty_assertions::assert_eq;
1529
1530 use super::super::error::Result;
1531 use super::super::mock_stream::MockStream;
1532 use super::*;
1533 use std::borrow::Cow;
1534 use std::future::Future;
1535
1536 use async_std::sync::{Arc, Mutex};
1537 use futures_util::StreamExt;
1538 use imap_proto::Status;
1539
1540 macro_rules! mock_client {
1541 ($s:expr) => {
1542 Client::new($s)
1543 };
1544 }
1545
1546 macro_rules! mock_session {
1547 ($s:expr) => {
1548 Session::new(mock_client!($s).conn)
1549 };
1550 }
1551
1552 macro_rules! assert_eq_bytes {
1553 ($a:expr, $b:expr, $c:expr) => {
1554 assert_eq!(
1555 std::str::from_utf8($a).unwrap(),
1556 std::str::from_utf8($b).unwrap(),
1557 $c
1558 )
1559 };
1560 }
1561
1562 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1563 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1564 async fn fetch_body() {
1565 let response = "a0 OK Logged in.\r\n\
1566 * 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n\
1567 a0 OK FETCH completed\r\n";
1568 let mut session = mock_session!(MockStream::new(response.as_bytes().to_vec()));
1569 session.read_response().await.unwrap().unwrap();
1570 session.read_response().await.unwrap().unwrap();
1571 }
1572
1573 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1574 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1575 async fn readline_delay_read() {
1576 let greeting = "* OK Dovecot ready.\r\n";
1577 let mock_stream = MockStream::default()
1578 .with_buf(greeting.as_bytes().to_vec())
1579 .with_delay();
1580
1581 let mut client = mock_client!(mock_stream);
1582 let actual_response = client.read_response().await.unwrap().unwrap();
1583 assert_eq!(
1584 actual_response.parsed(),
1585 &Response::Data {
1586 status: Status::Ok,
1587 outcome: imap_proto::Outcome {
1588 code: None,
1589 information: Some(Cow::Borrowed("Dovecot ready."))
1590 },
1591 }
1592 );
1593 }
1594
1595 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1596 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1597 async fn readline_eof() {
1598 let mock_stream = MockStream::default().with_eof();
1599 let mut client = mock_client!(mock_stream);
1600 let res = client.read_response().await.unwrap();
1601 assert!(res.is_none());
1602 }
1603
1604 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1605 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1606 #[should_panic]
1607 async fn readline_err() {
1608 let mock_stream = MockStream::default().with_err();
1610 let mut client = mock_client!(mock_stream);
1611 client.read_response().await.unwrap().unwrap();
1612 }
1613
1614 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1615 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1616 async fn authenticate() {
1617 let response = b"+ YmFy\r\n\
1618 A0001 OK Logged in\r\n"
1619 .to_vec();
1620 let command = "A0001 AUTHENTICATE PLAIN\r\n\
1621 Zm9v\r\n";
1622 let mock_stream = MockStream::new(response);
1623 let client = mock_client!(mock_stream);
1624 enum Authenticate {
1625 Auth,
1626 }
1627 impl Authenticator for &Authenticate {
1628 type Response = Vec<u8>;
1629 fn process(&mut self, challenge: &[u8]) -> Self::Response {
1630 assert!(challenge == b"bar", "Invalid authenticate challenge");
1631 b"foo".to_vec()
1632 }
1633 }
1634 let session = client
1635 .authenticate("PLAIN", &Authenticate::Auth)
1636 .await
1637 .ok()
1638 .unwrap();
1639 assert_eq_bytes!(
1640 &session.stream.inner.written_buf,
1641 command.as_bytes(),
1642 "Invalid authenticate command"
1643 );
1644 }
1645
1646 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1647 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1648 async fn login() {
1649 let response = b"A0001 OK Logged in\r\n".to_vec();
1650 let username = "username";
1651 let password = "password";
1652 let command = format!("A0001 LOGIN {} {}\r\n", quote!(username), quote!(password));
1653 let mock_stream = MockStream::new(response);
1654 let client = mock_client!(mock_stream);
1655 match client.login(username, password).await {
1656 Ok(session) => {
1657 assert_eq!(
1658 session.stream.inner.written_buf,
1659 command.as_bytes().to_vec(),
1660 "Invalid login command"
1661 );
1662 }
1663 _ => {
1664 unreachable!("invalid login");
1665 }
1666 }
1667 }
1668
1669 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1670 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1671 async fn login_ignores_completion_for_other_command_tag() {
1672 let response = b"A9999 NO Other command rejected\r\n\
1673 A0001 OK Logged in\r\n"
1674 .to_vec();
1675 let client = mock_client!(MockStream::new(response));
1676
1677 let result = client.login("username", "password").await;
1678
1679 assert!(
1680 result.is_ok(),
1681 "LOGIN must use only its matching completion"
1682 );
1683 }
1684
1685 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1686 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1687 async fn login_with_capabilities() {
1688 let response = b"A0001 OK [CAPABILITY IMAP4rev1 IDLE MOVE] Logged in\r\n".to_vec();
1689 let username = "username";
1690 let password = "password";
1691 let command = format!("A0001 LOGIN {} {}\r\n", quote!(username), quote!(password));
1692 let mock_stream = MockStream::new(response);
1693 let client = mock_client!(mock_stream);
1694 if let Ok((session, capabilities)) =
1695 client.login_with_capabilities(username, password).await
1696 {
1697 assert_eq!(
1698 session.stream.inner.written_buf,
1699 command.as_bytes().to_vec(),
1700 "Invalid login command"
1701 );
1702 let capabilities = capabilities.expect("Capabilities should not be None");
1703 assert_eq!(capabilities.len(), 3);
1704 assert!(capabilities.has(&Capability::Imap4rev1));
1705 assert!(capabilities.has(&Capability::Atom("IDLE".to_string())));
1706 assert!(capabilities.has(&Capability::Atom("MOVE".to_string())));
1707 assert!(!capabilities.has(&Capability::Atom("ID".to_string())));
1708 } else {
1709 unreachable!("invalid login");
1710 }
1711 }
1712
1713 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1715 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1716 async fn banner_en_dash() {
1717 let response = b"* OK [CAPABILITY IMAP4rev1 LOGIN-REFERRALS ID ENABLE IDLE SASL-IR LITERAL+ AUTH=PLAIN AUTH=LOGIN AUTH=XOAUTH2] Logged in \xe2\x80\x93 go ahead!\r\n";
1718 let mock_stream = MockStream::new(response.to_vec());
1719 let mut session = mock_session!(mock_stream);
1720 let response_data = session.read_response().await.unwrap().unwrap();
1721 let Response::Data { status, .. } = response_data.parsed() else {
1722 panic!("Unexpected parsing result: {response_data:?}");
1723 };
1724 assert!(matches!(status, imap_proto::rfc3501::Status::Ok));
1725 }
1726
1727 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1729 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1730 async fn login_with_capabilities_and_en_dash() {
1731 let response = b"A0001 OK [CAPABILITY IMAP4rev1 LOGIN-REFERRALS ID ENABLE IDLE SASL-IR LITERAL+ AUTH=PLAIN AUTH=LOGIN AUTH=XOAUTH2] Logged in \xe2\x80\x93 go ahead!\r\n".to_vec();
1732 let username = "username";
1733 let password = "password";
1734 let command = format!("A0001 LOGIN {} {}\r\n", quote!(username), quote!(password));
1735 let mock_stream = MockStream::new(response);
1736 let client = mock_client!(mock_stream);
1737 let (session, capabilities) = client
1738 .login_with_capabilities(username, password)
1739 .await
1740 .unwrap();
1741
1742 assert_eq!(
1743 session.stream.inner.written_buf,
1744 command.as_bytes().to_vec(),
1745 "Invalid login command"
1746 );
1747 let capabilities = capabilities.expect("Capabilities should not be None");
1748 assert_eq!(capabilities.len(), 10);
1749 assert!(capabilities.has(&Capability::Imap4rev1));
1750 assert!(!capabilities.has(&Capability::Atom("MOVE".to_string())));
1751 assert!(capabilities.has(&Capability::Atom("IDLE".to_string())));
1752 assert!(capabilities.has(&Capability::Atom("ID".to_string())));
1753 }
1754
1755 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1758 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1759 async fn login_without_capabilities() {
1760 let response = b"A0001 OK Logged in\r\n".to_vec();
1761 let username = "username";
1762 let password = "password";
1763 let command = format!("A0001 LOGIN {} {}\r\n", quote!(username), quote!(password));
1764 let mock_stream = MockStream::new(response);
1765 let client = mock_client!(mock_stream);
1766 if let Ok((session, capabilities)) =
1767 client.login_with_capabilities(username, password).await
1768 {
1769 assert_eq!(
1770 session.stream.inner.written_buf,
1771 command.as_bytes().to_vec(),
1772 "Invalid login command"
1773 );
1774 assert!(capabilities.is_none());
1775 } else {
1776 unreachable!("invalid login");
1777 }
1778 }
1779
1780 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1781 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1782 async fn logout() {
1783 let response = b"A0001 OK Logout completed.\r\n".to_vec();
1784 let command = "A0001 LOGOUT\r\n";
1785 let mock_stream = MockStream::new(response);
1786 let mut session = mock_session!(mock_stream);
1787 session.logout().await.unwrap();
1788 assert!(
1789 session.stream.inner.written_buf == command.as_bytes().to_vec(),
1790 "Invalid logout command"
1791 );
1792 }
1793
1794 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1795 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1796 async fn rename() {
1797 let response = b"A0001 OK RENAME completed\r\n".to_vec();
1798 let current_mailbox_name = "INBOX";
1799 let new_mailbox_name = "NEWINBOX";
1800 let command = format!(
1801 "A0001 RENAME {} {}\r\n",
1802 quote!(current_mailbox_name),
1803 quote!(new_mailbox_name)
1804 );
1805 let mock_stream = MockStream::new(response);
1806 let mut session = mock_session!(mock_stream);
1807 session
1808 .rename(current_mailbox_name, new_mailbox_name)
1809 .await
1810 .unwrap();
1811 assert!(
1812 session.stream.inner.written_buf == command.as_bytes().to_vec(),
1813 "Invalid rename command"
1814 );
1815 }
1816
1817 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1818 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1819 async fn subscribe() {
1820 let response = b"A0001 OK SUBSCRIBE completed\r\n".to_vec();
1821 let mailbox = "INBOX";
1822 let command = format!("A0001 SUBSCRIBE {}\r\n", quote!(mailbox));
1823 let mock_stream = MockStream::new(response);
1824 let mut session = mock_session!(mock_stream);
1825 session.subscribe(mailbox).await.unwrap();
1826 assert!(
1827 session.stream.inner.written_buf == command.as_bytes().to_vec(),
1828 "Invalid subscribe command"
1829 );
1830 }
1831
1832 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1833 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1834 async fn unsubscribe() {
1835 let response = b"A0001 OK UNSUBSCRIBE completed\r\n".to_vec();
1836 let mailbox = "INBOX";
1837 let command = format!("A0001 UNSUBSCRIBE {}\r\n", quote!(mailbox));
1838 let mock_stream = MockStream::new(response);
1839 let mut session = mock_session!(mock_stream);
1840 session.unsubscribe(mailbox).await.unwrap();
1841 assert!(
1842 session.stream.inner.written_buf == command.as_bytes().to_vec(),
1843 "Invalid unsubscribe command"
1844 );
1845 }
1846
1847 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1848 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1849 async fn expunge() {
1850 let response = b"A0001 OK EXPUNGE completed\r\n".to_vec();
1851 let mock_stream = MockStream::new(response);
1852 let mut session = mock_session!(mock_stream);
1853 session.expunge().await.unwrap().collect::<Vec<_>>().await;
1854 assert!(
1855 session.stream.inner.written_buf == b"A0001 EXPUNGE\r\n".to_vec(),
1856 "Invalid expunge command"
1857 );
1858 }
1859
1860 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1861 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1862 async fn uid_expunge() {
1863 let response = b"* 2 EXPUNGE\r\n\
1864 * 3 EXPUNGE\r\n\
1865 * 4 EXPUNGE\r\n\
1866 A0001 OK UID EXPUNGE completed\r\n"
1867 .to_vec();
1868 let mock_stream = MockStream::new(response);
1869 let mut session = mock_session!(mock_stream);
1870 session
1871 .uid_expunge("2:4")
1872 .await
1873 .unwrap()
1874 .collect::<Vec<_>>()
1875 .await;
1876 assert!(
1877 session.stream.inner.written_buf == b"A0001 UID EXPUNGE 2:4\r\n".to_vec(),
1878 "Invalid expunge command"
1879 );
1880 }
1881
1882 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1883 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1884 async fn check() {
1885 let response = b"A0001 OK CHECK completed\r\n".to_vec();
1886 let mock_stream = MockStream::new(response);
1887 let mut session = mock_session!(mock_stream);
1888 session.check().await.unwrap();
1889 assert!(
1890 session.stream.inner.written_buf == b"A0001 CHECK\r\n".to_vec(),
1891 "Invalid check command"
1892 );
1893 }
1894
1895 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1896 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1897 async fn examine() {
1898 let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\
1899 * OK [PERMANENTFLAGS ()] Read-only mailbox.\r\n\
1900 * 1 EXISTS\r\n\
1901 * 1 RECENT\r\n\
1902 * OK [UNSEEN 1] First unseen.\r\n\
1903 * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\
1904 * OK [UIDNEXT 2] Predicted next UID\r\n\
1905 A0001 OK [READ-ONLY] Select completed.\r\n"
1906 .to_vec();
1907 let expected_mailbox = Mailbox {
1908 flags: vec![
1909 Flag::Answered,
1910 Flag::Flagged,
1911 Flag::Deleted,
1912 Flag::Seen,
1913 Flag::Draft,
1914 ],
1915 exists: 1,
1916 recent: 1,
1917 unseen: Some(1),
1918 permanent_flags: vec![],
1919 uid_next: Some(2),
1920 uid_validity: Some(1257842737),
1921 highest_modseq: None,
1922 };
1923 let mailbox_name = "INBOX";
1924 let command = format!("A0001 EXAMINE {}\r\n", quote!(mailbox_name));
1925 let mock_stream = MockStream::new(response);
1926 let mut session = mock_session!(mock_stream);
1927 let mailbox = session.examine(mailbox_name).await.unwrap();
1928 assert!(
1929 session.stream.inner.written_buf == command.as_bytes().to_vec(),
1930 "Invalid examine command"
1931 );
1932 assert_eq!(mailbox, expected_mailbox);
1933 }
1934
1935 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1936 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1937 async fn select() {
1938 let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\
1939 * OK [PERMANENTFLAGS (\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)] \
1940 Read-only mailbox.\r\n\
1941 * 1 EXISTS\r\n\
1942 * 1 RECENT\r\n\
1943 * OK [UNSEEN 1] First unseen.\r\n\
1944 * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\
1945 * OK [UIDNEXT 2] Predicted next UID\r\n\
1946 * OK [HIGHESTMODSEQ 90060115205545359] Highest mailbox modsequence\r\n\
1947 A0001 OK [READ-ONLY] Select completed.\r\n"
1948 .to_vec();
1949 let expected_mailbox = Mailbox {
1950 flags: vec![
1951 Flag::Answered,
1952 Flag::Flagged,
1953 Flag::Deleted,
1954 Flag::Seen,
1955 Flag::Draft,
1956 ],
1957 exists: 1,
1958 recent: 1,
1959 unseen: Some(1),
1960 permanent_flags: vec![
1961 Flag::MayCreate,
1962 Flag::Answered,
1963 Flag::Flagged,
1964 Flag::Deleted,
1965 Flag::Draft,
1966 Flag::Seen,
1967 ],
1968 uid_next: Some(2),
1969 uid_validity: Some(1257842737),
1970 highest_modseq: Some(90060115205545359),
1971 };
1972 let mailbox_name = "INBOX";
1973 let command = format!("A0001 SELECT {}\r\n", quote!(mailbox_name));
1974 let mock_stream = MockStream::new(response);
1975 let mut session = mock_session!(mock_stream);
1976 let mailbox = session.select(mailbox_name).await.unwrap();
1977 assert!(
1978 session.stream.inner.written_buf == command.as_bytes().to_vec(),
1979 "Invalid select command"
1980 );
1981 assert_eq!(mailbox, expected_mailbox);
1982 }
1983
1984 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
1985 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
1986 async fn search() {
1987 let response = b"* SEARCH 1 2 3 4 5\r\n\
1988 A0001 OK Search completed\r\n"
1989 .to_vec();
1990 let mock_stream = MockStream::new(response);
1991 let mut session = mock_session!(mock_stream);
1992 let ids = session.search("Unseen").await.unwrap();
1993 let ids: HashSet<u32> = ids.iter().cloned().collect();
1994 assert!(
1995 session.stream.inner.written_buf == b"A0001 SEARCH Unseen\r\n".to_vec(),
1996 "Invalid search command"
1997 );
1998 assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
1999 }
2000
2001 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2002 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2003 async fn uid_search() {
2004 let response = b"* SEARCH 1 2 3 4 5\r\n\
2005 A0001 OK Search completed\r\n"
2006 .to_vec();
2007 let mock_stream = MockStream::new(response);
2008 let mut session = mock_session!(mock_stream);
2009 let ids = session.uid_search("Unseen").await.unwrap();
2010 let ids: HashSet<Uid> = ids.iter().cloned().collect();
2011 assert!(
2012 session.stream.inner.written_buf == b"A0001 UID SEARCH Unseen\r\n".to_vec(),
2013 "Invalid search command"
2014 );
2015 assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
2016 }
2017
2018 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2019 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2020 async fn uid_search_unordered() {
2021 let response = b"* SEARCH 1 2 3 4 5\r\n\
2022 A0002 OK CAPABILITY completed\r\n\
2023 A0001 OK Search completed\r\n"
2024 .to_vec();
2025 let mock_stream = MockStream::new(response);
2026 let mut session = mock_session!(mock_stream);
2027 let ids = session.uid_search("Unseen").await.unwrap();
2028 let ids: HashSet<Uid> = ids.iter().cloned().collect();
2029 assert!(
2030 session.stream.inner.written_buf == b"A0001 UID SEARCH Unseen\r\n".to_vec(),
2031 "Invalid search command"
2032 );
2033 assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
2034 }
2035
2036 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2037 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2038 async fn capability() {
2039 let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\
2040 A0001 OK CAPABILITY completed\r\n"
2041 .to_vec();
2042 let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"];
2043 let mock_stream = MockStream::new(response);
2044 let mut session = mock_session!(mock_stream);
2045 let capabilities = session.capabilities().await.unwrap();
2046 assert!(
2047 session.stream.inner.written_buf == b"A0001 CAPABILITY\r\n".to_vec(),
2048 "Invalid capability command"
2049 );
2050 assert_eq!(capabilities.len(), 4);
2051 for e in expected_capabilities {
2052 assert!(capabilities.has_str(e));
2053 }
2054 }
2055
2056 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2057 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2058 async fn create() {
2059 let response = b"A0001 OK CREATE completed\r\n".to_vec();
2060 let mailbox_name = "INBOX";
2061 let command = format!("A0001 CREATE {}\r\n", quote!(mailbox_name));
2062 let mock_stream = MockStream::new(response);
2063 let mut session = mock_session!(mock_stream);
2064 session.create(mailbox_name).await.unwrap();
2065 assert!(
2066 session.stream.inner.written_buf == command.as_bytes().to_vec(),
2067 "Invalid create command"
2068 );
2069 }
2070
2071 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2072 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2073 async fn delete() {
2074 let response = b"A0001 OK DELETE completed\r\n".to_vec();
2075 let mailbox_name = "INBOX";
2076 let command = format!("A0001 DELETE {}\r\n", quote!(mailbox_name));
2077 let mock_stream = MockStream::new(response);
2078 let mut session = mock_session!(mock_stream);
2079 session.delete(mailbox_name).await.unwrap();
2080 assert!(
2081 session.stream.inner.written_buf == command.as_bytes().to_vec(),
2082 "Invalid delete command"
2083 );
2084 }
2085
2086 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2087 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2088 async fn noop() {
2089 let response = b"A0001 OK NOOP completed\r\n".to_vec();
2090 let mock_stream = MockStream::new(response);
2091 let mut session = mock_session!(mock_stream);
2092 session.noop().await.unwrap();
2093 assert!(
2094 session.stream.inner.written_buf == b"A0001 NOOP\r\n".to_vec(),
2095 "Invalid noop command"
2096 );
2097 }
2098
2099 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2100 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2101 async fn close() {
2102 let response = b"A0001 OK CLOSE completed\r\n".to_vec();
2103 let mock_stream = MockStream::new(response);
2104 let mut session = mock_session!(mock_stream);
2105 session.close().await.unwrap();
2106 assert!(
2107 session.stream.inner.written_buf == b"A0001 CLOSE\r\n".to_vec(),
2108 "Invalid close command"
2109 );
2110 }
2111
2112 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2113 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2114 async fn store() {
2115 generic_store(" ", |c, set, query| async move {
2116 c.lock()
2117 .await
2118 .store(set, query)
2119 .await?
2120 .collect::<Vec<_>>()
2121 .await;
2122 Ok(())
2123 })
2124 .await;
2125 }
2126
2127 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2128 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2129 async fn uid_store() {
2130 generic_store(" UID ", |c, set, query| async move {
2131 c.lock()
2132 .await
2133 .uid_store(set, query)
2134 .await?
2135 .collect::<Vec<_>>()
2136 .await;
2137 Ok(())
2138 })
2139 .await;
2140 }
2141
2142 async fn generic_store<'a, F, T, K>(prefix: &'a str, op: F)
2143 where
2144 F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
2145 K: 'a + Future<Output = Result<T>>,
2146 {
2147 let res = "* 2 FETCH (FLAGS (\\Deleted \\Seen))\r\n\
2148 * 3 FETCH (FLAGS (\\Deleted))\r\n\
2149 * 4 FETCH (FLAGS (\\Deleted \\Flagged \\Seen))\r\n\
2150 A0001 OK STORE completed\r\n";
2151
2152 generic_with_uid(res, "STORE", "2.4", "+FLAGS (\\Deleted)", prefix, op).await;
2153 }
2154
2155 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2156 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2157 async fn copy() {
2158 generic_copy(" ", |c, set, query| async move {
2159 c.lock().await.copy(set, query).await?;
2160 Ok(())
2161 })
2162 .await;
2163 }
2164
2165 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2166 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2167 async fn uid_copy() {
2168 generic_copy(" UID ", |c, set, query| async move {
2169 c.lock().await.uid_copy(set, query).await?;
2170 Ok(())
2171 })
2172 .await;
2173 }
2174
2175 async fn generic_copy<'a, F, T, K>(prefix: &'a str, op: F)
2176 where
2177 F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
2178 K: 'a + Future<Output = Result<T>>,
2179 {
2180 let resp = "A0001 OK COPY completed\r\n".as_bytes().to_vec();
2181 let seq = "2:4";
2182 let query = "MEETING";
2183 let line = format!("A0001{prefix}COPY {seq} {}\r\n", quote!(query));
2184 let session = Arc::new(Mutex::new(mock_session!(MockStream::new(resp))));
2185
2186 {
2187 let _ = op(session.clone(), seq, query).await.unwrap();
2188 }
2189 assert!(
2190 session.lock().await.stream.inner.written_buf == line.as_bytes().to_vec(),
2191 "Invalid command"
2192 );
2193 }
2194
2195 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2196 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2197 async fn mv() {
2198 let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\
2199 * 2 EXPUNGE\r\n\
2200 * 1 EXPUNGE\r\n\
2201 A0001 OK Move completed\r\n"
2202 .to_vec();
2203 let mailbox_name = "MEETING";
2204 let command = format!("A0001 MOVE 1:2 {}\r\n", quote!(mailbox_name));
2205 let mock_stream = MockStream::new(response);
2206 let mut session = mock_session!(mock_stream);
2207 session.mv("1:2", mailbox_name).await.unwrap();
2208 assert!(
2209 session.stream.inner.written_buf == command.as_bytes().to_vec(),
2210 "Invalid move command"
2211 );
2212 }
2213
2214 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2215 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2216 async fn uid_mv() {
2217 let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\
2218 * 2 EXPUNGE\r\n\
2219 * 1 EXPUNGE\r\n\
2220 A0001 OK Move completed\r\n"
2221 .to_vec();
2222 let mailbox_name = "MEETING";
2223 let command = format!("A0001 UID MOVE 41:42 {}\r\n", quote!(mailbox_name));
2224 let mock_stream = MockStream::new(response);
2225 let mut session = mock_session!(mock_stream);
2226 session.uid_mv("41:42", mailbox_name).await.unwrap();
2227 assert!(
2228 session.stream.inner.written_buf == command.as_bytes().to_vec(),
2229 "Invalid uid move command"
2230 );
2231 }
2232
2233 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2234 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2235 async fn fetch() {
2236 generic_fetch(" ", |c, seq, query| async move {
2237 c.lock()
2238 .await
2239 .fetch(seq, query)
2240 .await?
2241 .collect::<Vec<_>>()
2242 .await;
2243
2244 Ok(())
2245 })
2246 .await;
2247 }
2248
2249 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2250 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2251 async fn uid_fetch() {
2252 generic_fetch(" UID ", |c, seq, query| async move {
2253 c.lock()
2254 .await
2255 .uid_fetch(seq, query)
2256 .await?
2257 .collect::<Vec<_>>()
2258 .await;
2259 Ok(())
2260 })
2261 .await;
2262 }
2263
2264 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2265 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2266 async fn fetch_unexpected_eof() {
2267 let response = b"".to_vec();
2269
2270 let mock_stream = MockStream::new(response);
2271 let mut session = mock_session!(mock_stream);
2272
2273 {
2274 let mut fetch_result = session
2275 .uid_fetch("1:*", "(FLAGS BODY.PEEK[])")
2276 .await
2277 .unwrap();
2278
2279 let err = fetch_result.try_next().await.unwrap_err();
2281 let Error::Io(io_err) = err else {
2282 panic!("Unexpected error type: {err}")
2283 };
2284 assert_eq!(io_err.kind(), io::ErrorKind::UnexpectedEof);
2285 }
2286
2287 assert_eq!(
2288 session.stream.inner.written_buf,
2289 b"A0001 UID FETCH 1:* (FLAGS BODY.PEEK[])\r\n".to_vec()
2290 );
2291 }
2292
2293 async fn generic_fetch<'a, F, T, K>(prefix: &'a str, op: F)
2294 where
2295 F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
2296 K: 'a + Future<Output = Result<T>>,
2297 {
2298 generic_with_uid(
2299 "A0001 OK FETCH completed\r\n",
2300 "FETCH",
2301 "1",
2302 "BODY[]",
2303 prefix,
2304 op,
2305 )
2306 .await;
2307 }
2308
2309 async fn generic_with_uid<'a, F, T, K>(
2310 res: &'a str,
2311 cmd: &'a str,
2312 seq: &'a str,
2313 query: &'a str,
2314 prefix: &'a str,
2315 op: F,
2316 ) where
2317 F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
2318 K: 'a + Future<Output = Result<T>>,
2319 {
2320 let resp = res.as_bytes().to_vec();
2321 let line = format!("A0001{prefix}{cmd} {seq} {query}\r\n");
2322 let session = Arc::new(Mutex::new(mock_session!(MockStream::new(resp))));
2323
2324 {
2325 let _ = op(session.clone(), seq, query).await.unwrap();
2326 }
2327 assert!(
2328 session.lock().await.stream.inner.written_buf == line.as_bytes().to_vec(),
2329 "Invalid command"
2330 );
2331 }
2332
2333 #[test]
2334 fn quote_backslash() {
2335 assert_eq!("\"test\\\\text\"", quote!(r"test\text"));
2336 }
2337
2338 #[test]
2339 fn quote_dquote() {
2340 assert_eq!("\"test\\\"text\"", quote!("test\"text"));
2341 }
2342
2343 #[test]
2344 fn validate_random() {
2345 assert_eq!(
2346 "\"~iCQ_k;>[&\\\"sVCvUW`e<<P!wJ\"",
2347 &validate_str("~iCQ_k;>[&\"sVCvUW`e<<P!wJ").unwrap()
2348 );
2349 }
2350
2351 #[test]
2352 fn validate_newline() {
2353 if let Err(ref e) = validate_str("test\nstring") {
2354 if let Error::Validate(ve) = e {
2355 if ve.0 == '\n' {
2356 return;
2357 }
2358 }
2359 panic!("Wrong error: {e:?}");
2360 }
2361 panic!("No error");
2362 }
2363
2364 #[test]
2365 #[allow(unreachable_patterns)]
2366 fn validate_carriage_return() {
2367 if let Err(ref e) = validate_str("test\rstring") {
2368 if let Error::Validate(ve) = e {
2369 if ve.0 == '\r' {
2370 return;
2371 }
2372 }
2373 panic!("Wrong error: {e:?}");
2374 }
2375 panic!("No error");
2376 }
2377
2378 #[cfg(feature = "runtime-tokio")]
2382 async fn handle_client(stream: tokio::io::DuplexStream) -> Result<()> {
2383 use tokio::io::AsyncBufReadExt;
2384
2385 let (reader, mut writer) = tokio::io::split(stream);
2386 let reader = tokio::io::BufReader::new(reader);
2387
2388 let mut lines = reader.lines();
2389 while let Some(line) = lines.next_line().await? {
2390 let (request_id, request) = line.split_once(' ').unwrap();
2391 eprintln!("Received request {request_id}.");
2392
2393 let (id, _) = request
2394 .strip_prefix("FETCH ")
2395 .unwrap()
2396 .split_once(' ')
2397 .unwrap();
2398 let id = id.parse().unwrap();
2399
2400 let mut body = concat!(
2401 "From: Bob <bob@example.com>\r\n",
2402 "To: Alice <alice@example.org>\r\n",
2403 "Subject: Test\r\n",
2404 "Message-Id: <foobar@example.com>\r\n",
2405 "Date: Sun, 22 Mar 2020 00:00:00 +0100\r\n",
2406 "\r\n",
2407 )
2408 .to_string();
2409 for _ in 1..id {
2410 body +=
2411 "012345678901234567890123456789012345678901234567890123456789012345678901\r\n";
2412 }
2413 let body_len = body.len();
2414
2415 let response = format!(
2416 "* {id} FETCH (RFC822.SIZE {body_len} BODY[] {{{body_len}}}\r\n{body} FLAGS (\\Seen))\r\n"
2417 );
2418 writer.write_all(response.as_bytes()).await?;
2419 writer
2420 .write_all(format!("{request_id} OK FETCH completed\r\n").as_bytes())
2421 .await?;
2422 writer.flush().await?;
2423 }
2424
2425 Ok(())
2426 }
2427
2428 #[cfg(feature = "runtime-tokio")]
2435 #[cfg_attr(
2436 feature = "runtime-tokio",
2437 tokio::test(flavor = "multi_thread", worker_threads = 2)
2438 )]
2439 async fn large_fetch() -> Result<()> {
2440 use futures_util::TryStreamExt;
2441
2442 let (client, server) = tokio::io::duplex(4096);
2443 tokio::spawn(handle_client(server));
2444
2445 let client = crate::Client::new(client);
2446 let mut imap_session = Session::new(client.conn);
2447
2448 for i in 200..300 {
2449 eprintln!("Fetching {i}.");
2450 let mut messages_stream = imap_session
2451 .fetch(format!("{i}"), "(RFC822.SIZE BODY.PEEK[] FLAGS)")
2452 .await?;
2453 let fetch = messages_stream
2454 .try_next()
2455 .await?
2456 .expect("no FETCH returned");
2457 let body = fetch.body().expect("message did not have a body!");
2458 assert_eq!(body.len(), 76 + 74 * i);
2459
2460 let no_fetch = messages_stream.try_next().await?;
2461 assert!(no_fetch.is_none());
2462 drop(messages_stream);
2463 }
2464
2465 Ok(())
2466 }
2467
2468 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2469 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2470 async fn status() {
2471 {
2472 let response = b"* STATUS INBOX (UIDNEXT 25)\r\n\
2473 A0001 OK [CLIENTBUG] Status on selected mailbox completed (0.001 + 0.000 secs).\r\n"
2474 .to_vec();
2475
2476 let mock_stream = MockStream::new(response);
2477 let mut session = mock_session!(mock_stream);
2478 let status = session.status("INBOX", "(UIDNEXT)").await.unwrap();
2479 assert_eq!(
2480 session.stream.inner.written_buf,
2481 b"A0001 STATUS \"INBOX\" (UIDNEXT)\r\n".to_vec()
2482 );
2483 assert_eq!(status.uid_next, Some(25));
2484 }
2485
2486 {
2487 let response = b"* STATUS INBOX (RECENT 15)\r\n\
2488 A0001 OK STATUS completed\r\n"
2489 .to_vec();
2490
2491 let mock_stream = MockStream::new(response);
2492 let mut session = mock_session!(mock_stream);
2493 let status = session.status("INBOX", "(RECENT)").await.unwrap();
2494 assert_eq!(
2495 session.stream.inner.written_buf,
2496 b"A0001 STATUS \"INBOX\" (RECENT)\r\n".to_vec()
2497 );
2498 assert_eq!(status.recent, 15);
2499 }
2500
2501 {
2502 let response = b"* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\n\
2504 A0001 OK STATUS completed\r\n"
2505 .to_vec();
2506
2507 let mock_stream = MockStream::new(response);
2508 let mut session = mock_session!(mock_stream);
2509 let status = session
2510 .status("blurdybloop", "(UIDNEXT MESSAGES)")
2511 .await
2512 .unwrap();
2513 assert_eq!(
2514 session.stream.inner.written_buf,
2515 b"A0001 STATUS \"blurdybloop\" (UIDNEXT MESSAGES)\r\n".to_vec()
2516 );
2517 assert_eq!(status.uid_next, Some(44292));
2518 assert_eq!(status.exists, 231);
2519 }
2520 }
2521
2522 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2523 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2524 async fn append() {
2525 {
2526 let response = b"+ OK\r\nA0001 OK [APPENDUID 1725735035 2] Append completed (0.052 + 12.097 + 0.049 secs).\r\n".to_vec();
2530
2531 let mock_stream = MockStream::new(response);
2532 let mut session = mock_session!(mock_stream);
2533 session
2534 .append("INBOX", Some(r"(\Seen)"), None, "foobarbaz")
2535 .await
2536 .unwrap();
2537 assert_eq!(
2538 session.stream.inner.written_buf,
2539 b"A0001 APPEND \"INBOX\" (\\Seen) {9}\r\nfoobarbaz\r\n".to_vec()
2540 );
2541 }
2542
2543 {
2544 let response = b"+ OK\r\n* 3 EXISTS\r\n* 2 RECENT\r\nA0001 OK [APPENDUID 1725735035 2] Append completed (0.052 + 12.097 + 0.049 secs).\r\n".to_vec();
2548
2549 let mock_stream = MockStream::new(response);
2550 let mut session = mock_session!(mock_stream);
2551 session
2552 .append("INBOX", Some(r"(\Seen)"), None, "foobarbaz")
2553 .await
2554 .unwrap();
2555 assert_eq!(
2556 session.stream.inner.written_buf,
2557 b"A0001 APPEND \"INBOX\" (\\Seen) {9}\r\nfoobarbaz\r\n".to_vec()
2558 );
2559 let exists_response = session.unsolicited_responses.recv().await.unwrap();
2560 assert_eq!(exists_response, UnsolicitedResponse::Exists(3));
2561 let recent_response = session.unsolicited_responses.recv().await.unwrap();
2562 assert_eq!(recent_response, UnsolicitedResponse::Recent(2));
2563 }
2564
2565 {
2566 let response =
2568 b"A0001 NO [TRYCREATE] Mailbox doesn't exist: foobar (0.001 + 0.000 secs)."
2569 .to_vec();
2570 let mock_stream = MockStream::new(response);
2571 let mut session = mock_session!(mock_stream);
2572 session
2573 .append("foobar", None, None, "foobarbaz")
2574 .await
2575 .unwrap_err();
2576 assert_eq!(
2577 session.stream.inner.written_buf,
2578 b"A0001 APPEND \"foobar\" {9}\r\n".to_vec()
2579 );
2580 }
2581 }
2582
2583 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2584 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2585 async fn get_metadata() {
2586 {
2587 let response = b"* METADATA \"INBOX\" (/private/comment \"My own comment\")\r\n\
2588 A0001 OK GETMETADATA complete\r\n"
2589 .to_vec();
2590
2591 let mock_stream = MockStream::new(response);
2592 let mut session = mock_session!(mock_stream);
2593 let metadata = session
2594 .get_metadata("INBOX", "", "/private/comment")
2595 .await
2596 .unwrap();
2597 assert_eq!(
2598 session.stream.inner.written_buf,
2599 b"A0001 GETMETADATA \"INBOX\" /private/comment\r\n".to_vec()
2600 );
2601 assert_eq!(metadata.len(), 1);
2602 assert_eq!(metadata[0].entry, "/private/comment");
2603 assert_eq!(metadata[0].value.as_ref().unwrap(), "My own comment");
2604 }
2605
2606 {
2607 let response = b"* METADATA \"INBOX\" (/shared/comment \"Shared comment\" /private/comment \"My own comment\")\r\n\
2608 A0001 OK GETMETADATA complete\r\n"
2609 .to_vec();
2610
2611 let mock_stream = MockStream::new(response);
2612 let mut session = mock_session!(mock_stream);
2613 let metadata = session
2614 .get_metadata("INBOX", "", "(/shared/comment /private/comment)")
2615 .await
2616 .unwrap();
2617 assert_eq!(
2618 session.stream.inner.written_buf,
2619 b"A0001 GETMETADATA \"INBOX\" (/shared/comment /private/comment)\r\n".to_vec()
2620 );
2621 assert_eq!(metadata.len(), 2);
2622 assert_eq!(metadata[0].entry, "/shared/comment");
2623 assert_eq!(metadata[0].value.as_ref().unwrap(), "Shared comment");
2624 assert_eq!(metadata[1].entry, "/private/comment");
2625 assert_eq!(metadata[1].value.as_ref().unwrap(), "My own comment");
2626 }
2627
2628 {
2629 let response = b"* METADATA \"\" (/shared/comment {15}\r\nChatmail server /shared/admin {28}\r\nmailto:root@nine.testrun.org)\r\n\
2630 A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
2631 .to_vec();
2632
2633 let mock_stream = MockStream::new(response);
2634 let mut session = mock_session!(mock_stream);
2635 let metadata = session
2636 .get_metadata("", "", "(/shared/comment /shared/admin)")
2637 .await
2638 .unwrap();
2639 assert_eq!(
2640 session.stream.inner.written_buf,
2641 b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
2642 );
2643 assert_eq!(metadata.len(), 2);
2644 assert_eq!(metadata[0].entry, "/shared/comment");
2645 assert_eq!(metadata[0].value.as_ref().unwrap(), "Chatmail server");
2646 assert_eq!(metadata[1].entry, "/shared/admin");
2647 assert_eq!(
2648 metadata[1].value.as_ref().unwrap(),
2649 "mailto:root@nine.testrun.org"
2650 );
2651 }
2652
2653 {
2654 let response = b"* METADATA \"\" (/shared/comment \"Chatmail server\")\r\n\
2655 * METADATA \"\" (/shared/admin \"mailto:root@nine.testrun.org\")\r\n\
2656 A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
2657 .to_vec();
2658
2659 let mock_stream = MockStream::new(response);
2660 let mut session = mock_session!(mock_stream);
2661 let metadata = session
2662 .get_metadata("", "", "(/shared/comment /shared/admin)")
2663 .await
2664 .unwrap();
2665 assert_eq!(
2666 session.stream.inner.written_buf,
2667 b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
2668 );
2669 assert_eq!(metadata.len(), 2);
2670 assert_eq!(metadata[0].entry, "/shared/comment");
2671 assert_eq!(metadata[0].value.as_ref().unwrap(), "Chatmail server");
2672 assert_eq!(metadata[1].entry, "/shared/admin");
2673 assert_eq!(
2674 metadata[1].value.as_ref().unwrap(),
2675 "mailto:root@nine.testrun.org"
2676 );
2677 }
2678
2679 {
2680 let response = b"* METADATA \"\" (/shared/comment NIL /shared/admin NIL)\r\n\
2681 A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
2682 .to_vec();
2683
2684 let mock_stream = MockStream::new(response);
2685 let mut session = mock_session!(mock_stream);
2686 let metadata = session
2687 .get_metadata("", "", "(/shared/comment /shared/admin)")
2688 .await
2689 .unwrap();
2690 assert_eq!(
2691 session.stream.inner.written_buf,
2692 b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
2693 );
2694 assert_eq!(metadata.len(), 2);
2695 assert_eq!(metadata[0].entry, "/shared/comment");
2696 assert_eq!(metadata[0].value, None);
2697 assert_eq!(metadata[1].entry, "/shared/admin");
2698 assert_eq!(metadata[1].value, None);
2699 }
2700 }
2701
2702 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2703 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2704 async fn test_get_quota_root() {
2705 {
2706 let response = b"* QUOTAROOT Sent Userquota\r\n\
2707 * QUOTA Userquota (STORAGE 4855 48576)\r\n\
2708 A0001 OK Getquotaroot completed (0.004 + 0.000 + 0.004 secs).\r\n"
2709 .to_vec();
2710
2711 let mock_stream = MockStream::new(response);
2712 let mut session = mock_session!(mock_stream);
2713 let (quotaroots, quota) = dbg!(session.get_quota_root("Sent").await.unwrap());
2714 assert_eq!(
2715 str::from_utf8(&session.stream.inner.written_buf).unwrap(),
2716 "A0001 GETQUOTAROOT \"Sent\"\r\n"
2717 );
2718 assert_eq!(
2719 quotaroots,
2720 vec![QuotaRoot {
2721 mailbox_name: "Sent".to_string(),
2722 quota_root_names: vec!["Userquota".to_string(),],
2723 },],
2724 );
2725 assert_eq!(
2726 quota,
2727 vec![Quota {
2728 root_name: "Userquota".to_string(),
2729 resources: vec![QuotaResource {
2730 name: QuotaResourceName::Storage,
2731 usage: 4855,
2732 limit: 48576,
2733 }],
2734 }]
2735 );
2736 assert_eq!(quota[0].resources[0].get_usage_percentage(), 9);
2737 }
2738
2739 {
2740 let response = b"* QUOTAROOT \"INBOX\" \"#19\"\r\n\
2741 * QUOTA \"#19\" (STORAGE 0 0)\r\n\
2742 A0001 OK GETQUOTAROOT successful.\r\n"
2743 .to_vec();
2744
2745 let mock_stream = MockStream::new(response);
2746 let mut session = mock_session!(mock_stream);
2747 let (quotaroots, quota) = session.get_quota_root("INBOX").await.unwrap();
2748 assert_eq!(
2749 str::from_utf8(&session.stream.inner.written_buf).unwrap(),
2750 "A0001 GETQUOTAROOT \"INBOX\"\r\n"
2751 );
2752 assert_eq!(
2753 quotaroots,
2754 vec![QuotaRoot {
2755 mailbox_name: "INBOX".to_string(),
2756 quota_root_names: vec!["#19".to_string(),],
2757 },],
2758 );
2759 assert_eq!(
2760 quota,
2761 vec![Quota {
2762 root_name: "#19".to_string(),
2763 resources: vec![QuotaResource {
2764 name: QuotaResourceName::Storage,
2765 usage: 0,
2766 limit: 0,
2767 }],
2768 }]
2769 );
2770 assert_eq!(quota[0].resources[0].get_usage_percentage(), 0);
2771 }
2772 }
2773
2774 #[cfg_attr(feature = "runtime-tokio", tokio::test)]
2775 #[cfg_attr(feature = "runtime-async-std", async_std::test)]
2776 async fn test_parsing_error() {
2777 let response = b"220 mail.example.org ESMTP Postcow\r\n".to_vec();
2779 let command = "A0001 NOOP\r\n";
2780 let mock_stream = MockStream::new(response);
2781 let mut session = mock_session!(mock_stream);
2782 assert!(
2783 session
2784 .noop()
2785 .await
2786 .unwrap_err()
2787 .to_string()
2788 .contains("220 mail.example.org ESMTP Postcow")
2789 );
2790 assert!(
2791 session.stream.inner.written_buf == command.as_bytes().to_vec(),
2792 "Invalid NOOP command"
2793 );
2794 }
2795}