From 0168c1dca83d17abc0ddc2fa38b8f4210568bcc0 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Mon, 27 Nov 2023 15:21:43 +0100 Subject: [PATCH] IMAP4rev1 Recent flag support --- crates/imap-proto/src/protocol/select.rs | 12 ++- crates/imap/src/core/client.rs | 16 +++- crates/imap/src/core/message.rs | 58 +++++++++++- crates/imap/src/core/mod.rs | 2 + crates/imap/src/core/writer.rs | 16 ++-- crates/imap/src/op/create.rs | 3 +- crates/imap/src/op/fetch.rs | 26 +++++- crates/imap/src/op/idle.rs | 1 + crates/imap/src/op/search.rs | 15 +--- crates/imap/src/op/select.rs | 11 ++- crates/imap/src/op/status.rs | 74 +++++++++++---- tests/src/imap/copy_move.rs | 110 ++++++++++++++++------- tests/src/imap/fetch.rs | 3 +- tests/src/imap/mod.rs | 5 +- 14 files changed, 265 insertions(+), 87 deletions(-) diff --git a/crates/imap-proto/src/protocol/select.rs b/crates/imap-proto/src/protocol/select.rs index 2dff6a62..8bf98d0f 100644 --- a/crates/imap-proto/src/protocol/select.rs +++ b/crates/imap-proto/src/protocol/select.rs @@ -70,9 +70,15 @@ impl ImapResponse for Response { } buf.extend_from_slice(b"* "); buf.extend_from_slice(self.total_messages.to_string().as_bytes()); - buf.extend_from_slice( - b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n", - ); + if self.recent_messages > 0 { + buf.extend_from_slice( + b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\Recent)\r\n", + ); + } else { + buf.extend_from_slice( + b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n", + ); + } if self.is_rev2 { self.mailbox.serialize(&mut buf, self.is_rev2, false); } else { diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index f5e43e46..acb1a601 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -36,10 +36,10 @@ use super::{SelectedMailbox, Session, SessionData, State, IMAP}; impl Session { pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result { - for line in String::from_utf8_lossy(bytes).split("\r\n") { + /*for line in String::from_utf8_lossy(bytes).split("\r\n") { //let c = println!("<- {:?}", &line[..std::cmp::min(line.len(), 100)]); let c = println!("{}", line); - } + }*/ tracing::trace!(parent: &self.span, event = "read", @@ -373,8 +373,16 @@ impl State { matches!(self, State::Authenticated { .. } | State::Selected { .. }) } - pub fn is_mailbox_selected(&self) -> bool { - matches!(self, State::Selected { .. }) + pub fn close_mailbox(&self) -> bool { + match self { + State::Selected { mailbox, data } => { + if mailbox.is_select { + data.clear_recent(&mailbox.id); + } + true + } + _ => false, + } } } diff --git a/crates/imap/src/core/message.rs b/crates/imap/src/core/message.rs index 8ee4ec25..1aed102c 100644 --- a/crates/imap/src/core/message.rs +++ b/crates/imap/src/core/message.rs @@ -33,11 +33,14 @@ use jmap_proto::{ object::Object, types::{collection::Collection, property::Property, value::Value}, }; -use store::write::{assert::HashedValue, BatchBuilder, F_VALUE}; +use store::{ + roaring::RoaringBitmap, + write::{assert::HashedValue, BatchBuilder, F_VALUE}, +}; use crate::core::ImapId; -use super::{MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData}; +use super::{Mailbox, MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData}; pub(crate) const MAX_RETRIES: usize = 10; @@ -107,7 +110,7 @@ impl SessionData { ) .await? .into_iter() - .zip(message_ids.into_iter()) + .zip(message_ids.iter()) { // Make sure the message is still in this mailbox if let Some(uid_mailbox) = uid_mailbox { @@ -137,6 +140,7 @@ impl SessionData { let mut try_count = 0; let mut uid_next = 1; let mut uid_other = 0; + let mut recent_messages = RoaringBitmap::new(); // Shuffle unassigned /*if unassigned.len() > 1 { @@ -231,6 +235,7 @@ impl SessionData { message_id = message_id, "Duplicate UID"); } + recent_messages.insert(message_id); } Err(store::Error::AssertValueFailed) if try_count < MAX_RETRIES => @@ -309,6 +314,21 @@ impl SessionData { uid_to_id.insert(uid, message_id); } + // Update recent flags + for account in self.mailboxes.lock().iter_mut() { + if account.account_id == mailbox.account_id { + let mailbox = account + .mailbox_state + .entry(mailbox.mailbox_id) + .or_insert_with(Mailbox::default); + mailbox.recent_messages &= &message_ids; + if !recent_messages.is_empty() { + mailbox.recent_messages |= &recent_messages; + } + break; + } + } + Ok(MailboxState { uid_next, uid_validity, @@ -424,6 +444,38 @@ impl SessionData { Err(StatusResponse::database_failure()) } } + + pub fn get_recent(&self, mailbox: &MailboxId) -> RoaringBitmap { + for account in self.mailboxes.lock().iter() { + if account.account_id == mailbox.account_id { + if let Some(mailbox) = account.mailbox_state.get(&mailbox.mailbox_id) { + return mailbox.recent_messages.clone(); + } + } + } + RoaringBitmap::new() + } + + pub fn get_recent_count(&self, mailbox: &MailboxId) -> usize { + for account in self.mailboxes.lock().iter() { + if account.account_id == mailbox.account_id { + if let Some(mailbox) = account.mailbox_state.get(&mailbox.mailbox_id) { + return mailbox.recent_messages.len() as usize; + } + } + } + 0 + } + + pub fn clear_recent(&self, mailbox: &MailboxId) { + for account in self.mailboxes.lock().iter_mut() { + if account.account_id == mailbox.account_id { + if let Some(mailbox) = account.mailbox_state.get_mut(&mailbox.mailbox_id) { + mailbox.recent_messages.clear(); + } + } + } + } } impl SelectedMailbox { diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs index 6a5718f4..41706642 100644 --- a/crates/imap/src/core/mod.rs +++ b/crates/imap/src/core/mod.rs @@ -42,6 +42,7 @@ use jmap::{ JMAP, }; use parking_lot::Mutex; +use store::roaring::RoaringBitmap; use tokio::{ io::{AsyncRead, ReadHalf}, sync::{mpsc, watch}, @@ -127,6 +128,7 @@ pub struct Mailbox { pub uid_validity: Option, pub uid_next: Option, pub size: Option, + pub recent_messages: RoaringBitmap, } #[derive(Debug)] diff --git a/crates/imap/src/core/writer.rs b/crates/imap/src/core/writer.rs index e45316c5..86ed287b 100644 --- a/crates/imap/src/core/writer.rs +++ b/crates/imap/src/core/writer.rs @@ -58,7 +58,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender { @@ -101,7 +101,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender { @@ -132,10 +132,10 @@ impl Session { pub async fn write_bytes(&self, bytes: impl Into>) -> crate::OpResult { let bytes = bytes.into(); - let c = println!( - "{:?}", + /*let c = println!( + "-> {:?}", String::from_utf8_lossy(&bytes[..std::cmp::min(bytes.len(), 100)]) - ); + );*/ if let Err(err) = self.writer.send(Event::Bytes(bytes)).await { debug!("Failed to send bytes: {}", err); @@ -149,10 +149,10 @@ impl Session { impl SessionData { pub async fn write_bytes(&self, bytes: impl Into>) -> bool { let bytes = bytes.into(); - let c = println!( - "{:?}", + /*let c = println!( + "-> {:?}", String::from_utf8_lossy(&bytes[..std::cmp::min(bytes.len(), 100)]) - ); + );*/ if let Err(err) = self.writer.send(Event::Bytes(bytes)).await { debug!("Failed to send bytes: {}", err); diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs index 50e7f2ba..4506906b 100644 --- a/crates/imap/src/op/create.rs +++ b/crates/imap/src/op/create.rs @@ -34,7 +34,7 @@ use jmap_proto::{ type_state::DataType, value::Value, }, }; -use store::{query::Filter, write::BatchBuilder}; +use store::{query::Filter, roaring::RoaringBitmap, write::BatchBuilder}; use tokio::io::AsyncRead; use crate::core::{Account, Mailbox, Session, SessionData}; @@ -217,6 +217,7 @@ impl SessionData { } else { None }, + recent_messages: RoaringBitmap::new(), }, ); } diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs index 8ab163c6..0e71446e 100644 --- a/crates/imap/src/op/fetch.rs +++ b/crates/imap/src/op/fetch.rs @@ -66,6 +66,7 @@ impl Session { Ok(arguments) => { let (data, mailbox) = self.state.select_data(); let is_qresync = self.is_qresync; + let is_rev2 = self.version.is_rev2(); let enabled_condstore = if !self.is_condstore && arguments.changed_since.is_some() || arguments.attributes.contains(&Attribute::ModSeq) @@ -78,9 +79,16 @@ impl Session { tokio::spawn(async move { data.write_bytes( - data.fetch(arguments, mailbox, is_uid, is_qresync, enabled_condstore) - .await - .into_bytes(), + data.fetch( + arguments, + mailbox, + is_uid, + is_qresync, + is_rev2, + enabled_condstore, + ) + .await + .into_bytes(), ) .await; }); @@ -98,6 +106,7 @@ impl SessionData { mailbox: Arc, is_uid: bool, is_qresync: bool, + is_rev2: bool, enabled_condstore: bool, ) -> StatusResponse { // Validate VANISHED parameter @@ -117,6 +126,11 @@ impl SessionData { Ok(modseq) => modseq, Err(response) => return response.with_tag(arguments.tag), }; + let recent_messages = if !is_rev2 { + self.get_recent(&mailbox.id).into() + } else { + None + }; // Convert IMAP ids to JMAP ids. let mut ids = match mailbox @@ -353,6 +367,12 @@ impl SessionData { if set_seen_flag { flags.push(Flag::Seen); } + if recent_messages + .as_ref() + .map_or(false, |recent| recent.contains(id)) + { + flags.push(Flag::Recent); + } items.push(DataItem::Flags { flags }); } Attribute::InternalDate => { diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs index 7c1d2ede..6277627e 100644 --- a/crates/imap/src/op/idle.rs +++ b/crates/imap/src/op/idle.rs @@ -268,6 +268,7 @@ impl SessionData { mailbox.clone(), true, is_qresync, + is_rev2, false, ) .await; diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index dac52910..540ec3da 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -574,17 +574,11 @@ impl SessionData { filters.push(query::Filter::End); } search::Filter::Recent => { - filters.push(query::Filter::is_in_bitmap( - Property::Keywords, - Keyword::Recent, - )); + filters.push(query::Filter::is_in_set(self.get_recent(&mailbox.id))); } search::Filter::New => { filters.push(query::Filter::And); - filters.push(query::Filter::is_in_bitmap( - Property::Keywords, - Keyword::Recent, - )); + filters.push(query::Filter::is_in_set(self.get_recent(&mailbox.id))); filters.push(query::Filter::Not); filters.push(query::Filter::is_in_bitmap( Property::Keywords, @@ -595,10 +589,7 @@ impl SessionData { } search::Filter::Old => { filters.push(query::Filter::Not); - filters.push(query::Filter::is_in_bitmap( - Property::Keywords, - Keyword::Seen, - )); + filters.push(query::Filter::is_in_set(self.get_recent(&mailbox.id))); filters.push(query::Filter::End); } search::Filter::Older(secs) => { diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs index 00dd139c..230598cb 100644 --- a/crates/imap/src/op/select.rs +++ b/crates/imap/src/op/select.rs @@ -52,13 +52,14 @@ impl Session { } if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) { - // Syncronize messages + // Synchronize messages match data.fetch_messages(&mailbox).await { Ok(state) => { - let closed_previous = self.state.is_mailbox_selected(); + let closed_previous = self.state.close_mailbox(); let is_condstore = self.is_condstore || arguments.condstore; // Build new state + let is_rev2 = self.version.is_rev2(); let uid_validity = state.uid_validity; let uid_next = state.uid_next; let total_messages = state.total_messages; @@ -105,6 +106,7 @@ impl Session { mailbox.clone(), true, true, + is_rev2, false, ) .await; @@ -115,12 +117,12 @@ impl Session { let response = Response { mailbox: ListItem::new(arguments.mailbox_name), total_messages, - recent_messages: 0, + recent_messages: data.get_recent_count(&mailbox.id), unseen_seq: 0, uid_validity, uid_next, closed_previous, - is_rev2: self.version.is_rev2(), + is_rev2, highest_modseq, mailbox_id: Id::from_parts( mailbox.id.account_id, @@ -164,6 +166,7 @@ impl Session { } pub async fn handle_unselect(&mut self, request: Request) -> crate::OpResult { + self.state.close_mailbox(); self.state = State::Authenticated { data: self.state.session_data(), }; diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs index 37ea70d9..84e3fbe2 100644 --- a/crates/imap/src/op/status.rs +++ b/crates/imap/src/op/status.rs @@ -29,7 +29,10 @@ use imap_proto::{ receiver::Request, Command, ResponseCode, StatusResponse, }; -use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; +use jmap_proto::{ + object::Object, + types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value}, +}; use store::roaring::RoaringBitmap; use store::Deserialize; use tokio::io::AsyncRead; @@ -127,7 +130,6 @@ impl SessionData { // Make sure all requested fields are up to date let mut items_update = Vec::with_capacity(items.len()); let mut items_response = Vec::with_capacity(items.len()); - let mut do_synchronize = false; for account in self.mailboxes.lock().iter_mut() { if account.account_id == mailbox.account_id { @@ -135,6 +137,7 @@ impl SessionData { .mailbox_state .entry(mailbox.mailbox_id) .or_insert_with(Mailbox::default); + let update_recent = mailbox_state.total_messages.is_none(); for item in items { match item { Status::Messages => { @@ -149,7 +152,6 @@ impl SessionData { items_response.push((*item, StatusItemType::Number(value as u64))); } else { items_update.push_unique(*item); - do_synchronize = true; } } Status::UidValidity => { @@ -157,7 +159,6 @@ impl SessionData { items_response.push((*item, StatusItemType::Number(value as u64))); } else { items_update.push_unique(*item); - do_synchronize = true; } } Status::Unseen => { @@ -197,7 +198,14 @@ impl SessionData { )); } Status::Recent => { - items_response.push((*item, StatusItemType::Number(0))); + if !update_recent { + items_response.push(( + *item, + StatusItemType::Number(mailbox_state.recent_messages.len()), + )); + } else { + items_update.push_unique(*item); + } } } } @@ -208,12 +216,6 @@ impl SessionData { if !items_update.is_empty() { // Retrieve latest values let mut values_update = Vec::with_capacity(items_update.len()); - let mailbox_state = if do_synchronize { - self.fetch_messages(&mailbox).await?.into() - } else { - None - }; - let mailbox_message_ids = self .jmap .get_tag( @@ -232,8 +234,38 @@ impl SessionData { for item in items_update { let result = match item { Status::Messages => mailbox_message_ids.as_ref().map(|v| v.len()).unwrap_or(0), - Status::UidNext => mailbox_state.as_ref().unwrap().uid_next as u64, - Status::UidValidity => mailbox_state.as_ref().unwrap().uid_validity as u64, + Status::UidNext => { + (self + .jmap + .get_property::( + mailbox.account_id, + Collection::Mailbox, + mailbox.mailbox_id, + Property::EmailIds, + ) + .await? + .unwrap_or(0) + + 1) as u64 + } + Status::UidValidity => self + .jmap + .get_property::>( + mailbox.account_id, + Collection::Mailbox, + mailbox.mailbox_id, + &Property::Value, + ) + .await? + .and_then(|obj| obj.get(&Property::Cid).as_uint()) + .ok_or_else(|| { + tracing::debug!(event = "error", + context = "store", + account_id = mailbox.account_id, + collection = ?Collection::Mailbox, + mailbox_id = mailbox.mailbox_id, + "Failed to obtain uid validity"); + StatusResponse::no("Mailbox unavailable.") + })?, Status::Unseen => { if let (Some(message_ids), Some(mailbox_message_ids)) = (&message_ids, &mailbox_message_ids) @@ -284,7 +316,11 @@ impl SessionData { 0 } } - Status::HighestModSeq | Status::MailboxId | Status::Recent => { + Status::Recent => { + self.fetch_messages(&mailbox).await?; + 0 + } + Status::HighestModSeq | Status::MailboxId => { unreachable!() } }; @@ -309,7 +345,15 @@ impl SessionData { Status::Unseen => mailbox_state.total_unseen = value.into(), Status::Deleted => mailbox_state.total_deleted = value.into(), Status::Size => mailbox_state.size = value.into(), - Status::HighestModSeq | Status::MailboxId | Status::Recent => { + Status::Recent => { + items_response + .iter_mut() + .find(|(i, _)| *i == Status::Recent) + .unwrap() + .1 = + StatusItemType::Number(mailbox_state.recent_messages.len()); + } + Status::HighestModSeq | Status::MailboxId => { unreachable!() } } diff --git a/tests/src/imap/copy_move.rs b/tests/src/imap/copy_move.rs index 055e0161..fac79573 100644 --- a/tests/src/imap/copy_move.rs +++ b/tests/src/imap/copy_move.rs @@ -25,59 +25,102 @@ use imap_proto::ResponseType; use super::{AssertResult, ImapConnection, Type}; -pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { +pub async fn test(_imap: &mut ImapConnection, imap_check: &mut ImapConnection) { // Check status - imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))") + imap_check + .send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE RECENT))") .await; - imap.assert_read(Type::Tagged, ResponseType::Ok) + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) .await - .assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)"); + .assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193 RECENT 0)"); // Select INBOX - imap.send("SELECT INBOX").await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("SELECT INBOX").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; // Copying to the same mailbox should fail - imap.send("COPY 1:* INBOX").await; - imap.assert_read(Type::Tagged, ResponseType::No) + imap_check.send("COPY 1:* INBOX").await; + imap_check + .assert_read(Type::Tagged, ResponseType::No) .await .assert_response_code("CANNOT"); // Copying to a non-existent mailbox should fail - imap.send("COPY 1:* \"/dev/null\"").await; - imap.assert_read(Type::Tagged, ResponseType::No) + imap_check.send("COPY 1:* \"/dev/null\"").await; + imap_check + .assert_read(Type::Tagged, ResponseType::No) .await .assert_response_code("TRYCREATE"); // Create test folders - imap.send("CREATE \"Scamorza Affumicata\"").await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; - imap.send("CREATE \"Burrata al Tartufo\"").await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("CREATE \"Scamorza Affumicata\"").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("CREATE \"Burrata al Tartufo\"").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; // Copy messages - imap.send("COPY 1,3,5,7 \"Scamorza Affumicata\"").await; - imap.assert_read(Type::Tagged, ResponseType::Ok) + imap_check + .send("COPY 1,3,5,7 \"Scamorza Affumicata\"") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) .await .assert_contains("COPYUID") .assert_contains("1:4"); // Check status - imap.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE)") + imap_check + .send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE RECENT)") .await; - imap.assert_read(Type::Tagged, ResponseType::Ok) + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) .await .assert_contains("MESSAGES 4") + .assert_contains("RECENT 4") .assert_contains("UNSEEN 4") .assert_contains("UIDNEXT 5") .assert_contains("SIZE 5851"); - // Move all messages to Burrata - imap.send("SELECT \"Scamorza Affumicata\"").await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; + // Check \Recent flag + imap_check.send("SELECT \"Scamorza Affumicata\"").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* 4 RECENT"); + imap_check.send("FETCH 1:* (UID FLAGS)").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("\\Recent", 4); + imap_check.send("UNSELECT").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE RECENT)") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("MESSAGES 4") + .assert_contains("RECENT 0") + .assert_contains("UNSEEN 4") + .assert_contains("UIDNEXT 5") + .assert_contains("SIZE 5851"); + imap_check.send("SELECT \"Scamorza Affumicata\"").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* 0 RECENT"); + imap_check.send("FETCH 1:* (UID FLAGS)").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("\\Recent", 0); - imap.send("MOVE 1:* \"Burrata al Tartufo\"").await; - imap.assert_read(Type::Tagged, ResponseType::Ok) + // Move all messages to Burrata + imap_check.send("MOVE 1:* \"Burrata al Tartufo\"").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) .await .assert_contains("* OK [COPYUID") .assert_contains("1:4") @@ -87,20 +130,23 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { .assert_contains("* 1 EXPUNGE"); // Check status - imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))") + imap_check + .send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))") .await; - imap.assert_read(Type::Tagged, ResponseType::Ok) + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) .await .assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 4 UNSEEN 4 SIZE 5851)") .assert_contains("\"Scamorza Affumicata\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)") .assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)"); // Move the messages back to Scamorza, UIDNEXT should increase. - imap.send("SELECT \"Burrata al Tartufo\"").await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("SELECT \"Burrata al Tartufo\"").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; - imap.send("MOVE 1:* \"Scamorza Affumicata\"").await; - imap.assert_read(Type::Tagged, ResponseType::Ok) + imap_check.send("MOVE 1:* \"Scamorza Affumicata\"").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) .await .assert_contains("* OK [COPYUID") .assert_contains("5:8") @@ -110,9 +156,11 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { .assert_contains("* 1 EXPUNGE"); // Check status - imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))") + imap_check + .send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))") .await; - imap.assert_read(Type::Tagged, ResponseType::Ok) + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) .await .assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)") .assert_contains("\"Scamorza Affumicata\" (UIDNEXT 9 MESSAGES 4 UNSEEN 4 SIZE 5851)") diff --git a/tests/src/imap/fetch.rs b/tests/src/imap/fetch.rs index d1e16290..cfd71106 100644 --- a/tests/src/imap/fetch.rs +++ b/tests/src/imap/fetch.rs @@ -144,7 +144,8 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { .assert_contains("* 7 FETCH (UID 7 ") .assert_contains("* 8 FETCH (UID 8 ") .assert_contains("* 9 FETCH (UID 9 ") - .assert_contains("* 10 FETCH (UID 10 "); + .assert_contains("* 10 FETCH (UID 10 ") + .assert_count("\\Recent", 0); imap.send("FETCH 7:* (UID FLAGS)").await; imap.assert_read(Type::Tagged, ResponseType::Ok) diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 0bff5d0b..0060f251 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -560,10 +560,11 @@ impl AssertResult for Vec { assert_eq!( self.iter().filter(|l| l.contains(text)).count(), occurrences, - "Expected {} occurrences of {:?}, found {}.", + "Expected {} occurrences of {:?}, found {} in {:?}.", occurrences, text, - self.iter().filter(|l| l.contains(text)).count() + self.iter().filter(|l| l.contains(text)).count(), + self ); self }