/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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, message::metadata::MessageData}; 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, str::FromStr}; use store::{ ValueKey, write::{AlignedBytes, Archive}, }; use types::{collection::Collection, id::Id}; pub async fn test(test: &TestServer) { println!("Running Email Set tests..."); let account = test.account("jdoe@example.com"); 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; update_preserves_uids(test, &client, account.id().document_id(), &mailbox_id).await; test.destroy_all_mailboxes(account).await; test.account("admin@example.com") .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::>(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::>, ) .await .unwrap(); // Create two test mailboxes let test_mailbox1_id = client .mailbox_create("Test 1", None::, Role::None) .await .unwrap() .take_id(); let test_mailbox2_id = client .mailbox_create("Test 2", None::, 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(); } async fn update_preserves_uids( test: &TestServer, client: &Client, account_id: u32, root_mailbox_id: &str, ) { let email_id = client .email_query( email::query::Filter::in_mailbox(root_mailbox_id).into(), None::>, ) .await .unwrap() .take_ids() .pop() .unwrap(); let document_id = Id::from_str(&email_id).unwrap().document_id(); let test_mailbox_id = client .mailbox_create("UID Test", None::, Role::None) .await .unwrap() .take_id(); let test_mailbox_document_id = Id::from_str(&test_mailbox_id).unwrap().document_id(); let uids = message_uids(test, account_id, document_id).await; let inbox_uid = uids[&INBOX_ID]; assert_ne!(inbox_uid, 0); // Full mailboxIds identical to the current ones plus a keyword change must keep the UID let mut request = client.build(); request .set_email() .update(&email_id) .mailbox_ids([root_mailbox_id]) .keywords(["uid-test"]); request .send_set_email() .await .unwrap() .updated(&email_id) .unwrap(); assert_eq!( message_uids(test, account_id, document_id).await, [(INBOX_ID, inbox_uid)].into_iter().collect() ); // Full mailboxIds that keeps a mailbox and adds another must only assign a UID to the new one let mut request = client.build(); request .set_email() .update(&email_id) .mailbox_ids([root_mailbox_id, test_mailbox_id.as_str()]); request .send_set_email() .await .unwrap() .updated(&email_id) .unwrap(); let uids = message_uids(test, account_id, document_id).await; assert_eq!(uids.len(), 2); assert_eq!(uids[&INBOX_ID], inbox_uid); assert_ne!(uids[&test_mailbox_document_id], 0); client.mailbox_destroy(&test_mailbox_id, true).await.unwrap(); } async fn message_uids( test: &TestServer, account_id: u32, document_id: u32, ) -> std::collections::BTreeMap { test.server .store() .get_value::>(ValueKey::archive( account_id, Collection::Email, document_id, )) .await .unwrap() .unwrap() .deserialize::() .unwrap() .mailboxes .iter() .map(|m| (m.mailbox_id, m.uid)) .collect() } 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::>(), result .mailbox_ids() .iter() .copied() .collect::>() ); assert_eq!( keywords.iter().copied().collect::>(), result.keywords().iter().copied().collect::>() ); }