mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-06 18:45:45 +08:00
IMAP4rev1 Recent flag support
This commit is contained in:
parent
8f6ac2d114
commit
0168c1dca8
14 changed files with 265 additions and 87 deletions
|
@ -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 {
|
||||
|
|
|
@ -36,10 +36,10 @@ use super::{SelectedMailbox, Session, SessionData, State, IMAP};
|
|||
|
||||
impl<T: AsyncRead> Session<T> {
|
||||
pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result<bool> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<u32>,
|
||||
pub uid_next: Option<u32>,
|
||||
pub size: Option<u32>,
|
||||
pub recent_messages: RoaringBitmap,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -58,7 +58,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender<Even
|
|||
size = bytes.len()
|
||||
);
|
||||
|
||||
let c = print!("{}", String::from_utf8_lossy(&bytes));
|
||||
//let c = print!("{}", String::from_utf8_lossy(&bytes));
|
||||
|
||||
match stream_tx.write_all(bytes.as_ref()).await {
|
||||
Ok(_) => {
|
||||
|
@ -101,7 +101,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender<Even
|
|||
size = bytes.len()
|
||||
);
|
||||
|
||||
let c = print!("{}", String::from_utf8_lossy(&bytes));
|
||||
//let c = print!("{}", String::from_utf8_lossy(&bytes));
|
||||
|
||||
match stream_tx.write_all(bytes.as_ref()).await {
|
||||
Ok(_) => {
|
||||
|
@ -132,10 +132,10 @@ impl<T: AsyncRead> Session<T> {
|
|||
pub async fn write_bytes(&self, bytes: impl Into<Cow<'static, [u8]>>) -> 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<T: AsyncRead> Session<T> {
|
|||
impl SessionData {
|
||||
pub async fn write_bytes(&self, bytes: impl Into<Cow<'static, [u8]>>) -> 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);
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ impl<T: AsyncRead> Session<T> {
|
|||
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<T: AsyncRead> Session<T> {
|
|||
|
||||
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<SelectedMailbox>,
|
||||
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 => {
|
||||
|
|
|
@ -268,6 +268,7 @@ impl SessionData {
|
|||
mailbox.clone(),
|
||||
true,
|
||||
is_qresync,
|
||||
is_rev2,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -52,13 +52,14 @@ impl<T: AsyncRead> Session<T> {
|
|||
}
|
||||
|
||||
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<T: AsyncRead> Session<T> {
|
|||
mailbox.clone(),
|
||||
true,
|
||||
true,
|
||||
is_rev2,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
@ -115,12 +117,12 @@ impl<T: AsyncRead> Session<T> {
|
|||
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<T: AsyncRead> Session<T> {
|
|||
}
|
||||
|
||||
pub async fn handle_unselect(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
self.state.close_mailbox();
|
||||
self.state = State::Authenticated {
|
||||
data: self.state.session_data(),
|
||||
};
|
||||
|
|
|
@ -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::<u32>(
|
||||
mailbox.account_id,
|
||||
Collection::Mailbox,
|
||||
mailbox.mailbox_id,
|
||||
Property::EmailIds,
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
+ 1) as u64
|
||||
}
|
||||
Status::UidValidity => self
|
||||
.jmap
|
||||
.get_property::<Object<Value>>(
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -560,10 +560,11 @@ impl AssertResult for Vec<String> {
|
|||
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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue