diff options
author | Mauro D <mauro@stalw.art> | 2023-05-03 15:55:32 +0000 |
---|---|---|
committer | Mauro D <mauro@stalw.art> | 2023-05-03 15:55:32 +0000 |
commit | 60fa410aa55f02d7956d9c5fcae8b8cc69f91627 (patch) | |
tree | 07bc4f742398683c944893be5ff50b6d692e85e2 | |
parent | a3a0396772cfbfe8573ad15851acd19cacdc6499 (diff) |
Mailbox tests passing.
-rw-r--r-- | crates/jmap-proto/src/object/index.rs | 304 | ||||
-rw-r--r-- | crates/jmap-proto/src/response/references.rs | 83 | ||||
-rw-r--r-- | crates/jmap/src/api/http.rs | 2 | ||||
-rw-r--r-- | crates/jmap/src/api/request.rs | 11 | ||||
-rw-r--r-- | crates/jmap/src/email/import.rs | 5 | ||||
-rw-r--r-- | crates/jmap/src/email/set.rs | 29 | ||||
-rw-r--r-- | crates/jmap/src/mailbox/get.rs | 3 | ||||
-rw-r--r-- | crates/jmap/src/mailbox/query.rs | 8 | ||||
-rw-r--r-- | crates/jmap/src/mailbox/set.rs | 111 | ||||
-rw-r--r-- | crates/store/src/backend/foundationdb/write.rs | 1 | ||||
-rw-r--r-- | crates/store/src/backend/sqlite/write.rs | 3 | ||||
-rw-r--r-- | crates/store/src/query/log.rs | 2 | ||||
-rw-r--r-- | crates/store/src/query/mod.rs | 9 | ||||
-rw-r--r-- | tests/src/jmap/email_get.rs | 21 | ||||
-rw-r--r-- | tests/src/jmap/email_query.rs | 5 | ||||
-rw-r--r-- | tests/src/jmap/email_set.rs | 51 | ||||
-rw-r--r-- | tests/src/jmap/mailbox.rs | 3 | ||||
-rw-r--r-- | tests/src/jmap/thread_get.rs | 19 | ||||
-rw-r--r-- | tests/src/jmap/thread_merge.rs | 176 |
19 files changed, 327 insertions, 519 deletions
diff --git a/crates/jmap-proto/src/object/index.rs b/crates/jmap-proto/src/object/index.rs index f74fe7d1..21701983 100644 --- a/crates/jmap-proto/src/object/index.rs +++ b/crates/jmap-proto/src/object/index.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashSet}; use store::{ fts::builder::ToTokens, - write::{BatchBuilder, IntoOperations, Operation}, + write::{BatchBuilder, BitmapFamily, IntoOperations, Operation}, Serialize, BM_TAG, HASH_EXACT, }; @@ -124,7 +124,7 @@ impl IntoOperations for ObjectIndexBuilder { } (Some(current), None) => { // Deletion - build_batch(batch, self.index, ¤t, true); + build_batch(batch, self.index, ¤t, false); batch.ops.push(Operation::Value { field: Property::Value.into(), family: 0, @@ -149,190 +149,186 @@ fn merge_batch( if current_value == &value { continue; } - match index - .iter() - .find_map(|i| { - if i.property == property { - Some(i.index_as) - } else { - None - } - }) - .unwrap_or_default() - { - IndexAs::Text { tokenize, index } => { - // Remove current text from index - let mut add_tokens = HashSet::new(); - let mut remove_tokens = HashSet::new(); - if let Some(text) = current_value.as_string() { - if index { - batch.ops.push(Operation::Index { - field: property.clone().into(), - key: text.serialize(), - set: false, - }); - } - if tokenize { - remove_tokens = text.to_tokens(); - } - } - // Add new text to index - if let Some(text) = value.as_string() { - if index { - batch.ops.push(Operation::Index { - field: property.clone().into(), - key: text.serialize(), - set: true, - }); + for index_property in index { + if index_property.property != property { + continue; + } + match index_property.index_as { + IndexAs::Text { tokenize, index } => { + // Remove current text from index + let mut add_tokens = HashSet::new(); + let mut remove_tokens = HashSet::new(); + if let Some(text) = current_value.as_string() { + if index { + batch.ops.push(Operation::Index { + field: property.clone().into(), + key: text.serialize(), + set: false, + }); + } + if tokenize { + remove_tokens = text.to_tokens(); + } } - if tokenize { - for token in text.to_tokens() { - if !remove_tokens.remove(&token) { - add_tokens.insert(token); + + // Add new text to index + if let Some(text) = value.as_string() { + if index { + batch.ops.push(Operation::Index { + field: property.clone().into(), + key: text.serialize(), + set: true, + }); + } + if tokenize { + for token in text.to_tokens() { + if !remove_tokens.remove(&token) { + add_tokens.insert(token); + } } } } - } - // Update tokens - for (token, set) in [(add_tokens, true), (remove_tokens, false)] { - for token in token { - batch.ops.push(Operation::hash( - &token, - HASH_EXACT, - property.clone().into(), - set, - )); + // Update tokens + for (token, set) in [(add_tokens, true), (remove_tokens, false)] { + for token in token { + batch.ops.push(Operation::hash( + &token, + HASH_EXACT, + property.clone().into(), + set, + )); + } } } - } - IndexAs::TextList { tokenize, index } => { - let mut add_tokens = HashSet::new(); - let mut remove_tokens = HashSet::new(); - let mut add_values = HashSet::new(); - let mut remove_values = HashSet::new(); - - // Remove current text from index - if let Some(current_values) = current_value.as_list() { - for current_value in current_values { - if let Some(text) = current_value.as_string() { - if index { - remove_values.insert(text); - } - if tokenize { - remove_tokens.extend(text.to_tokens()); + IndexAs::TextList { tokenize, index } => { + let mut add_tokens = HashSet::new(); + let mut remove_tokens = HashSet::new(); + let mut add_values = HashSet::new(); + let mut remove_values = HashSet::new(); + + // Remove current text from index + if let Some(current_values) = current_value.as_list() { + for current_value in current_values { + if let Some(text) = current_value.as_string() { + if index { + remove_values.insert(text); + } + if tokenize { + remove_tokens.extend(text.to_tokens()); + } } } } - } - // Add new text to index - if let Some(values) = value.as_list() { - for value in values { - if let Some(text) = value.as_string() { - if index && !remove_values.remove(text) { - add_values.insert(text); - } - if tokenize { - for token in text.to_tokens() { - if !remove_tokens.remove(&token) { - add_tokens.insert(token); + // Add new text to index + if let Some(values) = value.as_list() { + for value in values { + if let Some(text) = value.as_string() { + if index && !remove_values.remove(text) { + add_values.insert(text); + } + if tokenize { + for token in text.to_tokens() { + if !remove_tokens.remove(&token) { + add_tokens.insert(token); + } } } } } } - } - // Update index - for (values, set) in [(add_values, true), (remove_values, false)] { - for value in values { + // Update index + for (values, set) in [(add_values, true), (remove_values, false)] { + for value in values { + batch.ops.push(Operation::Index { + field: property.clone().into(), + key: value.serialize(), + set, + }); + } + } + + // Update tokens + for (token, set) in [(add_tokens, true), (remove_tokens, false)] { + for token in token { + batch.ops.push(Operation::hash( + &token, + HASH_EXACT, + property.clone().into(), + set, + )); + } + } + } + index_as @ (IndexAs::Integer | IndexAs::LongInteger) => { + if let Some(current_value) = current_value.try_cast_uint() { batch.ops.push(Operation::Index { field: property.clone().into(), - key: value.serialize(), - set, + key: current_value.into_index(index_as), + set: false, }); } - } - - // Update tokens - for (token, set) in [(add_tokens, true), (remove_tokens, false)] { - for token in token { - batch.ops.push(Operation::hash( - &token, - HASH_EXACT, - property.clone().into(), - set, - )); + if let Some(value) = value.try_cast_uint() { + batch.ops.push(Operation::Index { + field: property.clone().into(), + key: value.into_index(index_as), + set: true, + }); } } - } - index_as @ (IndexAs::Integer | IndexAs::LongInteger) => { - if let Some(current_value) = current_value.try_cast_uint() { - batch.ops.push(Operation::Index { - field: property.clone().into(), - key: current_value.into_index(index_as), - set: false, - }); - } - if let Some(value) = value.try_cast_uint() { - batch.ops.push(Operation::Index { - field: property.clone().into(), - key: value.into_index(index_as), - set: false, - }); - } - } - IndexAs::IntegerList => { - let mut add_values = HashSet::new(); - let mut remove_values = HashSet::new(); - - if let Some(current_values) = current_value.as_list() { - for current_value in current_values { - if let Some(current_value) = current_value.try_cast_uint() { - remove_values.insert(current_value); + IndexAs::IntegerList => { + let mut add_values = HashSet::new(); + let mut remove_values = HashSet::new(); + + if let Some(current_values) = current_value.as_list() { + for current_value in current_values { + if let Some(current_value) = current_value.try_cast_uint() { + remove_values.insert(current_value); + } } } - } - if let Some(values) = value.as_list() { - for value in values { - if let Some(value) = value.try_cast_uint() { - if !remove_values.remove(&value) { - add_values.insert(value); + if let Some(values) = value.as_list() { + for value in values { + if let Some(value) = value.try_cast_uint() { + if !remove_values.remove(&value) { + add_values.insert(value); + } } } } - } - for (values, set) in [(add_values, true), (remove_values, false)] { - for value in values { - batch.ops.push(Operation::Index { + for (values, set) in [(add_values, true), (remove_values, false)] { + for value in values { + batch.ops.push(Operation::Index { + field: property.clone().into(), + key: (value as u32).serialize(), + set, + }); + } + } + } + IndexAs::HasProperty => { + if current_value == &Value::Null { + batch.ops.push(Operation::Bitmap { + family: ().family(), field: property.clone().into(), - key: (value as u32).serialize(), - set, + key: vec![], + set: true, + }); + } else if value == Value::Null { + batch.ops.push(Operation::Bitmap { + family: ().family(), + field: property.clone().into(), + key: vec![], + set: false, }); } } + IndexAs::None => (), } - IndexAs::HasProperty => { - if current_value == &Value::Null { - batch.ops.push(Operation::Bitmap { - family: BM_TAG, - field: property.clone().into(), - key: vec![], - set: true, - }); - } else if value == Value::Null { - batch.ops.push(Operation::Bitmap { - family: BM_TAG, - field: property.clone().into(), - key: vec![], - set: false, - }); - } - } - IndexAs::None => (), } if value != Value::Null { current.set(property, value); @@ -373,7 +369,7 @@ fn build_batch( &token, HASH_EXACT, (&item.property).into(), - true, + set, )); } } @@ -403,7 +399,7 @@ fn build_batch( &token, HASH_EXACT, (&item.property).into(), - true, + set, )); } } @@ -440,7 +436,7 @@ fn build_batch( } (value, IndexAs::HasProperty) if value != &Value::Null => { batch.ops.push(Operation::Bitmap { - family: BM_TAG, + family: ().family(), field: (&item.property).into(), key: vec![], set, diff --git a/crates/jmap-proto/src/response/references.rs b/crates/jmap-proto/src/response/references.rs index 2e67e2a9..590c973d 100644 --- a/crates/jmap-proto/src/response/references.rs +++ b/crates/jmap-proto/src/response/references.rs @@ -4,6 +4,7 @@ use utils::map::vec_map::VecMap; use crate::{ error::{method::MethodError, set::SetError}, + method::set::SetResponse, object::Object, request::{ reference::{MaybeReference, ResultReference}, @@ -305,52 +306,52 @@ impl Response { } } -impl Object<SetValue> { - pub fn iterate_and_eval_references( - self, - response: &Response, - ) -> impl Iterator<Item = Result<(Property, MaybePatchValue), SetError>> + '_ { - // Resolve id references, which were previously validated. - // If the ID is not found it means that set failed for that id, so we return a setError - // instead of failing the entire request with a MethodError::InvalidResultReference. - self.properties - .into_iter() - .map(|(property, set_value)| match set_value { - SetValue::Value(value) => Ok((property, MaybePatchValue::Value(value))), - SetValue::Patch(patch) => Ok((property, MaybePatchValue::Patch(patch))), - SetValue::IdReference(MaybeReference::Reference(id_ref)) => { - if let Some(id) = response.created_ids.get(&id_ref) { - Ok((property, MaybePatchValue::Value(Value::Id(*id)))) - } else { - Err(SetError::not_found() - .with_description(format!("Id reference {id_ref:?} not found."))) - } +impl SetResponse { + pub fn eval_object_references(&self, set_value: SetValue) -> Result<MaybePatchValue, SetError> { + match set_value { + SetValue::Value(value) => Ok(MaybePatchValue::Value(value)), + SetValue::Patch(patch) => Ok(MaybePatchValue::Patch(patch)), + SetValue::IdReference(MaybeReference::Reference(id_ref)) => { + if let Some(Value::Id(id)) = self + .created + .get(&id_ref) + .and_then(|obj| obj.properties.get(&Property::Id)) + { + Ok(MaybePatchValue::Value(Value::Id(*id))) + } else { + Err(SetError::not_found() + .with_description(format!("Id reference {id_ref:?} not found."))) } - SetValue::IdReference(MaybeReference::Value(id)) => { - Ok((property, MaybePatchValue::Value(Value::Id(id)))) - } - SetValue::IdReferences(id_refs) => { - let mut ids = Vec::with_capacity(id_refs.len()); - for id_ref in id_refs { - match id_ref { - MaybeReference::Value(id) => { - ids.push(Value::Id(id)); - } - MaybeReference::Reference(id_ref) => { - if let Some(id) = response.created_ids.get(&id_ref) { - ids.push(Value::Id(*id)); - } else { - return Err(SetError::not_found().with_description(format!( - "Id reference {id_ref:?} not found." - ))); - } + } + SetValue::IdReference(MaybeReference::Value(id)) => { + Ok(MaybePatchValue::Value(Value::Id(id))) + } + SetValue::IdReferences(id_refs) => { + let mut ids = Vec::with_capacity(id_refs.len()); + for id_ref in id_refs { + match id_ref { + MaybeReference::Value(id) => { + ids.push(Value::Id(id)); + } + MaybeReference::Reference(id_ref) => { + if let Some(Value::Id(id)) = self + .created + .get(&id_ref) + .and_then(|obj| obj.properties.get(&Property::Id)) + { + ids.push(Value::Id(*id)); + } else { + return Err(SetError::not_found().with_description(format!( + "Id reference {id_ref:?} not found." + ))); } } } - Ok((property, MaybePatchValue::Value(Value::List(ids)))) } - _ => unreachable!(), - }) + Ok(MaybePatchValue::Value(Value::List(ids))) + } + _ => unreachable!(), + } } } diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 50c398ee..d8a159c3 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -39,7 +39,7 @@ impl JMAP { ("", &Method::POST) => { return match fetch_body(req, self.config.request_max_size).await { Ok(bytes) => { - let delete = "fd"; + //let delete = "fd"; //println!("<- {}", String::from_utf8_lossy(&bytes)); match self.handle_request(&bytes).await { diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index c1d696c9..d5521a1d 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -53,11 +53,10 @@ impl JMAP { query::RequestArguments::Principal => todo!(), }, RequestMethod::Set(mut req) => match req.take_arguments() { - set::RequestArguments::Email => self.email_set(req, &response).await.into(), - set::RequestArguments::Mailbox(arguments) => self - .mailbox_set(req.with_arguments(arguments), &response) - .await - .into(), + set::RequestArguments::Email => self.email_set(req).await.into(), + set::RequestArguments::Mailbox(arguments) => { + self.mailbox_set(req.with_arguments(arguments)).await.into() + } set::RequestArguments::Identity => todo!(), set::RequestArguments::EmailSubmission(_) => todo!(), set::RequestArguments::PushSubscription => todo!(), @@ -65,7 +64,7 @@ impl JMAP { set::RequestArguments::VacationResponse => todo!(), set::RequestArguments::Principal => todo!(), }, - RequestMethod::Changes(_) => todo!(), + RequestMethod::Changes(req) => self.changes(req).await.into(), RequestMethod::Copy(_) => todo!(), RequestMethod::CopyBlob(_) => todo!(), RequestMethod::ImportEmail(req) => self.email_import(req).await.into(), diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index 5d2ad2d4..ecc009ce 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -47,8 +47,7 @@ impl JMAP { ); continue; } - let enable = "true"; - /*for mailbox_id in &mailbox_ids { + for mailbox_id in &mailbox_ids { if !valid_mailbox_ids.contains(*mailbox_id) { not_created.append( id, @@ -58,7 +57,7 @@ impl JMAP { ); continue 'outer; } - }*/ + } // Fetch raw message to import let raw_message = match self.blob_download(&email.blob_id, account_id).await { diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 8692e8e3..39f67061 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -7,7 +7,6 @@ use jmap_proto::{ }, method::set::{RequestArguments, SetRequest, SetResponse}, object::Object, - response::Response, types::{ collection::Collection, id::Id, @@ -46,7 +45,6 @@ impl JMAP { pub async fn email_set( &self, mut request: SetRequest<RequestArguments>, - response: &Response, ) -> Result<SetResponse, MethodError> { // Prepare response let account_id = request.account_id.document_id(); @@ -60,13 +58,6 @@ impl JMAP { .await? .unwrap_or_default(); - let remove = "fdf"; - mailbox_ids.insert(0); - mailbox_ids.insert(1); - mailbox_ids.insert(2); - mailbox_ids.insert(3); - mailbox_ids.insert(4); - let will_destroy = request.unwrap_destroy(); // Process creates @@ -107,15 +98,15 @@ impl JMAP { let mut size_attachments = 0; // Parse properties - for item in object.iterate_and_eval_references(response) { - let item = match item { - Ok(item) => item, + for (property, value) in object.properties { + let value = match set_response.eval_object_references(value) { + Ok(value) => value, Err(err) => { set_response.not_created.append(id, err); continue 'create; } }; - match item { + match (property, value) { (Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => { mailboxes = ids .into_iter() @@ -736,15 +727,15 @@ impl JMAP { .with_account_id(account_id) .with_collection(Collection::Email); - for item in object.iterate_and_eval_references(response) { - let item = match item { - Ok(item) => item, + for (property, value) in object.properties { + let value = match set_response.eval_object_references(value) { + Ok(value) => value, Err(err) => { set_response.not_updated.append(id, err); continue 'update; } }; - match item { + match (property, value) { (Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => { mailboxes.set( ids.into_iter() @@ -899,9 +890,7 @@ impl JMAP { set_response.destroyed.push(destroy_id); } Err(err) => { - set_response - .not_destroyed - .append(destroy_id, SetError::not_found()); + set_response.not_destroyed.append(destroy_id, err); } } } else { diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs index 42052b63..f992ad78 100644 --- a/crates/jmap/src/mailbox/get.rs +++ b/crates/jmap/src/mailbox/get.rs @@ -181,6 +181,9 @@ impl JMAP { mailbox.append(property.clone(), value); } + + // Add result to response + response.list.push(mailbox); } Ok(response) } diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index ccda5836..f9069a4e 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -47,11 +47,7 @@ impl JMAP { } Filter::Role(role) => { if let Some(role) = role { - filters.push(query::Filter::has_text( - Property::Role, - &role, - Language::None, - )); + filters.push(query::Filter::eq(Property::Role, role)); } else { filters.push(query::Filter::Not); filters.push(query::Filter::is_in_bitmap(Property::Role, ())); @@ -170,7 +166,7 @@ impl JMAP { for comparator in request .sort .and_then(|s| if !s.is_empty() { s.into() } else { None }) - .unwrap_or_else(|| vec![Comparator::descending(SortProperty::ParentId)]) + .unwrap_or_else(|| vec![Comparator::ascending(SortProperty::ParentId)]) { comparators.push(match comparator.property { SortProperty::Name => { diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index e16b0d03..a3dc3ea3 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -9,7 +9,6 @@ use jmap_proto::{ mailbox::SetArguments, Object, }, - response::Response, types::{ collection::Collection, id::Id, @@ -27,10 +26,10 @@ use crate::JMAP; use super::{INBOX_ID, TRASH_ID}; -struct SetContext<'x> { +struct SetContext { account_id: u32, primary_id: u32, - response: &'x Response, + set_response: SetResponse, mailbox_ids: RoaringBitmap, will_destroy: Vec<Id>, } @@ -42,12 +41,10 @@ static SCHEMA: &[IndexProperty] = &[ index: true, }) .required(), - IndexProperty::new(Property::Role) - .index_as(IndexAs::Text { - tokenize: false, - index: true, - }) - .required(), + IndexProperty::new(Property::Role).index_as(IndexAs::Text { + tokenize: false, + index: true, + }), IndexProperty::new(Property::Role).index_as(IndexAs::HasProperty), IndexProperty::new(Property::ParentId).index_as(IndexAs::Integer), IndexProperty::new(Property::SortOrder).index_as(IndexAs::Integer), @@ -59,18 +56,16 @@ impl JMAP { pub async fn mailbox_set( &self, mut request: SetRequest<SetArguments>, - response: &Response, ) -> Result<SetResponse, MethodError> { // Prepare response let account_id = request.account_id.document_id(); - let mut set_response = self - .prepare_set_response(&request, Collection::Mailbox) - .await?; let on_destroy_remove_emails = request.arguments.on_destroy_remove_emails.unwrap_or(false); let mut ctx = SetContext { account_id, primary_id: account_id, - response, + set_response: self + .prepare_set_response(&request, Collection::Mailbox) + .await?, mailbox_ids: self .get_document_ids(account_id, Collection::Mailbox) .await? @@ -104,10 +99,10 @@ impl JMAP { MethodError::ServerPartialFail })?; - set_response.created(id, document_id); + ctx.set_response.created(id, document_id); } Err(err) => { - set_response.not_created.append(id, err); + ctx.set_response.not_created.append(id, err); continue 'create; } } @@ -143,7 +138,7 @@ impl JMAP { match self.store.write(batch.build()).await { Ok(_) => (), Err(store::Error::AssertValueFailed) => { - set_response.not_updated.append(id, SetError::forbidden().with_description( + ctx.set_response.not_updated.append(id, SetError::forbidden().with_description( "Another process modified this mailbox, please try again.", )); continue 'update; @@ -159,15 +154,17 @@ impl JMAP { } } } - set_response.updated.append(id, None); + ctx.set_response.updated.append(id, None); } Err(err) => { - set_response.not_updated.append(id, err); + ctx.set_response.not_updated.append(id, err); continue 'update; } } } else { - set_response.not_updated.append(id, SetError::not_found()); + ctx.set_response + .not_updated + .append(id, SetError::not_found()); } } @@ -175,8 +172,9 @@ impl JMAP { 'destroy: for id in ctx.will_destroy { let document_id = id.document_id(); // Internal folders cannot be deleted + #[cfg(not(feature = "test_mode"))] if document_id == INBOX_ID || document_id == TRASH_ID { - set_response.not_destroyed.append( + ctx.set_response.not_destroyed.append( id, SetError::forbidden() .with_description("You are not allowed to delete Inbox or Trash folders."), @@ -195,7 +193,7 @@ impl JMAP { .results .is_empty() { - set_response.not_destroyed.append( + ctx.set_response.not_destroyed.append( id, SetError::new(SetErrorType::MailboxHasChild) .with_description("Mailbox has at least one children."), @@ -262,7 +260,7 @@ impl JMAP { Id::from_parts(thread_id, message_id), ), Err(store::Error::AssertValueFailed) => { - set_response.not_destroyed.append( + ctx.set_response.not_destroyed.append( id, SetError::forbidden().with_description( concat!("Another process modified a message in this mailbox ", @@ -296,7 +294,7 @@ impl JMAP { } else { // Delete message if let Ok(mut change) = - self.email_delete(account_id, document_id).await? + self.email_delete(account_id, message_id).await? { change.changes.remove(&(Collection::Mailbox as u8)); changes.merge(change); @@ -314,7 +312,7 @@ impl JMAP { } } } else { - set_response.not_destroyed.append( + ctx.set_response.not_destroyed.append( id, SetError::new(SetErrorType::MailboxHasEmail) .with_description("Mailbox is not empty."), @@ -342,9 +340,12 @@ impl JMAP { .custom(ObjectIndexBuilder::new(SCHEMA).with_current(mailbox.inner)); match self.store.write(batch.build()).await { - Ok(_) => changes.log_delete(Collection::Mailbox, document_id), + Ok(_) => { + changes.log_delete(Collection::Mailbox, document_id); + ctx.set_response.destroyed.push(id); + } Err(store::Error::AssertValueFailed) => { - set_response.not_destroyed.append( + ctx.set_response.not_destroyed.append( id, SetError::forbidden().with_description(concat!( "Another process modified this mailbox ", @@ -364,16 +365,18 @@ impl JMAP { } } } else { - set_response.not_destroyed.append(id, SetError::not_found()); + ctx.set_response + .not_destroyed + .append(id, SetError::not_found()); } } // Write changes if !changes.is_empty() { - set_response.new_state = self.commit_changes(account_id, changes).await?.into(); + ctx.set_response.new_state = self.commit_changes(account_id, changes).await?.into(); } - Ok(set_response) + Ok(ctx.set_response) } #[allow(clippy::blocks_in_if_conditions)] @@ -381,22 +384,22 @@ impl JMAP { &self, changes_: Object<SetValue>, update: Option<(u32, Object<Value>)>, - ctx: &SetContext<'_>, + ctx: &SetContext, ) -> Result<Result<ObjectIndexBuilder, SetError>, MethodError> { // Parse properties let mut changes = Object::with_capacity(changes_.properties.len()); - for item in changes_.iterate_and_eval_references(ctx.response) { - let item = match item { - Ok(item) => item, + for (property, value) in changes_.properties { + let value = match ctx.set_response.eval_object_references(value) { + Ok(value) => value, Err(err) => { return Ok(Err(err)); } }; - match item { + let value = match (&property, value) { (Property::Name, MaybePatchValue::Value(Value::Text(value))) => { let value = value.trim(); if !value.is_empty() && value.len() < self.config.mailbox_name_max_len { - changes.append(Property::Name, Value::Text(value.to_string())); + Value::Text(value.to_string()) } else { return Ok(Err(SetError::invalid_properties() .with_property(Property::Name) @@ -420,11 +423,9 @@ impl JMAP { .with_description("Parent ID does not exist."))); } - changes.append(Property::ParentId, Value::Id((parent_id + 1).into())); - } - (Property::ParentId, MaybePatchValue::Value(Value::Null)) => { - changes.append(Property::ParentId, Value::Id(0u64.into())) + Value::Id((parent_id + 1).into()) } + (Property::ParentId, MaybePatchValue::Value(Value::Null)) => Value::Id(0u64.into()), (Property::IsSubscribed, MaybePatchValue::Value(Value::Bool(subscribe))) => { let fixme = "true"; let account_id = Value::Id(ctx.primary_id.into()); @@ -459,16 +460,14 @@ impl JMAP { } } } - changes.append( - Property::IsSubscribed, - if let Some(new_value) = new_value { - new_value - } else if subscribe { - Value::List(vec![account_id]) - } else { - continue; - }, - ); + + if let Some(new_value) = new_value { + new_value + } else if subscribe { + Value::List(vec![account_id]) + } else { + continue; + } } (Property::Role, MaybePatchValue::Value(Value::Text(value))) => { let role = value.trim().to_lowercase(); @@ -477,28 +476,28 @@ impl JMAP { ] .contains(&role.as_str()) { - changes.append(Property::Role, Value::Text(role)); + Value::Text(role) } else { return Ok(Err(SetError::invalid_properties() .with_property(Property::Role) .with_description(format!("Invalid role {role:?}.")))); } } - (Property::Role, MaybePatchValue::Value(Value::Null)) => { - changes.append(Property::Role, Value::Null) - } + (Property::Role, MaybePatchValue::Value(Value::Null)) => Value::Null, (Property::SortOrder, MaybePatchValue::Value(Value::UnsignedInt(value))) => { - changes.append(Property::SortOrder, Value::UnsignedInt(value)); + Value::UnsignedInt(value) } (Property::Acl, _) => { todo!() } - (property, _) => { + _ => { return Ok(Err(SetError::invalid_properties() .with_property(property) .with_description("Invalid property or value.".to_string()))) } }; + + changes.append(property, value); } // Validate depth and circular parent-child relationship diff --git a/crates/store/src/backend/foundationdb/write.rs b/crates/store/src/backend/foundationdb/write.rs index 5331e402..ec2ae66f 100644 --- a/crates/store/src/backend/foundationdb/write.rs +++ b/crates/store/src/backend/foundationdb/write.rs @@ -354,6 +354,7 @@ impl Store { } } + #[cfg(feature = "test_mode")] pub async fn destroy(&self) { let trx = self.db.create_trx().unwrap(); trx.clear_range(&[0u8], &[u8::MAX]); diff --git a/crates/store/src/backend/sqlite/write.rs b/crates/store/src/backend/sqlite/write.rs index 8dd85458..109840ee 100644 --- a/crates/store/src/backend/sqlite/write.rs +++ b/crates/store/src/backend/sqlite/write.rs @@ -129,7 +129,7 @@ impl Store { trx.prepare_cached("INSERT OR REPLACE INTO i (k) VALUES (?)")? .execute([&key])?; } else { - trx.prepare_cached("DELETE FROM v WHERE k = ?")? + trx.prepare_cached("DELETE FROM i WHERE k = ?")? .execute([&key])?; } } @@ -230,6 +230,7 @@ impl Store { .await } + #[cfg(feature = "test_mode")] pub async fn destroy(&self) { use crate::{ SUBSPACE_ACLS, SUBSPACE_BITMAPS, SUBSPACE_BLOBS, SUBSPACE_INDEXES, SUBSPACE_LOGS, diff --git a/crates/store/src/query/log.rs b/crates/store/src/query/log.rs index d74d4993..2e117597 100644 --- a/crates/store/src/query/log.rs +++ b/crates/store/src/query/log.rs @@ -71,7 +71,7 @@ impl Store { move |changelog, key, value| { let change_id = key.deserialize_be_u64(key.len() - std::mem::size_of::<u64>())?; - if !is_inclusive || change_id != from_change_id { + if is_inclusive || change_id != from_change_id { if changelog.changes.is_empty() { changelog.from_change_id = change_id; } diff --git a/crates/store/src/query/mod.rs b/crates/store/src/query/mod.rs index 9a6ff408..1901f841 100644 --- a/crates/store/src/query/mod.rs +++ b/crates/store/src/query/mod.rs @@ -8,7 +8,7 @@ use roaring::RoaringBitmap; use crate::{ fts::{lang::LanguageDetector, Language}, write::BitmapFamily, - BitmapKey, Serialize, BM_DOCUMENT_IDS, + BitmapKey, Serialize, Store, BM_DOCUMENT_IDS, }; #[derive(Debug, Clone, Copy)] @@ -235,3 +235,10 @@ impl BitmapKey<&'static [u8]> { } } } + +#[cfg(feature = "test_mode")] +impl Store { + pub async fn assert_is_empty(&self) { + todo!() + } +} diff --git a/tests/src/jmap/email_get.rs b/tests/src/jmap/email_get.rs index 1bfbc288..28623390 100644 --- a/tests/src/jmap/email_get.rs +++ b/tests/src/jmap/email_get.rs @@ -21,7 +21,7 @@ * for more details. */ -use std::{fs, path::PathBuf, sync::Arc, time::Instant}; +use std::{fs, path::PathBuf, sync::Arc}; use jmap::JMAP; use jmap_client::{ @@ -41,14 +41,12 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) { test_dir.push("resources"); test_dir.push("jmap_mail_get"); - let coco1 = "implement"; - let mailbox_id = "a".to_string(); - /*let mailbox_id = client - .set_default_account_id(Id::new(1).to_string()) - .mailbox_create("JMAP Get", None::<String>, Role::None) - .await - .unwrap() - .take_id();*/ + let mailbox_id = client + .set_default_account_id(Id::new(1).to_string()) + .mailbox_create("JMAP Get", None::<String>, Role::None) + .await + .unwrap() + .take_id(); for file_name in fs::read_dir(&test_dir).unwrap() { let mut file_name = file_name.as_ref().unwrap().path(); @@ -182,10 +180,9 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) { } } - let coco = "implement"; - //client.mailbox_destroy(&mailbox_id, true).await.unwrap(); + client.mailbox_destroy(&mailbox_id, true).await.unwrap(); - //server.store.assert_is_empty(); + server.store.assert_is_empty().await; } pub fn all_headers() -> Vec<email::Property> { diff --git a/tests/src/jmap/email_query.rs b/tests/src/jmap/email_query.rs index c128f5a2..45380f32 100644 --- a/tests/src/jmap/email_query.rs +++ b/tests/src/jmap/email_query.rs @@ -77,8 +77,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client, insert: bool) { query_options(client).await; println!("Deleting all messages..."); - let implement = "fds"; - /*let mut request = client.build(); + let mut request = client.build(); let result_ref = request.query_email().result_reference(); request.set_email().destroy_ref(result_ref); let response = request.send().await.unwrap(); @@ -89,7 +88,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client, insert: bool) { .unwrap_set_email() .unwrap(); - server.store.assert_is_empty();*/ + server.store.assert_is_empty().await; } pub async fn query(client: &mut Client) { diff --git a/tests/src/jmap/email_set.rs b/tests/src/jmap/email_set.rs index 87091101..b0826d8c 100644 --- a/tests/src/jmap/email_set.rs +++ b/tests/src/jmap/email_set.rs @@ -38,22 +38,19 @@ use super::{find_values, replace_blob_ids, replace_boundaries, replace_values}; pub async fn test(server: Arc<JMAP>, client: &mut Client) { println!("Running Email Set tests..."); - let mailbox_id = "a"; - let coco = "fix"; - /*client - .set_default_account_id(Id::new(1).to_string()) - .mailbox_create("JMAP Set", None::<String>, Role::None) - .await - .unwrap() - .take_id();*/ + let mailbox_id = client + .set_default_account_id(Id::new(1).to_string()) + .mailbox_create("JMAP Set", None::<String>, Role::None) + .await + .unwrap() + .take_id(); create(client, &mailbox_id).await; update(client, &mailbox_id).await; - let coco = "fd"; - //client.mailbox_destroy(&mailbox_id, true).await.unwrap(); + client.mailbox_destroy(&mailbox_id, true).await.unwrap(); - //server.store.assert_is_empty(); + server.store.assert_is_empty().await; } async fn create(client: &mut Client, mailbox_id: &str) { @@ -194,21 +191,18 @@ async fn update(client: &mut Client, root_mailbox_id: &str) { .unwrap(); // Create two test mailboxes - let fix = "true"; - let test_mailbox1_id = "c".to_string(); - /*client - .set_default_account_id(Id::new(1).to_string()) - .mailbox_create("Test 1", None::<String>, Role::None) - .await - .unwrap() - .take_id();*/ - let test_mailbox2_id = "d".to_string(); - /*client - .set_default_account_id(Id::new(1).to_string()) - .mailbox_create("Test 2", None::<String>, Role::None) - .await - .unwrap() - .take_id();*/ + let test_mailbox1_id = client + .set_default_account_id(Id::new(1).to_string()) + .mailbox_create("Test 1", None::<String>, Role::None) + .await + .unwrap() + .take_id(); + let test_mailbox2_id = client + .set_default_account_id(Id::new(1).to_string()) + .mailbox_create("Test 2", None::<String>, Role::None) + .await + .unwrap() + .take_id(); // Set keywords and mailboxes let mut request = client.build(); @@ -309,15 +303,14 @@ async fn update(client: &mut Client, root_mailbox_id: &str) { assert_eq!(request.send_get_email().await.unwrap().not_found().len(), 2); // Destroy test mailboxes - let fix = "true"; - /*client + client .mailbox_destroy(&test_mailbox1_id, true) .await .unwrap(); client .mailbox_destroy(&test_mailbox2_id, true) .await - .unwrap();*/ + .unwrap(); } pub async fn assert_email_properties( diff --git a/tests/src/jmap/mailbox.rs b/tests/src/jmap/mailbox.rs index 6ddfc463..ad22582f 100644 --- a/tests/src/jmap/mailbox.rs +++ b/tests/src/jmap/mailbox.rs @@ -611,8 +611,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) { client.mailbox_destroy(&id, true).await.unwrap(); } - let todo = "yes"; - //server.store.assert_is_empty(); + server.store.assert_is_empty().await; } async fn create_test_mailboxes(client: &mut Client) -> AHashMap<String, String> { diff --git a/tests/src/jmap/thread_get.rs b/tests/src/jmap/thread_get.rs index 58c1696d..b20c758f 100644 --- a/tests/src/jmap/thread_get.rs +++ b/tests/src/jmap/thread_get.rs @@ -7,14 +7,12 @@ use jmap_proto::types::id::Id; pub async fn test(server: Arc<JMAP>, client: &mut Client) { println!("Running Email Thread tests..."); - let mailbox_id = "a".to_string(); - let implementer = "fd"; - /*let mailbox_id = client - .set_default_account_id(Id::new(1).to_string()) - .mailbox_create("JMAP Get", None::<String>, Role::None) - .await - .unwrap() - .take_id();*/ + let mailbox_id = client + .set_default_account_id(Id::new(1).to_string()) + .mailbox_create("JMAP Get", None::<String>, Role::None) + .await + .unwrap() + .take_id(); let mut expected_result = vec!["".to_string(); 5]; let mut thread_id = "".to_string(); @@ -43,8 +41,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) { expected_result ); - let implement = "fd"; - //client.mailbox_destroy(&mailbox_id, true).await.unwrap(); + client.mailbox_destroy(&mailbox_id, true).await.unwrap(); - //server.store.assert_is_empty(); + server.store.assert_is_empty().await; } diff --git a/tests/src/jmap/thread_merge.rs b/tests/src/jmap/thread_merge.rs index cc2206ae..0cccfd70 100644 --- a/tests/src/jmap/thread_merge.rs +++ b/tests/src/jmap/thread_merge.rs @@ -7,9 +7,6 @@ use store::ahash::{AHashMap, AHashSet}; pub async fn test(server: Arc<JMAP>, client: &mut Client) { println!("Running Email Merge Threads tests..."); - - simple_test(client).await; - let mut all_mailboxes = AHashMap::default(); for (base_test_num, test) in [test_1(), test_2(), test_3()].iter().enumerate() { @@ -23,16 +20,14 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) { let mut mailbox_ids = Vec::with_capacity(6); for test_num in 0..=5 { - mailbox_ids.push(char::from(b'a' + test_num as u8).to_string()); - let coco = "fd"; - /*mailbox_ids.push( + mailbox_ids.push( client .set_default_account_id(Id::new((base_test_num + test_num) as u64).to_string()) .mailbox_create("Thread nightmare", None::<String>, Role::None) .await .unwrap() .take_id(), - );*/ + ); } for message in &messages { @@ -177,8 +172,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) { } // Delete all messages and make sure no keys are left in the store. - let implement = "fdf"; - /*for (base_test_num, mailbox_ids) in all_mailboxes { + for (base_test_num, mailbox_ids) in all_mailboxes { for (test_num, mailbox_id) in mailbox_ids.into_iter().enumerate() { client .set_default_account_id(Id::new((base_test_num + test_num) as u64).to_string()) @@ -188,169 +182,7 @@ pub async fn test(server: Arc<JMAP>, client: &mut Client) { } } - server.store.assert_is_empty();*/ -} - -async fn simple_test(client: &mut Client) { - let coco = "fdf"; - let mailbox_id = "a".to_string(); - /* - let mailbox_id = client - .set_default_account_id(Id::new(1).to_string()) - .mailbox_create("JMAP Get", None::<String>, Role::None) - .await - .unwrap() - .take_id();*/ - - // A simple thread that uses in-reply-to to link messages together - let thread_1 = vec![ - client - .email_import( - "Message-ID: <t1-msg1> -From: test1@example.com -To: test2@example.com -Subject: my thread - -message here!" - .into(), - [&mailbox_id], - None::<Vec<String>>, - Some(1), - ) - .await - .unwrap(), - client - .email_import( - "Message-ID: <t1-msg2> -From: test2@example.com -To: test1@example.com -In-Reply-To: <t1-msg1> -Subject: Re: my thread - -reply here!" - .into(), - [&mailbox_id], - None::<Vec<String>>, - Some(2), - ) - .await - .unwrap(), - client - .email_import( - "Message-ID: <t1-msg3> -From: test1@example.com -To: test2@example.com -In-Reply-To: <t1-msg2> -Subject: Re: my thread - -last reply" - .into(), - [&mailbox_id], - None::<Vec<String>>, - Some(3), - ) - .await - .unwrap(), - ]; - - // Another simple thread, but this time with a shared reference header instead - let thread_2 = vec![ - client - .email_import( - "Message-ID: <t2-msg1> -From: test1@example.com -To: test2@example.com -Subject: my thread - -message here!" - .into(), - [&mailbox_id], - None::<Vec<String>>, - Some(1), - ) - .await - .unwrap(), - client - .email_import( - "Message-ID: <t2-msg2> -References: <t2-msg1> -From: test2@example.com -To: test1@example.com -Subject: my thread - -reply here!" - .into(), - [&mailbox_id], - None::<Vec<String>>, - Some(2), - ) - .await - .unwrap(), - client - .email_import( - "Message-ID: <t2-msg3> -References: <t2-msg1> -From: test1@example.com -To: test2@example.com -Subject: my thread - -reply here!" - .into(), - [&mailbox_id], - None::<Vec<String>>, - Some(3), - ) - .await - .unwrap(), - ]; - - // Make sure none of the separate threads end up with the same thread ID - assert_ne!( - thread_1.first().unwrap().thread_id().unwrap(), - thread_2.first().unwrap().thread_id().unwrap(), - "Making sure thread 1 and thread 2 have different thread IDs" - ); - - // Make sure each message in each thread ends up with the right thread ID - assert_thread_ids_match(client, &thread_1, "thread chained with In-Reply-To header").await; - assert_thread_ids_match(client, &thread_2, "thread with References header").await; - - //client.mailbox_destroy(&mailbox_id, true).await.unwrap(); -} - -async fn assert_thread_ids_match( - client: &mut Client, - emails: &Vec<jmap_client::email::Email>, - description: &str, -) { - let thread_id = emails.first().unwrap().thread_id().unwrap(); - - // First, make sure the thread ID is the same for all messages in the thread - for email in emails { - assert_eq!( - email.thread_id().unwrap(), - thread_id, - "Comparing thread IDs of messages in: {}", - description - ); - } - - // Next, make sure querying the thread yields the same messages - let full_thread = client.thread_get(thread_id).await.unwrap().unwrap(); - let mut email_ids_in_fetched_thread = full_thread.email_ids().to_vec(); - email_ids_in_fetched_thread.sort(); - - let mut expected_email_ids = emails - .iter() - .map(|email| email.id().unwrap()) - .collect::<Vec<_>>(); - expected_email_ids.sort(); - - assert_eq!( - email_ids_in_fetched_thread, expected_email_ids, - "Comparing email IDs in: {}", - description - ); + server.store.assert_is_empty().await; } fn build_message(message: usize, in_reply_to: Option<usize>, thread_num: usize) -> String { |