Mailbox tests passing.

This commit is contained in:
Mauro D 2023-05-03 15:55:32 +00:00
parent a3a0396772
commit 60fa410aa5
19 changed files with 334 additions and 526 deletions

View file

@ -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, &current, true);
build_batch(batch, self.index, &current, 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,
});
}
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,
));
}
}
for index_property in index {
if index_property.property != property {
continue;
}
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();
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();
}
}
// 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(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);
}
}
}
}
}
// 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);
// 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());
}
}
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 {
batch.ops.push(Operation::Index {
field: property.clone().into(),
key: value.serialize(),
set,
});
// 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: 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);
// 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(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);
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: true,
});
}
}
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);
}
}
}
}
}
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,

View file

@ -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!(),
}
}
}

View file

@ -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 {

View file

@ -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(),

View file

@ -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 {

View file

@ -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 {

View file

@ -181,6 +181,9 @@ impl JMAP {
mailbox.append(property.clone(), value);
}
// Add result to response
response.list.push(mailbox);
}
Ok(response)
}

View file

@ -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 => {

View file

@ -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

View file

@ -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]);

View file

@ -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,

View file

@ -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;
}

View file

@ -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!()
}
}

View file

@ -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> {

View file

@ -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) {

View file

@ -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(

View file

@ -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> {

View file

@ -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;
}

View file

@ -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 {