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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}]
|
||||
}]
|
||||
}
|
||||
]
|
||||
"#;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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!(
|
||||
"<"<mark>孫</mark><mark>子</mark><mark>兵法</mark>:"> ",
|
||||
"<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; ",
|
||||
""wir leben", sprach ein Redner, "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;
|
||||
}
|
||||
@@ -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<_>>()
|
||||
);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user