Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+755
View File
@@ -0,0 +1,755 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use ::email::mailbox::{INBOX_ID, TRASH_ID};
use jmap_client::{
core::{
error::{MethodError, MethodErrorType},
set::{SetError, SetErrorType},
},
email::{self, Property, import::EmailImportResponse, query::Filter},
mailbox::{self, Role},
principal::ACL,
};
use registry::schema::prelude::ObjectType;
use serde_json::json;
use std::fmt::Debug;
use store::ahash::AHashMap;
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running ACL tests...");
// Create a group and three test accounts
let inbox_id = Id::new(INBOX_ID as u64).to_string();
let trash_id = Id::new(TRASH_ID as u64).to_string();
let admin = test.account("[email protected]");
let john = test.account("[email protected]");
let jane = test.account("[email protected]");
let bill = test.account("[email protected]");
let sales = test.account("[email protected]");
// Authenticate all accounts
let mut john_client = john.jmap_client().await;
let mut jane_client = jane.jmap_client().await;
let mut bill_client = bill.jmap_client().await;
// Insert two emails in each account
let mut email_ids = AHashMap::default();
for (client, account_id, name) in [
(&mut john_client, john.id(), "john"),
(&mut jane_client, jane.id(), "jane"),
(&mut bill_client, bill.id(), "bill"),
(&mut admin.jmap_client().await, sales.id(), "sales"),
] {
let user_name = client.session().username().to_string();
let mut ids = Vec::with_capacity(2);
for (mailbox_id, mailbox_name) in [(&inbox_id, "inbox"), (&trash_id, "trash")] {
ids.push(
client
.set_default_account_id(account_id.to_string())
.email_import(
format!(
concat!(
"From: [email protected]\r\n",
"To: {}\r\n",
"Subject: Owned by {} in {}\r\n",
"\r\n",
"This message is owned by {}.",
),
user_name, name, mailbox_name, name
)
.into_bytes(),
[mailbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id(),
);
}
email_ids.insert(name, ids);
}
// John should have access to his emails only
assert_eq!(
john_client
.email_get(
email_ids.get("john").unwrap().first().unwrap(),
[Property::Subject].into(),
)
.await
.unwrap()
.unwrap()
.subject()
.unwrap(),
"Owned by john in inbox"
);
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.email_get(
email_ids.get("jane").unwrap().first().unwrap(),
[Property::Subject].into(),
)
.await,
);
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.mailbox_get(&inbox_id, None::<Vec<_>>)
.await,
);
assert_forbidden(
john_client
.set_default_account_id(sales.id_string())
.email_get(
email_ids.get("sales").unwrap().first().unwrap(),
[Property::Subject].into(),
)
.await,
);
assert_forbidden(
john_client
.set_default_account_id(sales.id_string())
.mailbox_get(&inbox_id, None::<Vec<_>>)
.await,
);
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.email_query(None::<Filter>, None::<Vec<_>>)
.await,
);
// Jane grants Inbox ReadItems access to John
jane_client
.mailbox_update_acl(&inbox_id, john.id_string(), [ACL::ReadItems])
.await
.unwrap();
// John should have ReadItems access to Inbox
assert_eq!(
john_client
.set_default_account_id(jane.id_string())
.email_get(
email_ids.get("jane").unwrap().first().unwrap(),
[Property::Subject].into(),
)
.await
.unwrap()
.unwrap()
.subject()
.unwrap(),
"Owned by jane in inbox"
);
assert_eq!(
john_client
.set_default_account_id(jane.id_string())
.email_query(None::<Filter>, None::<Vec<_>>)
.await
.unwrap()
.ids(),
[email_ids.get("jane").unwrap().first().unwrap().as_str()]
);
// John's session resource should contain Jane's account details
john_client.refresh_session().await.unwrap();
assert_eq!(
john_client
.session()
.account(jane.id_string())
.unwrap()
.name(),
"[email protected]"
);
// John should not have access to emails in Jane's Trash folder
assert!(
john_client
.set_default_account_id(jane.id_string())
.email_get(
email_ids.get("jane").unwrap().last().unwrap(),
[Property::Subject].into(),
)
.await
.unwrap()
.is_none()
);
// Email/changes must not leak ids of emails in folders John cannot read
let jane_inbox_email = email_ids.get("jane").unwrap().first().unwrap().clone();
let jane_trash_email = email_ids.get("jane").unwrap().last().unwrap().clone();
let changed_ids = john_client
.set_default_account_id(jane.id_string())
.email_changes("n", None)
.await
.unwrap()
.created()
.to_vec();
assert!(
changed_ids.contains(&jane_inbox_email),
"Email/changes should report the shared Inbox email"
);
assert!(
!changed_ids.contains(&jane_trash_email),
"Email/changes leaked the id of a non-shared Trash email"
);
// John should only be able to copy blobs he has access to
let blob_id = jane_client
.email_get(
email_ids.get("jane").unwrap().first().unwrap(),
[Property::BlobId].into(),
)
.await
.unwrap()
.unwrap()
.take_blob_id();
john_client
.set_default_account_id(john.id_string())
.blob_copy(jane.id_string(), &blob_id)
.await
.unwrap();
let blob_id = jane_client
.email_get(
email_ids.get("jane").unwrap().last().unwrap(),
[Property::BlobId].into(),
)
.await
.unwrap()
.unwrap()
.take_blob_id();
assert_forbidden(
john_client
.set_default_account_id(john.id_string())
.blob_copy(jane.id_string(), &blob_id)
.await,
);
// John only has ReadItems access to Inbox
jane_client
.mailbox_update_acl(&inbox_id, john.id_string(), [ACL::ReadItems])
.await
.unwrap();
assert_eq!(
john_client
.set_default_account_id(jane.id_string())
.mailbox_get(&inbox_id, [mailbox::Property::MyRights].into())
.await
.unwrap()
.unwrap()
.my_rights()
.unwrap()
.acl_list(),
vec![ACL::ReadItems]
);
// Try to add items using import and copy
let blob_id = john_client
.set_default_account_id(john.id_string())
.upload(
Some(john.id_string()),
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: Created by john in jane's inbox\r\n",
"\r\n",
"This message is owned by jane.",
)
.as_bytes()
.to_vec(),
None,
)
.await
.unwrap()
.take_blob_id();
let mut request = john_client.set_default_account_id(jane.id_string()).build();
let email_id = request
.import_email()
.email(&blob_id)
.mailbox_ids([&inbox_id])
.create_id();
assert_forbidden(
request
.send_single::<EmailImportResponse>()
.await
.unwrap()
.created(&email_id),
);
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.email_copy(
john.id_string(),
email_ids.get("john").unwrap().last().unwrap(),
[&inbox_id],
None::<Vec<&str>>,
None,
)
.await,
);
// Grant access and try again
jane_client
.mailbox_update_acl(&inbox_id, john.id_string(), [ACL::ReadItems, ACL::AddItems])
.await
.unwrap();
let mut request = john_client.set_default_account_id(jane.id_string()).build();
let email_id = request
.import_email()
.email(&blob_id)
.mailbox_ids([&inbox_id])
.create_id();
let email_id = request
.send_single::<EmailImportResponse>()
.await
.unwrap()
.created(&email_id)
.unwrap()
.take_id();
let email_id_2 = john_client
.set_default_account_id(jane.id_string())
.email_copy(
john.id_string(),
email_ids.get("john").unwrap().last().unwrap(),
[&inbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
assert_eq!(
jane_client
.email_get(&email_id, [Property::Subject].into(),)
.await
.unwrap()
.unwrap()
.subject()
.unwrap(),
"Created by john in jane's inbox"
);
assert_eq!(
jane_client
.email_get(&email_id_2, [Property::Subject].into(),)
.await
.unwrap()
.unwrap()
.subject()
.unwrap(),
"Owned by john in trash"
);
// Try removing items
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.email_destroy(&email_id)
.await,
);
jane_client
.mailbox_update_acl(
&inbox_id,
john.id_string(),
[ACL::ReadItems, ACL::AddItems, ACL::RemoveItems],
)
.await
.unwrap();
john_client
.set_default_account_id(jane.id_string())
.email_destroy(&email_id)
.await
.unwrap();
// Try to set keywords
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.email_set_keyword(&email_id_2, "$seen", true)
.await,
);
jane_client
.mailbox_update_acl(
&inbox_id,
john.id_string(),
[
ACL::ReadItems,
ACL::AddItems,
ACL::RemoveItems,
ACL::SetKeywords,
],
)
.await
.unwrap();
john_client
.set_default_account_id(jane.id_string())
.email_set_keyword(&email_id_2, "$seen", true)
.await
.unwrap();
john_client
.set_default_account_id(jane.id_string())
.email_set_keyword(&email_id_2, "my-keyword", true)
.await
.unwrap();
// Try to create a child
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.mailbox_create("John's mailbox", None::<&str>, Role::None)
.await,
);
jane_client
.mailbox_update_acl(
&inbox_id,
john.id_string(),
[
ACL::ReadItems,
ACL::AddItems,
ACL::RemoveItems,
ACL::SetKeywords,
ACL::CreateChild,
],
)
.await
.unwrap();
let mailbox_id = john_client
.set_default_account_id(jane.id_string())
.mailbox_create("John's mailbox", Some(&inbox_id), Role::None)
.await
.unwrap()
.take_id();
// Try renaming a mailbox
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.mailbox_rename(&mailbox_id, "John's private mailbox")
.await,
);
jane_client
.mailbox_update_acl(&mailbox_id, john.id_string(), [ACL::ReadItems, ACL::Rename])
.await
.unwrap();
john_client
.set_default_account_id(jane.id_string())
.mailbox_rename(&mailbox_id, "John's private mailbox")
.await
.unwrap();
// Try moving a message
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.email_set_mailbox(&email_id_2, &mailbox_id, true)
.await,
);
jane_client
.mailbox_update_acl(
&mailbox_id,
john.id_string(),
[ACL::ReadItems, ACL::Rename, ACL::AddItems],
)
.await
.unwrap();
john_client
.set_default_account_id(jane.id_string())
.email_set_mailbox(&email_id_2, &mailbox_id, true)
.await
.unwrap();
// Try deleting a mailbox
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.mailbox_destroy(&mailbox_id, true)
.await,
);
jane_client
.mailbox_update_acl(
&mailbox_id,
john.id_string(),
[ACL::ReadItems, ACL::Rename, ACL::AddItems, ACL::Delete],
)
.await
.unwrap();
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.mailbox_destroy(&mailbox_id, true)
.await,
);
jane_client
.mailbox_update_acl(
&mailbox_id,
john.id_string(),
[
ACL::ReadItems,
ACL::Rename,
ACL::AddItems,
ACL::Delete,
ACL::RemoveItems,
],
)
.await
.unwrap();
john_client
.set_default_account_id(jane.id_string())
.mailbox_destroy(&mailbox_id, true)
.await
.unwrap();
// Try changing ACL
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.mailbox_update_acl(&inbox_id, bill.id_string(), [ACL::ReadItems])
.await,
);
assert_forbidden(
bill_client
.set_default_account_id(jane.id_string())
.email_query(None::<Filter>, None::<Vec<_>>)
.await,
);
jane_client
.mailbox_update_acl(
&inbox_id,
john.id_string(),
[
ACL::ReadItems,
ACL::AddItems,
ACL::RemoveItems,
ACL::SetKeywords,
ACL::CreateChild,
ACL::Rename,
ACL::Administer,
],
)
.await
.unwrap();
assert_eq!(
john_client
.set_default_account_id(jane.id_string())
.mailbox_get(&inbox_id, [mailbox::Property::MyRights].into())
.await
.unwrap()
.unwrap()
.my_rights()
.unwrap()
.acl_list(),
vec![
ACL::ReadItems,
ACL::AddItems,
ACL::RemoveItems,
ACL::SetSeen,
ACL::SetKeywords,
ACL::CreateChild,
ACL::Rename
]
);
john_client
.set_default_account_id(jane.id_string())
.mailbox_update_acl(&inbox_id, bill.id_string(), [ACL::ReadItems])
.await
.unwrap();
assert_eq!(
bill_client
.set_default_account_id(jane.id_string())
.email_query(
None::<Filter>,
vec![email::query::Comparator::subject()].into()
)
.await
.unwrap()
.ids(),
[
email_ids.get("jane").unwrap().first().unwrap().as_str(),
&email_id_2
]
);
// Revoke all access to John
jane_client
.mailbox_update_acl(&inbox_id, john.id_string(), [])
.await
.unwrap();
assert_forbidden(
john_client
.set_default_account_id(jane.id_string())
.email_get(
email_ids.get("jane").unwrap().first().unwrap(),
[Property::Subject].into(),
)
.await,
);
john_client.refresh_session().await.unwrap();
assert!(john_client.session().account(jane.id_string()).is_none());
assert_eq!(
bill_client
.set_default_account_id(jane.id_string())
.email_get(
email_ids.get("jane").unwrap().first().unwrap(),
[Property::Subject].into(),
)
.await
.unwrap()
.unwrap()
.subject()
.unwrap(),
"Owned by jane in inbox"
);
// Add John and Jane to the Sales group
let sales_id = test.account("[email protected]").id();
for name in ["[email protected]", "[email protected]"] {
admin
.registry_update_object(
ObjectType::Account,
test.account(name).id(),
json!({
"memberGroupIds": { sales_id: true },
}),
)
.await;
}
john_client.refresh_session().await.unwrap();
jane_client.refresh_session().await.unwrap();
bill_client.refresh_session().await.unwrap();
assert_eq!(
john_client
.session()
.account(sales.id_string())
.unwrap()
.name(),
"[email protected]"
);
assert!(
!john_client
.session()
.account(sales.id_string())
.unwrap()
.is_personal()
);
assert_eq!(
jane_client
.session()
.account(sales.id_string())
.unwrap()
.name(),
"[email protected]"
);
assert!(bill_client.session().account(sales.id_string()).is_none());
// Insert a message in Sales's inbox
let blob_id = john_client
.set_default_account_id(sales.id_string())
.upload(
Some(sales.id_string()),
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: Created by john in sales\r\n",
"\r\n",
"This message is owned by sales.",
)
.as_bytes()
.to_vec(),
None,
)
.await
.unwrap()
.take_blob_id();
let mut request = john_client.build();
let email_id = request
.import_email()
.email(&blob_id)
.mailbox_ids([&inbox_id])
.create_id();
let email_id = request
.send_single::<EmailImportResponse>()
.await
.unwrap()
.created(&email_id)
.unwrap()
.take_id();
// Both Jane and John should be able to see this message, but not Bill
assert_eq!(
john_client
.set_default_account_id(sales.id_string())
.email_get(&email_id, [Property::Subject].into(),)
.await
.unwrap()
.unwrap()
.subject()
.unwrap(),
"Created by john in sales"
);
assert_eq!(
jane_client
.set_default_account_id(sales.id_string())
.email_get(&email_id, [Property::Subject].into(),)
.await
.unwrap()
.unwrap()
.subject()
.unwrap(),
"Created by john in sales"
);
assert_forbidden(
bill_client
.set_default_account_id(sales.id_string())
.email_get(&email_id, [Property::Subject].into())
.await,
);
// Remove John from the sales group
admin
.registry_update_object(
ObjectType::Account,
test.account("[email protected]").id(),
json!({
"memberGroupIds": { sales_id: false },
}),
)
.await;
assert_forbidden(
john_client
.set_default_account_id(sales.id_string())
.email_get(&email_id, [Property::Subject].into())
.await,
);
// Destroy test account data
for account in [john, bill, jane, sales] {
admin
.destroy_all_mailboxes_for_account(account.id().document_id())
.await;
}
test.assert_is_empty().await;
}
pub fn assert_forbidden<T: Debug>(result: Result<T, jmap_client::Error>) {
if !matches!(
result,
Err(jmap_client::Error::Method(MethodError {
p_type: MethodErrorType::Forbidden
})) | Err(jmap_client::Error::Set(SetError {
type_: SetErrorType::BlobNotFound | SetErrorType::Forbidden,
..
}))
) {
panic!("Expected forbidden, got {:?}", result);
}
}
+336
View File
@@ -0,0 +1,336 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use jmap_proto::types::state::State;
use std::str::FromStr;
use store::{ahash::AHashSet, write::BatchBuilder};
use types::{
collection::{Collection, SyncCollection},
id::Id,
};
pub async fn test(test: &TestServer) {
println!("Running Email Changes tests...");
let server = test.server.clone();
let account = test.account("[email protected]");
let client = account.jmap_client().await;
let mut states = vec![State::Initial];
for (changes, expected_changelog) in [
(
vec![
LogAction::Insert(0),
LogAction::Insert(1),
LogAction::Insert(2),
],
vec![vec![vec![0, 1, 2], vec![], vec![]]],
),
(
vec![
LogAction::Move(0, 3),
LogAction::Insert(4),
LogAction::Insert(5),
LogAction::Update(1),
LogAction::Update(2),
],
vec![
vec![vec![1, 2, 3, 4, 5], vec![], vec![]],
vec![vec![3, 4, 5], vec![1, 2], vec![0]],
],
),
(
vec![
LogAction::Delete(1),
LogAction::Insert(6),
LogAction::Insert(7),
LogAction::Update(2),
],
vec![
vec![vec![2, 3, 4, 5, 6, 7], vec![], vec![]],
vec![vec![3, 4, 5, 6, 7], vec![2], vec![0, 1]],
vec![vec![6, 7], vec![2], vec![1]],
],
),
(
vec![
LogAction::Update(4),
LogAction::Update(5),
LogAction::Update(6),
LogAction::Update(7),
],
vec![
vec![vec![2, 3, 4, 5, 6, 7], vec![], vec![]],
vec![vec![3, 4, 5, 6, 7], vec![2], vec![0, 1]],
vec![vec![6, 7], vec![2, 4, 5], vec![1]],
vec![vec![], vec![4, 5, 6, 7], vec![]],
],
),
(
vec![
LogAction::Delete(4),
LogAction::Delete(5),
LogAction::Delete(6),
LogAction::Delete(7),
],
vec![
vec![vec![2, 3], vec![], vec![]],
vec![vec![3], vec![2], vec![0, 1]],
vec![vec![], vec![2], vec![1, 4, 5]],
vec![vec![], vec![], vec![4, 5, 6, 7]],
vec![vec![], vec![], vec![4, 5, 6, 7]],
],
),
(
vec![
LogAction::Insert(8),
LogAction::Insert(9),
LogAction::Insert(10),
LogAction::Update(3),
],
vec![
vec![vec![2, 3, 8, 9, 10], vec![], vec![]],
vec![vec![3, 8, 9, 10], vec![2], vec![0, 1]],
vec![vec![8, 9, 10], vec![2, 3], vec![1, 4, 5]],
vec![vec![8, 9, 10], vec![3], vec![4, 5, 6, 7]],
vec![vec![8, 9, 10], vec![3], vec![4, 5, 6, 7]],
vec![vec![8, 9, 10], vec![3], vec![]],
],
),
(
vec![LogAction::Update(2), LogAction::Update(8)],
vec![
vec![vec![2, 3, 8, 9, 10], vec![], vec![]],
vec![vec![3, 8, 9, 10], vec![2], vec![0, 1]],
vec![vec![8, 9, 10], vec![2, 3], vec![1, 4, 5]],
vec![vec![8, 9, 10], vec![2, 3], vec![4, 5, 6, 7]],
vec![vec![8, 9, 10], vec![2, 3], vec![4, 5, 6, 7]],
vec![vec![8, 9, 10], vec![2, 3], vec![]],
vec![vec![], vec![2, 8], vec![]],
],
),
(
vec![
LogAction::Move(9, 11),
LogAction::Move(10, 12),
LogAction::Delete(8),
],
vec![
vec![vec![2, 3, 11, 12], vec![], vec![]],
vec![vec![3, 11, 12], vec![2], vec![0, 1]],
vec![vec![11, 12], vec![2, 3], vec![1, 4, 5]],
vec![vec![11, 12], vec![2, 3], vec![4, 5, 6, 7]],
vec![vec![11, 12], vec![2, 3], vec![4, 5, 6, 7]],
vec![vec![11, 12], vec![2, 3], vec![]],
vec![vec![11, 12], vec![2], vec![8, 9, 10]],
vec![vec![11, 12], vec![], vec![8, 9, 10]],
],
),
]
.into_iter()
{
let mut batch = BatchBuilder::new();
batch
.with_account_id(account.id().document_id())
.with_collection(Collection::Email);
for change in changes {
match change {
LogAction::Insert(id) => {
batch
.with_document(id as u32)
.log_item_insert(SyncCollection::Email, None);
}
LogAction::Update(id) => {
batch
.with_document(id as u32)
.log_item_update(SyncCollection::Email, None);
}
LogAction::Delete(id) => {
batch
.with_document(id as u32)
.log_item_delete(SyncCollection::Email, None);
}
LogAction::UpdateChild(id) => {
batch.log_container_property_change(SyncCollection::Email, id as u32);
}
LogAction::Move(old_id, new_id) => {
batch
.with_document(old_id as u32)
.log_item_delete(SyncCollection::Email, None)
.with_document(new_id as u32)
.log_item_insert(SyncCollection::Email, None);
}
}
}
server
.core
.storage
.data
.write(batch.build_all())
.await
.unwrap();
let mut new_state = State::Initial;
for (test_num, state) in (states).iter().enumerate() {
let changes = client.email_changes(state.to_string(), None).await.unwrap();
assert_eq!(
expected_changelog[test_num],
[changes.created(), changes.updated(), changes.destroyed()]
.into_iter()
.map(|list| {
let mut list = list
.iter()
.map(|i| Id::from_str(i).unwrap().into())
.collect::<Vec<u64>>();
list.sort_unstable();
list
})
.collect::<Vec<Vec<_>>>(),
"test_num: {}, state: {:?}",
test_num,
state
);
if &State::Initial == state {
new_state = State::parse_str(changes.new_state()).unwrap();
}
for max_changes in 1..=8 {
let mut insertions = expected_changelog[test_num][0]
.iter()
.copied()
.collect::<AHashSet<_>>();
let mut updates = expected_changelog[test_num][1]
.iter()
.copied()
.collect::<AHashSet<_>>();
let mut deletions = expected_changelog[test_num][2]
.iter()
.copied()
.collect::<AHashSet<_>>();
let mut int_state = state.clone();
for _ in 0..100 {
let changes = client
.email_changes(int_state.to_string(), max_changes.into())
.await
.unwrap();
assert!(
changes.created().len()
+ changes.updated().len()
+ changes.destroyed().len()
<= max_changes,
"{} > {}",
changes.created().len()
+ changes.updated().len()
+ changes.destroyed().len(),
max_changes
);
changes.created().iter().for_each(|id| {
assert!(
insertions.remove(&Id::from_str(id).unwrap()),
"{:?} != {}",
insertions,
Id::from_str(id).unwrap()
);
});
changes.updated().iter().for_each(|id| {
assert!(
updates.remove(&Id::from_str(id).unwrap()),
"{:?} != {}",
updates,
Id::from_str(id).unwrap()
);
});
changes.destroyed().iter().for_each(|id| {
assert!(
deletions.remove(&Id::from_str(id).unwrap()),
"{:?} != {}",
deletions,
Id::from_str(id).unwrap()
);
});
int_state = State::parse_str(changes.new_state()).unwrap();
if !changes.has_more_changes() {
break;
}
}
assert_eq!(
insertions.len(),
0,
"test_num: {}, state: {:?}, pending: {:?}",
test_num,
state,
insertions
);
assert_eq!(
updates.len(),
0,
"test_num: {}, state: {:?}, pending: {:?}",
test_num,
state,
updates
);
assert_eq!(
deletions.len(),
0,
"test_num: {}, state: {:?}, pending: {:?}",
test_num,
state,
deletions
);
}
}
states.push(new_state);
}
let changes = client
.email_changes(State::Initial.to_string(), None)
.await
.unwrap();
let mut created = changes
.created()
.iter()
.map(|i| Id::from_str(i).unwrap().into())
.collect::<Vec<u64>>();
created.sort_unstable();
assert_eq!(created, vec![2, 3, 11, 12]);
assert_eq!(changes.updated(), Vec::<String>::new());
assert_eq!(changes.destroyed(), Vec::<String>::new());
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
#[derive(Debug, Clone, Copy)]
pub enum LogAction {
Insert(u64),
Update(u64),
Delete(u64),
UpdateChild(u64),
Move(u64, u64),
}
pub trait ParseState: Sized {
fn parse_str(state: &str) -> Option<Self>;
}
impl ParseState for State {
fn parse_str(state: &str) -> Option<Self> {
State::parse(state)
}
}
+102
View File
@@ -0,0 +1,102 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use jmap_client::mailbox::Role;
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running Email Copy tests...");
let account = test.account("[email protected]");
let mut client = account.jmap_client().await;
// Create a mailbox on account 1
let ac1_mailbox_id = client
.set_default_account_id(Id::new(1).to_string())
.mailbox_create("Copy Test Ac# 1", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Insert a message on account 1
let ac1_email_id = client
.email_import(
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
)
.as_bytes()
.to_vec(),
[&ac1_mailbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
// Create a mailbox on account 2
let ac2_mailbox_id = client
.set_default_account_id(Id::new(2).to_string())
.mailbox_create("Copy Test Ac# 2", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Copy the email and delete it from the first account
let mut request = client.build();
request
.copy_email(Id::new(1).to_string())
.on_success_destroy_original(true)
.create(&ac1_email_id)
.mailbox_id(&ac2_mailbox_id, true)
.keyword("$draft", true)
.received_at(311923920);
let ac2_email_id = request
.send()
.await
.unwrap()
.method_response_by_pos(0)
.unwrap_copy_email()
.unwrap()
.created(&ac1_email_id)
.unwrap()
.take_id();
// Check that the email was copied
let email = client
.email_get(&ac2_email_id, None::<Vec<_>>)
.await
.unwrap()
.unwrap();
assert_eq!(
email.preview().unwrap(),
"I'm going to need those TPS reports ASAP. So, if you could do that, that'd be great."
);
assert_eq!(email.subject().unwrap(), "TPS Report");
assert_eq!(email.mailbox_ids(), &[&ac2_mailbox_id]);
assert_eq!(email.keywords(), &["$draft"]);
assert_eq!(email.received_at().unwrap(), 311923920);
// Check that the email was deleted
assert!(
client
.set_default_account_id(Id::new(1).to_string())
.email_get(&ac1_email_id, None::<Vec<_>>)
.await
.unwrap()
.is_none()
);
// Empty store
account.destroy_all_mailboxes_for_account(1).await;
account.destroy_all_mailboxes_for_account(2).await;
test.assert_is_empty().await;
}
+338
View File
@@ -0,0 +1,338 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{jmap::replace_blob_ids, utils::server::TestServer};
use ::email::mailbox::INBOX_ID;
use jmap_client::email::{self, Header, HeaderForm, import::EmailImportResponse};
use mail_parser::HeaderName;
use registry::schema::prelude::ObjectType;
use std::{fs, path::PathBuf};
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running Email Get tests...");
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("resources");
test_dir.push("jmap");
test_dir.push("email_get");
let mailbox_id = Id::from(INBOX_ID).to_string();
let account = test.account("[email protected]");
let client = account.jmap_client().await;
for file_name in fs::read_dir(&test_dir).unwrap() {
let mut file_name = file_name.as_ref().unwrap().path();
if file_name.extension().is_none_or(|e| e != "eml") {
continue;
}
let is_headers_test = file_name.file_name().unwrap() == "headers.eml";
let blob = fs::read(&file_name).unwrap();
let blob_len = blob.len();
// Import email
let mut request = client.build();
let import_request = request
.import_email()
.email(
client
.upload(None, blob, None)
.await
.unwrap()
.take_blob_id(),
)
.mailbox_ids([mailbox_id.clone()])
.keywords(["tag".to_string()])
.received_at((blob_len * 1000000) as i64);
let id = import_request.create_id();
let mut response = request.send_single::<EmailImportResponse>().await.unwrap();
assert_ne!(response.old_state(), Some(response.new_state()));
let email = response.created(&id).unwrap();
let mut request = client.build();
request
.get_email()
.ids([email.id().unwrap()])
.properties([
email::Property::Id,
email::Property::BlobId,
email::Property::ThreadId,
email::Property::MailboxIds,
email::Property::Keywords,
email::Property::Size,
email::Property::ReceivedAt,
email::Property::MessageId,
email::Property::InReplyTo,
email::Property::References,
email::Property::Sender,
email::Property::From,
email::Property::To,
email::Property::Cc,
email::Property::Bcc,
email::Property::ReplyTo,
email::Property::Subject,
email::Property::SentAt,
email::Property::HasAttachment,
email::Property::Preview,
email::Property::BodyValues,
email::Property::TextBody,
email::Property::HtmlBody,
email::Property::Attachments,
email::Property::BodyStructure,
])
.arguments()
.body_properties(if !is_headers_test {
[
email::BodyProperty::PartId,
email::BodyProperty::BlobId,
email::BodyProperty::Size,
email::BodyProperty::Name,
email::BodyProperty::Type,
email::BodyProperty::Charset,
email::BodyProperty::Headers,
email::BodyProperty::Disposition,
email::BodyProperty::Cid,
email::BodyProperty::Language,
email::BodyProperty::Location,
]
} else {
[
email::BodyProperty::PartId,
email::BodyProperty::Size,
email::BodyProperty::Name,
email::BodyProperty::Type,
email::BodyProperty::Charset,
email::BodyProperty::Disposition,
email::BodyProperty::Cid,
email::BodyProperty::Language,
email::BodyProperty::Location,
email::BodyProperty::Header(Header {
name: "X-Custom-Header".into(),
form: HeaderForm::Raw,
all: false,
}),
email::BodyProperty::Header(Header {
name: "X-Custom-Header-2".into(),
form: HeaderForm::Raw,
all: false,
}),
]
})
.fetch_all_body_values(true)
.max_body_value_bytes(100);
let mut result = request
.send_get_email()
.await
.unwrap()
.take_list()
.pop()
.unwrap()
.into_test();
if is_headers_test {
for property in all_headers() {
let mut request = client.build();
request
.get_email()
.ids([email.id().unwrap()])
.properties([property]);
result.headers.extend(
request
.send_get_email()
.await
.unwrap()
.take_list()
.pop()
.unwrap()
.into_test()
.headers,
);
}
}
let result = replace_blob_ids(serde_json::to_string_pretty(&result).unwrap());
file_name.set_extension("json");
if fs::read(&file_name).unwrap() != result.as_bytes() {
file_name.set_extension("failed");
fs::write(&file_name, result.as_bytes()).unwrap();
panic!("Test failed, output saved to {}", file_name.display());
}
}
test.destroy_all_mailboxes(account).await;
test.account("[email protected]")
.registry_destroy_all(ObjectType::SpamTrainingSample)
.await;
test.assert_is_empty().await;
}
pub fn all_headers() -> Vec<email::Property> {
let mut properties = Vec::new();
for header in [
HeaderName::From,
HeaderName::To,
HeaderName::Cc,
HeaderName::Bcc,
HeaderName::Other("X-Address-Single".into()),
HeaderName::Other("X-Address".into()),
HeaderName::Other("X-AddressList-Single".into()),
HeaderName::Other("X-AddressList".into()),
HeaderName::Other("X-AddressesGroup-Single".into()),
HeaderName::Other("X-AddressesGroup".into()),
] {
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: false,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Addresses,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Addresses,
name: header.as_str().to_string(),
all: false,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::GroupedAddresses,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::GroupedAddresses,
name: header.as_str().to_string(),
all: false,
}));
}
for header in [
HeaderName::ListPost,
HeaderName::ListSubscribe,
HeaderName::ListUnsubscribe,
HeaderName::ListOwner,
HeaderName::Other("X-List-Single".into()),
HeaderName::Other("X-List".into()),
] {
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: false,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::URLs,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::URLs,
name: header.as_str().to_string(),
all: false,
}));
}
for header in [
HeaderName::Date,
HeaderName::ResentDate,
HeaderName::Other("X-Date-Single".into()),
HeaderName::Other("X-Date".into()),
] {
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: false,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Date,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Date,
name: header.as_str().to_string(),
all: false,
}));
}
for header in [
HeaderName::MessageId,
HeaderName::References,
HeaderName::Other("X-Id-Single".into()),
HeaderName::Other("X-Id".into()),
] {
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: false,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::MessageIds,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::MessageIds,
name: header.as_str().to_string(),
all: false,
}));
}
for header in [
HeaderName::Subject,
HeaderName::Keywords,
HeaderName::Other("X-Text-Single".into()),
HeaderName::Other("X-Text".into()),
] {
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Raw,
name: header.as_str().to_string(),
all: false,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Text,
name: header.as_str().to_string(),
all: true,
}));
properties.push(email::Property::Header(Header {
form: HeaderForm::Text,
name: header.as_str().to_string(),
all: false,
}));
}
properties
}
+768
View File
@@ -0,0 +1,768 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use jmap_client::{
Error, Set,
client::Client,
core::{
query::Filter,
set::{SetError, SetErrorType, SetObject, SetRequest},
},
mailbox::{self, Mailbox, Role},
};
use jmap_proto::types::state::State;
use serde::{Deserialize, Serialize};
use store::ahash::AHashMap;
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running Mailbox tests...");
let account = test.account("[email protected]");
let client = account.jmap_client().await;
// Create test mailboxes
test.destroy_all_mailboxes(account).await;
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",
"l.1",
"l.2",
"l.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",
"l.1",
"1.1",
"1.1.1",
"1.1.1.1",
"1.1.1.1.1",
"1.2",
"1.2.1",
"l.2",
"l.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<_>>(),
[
"l.1",
"1.1",
"1.1.1",
"1.1.1.1",
"1.1.1.1.1",
"1.2",
"1.2.1",
"l.2",
"l.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["l.2"]).name("Level 3");
let result = request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["l.2"]);
assert!(
matches!(
result,
Err(Error::Set(SetError {
type_: SetErrorType::AlreadyExists,
..
}))
),
"{result:?}",
);
// Circular relationship
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["l.1"])
.parent_id((&id_map["1.1.1.1.1"]).into());
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["l.1"]),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["l.1"])
.parent_id((&id_map["l.1"]).into());
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["l.1"]),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Invalid parentId
let mut request = client.build();
request
.set_mailbox()
.update(&id_map["l.1"])
.parent_id(Id::new(u64::MAX).to_string().into());
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.updated(&id_map["l.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["l.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: [email protected]\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["l.1"]]);
assert!(matches!(
request
.send_set_mailbox()
.await
.unwrap()
.destroyed(&id_map["l.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["l.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["l.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"]
);
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
async fn create_test_mailboxes(client: &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": "l.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": "l.2",
"order": 8
},
{
"name": "Level 3",
"id": "l.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
}]
}]
}
]
"#;
+21
View File
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod acl;
pub mod changes;
pub mod copy;
pub mod get;
pub mod mailbox;
pub mod parse;
pub mod query;
pub mod query_changes;
pub mod search_snippet;
pub mod set;
pub mod sieve_script;
pub mod submission;
pub mod thread_get;
pub mod thread_merge;
pub mod vacation_response;
+229
View File
@@ -0,0 +1,229 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
jmap::{mail::get::all_headers, replace_blob_ids},
utils::server::TestServer,
};
use jmap_client::{
email::{self, Header, HeaderForm},
mailbox::Role,
};
use std::{fs, path::PathBuf};
pub async fn test(test: &TestServer) {
println!("Running Email Parse tests...");
let account = test.account("[email protected]");
let client = account.jmap_client().await;
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("resources");
test_dir.push("jmap");
test_dir.push("email_parse");
let mailbox_id = client
.mailbox_create("JMAP Parse", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Test parsing an email attachment
for test_name in ["attachment.eml", "attachment_b64.eml"] {
let mut test_file = test_dir.clone();
test_file.push(test_name);
let email = client
.email_import(
fs::read(&test_file).unwrap(),
[mailbox_id.clone()],
None::<Vec<String>>,
None,
)
.await
.unwrap();
let blob_id = client
.email_get(email.id().unwrap(), Some([email::Property::Attachments]))
.await
.unwrap()
.unwrap()
.attachments()
.unwrap()
.first()
.unwrap()
.blob_id()
.unwrap()
.to_string();
let email = client
.email_parse(
&blob_id,
[
email::Property::Id,
email::Property::BlobId,
email::Property::ThreadId,
email::Property::MailboxIds,
email::Property::Keywords,
email::Property::Size,
email::Property::ReceivedAt,
email::Property::MessageId,
email::Property::InReplyTo,
email::Property::References,
email::Property::Sender,
email::Property::From,
email::Property::To,
email::Property::Cc,
email::Property::Bcc,
email::Property::ReplyTo,
email::Property::Subject,
email::Property::SentAt,
email::Property::HasAttachment,
email::Property::Preview,
email::Property::BodyValues,
email::Property::TextBody,
email::Property::HtmlBody,
email::Property::Attachments,
email::Property::BodyStructure,
]
.into(),
[
email::BodyProperty::PartId,
email::BodyProperty::BlobId,
email::BodyProperty::Size,
email::BodyProperty::Name,
email::BodyProperty::Type,
email::BodyProperty::Charset,
email::BodyProperty::Headers,
email::BodyProperty::Disposition,
email::BodyProperty::Cid,
email::BodyProperty::Language,
email::BodyProperty::Location,
]
.into(),
100.into(),
)
.await
.unwrap();
if !test_name.contains("_b64") {
for parts in [
email.text_body().unwrap(),
email.html_body().unwrap(),
email.attachments().unwrap(),
] {
for part in parts {
let blob_id = part.blob_id().unwrap();
let inner_blob = client.download(blob_id).await.unwrap();
test_file.set_extension(format!("part{}", part.part_id().unwrap()));
//fs::write(&test_file, inner_blob).unwrap();
let expected_inner_blob = fs::read(&test_file).unwrap();
assert_eq!(
inner_blob,
expected_inner_blob,
"file: {}",
test_file.display()
);
}
}
}
test_file.set_extension("json");
let result = replace_blob_ids(serde_json::to_string_pretty(&email.into_test()).unwrap());
if fs::read(&test_file).unwrap() != result.as_bytes() {
test_file.set_extension("failed");
fs::write(&test_file, result.as_bytes()).unwrap();
panic!("Test failed, output saved to {}", test_file.display());
}
}
// Test header parsing on a temporary blob
let mut test_file = test_dir;
test_file.push("headers.eml");
let blob_id = client
.upload(None, fs::read(&test_file).unwrap(), None)
.await
.unwrap()
.take_blob_id();
let mut email = client
.email_parse(
&blob_id,
[
email::Property::Id,
email::Property::MessageId,
email::Property::InReplyTo,
email::Property::References,
email::Property::Sender,
email::Property::From,
email::Property::To,
email::Property::Cc,
email::Property::Bcc,
email::Property::ReplyTo,
email::Property::Subject,
email::Property::SentAt,
email::Property::Preview,
email::Property::TextBody,
email::Property::HtmlBody,
email::Property::Attachments,
]
.into(),
[
email::BodyProperty::Size,
email::BodyProperty::Name,
email::BodyProperty::Type,
email::BodyProperty::Charset,
email::BodyProperty::Disposition,
email::BodyProperty::Cid,
email::BodyProperty::Language,
email::BodyProperty::Location,
email::BodyProperty::Header(Header {
name: "X-Custom-Header".into(),
form: HeaderForm::Raw,
all: false,
}),
email::BodyProperty::Header(Header {
name: "X-Custom-Header-2".into(),
form: HeaderForm::Raw,
all: false,
}),
]
.into(),
100.into(),
)
.await
.unwrap()
.into_test();
for property in all_headers() {
email.headers.extend(
client
.email_parse(&blob_id, [property].into(), [].into(), None)
.await
.unwrap()
.into_test()
.headers,
);
}
test_file.set_extension("json");
let result = replace_blob_ids(serde_json::to_string_pretty(&email).unwrap());
if fs::read(&test_file).unwrap() != result.as_bytes() {
test_file.set_extension("failed");
fs::write(&test_file, result.as_bytes()).unwrap();
panic!("Test failed, output saved to {}", test_file.display());
}
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
+908
View File
@@ -0,0 +1,908 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::store::{deflate_test_resource, query::FIELDS};
use crate::utils::account::Account;
use crate::utils::server::TestServer;
use ::email::{cache::MessageCacheFetch, mailbox::Mailbox};
use ahash::AHashSet;
use common::storage::index::ObjectIndexBuilder;
use jmap_client::{
client::Client,
core::query::{Comparator, Filter},
email,
};
use mail_builder::{
MessageBuilder,
headers::{date::Date, message_id::MessageId, text::Text},
};
use mail_parser::HeaderName;
use std::{collections::hash_map::Entry, str::FromStr, time::Instant};
use store::{
ahash::AHashMap,
write::{BatchBuilder, now},
};
use types::{collection::Collection, id::Id, special_use::SpecialUse};
const MAX_THREADS: usize = 100;
const MAX_MESSAGES: usize = 1000;
const MAX_MESSAGES_PER_THREAD: usize = 100;
pub async fn test(test: &TestServer) {
println!("Running Email Query tests...");
let server = test.server.clone();
let account = test.account("[email protected]");
let client = account.jmap_client().await;
if test.is_reset() {
// Add some "virtual" mailbox ids so create doesn't fail
let mut batch = BatchBuilder::new();
let account_id = Id::from_str(client.default_account_id())
.unwrap()
.document_id();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox);
for mailbox_id in 1545..3010 {
batch
.with_document(mailbox_id)
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(Mailbox {
name: format!("Mailbox {mailbox_id}"),
role: SpecialUse::None,
parent_id: 0,
sort_order: None,
uid_validity: 0,
subscribers: vec![],
acls: vec![],
}))
.unwrap();
}
server
.core
.storage
.data
.write(batch.build_all())
.await
.unwrap();
// Create test messages
println!("Inserting JMAP Mail query test messages...");
create(test, account).await;
assert_eq!(
test.server
.get_cached_messages(account_id)
.await
.unwrap()
.emails
.items
.iter()
.map(|m| m.thread_id)
.collect::<AHashSet<_>>()
.len(),
MAX_THREADS
);
// Wait for indexing to complete
test.wait_for_tasks().await;
}
let can_stem = !test.server.search_store().is_mysql();
println!("Running JMAP Mail query tests...");
query(&client, can_stem).await;
println!("Running JMAP Mail query options tests...");
query_options(&client).await;
println!("Deleting all messages...");
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();
response
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_set_email()
.unwrap();
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
pub async fn query(client: &Client, can_stem: bool) {
for (filter, sort, expected_results) in [
(
Filter::and(vec![
(email::query::Filter::after(1850)),
(email::query::Filter::from("george")),
]),
vec![
email::query::Comparator::subject(),
email::query::Comparator::sent_at(),
],
vec![
"N01389", "T10115", "N00618", "N03500", "T01587", "T00397", "N01561", "N05250",
"N03973", "N04973", "N04057", "N01940", "N01539", "N01612", "N04484", "N01954",
"N05998", "T02053", "AR00171", "AR00172", "AR00176",
],
),
(
Filter::and(vec![
(email::query::Filter::in_mailbox(Id::new(1768u64).to_string())),
(email::query::Filter::cc("canvas")),
]),
vec![
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec!["T01882", "N04689", "T00925", "N00121"],
),
(
Filter::and(vec![
(email::query::Filter::text(if can_stem { "study" } else { "studies" })),
(email::query::Filter::in_mailbox_other_than(vec![
Id::new(1991).to_string(),
Id::new(1870).to_string(),
Id::new(2011).to_string(),
Id::new(1951).to_string(),
Id::new(1902).to_string(),
Id::new(1808).to_string(),
Id::new(1963).to_string(),
])),
]),
vec![
email::query::Comparator::subject(),
email::query::Comparator::sent_at(),
],
if can_stem {
vec![
/*"T10330", "N01744", "N01743", "N04885", "N02688", "N02122", "A00059", "A00058",
"N02123", "T00651", "T09439", "N05001", "T05848", "T05508",*/
"T09187", "T10330", "N01744", "N01743", "N04885", "N02688", "N02122", "A00059",
"A00057", "A00058", "N02123", "T00651", "T09439", "N05001", "A01072", "A01061",
"AR00050", "T02310", "T05848", "T05508", "P20078", "P20079",
]
} else {
vec!["T10330", "N02122", "N02123", "T09439"]
},
),
(
Filter::and(vec![
(email::query::Filter::has_keyword("N0")).into(),
Filter::not(vec![(email::query::Filter::from("collins"))]),
(email::query::Filter::body("bequeathed")).into(),
]),
vec![
email::query::Comparator::subject(),
email::query::Comparator::sent_at(),
],
vec![
"N02640", "A01020", "N01250", "T03430", "N01800", "N00620", "N05250", "N04630",
"A01040",
],
),
(
email::query::Filter::not_keyword("artist").into(),
vec![
email::query::Comparator::subject(),
email::query::Comparator::sent_at(),
],
vec!["T08626", "T09334", "T09455", "N01737", "T10965"],
),
(
Filter::and(vec![
(email::query::Filter::after(1970)),
(email::query::Filter::before(1972)),
(email::query::Filter::text("colour")),
]),
vec![
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec!["T01745", "P01436", "P01437"],
),
(
Filter::and(vec![(email::query::Filter::text("'cats and dogs'"))]),
vec![email::query::Comparator::from()],
vec!["P77623"],
),
(
Filter::and(vec![
(email::query::Filter::header(
HeaderName::Comments.to_string(),
Some("attributed"),
)),
(email::query::Filter::from("john")),
(email::query::Filter::cc("oil")),
]),
vec![email::query::Comparator::from()],
vec!["T10965"],
),
(
Filter::and(vec![
(email::query::Filter::all_in_thread_have_keyword("N")),
(email::query::Filter::before(1800)),
]),
vec![
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"N01496", "N05916", "N01046", "N00675", "N01320", "N01321", "N00273", "N01453",
"N02984",
],
),
(
Filter::and(vec![
(email::query::Filter::none_in_thread_have_keyword("N")),
(email::query::Filter::after(1995)),
]),
vec![
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"AR00163", "AR00164", "AR00472", "P11481", "AR00066", "AR00178", "P77895",
"P77896", "P77897",
],
),
(
Filter::and(vec![
(email::query::Filter::some_in_thread_have_keyword("Bronze")),
(email::query::Filter::before(1878)),
]),
vec![
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"N04326", "N01610", "N02920", "N01587", "T00167", "T00168", "N01554", "N01535",
"N01536", "N01622", "N01754", "N01594",
],
),
// Sorting tests
(
email::query::Filter::before(1800).into(),
vec![
email::query::Comparator::all_in_thread_have_keyword("N"),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"T09417", "T01882", "T08820", "N04689", "T08891", "T00986", "N00316", "N03544",
"N04296", "N04297", "T08234", "N00112", "T00211", "N01497", "N02639", "N02640",
"T00925", "T11683", "T08269", "D00001", "D00002", "D00046", "N00121", "N00126",
"T08626", "N01496", "N05916", "N01046", "N00675", "N01320", "N01321", "N00273",
"N01453", "N02984",
],
),
(
email::query::Filter::before(1800).into(),
vec![
email::query::Comparator::all_in_thread_have_keyword("N").descending(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"N01496", "N05916", "N01046", "N00675", "N01320", "N01321", "N00273", "N01453",
"N02984", "T09417", "T01882", "T08820", "N04689", "T08891", "T00986", "N00316",
"N03544", "N04296", "N04297", "T08234", "N00112", "T00211", "N01497", "N02639",
"N02640", "T00925", "T11683", "T08269", "D00001", "D00002", "D00046", "N00121",
"N00126", "T08626",
],
),
(
Filter::and(vec![
(email::query::Filter::after(1875)),
(email::query::Filter::before(1878)),
]),
vec![
email::query::Comparator::some_in_thread_have_keyword("Bronze"),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"N01559", "N02123", "N01940", "N03594", "N01494", "N04271", "N04326", "N01610",
"N02920", "N01587", "T00167", "T00168", "N01554", "N01535", "N01536", "N01622",
"N01754", "N01594",
],
),
(
Filter::and(vec![
(email::query::Filter::after(1875)),
(email::query::Filter::before(1878)),
]),
vec![
email::query::Comparator::some_in_thread_have_keyword("Bronze").descending(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"N04326", "N01610", "N02920", "N01587", "T00167", "T00168", "N01554", "N01535",
"N01536", "N01622", "N01754", "N01594", "N01559", "N02123", "N01940", "N03594",
"N01494", "N04271",
],
),
(
Filter::and(vec![
(email::query::Filter::after(1786)),
(email::query::Filter::before(1840)),
(email::query::Filter::has_keyword("T")),
]),
vec![
email::query::Comparator::has_keyword("attributed to"),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"T09417", "T08951", "T01851", "T01852", "T08761", "T08123", "T08756", "T10561",
"T10562", "T10563", "T00986", "T03424", "T03427", "T08234", "T08133", "T06866",
"T08897", "T00996", "T00997", "T01095", "T03393", "T09456", "T00188", "T02362",
"T09065", "T09547", "T10330", "T09187", "T03433", "T08635", "T02366", "T03436",
"T09150", "T01861", "T09759", "T11683", "T02368", "T02369", "T08269", "T01018",
"T10066", "T01710", "T01711", "T05764", "T09455", "T09334", "T10965", "T08626",
],
),
(
Filter::and(vec![
(email::query::Filter::after(1786)),
(email::query::Filter::before(1840)),
(email::query::Filter::has_keyword("T")),
]),
vec![
email::query::Comparator::has_keyword("attributed to").descending(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
vec![
"T09455", "T09334", "T10965", "T08626", "T09417", "T08951", "T01851", "T01852",
"T08761", "T08123", "T08756", "T10561", "T10562", "T10563", "T00986", "T03424",
"T03427", "T08234", "T08133", "T06866", "T08897", "T00996", "T00997", "T01095",
"T03393", "T09456", "T00188", "T02362", "T09065", "T09547", "T10330", "T09187",
"T03433", "T08635", "T02366", "T03436", "T09150", "T01861", "T09759", "T11683",
"T02368", "T02369", "T08269", "T01018", "T10066", "T01710", "T01711", "T05764",
],
),
] {
let mut request = client.build();
let query_request = request
.query_email()
.filter(filter.clone())
.sort(sort.clone())
.calculate_total(true);
query_request.arguments().collapse_threads(false);
let query_result_ref = query_request.result_reference();
request
.get_email()
.ids_ref(query_result_ref)
.properties([email::Property::MessageId]);
let results = request
.send()
.await
.unwrap_or_else(|_| panic!("invalid response for {filter:?}"))
.unwrap_method_responses()
.pop()
.unwrap_or_else(|| panic!("invalid response for {filter:?}"))
.unwrap_get_email()
.unwrap_or_else(|_| panic!("invalid response for {filter:?}"))
.take_list()
.into_iter()
.map(|e| e.message_id().unwrap().first().unwrap().to_string())
.collect::<Vec<_>>();
let mut missing = Vec::new();
let mut extra = Vec::new();
for &expected in &expected_results {
if !results.iter().any(|r| r.as_str() == expected) {
missing.push(expected);
}
}
for result in &results {
if !expected_results.contains(&result.as_str()) {
extra.push(result.as_str());
}
}
assert_eq!(
results, expected_results,
"failed test!\nfilter: {filter:?}\nsort: {sort:?}\nmissing: {missing:?}\nextra: {extra:?}"
);
}
}
pub async fn query_options(client: &Client) {
for (query, expected_results, expected_results_collapsed) in [
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 0,
anchor: None,
anchor_offset: 0,
limit: 10,
},
vec![
"N01496", "N01320", "N01321", "N05916", "N00273", "N01453", "N02984", "T08820",
"N00112", "T00211",
],
vec![
"N01496", "N01320", "N05916", "N01453", "T08820", "N01046", "N00675", "T08891",
"T01882", "N04296",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 10,
anchor: None,
anchor_offset: 0,
limit: 10,
},
vec![
"N01046", "N00675", "T08891", "N00126", "T01882", "N04689", "T00925", "N00121",
"N04296", "N04297",
],
vec![
"T08234", "T09417", "N01110", "T08123", "N01039", "T09456", "T08951", "N01273",
"N00373", "T09547",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: -10,
anchor: None,
anchor_offset: 0,
limit: 0,
},
vec![
"T07236", "P11481", "AR00066", "P77895", "P77896", "P77897", "AR00163", "AR00164",
"AR00472", "AR00178",
],
vec![
"P07639", "P07522", "AR00089", "P02949", "T05820", "P11441", "T06971", "P11481",
"AR00163", "AR00164",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: -20,
anchor: None,
anchor_offset: 0,
limit: 10,
},
vec![
"P20079", "AR00024", "AR00182", "P20048", "P20044", "P20045", "P20046", "T06971",
"AR00177", "P77935",
],
vec![
"T00300", "P06033", "T02310", "T02135", "P04006", "P03166", "P01358", "P07133",
"P03138", "T03562",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: -100000,
anchor: None,
anchor_offset: 0,
limit: 1,
},
vec!["N01496"],
vec!["N01496"],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: -1,
anchor: None,
anchor_offset: 0,
limit: 100000,
},
vec!["AR00178"],
vec!["AR00164"],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 0,
anchor: get_anchor(client, "N01205").await,
anchor_offset: 0,
limit: 10,
},
vec![
"N01205", "N01976", "T01139", "N01525", "T00176", "N01405", "N02396", "N04885",
"N01526", "N02134",
],
vec![
"N01205", "N01526", "T01455", "N01969", "N05250", "N01781", "N00759", "A00057",
"N03527", "N01558",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 0,
anchor: get_anchor(client, "N01205").await,
anchor_offset: 10,
limit: 10,
},
vec![
"N01933", "N03618", "T03904", "N02398", "N02399", "N02688", "T01455", "N03051",
"N01500", "N03411",
],
vec![
"N01559", "N04326", "N06017", "N01553", "N01617", "N01528", "N01539", "T09439",
"N01593", "N03988",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 0,
anchor: get_anchor(client, "N01205").await,
anchor_offset: -10,
limit: 10,
},
vec![
"T03614", "N05779", "N04652", "N01534", "A00845", "N03409", "N03410", "N02061",
"N02426", "N00662",
],
vec![
"N00436", "N00443", "N02237", "T03025", "N01722", "N01356", "N01800", "T05475",
"T01587", "N05779",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 0,
anchor: get_anchor(client, "N01496").await,
anchor_offset: -10,
limit: 10,
},
vec![
"N01496", "N01320", "N01321", "N05916", "N00273", "N01453", "N02984", "T08820",
"N00112", "T00211",
],
vec![
"N01496", "N01320", "N05916", "N01453", "T08820", "N01046", "N00675", "T08891",
"T01882", "N04296",
],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 0,
anchor: get_anchor(client, "AR00164").await,
anchor_offset: 10,
limit: 10,
},
vec![],
vec![],
),
(
EmailQuery {
filter: None,
sort: vec![
email::query::Comparator::subject(),
email::query::Comparator::from(),
email::query::Comparator::sent_at(),
],
position: 0,
anchor: get_anchor(client, "AR00164").await,
anchor_offset: 0,
limit: 0,
},
vec!["AR00164", "AR00472", "AR00178"],
vec!["AR00164"],
),
] {
for (test_num, expected_results) in [expected_results, expected_results_collapsed]
.into_iter()
.enumerate()
{
let mut request = client.build();
let query_request = request
.query_email()
.sort(query.sort.clone())
.position(query.position)
.calculate_total(true);
if query.limit > 0 {
query_request.limit(query.limit);
}
if let Some(filter) = query.filter.as_ref() {
query_request.filter(filter.clone());
}
if let Some(anchor) = query.anchor.as_ref() {
query_request.anchor(anchor);
query_request.anchor_offset(query.anchor_offset);
}
query_request.arguments().collapse_threads(test_num == 1);
if !expected_results.is_empty() {
let query_result_ref = query_request.result_reference();
request
.get_email()
.ids_ref(query_result_ref)
.properties([email::Property::MessageId]);
assert_eq!(
request
.send()
.await
.unwrap()
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_get_email()
.unwrap()
.take_list()
.into_iter()
.map(|e| e.message_id().unwrap().first().unwrap().to_string())
.collect::<Vec<_>>(),
expected_results,
"{:#?} ({})",
query,
test_num == 1
);
} else {
assert_eq!(
request.send_query_email().await.unwrap().ids(),
Vec::<&str>::new()
);
}
}
}
}
pub async fn create(test: &TestServer, account: &Account) {
let sent_at = now();
let now = Instant::now();
let mut fields = AHashMap::default();
for (field_num, field) in FIELDS.iter().enumerate() {
fields.insert(field.to_string(), field_num);
}
let mut total_messages = 0;
let mut total_threads = 0;
let mut thread_count = AHashMap::default();
let mut artist_count = AHashMap::default();
let mut messages = Vec::new();
let mut chunks = Vec::new();
'outer: for (idx, record) in csv::ReaderBuilder::new()
.has_headers(true)
.from_reader(&deflate_test_resource("artwork_data.csv.gz")[..])
.records()
.enumerate()
{
let record = record.unwrap();
let mut values_str = AHashMap::default();
let mut values_int = AHashMap::default();
for field_name in [
"year",
"acquisitionYear",
"accession_number",
"artist",
"artistRole",
"medium",
"title",
"creditLine",
"inscription",
] {
let field = record.get(fields[field_name]).unwrap();
if field.is_empty()
|| (field_name == "title" && (field.contains('[') || field.contains(']')))
{
continue 'outer;
} else if field_name == "year" || field_name == "acquisitionYear" {
let field = field.parse::<i32>().unwrap_or(0);
if field < 1000 {
continue 'outer;
}
values_int.insert(field_name.to_string(), field);
} else {
values_str.insert(field_name.to_string(), field.to_string());
}
}
let val = artist_count
.entry(values_str["artist"].clone())
.or_insert(0);
if *val == 3 {
continue;
}
*val += 1;
match thread_count.entry(values_int["year"]) {
Entry::Occupied(mut e) => {
let messages_per_thread = e.get_mut();
if *messages_per_thread == MAX_MESSAGES_PER_THREAD {
continue;
}
*messages_per_thread += 1;
}
Entry::Vacant(e) => {
if total_threads == MAX_THREADS {
continue;
}
total_threads += 1;
e.insert(1);
}
}
total_messages += 1;
let mut keywords = Vec::new();
for keyword in [
values_str["medium"].to_string(),
values_str["artistRole"].to_string(),
values_str["accession_number"][0..1].to_string(),
format!(
"N{}",
&values_str["accession_number"][values_str["accession_number"].len() - 1..]
),
] {
if keyword == "attributed to"
|| keyword == "T"
|| keyword == "N0"
|| keyword == "N"
|| keyword == "artist"
|| keyword == "Bronze"
{
keywords.push(keyword);
}
}
let message = MessageBuilder::new()
.from((values_str["artist"].as_str(), "[email protected]"))
.cc((values_str["medium"].as_str(), "[email protected]"))
.subject(format!("Year {}", values_int["year"]))
.date(Date::new(sent_at as i64 + idx as i64))
.message_id(values_str["accession_number"].as_str())
.header("References", MessageId::new(values_int["year"].to_string()))
.header("Comments", Text::new(values_str["artistRole"].as_str()))
.text_body(format!(
"{}\n{}\n",
values_str["creditLine"], values_str["inscription"]
))
.attachment("text/plain", "details.txt", values_str["title"].as_bytes())
.write_to_vec()
.unwrap();
messages.push((
message,
[
Id::new(values_int["year"] as u64).to_string(),
Id::new((values_int["acquisitionYear"] + 1000) as u64).to_string(),
],
keywords,
values_int["year"] as i64,
));
if messages.len() == 100 {
chunks.push(messages);
messages = Vec::new();
}
if total_messages == MAX_MESSAGES {
break;
}
}
if !messages.is_empty() {
chunks.push(messages);
}
let mut tasks = Vec::new();
for chunk in chunks {
let client = account.jmap_client().await;
tasks.push(tokio::spawn(async move {
for (raw_message, mailbox_ids, keywords, sent_at) in chunk {
client
.email_import(raw_message, mailbox_ids, keywords.into(), Some(sent_at))
.await
.unwrap();
}
}));
}
for task in tasks {
task.await.unwrap();
}
test.wait_for_tasks().await;
println!(
"Imported {} messages in {} ms (single thread).",
total_messages,
now.elapsed().as_millis()
);
}
async fn get_anchor(client: &Client, anchor: &str) -> Option<String> {
client
.email_query(
email::query::Filter::header("Message-Id", anchor.into()).into(),
None::<Vec<_>>,
)
.await
.unwrap()
.take_ids()
.pop()
.unwrap()
.into()
}
#[derive(Debug, Clone)]
pub struct EmailQuery {
pub filter: Option<Filter<email::query::Filter>>,
pub sort: Vec<Comparator<email::query::Comparator>>,
pub position: i32,
pub anchor: Option<String>,
pub anchor_offset: i32,
pub limit: usize,
}
+337
View File
@@ -0,0 +1,337 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
jmap::mail::changes::{LogAction, ParseState},
utils::server::TestServer,
};
use ::email::message::metadata::MessageData;
use common::storage::index::ObjectIndexBuilder;
use jmap_client::{
core::query::{Comparator, Filter},
email,
mailbox::Role,
};
use jmap_proto::types::state::State;
use std::str::FromStr;
use store::{
ValueKey,
ahash::{AHashMap, AHashSet},
write::{AlignedBytes, Archive, BatchBuilder},
};
use types::{
collection::{Collection, SyncCollection},
id::Id,
};
pub async fn test(test: &TestServer) {
println!("Running Email QueryChanges tests...");
let server = test.server.clone();
let account = test.account("[email protected]");
let client = account.jmap_client().await;
let mailbox1_id = client
.mailbox_create("JMAP Changes 1", None::<String>, Role::None)
.await
.unwrap()
.take_id();
let mailbox2_id = client
.mailbox_create("JMAP Changes 2", None::<String>, Role::None)
.await
.unwrap()
.take_id();
let mut states = vec![State::Initial];
let mut id_map = AHashMap::default();
let mut updated_ids = AHashSet::default();
let mut removed_ids = AHashSet::default();
let mut type1_ids = AHashSet::default();
let mut thread_id_map: AHashMap<u32, Id> = AHashMap::default();
let mut thread_id = 100;
for (change_num, change) in [
LogAction::Insert(0),
LogAction::Insert(1),
LogAction::Insert(2),
LogAction::Move(0, 3),
LogAction::Insert(4),
LogAction::Insert(5),
LogAction::Update(1),
LogAction::Update(2),
LogAction::Delete(1),
LogAction::Insert(6),
LogAction::Insert(7),
LogAction::Update(2),
LogAction::Update(4),
LogAction::Update(5),
LogAction::Update(6),
LogAction::Update(7),
LogAction::Delete(4),
LogAction::Delete(5),
LogAction::Delete(6),
LogAction::Insert(8),
LogAction::Insert(9),
LogAction::Insert(10),
LogAction::Update(3),
LogAction::Update(2),
LogAction::Update(8),
LogAction::Move(9, 11),
LogAction::Move(10, 12),
LogAction::Delete(8),
]
.iter()
.enumerate()
{
match &change {
LogAction::Insert(id) => {
let jmap_id = Id::from_str(
client
.email_import(
format!(
"From: test_{}\nSubject: test_{}\n\ntest",
if change_num % 2 == 0 { 1 } else { 2 },
*id
)
.into_bytes(),
[if change_num % 2 == 0 {
&mailbox1_id
} else {
&mailbox2_id
}],
[if change_num % 2 == 0 { "1" } else { "2" }].into(),
Some(*id as i64),
)
.await
.unwrap()
.id()
.unwrap(),
)
.unwrap();
id_map.insert(*id, jmap_id);
if change_num % 2 == 0 {
type1_ids.insert(jmap_id);
}
thread_id_map.entry(jmap_id.prefix_id()).or_insert(jmap_id);
}
LogAction::Update(id) => {
let id = *id_map.get(id).unwrap();
let mut batch = BatchBuilder::new();
batch
.with_document(id.document_id())
.log_item_update(SyncCollection::Email, id.prefix_id().into());
server.store().write(batch.build_all()).await.unwrap();
updated_ids.insert(id);
}
LogAction::Delete(id) => {
let id = *id_map.get(id).unwrap();
client.email_destroy(&id.to_string()).await.unwrap();
removed_ids.insert(id);
}
LogAction::Move(from, to) => {
let id = *id_map.get(from).unwrap();
let new_id = Id::from_parts(thread_id, id.document_id());
//let new_thread_id = store::rand::random::<u32>();
let old_message_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account.id().document_id(),
Collection::Email,
id.document_id(),
))
.await
.unwrap()
.unwrap();
let old_message = old_message_.to_unarchived::<MessageData>().unwrap();
let mut new_message = old_message.deserialize::<MessageData>().unwrap();
new_message.thread_id = thread_id;
server
.core
.storage
.data
.write(
BatchBuilder::new()
.with_account_id(account.id().document_id())
.with_collection(Collection::Email)
.with_document(id.document_id())
.custom(
ObjectIndexBuilder::new()
.with_current(old_message)
.with_changes(new_message),
)
.unwrap()
.build_all(),
)
.await
.unwrap();
id_map.insert(*to, new_id);
if type1_ids.contains(&id) {
type1_ids.insert(new_id);
}
removed_ids.insert(id);
thread_id_map.insert(new_id.prefix_id(), new_id);
thread_id += 1;
}
LogAction::UpdateChild(_) => unreachable!(),
}
let mut new_state = State::Initial;
for state in &states {
for (test_num, query) in vec![
QueryChanges {
filter: None,
sort: vec![email::query::Comparator::received_at()],
since_query_state: state.clone(),
max_changes: 0,
up_to_id: None,
collapse_threads: false,
},
QueryChanges {
filter: Some(email::query::Filter::from("test_1").into()),
sort: vec![email::query::Comparator::received_at()],
since_query_state: state.clone(),
max_changes: 0,
up_to_id: None,
collapse_threads: false,
},
QueryChanges {
filter: Some(email::query::Filter::in_mailbox(&mailbox1_id).into()),
sort: vec![email::query::Comparator::received_at()],
since_query_state: state.clone(),
max_changes: 0,
up_to_id: None,
collapse_threads: false,
},
QueryChanges {
filter: None,
sort: vec![email::query::Comparator::received_at()],
since_query_state: state.clone(),
max_changes: 0,
up_to_id: id_map
.get(&7)
.map(|id| id.to_string().into())
.unwrap_or(None),
collapse_threads: false,
},
QueryChanges {
filter: None,
sort: vec![email::query::Comparator::received_at()],
since_query_state: state.clone(),
max_changes: 0,
up_to_id: None,
collapse_threads: true,
},
]
.into_iter()
.enumerate()
{
if (test_num == 3 || test_num == 4) && query.up_to_id.is_none() {
continue;
}
if test_num == 4 && !query.collapse_threads {
continue;
}
let mut request = client.build();
let query_request = request
.query_email_changes(query.since_query_state.to_string())
.sort(query.sort);
if let Some(filter) = query.filter {
query_request.filter(filter);
}
if let Some(up_to_id) = query.up_to_id {
query_request.up_to_id(up_to_id);
}
if query.collapse_threads {
query_request.arguments().collapse_threads(true);
}
let changes = request.send_query_email_changes().await.unwrap();
if test_num == 0 || test_num == 1 {
// Immutable filters should not return modified ids, only deletions.
for id in changes.removed() {
let id = Id::from_str(id).unwrap();
assert!(
removed_ids.contains(&id),
"{:?} (id: {:?})",
changes,
id_map.iter().find(|(_, v)| **v == id).map(|(k, _)| k)
);
}
}
if test_num == 1 || test_num == 2 {
// Only type 1 results should be added to the list.
for item in changes.added() {
let id = Id::from_str(item.id()).unwrap();
assert!(
type1_ids.contains(&id),
"{:?} (id: {:?})",
changes,
id_map.iter().find(|(_, v)| **v == id).map(|(k, _)| k)
);
}
}
if test_num == 3 {
// Only ids up to 7 should be added to the list.
for item in changes.added() {
let item_id = Id::from_str(item.id()).unwrap();
let id = id_map.iter().find(|(_, v)| **v == item_id).unwrap().0;
assert!(id <= &7, "{:?} (id: {})", changes, id);
}
}
if test_num == 4 {
// With collapse_threads, only first email per thread should be added.
let mut seen_threads = AHashSet::new();
for item in changes.added() {
let item_id = Id::from_str(item.id()).unwrap();
let thread_id = item_id.prefix_id();
assert!(
seen_threads.insert(thread_id),
"Thread {} appears multiple times with collapse_threads: {:?}",
thread_id,
changes
);
// Verify this is the first email in this thread
assert_eq!(
thread_id_map.get(&thread_id),
Some(&item_id),
"Expected first email in thread {}, got {:?}",
thread_id,
item_id
);
}
}
if let State::Initial = state {
new_state = State::parse_str(changes.new_query_state()).unwrap();
}
}
}
states.push(new_state);
}
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
#[derive(Debug, Clone)]
pub struct QueryChanges {
pub filter: Option<Filter<email::query::Filter>>,
pub sort: Vec<Comparator<email::query::Comparator>>,
pub since_query_state: State,
pub max_changes: usize,
pub up_to_id: Option<String>,
pub collapse_threads: bool,
}
+174
View File
@@ -0,0 +1,174 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use email::mailbox::INBOX_ID;
use jmap_client::{core::query, email::query::Filter};
use std::{fs, path::PathBuf};
use store::ahash::AHashMap;
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running SearchSnippet tests...");
let account = test.account("[email protected]");
let client = account.jmap_client().await;
let mailbox_id = Id::from(INBOX_ID).to_string();
let mut email_ids = AHashMap::default();
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("resources");
test_dir.push("jmap");
test_dir.push("email_snippet");
// Import test messages
for email_name in [
"html",
"subpart",
"mixed",
"text_plain",
"text_plain_chinese",
] {
let mut file_name = test_dir.clone();
file_name.push(format!("{}.eml", email_name));
let email_id = client
.email_import(
fs::read(&file_name).unwrap(),
[&mailbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
email_ids.insert(email_name, email_id);
}
test.wait_for_tasks().await;
let can_stem = test.server.search_store().internal_fts().is_some();
// Run tests
for (filter, email_name, snippet_subject, snippet_preview) in [
(
query::Filter::or(vec![
query::Filter::or(vec![Filter::subject("friend"), Filter::subject("help")]),
query::Filter::or(vec![Filter::body("secret"), Filter::body("call")]),
]),
"text_plain",
Some("<mark>Help</mark> a <mark>friend</mark> from Abidjan Côte d'Ivoire"),
Some(concat!(
"d'Ivoire. He <mark>secretly</mark> <mark>called</mark> me on his bedside ",
"and told me that he has a sum of $7.5M (Seven Million five Hundred Thousand",
" Dollars) left in a suspense account in a local bank here in Abidjan Côte ",
"d'Ivoire, that he used my name a"
)),
),
(
Filter::text("côte").into(),
"text_plain",
Some("Help a friend from Abidjan <mark>Côte</mark> d'Ivoire"),
Some(concat!(
"in Abidjan <mark>Côte</mark> d'Ivoire. He secretly called me on ",
"his bedside and told me that he has a sum of $7.5M (Seven ",
"Million five Hundred Thousand Dollars) left in a suspense ",
"account in a local bank here in Abidjan <mark>Côte</mark> d'Ivoire, that "
)),
),
(
Filter::text("\"your country\"").into(),
"text_plain",
None,
Some(concat!(
"over to <mark>your</mark> <mark>country</mark> to further my education and ",
"to secure a residential permit for me in <mark>your</mark> <mark>country",
"</mark>. Moreover, I am willing to offer you 30 percent of the total sum as ",
"compensation for your effort inp",
)),
),
(
Filter::text("overseas").into(),
"text_plain",
None,
Some("nominated account <mark>overseas</mark>. "),
),
(
Filter::text("孫子兵法").into(),
"text_plain_chinese",
Some("<mark>孫</mark><mark>子</mark><mark>兵法</mark>"),
Some(concat!(
"&lt;&quot;<mark>孫</mark><mark>子</mark><mark>兵法</mark>&quot;&gt; ",
"<mark>孫</mark><mark>子</mark>曰:兵者,國之大事,死生之地,存亡之道,",
"不可不察也。 <mark>孫</mark><mark>子</mark>曰:凡用兵之法,馳車千駟"
)),
),
(
Filter::text("cia").into(),
"subpart",
None,
Some("shouldn't the <mark>CIA</mark> have something like that? Bill"),
),
(
Filter::text("frösche").into(),
"html",
Some("Die Hasen und die <mark>Frösche</mark>"),
Some(concat!(
"und die <mark>Frösche</mark> Die Hasen klagten einst über ihre mißliche Lage; ",
"&quot;wir leben&quot;, sprach ein Redner, &quot;in steter Furcht vor Menschen und ",
"Tieren, eine Beute der Hunde, der Adler, ja fast aller Raubtiere! ",
"Unsere stete Angst ist är"
)),
),
(
Filter::text(if can_stem {
"es:galería vasto biblioteca"
} else {
"es:galería vastos biblioteca"
})
.into(),
"mixed",
Some("<mark>Biblioteca</mark> de Babel"),
Some(concat!(
"llaman la *<mark>Biblioteca</mark>*) se compone de un número indefinido, y tal ",
"vez infinito, de <mark>galerías</mark> hexagonales, con <mark>vastos</mark> ",
"pozos de ventilación en el medio, cercados por barandas bajísimas. Desde ",
"cualquier hexágono se "
)),
),
] {
let mut request = client.build();
let result_ref = request
.query_email()
.filter(filter.clone())
.result_reference();
request
.get_search_snippet()
.filter(filter)
.email_ids_ref(result_ref);
let response = request
.send()
.await
.unwrap()
.unwrap_method_responses()
.pop()
.unwrap()
.unwrap_get_search_snippet()
.unwrap();
let snippet = response
.snippet(email_ids.get(email_name).unwrap())
.unwrap_or_else(|| panic!("No snippet for {}", email_name));
assert_eq!(snippet_subject, snippet.subject());
assert_eq!(snippet_preview, snippet.preview());
assert!(
snippet.preview().map_or(0, |p| p.len()) <= 255,
"len: {}",
snippet.preview().map_or(0, |p| p.len())
);
}
// Destroy test data
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
+327
View File
@@ -0,0 +1,327 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
jmap::{find_values, replace_blob_ids, replace_boundaries, replace_values},
utils::server::TestServer,
};
use ::email::mailbox::INBOX_ID;
use ahash::AHashSet;
use jmap_client::{
Error, Set,
client::Client,
core::set::{SetError, SetErrorType},
email::{self, Email},
mailbox::Role,
};
use registry::schema::prelude::ObjectType;
use std::{fs, path::PathBuf};
use types::id::Id;
pub async fn test(test: &TestServer) {
println!("Running Email Set tests...");
let account = test.account("[email protected]");
let client = account.jmap_client().await;
let mailbox_id = Id::from(INBOX_ID).to_string();
create(&client, &mailbox_id).await;
update(&client, &mailbox_id).await;
test.destroy_all_mailboxes(account).await;
test.account("[email protected]")
.registry_destroy_all(ObjectType::SpamTrainingSample)
.await;
test.assert_is_empty().await;
}
async fn create(client: &Client, mailbox_id: &str) {
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("resources");
test_dir.push("jmap");
test_dir.push("email_set");
for file_name in fs::read_dir(&test_dir).unwrap() {
let mut file_name = file_name.as_ref().unwrap().path();
if file_name.extension().is_none_or(|e| e != "json") {
continue;
}
println!("Creating email from {:?}", file_name);
// Upload blobs
let mut json_request = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
let blob_values = find_values(&json_request, "\"blobId\"");
if !blob_values.is_empty() {
let mut blob_ids = Vec::with_capacity(blob_values.len());
for blob_value in &blob_values {
let blob_value = blob_value.replace("\\r", "\r").replace("\\n", "\n");
blob_ids.push(
client
.upload(None, blob_value.into_bytes(), None)
.await
.unwrap()
.take_blob_id(),
);
}
json_request = replace_values(json_request, &blob_values, &blob_ids);
}
// Create message and obtain its blobId
let mut request = client.build();
let mut create_item =
serde_json::from_slice::<Email<Set>>(json_request.as_bytes()).unwrap();
create_item.mailbox_ids([mailbox_id]);
let create_id = request.set_email().create_item(create_item);
let created_email = request
.send_set_email()
.await
.unwrap()
.created(&create_id)
.unwrap();
// Download raw message
let raw_message = client
.download(created_email.blob_id().unwrap())
.await
.unwrap();
// Fetch message
let mut request = client.build();
request
.get_email()
.ids([created_email.id().unwrap()])
.properties([
email::Property::Id,
email::Property::BlobId,
email::Property::ThreadId,
email::Property::MailboxIds,
email::Property::Keywords,
email::Property::ReceivedAt,
email::Property::MessageId,
email::Property::InReplyTo,
email::Property::References,
email::Property::Sender,
email::Property::From,
email::Property::To,
email::Property::Cc,
email::Property::Bcc,
email::Property::ReplyTo,
email::Property::Subject,
email::Property::SentAt,
email::Property::HasAttachment,
email::Property::Preview,
email::Property::BodyValues,
email::Property::TextBody,
email::Property::HtmlBody,
email::Property::Attachments,
email::Property::BodyStructure,
])
.arguments()
.body_properties([
email::BodyProperty::PartId,
email::BodyProperty::BlobId,
email::BodyProperty::Size,
email::BodyProperty::Name,
email::BodyProperty::Type,
email::BodyProperty::Charset,
email::BodyProperty::Headers,
email::BodyProperty::Disposition,
email::BodyProperty::Cid,
email::BodyProperty::Language,
email::BodyProperty::Location,
])
.fetch_all_body_values(true)
.max_body_value_bytes(100);
let email = request
.send_get_email()
.await
.unwrap()
.pop()
.unwrap()
.into_test();
// Compare raw message
file_name.set_extension("eml");
let result = replace_boundaries(String::from_utf8(raw_message).unwrap());
if fs::read(&file_name).unwrap() != result.as_bytes() {
file_name.set_extension("eml_failed");
fs::write(&file_name, result.as_bytes()).unwrap();
panic!("Test failed, output saved to {}", file_name.display());
}
// Compare response
file_name.set_extension("jmap");
let result = replace_blob_ids(replace_boundaries(
serde_json::to_string_pretty(&email).unwrap(),
));
if fs::read(&file_name).unwrap() != result.as_bytes() {
file_name.set_extension("jmap_failed");
fs::write(&file_name, result.as_bytes()).unwrap();
panic!("Test failed, output saved to {}", file_name.display());
}
}
}
async fn update(client: &Client, root_mailbox_id: &str) {
// Obtain all messageIds previously created
let mailbox = client
.email_query(
email::query::Filter::in_mailbox(root_mailbox_id).into(),
None::<Vec<_>>,
)
.await
.unwrap();
// Create two test mailboxes
let test_mailbox1_id = client
.mailbox_create("Test 1", None::<String>, Role::None)
.await
.unwrap()
.take_id();
let test_mailbox2_id = client
.mailbox_create("Test 2", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Set keywords and mailboxes
let mut request = client.build();
request
.set_email()
.update(mailbox.id(0))
.mailbox_ids([&test_mailbox1_id, &test_mailbox2_id])
.keywords(["test1", "test2"]);
request
.send_set_email()
.await
.unwrap()
.updated(mailbox.id(0))
.unwrap();
assert_email_properties(
client,
mailbox.id(0),
&[&test_mailbox1_id, &test_mailbox2_id],
&["test1", "test2"],
)
.await;
// Patch keywords and mailboxes
let mut request = client.build();
request
.set_email()
.update(mailbox.id(0))
.mailbox_id(&test_mailbox1_id, false)
.keyword("test1", true)
.keyword("test2", false)
.keyword("test3", true);
request
.send_set_email()
.await
.unwrap()
.updated(mailbox.id(0))
.unwrap();
assert_email_properties(
client,
mailbox.id(0),
&[&test_mailbox2_id],
&["test1", "test3"],
)
.await;
// Orphan messages should not be permitted
let mut request = client.build();
request
.set_email()
.update(mailbox.id(0))
.mailbox_id(&test_mailbox2_id, false);
assert!(matches!(
request
.send_set_email()
.await
.unwrap()
.updated(mailbox.id(0)),
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Updating and destroying the same item should not be allowed
let mut request = client.build();
let set_email_request = request.set_email();
set_email_request
.update(mailbox.id(0))
.mailbox_id(&test_mailbox2_id, false);
set_email_request.destroy([mailbox.id(0)]);
assert!(matches!(
request
.send_set_email()
.await
.unwrap()
.updated(mailbox.id(0)),
Err(Error::Set(SetError {
type_: SetErrorType::WillDestroy,
..
}))
));
// Delete some messages
let mut request = client.build();
request.set_email().destroy([mailbox.id(1), mailbox.id(2)]);
assert_eq!(
request
.send_set_email()
.await
.unwrap()
.destroyed_ids()
.unwrap()
.count(),
2
);
let mut request = client.build();
request.get_email().ids([mailbox.id(1), mailbox.id(2)]);
assert_eq!(request.send_get_email().await.unwrap().not_found().len(), 2);
// Destroy test mailboxes
client
.mailbox_destroy(&test_mailbox1_id, true)
.await
.unwrap();
client
.mailbox_destroy(&test_mailbox2_id, true)
.await
.unwrap();
}
pub async fn assert_email_properties(
client: &Client,
message_id: &str,
mailbox_ids: &[&str],
keywords: &[&str],
) {
let result = client
.email_get(
message_id,
[email::Property::MailboxIds, email::Property::Keywords].into(),
)
.await
.unwrap()
.unwrap();
assert_eq!(
mailbox_ids.iter().copied().collect::<AHashSet<_>>(),
result
.mailbox_ids()
.iter()
.copied()
.collect::<AHashSet<_>>()
);
assert_eq!(
keywords.iter().copied().collect::<AHashSet<_>>(),
result.keywords().iter().copied().collect::<AHashSet<_>>()
);
}
+581
View File
@@ -0,0 +1,581 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
jmap::mail::submission::{MockMessage, assert_message_delivery, spawn_mock_smtp_server},
utils::{dns::DnsCache, server::TestServer, smtp::SmtpConnection},
};
use jmap_client::{
Error,
core::set::{SetError, SetErrorType},
email, mailbox,
sieve::query::{Comparator, Filter},
};
use registry::schema::{prelude::ObjectType, structs::SieveUserScript};
use std::{
fs,
path::PathBuf,
time::{Duration, Instant},
};
pub async fn test(test: &TestServer) {
println!("Running Sieve tests...");
// Create a global script
let admin = test.account("[email protected]");
admin
.registry_create_object(SieveUserScript {
contents: "require \"reject\";\nreject \"Rejected from a global script.\";\nstop;\n"
.into(),
description: None,
is_active: true,
name: "common".into(),
})
.await;
admin.reload_settings().await;
let server = test.server.clone();
let account = test.account("[email protected]");
let client = account.jmap_client().await;
// Validate scripts
client
.sieve_script_validate(get_script("validate_ok"))
.await
.unwrap();
assert!(matches!(
client
.sieve_script_validate(get_script("validate_error"))
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidScript,
..
}))
));
// Create 5 Sieve scripts, all deactivated.
let mut script_ids = Vec::new();
for i in 0..5 {
script_ids.push(
client
.sieve_script_create(
format!("script_{}", i + 1),
format!("require \"fileinto\"; fileinto \"{}\";", i + 1).into_bytes(),
false,
)
.await
.unwrap()
.take_id(),
);
}
let response = client
.sieve_script_query(Filter::is_active(false).into(), [Comparator::name()].into())
.await
.unwrap();
assert_eq!(response.ids().len(), 5);
for (pos, id) in response.ids().iter().enumerate() {
let script = client
.sieve_script_get(id, None::<Vec<_>>)
.await
.unwrap()
.unwrap();
assert_eq!(script.name().unwrap(), format!("script_{}", pos + 1));
assert_eq!(
String::from_utf8(client.download(script.blob_id().unwrap()).await.unwrap()).unwrap(),
format!("require \"fileinto\"; fileinto \"{}\";", pos + 1)
);
}
// Activate last script twice and then the first script
for _ in 0..2 {
client
.sieve_script_activate(script_ids.last().unwrap())
.await
.unwrap();
assert_eq!(
client
.sieve_script_query(Filter::is_active(true).into(), [Comparator::name()].into())
.await
.unwrap()
.ids(),
vec![script_ids.last().unwrap().to_string()]
);
}
client
.sieve_script_activate(script_ids.first().unwrap())
.await
.unwrap();
assert_eq!(
client
.sieve_script_query(Filter::is_active(true).into(), [Comparator::name()].into())
.await
.unwrap()
.ids(),
vec![script_ids.first().unwrap().to_string()]
);
// Destroying an active script should not work
assert!(matches!(
client
.sieve_script_destroy(script_ids.first().unwrap())
.await,
Err(Error::Set(SetError {
type_: SetErrorType::ScriptIsActive,
..
}))
));
// Deactivate all scripts
client.sieve_script_deactivate().await.unwrap();
assert_eq!(
client
.sieve_script_query(Filter::is_active(true).into(), [Comparator::name()].into())
.await
.unwrap()
.ids(),
Vec::<String>::new()
);
// Connect to LMTP service
let mut lmtp = SmtpConnection::connect().await;
// Run mailbox, fileinto, flags tests
client
.sieve_script_create("test_mailbox", get_script("test_mailbox"), true)
.await
.unwrap();
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
// Make sure all folders were created
let mailbox_names = "My/Nested/Mailbox/with/multiple/levels/Folder"
.split('/')
.collect::<Vec<_>>();
let mut mailbox_ids = Vec::new();
for &mailbox in &mailbox_names {
let mut response = client
.mailbox_query(mailbox::query::Filter::name(mailbox).into(), None::<Vec<_>>)
.await
.unwrap();
assert!(
!response.ids().is_empty(),
"Mailbox {} was not created.",
mailbox
);
mailbox_ids.extend(response.take_ids());
}
assert_eq!(mailbox_ids.len(), mailbox_names.len());
// Make sure the message was delivered to the right folders
let message_ids = client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.take_ids();
assert_eq!(message_ids.len(), 1, "too many messages {:?}", message_ids);
let email = client
.email_get(
message_ids.last().unwrap(),
[email::Property::MailboxIds, email::Property::Keywords].into(),
)
.await
.unwrap()
.unwrap();
assert_eq!(
email.keywords().len(),
2,
"Expected 2 keywords, found {:?}.",
email.keywords()
);
for keyword in ["$important", "$seen"] {
if !email.keywords().contains(&keyword) {
panic!("Keyword {} not found in {:?}.", keyword, email.keywords());
}
}
assert_eq!(
email.mailbox_ids().len(),
3,
"Expected 3 mailbox ids, found {:?}.",
email.mailbox_ids()
);
let drafts_id = client
.mailbox_query(
mailbox::query::Filter::name("Drafts").into(),
None::<Vec<_>>,
)
.await
.unwrap()
.take_ids()
.pop()
.expect("Drafts mailbox not found.");
assert!(
email.mailbox_ids().contains(&drafts_id.as_str()),
"Drafts mailbox {} not found in {:?}.",
drafts_id,
email.mailbox_ids()
);
for mailbox_pos in [mailbox_ids.len() - 1, mailbox_ids.len() - 2] {
if !email
.mailbox_ids()
.contains(&mailbox_ids[mailbox_pos].as_str())
{
panic!(
"Mailbox {} ({}) not found in {:?}.",
mailbox_names[mailbox_pos],
mailbox_ids[mailbox_pos],
email.keywords()
);
}
}
// Run discard and duplicate tests
client
.sieve_script_create(
"test_discard_reject",
get_script("test_discard_reject"),
true,
)
.await
.unwrap();
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <[email protected]>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your TPS reports before ",
"going on holidays."
),
)
.await;
assert_eq!(
client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.ids()
.len(),
1,
"Discard failed."
);
// Let one sec duplicate ids expire
tokio::time::sleep(Duration::from_millis(1100)).await;
// Start mock SMTP server
let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server();
server.ipv4_add(
"localhost",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
// Run reject and duplicate check tests
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <[email protected]>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your T.P.S. reports before ",
"going on holidays."
),
)
.await;
assert_eq!(
client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.ids()
.len(),
1,
"Reject failed."
);
assert_message_delivery(
&mut smtp_rx,
MockMessage::new("<>", ["<[email protected]>"], "@No soup for you"),
)
.await;
// Run include tests
client
.sieve_script_create("test_include_this", get_script("test_include_this"), false)
.await
.unwrap();
client
.sieve_script_create("test_include", get_script("test_include"), true)
.await
.unwrap();
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <[email protected]>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your T.P.S. reports before ",
"going on holidays."
),
)
.await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<>",
["<[email protected]>"],
"@Rejected from an included script",
),
)
.await;
client
.sieve_script_create(
"Test Script",
concat!(
"require \"reject\";\n",
"reject \"Rejected from a mixed-case included script.\";\n",
"stop;\n"
)
.as_bytes()
.to_vec(),
false,
)
.await
.unwrap();
client
.sieve_script_create("test_include_case", get_script("test_include_case"), true)
.await
.unwrap();
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <[email protected]>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your T.P.S. reports before ",
"going on holidays."
),
)
.await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<>",
["<[email protected]>"],
"@Rejected from a mixed-case included script",
),
)
.await;
// Run include global tests
client
.sieve_script_create(
"test_include_global",
get_script("test_include_global"),
true,
)
.await
.unwrap();
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"Bcc: Undisclosed recipients;\r\n",
"Message-ID: <[email protected]>\r\n",
"Subject: Holidays\r\n",
"\r\n",
"Remember to file your T.P.S. reports before ",
"going on holidays."
),
)
.await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<>",
["<[email protected]>"],
"@Rejected from a global script",
),
)
.await;
// Run enclose + redirect tests
client
.sieve_script_create(
"test_redirect_enclose",
get_script("test_redirect_enclose"),
true,
)
.await
.unwrap();
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<[email protected]>",
["<[email protected]>"],
"@Attached you'll find",
),
)
.await;
assert_eq!(
client
.email_query(None::<email::query::Filter>, None::<Vec<_>>)
.await
.unwrap()
.ids()
.len(),
1,
"Redirected message was stored."
);
// Run notify + editheader + notify + fcc tests
client
.sieve_script_create("test_notify_fcc", get_script("test_notify_fcc"), true)
.await
.unwrap();
smtp_settings.lock().do_stop = true;
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: Urgently I need those TPS Reports\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<[email protected]>",
["<[email protected]>"],
"@It's TPS-o-clock",
),
)
.await;
let mut request = client.build();
request.get_email().properties([
email::Property::MailboxIds,
email::Property::Keywords,
email::Property::Subject,
]);
let emails = request.send_get_email().await.unwrap().take_list();
assert_eq!(
emails.len(),
3,
"Two new messages were expected: {:#?}.",
emails
);
'outer: for (subject, folder, keywords) in [
("It's TPS-o-clock", "Notifications", ""),
(
"Urgently I need those **censored** Reports",
"Inbox",
"$seen",
),
] {
for email in &emails {
if email.subject().unwrap().eq(subject) {
if !keywords.is_empty() && !email.keywords().contains(&keywords) {
panic!("Keyword {:?} not found in: {:#?}", keywords, email);
}
let mailbox_id = client
.mailbox_query(
mailbox::query::Filter::name(folder.to_string()).into(),
None::<Vec<_>>,
)
.await
.unwrap()
.take_ids()
.pop()
.unwrap_or_else(|| panic!("Mailbox {:?} not found", folder));
if !email.mailbox_ids().contains(&mailbox_id.as_str()) {
panic!(
"Mailbox {:?} ({}) not found in: {:#?}",
folder, mailbox_id, email
);
}
continue 'outer;
}
}
panic!("Email {:?} not found in: {:#?}", subject, emails);
}
// Remove test data
client.sieve_script_deactivate().await.unwrap();
let mut request = client.build();
request.query_sieve_script();
for id in request.send_query_sieve_script().await.unwrap().take_ids() {
client.sieve_script_destroy(&id).await.unwrap();
}
test.destroy_all_mailboxes(account).await;
admin
.registry_destroy_all(ObjectType::SieveUserScript)
.await;
test.assert_is_empty().await;
}
fn get_script(name: &str) -> Vec<u8> {
let mut script_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
script_path.push("resources");
script_path.push("jmap");
script_path.push("sieve");
script_path.push(format!("{}.sieve", name));
fs::read(script_path).unwrap()
}
+762
View File
@@ -0,0 +1,762 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
jmap::mail::set::assert_email_properties,
utils::{dns::DnsCache, server::TestServer},
};
use ahash::AHashMap;
use common::auth::{AccountCache, EmailAddress};
use jmap_client::{
Error,
client::Client,
core::{
response::IdentityGetResponse,
set::{SetError, SetErrorType, SetObject},
},
email_submission::{Address, Delivered, DeliveryStatus, Displayed, UndoStatus, query::Filter},
mailbox::Role,
};
use mail_parser::DateTime;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use store::{parking_lot::Mutex, write::BatchBuilder};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
net::TcpListener,
sync::mpsc,
};
use types::{collection::Collection, field::PrincipalField, id::Id};
#[derive(Default, Debug, PartialEq, Eq)]
pub struct MockMessage {
pub mail_from: String,
pub rcpt_to: Vec<String>,
pub message: String,
}
impl MockMessage {
pub fn new<T, U>(mail_from: T, rcpt_to: U, message: T) -> Self
where
T: Into<String>,
U: IntoIterator<Item = T>,
{
Self {
mail_from: mail_from.into(),
rcpt_to: rcpt_to.into_iter().map(|s| s.into()).collect(),
message: message.into(),
}
}
}
#[derive(Default)]
pub struct MockSMTPSettings {
pub fail_mail_from: bool,
pub fail_rcpt_to: bool,
pub fail_message: bool,
pub do_stop: bool,
}
#[allow(clippy::disallowed_types)]
pub async fn test(test: &TestServer) {
println!("Running E-mail submissions tests...");
// Start mock SMTP server
let server = test.server.clone();
let account = test.account("[email protected]");
let client = account.jmap_client().await;
let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server();
server.ipv4_add(
"localhost",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + std::time::Duration::from_secs(10),
);
// Test automatic identity creation
for (identity_id, email) in [(2u64, "[email protected]"), (1u64, "[email protected]")] {
let identity = client
.identity_get(&Id::from(identity_id).to_string(), None)
.await
.unwrap()
.unwrap();
assert_eq!(identity.email().unwrap(), email);
assert_eq!(identity.name().unwrap(), "John Doe");
}
// Users should be allowed to create identities only
// using email addresses associated to their principal
let iid1 = client
.identity_create("John Doe", "[email protected]")
.await
.unwrap()
.take_id();
let iid2 = client
.identity_create("John Doe (secondary)", "[email protected]")
.await
.unwrap()
.take_id();
assert!(matches!(
client
.identity_create("John the Spammer", "[email protected]")
.await,
Err(jmap_client::Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
client.identity_destroy(&iid1).await.unwrap();
client.identity_destroy(&iid2).await.unwrap();
// Create an identity without using a valid address should fail
match client
.identity_create("John Doe", "[email protected]")
.await
.unwrap_err()
{
Error::Set(err) => assert_eq!(err.error(), &SetErrorType::InvalidProperties),
err => panic!("Unexpected error: {:?}", err),
}
// Create an identity
let identity_id = client
.identity_create("John Doe (manually created)", "[email protected]")
.await
.unwrap()
.take_id();
// Create test mailboxes
let mailbox_id = client
.mailbox_create("JMAP EmailSubmission", None::<String>, Role::None)
.await
.unwrap()
.take_id();
let mailbox_id_2 = client
.mailbox_create("JMAP EmailSubmission 2", None::<String>, Role::None)
.await
.unwrap()
.take_id();
// Import an email without any recipients
let email_id = client
.email_import(
b"From: [email protected]\nSubject: hey\n\ntest".to_vec(),
[&mailbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
// Submission without a valid emailId or identityId should fail
assert!(matches!(
client
.email_submission_create(Id::new(123456).to_string(), &identity_id)
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
assert!(matches!(
client
.email_submission_create(&email_id, Id::new(123456).to_string())
.await,
Err(Error::Set(SetError {
type_: SetErrorType::InvalidProperties,
..
}))
));
// Submissions of e-mails without any recipients should fail
assert!(matches!(
client
.email_submission_create(&email_id, &identity_id)
.await,
Err(Error::Set(SetError {
type_: SetErrorType::NoRecipients,
..
}))
));
// Submissions with an envelope that does not match
// the identity from address should fail
assert!(matches!(
client
.email_submission_create_envelope(
&email_id,
&identity_id,
"[email protected]",
Vec::<Address>::new(),
)
.await,
Err(Error::Set(SetError {
type_: SetErrorType::ForbiddenFrom,
..
}))
));
// Submit a valid message submission
let email_body = concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Bcc: [email protected]\r\n",
"Subject: hey\r\n\r\n",
"test"
);
let email_id = client
.email_import(
email_body.as_bytes().to_vec(),
[&mailbox_id],
None::<Vec<&str>>,
None,
)
.await
.unwrap()
.take_id();
client
.email_submission_create(&email_id, &identity_id)
.await
.unwrap();
// Confirm that the message has been delivered
let email_body = email_body.replace("Bcc: [email protected]\r\n", "");
assert_message_delivery(
&mut smtp_rx,
MockMessage::new(
"<[email protected]>",
["<[email protected]>", "<[email protected]>"],
&email_body,
),
)
.await;
// Manually add recipients to the envelope and confirm submission
let email_submission_id = client
.email_submission_create_envelope(
&email_id,
&identity_id,
"[email protected]",
[
"[email protected]", // Should be de-duplicated
"[email protected]",
"[email protected] ",
" james@other_domain.com ", // Should be sanitized
" [email protected] ",
],
)
.await
.unwrap()
.take_id();
for _ in 0..3 {
let mut message = expect_message_delivery(&mut smtp_rx).await;
assert_eq!(message.mail_from, "<[email protected]>");
let rcpt_to = message.rcpt_to.pop().unwrap();
assert!(
[
"<james@other_domain.com>",
"<[email protected]>",
"<[email protected]>",
]
.contains(&rcpt_to.as_str())
);
assert!(
message.message.contains(&email_body),
"Got [{}], Expected[{}]",
message.message,
email_body
);
}
// Confirm that the email submission status was updated
tokio::time::sleep(Duration::from_millis(100)).await;
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(email_submission.undo_status().unwrap(), &UndoStatus::Final);
assert_eq!(
email_submission.delivery_status().unwrap(),
&AHashMap::from_iter([
(
"[email protected]".to_string(),
DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown)
),
(
"[email protected]".to_string(),
DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown)
),
(
"james@other_domain.com".to_string(),
DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown)
),
])
);
// SMTP rejects some of the recipients
let email_submission_id = client
.email_submission_create_envelope(
&email_id,
&identity_id,
"[email protected]",
[
"[email protected]",
"delay@other_domain.com",
"[email protected]",
"[email protected]",
],
)
.await
.unwrap()
.take_id();
assert_message_delivery(
&mut smtp_rx,
MockMessage::new("<[email protected]>", ["<[email protected]>"], &email_body),
)
.await;
expect_nothing(&mut smtp_rx).await;
// Verify SMTP replies
tokio::time::sleep(Duration::from_millis(100)).await;
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(
email_submission.undo_status().unwrap(),
&UndoStatus::Pending
);
assert_eq!(
email_submission.delivery_status().unwrap(),
&AHashMap::from_iter([
(
"[email protected]".to_string(),
DeliveryStatus::new(
"550 5.1.2 Mailbox does not exist.",
Delivered::No,
Displayed::Unknown
)
),
(
"delay@other_domain.com".to_string(),
DeliveryStatus::new(
"Code: 451, Enhanced code: 4.5.3, Message: Try again later.",
Delivered::Queued,
Displayed::Unknown
)
),
(
"[email protected]".to_string(),
DeliveryStatus::new(
"Code: 550, Enhanced code: 0.0.0, Message: I refuse to accept that recipient.",
Delivered::No,
Displayed::Unknown
)
),
(
"[email protected]".to_string(),
DeliveryStatus::new(
"Code: 250, Enhanced code: 0.0.0, Message: OK",
Delivered::Yes,
Displayed::Unknown
)
),
])
);
// Cancel submission
client
.email_submission_change_status(&email_submission_id, UndoStatus::Canceled)
.await
.unwrap();
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(
email_submission.undo_status().unwrap(),
&UndoStatus::Canceled
);
assert_eq!(
email_submission.delivery_status().unwrap(),
&AHashMap::from_iter([
(
"[email protected]".to_string(),
DeliveryStatus::new(
"550 5.1.2 Mailbox does not exist.",
Delivered::No,
Displayed::Unknown
)
),
(
"delay@other_domain.com".to_string(),
DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown)
),
(
"[email protected]".to_string(),
DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown)
),
(
"[email protected]".to_string(),
DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown)
),
])
);
// Confirm that the sendAt property is updated when using FUTURERELEASE
let hold_until_date = "2079-11-20T05:00:00Z";
let hold_until = DateTime::parse_rfc3339(hold_until_date)
.unwrap()
.to_timestamp();
let email_submission_id = client
.email_submission_create_envelope(
&email_id,
&identity_id,
Address::new("[email protected]")
.parameter("HOLDUNTIL", Some(hold_until_date.to_string())),
["[email protected]"],
)
.await
.unwrap()
.take_id();
tokio::time::sleep(Duration::from_millis(100)).await;
let email_submission = client
.email_submission_get(&email_submission_id, None)
.await
.unwrap()
.unwrap();
assert_eq!(email_submission.send_at().unwrap(), hold_until);
assert_eq!(
email_submission.undo_status().unwrap(),
&UndoStatus::Pending
);
assert_eq!(
email_submission.delivery_status().unwrap(),
&AHashMap::from_iter([(
"[email protected]".to_string(),
DeliveryStatus::new("250 2.1.5 Queued", Delivered::Queued, Displayed::Unknown)
),])
);
// Confirm that the query undoStatus filter agrees with EmailSubmission/get
assert!(
client
.email_submission_query(
Filter::undo_status(UndoStatus::Pending).into(),
None::<Vec<_>>
)
.await
.unwrap()
.take_ids()
.contains(&email_submission_id)
);
assert!(
!client
.email_submission_query(
Filter::undo_status(UndoStatus::Final).into(),
None::<Vec<_>>
)
.await
.unwrap()
.take_ids()
.contains(&email_submission_id)
);
// Verify onSuccessUpdateEmail action
let mut request = client.build();
let set_request = request.set_email_submission();
let create_id = set_request
.create()
.email_id(&email_id)
.identity_id(&identity_id)
.create_id()
.unwrap();
set_request
.arguments()
.on_success_update_email(&create_id)
.keyword("$draft", true)
.mailbox_id(&mailbox_id, false)
.mailbox_id(&mailbox_id_2, true);
request.send().await.unwrap().unwrap_method_responses();
assert_email_properties(&client, &email_id, &[&mailbox_id_2], &["$draft"]).await;
// Verify onSuccessDestroyEmail action
let mut request = client.build();
let set_request = request.set_email_submission();
let create_id = set_request
.create()
.email_id(&email_id)
.identity_id(&identity_id)
.create_id()
.unwrap();
set_request.arguments().on_success_destroy_email(&create_id);
request.send().await.unwrap().unwrap_method_responses();
assert!(
client
.email_get(&email_id, None::<Vec<_>>)
.await
.unwrap()
.is_none()
);
smtp_settings.lock().do_stop = true;
// Identities are created and destroyed as the account's addresses change
let account_id = account.id().document_id();
let account_cache = test.server.inner.cache.accounts.get(&account_id).unwrap();
let mut updated_cache = AccountCache::clone(&account_cache);
updated_cache.addresses = updated_cache
.addresses
.iter()
.cloned()
.chain([EmailAddress {
local_part: "jdoe.temp".into(),
domain_id: account_cache.addresses.first().unwrap().domain_id,
}])
.collect();
test.server
.inner
.cache
.accounts
.insert(account_id, Arc::new(updated_cache));
assert!(
identity_id_by_email(&client, "[email protected]")
.await
.is_some(),
"Identity was not created for the new address"
);
test.server
.inner
.cache
.accounts
.insert(account_id, account_cache);
assert!(
identity_id_by_email(&client, "[email protected]")
.await
.is_none(),
"Identity was not destroyed for the removed address"
);
assert!(
identity_id_by_email(&client, "[email protected]")
.await
.is_some(),
"Identity for a valid address was destroyed"
);
// Destroy the created mailbox, identity and all submissions
for identity_id in [
identity_id,
Id::from(1u64).to_string(),
Id::from(2u64).to_string(),
] {
client.identity_destroy(&identity_id).await.unwrap();
}
for id in client
.email_submission_query(None::<Filter>, None::<Vec<_>>)
.await
.unwrap()
.take_ids()
{
let _ = client
.email_submission_change_status(&id, UndoStatus::Canceled)
.await;
client.email_submission_destroy(&id).await.unwrap();
}
test.destroy_all_mailboxes(account).await;
let mut batch = BatchBuilder::new();
batch
.with_account_id(account.id().document_id())
.with_collection(Collection::Principal)
.with_document(0)
.clear(PrincipalField::IdentityAddresses);
test.server.commit_batch(batch).await.unwrap();
test.assert_is_empty().await;
}
pub fn spawn_mock_smtp_server() -> (mpsc::Receiver<MockMessage>, Arc<Mutex<MockSMTPSettings>>) {
// Create channels
let (event_tx, event_rx) = mpsc::channel::<MockMessage>(100);
let _settings = Arc::new(Mutex::new(MockSMTPSettings::default()));
let settings = _settings.clone();
// Start mock SMTP server
tokio::spawn(async move {
let listener = TcpListener::bind("127.0.0.1:9999")
.await
.unwrap_or_else(|e| {
panic!("Failed to bind mock SMTP server to 127.0.0.1:9999: {}", e);
});
while let Ok((mut stream, _)) = listener.accept().await {
let (rx, mut tx) = stream.split();
let mut rx = BufReader::new(rx);
let mut buf = String::with_capacity(128);
let mut message = MockMessage::default();
tx.write_all(b"220 [127.0.0.1] Clueless host service ready\r\n")
.await
.unwrap();
while rx.read_line(&mut buf).await.is_ok() {
//print!("-> {}", buf);
if buf.starts_with("EHLO") {
tx.write_all(b"250 Hi there, but I have no extensions to offer :-(\r\n")
.await
.unwrap();
} else if buf.starts_with("MAIL FROM") {
if settings.lock().fail_mail_from {
tx.write_all("552-I do not\r\n552 like that MAIL FROM.\r\n".as_bytes())
.await
.unwrap();
} else {
message.mail_from = buf.split_once(':').unwrap().1.trim().to_string();
tx.write_all(b"250 OK\r\n").await.unwrap();
}
} else if buf.starts_with("RCPT TO") {
if buf.contains("fail@") {
tx.write_all(
"550-I refuse to\r\n550 accept that recipient.\r\n".as_bytes(),
)
.await
.unwrap();
} else if buf.contains("delay@") {
tx.write_all("451 4.5.3 Try again later.\r\n".as_bytes())
.await
.unwrap();
} else {
message
.rcpt_to
.push(buf.split(':').nth(1).unwrap().trim().to_string());
tx.write_all(b"250 OK\r\n").await.unwrap();
}
} else if buf.starts_with("DATA") {
if settings.lock().fail_message {
tx.write_all(
"503-Thank you but I am\r\n503 saving myself for dessert.\r\n"
.as_bytes(),
)
.await
.unwrap();
} else if !message.mail_from.is_empty() && !message.rcpt_to.is_empty() {
tx.write_all(b"354 Start feeding me now some quality content please\r\n")
.await
.unwrap();
buf.clear();
while rx.read_line(&mut buf).await.is_ok() {
if buf.starts_with('.') && buf.len() < 4 {
message.message = message.message.trim().to_string();
break;
} else {
message.message += buf.as_str();
buf.clear();
}
}
tx.write_all(b"250 Great success!\r\n").await.unwrap();
message.rcpt_to.sort_unstable();
event_tx.send(message).await.unwrap();
message = MockMessage::default();
} else {
tx.write_all("554 You forgot to tell me a few things.\r\n".as_bytes())
.await
.unwrap();
}
} else if buf.starts_with("QUIT") {
tx.write_all("250 Arrivederci!\r\n".as_bytes())
.await
.unwrap();
break;
} else if buf.starts_with("RSET") {
tx.write_all("250 Your wish is my command.\r\n".as_bytes())
.await
.unwrap();
message = MockMessage::default();
} else {
println!("Unknown command: {}", buf.trim());
}
buf.clear();
}
if settings.lock().do_stop {
//println!("Mock SMTP server stopped.");
break;
}
}
});
(event_rx, _settings)
}
pub async fn expect_message_delivery(event_rx: &mut mpsc::Receiver<MockMessage>) -> MockMessage {
match tokio::time::timeout(Duration::from_millis(3000), event_rx.recv()).await {
Ok(Some(message)) => {
//println!("Got message [{}]", message.message);
message
}
result => {
panic!("Timeout waiting for message, got: {:?}", result);
}
}
}
pub async fn assert_message_delivery(
event_rx: &mut mpsc::Receiver<MockMessage>,
expected_message: MockMessage,
) {
let message = expect_message_delivery(event_rx).await;
assert_eq!(message.mail_from, expected_message.mail_from);
assert_eq!(message.rcpt_to, expected_message.rcpt_to);
if let Some(needle) = expected_message.message.strip_prefix('@') {
assert!(
message.message.contains(needle),
"[{}] needle = {:?}",
message.message,
needle
);
} else {
assert!(
message.message.contains(&expected_message.message),
"Got [{}], Expected[{}]",
message.message,
expected_message.message
);
}
}
pub async fn expect_nothing(event_rx: &mut mpsc::Receiver<MockMessage>) {
match tokio::time::timeout(Duration::from_millis(500), event_rx.recv()).await {
Err(_) => {}
message => {
panic!("Received a message when expecting nothing: {:?}", message);
}
}
}
async fn identity_id_by_email(client: &Client, email: &str) -> Option<String> {
let mut request = client.build();
request.get_identity();
request
.send_single::<IdentityGetResponse>()
.await
.unwrap()
.take_list()
.into_iter()
.find(|identity| identity.email() == Some(email))
.map(|mut identity| identity.take_id())
}
+52
View File
@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use jmap_client::mailbox::Role;
pub async fn test(test: &TestServer) {
println!("Running Email Thread tests...");
let account = test.account("[email protected]");
let client = account.jmap_client().await;
let mailbox_id = client
.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();
for num in [5, 3, 1, 2, 4] {
let mut email = client
.email_import(
format!("Subject: test\nReferences: <1234>\n\n{}", num).into_bytes(),
[&mailbox_id],
None::<Vec<String>>,
Some(10000i64 + num as i64),
)
.await
.unwrap();
thread_id = email.thread_id().unwrap().to_string();
expected_result[num - 1] = email.take_id();
}
test.wait_for_tasks().await;
assert_eq!(
client
.thread_get(&thread_id)
.await
.unwrap()
.unwrap()
.email_ids(),
expected_result
);
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
+864
View File
@@ -0,0 +1,864 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
store::deflate_test_resource,
utils::server::{DestroyAllMailboxes, TestServer},
};
use ::email::{
cache::MessageCacheFetch,
mailbox::INBOX_ID,
message::ingest::{EmailIngest, IngestEmail, IngestSource},
};
use common::auth::AccessToken;
use jmap_client::{email, mailbox::Role};
use mail_parser::{MessageParser, mailbox::mbox::MessageIterator};
use std::{io::Cursor, str::FromStr, time::Duration};
use store::{
ahash::AHashSet,
rand::{self, RngExt},
};
use types::id::Id;
pub async fn test(test: &TestServer) {
test_single_thread(test).await;
test_multi_thread(test).await;
}
async fn test_single_thread(test_server: &TestServer) {
println!("Running Email Merge Threads tests...");
let account = test_server.account("[email protected]");
let mut client = account.jmap_client().await;
let mut account_ids = Vec::new();
for name in [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
] {
account_ids.push(test_server.account(name).id_string());
}
for (test_group_num, test) in [test_1(), test_2(), test_3()].iter().enumerate() {
let mut messages = Vec::new();
let mut total_messages = 0;
let mut messages_per_thread =
build_messages(test, &mut messages, &mut total_messages, None, 0);
messages_per_thread.sort_unstable();
let mut mailbox_ids = Vec::with_capacity(6);
for account_id in &account_ids {
mailbox_ids.push(
client
.set_default_account_id(*account_id)
.mailbox_create("Thread nightmare", None::<String>, Role::None)
.await
.unwrap()
.take_id(),
);
}
for message in &messages {
client
.set_default_account_id(account_ids[0])
.email_import(
message.to_string().into_bytes(),
[mailbox_ids[0].clone()],
None::<Vec<String>>,
None,
)
.await
.unwrap();
}
for message in messages.iter().rev() {
client
.set_default_account_id(account_ids[1])
.email_import(
message.to_string().into_bytes(),
[mailbox_ids[1].clone()],
None::<Vec<String>>,
None,
)
.await
.unwrap();
}
for chunk in messages.chunks(5) {
client.set_default_account_id(account_ids[2]);
for message in chunk {
client
.email_import(
message.to_string().into_bytes(),
[mailbox_ids[2].clone()],
None::<Vec<String>>,
None,
)
.await
.unwrap();
}
client.set_default_account_id(account_ids[3]);
for message in chunk.iter().rev() {
client
.email_import(
message.to_string().into_bytes(),
[mailbox_ids[3].clone()],
None::<Vec<String>>,
None,
)
.await
.unwrap();
}
}
for chunk in messages.chunks(5).rev() {
client.set_default_account_id(account_ids[4]);
for message in chunk {
client
.email_import(
message.to_string().into_bytes(),
[mailbox_ids[4].clone()],
None::<Vec<String>>,
None,
)
.await
.unwrap();
}
client.set_default_account_id(account_ids[5]);
for message in chunk.iter().rev() {
client
.email_import(
message.to_string().into_bytes(),
[mailbox_ids[5].clone()],
None::<Vec<String>>,
None,
)
.await
.unwrap();
}
}
test_server.wait_for_tasks().await;
for test_num in 0..=5 {
let result = client
.set_default_account_id(account_ids[test_num])
.email_query(
email::query::Filter::in_mailbox(mailbox_ids[test_num].clone()).into(),
None::<Vec<_>>,
)
.await
.unwrap();
assert_eq!(
result.ids().len(),
total_messages,
"test# {}/{}",
test_group_num,
test_num
);
let thread_ids: AHashSet<u32> = result
.ids()
.iter()
.map(|id| Id::from_str(id).unwrap().prefix_id())
.collect();
let mut messages_per_thread_db = Vec::new();
for thread_id in thread_ids {
messages_per_thread_db.push(
client
.thread_get(&Id::new(thread_id as u64).to_string())
.await
.unwrap()
.unwrap()
.email_ids()
.len(),
);
}
messages_per_thread_db.sort_unstable();
assert_eq!(messages_per_thread_db, messages_per_thread);
println!("passed test# {}/{}", test_group_num, test_num);
}
for account_id in &account_ids {
client
.set_default_account_id(*account_id)
.destroy_all_mailboxes()
.await;
}
test_server.wait_for_tasks().await;
test_server.assert_is_empty().await;
}
test_server.assert_is_empty().await;
}
#[allow(dead_code)]
async fn test_multi_thread(test: &TestServer) {
println!("Running Email Merge Threads tests (multi-threaded)...");
let mut handles = vec![];
let account = test.account("[email protected]");
let account_id = account.id().document_id();
let mailbox_id = INBOX_ID;
for message in MessageIterator::new(Cursor::new(deflate_test_resource("mailbox.gz")))
.collect::<Vec<_>>()
.into_iter()
{
let message = message.unwrap();
let server = test.server.clone();
handles.push(tokio::task::spawn(async move {
let mut retry_count = 0;
loop {
match server
.email_ingest(IngestEmail {
raw_message: message.contents(),
message: MessageParser::new().parse(message.contents()),
blob_hash: None,
access_token: &AccessToken::from_id_maybe_invalid(account_id),
mailbox_ids: vec![mailbox_id],
keywords: vec![],
received_at: None,
source: IngestSource::Smtp {
deliver_to: "[email protected]",
is_sender_authenticated: true,
is_spam: false,
},
session_id: 0,
})
.await
{
Ok(_) => break,
Err(err) => {
if err.is_assertion_failure() && retry_count < 10 {
//println!("Retrying ingest for {}...", message.from());
let backoff = rand::rng().random_range(50..=300);
tokio::time::sleep(Duration::from_millis(backoff)).await;
retry_count += 1;
continue;
}
panic!("Failed to ingest message: {:?}", err);
}
}
}
}));
}
// Wait for all tasks to complete
let messages = handles.len();
println!("Waiting for {} tasks to complete...", messages);
for handle in handles {
handle.await.expect("Task panicked");
}
assert_eq!(
messages,
test.server
.get_cached_messages(account_id)
.await
.unwrap()
.emails
.items
.len(),
);
println!("Deleting all messages...");
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}
fn build_message(message: usize, in_reply_to: Option<usize>, thread_num: usize) -> String {
if let Some(in_reply_to) = in_reply_to {
format!(
"Message-ID: <{}>\nReferences: <{}>\nSubject: re: T{}\n\nreply\n",
message, in_reply_to, thread_num
)
} else {
format!(
"Message-ID: <{}>\nSubject: T{}\n\nmsg\n",
message, thread_num
)
}
}
fn build_messages(
three: &ThreadTest,
messages: &mut Vec<String>,
total_messages: &mut usize,
in_reply_to: Option<usize>,
thread_num: usize,
) -> Vec<usize> {
let mut messages_per_thread = Vec::new();
match three {
ThreadTest::Message => {
*total_messages += 1;
messages.push(build_message(*total_messages, in_reply_to, thread_num));
}
ThreadTest::MessageWithReplies(replies) => {
*total_messages += 1;
messages.push(build_message(*total_messages, in_reply_to, thread_num));
let in_reply_to = Some(*total_messages);
for reply in replies {
build_messages(reply, messages, total_messages, in_reply_to, thread_num);
}
}
ThreadTest::Root(items) => {
for (thread_num, item) in items.iter().enumerate() {
let count_start = *total_messages;
build_messages(item, messages, total_messages, None, thread_num);
messages_per_thread.push(*total_messages - count_start);
}
}
}
messages_per_thread
}
pub fn build_thread_test_messages() -> Vec<String> {
let mut messages = Vec::new();
let mut total_messages = 0;
build_messages(&test_3(), &mut messages, &mut total_messages, None, 0);
messages
}
pub enum ThreadTest {
Message,
MessageWithReplies(Vec<ThreadTest>),
Root(Vec<ThreadTest>),
}
fn test_1() -> ThreadTest {
ThreadTest::Root(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
]),
]),
]),
]),
]),
]),
]),
])
}
fn test_2() -> ThreadTest {
ThreadTest::Root(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(
vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
]),
ThreadTest::Message,
]),
]),
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
]),
],
)]),
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
]),
]),
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
]),
]),
ThreadTest::Message,
ThreadTest::Message,
])]),
]),
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
]),
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
]),
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::Message,
ThreadTest::Message,
]),
]),
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::Message, ThreadTest::Message]),
])
}
fn test_3() -> ThreadTest {
ThreadTest::Root(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::Message, ThreadTest::Message]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(
vec![ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
])],
)]),
ThreadTest::Message,
ThreadTest::Message,
])]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
]),
]),
]),
]),
]),
ThreadTest::Message,
ThreadTest::Message,
])]),
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
])]),
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::Message, ThreadTest::Message]),
ThreadTest::Message,
ThreadTest::Message,
])]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(
vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::Message,
],
)]),
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(
vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
],
)]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
]),
])]),
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![ThreadTest::Message]),
ThreadTest::Message,
]),
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
]),
]),
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::Message,
]),
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![
ThreadTest::Message,
ThreadTest::MessageWithReplies(vec![ThreadTest::MessageWithReplies(
vec![ThreadTest::Message, ThreadTest::Message],
)]),
ThreadTest::Message,
]),
ThreadTest::Message,
]),
]),
]),
]),
])
}
+166
View File
@@ -0,0 +1,166 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
jmap::mail::submission::{
MockMessage, assert_message_delivery, expect_nothing, spawn_mock_smtp_server,
},
utils::{dns::DnsCache, server::TestServer, smtp::SmtpConnection},
};
use chrono::{TimeDelta, Utc};
use std::time::Instant;
pub async fn test(test: &TestServer) {
println!("Running Vacation Response tests...");
// Create test account
let server = test.server.clone();
let account = test.account("[email protected]");
let client = account.jmap_client().await;
// Start mock SMTP server
let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server();
server.ipv4_add(
"localhost",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + std::time::Duration::from_secs(10),
);
// Let people know that we'll be down in Kokomo
client
.vacation_response_enable(
"Off the Florida Keys there's a place called Kokomo",
"That's where you wanna go to get away from it all".into(),
"That's where <b>you wanna go</b> to get away from it all".into(),
)
.await
.unwrap();
// Connect to LMTP service
let mut lmtp = SmtpConnection::connect().await;
// Send a message
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report\r\n",
"\r\n",
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great."
),
)
.await;
// Await vacation response
assert_message_delivery(
&mut smtp_rx,
MockMessage::new("<[email protected]>", ["<[email protected]>"], "@Kokomo"),
)
.await;
// Further messages from the same recipient should not
// trigger a vacation response
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: TPS Report -- friendly reminder\r\n",
"\r\n",
"Listen, are you gonna have those TPS reports for us this afternoon?",
),
)
.await;
expect_nothing(&mut smtp_rx).await;
// Messages from MAILER-DAEMON should not
// trigger a vacation response
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: Delivery Failure\r\n",
"\r\n",
"I tried so hard and got so far but in the end it wasn't delivered.",
),
)
.await;
expect_nothing(&mut smtp_rx).await;
// Vacation responses should honor the configured date ranges
client
.vacation_response_set_dates(
(Utc::now() + TimeDelta::try_days(1).unwrap_or_default())
.timestamp()
.into(),
None,
)
.await
.unwrap();
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: When were you going on holidays?\r\n",
"\r\n",
"I'm asking because Bill really wants those TPS reports.",
),
)
.await;
expect_nothing(&mut smtp_rx).await;
client
.vacation_response_set_dates(
(Utc::now() - TimeDelta::try_days(1).unwrap_or_default())
.timestamp()
.into(),
None,
)
.await
.unwrap();
smtp_settings.lock().do_stop = true;
lmtp.ingest(
"[email protected]",
&["[email protected]"],
concat!(
"From: [email protected]\r\n",
"To: [email protected]\r\n",
"Subject: When were you going on holidays?\r\n",
"\r\n",
"I'm asking because Bill really wants those TPS reports.",
),
)
.await;
lmtp.quit().await;
assert_message_delivery(
&mut smtp_rx,
MockMessage::new("<[email protected]>", ["<[email protected]>"], "@Kokomo"),
)
.await;
// Remove test data
client.vacation_response_disable().await.unwrap();
client.sieve_script_deactivate().await.unwrap();
let mut request = client.build();
request.query_sieve_script();
for id in request.send_query_sieve_script().await.unwrap().take_ids() {
client.sieve_script_destroy(&id).await.unwrap();
}
test.destroy_all_mailboxes(account).await;
test.assert_is_empty().await;
}