Mailbox implementation before testing

This commit is contained in:
Mauro D
2023-05-02 15:51:26 +00:00
parent 9360de22b7
commit a3a0396772
43 changed files with 3642 additions and 457 deletions

773
tests/src/jmap/mailbox.rs Normal file
View File

@@ -0,0 +1,773 @@
/*
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
*
* This file is part of the Stalwart JMAP Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::sync::Arc;
use jmap::JMAP;
use jmap_client::{
client::Client,
core::{
query::Filter,
set::{SetError, SetErrorType, SetObject, SetRequest},
},
mailbox::{self, Mailbox, Role},
Error, Set,
};
use jmap_proto::types::{id::Id, state::State};
use serde::{Deserialize, Serialize};
use store::ahash::AHashMap;
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
println!("Running Mailbox tests...");
// Create test mailboxes
let id_map = create_test_mailboxes(client).await;
// Sort by name
assert_eq!(
client
.mailbox_query(
None::<mailbox::query::Filter>,
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
[
"drafts",
"spam2",
"inbox",
"1",
"2",
"3",
"sent",
"spam",
"1.1",
"1.2",
"trash",
"spam1",
"1.1.1.1",
"1.1.1.1.1",
"1.1.1",
"1.2.1"
]
);
// Sort by name as tree
let mut request = client.build();
request
.query_mailbox()
.sort([mailbox::query::Comparator::name()])
.arguments()
.sort_as_tree(true);
assert_eq!(
request
.send_query_mailbox()
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
[
"drafts",
"inbox",
"1",
"1.1",
"1.1.1",
"1.1.1.1",
"1.1.1.1.1",
"1.2",
"1.2.1",
"2",
"3",
"sent",
"spam",
"spam1",
"spam2",
"trash"
]
);
// Sort as tree with filters
let mut request = client.build();
request
.query_mailbox()
.filter(mailbox::query::Filter::name("level"))
.sort([mailbox::query::Comparator::name()])
.arguments()
.sort_as_tree(true);
assert_eq!(
request
.send_query_mailbox()
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
[
"1",
"1.1",
"1.1.1",
"1.1.1.1",
"1.1.1.1.1",
"1.2",
"1.2.1",
"2",
"3"
]
);
// Filter as tree
let mut request = client.build();
request
.query_mailbox()
.filter(mailbox::query::Filter::name("spam"))
.sort([mailbox::query::Comparator::name()])
.arguments()
.filter_as_tree(true)
.sort_as_tree(true);
assert_eq!(
request
.send_query_mailbox()
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
["spam", "spam1", "spam2"]
);
let mut request = client.build();
request
.query_mailbox()
.filter(mailbox::query::Filter::name("level"))
.sort([mailbox::query::Comparator::name()])
.arguments()
.filter_as_tree(true)
.sort_as_tree(true);
assert_eq!(
request.send_query_mailbox().await.unwrap().ids(),
Vec::<&str>::new()
);
// Filter by role
assert_eq!(
client
.mailbox_query(
mailbox::query::Filter::role(Role::Inbox).into(),
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
["inbox"]
);
assert_eq!(
client
.mailbox_query(
mailbox::query::Filter::has_any_role(true).into(),
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
["drafts", "inbox", "sent", "spam", "trash"]
);
// Duplicate role
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["sent"])
.role(Role::Inbox);
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["sent"]),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Duplicate name
let mut request = client.build();
request.set_mailbox().update(&id_map["2"]).name("Level 3");
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["2"]),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Circular relationship
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["1"])
.parent_id((&id_map["1.1.1.1.1"]).into());
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["1"]),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["1"])
.parent_id((&id_map["1"]).into());
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["1"]),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Invalid parentId
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["1"])
.parent_id(Id::new(u64::MAX).to_string().into());
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["1"]),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Obtain state
let state = client
.mailbox_changes(State::Initial.to_string(), 0)
.await
.unwrap()
.new_state()
.to_string();
// Rename and move mailbox
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["1.1.1.1.1"])
.name("Renamed and moved")
.parent_id((&id_map["2"]).into());
assert!(request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["1.1.1.1.1"])
.is_ok());
// Verify changes
let state = client.mailbox_changes(state, 0).await.unwrap();
assert_eq!(state.created().len(), 0);
assert_eq!(state.updated().len(), 1);
assert_eq!(state.destroyed().len(), 0);
assert_eq!(state.arguments().updated_properties(), None);
let state = state.new_state().to_string();
// Insert email into Inbox
let mail_id = client
.email_import(
b"From: test@test.com\nSubject: hey\n\ntest".to_vec(),
[&id_map["inbox"]],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
// Inbox's total and unread count should have increased
let inbox = client
.mailbox_get(
&id_map["inbox"],
[
mailbox::Property::TotalEmails,
mailbox::Property::UnreadEmails,
mailbox::Property::TotalThreads,
mailbox::Property::UnreadThreads,
]
.into(),
)
.await
.unwrap()
.unwrap();
assert_eq!(inbox.total_emails(), 1);
assert_eq!(inbox.unread_emails(), 1);
assert_eq!(inbox.total_threads(), 1);
assert_eq!(inbox.unread_threads(), 1);
// Set email to read and fetch properties again
client
.email_set_keyword(&mail_id, "$seen", true)
.await
.unwrap();
let inbox = client
.mailbox_get(
&id_map["inbox"],
[
mailbox::Property::TotalEmails,
mailbox::Property::UnreadEmails,
mailbox::Property::TotalThreads,
mailbox::Property::UnreadThreads,
]
.into(),
)
.await
.unwrap()
.unwrap();
assert_eq!(inbox.total_emails(), 1);
assert_eq!(inbox.unread_emails(), 0);
assert_eq!(inbox.total_threads(), 1);
assert_eq!(inbox.unread_threads(), 0);
// Only email properties must have changed
let prev_state = state.clone();
let state = client.mailbox_changes(state, 0).await.unwrap();
assert_eq!(state.created().len(), 0);
assert_eq!(
state
.updated()
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>(),
&[&id_map["inbox"]]
);
assert_eq!(state.destroyed().len(), 0);
assert_eq!(
state.arguments().updated_properties(),
Some(
&[
mailbox::Property::TotalEmails,
mailbox::Property::UnreadEmails,
mailbox::Property::TotalThreads,
mailbox::Property::UnreadThreads,
][..]
)
);
let state = state.new_state().to_string();
// Use updatedProperties in a query
let mut request = client.build();
let changes_request = request.changes_mailbox(prev_state).max_changes(0);
let properties_ref = changes_request.updated_properties_reference();
let updated_ref = changes_request.updated_reference();
request
.get_mailbox()
.ids_ref(updated_ref)
.properties_ref(properties_ref);
let mut changed_mailboxes = request
.send()
.await
.unwrap()
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_get_mailbox()
.unwrap()
.take_list();
assert_eq!(changed_mailboxes.len(), 1);
let inbox = changed_mailboxes.pop().unwrap();
assert_eq!(inbox.id().unwrap(), &id_map["inbox"]);
assert_eq!(inbox.total_emails(), 1);
assert_eq!(inbox.unread_emails(), 0);
assert_eq!(inbox.total_threads(), 1);
assert_eq!(inbox.unread_threads(), 0);
assert_eq!(inbox.name(), None);
assert_eq!(inbox.my_rights(), None);
// Move email from Inbox to Trash
client
.email_set_mailboxes(&mail_id, [&id_map["trash"]])
.await
.unwrap();
// E-mail properties of both Inbox and Trash must have changed
let state = client.mailbox_changes(state, 0).await.unwrap();
assert_eq!(state.created().len(), 0);
assert_eq!(state.updated().len(), 2);
assert_eq!(state.destroyed().len(), 0);
let mut folder_ids = vec![&id_map["trash"], &id_map["inbox"]];
let mut updated_ids = state
.updated()
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>();
updated_ids.sort_unstable();
folder_ids.sort_unstable();
assert_eq!(updated_ids, folder_ids);
assert_eq!(
state.arguments().updated_properties(),
Some(
&[
mailbox::Property::TotalEmails,
mailbox::Property::UnreadEmails,
mailbox::Property::TotalThreads,
mailbox::Property::UnreadThreads,
][..]
)
);
// Deleting folders with children is not allowed
let mut request = client.build();
request.set_mailbox().destroy([&id_map["1"]]);
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.destroyed(&id_map["1"]),
Err(Error::Set(SetError {
type_: SetErrorType::MailboxHasChild,
..
}))
));
// Deleting folders with contents is not allowed (unless remove_emails is true)
let mut request = client.build();
request.set_mailbox().destroy([&id_map["trash"]]);
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.destroyed(&id_map["trash"]),
Err(Error::Set(SetError {
type_: SetErrorType::MailboxHasEmail,
..
}))
));
// Delete Trash folder and its contents
let mut request = client.build();
request
.set_mailbox()
.destroy([&id_map["trash"]])
.arguments()
.on_destroy_remove_emails(true);
assert!(request
.send_set_mailbox()
.await
.unwrap()
.destroyed(&id_map["trash"])
.is_ok());
// Verify that Trash folder and its contents are gone
assert!(client
.mailbox_get(&id_map["trash"], None::<Vec<_>>)
.await
.unwrap()
.is_none());
assert!(client
.email_get(&mail_id, None::<Vec<_>>)
.await
.unwrap()
.is_none());
// Check search results after changing folder properties
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["drafts"])
.name("Borradores")
.sort_order(100)
.parent_id((&id_map["2"]).into())
.role(Role::None);
assert!(request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["drafts"])
.is_ok());
assert_eq!(
client
.mailbox_query(
Filter::and([
mailbox::query::Filter::name("Borradores").into(),
mailbox::query::Filter::parent_id((&id_map["2"]).into()).into(),
Filter::not([mailbox::query::Filter::has_any_role(true)])
])
.into(),
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
["drafts"]
);
assert!(client
.mailbox_query(
mailbox::query::Filter::name("Drafts").into(),
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.is_empty());
assert!(client
.mailbox_query(
mailbox::query::Filter::role(Role::Drafts).into(),
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.is_empty());
assert_eq!(
client
.mailbox_query(
mailbox::query::Filter::parent_id(None::<&str>).into(),
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
["inbox", "sent", "spam"]
);
assert_eq!(
client
.mailbox_query(
mailbox::query::Filter::has_any_role(true).into(),
[mailbox::query::Comparator::name()].into()
)
.await
.unwrap()
.ids()
.iter()
.map(|id| id_map.get(id).unwrap())
.collect::<Vec<_>>(),
["inbox", "sent", "spam"]
);
let mut request = client.build();
request.query_mailbox().arguments().sort_as_tree(true);
let mut ids = request.send_query_mailbox().await.unwrap().take_ids();
ids.reverse();
for id in ids {
client.mailbox_destroy(&id, true).await.unwrap();
}
let todo = "yes";
//server.store.assert_is_empty();
}
async fn create_test_mailboxes(client: &mut Client) -> AHashMap<String, String> {
let mut mailbox_map = AHashMap::default();
let mut request = client.build();
build_create_query(
request.set_mailbox(),
&mut mailbox_map,
serde_json::from_slice(TEST_MAILBOXES).unwrap(),
None,
);
let mut result = request.send_set_mailbox().await.unwrap();
let mut id_map = AHashMap::with_capacity(mailbox_map.len());
for (create_id, local_id) in mailbox_map {
let server_id = result.created(&create_id).unwrap().take_id();
id_map.insert(local_id.clone(), server_id.clone());
id_map.insert(server_id, local_id);
}
id_map
}
fn build_create_query(
request: &mut SetRequest<Mailbox<Set>>,
mailbox_map: &mut AHashMap<String, String>,
mailboxes: Vec<TestMailbox>,
parent_id: Option<String>,
) {
for mailbox in mailboxes {
let create_mailbox = request
.create()
.name(mailbox.name)
.sort_order(mailbox.order);
if let Some(role) = mailbox.role {
create_mailbox.role(role);
}
if let Some(parent_id) = &parent_id {
create_mailbox.parent_id_ref(parent_id);
}
let create_mailbox_id = create_mailbox.create_id().unwrap();
mailbox_map.insert(create_mailbox_id.clone(), mailbox.id);
if let Some(children) = mailbox.children {
build_create_query(request, mailbox_map, children, create_mailbox_id.into());
}
}
}
#[derive(Serialize, Deserialize)]
struct TestMailbox {
id: String,
name: String,
role: Option<Role>,
order: u32,
children: Option<Vec<TestMailbox>>,
}
const TEST_MAILBOXES: &[u8] = br#"
[
{
"id": "inbox",
"name": "Inbox",
"role": "INBOX",
"order": 5,
"children": [
{
"name": "Level 1",
"id": "1",
"order": 4,
"children": [
{
"name": "Sub-Level 1.1",
"id": "1.1",
"order": 3,
"children": [
{
"name": "Z-Sub-Level 1.1.1",
"id": "1.1.1",
"order": 2,
"children": [
{
"name": "X-Sub-Level 1.1.1.1",
"id": "1.1.1.1",
"order": 1,
"children": [
{
"name": "Y-Sub-Level 1.1.1.1.1",
"id": "1.1.1.1.1",
"order": 0
}
]
}
]
}
]
},
{
"name": "Sub-Level 1.2",
"id": "1.2",
"order": 7,
"children": [
{
"name": "Z-Sub-Level 1.2.1",
"id": "1.2.1",
"order": 6
}
]
}
]
},
{
"name": "Level 2",
"id": "2",
"order": 8
},
{
"name": "Level 3",
"id": "3",
"order": 9
}
]
},
{
"id": "sent",
"name": "Sent",
"role": "SENT",
"order": 15
},
{
"id": "drafts",
"name": "Drafts",
"role": "DRAFTS",
"order": 14
},
{
"id": "trash",
"name": "Trash",
"role": "TRASH",
"order": 13
},
{
"id": "spam",
"name": "Spam",
"role": "JUNK",
"order": 12,
"children": [{
"id": "spam1",
"name": "Work Spam",
"order": 11,
"children": [{
"id": "spam2",
"name": "Friendly Spam",
"order": 10
}]
}]
}
]
"#;

View File

@@ -10,6 +10,7 @@ use crate::{add_test_certs, store::TempDir};
pub mod email_get;
pub mod email_query;
pub mod email_set;
pub mod mailbox;
pub mod thread_get;
pub mod thread_merge;
@@ -51,10 +52,11 @@ pub async fn jmap_tests() {
let delete = true;
let mut params = init_jmap_tests(delete).await;
//email_get::test(params.server.clone(), &mut params.client).await;
email_set::test(params.server.clone(), &mut params.client).await;
//email_set::test(params.server.clone(), &mut params.client).await;
//email_query::test(params.server.clone(), &mut params.client, delete).await;
//thread_get::test(params.server.clone(), &mut params.client).await;
//thread_merge::test(params.server.clone(), &mut params.client).await;
mailbox::test(params.server.clone(), &mut params.client).await;
if delete {
params.temp_dir.delete();
}

View File

@@ -327,7 +327,7 @@ pub async fn test_filter(db: Arc<Store>) {
.sort(
docset,
vec![Comparator::ascending(fields["accession_number"])],
Pagination::new(0, 0, None, 0, None, false),
Pagination::new(0, 0, None, 0),
)
.await
.unwrap();
@@ -423,7 +423,7 @@ pub async fn test_sort(db: Arc<Store>) {
.sort(
docset,
sort,
Pagination::new(expected_results.len(), 0, None, 0, None, false),
Pagination::new(expected_results.len(), 0, None, 0),
)
.await
.unwrap();