diff --git a/src/chat.rs b/src/chat.rs index 314c930cd..16431b985 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4783,3371 +4783,4 @@ impl Context { } #[cfg(test)] -mod tests { - use super::*; - use crate::chatlist::get_archived_cnt; - use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; - use crate::headerdef::HeaderDef; - use crate::message::{delete_msgs, MessengerMessage}; - use crate::receive_imf::receive_imf; - use crate::test_utils::{sync, TestContext, TestContextManager, TimeShiftFalsePositiveNote}; - use strum::IntoEnumIterator; - use tokio::fs; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_info() { - let t = TestContext::new().await; - let chat = t.create_chat_with_contact("bob", "bob@example.com").await; - let info = chat.get_info(&t).await.unwrap(); - - // Ensure we can serialize this. - println!("{}", serde_json::to_string_pretty(&info).unwrap()); - - let expected = r#" - { - "id": 10, - "type": 100, - "name": "bob", - "archived": false, - "param": "", - "gossiped_timestamp": 0, - "is_sending_locations": false, - "color": 35391, - "profile_image": "", - "draft": "", - "is_muted": false, - "ephemeral_timer": "Disabled" - } - "#; - - // Ensure we can deserialize this. - let loaded: ChatInfo = serde_json::from_str(expected).unwrap(); - assert_eq!(info, loaded); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_draft_no_draft() { - let t = TestContext::new().await; - let chat = t.get_self_chat().await; - let draft = chat.id.get_draft(&t).await.unwrap(); - assert!(draft.is_none()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_draft_special_chat_id() { - let t = TestContext::new().await; - let draft = DC_CHAT_ID_LAST_SPECIAL.get_draft(&t).await.unwrap(); - assert!(draft.is_none()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_draft_no_chat() { - // This is a weird case, maybe this should be an error but we - // do not get this info from the database currently. - let t = TestContext::new().await; - let draft = ChatId::new(42).get_draft(&t).await.unwrap(); - assert!(draft.is_none()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_draft() { - let t = TestContext::new().await; - let chat_id = &t.get_self_chat().await.id; - let mut msg = Message::new_text("hello".to_string()); - - chat_id.set_draft(&t, Some(&mut msg)).await.unwrap(); - let draft = chat_id.get_draft(&t).await.unwrap().unwrap(); - let msg_text = msg.get_text(); - let draft_text = draft.get_text(); - assert_eq!(msg_text, draft_text); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_draft() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?; - - let mut msg = Message::new_text("hi!".to_string()); - chat_id.set_draft(&t, Some(&mut msg)).await?; - assert!(chat_id.get_draft(&t).await?.is_some()); - - let mut msg = Message::new_text("another".to_string()); - chat_id.set_draft(&t, Some(&mut msg)).await?; - assert!(chat_id.get_draft(&t).await?.is_some()); - - chat_id.set_draft(&t, None).await?; - assert!(chat_id.get_draft(&t).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_forwarding_draft_failing() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = &t.get_self_chat().await.id; - let mut msg = Message::new_text("hello".to_string()); - chat_id.set_draft(&t, Some(&mut msg)).await?; - assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id); - - let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - assert!(forward_msgs(&t, &[msg.id], chat_id2).await.is_err()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_draft_stable_ids() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = &t.get_self_chat().await.id; - let mut msg = Message::new_text("hello".to_string()); - assert_eq!(msg.id, MsgId::new_unset()); - assert!(chat_id.get_draft_msg_id(&t).await?.is_none()); - - chat_id.set_draft(&t, Some(&mut msg)).await?; - let id_after_1st_set = msg.id; - assert_ne!(id_after_1st_set, MsgId::new_unset()); - assert_eq!( - id_after_1st_set, - chat_id.get_draft_msg_id(&t).await?.unwrap() - ); - assert_eq!(id_after_1st_set, chat_id.get_draft(&t).await?.unwrap().id); - - msg.set_text("hello2".to_string()); - chat_id.set_draft(&t, Some(&mut msg)).await?; - let id_after_2nd_set = msg.id; - - assert_eq!(id_after_2nd_set, id_after_1st_set); - assert_eq!( - id_after_2nd_set, - chat_id.get_draft_msg_id(&t).await?.unwrap() - ); - let test = chat_id.get_draft(&t).await?.unwrap(); - assert_eq!(id_after_2nd_set, test.id); - assert_eq!(id_after_2nd_set, msg.id); - assert_eq!(test.text, "hello2".to_string()); - assert_eq!(test.state, MessageState::OutDraft); - - let id_after_send = send_msg(&t, *chat_id, &mut msg).await?; - assert_eq!(id_after_send, id_after_1st_set); - - let test = Message::load_from_db(&t, id_after_send).await?; - assert!(!test.hidden); // sent draft must no longer be hidden - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_only_one_draft_per_chat() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?; - - let msgs: Vec = (1..=1000) - .map(|i| Message::new_text(i.to_string())) - .collect(); - let mut tasks = Vec::new(); - for mut msg in msgs { - let ctx = t.clone(); - let task = tokio::spawn(async move { - let ctx = ctx; - chat_id.set_draft(&ctx, Some(&mut msg)).await - }); - tasks.push(task); - } - futures::future::join_all(tasks.into_iter()).await; - - assert!(chat_id.get_draft(&t).await?.is_some()); - - chat_id.set_draft(&t, None).await?; - assert!(chat_id.get_draft(&t).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_change_quotes_on_reused_message_object() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; - let quote1 = - Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote1".to_string()).await?) - .await?; - let quote2 = - Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote2".to_string()).await?) - .await?; - - // save a draft - let mut draft = Message::new_text("draft text".to_string()); - chat_id.set_draft(&t, Some(&mut draft)).await?; - - let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, "draft text".to_string()); - assert!(test.quoted_text().is_none()); - assert!(test.quoted_message(&t).await?.is_none()); - - // add quote to same message object - draft.set_quote(&t, Some("e1)).await?; - chat_id.set_draft(&t, Some(&mut draft)).await?; - - let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, "draft text".to_string()); - assert_eq!(test.quoted_text(), Some("quote1".to_string())); - assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id); - - // change quote on same message object - draft.set_text("another draft text".to_string()); - draft.set_quote(&t, Some("e2)).await?; - chat_id.set_draft(&t, Some(&mut draft)).await?; - - let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, "another draft text".to_string()); - assert_eq!(test.quoted_text(), Some("quote2".to_string())); - assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id); - - // remove quote on same message object - draft.set_quote(&t, None).await?; - chat_id.set_draft(&t, Some(&mut draft)).await?; - - let test = Message::load_from_db(&t, draft.id).await?; - assert_eq!(test.text, "another draft text".to_string()); - assert!(test.quoted_text().is_none()); - assert!(test.quoted_message(&t).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_quote_replies() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?; - let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?; - - let one2one_chat_id = alice.create_chat(&bob).await.id; - let one2one_msg_id = send_text_msg(&alice, one2one_chat_id, "foo".to_string()).await?; - let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?; - - // quoting messages in same chat is okay - let mut msg = Message::new_text("baz".to_string()); - msg.set_quote(&alice, Some(&grp_msg)).await?; - let result = send_msg(&alice, grp_chat_id, &mut msg).await; - assert!(result.is_ok()); - - let mut msg = Message::new_text("baz".to_string()); - msg.set_quote(&alice, Some(&one2one_msg)).await?; - let result = send_msg(&alice, one2one_chat_id, &mut msg).await; - assert!(result.is_ok()); - let one2one_quote_reply_msg_id = result.unwrap(); - - // quoting messages from groups to one-to-ones is okay ("reply privately") - let mut msg = Message::new_text("baz".to_string()); - msg.set_quote(&alice, Some(&grp_msg)).await?; - let result = send_msg(&alice, one2one_chat_id, &mut msg).await; - assert!(result.is_ok()); - - // quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ... - let mut msg = Message::new_text("baz".to_string()); - msg.set_quote(&alice, Some(&one2one_msg)).await?; - let result = send_msg(&alice, grp_chat_id, &mut msg).await; - assert!(result.is_err()); - - // ... but forwarding messages with quotes is allowed - let result = forward_msgs(&alice, &[one2one_quote_reply_msg_id], grp_chat_id).await; - assert!(result.is_ok()); - - // ... and bots are not restricted - alice.set_config(Config::Bot, Some("1")).await?; - let result = send_msg(&alice, grp_chat_id, &mut msg).await; - assert!(result.is_ok()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_contact_to_chat_ex_add_self() { - // Adding self to a contact should succeed, even though it's pointless. - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); - let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false) - .await - .unwrap(); - assert_eq!(added, false); - } - - /// Test adding and removing members in a group chat. - /// - /// Make sure messages sent outside contain authname - /// and displayed messages contain locally set name. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_member_add_remove() -> Result<()> { - let mut tcm = TestContextManager::new(); - - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - // Disable encryption so we can inspect raw message contents. - alice.set_config(Config::E2eeEnabled, Some("0")).await?; - bob.set_config(Config::E2eeEnabled, Some("0")).await?; - - // Create contact for Bob on the Alice side with name "robert". - let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?; - - // Set Bob authname to "Bob" and send it to Alice. - bob.set_config(Config::Displayname, Some("Bob")).await?; - tcm.send_recv(&bob, &alice, "Hello!").await; - - // Check that Alice has Bob's name set to "robert" and authname set to "Bob". - { - let alice_bob_contact = Contact::get_by_id(&alice, alice_bob_contact_id).await?; - assert_eq!(alice_bob_contact.get_name(), "robert"); - - // This is the name that will be sent outside. - assert_eq!(alice_bob_contact.get_authname(), "Bob"); - - assert_eq!(alice_bob_contact.get_display_name(), "robert"); - } - - // Create and promote a group. - let alice_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; - let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; - add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; - let sent = alice - .send_text(alice_chat_id, "Hi! I created a group.") - .await; - assert!(sent.payload.contains("Hi! I created a group.")); - - // Alice adds Bob to the chat. - add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; - let sent = alice.pop_sent_msg().await; - assert!(sent - .payload - .contains("I added member Bob (bob@example.net).")); - // Locally set name "robert" should not leak. - assert!(!sent.payload.contains("robert")); - assert_eq!( - sent.load_from_db().await.get_text(), - "You added member robert (bob@example.net)." - ); - - // Alice removes Bob from the chat. - remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; - let sent = alice.pop_sent_msg().await; - assert!(sent - .payload - .contains("I removed member Bob (bob@example.net).")); - assert!(!sent.payload.contains("robert")); - assert_eq!( - sent.load_from_db().await.get_text(), - "You removed member robert (bob@example.net)." - ); - - // Alice leaves the chat. - remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?; - let sent = alice.pop_sent_msg().await; - assert!(sent.payload.contains("I left the group.")); - assert_eq!(sent.load_from_db().await.get_text(), "You left the group."); - - Ok(()) - } - - /// Test parallel removal of user from the chat and leaving the group. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parallel_member_remove() -> Result<()> { - let mut tcm = TestContextManager::new(); - - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - alice.set_config(Config::E2eeEnabled, Some("0")).await?; - bob.set_config(Config::E2eeEnabled, Some("0")).await?; - - let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; - let alice_claire_contact_id = - Contact::create(&alice, "Claire", "claire@example.net").await?; - - // Create and promote a group. - let alice_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; - add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; - add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; - let alice_sent_msg = alice - .send_text(alice_chat_id, "Hi! I created a group.") - .await; - let bob_received_msg = bob.recv_msg(&alice_sent_msg).await; - - let bob_chat_id = bob_received_msg.get_chat_id(); - bob_chat_id.accept(&bob).await?; - - // Alice adds Claire to the chat. - add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?; - let alice_sent_add_msg = alice.pop_sent_msg().await; - - // Bob leaves the chat. - remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; - bob.pop_sent_msg().await; - - // Bob receives a msg about Alice adding Claire to the group. - bob.recv_msg(&alice_sent_add_msg).await; - - SystemTime::shift(Duration::from_secs(3600)); - - // Alice sends a message to Bob because the message about leaving is lost. - let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await; - bob.recv_msg(&alice_sent_msg).await; - - bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove") - .await; - - // Alice removes Bob from the chat. - remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; - let alice_sent_remove_msg = alice.pop_sent_msg().await; - - // Bob receives a msg about Alice removing him from the group. - let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await; - - // Test that remove message is rewritten. - assert_eq!( - bob_received_remove_msg.get_text(), - "Member Me (bob@example.net) removed by alice@example.org." - ); - - Ok(()) - } - - /// Test that member removal is synchronized eventually even if the message is lost. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_msg_with_implicit_member_removed() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let alice_bob_contact_id = - Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?; - let fiona_addr = "fiona@example.net"; - let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?; - let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?; - let alice_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; - add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; - let sent_msg = alice.send_text(alice_chat_id, "I created a group").await; - let bob_received_msg = bob.recv_msg(&sent_msg).await; - let bob_chat_id = bob_received_msg.get_chat_id(); - bob_chat_id.accept(&bob).await?; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); - - add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; - let sent_msg = alice.pop_sent_msg().await; - bob.recv_msg(&sent_msg).await; - - // Bob removed Fiona, but the message is lost. - remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?; - bob.pop_sent_msg().await; - - // This doesn't add Fiona back because Bob just removed them. - let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await; - bob.recv_msg(&sent_msg).await; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); - - // Even after some time Fiona is not added back. - SystemTime::shift(Duration::from_secs(3600)); - let sent_msg = alice.send_text(alice_chat_id, "Welcome back, Fiona!").await; - bob.recv_msg(&sent_msg).await; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); - - // If Bob sends a message to Alice now, Fiona is removed. - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3); - let sent_msg = bob - .send_text(alice_chat_id, "I have removed Fiona some time ago.") - .await; - alice.recv_msg(&sent_msg).await; - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_modify_chat_multi_device() -> Result<()> { - let a1 = TestContext::new_alice().await; - let a2 = TestContext::new_alice().await; - a1.set_config_bool(Config::BccSelf, true).await?; - - // create group and sync it to the second device - let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?; - let sent = a1.send_text(a1_chat_id, "ho!").await; - let a1_msg = a1.get_last_msg().await; - let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?; - - let a2_msg = a2.recv_msg(&sent).await; - let a2_chat_id = a2_msg.chat_id; - let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?; - - assert!(!a1_msg.is_system_message()); - assert!(!a2_msg.is_system_message()); - assert_eq!(a1_chat.grpid, a2_chat.grpid); - assert_eq!(a1_chat.name, "foo"); - assert_eq!(a2_chat.name, "foo"); - assert_eq!(a1_chat.get_profile_image(&a1).await?, None); - assert_eq!(a2_chat.get_profile_image(&a2).await?, None); - assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 1); - assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1); - - // add a member to the group - let bob = Contact::create(&a1, "", "bob@example.org").await?; - add_contact_to_chat(&a1, a1_chat_id, bob).await?; - let a1_msg = a1.get_last_msg().await; - - let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; - - assert!(a1_msg.is_system_message()); - assert!(a2_msg.is_system_message()); - assert_eq!(a1_msg.get_info_type(), SystemMessage::MemberAddedToGroup); - assert_eq!(a2_msg.get_info_type(), SystemMessage::MemberAddedToGroup); - assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 2); - assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 2); - assert_eq!(get_past_chat_contacts(&a1, a1_chat_id).await?.len(), 0); - assert_eq!(get_past_chat_contacts(&a2, a2_chat_id).await?.len(), 0); - - // rename the group - set_chat_name(&a1, a1_chat_id, "bar").await?; - let a1_msg = a1.get_last_msg().await; - - let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; - - assert!(a1_msg.is_system_message()); - assert!(a2_msg.is_system_message()); - assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged); - assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged); - assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar"); - assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar"); - - // remove member from group - remove_contact_from_chat(&a1, a1_chat_id, bob).await?; - let a1_msg = a1.get_last_msg().await; - - let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; - - assert!(a1_msg.is_system_message()); - assert!(a2_msg.is_system_message()); - assert_eq!( - a1_msg.get_info_type(), - SystemMessage::MemberRemovedFromGroup - ); - assert_eq!( - a2_msg.get_info_type(), - SystemMessage::MemberRemovedFromGroup - ); - assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 1); - assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1); - assert_eq!(get_past_chat_contacts(&a1, a1_chat_id).await?.len(), 1); - assert_eq!(get_past_chat_contacts(&a2, a2_chat_id).await?.len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_modify_chat_disordered() -> Result<()> { - let _n = TimeShiftFalsePositiveNote; - - // Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy - // (time shift is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then) - let alice = TestContext::new_alice().await; - - let bob_id = Contact::create(&alice, "", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "", "claire@foo.de").await?; - let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?; - - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; - - add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; - let add1 = alice.pop_sent_msg().await; - - add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; - let add2 = alice.pop_sent_msg().await; - SystemTime::shift(Duration::from_millis(1100)); - - add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?; - let add3 = alice.pop_sent_msg().await; - SystemTime::shift(Duration::from_millis(1100)); - - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4); - - remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; - let remove1 = alice.pop_sent_msg().await; - SystemTime::shift(Duration::from_millis(1100)); - - remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?; - let remove2 = alice.pop_sent_msg().await; - - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); - - // Bob receives the add and deletion messages out of order - let bob = TestContext::new_bob().await; - bob.recv_msg(&add1).await; - let bob_chat_id = bob.recv_msg(&add3).await.chat_id; - bob.recv_msg_trash(&add2).await; // No-op addition message is trashed. - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4); - - bob.recv_msg(&remove2).await; - bob.recv_msg(&remove1).await; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); - - Ok(()) - } - - /// Tests that if member added message is completely lost, - /// member is eventually added. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_lost_member_added() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob]) - .await; - let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; - let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id; - assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); - - // Attempt to add member, but message is lost. - let claire_id = Contact::create(alice, "", "claire@foo.de").await?; - add_contact_to_chat(alice, alice_chat_id, claire_id).await?; - alice.pop_sent_msg().await; - - let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await; - bob.recv_msg(&alice_sent).await; - assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3); - - Ok(()) - } - - /// Test that group updates are robust to lost messages and eventual out of order arrival. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_modify_chat_lost() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - - let bob_id = Contact::create(&alice, "", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "", "claire@foo.de").await?; - let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?; - - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; - add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; - add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?; - - send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; - let add = alice.pop_sent_msg().await; - SystemTime::shift(Duration::from_millis(1100)); - - remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; - let remove1 = alice.pop_sent_msg().await; - SystemTime::shift(Duration::from_millis(1100)); - - remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?; - let remove2 = alice.pop_sent_msg().await; - - let bob = tcm.bob().await; - - bob.recv_msg(&add).await; - let bob_chat_id = bob.get_last_msg().await.chat_id; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4); - - // First removal message is lost. - // Nevertheless, two members are removed. - bob.recv_msg(&remove2).await; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); - - // Eventually, first removal message arrives. - // This has no effect. - bob.recv_msg(&remove1).await; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_leave_group() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Create group chat with Bob. - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - let bob_contact = Contact::create(&alice, "", "bob@example.net").await?; - add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?; - - // Alice sends first message to group. - let sent_msg = alice.send_text(alice_chat_id, "Hello!").await; - let bob_msg = bob.recv_msg(&sent_msg).await; - - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); - - // Bob leaves the group. - let bob_chat_id = bob_msg.chat_id; - bob_chat_id.accept(&bob).await?; - remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; - - let leave_msg = bob.pop_sent_msg().await; - alice.recv_msg(&leave_msg).await; - - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1); - - Ok(()) - } - - /// Test that adding or removing contacts in 1:1 chat is not allowed. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_remove_contact_for_single() { - let ctx = TestContext::new_alice().await; - let bob = Contact::create(&ctx, "", "bob@f.br").await.unwrap(); - let chat_id = ChatId::create_for_contact(&ctx, bob).await.unwrap(); - let chat = Chat::load_from_db(&ctx, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); - - // adding or removing contacts from one-to-one-chats result in an error - let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap(); - let added = add_contact_to_chat_ex(&ctx, Nosync, chat.id, claire, false).await; - assert!(added.is_err()); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); - - let removed = remove_contact_from_chat(&ctx, chat.id, claire).await; - assert!(removed.is_err()); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); - - let removed = remove_contact_from_chat(&ctx, chat.id, ContactId::SELF).await; - assert!(removed.is_err()); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_self_talk() -> Result<()> { - let t = TestContext::new_alice().await; - let chat = &t.get_self_chat().await; - assert!(!chat.id.is_special()); - assert!(chat.is_self_talk()); - assert!(chat.visibility == ChatVisibility::Normal); - assert!(!chat.is_device_talk()); - assert!(chat.can_send(&t).await?); - assert_eq!(chat.name, stock_str::saved_messages(&t).await); - assert!(chat.get_profile_image(&t).await?.is_some()); - - let msg_id = send_text_msg(&t, chat.id, "foo self".to_string()).await?; - let msg = Message::load_from_db(&t, msg_id).await?; - assert_eq!(msg.from_id, ContactId::SELF); - assert_eq!(msg.to_id, ContactId::SELF); - assert!(msg.get_showpadlock()); - - let sent_msg = t.pop_sent_msg().await; - let t2 = TestContext::new_alice().await; - t2.recv_msg(&sent_msg).await; - let chat = &t2.get_self_chat().await; - let msg = t2.get_last_msg_in(chat.id).await; - assert_eq!(msg.text, "foo self".to_string()); - assert_eq!(msg.from_id, ContactId::SELF); - assert_eq!(msg.to_id, ContactId::SELF); - assert!(msg.get_showpadlock()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_device_msg_unlabelled() { - let t = TestContext::new().await; - - // add two device-messages - let mut msg1 = Message::new_text("first message".to_string()); - let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await; - assert!(msg1_id.is_ok()); - - let mut msg2 = Message::new_text("second message".to_string()); - let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await; - assert!(msg2_id.is_ok()); - assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); - - // check added messages - let msg1 = message::Message::load_from_db(&t, msg1_id.unwrap()).await; - assert!(msg1.is_ok()); - let msg1 = msg1.unwrap(); - assert_eq!(msg1.text, "first message"); - assert_eq!(msg1.from_id, ContactId::DEVICE); - assert_eq!(msg1.to_id, ContactId::SELF); - assert!(!msg1.is_info()); - assert!(!msg1.is_setupmessage()); - - let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await; - assert!(msg2.is_ok()); - let msg2 = msg2.unwrap(); - assert_eq!(msg2.text, "second message"); - - // check device chat - assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_device_msg_labelled() -> Result<()> { - let t = TestContext::new().await; - - // add two device-messages with the same label (second attempt is not added) - let mut msg1 = Message::new_text("first message".to_string()); - let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await; - assert!(msg1_id.is_ok()); - assert!(!msg1_id.as_ref().unwrap().is_unset()); - - let mut msg2 = Message::new_text("second message".to_string()); - let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await; - assert!(msg2_id.is_ok()); - assert!(msg2_id.as_ref().unwrap().is_unset()); - - // check added message - let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await?; - assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id); - assert_eq!(msg1.text, "first message"); - assert_eq!(msg1.from_id, ContactId::DEVICE); - assert_eq!(msg1.to_id, ContactId::SELF); - assert!(!msg1.is_info()); - assert!(!msg1.is_setupmessage()); - - // check device chat - let chat_id = msg1.chat_id; - - assert_eq!(chat_id.get_msg_cnt(&t).await?, 1); - assert!(!chat_id.is_special()); - let chat = Chat::load_from_db(&t, chat_id).await?; - assert_eq!(chat.get_type(), Chattype::Single); - assert!(chat.is_device_talk()); - assert!(!chat.is_self_talk()); - assert!(!chat.can_send(&t).await?); - assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat)); - - assert_eq!(chat.name, stock_str::device_messages(&t).await); - assert!(chat.get_profile_image(&t).await?.is_some()); - - // delete device message, make sure it is not added again - message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()]).await?; - let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await; - assert!(msg1.is_err() || msg1.unwrap().chat_id.is_trash()); - let msg3_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await; - assert!(msg3_id.is_ok()); - assert!(msg2_id.as_ref().unwrap().is_unset()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_device_msg_label_only() { - let t = TestContext::new().await; - let res = add_device_msg(&t, Some(""), None).await; - assert!(res.is_err()); - let res = add_device_msg(&t, Some("some-label"), None).await; - assert!(res.is_ok()); - - let mut msg = Message::new_text("message text".to_string()); - - let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await; - assert!(msg_id.is_ok()); - assert!(msg_id.as_ref().unwrap().is_unset()); - - let msg_id = add_device_msg(&t, Some("unused-label"), Some(&mut msg)).await; - assert!(msg_id.is_ok()); - assert!(!msg_id.as_ref().unwrap().is_unset()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_was_device_msg_ever_added() { - let t = TestContext::new().await; - add_device_msg(&t, Some("some-label"), None).await.ok(); - assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap()); - - let mut msg = Message::new_text("message text".to_string()); - add_device_msg(&t, Some("another-label"), Some(&mut msg)) - .await - .ok(); - assert!(was_device_msg_ever_added(&t, "another-label") - .await - .unwrap()); - - assert!(!was_device_msg_ever_added(&t, "unused-label").await.unwrap()); - - assert!(was_device_msg_ever_added(&t, "").await.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_device_chat() { - let t = TestContext::new().await; - - let mut msg = Message::new_text("message text".to_string()); - add_device_msg(&t, Some("some-label"), Some(&mut msg)) - .await - .ok(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - - // after the device-chat and all messages are deleted, a re-adding should do nothing - chats.get_chat_id(0).unwrap().delete(&t).await.ok(); - add_device_msg(&t, Some("some-label"), Some(&mut msg)) - .await - .ok(); - assert_eq!(chatlist_len(&t, 0).await, 0) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_device_chat_cannot_sent() { - let t = TestContext::new().await; - t.update_device_chats().await.unwrap(); - let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE) - .await - .unwrap(); - - let mut msg = Message::new_text("message text".to_string()); - assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err()); - - let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); - assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_and_reset_all_device_msgs() { - let t = TestContext::new().await; - let mut msg = Message::new_text("message text".to_string()); - let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg)) - .await - .unwrap(); - - // adding a device message with the same label won't be executed again ... - assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap()); - let msg_id2 = add_device_msg(&t, Some("some-label"), Some(&mut msg)) - .await - .unwrap(); - assert!(msg_id2.is_unset()); - - // ... unless everything is deleted and reset - as needed eg. on device switch - delete_and_reset_all_device_msgs(&t).await.unwrap(); - assert!(!was_device_msg_ever_added(&t, "some-label").await.unwrap()); - let msg_id3 = add_device_msg(&t, Some("some-label"), Some(&mut msg)) - .await - .unwrap(); - assert_ne!(msg_id1, msg_id3); - } - - async fn chatlist_len(ctx: &Context, listflags: usize) -> usize { - Chatlist::try_load(ctx, listflags, None, None) - .await - .unwrap() - .len() - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_archive() { - // create two chats - let t = TestContext::new().await; - let mut msg = Message::new_text("foo".to_string()); - let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); - let chat_id1 = message::Message::load_from_db(&t, msg_id) - .await - .unwrap() - .chat_id; - let chat_id2 = t.get_self_chat().await.id; - assert!(!chat_id1.is_special()); - assert!(!chat_id2.is_special()); - - assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); - assert_eq!(chatlist_len(&t, 0).await, 2); - assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 2); - assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 0); - assert_eq!(DC_GCL_ARCHIVED_ONLY, 0x01); - assert_eq!(DC_GCL_NO_SPECIALS, 0x02); - - // archive first chat - assert!(chat_id1 - .set_visibility(&t, ChatVisibility::Archived) - .await - .is_ok()); - assert!( - Chat::load_from_db(&t, chat_id1) - .await - .unwrap() - .get_visibility() - == ChatVisibility::Archived - ); - assert!( - Chat::load_from_db(&t, chat_id2) - .await - .unwrap() - .get_visibility() - == ChatVisibility::Normal - ); - assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); - assert_eq!(chatlist_len(&t, 0).await, 2); // including DC_CHAT_ID_ARCHIVED_LINK now - assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 1); - assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 1); - - // archive second chat - assert!(chat_id2 - .set_visibility(&t, ChatVisibility::Archived) - .await - .is_ok()); - assert!( - Chat::load_from_db(&t, chat_id1) - .await - .unwrap() - .get_visibility() - == ChatVisibility::Archived - ); - assert!( - Chat::load_from_db(&t, chat_id2) - .await - .unwrap() - .get_visibility() - == ChatVisibility::Archived - ); - assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); - assert_eq!(chatlist_len(&t, 0).await, 1); // only DC_CHAT_ID_ARCHIVED_LINK now - assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 0); - assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 2); - - // archive already archived first chat, unarchive second chat two times - assert!(chat_id1 - .set_visibility(&t, ChatVisibility::Archived) - .await - .is_ok()); - assert!(chat_id2 - .set_visibility(&t, ChatVisibility::Normal) - .await - .is_ok()); - assert!(chat_id2 - .set_visibility(&t, ChatVisibility::Normal) - .await - .is_ok()); - assert!( - Chat::load_from_db(&t, chat_id1) - .await - .unwrap() - .get_visibility() - == ChatVisibility::Archived - ); - assert!( - Chat::load_from_db(&t, chat_id2) - .await - .unwrap() - .get_visibility() - == ChatVisibility::Normal - ); - assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); - assert_eq!(chatlist_len(&t, 0).await, 2); - assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 1); - assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_unarchive_if_muted() -> Result<()> { - let t = TestContext::new_alice().await; - - async fn msg_from_bob(t: &TestContext, num: u32) -> Result<()> { - receive_imf( - t, - format!( - "From: bob@example.net\n\ - To: alice@example.org\n\ - Message-ID: <{num}@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2022 19:37:57 +0000\n\ - \n\ - hello\n" - ) - .as_bytes(), - false, - ) - .await?; - Ok(()) - } - - msg_from_bob(&t, 1).await?; - let chat_id = t.get_last_msg().await.get_chat_id(); - chat_id.accept(&t).await?; - chat_id.set_visibility(&t, ChatVisibility::Archived).await?; - assert_eq!(get_archived_cnt(&t).await?, 1); - - // not muted chat is unarchived on receiving a message - msg_from_bob(&t, 2).await?; - assert_eq!(get_archived_cnt(&t).await?, 0); - - // forever muted chat is not unarchived on receiving a message - chat_id.set_visibility(&t, ChatVisibility::Archived).await?; - set_muted(&t, chat_id, MuteDuration::Forever).await?; - msg_from_bob(&t, 3).await?; - assert_eq!(get_archived_cnt(&t).await?, 1); - - // otherwise muted chat is not unarchived on receiving a message - set_muted( - &t, - chat_id, - MuteDuration::Until( - SystemTime::now() - .checked_add(Duration::from_secs(1000)) - .unwrap(), - ), - ) - .await?; - msg_from_bob(&t, 4).await?; - assert_eq!(get_archived_cnt(&t).await?, 1); - - // expired mute will unarchive the chat - set_muted( - &t, - chat_id, - MuteDuration::Until( - SystemTime::now() - .checked_sub(Duration::from_secs(1000)) - .unwrap(), - ), - ) - .await?; - msg_from_bob(&t, 5).await?; - assert_eq!(get_archived_cnt(&t).await?, 0); - - // no unarchiving on sending to muted chat or on adding info messages to muted chat - chat_id.set_visibility(&t, ChatVisibility::Archived).await?; - set_muted(&t, chat_id, MuteDuration::Forever).await?; - send_text_msg(&t, chat_id, "out".to_string()).await?; - add_info_msg(&t, chat_id, "info", time()).await?; - assert_eq!(get_archived_cnt(&t).await?, 1); - - // finally, unarchive on sending to not muted chat - set_muted(&t, chat_id, MuteDuration::NotMuted).await?; - send_text_msg(&t, chat_id, "out2".to_string()).await?; - assert_eq!(get_archived_cnt(&t).await?, 0); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_archive_fresh_msgs() -> Result<()> { - let t = TestContext::new_alice().await; - - async fn msg_from(t: &TestContext, name: &str, num: u32) -> Result<()> { - receive_imf( - t, - format!( - "From: {name}@example.net\n\ - To: alice@example.org\n\ - Message-ID: <{num}@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2022 19:37:57 +0000\n\ - \n\ - hello\n" - ) - .as_bytes(), - false, - ) - .await?; - Ok(()) - } - - // receive some messages in archived+muted chats - msg_from(&t, "bob", 1).await?; - let bob_chat_id = t.get_last_msg().await.get_chat_id(); - bob_chat_id.accept(&t).await?; - set_muted(&t, bob_chat_id, MuteDuration::Forever).await?; - bob_chat_id - .set_visibility(&t, ChatVisibility::Archived) - .await?; - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); - - msg_from(&t, "bob", 2).await?; - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); - - msg_from(&t, "bob", 3).await?; - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); - - msg_from(&t, "claire", 4).await?; - let claire_chat_id = t.get_last_msg().await.get_chat_id(); - claire_chat_id.accept(&t).await?; - set_muted(&t, claire_chat_id, MuteDuration::Forever).await?; - claire_chat_id - .set_visibility(&t, ChatVisibility::Archived) - .await?; - msg_from(&t, "claire", 5).await?; - msg_from(&t, "claire", 6).await?; - msg_from(&t, "claire", 7).await?; - assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); - assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 3); - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); - - // mark one of the archived+muted chats as noticed: check that the archive-link counter is changed as well - t.evtracker.clear_events(); - marknoticed_chat(&t, claire_chat_id).await?; - let ev = t - .evtracker - .get_matching(|ev| { - matches!( - ev, - EventType::MsgsChanged { - chat_id: DC_CHAT_ID_ARCHIVED_LINK, - .. - } - ) - }) - .await; - assert_eq!( - ev, - EventType::MsgsChanged { - chat_id: DC_CHAT_ID_ARCHIVED_LINK, - msg_id: MsgId::new(0), - } - ); - assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); - assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); - - // receive some more messages - msg_from(&t, "claire", 8).await?; - assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); - assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 1); - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); - assert_eq!(t.get_fresh_msgs().await?.len(), 0); - - msg_from(&t, "dave", 9).await?; - let dave_chat_id = t.get_last_msg().await.get_chat_id(); - dave_chat_id.accept(&t).await?; - assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); - assert_eq!(t.get_fresh_msgs().await?.len(), 1); - - // mark the archived-link as noticed: check that the real chats are noticed as well - marknoticed_chat(&t, DC_CHAT_ID_ARCHIVED_LINK).await?; - assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 0); - assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); - assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); - assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); - assert_eq!(t.get_fresh_msgs().await?.len(), 1); - - Ok(()) - } - - async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec { - let chatlist = Chatlist::try_load(ctx, listflags, None, None) - .await - .unwrap(); - let mut result = Vec::new(); - for chatlist_index in 0..chatlist.len() { - result.push(chatlist.get_chat_id(chatlist_index).unwrap()) - } - result - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pinned() { - let t = TestContext::new().await; - - // create 3 chats, wait 1 second in between to get a reliable order (we order by time) - let mut msg = Message::new_text("foo".to_string()); - let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); - let chat_id1 = message::Message::load_from_db(&t, msg_id) - .await - .unwrap() - .chat_id; - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - let chat_id2 = t.get_self_chat().await.id; - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); - - let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await; - assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]); - - // pin - assert!(chat_id1 - .set_visibility(&t, ChatVisibility::Pinned) - .await - .is_ok()); - assert_eq!( - Chat::load_from_db(&t, chat_id1) - .await - .unwrap() - .get_visibility(), - ChatVisibility::Pinned - ); - - // check if chat order changed - let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await; - assert_eq!(chatlist, vec![chat_id1, chat_id3, chat_id2]); - - // unpin - assert!(chat_id1 - .set_visibility(&t, ChatVisibility::Normal) - .await - .is_ok()); - assert_eq!( - Chat::load_from_db(&t, chat_id1) - .await - .unwrap() - .get_visibility(), - ChatVisibility::Normal - ); - - // check if chat order changed back - let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await; - assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pinned_after_new_msgs() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat_id = alice.create_chat(&bob).await.id; - let bob_chat_id = bob.create_chat(&alice).await.id; - - assert!(alice_chat_id - .set_visibility(&alice, ChatVisibility::Pinned) - .await - .is_ok()); - assert_eq!( - Chat::load_from_db(&alice, alice_chat_id) - .await? - .get_visibility(), - ChatVisibility::Pinned, - ); - - send_text_msg(&alice, alice_chat_id, "hi!".into()).await?; - assert_eq!( - Chat::load_from_db(&alice, alice_chat_id) - .await? - .get_visibility(), - ChatVisibility::Pinned, - ); - - let mut msg = Message::new_text("hi!".into()); - let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; - let msg = alice.recv_msg(&sent_msg).await; - assert_eq!(msg.chat_id, alice_chat_id); - assert_eq!( - Chat::load_from_db(&alice, alice_chat_id) - .await? - .get_visibility(), - ChatVisibility::Pinned, - ); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_chat_name() { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); - assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().get_name(), - "foo" - ); - - set_chat_name(&t, chat_id, "bar").await.unwrap(); - assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().get_name(), - "bar" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_same_chat_twice() { - let context = TestContext::new().await; - let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de") - .await - .unwrap(); - assert_ne!(contact1, ContactId::UNDEFINED); - - let chat_id = ChatId::create_for_contact(&context.ctx, contact1) - .await - .unwrap(); - assert!(!chat_id.is_special(), "chat_id too small {chat_id}"); - let chat = Chat::load_from_db(&context.ctx, chat_id).await.unwrap(); - - let chat2_id = ChatId::create_for_contact(&context.ctx, contact1) - .await - .unwrap(); - assert_eq!(chat2_id, chat_id); - let chat2 = Chat::load_from_db(&context.ctx, chat2_id).await.unwrap(); - - assert_eq!(chat2.name, chat.name); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_shall_attach_selfavatar() -> Result<()> { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - assert!(!shall_attach_selfavatar(&t, chat_id).await?); - - let (contact_id, _) = Contact::add_or_lookup( - &t, - "", - &ContactAddress::new("foo@bar.org")?, - Origin::IncomingUnknownTo, - ) - .await?; - add_contact_to_chat(&t, chat_id, contact_id).await?; - assert!(shall_attach_selfavatar(&t, chat_id).await?); - - chat_id.set_selfavatar_timestamp(&t, time()).await?; - assert!(!shall_attach_selfavatar(&t, chat_id).await?); - - t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending - assert!(shall_attach_selfavatar(&t, chat_id).await?); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_mute_duration() { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); - // Initial - assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), - false - ); - // Forever - set_muted(&t, chat_id, MuteDuration::Forever).await.unwrap(); - assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), - true - ); - // unMute - set_muted(&t, chat_id, MuteDuration::NotMuted) - .await - .unwrap(); - assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), - false - ); - // Timed in the future - set_muted( - &t, - chat_id, - MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)), - ) - .await - .unwrap(); - assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), - true - ); - // Time in the past - set_muted( - &t, - chat_id, - MuteDuration::Until(SystemTime::now() - Duration::from_secs(3600)), - ) - .await - .unwrap(); - assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), - false - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_info_msg() -> Result<()> { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - add_info_msg(&t, chat_id, "foo info", 200000).await?; - - let msg = t.get_last_msg_in(chat_id).await; - assert_eq!(msg.get_chat_id(), chat_id); - assert_eq!(msg.get_viewtype(), Viewtype::Text); - assert_eq!(msg.get_text(), "foo info"); - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::Unknown); - assert!(msg.parent(&t).await?.is_none()); - assert!(msg.quoted_message(&t).await?.is_none()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_info_msg_with_cmd() -> Result<()> { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let msg_id = add_info_msg_with_cmd( - &t, - chat_id, - "foo bar info", - SystemMessage::EphemeralTimerChanged, - 10000, - None, - None, - None, - ) - .await?; - - let msg = Message::load_from_db(&t, msg_id).await?; - assert_eq!(msg.get_chat_id(), chat_id); - assert_eq!(msg.get_viewtype(), Viewtype::Text); - assert_eq!(msg.get_text(), "foo bar info"); - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged); - assert!(msg.parent(&t).await?.is_none()); - assert!(msg.quoted_message(&t).await?.is_none()); - - let msg2 = t.get_last_msg_in(chat_id).await; - assert_eq!(msg.get_id(), msg2.get_id()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_lookup_by_contact_id() { - let ctx = TestContext::new_alice().await; - - // create contact, then unblocked chat - let contact_id = Contact::create(&ctx, "", "bob@foo.de").await.unwrap(); - assert_ne!(contact_id, ContactId::UNDEFINED); - let found = ChatId::lookup_by_contact(&ctx, contact_id).await.unwrap(); - assert!(found.is_none()); - - let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap(); - let chat2 = ChatIdBlocked::lookup_by_contact(&ctx, contact_id) - .await - .unwrap() - .unwrap(); - assert_eq!(chat_id, chat2.id); - assert_eq!(chat2.blocked, Blocked::Not); - - // create contact, then blocked chat - let contact_id = Contact::create(&ctx, "", "claire@foo.de").await.unwrap(); - let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Yes) - .await - .unwrap() - .id; - let chat2 = ChatIdBlocked::lookup_by_contact(&ctx, contact_id) - .await - .unwrap() - .unwrap(); - assert_eq!(chat_id, chat2.id); - assert_eq!(chat2.blocked, Blocked::Yes); - - // test nonexistent contact - let found = ChatId::lookup_by_contact(&ctx, ContactId::new(1234)) - .await - .unwrap(); - assert!(found.is_none()); - - let found = ChatIdBlocked::lookup_by_contact(&ctx, ContactId::new(1234)) - .await - .unwrap(); - assert!(found.is_none()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_lookup_self_by_contact_id() { - let ctx = TestContext::new_alice().await; - - let chat = ChatId::lookup_by_contact(&ctx, ContactId::SELF) - .await - .unwrap(); - assert!(chat.is_none()); - - ctx.update_device_chats().await.unwrap(); - let chat = ChatIdBlocked::lookup_by_contact(&ctx, ContactId::SELF) - .await - .unwrap() - .unwrap(); - assert!(!chat.id.is_special()); - assert!(chat.id.is_self_talk(&ctx).await.unwrap()); - assert_eq!(chat.blocked, Blocked::Not); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_group_with_removed_message_id() -> Result<()> { - // Alice creates a group with Bob, sends a message to bob - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let alice_bob_contact = alice.add_or_lookup_contact(&bob).await; - let contact_id = alice_bob_contact.id; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?; - - add_contact_to_chat(&alice, alice_chat_id, contact_id).await?; - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); - send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?; - assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 1); - - // Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com). - let sent_msg = alice.pop_sent_msg().await; - let msg = sent_msg.payload(); - assert_eq!(msg.match_indices("Message-ID: <").count(), 2); - assert_eq!(msg.match_indices("References: <").count(), 1); - let msg = msg.replace("Message-ID: <", "Message-ID: Result<()> { - let t = TestContext::new_alice().await; - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - receive_imf( - &t, - b"From: bob@example.org\n\ - To: alice@example.org\n\ - Message-ID: <1@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Fri, 23 Apr 2021 10:00:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chats.len(), 1); - assert_eq!(chats.get_chat_id(0)?, chat.id); - assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 1); - assert_eq!(t.get_fresh_msgs().await?.len(), 1); - - let msgs = get_chat_msgs(&t, chat.id).await?; - assert_eq!(msgs.len(), 1); - let msg_id = match msgs.first().unwrap() { - ChatItem::Message { msg_id } => *msg_id, - _ => MsgId::new_unset(), - }; - let msg = message::Message::load_from_db(&t, msg_id).await?; - assert_eq!(msg.state, MessageState::InFresh); - - marknoticed_chat(&t, chat.id).await?; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let msg = message::Message::load_from_db(&t, msg_id).await?; - assert_eq!(msg.state, MessageState::InNoticed); - assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 0); - assert_eq!(t.get_fresh_msgs().await?.len(), 0); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_contact_request_fresh_messages() -> Result<()> { - let t = TestContext::new_alice().await; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chats.len(), 0); - - receive_imf( - &t, - b"From: bob@example.org\n\ - To: alice@example.org\n\ - Message-ID: <1@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2021 19:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - assert!(Chat::load_from_db(&t, chat_id) - .await - .unwrap() - .is_contact_request()); - assert_eq!(chat_id.get_msg_cnt(&t).await?, 1); - assert_eq!(chat_id.get_fresh_msg_cnt(&t).await?, 1); - let msgs = get_chat_msgs(&t, chat_id).await?; - assert_eq!(msgs.len(), 1); - let msg_id = match msgs.first().unwrap() { - ChatItem::Message { msg_id } => *msg_id, - _ => MsgId::new_unset(), - }; - let msg = message::Message::load_from_db(&t, msg_id).await?; - assert_eq!(msg.state, MessageState::InFresh); - - // Contact requests are excluded from global badge. - assert_eq!(t.get_fresh_msgs().await?.len(), 0); - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let msg = message::Message::load_from_db(&t, msg_id).await?; - assert_eq!(msg.state, MessageState::InFresh); - assert_eq!(t.get_fresh_msgs().await?.len(), 0); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_contact_request_archive() -> Result<()> { - let t = TestContext::new_alice().await; - - receive_imf( - &t, - b"From: bob@example.org\n\ - To: alice@example.org\n\ - Message-ID: <2@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2021 19:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0)?; - assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request()); - assert_eq!(get_archived_cnt(&t).await?, 0); - - // archive request without accepting or blocking - chat_id.set_visibility(&t, ChatVisibility::Archived).await?; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0)?; - assert!(chat_id.is_archived_link()); - assert_eq!(get_archived_cnt(&t).await?, 1); - - let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?; - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0)?; - assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_classic_email_chat() -> Result<()> { - let alice = TestContext::new_alice().await; - - // Alice receives a classic (non-chat) message from Bob. - receive_imf( - &alice, - b"From: bob@example.org\n\ - To: alice@example.org\n\ - Message-ID: <1@example.org>\n\ - Date: Sun, 22 Mar 2021 19:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - - let msg = alice.get_last_msg().await; - let chat_id = msg.chat_id; - assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1); - - let msgs = get_chat_msgs(&alice, chat_id).await?; - assert_eq!(msgs.len(), 1); - - // Alice disables receiving classic emails. - alice - .set_config(Config::ShowEmails, Some("0")) - .await - .unwrap(); - - // Already received classic email should still be in the chat. - assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1); - - let msgs = get_chat_msgs(&alice, chat_id).await?; - assert_eq!(msgs.len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_get_color() -> Result<()> { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; - let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?; - assert_eq!(color1, 0x008772); - - // upper-/lowercase makes a difference for the colors, these are different groups - // (in contrast to email addresses, where upper-/lowercase is ignored in practise) - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "A CHAT").await?; - let color2 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?; - assert_ne!(color2, color1); - Ok(()) - } - - async fn test_sticker( - filename: &str, - bytes: &[u8], - res_viewtype: Viewtype, - w: i32, - h: i32, - ) -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let bob_chat = bob.create_chat(&alice).await; - - let file = alice.get_blobdir().join(filename); - tokio::fs::write(&file, bytes).await?; - - let mut msg = Message::new(Viewtype::Sticker); - msg.set_file(file.to_str().unwrap(), None); - - let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; - let mime = sent_msg.payload(); - if res_viewtype == Viewtype::Sticker { - assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1); - } - - let msg = bob.recv_msg(&sent_msg).await; - assert_eq!(msg.chat_id, bob_chat.id); - assert_eq!(msg.get_viewtype(), res_viewtype); - let msg_filename = msg.get_filename().unwrap(); - match res_viewtype { - Viewtype::Sticker => assert_eq!(msg_filename, filename), - Viewtype::Image => assert!(msg_filename.starts_with("image_")), - _ => panic!("Not implemented"), - } - assert_eq!(msg.get_width(), w); - assert_eq!(msg.get_height(), h); - assert!(msg.get_filebytes(&bob).await?.unwrap() > 250); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sticker_png() -> Result<()> { - test_sticker( - "sticker.png", - include_bytes!("../test-data/image/logo.png"), - Viewtype::Sticker, - 135, - 135, - ) - .await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sticker_jpeg() -> Result<()> { - test_sticker( - "sticker.jpg", - include_bytes!("../test-data/image/avatar1000x1000.jpg"), - Viewtype::Image, - 1000, - 1000, - ) - .await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sticker_jpeg_force() { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - - let file = alice.get_blobdir().join("sticker.jpg"); - tokio::fs::write( - &file, - include_bytes!("../test-data/image/avatar1000x1000.jpg"), - ) - .await - .unwrap(); - - // Images without force_sticker should be turned into [Viewtype::Image] - let mut msg = Message::new(Viewtype::Sticker); - msg.set_file(file.to_str().unwrap(), None); - let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; - let msg = bob.recv_msg(&sent_msg).await; - assert_eq!(msg.get_viewtype(), Viewtype::Image); - - // Images with `force_sticker = true` should keep [Viewtype::Sticker] - let mut msg = Message::new(Viewtype::Sticker); - msg.set_file(file.to_str().unwrap(), None); - msg.force_sticker(); - let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; - let msg = bob.recv_msg(&sent_msg).await; - assert_eq!(msg.get_viewtype(), Viewtype::Sticker); - - // Images with `force_sticker = true` should keep [Viewtype::Sticker] - // even on drafted messages - let mut msg = Message::new(Viewtype::Sticker); - msg.set_file(file.to_str().unwrap(), None); - msg.force_sticker(); - alice_chat - .id - .set_draft(&alice, Some(&mut msg)) - .await - .unwrap(); - let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap(); - let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; - let msg = bob.recv_msg(&sent_msg).await; - assert_eq!(msg.get_viewtype(), Viewtype::Sticker); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sticker_gif() -> Result<()> { - test_sticker( - "sticker.gif", - include_bytes!("../test-data/image/logo.gif"), - Viewtype::Sticker, - 135, - 135, - ) - .await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sticker_forward() -> Result<()> { - // create chats - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let bob_chat = bob.create_chat(&alice).await; - - // create sticker - let file_name = "sticker.png"; - let bytes = include_bytes!("../test-data/image/logo.png"); - let file = alice.get_blobdir().join(file_name); - tokio::fs::write(&file, bytes).await?; - let mut msg = Message::new(Viewtype::Sticker); - msg.set_file(file.to_str().unwrap(), None); - - // send sticker to bob - let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; - let msg = bob.recv_msg(&sent_msg).await; - - // forward said sticker to alice - forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?; - let forwarded_msg = bob.pop_sent_msg().await; - - let msg = alice.recv_msg(&forwarded_msg).await; - // forwarded sticker should not have forwarded-flag - assert!(!msg.is_forwarded()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_forward() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let bob_chat = bob.create_chat(&alice).await; - - let mut msg = Message::new_text("Hi Bob".to_owned()); - let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; - let msg = bob.recv_msg(&sent_msg).await; - - forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?; - - let forwarded_msg = bob.pop_sent_msg().await; - let msg = alice.recv_msg(&forwarded_msg).await; - assert_eq!(msg.get_text(), "Hi Bob"); - assert!(msg.is_forwarded()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_forward_info_msg() -> Result<()> { - let t = TestContext::new_alice().await; - - let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a").await?; - send_text_msg(&t, chat_id1, "msg one".to_string()).await?; - let bob_id = Contact::create(&t, "", "bob@example.net").await?; - add_contact_to_chat(&t, chat_id1, bob_id).await?; - let msg1 = t.get_last_msg_in(chat_id1).await; - assert!(msg1.is_info()); - assert!(msg1.get_text().contains("bob@example.net")); - - let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?; - assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0); - forward_msgs(&t, &[msg1.id], chat_id2).await?; - let msg2 = t.get_last_msg_in(chat_id2).await; - assert!(!msg2.is_info()); // forwarded info-messages lose their info-state - assert_eq!(msg2.get_info_type(), SystemMessage::Unknown); - assert_ne!(msg2.from_id, ContactId::INFO); - assert_ne!(msg2.to_id, ContactId::INFO); - assert_eq!(msg2.get_text(), msg1.get_text()); - assert!(msg2.is_forwarded()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_forward_quote() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let bob_chat = bob.create_chat(&alice).await; - - // Alice sends a message to Bob. - let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; - let received_msg = bob.recv_msg(&sent_msg).await; - - // Bob quotes received message and sends a reply to Alice. - let mut reply = Message::new_text("Reply".to_owned()); - reply.set_quote(&bob, Some(&received_msg)).await?; - let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await; - let received_reply = alice.recv_msg(&sent_reply).await; - - // Alice forwards a reply. - forward_msgs(&alice, &[received_reply.id], alice_chat.get_id()).await?; - let forwarded_msg = alice.pop_sent_msg().await; - let alice_forwarded_msg = bob.recv_msg(&forwarded_msg).await; - assert!(alice_forwarded_msg.quoted_message(&alice).await?.is_none()); - assert_eq!( - alice_forwarded_msg.quoted_text(), - Some("Hi Bob".to_string()) - ); - - let bob_forwarded_msg = bob.get_last_msg().await; - assert!(bob_forwarded_msg.quoted_message(&bob).await?.is_none()); - assert_eq!(bob_forwarded_msg.quoted_text(), Some("Hi Bob".to_string())); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_forward_group() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let alice_chat = alice.create_chat(&bob).await; - let bob_chat = bob.create_chat(&alice).await; - - // Alice creates a group with Bob. - let alice_group_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?; - add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?; - add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?; - let sent_group_msg = alice - .send_text(alice_group_chat_id, "Hi Bob and Claire") - .await; - let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id; - - // Alice deletes a message on her device. - // This is needed to make assignment of further messages received in this group - // based on `References:` header harder. - // Previously this exposed a bug, so this is a regression test. - message::delete_msgs(&alice, &[sent_group_msg.sender_msg_id]).await?; - - // Alice sends a message to Bob. - let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; - let received_msg = bob.recv_msg(&sent_msg).await; - assert_eq!(received_msg.get_text(), "Hi Bob"); - assert_eq!(received_msg.chat_id, bob_chat.id); - - // Alice sends another message to Bob, this has first message as a parent. - let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await; - let received_msg = bob.recv_msg(&sent_msg).await; - assert_eq!(received_msg.get_text(), "Hello Bob"); - assert_eq!(received_msg.chat_id, bob_chat.id); - - // Bob forwards message to a group chat with Alice. - forward_msgs(&bob, &[received_msg.id], bob_group_chat_id).await?; - let forwarded_msg = bob.pop_sent_msg().await; - alice.recv_msg(&forwarded_msg).await; - - let received_forwarded_msg = alice.get_last_msg_in(alice_group_chat_id).await; - assert!(received_forwarded_msg.is_forwarded()); - assert_eq!(received_forwarded_msg.chat_id, alice_group_chat_id); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_only_minimal_data_are_forwarded() -> Result<()> { - // send a message from Alice to a group with Bob - let alice = TestContext::new_alice().await; - alice - .set_config(Config::Displayname, Some("secretname")) - .await?; - let bob_id = Contact::create(&alice, "bob", "bob@example.net").await?; - let group_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?; - add_contact_to_chat(&alice, group_id, bob_id).await?; - let mut msg = Message::new_text("bla foo".to_owned()); - let sent_msg = alice.send_msg(group_id, &mut msg).await; - assert!(sent_msg.payload().contains("secretgrpname")); - assert!(sent_msg.payload().contains("secretname")); - assert!(sent_msg.payload().contains("alice")); - - // Bob forwards that message to Claire - - // Claire should not get information about Alice for the original Group - let bob = TestContext::new_bob().await; - let orig_msg = bob.recv_msg(&sent_msg).await; - let claire_id = Contact::create(&bob, "claire", "claire@foo").await?; - let single_id = ChatId::create_for_contact(&bob, claire_id).await?; - let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?; - add_contact_to_chat(&bob, group_id, claire_id).await?; - let broadcast_id = create_broadcast_list(&bob).await?; - add_contact_to_chat(&bob, broadcast_id, claire_id).await?; - for chat_id in &[single_id, group_id, broadcast_id] { - forward_msgs(&bob, &[orig_msg.id], *chat_id).await?; - let sent_msg = bob.pop_sent_msg().await; - assert!(sent_msg - .payload() - .contains("---------- Forwarded message ----------")); - assert!(!sent_msg.payload().contains("secretgrpname")); - assert!(!sent_msg.payload().contains("secretname")); - assert!(!sent_msg.payload().contains("alice")); - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_save_msgs() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - - let sent = alice.send_text(alice_chat.get_id(), "hi, bob").await; - let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?; - assert!(sent_msg.get_saved_msg_id(&alice).await?.is_none()); - assert!(sent_msg.get_original_msg_id(&alice).await?.is_none()); - - let self_chat = alice.get_self_chat().await; - save_msgs(&alice, &[sent.sender_msg_id]).await?; - - let saved_msg = alice.get_last_msg_in(self_chat.id).await; - assert_ne!(saved_msg.get_id(), sent.sender_msg_id); - assert!(saved_msg.get_saved_msg_id(&alice).await?.is_none()); - assert_eq!( - saved_msg.get_original_msg_id(&alice).await?.unwrap(), - sent.sender_msg_id - ); - assert_eq!(saved_msg.get_text(), "hi, bob"); - assert!(!saved_msg.is_forwarded()); // UI should not flag "saved messages" as "forwarded" - assert_eq!(saved_msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(saved_msg.get_from_id(), ContactId::SELF); - assert_eq!(saved_msg.get_state(), MessageState::OutDelivered); - assert_ne!(saved_msg.rfc724_mid(), sent_msg.rfc724_mid()); - - let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?; - assert_eq!( - sent_msg.get_saved_msg_id(&alice).await?.unwrap(), - saved_msg.id - ); - assert!(sent_msg.get_original_msg_id(&alice).await?.is_none()); - - let rcvd_msg = bob.recv_msg(&sent).await; - let self_chat = bob.get_self_chat().await; - save_msgs(&bob, &[rcvd_msg.id]).await?; - let saved_msg = bob.get_last_msg_in(self_chat.id).await; - assert_ne!(saved_msg.get_id(), rcvd_msg.id); - assert_eq!( - saved_msg.get_original_msg_id(&bob).await?.unwrap(), - rcvd_msg.id - ); - assert_eq!(saved_msg.get_text(), "hi, bob"); - assert!(!saved_msg.is_forwarded()); - assert_eq!(saved_msg.is_dc_message, MessengerMessage::Yes); - assert_ne!(saved_msg.get_from_id(), ContactId::SELF); - assert_eq!(saved_msg.get_state(), MessageState::InSeen); - assert_ne!(saved_msg.rfc724_mid(), rcvd_msg.rfc724_mid()); - - // delete original message - delete_msgs(&bob, &[rcvd_msg.id]).await?; - let saved_msg = Message::load_from_db(&bob, saved_msg.id).await?; - assert!(saved_msg.get_original_msg_id(&bob).await?.is_none()); - - // delete original chat - rcvd_msg.chat_id.delete(&bob).await?; - let msg = Message::load_from_db(&bob, saved_msg.id).await?; - assert!(msg.get_original_msg_id(&bob).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_saved_msgs_not_added_to_shared_chats() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - let msg = tcm.send_recv_accept(&alice, &bob, "hi, bob").await; - - let self_chat = bob.get_self_chat().await; - save_msgs(&bob, &[msg.id]).await?; - let msg = bob.get_last_msg_in(self_chat.id).await; - let contact = Contact::get_by_id(&bob, msg.get_from_id()).await?; - assert_eq!(contact.get_addr(), "alice@example.org"); - - let shared_chats = Chatlist::try_load(&bob, 0, None, Some(contact.id)).await?; - assert_eq!(shared_chats.len(), 1); - assert_eq!( - shared_chats.get_chat_id(0).unwrap(), - bob.get_chat(&alice).await.id - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_forward_from_saved_to_saved() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let sent = alice.send_text(alice.create_chat(&bob).await.id, "k").await; - - bob.recv_msg(&sent).await; - let orig = bob.get_last_msg().await; - let self_chat = bob.get_self_chat().await; - save_msgs(&bob, &[orig.id]).await?; - let saved1 = bob.get_last_msg().await; - assert_eq!( - saved1.get_original_msg_id(&bob).await?.unwrap(), - sent.sender_msg_id - ); - assert_ne!(saved1.from_id, ContactId::SELF); - - forward_msgs(&bob, &[saved1.id], self_chat.id).await?; - let saved2 = bob.get_last_msg().await; - assert!(saved2.get_original_msg_id(&bob).await?.is_none(),); - assert_eq!(saved2.from_id, ContactId::SELF); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_save_from_saved_to_saved_failing() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let sent = alice.send_text(alice.create_chat(&bob).await.id, "k").await; - - bob.recv_msg(&sent).await; - let orig = bob.get_last_msg().await; - save_msgs(&bob, &[orig.id]).await?; - let saved1 = bob.get_last_msg().await; - - let result = save_msgs(&bob, &[saved1.id]).await; - assert!(result.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_resend_own_message() -> Result<()> { - // Alice creates group with Bob and sends an initial message - let alice = TestContext::new_alice().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; - let sent1 = alice.send_text(alice_grp, "alice->bob").await; - - // Alice adds Claire to group and resends her own initial message - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "claire@example.org").await?, - ) - .await?; - let sent2 = alice.pop_sent_msg().await; - let resent_msg_id = sent1.sender_msg_id; - resend_msgs(&alice, &[resent_msg_id]).await?; - assert_eq!( - resent_msg_id.get_state(&alice).await?, - MessageState::OutPending - ); - resend_msgs(&alice, &[resent_msg_id]).await?; - // Message can be re-sent multiple times. - assert_eq!( - resent_msg_id.get_state(&alice).await?, - MessageState::OutPending - ); - alice.pop_sent_msg().await; - // There's still one more pending SMTP job. - assert_eq!( - resent_msg_id.get_state(&alice).await?, - MessageState::OutPending - ); - let sent3 = alice.pop_sent_msg().await; - assert_eq!( - resent_msg_id.get_state(&alice).await?, - MessageState::OutDelivered - ); - - // Bob receives all messages - let bob = TestContext::new_bob().await; - let msg = bob.recv_msg(&sent1).await; - let sent1_ts_sent = msg.timestamp_sent; - assert_eq!(msg.get_text(), "alice->bob"); - assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2); - assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1); - bob.recv_msg(&sent2).await; - assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); - assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2); - let received = bob.recv_msg_opt(&sent3).await; - // No message should actually be added since we already know this message: - assert!(received.is_none()); - assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); - assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2); - - // Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob - let claire = TestContext::new().await; - claire.configure_addr("claire@example.org").await; - claire.recv_msg(&sent2).await; - let msg = claire.recv_msg(&sent3).await; - assert_eq!(msg.get_text(), "alice->bob"); - assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3); - assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2); - let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?; - assert_eq!(msg_from.get_addr(), "alice@example.org"); - assert!(sent1_ts_sent < msg.timestamp_sent); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_resend_foreign_message_fails() -> Result<()> { - let alice = TestContext::new_alice().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; - let sent1 = alice.send_text(alice_grp, "alice->bob").await; - - let bob = TestContext::new_bob().await; - let msg = bob.recv_msg(&sent1).await; - assert!(resend_msgs(&bob, &[msg.id]).await.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_resend_opportunistically_encryption() -> Result<()> { - // Alice creates group with Bob and sends an initial message - let alice = TestContext::new_alice().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; - let sent1 = alice.send_text(alice_grp, "alice->bob").await; - - // Bob now can send an encrypted message - let bob = TestContext::new_bob().await; - let msg = bob.recv_msg(&sent1).await; - assert!(!msg.get_showpadlock()); - - msg.chat_id.accept(&bob).await?; - let sent2 = bob.send_text(msg.chat_id, "bob->alice").await; - let msg = bob.get_last_msg().await; - assert!(msg.get_showpadlock()); - - // Bob adds Claire and resends his last message: this will drop encryption in opportunistic chats - add_contact_to_chat( - &bob, - msg.chat_id, - Contact::create(&bob, "", "claire@example.org").await?, - ) - .await?; - let _sent3 = bob.pop_sent_msg().await; - resend_msgs(&bob, &[sent2.sender_msg_id]).await?; - let _sent4 = bob.pop_sent_msg().await; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_resend_info_message_fails() -> Result<()> { - let alice = TestContext::new_alice().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; - alice.send_text(alice_grp, "alice->bob").await; - - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "claire@example.org").await?, - ) - .await?; - let sent2 = alice.pop_sent_msg().await; - assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_can_send_group() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = Contact::create(&alice, "", "bob@f.br").await?; - let chat_id = ChatId::create_for_contact(&alice, bob).await?; - let chat = Chat::load_from_db(&alice, chat_id).await?; - assert!(chat.can_send(&alice).await?); - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - assert_eq!( - Chat::load_from_db(&alice, chat_id) - .await? - .can_send(&alice) - .await?, - true - ); - remove_contact_from_chat(&alice, chat_id, ContactId::SELF).await?; - assert_eq!( - Chat::load_from_db(&alice, chat_id) - .await? - .can_send(&alice) - .await?, - false - ); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_broadcast() -> Result<()> { - // create two context, send two messages so both know the other - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let chat_alice = alice.create_chat(&bob).await; - send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; - bob.recv_msg(&alice.pop_sent_msg().await).await; - - let chat_bob = bob.create_chat(&alice).await; - send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; - let msg = alice.recv_msg(&bob.pop_sent_msg().await).await; - assert!(msg.get_showpadlock()); - - // test broadcast list - let broadcast_id = create_broadcast_list(&alice).await?; - add_contact_to_chat( - &alice, - broadcast_id, - get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(), - ) - .await?; - set_chat_name(&alice, broadcast_id, "Broadcast list").await?; - { - let chat = Chat::load_from_db(&alice, broadcast_id).await?; - assert_eq!(chat.typ, Chattype::Broadcast); - assert_eq!(chat.name, "Broadcast list"); - assert!(!chat.is_self_talk()); - - send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?; - let msg = alice.get_last_msg().await; - assert_eq!(msg.chat_id, chat.id); - } - - { - let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), "ola!"); - assert_eq!(msg.subject, "Broadcast list"); - assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data - let chat = Chat::load_from_db(&bob, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_ne!(chat.id, chat_bob.id); - assert_eq!(chat.name, "Broadcast list"); - assert!(!chat.is_self_talk()); - } - - { - // Alice changes the name: - set_chat_name(&alice, broadcast_id, "My great broadcast").await?; - let sent = alice.send_text(broadcast_id, "I changed the title!").await; - - let msg = bob.recv_msg(&sent).await; - assert_eq!(msg.subject, "Re: My great broadcast"); - let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?; - assert_eq!(bob_chat.name, "My great broadcast"); - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_broadcast_multidev() -> Result<()> { - let alices = [ - TestContext::new_alice().await, - TestContext::new_alice().await, - ]; - let bob = TestContext::new_bob().await; - let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id; - - let a0_broadcast_id = create_broadcast_list(&alices[0]).await?; - let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?; - set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?; - let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await; - let msg = alices[1].recv_msg(&sent_msg).await; - let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid) - .await? - .unwrap() - .0; - assert_eq!(msg.chat_id, a1_broadcast_id); - let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?; - assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast); - assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42"); - assert!(get_chat_contacts(&alices[1], a1_broadcast_id) - .await? - .is_empty()); - - add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?; - set_chat_name(&alices[1], a1_broadcast_id, "Broadcast list 43").await?; - let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await; - let msg = alices[0].recv_msg(&sent_msg).await; - assert_eq!(msg.chat_id, a0_broadcast_id); - let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?; - assert_eq!(a0_broadcast_chat.get_type(), Chattype::Broadcast); - assert_eq!(a0_broadcast_chat.get_name(), "Broadcast list 42"); - assert!(get_chat_contacts(&alices[0], a0_broadcast_id) - .await? - .is_empty()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_for_contact_with_blocked() -> Result<()> { - let t = TestContext::new().await; - let (contact_id, _) = Contact::add_or_lookup( - &t, - "", - &ContactAddress::new("foo@bar.org")?, - Origin::ManuallyCreated, - ) - .await?; - - // create a blocked chat - let chat_id_orig = - ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; - assert!(!chat_id_orig.is_special()); - let chat = Chat::load_from_db(&t, chat_id_orig).await?; - assert_eq!(chat.blocked, Blocked::Yes); - - // repeating the call, the same chat must still be blocked - let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; - assert_eq!(chat_id, chat_id_orig); - let chat = Chat::load_from_db(&t, chat_id).await?; - assert_eq!(chat.blocked, Blocked::Yes); - - // already created chats are unblocked if requested - let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Not).await?; - assert_eq!(chat_id, chat_id_orig); - let chat = Chat::load_from_db(&t, chat_id).await?; - assert_eq!(chat.blocked, Blocked::Not); - - // however, already created chats are not re-blocked - let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; - assert_eq!(chat_id, chat_id_orig); - let chat = Chat::load_from_db(&t, chat_id).await?; - assert_eq!(chat.blocked, Blocked::Not); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_get_encryption_info() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?; - let contact_fiona = Contact::create(&alice, "", "fiona@example.net").await?; - - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - assert_eq!(chat_id.get_encryption_info(&alice).await?, ""); - - add_contact_to_chat(&alice, chat_id, contact_bob).await?; - assert_eq!( - chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - bob@example.net" - ); - - add_contact_to_chat(&alice, chat_id, contact_fiona).await?; - assert_eq!( - chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - fiona@example.net\n\ - bob@example.net" - ); - - let direct_chat = bob.create_chat(&alice).await; - send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?; - alice.recv_msg(&bob.pop_sent_msg().await).await; - - assert_eq!( - chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - fiona@example.net\n\ - \n\ - End-to-end encryption preferred:\n\ - bob@example.net" - ); - - bob.set_config(Config::E2eeEnabled, Some("0")).await?; - send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?; - alice.recv_msg(&bob.pop_sent_msg().await).await; - - assert_eq!( - chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - fiona@example.net\n\ - \n\ - End-to-end encryption available:\n\ - bob@example.net" - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_chat_media() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?; - - assert_eq!( - get_chat_media( - &t, - Some(chat_id1), - Viewtype::Image, - Viewtype::Sticker, - Viewtype::Unknown - ) - .await? - .len(), - 0 - ); - - async fn send_media( - t: &TestContext, - chat_id: ChatId, - msg_type: Viewtype, - name: &str, - bytes: &[u8], - ) -> Result { - let file = t.get_blobdir().join(name); - tokio::fs::write(&file, bytes).await?; - let mut msg = Message::new(msg_type); - msg.set_file(file.to_str().unwrap(), None); - send_msg(t, chat_id, &mut msg).await - } - - send_media( - &t, - chat_id1, - Viewtype::Image, - "a.jpg", - include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"), - ) - .await?; - send_media( - &t, - chat_id1, - Viewtype::Sticker, - "b.png", - include_bytes!("../test-data/image/logo.png"), - ) - .await?; - let second_image_msg_id = send_media( - &t, - chat_id2, - Viewtype::Image, - "c.jpg", - include_bytes!("../test-data/image/avatar64x64.png"), - ) - .await?; - send_media( - &t, - chat_id2, - Viewtype::Webxdc, - "d.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc"), - ) - .await?; - - assert_eq!( - get_chat_media( - &t, - Some(chat_id1), - Viewtype::Image, - Viewtype::Unknown, - Viewtype::Unknown, - ) - .await? - .len(), - 1 - ); - assert_eq!( - get_chat_media( - &t, - Some(chat_id1), - Viewtype::Sticker, - Viewtype::Unknown, - Viewtype::Unknown, - ) - .await? - .len(), - 1 - ); - assert_eq!( - get_chat_media( - &t, - Some(chat_id1), - Viewtype::Sticker, - Viewtype::Image, - Viewtype::Unknown, - ) - .await? - .len(), - 2 - ); - assert_eq!( - get_chat_media( - &t, - Some(chat_id2), - Viewtype::Webxdc, - Viewtype::Unknown, - Viewtype::Unknown, - ) - .await? - .len(), - 1 - ); - assert_eq!( - get_chat_media( - &t, - None, - Viewtype::Image, - Viewtype::Unknown, - Viewtype::Unknown, - ) - .await? - .len(), - 2 - ); - assert_eq!( - get_chat_media( - &t, - None, - Viewtype::Image, - Viewtype::Sticker, - Viewtype::Unknown, - ) - .await? - .len(), - 3 - ); - assert_eq!( - get_chat_media( - &t, - None, - Viewtype::Image, - Viewtype::Sticker, - Viewtype::Webxdc, - ) - .await? - .len(), - 4 - ); - - // Delete an image. - delete_msgs(&t, &[second_image_msg_id]).await?; - assert_eq!( - get_chat_media( - &t, - None, - Viewtype::Image, - Viewtype::Sticker, - Viewtype::Webxdc, - ) - .await? - .len(), - 3 - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_blob_renaming() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - add_contact_to_chat( - &alice, - chat_id, - Contact::create(&alice, "bob", "bob@example.net").await?, - ) - .await?; - let dir = tempfile::tempdir()?; - let file = dir.path().join("harmless_file.\u{202e}txt.exe"); - fs::write(&file, "aaa").await?; - let mut msg = Message::new(Viewtype::File); - msg.set_file(file.to_str().unwrap(), None); - let msg = bob.recv_msg(&alice.send_msg(chat_id, &mut msg).await).await; - - // the file bob receives should not contain BIDI-control characters - assert_eq!( - Some("$BLOBDIR/harmless_file.txt.exe"), - msg.param.get(Param::File), - ); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_blocked() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - let bob = TestContext::new_bob().await; - - let ba_chat = bob.create_chat(alice0).await; - let sent_msg = bob.send_text(ba_chat.id, "hi").await; - let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; - alice1.recv_msg(&sent_msg).await; - let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id; - - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request); - a0b_chat_id.accept(alice0).await?; - sync(alice0, alice1).await; - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not); - a0b_chat_id.block(alice0).await?; - sync(alice0, alice1).await; - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Yes); - a0b_chat_id.unblock(alice0).await?; - sync(alice0, alice1).await; - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not); - - // Unblocking a 1:1 chat doesn't unblock the contact currently. - Contact::unblock(alice0, a0b_contact_id).await?; - - assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked()); - Contact::block(alice0, a0b_contact_id).await?; - sync(alice0, alice1).await; - assert!(alice1.add_or_lookup_contact(&bob).await.is_blocked()); - Contact::unblock(alice0, a0b_contact_id).await?; - sync(alice0, alice1).await; - assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked()); - - // Test accepting and blocking groups. This way we test: - // - Group chats synchronisation. - // - That blocking a group deletes it on other devices. - let fiona = TestContext::new_fiona().await; - let fiona_grp_chat_id = fiona - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0]) - .await; - let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await; - let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; - let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id; - let a1_grp_chat = Chat::load_from_db(alice1, a1_grp_chat_id).await?; - assert_eq!(a1_grp_chat.blocked, Blocked::Request); - a0_grp_chat_id.accept(alice0).await?; - sync(alice0, alice1).await; - let a1_grp_chat = Chat::load_from_db(alice1, a1_grp_chat_id).await?; - assert_eq!(a1_grp_chat.blocked, Blocked::Not); - a0_grp_chat_id.block(alice0).await?; - sync(alice0, alice1).await; - assert!(Chat::load_from_db(alice1, a1_grp_chat_id).await.is_err()); - assert!( - !alice1 - .sql - .exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,)) - .await? - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_accept_before_first_msg() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - let bob = TestContext::new_bob().await; - - let ba_chat = bob.create_chat(alice0).await; - let sent_msg = bob.send_text(ba_chat.id, "hi").await; - let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request); - a0b_chat_id.accept(alice0).await?; - let a0b_contact = alice0.add_or_lookup_contact(&bob).await; - assert_eq!(a0b_contact.origin, Origin::CreateChat); - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not); - - sync(alice0, alice1).await; - let a1b_contact = alice1.add_or_lookup_contact(&bob).await; - assert_eq!(a1b_contact.origin, Origin::CreateChat); - let a1b_chat = alice1.get_chat(&bob).await; - assert_eq!(a1b_chat.blocked, Blocked::Not); - let chats = Chatlist::try_load(alice1, 0, None, None).await?; - assert_eq!(chats.len(), 1); - - let rcvd_msg = alice1.recv_msg(&sent_msg).await; - assert_eq!(rcvd_msg.chat_id, a1b_chat.id); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_block_before_first_msg() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - let bob = TestContext::new_bob().await; - - let ba_chat = bob.create_chat(alice0).await; - let sent_msg = bob.send_text(ba_chat.id, "hi").await; - let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request); - a0b_chat_id.block(alice0).await?; - let a0b_contact = alice0.add_or_lookup_contact(&bob).await; - assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom); - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes); - - sync(alice0, alice1).await; - let a1b_contact = alice1.add_or_lookup_contact(&bob).await; - assert_eq!(a1b_contact.origin, Origin::Hidden); - assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id) - .await? - .is_none()); - - let rcvd_msg = alice1.recv_msg(&sent_msg).await; - let a1b_contact = alice1.add_or_lookup_contact(&bob).await; - assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom); - let a1b_chat = alice1.get_chat(&bob).await; - assert_eq!(a1b_chat.blocked, Blocked::Yes); - assert_eq!(rcvd_msg.chat_id, a1b_chat.id); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_adhoc_grp() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - - let mut chat_ids = Vec::new(); - for a in [alice0, alice1] { - let msg = receive_imf( - a, - b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\ - From: alice@example.org\r\n\ - To: , \r\n\ - Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\ - Message-ID: \r\n\ - Chat-Version: 1.0\r\n\ - \r\n\ - hi\r\n", - false, - ) - .await? - .unwrap(); - chat_ids.push(msg.chat_id); - } - let chat1 = Chat::load_from_db(alice1, chat_ids[1]).await?; - assert_eq!(chat1.typ, Chattype::Group); - assert!(chat1.grpid.is_empty()); - - // Test synchronisation on chat blocking because it causes chat deletion currently and thus - // requires generating a sync message in advance. - chat_ids[0].block(alice0).await?; - sync(alice0, alice1).await; - assert!(Chat::load_from_db(alice1, chat_ids[1]).await.is_err()); - assert!( - !alice1 - .sql - .exists("SELECT COUNT(*) FROM chats WHERE id=?", (chat_ids[1],)) - .await? - ); - - Ok(()) - } - - /// Tests syncing of chat visibility on a self-chat. This way we test: - /// - Self-chat synchronisation. - /// - That sync messages don't unarchive the self-chat. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_visibility() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - let a0self_chat_id = alice0.get_self_chat().await.id; - - assert_eq!( - alice1.get_self_chat().await.get_visibility(), - ChatVisibility::Normal - ); - let mut visibilities = - ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal)); - visibilities.next(); - for v in visibilities { - a0self_chat_id.set_visibility(alice0, v).await?; - sync(alice0, alice1).await; - for a in [alice0, alice1] { - assert_eq!(a.get_self_chat().await.get_visibility(), v); - } - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_muted() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - let bob = TestContext::new_bob().await; - let a0b_chat_id = alice0.create_chat(&bob).await.id; - alice1.create_chat(&bob).await; - - assert_eq!( - alice1.get_chat(&bob).await.mute_duration, - MuteDuration::NotMuted - ); - let mute_durations = [ - MuteDuration::Forever, - MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)), - MuteDuration::NotMuted, - ]; - for m in mute_durations { - set_muted(alice0, a0b_chat_id, m).await?; - sync(alice0, alice1).await; - let m = match m { - MuteDuration::Until(time) => MuteDuration::Until( - SystemTime::UNIX_EPOCH - + Duration::from_secs( - time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(), - ), - ), - _ => m, - }; - assert_eq!(alice1.get_chat(&bob).await.mute_duration, m); - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_broadcast() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - let bob = TestContext::new_bob().await; - let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id; - - let a0_broadcast_id = create_broadcast_list(alice0).await?; - sync(alice0, alice1).await; - let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?; - let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid) - .await? - .unwrap() - .0; - let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?; - assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast); - assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name()); - assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty()); - add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?; - sync(alice0, alice1).await; - let a1b_contact_id = Contact::lookup_id_by_addr( - alice1, - &bob.get_config(Config::Addr).await?.unwrap(), - Origin::Hidden, - ) - .await? - .unwrap(); - assert_eq!( - get_chat_contacts(alice1, a1_broadcast_id).await?, - vec![a1b_contact_id] - ); - let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await; - let msg = bob.recv_msg(&sent_msg).await; - let chat = Chat::load_from_db(&bob, msg.chat_id).await?; - assert_eq!(chat.get_type(), Chattype::Mailinglist); - let msg = alice0.recv_msg(&sent_msg).await; - assert_eq!(msg.chat_id, a0_broadcast_id); - remove_contact_from_chat(alice0, a0_broadcast_id, a0b_contact_id).await?; - sync(alice0, alice1).await; - assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_name() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - let a0_broadcast_id = create_broadcast_list(alice0).await?; - sync(alice0, alice1).await; - let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?; - set_chat_name(alice0, a0_broadcast_id, "Broadcast list 42").await?; - sync(alice0, alice1).await; - let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid) - .await? - .unwrap() - .0; - let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?; - assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast); - assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42"); - Ok(()) - } - - /// Tests sending JPEG image with .png extension. - /// - /// This is a regression test, previously sending failed - /// because image was passed to PNG decoder - /// and it failed to decode image. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_jpeg_with_png_ext() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let bytes = include_bytes!("../test-data/image/screenshot.jpg"); - let file = alice.get_blobdir().join("screenshot.png"); - tokio::fs::write(&file, bytes).await?; - let mut msg = Message::new(Viewtype::Image); - msg.set_file(file.to_str().unwrap(), None); - - let alice_chat = alice.create_chat(&bob).await; - let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; - let _msg = bob.recv_msg(&sent_msg).await; - - Ok(()) - } - - /// Tests that info message is ignored when constructing `In-Reply-To`. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_info_not_referenced() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let bob_received_message = tcm.send_recv_accept(alice, bob, "Hi!").await; - let bob_chat_id = bob_received_message.chat_id; - add_info_msg(bob, bob_chat_id, "Some info", create_smeared_timestamp(bob)).await?; - - // Bob sends a message. - // This message should reference Alice's "Hi!" message and not the info message. - let sent = bob.send_text(bob_chat_id, "Hi hi!").await; - let mime_message = alice.parse_msg(&sent).await; - - let in_reply_to = mime_message.get_header(HeaderDef::InReplyTo).unwrap(); - assert_eq!( - in_reply_to, - format!("<{}>", bob_received_message.rfc724_mid) - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_do_not_overwrite_draft() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let mut msg = Message::new_text("This is a draft message".to_string()); - let self_chat = alice.get_self_chat().await.id; - self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap(); - let draft1 = self_chat.get_draft(&alice).await?.unwrap(); - SystemTime::shift(Duration::from_secs(1)); - self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap(); - let draft2 = self_chat.get_draft(&alice).await?.unwrap(); - assert_eq!(draft1.timestamp_sort, draft2.timestamp_sort); - - Ok(()) - } - - /// Test group consistency. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_member_bug() -> Result<()> { - let mut tcm = TestContextManager::new(); - - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; - - // Create a group. - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; - add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; - add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; - - // Promote the group. - let alice_sent_msg = alice - .send_text(alice_chat_id, "Hi! I created a group.") - .await; - let bob_received_msg = bob.recv_msg(&alice_sent_msg).await; - - let bob_chat_id = bob_received_msg.get_chat_id(); - bob_chat_id.accept(bob).await?; - - // Alice removes Fiona from the chat. - remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; - let _alice_sent_add_msg = alice.pop_sent_msg().await; - - SystemTime::shift(Duration::from_secs(3600)); - - // Bob sends a message - // to Alice and Fiona because he still has not received - // a message about Fiona being removed. - let bob_sent_msg = bob.send_text(bob_chat_id, "Hi Alice!").await; - - // Alice receives a message. - // This should not add Fiona back. - let _alice_received_msg = alice.recv_msg(&bob_sent_msg).await; - - assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 2); - - Ok(()) - } - - /// Test that tombstones for past members are added to chats_contacts table - /// even if the row did not exist before. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_past_members() -> Result<()> { - let mut tcm = TestContextManager::new(); - - let alice = &tcm.alice().await; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; - - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; - add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; - alice - .send_text(alice_chat_id, "Hi! I created a group.") - .await; - remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; - assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1); - - let bob = &tcm.bob().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; - add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; - - let add_message = alice.pop_sent_msg().await; - let bob_add_message = bob.recv_msg(&add_message).await; - let bob_chat_id = bob_add_message.chat_id; - assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); - assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn non_member_cannot_modify_member_list() -> Result<()> { - let mut tcm = TestContextManager::new(); - - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; - - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; - add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; - let alice_sent_msg = alice - .send_text(alice_chat_id, "Hi! I created a group.") - .await; - let bob_received_msg = bob.recv_msg(&alice_sent_msg).await; - let bob_chat_id = bob_received_msg.get_chat_id(); - bob_chat_id.accept(bob).await?; - - let bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?; - - // Alice removes Bob and Bob adds Fiona at the same time. - remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?; - add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?; - - let bob_sent_add_msg = bob.pop_sent_msg().await; - - // Alice ignores Bob's message because Bob is not a member. - assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1); - alice.recv_msg_trash(&bob_sent_add_msg).await; - assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn unpromoted_group_no_tombstones() -> Result<()> { - let mut tcm = TestContextManager::new(); - - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; - let fiona_addr = "fiona@example.net"; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?; - - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; - add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; - add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; - assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3); - assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0); - - remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; - assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 2); - - // There should be no tombstone because the group is not promoted yet. - assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0); - - let sent = alice.send_text(alice_chat_id, "Hello group!").await; - let payload = sent.payload(); - assert_eq!(payload.contains("Hello group!"), true); - assert_eq!(payload.contains(&bob_addr), true); - assert_eq!(payload.contains(fiona_addr), false); - - let bob_msg = bob.recv_msg(&sent).await; - let bob_chat_id = bob_msg.chat_id; - assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); - assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 0); - - Ok(()) - } -} +mod chat_tests; diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs new file mode 100644 index 000000000..308a34be8 --- /dev/null +++ b/src/chat/chat_tests.rs @@ -0,0 +1,3360 @@ +use super::*; +use crate::chatlist::get_archived_cnt; +use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; +use crate::headerdef::HeaderDef; +use crate::message::{delete_msgs, MessengerMessage}; +use crate::receive_imf::receive_imf; +use crate::test_utils::{sync, TestContext, TestContextManager, TimeShiftFalsePositiveNote}; +use strum::IntoEnumIterator; +use tokio::fs; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_info() { + let t = TestContext::new().await; + let chat = t.create_chat_with_contact("bob", "bob@example.com").await; + let info = chat.get_info(&t).await.unwrap(); + + // Ensure we can serialize this. + println!("{}", serde_json::to_string_pretty(&info).unwrap()); + + let expected = r#" + { + "id": 10, + "type": 100, + "name": "bob", + "archived": false, + "param": "", + "gossiped_timestamp": 0, + "is_sending_locations": false, + "color": 35391, + "profile_image": "", + "draft": "", + "is_muted": false, + "ephemeral_timer": "Disabled" + } + "#; + + // Ensure we can deserialize this. + let loaded: ChatInfo = serde_json::from_str(expected).unwrap(); + assert_eq!(info, loaded); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_draft_no_draft() { + let t = TestContext::new().await; + let chat = t.get_self_chat().await; + let draft = chat.id.get_draft(&t).await.unwrap(); + assert!(draft.is_none()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_draft_special_chat_id() { + let t = TestContext::new().await; + let draft = DC_CHAT_ID_LAST_SPECIAL.get_draft(&t).await.unwrap(); + assert!(draft.is_none()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_draft_no_chat() { + // This is a weird case, maybe this should be an error but we + // do not get this info from the database currently. + let t = TestContext::new().await; + let draft = ChatId::new(42).get_draft(&t).await.unwrap(); + assert!(draft.is_none()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_draft() { + let t = TestContext::new().await; + let chat_id = &t.get_self_chat().await.id; + let mut msg = Message::new_text("hello".to_string()); + + chat_id.set_draft(&t, Some(&mut msg)).await.unwrap(); + let draft = chat_id.get_draft(&t).await.unwrap().unwrap(); + let msg_text = msg.get_text(); + let draft_text = draft.get_text(); + assert_eq!(msg_text, draft_text); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_draft() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?; + + let mut msg = Message::new_text("hi!".to_string()); + chat_id.set_draft(&t, Some(&mut msg)).await?; + assert!(chat_id.get_draft(&t).await?.is_some()); + + let mut msg = Message::new_text("another".to_string()); + chat_id.set_draft(&t, Some(&mut msg)).await?; + assert!(chat_id.get_draft(&t).await?.is_some()); + + chat_id.set_draft(&t, None).await?; + assert!(chat_id.get_draft(&t).await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forwarding_draft_failing() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = &t.get_self_chat().await.id; + let mut msg = Message::new_text("hello".to_string()); + chat_id.set_draft(&t, Some(&mut msg)).await?; + assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id); + + let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + assert!(forward_msgs(&t, &[msg.id], chat_id2).await.is_err()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_draft_stable_ids() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = &t.get_self_chat().await.id; + let mut msg = Message::new_text("hello".to_string()); + assert_eq!(msg.id, MsgId::new_unset()); + assert!(chat_id.get_draft_msg_id(&t).await?.is_none()); + + chat_id.set_draft(&t, Some(&mut msg)).await?; + let id_after_1st_set = msg.id; + assert_ne!(id_after_1st_set, MsgId::new_unset()); + assert_eq!( + id_after_1st_set, + chat_id.get_draft_msg_id(&t).await?.unwrap() + ); + assert_eq!(id_after_1st_set, chat_id.get_draft(&t).await?.unwrap().id); + + msg.set_text("hello2".to_string()); + chat_id.set_draft(&t, Some(&mut msg)).await?; + let id_after_2nd_set = msg.id; + + assert_eq!(id_after_2nd_set, id_after_1st_set); + assert_eq!( + id_after_2nd_set, + chat_id.get_draft_msg_id(&t).await?.unwrap() + ); + let test = chat_id.get_draft(&t).await?.unwrap(); + assert_eq!(id_after_2nd_set, test.id); + assert_eq!(id_after_2nd_set, msg.id); + assert_eq!(test.text, "hello2".to_string()); + assert_eq!(test.state, MessageState::OutDraft); + + let id_after_send = send_msg(&t, *chat_id, &mut msg).await?; + assert_eq!(id_after_send, id_after_1st_set); + + let test = Message::load_from_db(&t, id_after_send).await?; + assert!(!test.hidden); // sent draft must no longer be hidden + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_only_one_draft_per_chat() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?; + + let msgs: Vec = (1..=1000) + .map(|i| Message::new_text(i.to_string())) + .collect(); + let mut tasks = Vec::new(); + for mut msg in msgs { + let ctx = t.clone(); + let task = tokio::spawn(async move { + let ctx = ctx; + chat_id.set_draft(&ctx, Some(&mut msg)).await + }); + tasks.push(task); + } + futures::future::join_all(tasks.into_iter()).await; + + assert!(chat_id.get_draft(&t).await?.is_some()); + + chat_id.set_draft(&t, None).await?; + assert!(chat_id.get_draft(&t).await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_change_quotes_on_reused_message_object() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let quote1 = + Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote1".to_string()).await?).await?; + let quote2 = + Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote2".to_string()).await?).await?; + + // save a draft + let mut draft = Message::new_text("draft text".to_string()); + chat_id.set_draft(&t, Some(&mut draft)).await?; + + let test = Message::load_from_db(&t, draft.id).await?; + assert_eq!(test.text, "draft text".to_string()); + assert!(test.quoted_text().is_none()); + assert!(test.quoted_message(&t).await?.is_none()); + + // add quote to same message object + draft.set_quote(&t, Some("e1)).await?; + chat_id.set_draft(&t, Some(&mut draft)).await?; + + let test = Message::load_from_db(&t, draft.id).await?; + assert_eq!(test.text, "draft text".to_string()); + assert_eq!(test.quoted_text(), Some("quote1".to_string())); + assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id); + + // change quote on same message object + draft.set_text("another draft text".to_string()); + draft.set_quote(&t, Some("e2)).await?; + chat_id.set_draft(&t, Some(&mut draft)).await?; + + let test = Message::load_from_db(&t, draft.id).await?; + assert_eq!(test.text, "another draft text".to_string()); + assert_eq!(test.quoted_text(), Some("quote2".to_string())); + assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id); + + // remove quote on same message object + draft.set_quote(&t, None).await?; + chat_id.set_draft(&t, Some(&mut draft)).await?; + + let test = Message::load_from_db(&t, draft.id).await?; + assert_eq!(test.text, "another draft text".to_string()); + assert!(test.quoted_text().is_none()); + assert!(test.quoted_message(&t).await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_quote_replies() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?; + let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?; + + let one2one_chat_id = alice.create_chat(&bob).await.id; + let one2one_msg_id = send_text_msg(&alice, one2one_chat_id, "foo".to_string()).await?; + let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?; + + // quoting messages in same chat is okay + let mut msg = Message::new_text("baz".to_string()); + msg.set_quote(&alice, Some(&grp_msg)).await?; + let result = send_msg(&alice, grp_chat_id, &mut msg).await; + assert!(result.is_ok()); + + let mut msg = Message::new_text("baz".to_string()); + msg.set_quote(&alice, Some(&one2one_msg)).await?; + let result = send_msg(&alice, one2one_chat_id, &mut msg).await; + assert!(result.is_ok()); + let one2one_quote_reply_msg_id = result.unwrap(); + + // quoting messages from groups to one-to-ones is okay ("reply privately") + let mut msg = Message::new_text("baz".to_string()); + msg.set_quote(&alice, Some(&grp_msg)).await?; + let result = send_msg(&alice, one2one_chat_id, &mut msg).await; + assert!(result.is_ok()); + + // quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ... + let mut msg = Message::new_text("baz".to_string()); + msg.set_quote(&alice, Some(&one2one_msg)).await?; + let result = send_msg(&alice, grp_chat_id, &mut msg).await; + assert!(result.is_err()); + + // ... but forwarding messages with quotes is allowed + let result = forward_msgs(&alice, &[one2one_quote_reply_msg_id], grp_chat_id).await; + assert!(result.is_ok()); + + // ... and bots are not restricted + alice.set_config(Config::Bot, Some("1")).await?; + let result = send_msg(&alice, grp_chat_id, &mut msg).await; + assert!(result.is_ok()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_contact_to_chat_ex_add_self() { + // Adding self to a contact should succeed, even though it's pointless. + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") + .await + .unwrap(); + let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false) + .await + .unwrap(); + assert_eq!(added, false); +} + +/// Test adding and removing members in a group chat. +/// +/// Make sure messages sent outside contain authname +/// and displayed messages contain locally set name. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_member_add_remove() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + // Disable encryption so we can inspect raw message contents. + alice.set_config(Config::E2eeEnabled, Some("0")).await?; + bob.set_config(Config::E2eeEnabled, Some("0")).await?; + + // Create contact for Bob on the Alice side with name "robert". + let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?; + + // Set Bob authname to "Bob" and send it to Alice. + bob.set_config(Config::Displayname, Some("Bob")).await?; + tcm.send_recv(&bob, &alice, "Hello!").await; + + // Check that Alice has Bob's name set to "robert" and authname set to "Bob". + { + let alice_bob_contact = Contact::get_by_id(&alice, alice_bob_contact_id).await?; + assert_eq!(alice_bob_contact.get_name(), "robert"); + + // This is the name that will be sent outside. + assert_eq!(alice_bob_contact.get_authname(), "Bob"); + + assert_eq!(alice_bob_contact.get_display_name(), "robert"); + } + + // Create and promote a group. + let alice_chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; + add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; + let sent = alice + .send_text(alice_chat_id, "Hi! I created a group.") + .await; + assert!(sent.payload.contains("Hi! I created a group.")); + + // Alice adds Bob to the chat. + add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + let sent = alice.pop_sent_msg().await; + assert!(sent + .payload + .contains("I added member Bob (bob@example.net).")); + // Locally set name "robert" should not leak. + assert!(!sent.payload.contains("robert")); + assert_eq!( + sent.load_from_db().await.get_text(), + "You added member robert (bob@example.net)." + ); + + // Alice removes Bob from the chat. + remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + let sent = alice.pop_sent_msg().await; + assert!(sent + .payload + .contains("I removed member Bob (bob@example.net).")); + assert!(!sent.payload.contains("robert")); + assert_eq!( + sent.load_from_db().await.get_text(), + "You removed member robert (bob@example.net)." + ); + + // Alice leaves the chat. + remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?; + let sent = alice.pop_sent_msg().await; + assert!(sent.payload.contains("I left the group.")); + assert_eq!(sent.load_from_db().await.get_text(), "You left the group."); + + Ok(()) +} + +/// Test parallel removal of user from the chat and leaving the group. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parallel_member_remove() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + alice.set_config(Config::E2eeEnabled, Some("0")).await?; + bob.set_config(Config::E2eeEnabled, Some("0")).await?; + + let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?; + let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; + let alice_claire_contact_id = Contact::create(&alice, "Claire", "claire@example.net").await?; + + // Create and promote a group. + let alice_chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; + let alice_sent_msg = alice + .send_text(alice_chat_id, "Hi! I created a group.") + .await; + let bob_received_msg = bob.recv_msg(&alice_sent_msg).await; + + let bob_chat_id = bob_received_msg.get_chat_id(); + bob_chat_id.accept(&bob).await?; + + // Alice adds Claire to the chat. + add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?; + let alice_sent_add_msg = alice.pop_sent_msg().await; + + // Bob leaves the chat. + remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; + bob.pop_sent_msg().await; + + // Bob receives a msg about Alice adding Claire to the group. + bob.recv_msg(&alice_sent_add_msg).await; + + SystemTime::shift(Duration::from_secs(3600)); + + // Alice sends a message to Bob because the message about leaving is lost. + let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await; + bob.recv_msg(&alice_sent_msg).await; + + bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove") + .await; + + // Alice removes Bob from the chat. + remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + let alice_sent_remove_msg = alice.pop_sent_msg().await; + + // Bob receives a msg about Alice removing him from the group. + let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await; + + // Test that remove message is rewritten. + assert_eq!( + bob_received_remove_msg.get_text(), + "Member Me (bob@example.net) removed by alice@example.org." + ); + + Ok(()) +} + +/// Test that member removal is synchronized eventually even if the message is lost. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_msg_with_implicit_member_removed() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let alice_bob_contact_id = + Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?; + let fiona_addr = "fiona@example.net"; + let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?; + let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?; + let alice_chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + let sent_msg = alice.send_text(alice_chat_id, "I created a group").await; + let bob_received_msg = bob.recv_msg(&sent_msg).await; + let bob_chat_id = bob_received_msg.get_chat_id(); + bob_chat_id.accept(&bob).await?; + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + + add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; + let sent_msg = alice.pop_sent_msg().await; + bob.recv_msg(&sent_msg).await; + + // Bob removed Fiona, but the message is lost. + remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?; + bob.pop_sent_msg().await; + + // This doesn't add Fiona back because Bob just removed them. + let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await; + bob.recv_msg(&sent_msg).await; + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + + // Even after some time Fiona is not added back. + SystemTime::shift(Duration::from_secs(3600)); + let sent_msg = alice.send_text(alice_chat_id, "Welcome back, Fiona!").await; + bob.recv_msg(&sent_msg).await; + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + + // If Bob sends a message to Alice now, Fiona is removed. + assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3); + let sent_msg = bob + .send_text(alice_chat_id, "I have removed Fiona some time ago.") + .await; + alice.recv_msg(&sent_msg).await; + assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_modify_chat_multi_device() -> Result<()> { + let a1 = TestContext::new_alice().await; + let a2 = TestContext::new_alice().await; + a1.set_config_bool(Config::BccSelf, true).await?; + + // create group and sync it to the second device + let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?; + let sent = a1.send_text(a1_chat_id, "ho!").await; + let a1_msg = a1.get_last_msg().await; + let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?; + + let a2_msg = a2.recv_msg(&sent).await; + let a2_chat_id = a2_msg.chat_id; + let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?; + + assert!(!a1_msg.is_system_message()); + assert!(!a2_msg.is_system_message()); + assert_eq!(a1_chat.grpid, a2_chat.grpid); + assert_eq!(a1_chat.name, "foo"); + assert_eq!(a2_chat.name, "foo"); + assert_eq!(a1_chat.get_profile_image(&a1).await?, None); + assert_eq!(a2_chat.get_profile_image(&a2).await?, None); + assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 1); + assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1); + + // add a member to the group + let bob = Contact::create(&a1, "", "bob@example.org").await?; + add_contact_to_chat(&a1, a1_chat_id, bob).await?; + let a1_msg = a1.get_last_msg().await; + + let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; + + assert!(a1_msg.is_system_message()); + assert!(a2_msg.is_system_message()); + assert_eq!(a1_msg.get_info_type(), SystemMessage::MemberAddedToGroup); + assert_eq!(a2_msg.get_info_type(), SystemMessage::MemberAddedToGroup); + assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 2); + assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 2); + assert_eq!(get_past_chat_contacts(&a1, a1_chat_id).await?.len(), 0); + assert_eq!(get_past_chat_contacts(&a2, a2_chat_id).await?.len(), 0); + + // rename the group + set_chat_name(&a1, a1_chat_id, "bar").await?; + let a1_msg = a1.get_last_msg().await; + + let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; + + assert!(a1_msg.is_system_message()); + assert!(a2_msg.is_system_message()); + assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged); + assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged); + assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar"); + assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar"); + + // remove member from group + remove_contact_from_chat(&a1, a1_chat_id, bob).await?; + let a1_msg = a1.get_last_msg().await; + + let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; + + assert!(a1_msg.is_system_message()); + assert!(a2_msg.is_system_message()); + assert_eq!( + a1_msg.get_info_type(), + SystemMessage::MemberRemovedFromGroup + ); + assert_eq!( + a2_msg.get_info_type(), + SystemMessage::MemberRemovedFromGroup + ); + assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 1); + assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1); + assert_eq!(get_past_chat_contacts(&a1, a1_chat_id).await?.len(), 1); + assert_eq!(get_past_chat_contacts(&a2, a2_chat_id).await?.len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_modify_chat_disordered() -> Result<()> { + let _n = TimeShiftFalsePositiveNote; + + // Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy + // (time shift is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then) + let alice = TestContext::new_alice().await; + + let bob_id = Contact::create(&alice, "", "bob@example.net").await?; + let claire_id = Contact::create(&alice, "", "claire@foo.de").await?; + let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?; + + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + + add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; + let add1 = alice.pop_sent_msg().await; + + add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; + let add2 = alice.pop_sent_msg().await; + SystemTime::shift(Duration::from_millis(1100)); + + add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?; + let add3 = alice.pop_sent_msg().await; + SystemTime::shift(Duration::from_millis(1100)); + + assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4); + + remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; + let remove1 = alice.pop_sent_msg().await; + SystemTime::shift(Duration::from_millis(1100)); + + remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?; + let remove2 = alice.pop_sent_msg().await; + + assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); + + // Bob receives the add and deletion messages out of order + let bob = TestContext::new_bob().await; + bob.recv_msg(&add1).await; + let bob_chat_id = bob.recv_msg(&add3).await.chat_id; + bob.recv_msg_trash(&add2).await; // No-op addition message is trashed. + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4); + + bob.recv_msg(&remove2).await; + bob.recv_msg(&remove1).await; + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + + Ok(()) +} + +/// Tests that if member added message is completely lost, +/// member is eventually added. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_lost_member_added() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob]) + .await; + let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; + let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id; + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); + + // Attempt to add member, but message is lost. + let claire_id = Contact::create(alice, "", "claire@foo.de").await?; + add_contact_to_chat(alice, alice_chat_id, claire_id).await?; + alice.pop_sent_msg().await; + + let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await; + bob.recv_msg(&alice_sent).await; + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3); + + Ok(()) +} + +/// Test that group updates are robust to lost messages and eventual out of order arrival. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_modify_chat_lost() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + + let bob_id = Contact::create(&alice, "", "bob@example.net").await?; + let claire_id = Contact::create(&alice, "", "claire@foo.de").await?; + let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?; + + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; + add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; + add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?; + + send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + let add = alice.pop_sent_msg().await; + SystemTime::shift(Duration::from_millis(1100)); + + remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; + let remove1 = alice.pop_sent_msg().await; + SystemTime::shift(Duration::from_millis(1100)); + + remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?; + let remove2 = alice.pop_sent_msg().await; + + let bob = tcm.bob().await; + + bob.recv_msg(&add).await; + let bob_chat_id = bob.get_last_msg().await.chat_id; + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4); + + // First removal message is lost. + // Nevertheless, two members are removed. + bob.recv_msg(&remove2).await; + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + + // Eventually, first removal message arrives. + // This has no effect. + bob.recv_msg(&remove1).await; + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_leave_group() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Create group chat with Bob. + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let bob_contact = Contact::create(&alice, "", "bob@example.net").await?; + add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?; + + // Alice sends first message to group. + let sent_msg = alice.send_text(alice_chat_id, "Hello!").await; + let bob_msg = bob.recv_msg(&sent_msg).await; + + assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); + + // Bob leaves the group. + let bob_chat_id = bob_msg.chat_id; + bob_chat_id.accept(&bob).await?; + remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; + + let leave_msg = bob.pop_sent_msg().await; + alice.recv_msg(&leave_msg).await; + + assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1); + + Ok(()) +} + +/// Test that adding or removing contacts in 1:1 chat is not allowed. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_remove_contact_for_single() { + let ctx = TestContext::new_alice().await; + let bob = Contact::create(&ctx, "", "bob@f.br").await.unwrap(); + let chat_id = ChatId::create_for_contact(&ctx, bob).await.unwrap(); + let chat = Chat::load_from_db(&ctx, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Single); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); + + // adding or removing contacts from one-to-one-chats result in an error + let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap(); + let added = add_contact_to_chat_ex(&ctx, Nosync, chat.id, claire, false).await; + assert!(added.is_err()); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); + + let removed = remove_contact_from_chat(&ctx, chat.id, claire).await; + assert!(removed.is_err()); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); + + let removed = remove_contact_from_chat(&ctx, chat.id, ContactId::SELF).await; + assert!(removed.is_err()); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_self_talk() -> Result<()> { + let t = TestContext::new_alice().await; + let chat = &t.get_self_chat().await; + assert!(!chat.id.is_special()); + assert!(chat.is_self_talk()); + assert!(chat.visibility == ChatVisibility::Normal); + assert!(!chat.is_device_talk()); + assert!(chat.can_send(&t).await?); + assert_eq!(chat.name, stock_str::saved_messages(&t).await); + assert!(chat.get_profile_image(&t).await?.is_some()); + + let msg_id = send_text_msg(&t, chat.id, "foo self".to_string()).await?; + let msg = Message::load_from_db(&t, msg_id).await?; + assert_eq!(msg.from_id, ContactId::SELF); + assert_eq!(msg.to_id, ContactId::SELF); + assert!(msg.get_showpadlock()); + + let sent_msg = t.pop_sent_msg().await; + let t2 = TestContext::new_alice().await; + t2.recv_msg(&sent_msg).await; + let chat = &t2.get_self_chat().await; + let msg = t2.get_last_msg_in(chat.id).await; + assert_eq!(msg.text, "foo self".to_string()); + assert_eq!(msg.from_id, ContactId::SELF); + assert_eq!(msg.to_id, ContactId::SELF); + assert!(msg.get_showpadlock()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_device_msg_unlabelled() { + let t = TestContext::new().await; + + // add two device-messages + let mut msg1 = Message::new_text("first message".to_string()); + let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await; + assert!(msg1_id.is_ok()); + + let mut msg2 = Message::new_text("second message".to_string()); + let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await; + assert!(msg2_id.is_ok()); + assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); + + // check added messages + let msg1 = message::Message::load_from_db(&t, msg1_id.unwrap()).await; + assert!(msg1.is_ok()); + let msg1 = msg1.unwrap(); + assert_eq!(msg1.text, "first message"); + assert_eq!(msg1.from_id, ContactId::DEVICE); + assert_eq!(msg1.to_id, ContactId::SELF); + assert!(!msg1.is_info()); + assert!(!msg1.is_setupmessage()); + + let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await; + assert!(msg2.is_ok()); + let msg2 = msg2.unwrap(); + assert_eq!(msg2.text, "second message"); + + // check device chat + assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_device_msg_labelled() -> Result<()> { + let t = TestContext::new().await; + + // add two device-messages with the same label (second attempt is not added) + let mut msg1 = Message::new_text("first message".to_string()); + let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await; + assert!(msg1_id.is_ok()); + assert!(!msg1_id.as_ref().unwrap().is_unset()); + + let mut msg2 = Message::new_text("second message".to_string()); + let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await; + assert!(msg2_id.is_ok()); + assert!(msg2_id.as_ref().unwrap().is_unset()); + + // check added message + let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await?; + assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id); + assert_eq!(msg1.text, "first message"); + assert_eq!(msg1.from_id, ContactId::DEVICE); + assert_eq!(msg1.to_id, ContactId::SELF); + assert!(!msg1.is_info()); + assert!(!msg1.is_setupmessage()); + + // check device chat + let chat_id = msg1.chat_id; + + assert_eq!(chat_id.get_msg_cnt(&t).await?, 1); + assert!(!chat_id.is_special()); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.get_type(), Chattype::Single); + assert!(chat.is_device_talk()); + assert!(!chat.is_self_talk()); + assert!(!chat.can_send(&t).await?); + assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat)); + + assert_eq!(chat.name, stock_str::device_messages(&t).await); + assert!(chat.get_profile_image(&t).await?.is_some()); + + // delete device message, make sure it is not added again + message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()]).await?; + let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await; + assert!(msg1.is_err() || msg1.unwrap().chat_id.is_trash()); + let msg3_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await; + assert!(msg3_id.is_ok()); + assert!(msg2_id.as_ref().unwrap().is_unset()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_device_msg_label_only() { + let t = TestContext::new().await; + let res = add_device_msg(&t, Some(""), None).await; + assert!(res.is_err()); + let res = add_device_msg(&t, Some("some-label"), None).await; + assert!(res.is_ok()); + + let mut msg = Message::new_text("message text".to_string()); + + let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await; + assert!(msg_id.is_ok()); + assert!(msg_id.as_ref().unwrap().is_unset()); + + let msg_id = add_device_msg(&t, Some("unused-label"), Some(&mut msg)).await; + assert!(msg_id.is_ok()); + assert!(!msg_id.as_ref().unwrap().is_unset()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_was_device_msg_ever_added() { + let t = TestContext::new().await; + add_device_msg(&t, Some("some-label"), None).await.ok(); + assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap()); + + let mut msg = Message::new_text("message text".to_string()); + add_device_msg(&t, Some("another-label"), Some(&mut msg)) + .await + .ok(); + assert!(was_device_msg_ever_added(&t, "another-label") + .await + .unwrap()); + + assert!(!was_device_msg_ever_added(&t, "unused-label").await.unwrap()); + + assert!(was_device_msg_ever_added(&t, "").await.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_device_chat() { + let t = TestContext::new().await; + + let mut msg = Message::new_text("message text".to_string()); + add_device_msg(&t, Some("some-label"), Some(&mut msg)) + .await + .ok(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + + // after the device-chat and all messages are deleted, a re-adding should do nothing + chats.get_chat_id(0).unwrap().delete(&t).await.ok(); + add_device_msg(&t, Some("some-label"), Some(&mut msg)) + .await + .ok(); + assert_eq!(chatlist_len(&t, 0).await, 0) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_device_chat_cannot_sent() { + let t = TestContext::new().await; + t.update_device_chats().await.unwrap(); + let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE) + .await + .unwrap(); + + let mut msg = Message::new_text("message text".to_string()); + assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err()); + + let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); + assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_and_reset_all_device_msgs() { + let t = TestContext::new().await; + let mut msg = Message::new_text("message text".to_string()); + let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg)) + .await + .unwrap(); + + // adding a device message with the same label won't be executed again ... + assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap()); + let msg_id2 = add_device_msg(&t, Some("some-label"), Some(&mut msg)) + .await + .unwrap(); + assert!(msg_id2.is_unset()); + + // ... unless everything is deleted and reset - as needed eg. on device switch + delete_and_reset_all_device_msgs(&t).await.unwrap(); + assert!(!was_device_msg_ever_added(&t, "some-label").await.unwrap()); + let msg_id3 = add_device_msg(&t, Some("some-label"), Some(&mut msg)) + .await + .unwrap(); + assert_ne!(msg_id1, msg_id3); +} + +async fn chatlist_len(ctx: &Context, listflags: usize) -> usize { + Chatlist::try_load(ctx, listflags, None, None) + .await + .unwrap() + .len() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_archive() { + // create two chats + let t = TestContext::new().await; + let mut msg = Message::new_text("foo".to_string()); + let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); + let chat_id1 = message::Message::load_from_db(&t, msg_id) + .await + .unwrap() + .chat_id; + let chat_id2 = t.get_self_chat().await.id; + assert!(!chat_id1.is_special()); + assert!(!chat_id2.is_special()); + + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); + assert_eq!(chatlist_len(&t, 0).await, 2); + assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 2); + assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 0); + assert_eq!(DC_GCL_ARCHIVED_ONLY, 0x01); + assert_eq!(DC_GCL_NO_SPECIALS, 0x02); + + // archive first chat + assert!(chat_id1 + .set_visibility(&t, ChatVisibility::Archived) + .await + .is_ok()); + assert!( + Chat::load_from_db(&t, chat_id1) + .await + .unwrap() + .get_visibility() + == ChatVisibility::Archived + ); + assert!( + Chat::load_from_db(&t, chat_id2) + .await + .unwrap() + .get_visibility() + == ChatVisibility::Normal + ); + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); + assert_eq!(chatlist_len(&t, 0).await, 2); // including DC_CHAT_ID_ARCHIVED_LINK now + assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 1); + assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 1); + + // archive second chat + assert!(chat_id2 + .set_visibility(&t, ChatVisibility::Archived) + .await + .is_ok()); + assert!( + Chat::load_from_db(&t, chat_id1) + .await + .unwrap() + .get_visibility() + == ChatVisibility::Archived + ); + assert!( + Chat::load_from_db(&t, chat_id2) + .await + .unwrap() + .get_visibility() + == ChatVisibility::Archived + ); + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); + assert_eq!(chatlist_len(&t, 0).await, 1); // only DC_CHAT_ID_ARCHIVED_LINK now + assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 0); + assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 2); + + // archive already archived first chat, unarchive second chat two times + assert!(chat_id1 + .set_visibility(&t, ChatVisibility::Archived) + .await + .is_ok()); + assert!(chat_id2 + .set_visibility(&t, ChatVisibility::Normal) + .await + .is_ok()); + assert!(chat_id2 + .set_visibility(&t, ChatVisibility::Normal) + .await + .is_ok()); + assert!( + Chat::load_from_db(&t, chat_id1) + .await + .unwrap() + .get_visibility() + == ChatVisibility::Archived + ); + assert!( + Chat::load_from_db(&t, chat_id2) + .await + .unwrap() + .get_visibility() + == ChatVisibility::Normal + ); + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); + assert_eq!(chatlist_len(&t, 0).await, 2); + assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 1); + assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_unarchive_if_muted() -> Result<()> { + let t = TestContext::new_alice().await; + + async fn msg_from_bob(t: &TestContext, num: u32) -> Result<()> { + receive_imf( + t, + format!( + "From: bob@example.net\n\ + To: alice@example.org\n\ + Message-ID: <{num}@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2022 19:37:57 +0000\n\ + \n\ + hello\n" + ) + .as_bytes(), + false, + ) + .await?; + Ok(()) + } + + msg_from_bob(&t, 1).await?; + let chat_id = t.get_last_msg().await.get_chat_id(); + chat_id.accept(&t).await?; + chat_id.set_visibility(&t, ChatVisibility::Archived).await?; + assert_eq!(get_archived_cnt(&t).await?, 1); + + // not muted chat is unarchived on receiving a message + msg_from_bob(&t, 2).await?; + assert_eq!(get_archived_cnt(&t).await?, 0); + + // forever muted chat is not unarchived on receiving a message + chat_id.set_visibility(&t, ChatVisibility::Archived).await?; + set_muted(&t, chat_id, MuteDuration::Forever).await?; + msg_from_bob(&t, 3).await?; + assert_eq!(get_archived_cnt(&t).await?, 1); + + // otherwise muted chat is not unarchived on receiving a message + set_muted( + &t, + chat_id, + MuteDuration::Until( + SystemTime::now() + .checked_add(Duration::from_secs(1000)) + .unwrap(), + ), + ) + .await?; + msg_from_bob(&t, 4).await?; + assert_eq!(get_archived_cnt(&t).await?, 1); + + // expired mute will unarchive the chat + set_muted( + &t, + chat_id, + MuteDuration::Until( + SystemTime::now() + .checked_sub(Duration::from_secs(1000)) + .unwrap(), + ), + ) + .await?; + msg_from_bob(&t, 5).await?; + assert_eq!(get_archived_cnt(&t).await?, 0); + + // no unarchiving on sending to muted chat or on adding info messages to muted chat + chat_id.set_visibility(&t, ChatVisibility::Archived).await?; + set_muted(&t, chat_id, MuteDuration::Forever).await?; + send_text_msg(&t, chat_id, "out".to_string()).await?; + add_info_msg(&t, chat_id, "info", time()).await?; + assert_eq!(get_archived_cnt(&t).await?, 1); + + // finally, unarchive on sending to not muted chat + set_muted(&t, chat_id, MuteDuration::NotMuted).await?; + send_text_msg(&t, chat_id, "out2".to_string()).await?; + assert_eq!(get_archived_cnt(&t).await?, 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_archive_fresh_msgs() -> Result<()> { + let t = TestContext::new_alice().await; + + async fn msg_from(t: &TestContext, name: &str, num: u32) -> Result<()> { + receive_imf( + t, + format!( + "From: {name}@example.net\n\ + To: alice@example.org\n\ + Message-ID: <{num}@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2022 19:37:57 +0000\n\ + \n\ + hello\n" + ) + .as_bytes(), + false, + ) + .await?; + Ok(()) + } + + // receive some messages in archived+muted chats + msg_from(&t, "bob", 1).await?; + let bob_chat_id = t.get_last_msg().await.get_chat_id(); + bob_chat_id.accept(&t).await?; + set_muted(&t, bob_chat_id, MuteDuration::Forever).await?; + bob_chat_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); + + msg_from(&t, "bob", 2).await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + msg_from(&t, "bob", 3).await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + msg_from(&t, "claire", 4).await?; + let claire_chat_id = t.get_last_msg().await.get_chat_id(); + claire_chat_id.accept(&t).await?; + set_muted(&t, claire_chat_id, MuteDuration::Forever).await?; + claire_chat_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + msg_from(&t, "claire", 5).await?; + msg_from(&t, "claire", 6).await?; + msg_from(&t, "claire", 7).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 3); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + + // mark one of the archived+muted chats as noticed: check that the archive-link counter is changed as well + t.evtracker.clear_events(); + marknoticed_chat(&t, claire_chat_id).await?; + let ev = t + .evtracker + .get_matching(|ev| { + matches!( + ev, + EventType::MsgsChanged { + chat_id: DC_CHAT_ID_ARCHIVED_LINK, + .. + } + ) + }) + .await; + assert_eq!( + ev, + EventType::MsgsChanged { + chat_id: DC_CHAT_ID_ARCHIVED_LINK, + msg_id: MsgId::new(0), + } + ); + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + // receive some more messages + msg_from(&t, "claire", 8).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(t.get_fresh_msgs().await?.len(), 0); + + msg_from(&t, "dave", 9).await?; + let dave_chat_id = t.get_last_msg().await.get_chat_id(); + dave_chat_id.accept(&t).await?; + assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(t.get_fresh_msgs().await?.len(), 1); + + // mark the archived-link as noticed: check that the real chats are noticed as well + marknoticed_chat(&t, DC_CHAT_ID_ARCHIVED_LINK).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(t.get_fresh_msgs().await?.len(), 1); + + Ok(()) +} + +async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec { + let chatlist = Chatlist::try_load(ctx, listflags, None, None) + .await + .unwrap(); + let mut result = Vec::new(); + for chatlist_index in 0..chatlist.len() { + result.push(chatlist.get_chat_id(chatlist_index).unwrap()) + } + result +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pinned() { + let t = TestContext::new().await; + + // create 3 chats, wait 1 second in between to get a reliable order (we order by time) + let mut msg = Message::new_text("foo".to_string()); + let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); + let chat_id1 = message::Message::load_from_db(&t, msg_id) + .await + .unwrap() + .chat_id; + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + let chat_id2 = t.get_self_chat().await.id; + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") + .await + .unwrap(); + + let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await; + assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]); + + // pin + assert!(chat_id1 + .set_visibility(&t, ChatVisibility::Pinned) + .await + .is_ok()); + assert_eq!( + Chat::load_from_db(&t, chat_id1) + .await + .unwrap() + .get_visibility(), + ChatVisibility::Pinned + ); + + // check if chat order changed + let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await; + assert_eq!(chatlist, vec![chat_id1, chat_id3, chat_id2]); + + // unpin + assert!(chat_id1 + .set_visibility(&t, ChatVisibility::Normal) + .await + .is_ok()); + assert_eq!( + Chat::load_from_db(&t, chat_id1) + .await + .unwrap() + .get_visibility(), + ChatVisibility::Normal + ); + + // check if chat order changed back + let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await; + assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pinned_after_new_msgs() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat_id = alice.create_chat(&bob).await.id; + let bob_chat_id = bob.create_chat(&alice).await.id; + + assert!(alice_chat_id + .set_visibility(&alice, ChatVisibility::Pinned) + .await + .is_ok()); + assert_eq!( + Chat::load_from_db(&alice, alice_chat_id) + .await? + .get_visibility(), + ChatVisibility::Pinned, + ); + + send_text_msg(&alice, alice_chat_id, "hi!".into()).await?; + assert_eq!( + Chat::load_from_db(&alice, alice_chat_id) + .await? + .get_visibility(), + ChatVisibility::Pinned, + ); + + let mut msg = Message::new_text("hi!".into()); + let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; + let msg = alice.recv_msg(&sent_msg).await; + assert_eq!(msg.chat_id, alice_chat_id); + assert_eq!( + Chat::load_from_db(&alice, alice_chat_id) + .await? + .get_visibility(), + ChatVisibility::Pinned, + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_chat_name() { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") + .await + .unwrap(); + assert_eq!( + Chat::load_from_db(&t, chat_id).await.unwrap().get_name(), + "foo" + ); + + set_chat_name(&t, chat_id, "bar").await.unwrap(); + assert_eq!( + Chat::load_from_db(&t, chat_id).await.unwrap().get_name(), + "bar" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_same_chat_twice() { + let context = TestContext::new().await; + let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de") + .await + .unwrap(); + assert_ne!(contact1, ContactId::UNDEFINED); + + let chat_id = ChatId::create_for_contact(&context.ctx, contact1) + .await + .unwrap(); + assert!(!chat_id.is_special(), "chat_id too small {chat_id}"); + let chat = Chat::load_from_db(&context.ctx, chat_id).await.unwrap(); + + let chat2_id = ChatId::create_for_contact(&context.ctx, contact1) + .await + .unwrap(); + assert_eq!(chat2_id, chat_id); + let chat2 = Chat::load_from_db(&context.ctx, chat2_id).await.unwrap(); + + assert_eq!(chat2.name, chat.name); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_shall_attach_selfavatar() -> Result<()> { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + assert!(!shall_attach_selfavatar(&t, chat_id).await?); + + let (contact_id, _) = Contact::add_or_lookup( + &t, + "", + &ContactAddress::new("foo@bar.org")?, + Origin::IncomingUnknownTo, + ) + .await?; + add_contact_to_chat(&t, chat_id, contact_id).await?; + assert!(shall_attach_selfavatar(&t, chat_id).await?); + + chat_id.set_selfavatar_timestamp(&t, time()).await?; + assert!(!shall_attach_selfavatar(&t, chat_id).await?); + + t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending + assert!(shall_attach_selfavatar(&t, chat_id).await?); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_mute_duration() { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") + .await + .unwrap(); + // Initial + assert_eq!( + Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), + false + ); + // Forever + set_muted(&t, chat_id, MuteDuration::Forever).await.unwrap(); + assert_eq!( + Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), + true + ); + // unMute + set_muted(&t, chat_id, MuteDuration::NotMuted) + .await + .unwrap(); + assert_eq!( + Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), + false + ); + // Timed in the future + set_muted( + &t, + chat_id, + MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)), + ) + .await + .unwrap(); + assert_eq!( + Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), + true + ); + // Time in the past + set_muted( + &t, + chat_id, + MuteDuration::Until(SystemTime::now() - Duration::from_secs(3600)), + ) + .await + .unwrap(); + assert_eq!( + Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), + false + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_info_msg() -> Result<()> { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + add_info_msg(&t, chat_id, "foo info", 200000).await?; + + let msg = t.get_last_msg_in(chat_id).await; + assert_eq!(msg.get_chat_id(), chat_id); + assert_eq!(msg.get_viewtype(), Viewtype::Text); + assert_eq!(msg.get_text(), "foo info"); + assert!(msg.is_info()); + assert_eq!(msg.get_info_type(), SystemMessage::Unknown); + assert!(msg.parent(&t).await?.is_none()); + assert!(msg.quoted_message(&t).await?.is_none()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_info_msg_with_cmd() -> Result<()> { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let msg_id = add_info_msg_with_cmd( + &t, + chat_id, + "foo bar info", + SystemMessage::EphemeralTimerChanged, + 10000, + None, + None, + None, + ) + .await?; + + let msg = Message::load_from_db(&t, msg_id).await?; + assert_eq!(msg.get_chat_id(), chat_id); + assert_eq!(msg.get_viewtype(), Viewtype::Text); + assert_eq!(msg.get_text(), "foo bar info"); + assert!(msg.is_info()); + assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged); + assert!(msg.parent(&t).await?.is_none()); + assert!(msg.quoted_message(&t).await?.is_none()); + + let msg2 = t.get_last_msg_in(chat_id).await; + assert_eq!(msg.get_id(), msg2.get_id()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_lookup_by_contact_id() { + let ctx = TestContext::new_alice().await; + + // create contact, then unblocked chat + let contact_id = Contact::create(&ctx, "", "bob@foo.de").await.unwrap(); + assert_ne!(contact_id, ContactId::UNDEFINED); + let found = ChatId::lookup_by_contact(&ctx, contact_id).await.unwrap(); + assert!(found.is_none()); + + let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap(); + let chat2 = ChatIdBlocked::lookup_by_contact(&ctx, contact_id) + .await + .unwrap() + .unwrap(); + assert_eq!(chat_id, chat2.id); + assert_eq!(chat2.blocked, Blocked::Not); + + // create contact, then blocked chat + let contact_id = Contact::create(&ctx, "", "claire@foo.de").await.unwrap(); + let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Yes) + .await + .unwrap() + .id; + let chat2 = ChatIdBlocked::lookup_by_contact(&ctx, contact_id) + .await + .unwrap() + .unwrap(); + assert_eq!(chat_id, chat2.id); + assert_eq!(chat2.blocked, Blocked::Yes); + + // test nonexistent contact + let found = ChatId::lookup_by_contact(&ctx, ContactId::new(1234)) + .await + .unwrap(); + assert!(found.is_none()); + + let found = ChatIdBlocked::lookup_by_contact(&ctx, ContactId::new(1234)) + .await + .unwrap(); + assert!(found.is_none()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_lookup_self_by_contact_id() { + let ctx = TestContext::new_alice().await; + + let chat = ChatId::lookup_by_contact(&ctx, ContactId::SELF) + .await + .unwrap(); + assert!(chat.is_none()); + + ctx.update_device_chats().await.unwrap(); + let chat = ChatIdBlocked::lookup_by_contact(&ctx, ContactId::SELF) + .await + .unwrap() + .unwrap(); + assert!(!chat.id.is_special()); + assert!(chat.id.is_self_talk(&ctx).await.unwrap()); + assert_eq!(chat.blocked, Blocked::Not); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_group_with_removed_message_id() -> Result<()> { + // Alice creates a group with Bob, sends a message to bob + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let alice_bob_contact = alice.add_or_lookup_contact(&bob).await; + let contact_id = alice_bob_contact.id; + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?; + + add_contact_to_chat(&alice, alice_chat_id, contact_id).await?; + assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); + send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?; + assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 1); + + // Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com). + let sent_msg = alice.pop_sent_msg().await; + let msg = sent_msg.payload(); + assert_eq!(msg.match_indices("Message-ID: <").count(), 2); + assert_eq!(msg.match_indices("References: <").count(), 1); + let msg = msg.replace("Message-ID: <", "Message-ID: Result<()> { + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + receive_imf( + &t, + b"From: bob@example.org\n\ + To: alice@example.org\n\ + Message-ID: <1@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Fri, 23 Apr 2021 10:00:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chats.len(), 1); + assert_eq!(chats.get_chat_id(0)?, chat.id); + assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(t.get_fresh_msgs().await?.len(), 1); + + let msgs = get_chat_msgs(&t, chat.id).await?; + assert_eq!(msgs.len(), 1); + let msg_id = match msgs.first().unwrap() { + ChatItem::Message { msg_id } => *msg_id, + _ => MsgId::new_unset(), + }; + let msg = message::Message::load_from_db(&t, msg_id).await?; + assert_eq!(msg.state, MessageState::InFresh); + + marknoticed_chat(&t, chat.id).await?; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let msg = message::Message::load_from_db(&t, msg_id).await?; + assert_eq!(msg.state, MessageState::InNoticed); + assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(t.get_fresh_msgs().await?.len(), 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_contact_request_fresh_messages() -> Result<()> { + let t = TestContext::new_alice().await; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chats.len(), 0); + + receive_imf( + &t, + b"From: bob@example.org\n\ + To: alice@example.org\n\ + Message-ID: <1@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2021 19:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + assert!(Chat::load_from_db(&t, chat_id) + .await + .unwrap() + .is_contact_request()); + assert_eq!(chat_id.get_msg_cnt(&t).await?, 1); + assert_eq!(chat_id.get_fresh_msg_cnt(&t).await?, 1); + let msgs = get_chat_msgs(&t, chat_id).await?; + assert_eq!(msgs.len(), 1); + let msg_id = match msgs.first().unwrap() { + ChatItem::Message { msg_id } => *msg_id, + _ => MsgId::new_unset(), + }; + let msg = message::Message::load_from_db(&t, msg_id).await?; + assert_eq!(msg.state, MessageState::InFresh); + + // Contact requests are excluded from global badge. + assert_eq!(t.get_fresh_msgs().await?.len(), 0); + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let msg = message::Message::load_from_db(&t, msg_id).await?; + assert_eq!(msg.state, MessageState::InFresh); + assert_eq!(t.get_fresh_msgs().await?.len(), 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_contact_request_archive() -> Result<()> { + let t = TestContext::new_alice().await; + + receive_imf( + &t, + b"From: bob@example.org\n\ + To: alice@example.org\n\ + Message-ID: <2@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2021 19:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0)?; + assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request()); + assert_eq!(get_archived_cnt(&t).await?, 0); + + // archive request without accepting or blocking + chat_id.set_visibility(&t, ChatVisibility::Archived).await?; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0)?; + assert!(chat_id.is_archived_link()); + assert_eq!(get_archived_cnt(&t).await?, 1); + + let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?; + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0)?; + assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_classic_email_chat() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Alice receives a classic (non-chat) message from Bob. + receive_imf( + &alice, + b"From: bob@example.org\n\ + To: alice@example.org\n\ + Message-ID: <1@example.org>\n\ + Date: Sun, 22 Mar 2021 19:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + + let msg = alice.get_last_msg().await; + let chat_id = msg.chat_id; + assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1); + + let msgs = get_chat_msgs(&alice, chat_id).await?; + assert_eq!(msgs.len(), 1); + + // Alice disables receiving classic emails. + alice + .set_config(Config::ShowEmails, Some("0")) + .await + .unwrap(); + + // Already received classic email should still be in the chat. + assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1); + + let msgs = get_chat_msgs(&alice, chat_id).await?; + assert_eq!(msgs.len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_get_color() -> Result<()> { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; + let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?; + assert_eq!(color1, 0x008772); + + // upper-/lowercase makes a difference for the colors, these are different groups + // (in contrast to email addresses, where upper-/lowercase is ignored in practise) + let t = TestContext::new().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "A CHAT").await?; + let color2 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?; + assert_ne!(color2, color1); + Ok(()) +} + +async fn test_sticker( + filename: &str, + bytes: &[u8], + res_viewtype: Viewtype, + w: i32, + h: i32, +) -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let bob_chat = bob.create_chat(&alice).await; + + let file = alice.get_blobdir().join(filename); + tokio::fs::write(&file, bytes).await?; + + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let mime = sent_msg.payload(); + if res_viewtype == Viewtype::Sticker { + assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1); + } + + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.chat_id, bob_chat.id); + assert_eq!(msg.get_viewtype(), res_viewtype); + let msg_filename = msg.get_filename().unwrap(); + match res_viewtype { + Viewtype::Sticker => assert_eq!(msg_filename, filename), + Viewtype::Image => assert!(msg_filename.starts_with("image_")), + _ => panic!("Not implemented"), + } + assert_eq!(msg.get_width(), w); + assert_eq!(msg.get_height(), h); + assert!(msg.get_filebytes(&bob).await?.unwrap() > 250); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sticker_png() -> Result<()> { + test_sticker( + "sticker.png", + include_bytes!("../../test-data/image/logo.png"), + Viewtype::Sticker, + 135, + 135, + ) + .await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sticker_jpeg() -> Result<()> { + test_sticker( + "sticker.jpg", + include_bytes!("../../test-data/image/avatar1000x1000.jpg"), + Viewtype::Image, + 1000, + 1000, + ) + .await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sticker_jpeg_force() { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + + let file = alice.get_blobdir().join("sticker.jpg"); + tokio::fs::write( + &file, + include_bytes!("../../test-data/image/avatar1000x1000.jpg"), + ) + .await + .unwrap(); + + // Images without force_sticker should be turned into [Viewtype::Image] + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Image); + + // Images with `force_sticker = true` should keep [Viewtype::Sticker] + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + msg.force_sticker(); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Sticker); + + // Images with `force_sticker = true` should keep [Viewtype::Sticker] + // even on drafted messages + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + msg.force_sticker(); + alice_chat + .id + .set_draft(&alice, Some(&mut msg)) + .await + .unwrap(); + let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap(); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Sticker); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sticker_gif() -> Result<()> { + test_sticker( + "sticker.gif", + include_bytes!("../../test-data/image/logo.gif"), + Viewtype::Sticker, + 135, + 135, + ) + .await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sticker_forward() -> Result<()> { + // create chats + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let bob_chat = bob.create_chat(&alice).await; + + // create sticker + let file_name = "sticker.png"; + let bytes = include_bytes!("../../test-data/image/logo.png"); + let file = alice.get_blobdir().join(file_name); + tokio::fs::write(&file, bytes).await?; + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + + // send sticker to bob + let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + + // forward said sticker to alice + forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?; + let forwarded_msg = bob.pop_sent_msg().await; + + let msg = alice.recv_msg(&forwarded_msg).await; + // forwarded sticker should not have forwarded-flag + assert!(!msg.is_forwarded()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forward() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let bob_chat = bob.create_chat(&alice).await; + + let mut msg = Message::new_text("Hi Bob".to_owned()); + let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + + forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?; + + let forwarded_msg = bob.pop_sent_msg().await; + let msg = alice.recv_msg(&forwarded_msg).await; + assert_eq!(msg.get_text(), "Hi Bob"); + assert!(msg.is_forwarded()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forward_info_msg() -> Result<()> { + let t = TestContext::new_alice().await; + + let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a").await?; + send_text_msg(&t, chat_id1, "msg one".to_string()).await?; + let bob_id = Contact::create(&t, "", "bob@example.net").await?; + add_contact_to_chat(&t, chat_id1, bob_id).await?; + let msg1 = t.get_last_msg_in(chat_id1).await; + assert!(msg1.is_info()); + assert!(msg1.get_text().contains("bob@example.net")); + + let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?; + assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0); + forward_msgs(&t, &[msg1.id], chat_id2).await?; + let msg2 = t.get_last_msg_in(chat_id2).await; + assert!(!msg2.is_info()); // forwarded info-messages lose their info-state + assert_eq!(msg2.get_info_type(), SystemMessage::Unknown); + assert_ne!(msg2.from_id, ContactId::INFO); + assert_ne!(msg2.to_id, ContactId::INFO); + assert_eq!(msg2.get_text(), msg1.get_text()); + assert!(msg2.is_forwarded()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forward_quote() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let bob_chat = bob.create_chat(&alice).await; + + // Alice sends a message to Bob. + let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; + let received_msg = bob.recv_msg(&sent_msg).await; + + // Bob quotes received message and sends a reply to Alice. + let mut reply = Message::new_text("Reply".to_owned()); + reply.set_quote(&bob, Some(&received_msg)).await?; + let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await; + let received_reply = alice.recv_msg(&sent_reply).await; + + // Alice forwards a reply. + forward_msgs(&alice, &[received_reply.id], alice_chat.get_id()).await?; + let forwarded_msg = alice.pop_sent_msg().await; + let alice_forwarded_msg = bob.recv_msg(&forwarded_msg).await; + assert!(alice_forwarded_msg.quoted_message(&alice).await?.is_none()); + assert_eq!( + alice_forwarded_msg.quoted_text(), + Some("Hi Bob".to_string()) + ); + + let bob_forwarded_msg = bob.get_last_msg().await; + assert!(bob_forwarded_msg.quoted_message(&bob).await?.is_none()); + assert_eq!(bob_forwarded_msg.quoted_text(), Some("Hi Bob".to_string())); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forward_group() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let alice_chat = alice.create_chat(&bob).await; + let bob_chat = bob.create_chat(&alice).await; + + // Alice creates a group with Bob. + let alice_group_chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?; + let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?; + add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?; + add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?; + let sent_group_msg = alice + .send_text(alice_group_chat_id, "Hi Bob and Claire") + .await; + let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id; + + // Alice deletes a message on her device. + // This is needed to make assignment of further messages received in this group + // based on `References:` header harder. + // Previously this exposed a bug, so this is a regression test. + message::delete_msgs(&alice, &[sent_group_msg.sender_msg_id]).await?; + + // Alice sends a message to Bob. + let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; + let received_msg = bob.recv_msg(&sent_msg).await; + assert_eq!(received_msg.get_text(), "Hi Bob"); + assert_eq!(received_msg.chat_id, bob_chat.id); + + // Alice sends another message to Bob, this has first message as a parent. + let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await; + let received_msg = bob.recv_msg(&sent_msg).await; + assert_eq!(received_msg.get_text(), "Hello Bob"); + assert_eq!(received_msg.chat_id, bob_chat.id); + + // Bob forwards message to a group chat with Alice. + forward_msgs(&bob, &[received_msg.id], bob_group_chat_id).await?; + let forwarded_msg = bob.pop_sent_msg().await; + alice.recv_msg(&forwarded_msg).await; + + let received_forwarded_msg = alice.get_last_msg_in(alice_group_chat_id).await; + assert!(received_forwarded_msg.is_forwarded()); + assert_eq!(received_forwarded_msg.chat_id, alice_group_chat_id); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_only_minimal_data_are_forwarded() -> Result<()> { + // send a message from Alice to a group with Bob + let alice = TestContext::new_alice().await; + alice + .set_config(Config::Displayname, Some("secretname")) + .await?; + let bob_id = Contact::create(&alice, "bob", "bob@example.net").await?; + let group_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?; + add_contact_to_chat(&alice, group_id, bob_id).await?; + let mut msg = Message::new_text("bla foo".to_owned()); + let sent_msg = alice.send_msg(group_id, &mut msg).await; + assert!(sent_msg.payload().contains("secretgrpname")); + assert!(sent_msg.payload().contains("secretname")); + assert!(sent_msg.payload().contains("alice")); + + // Bob forwards that message to Claire - + // Claire should not get information about Alice for the original Group + let bob = TestContext::new_bob().await; + let orig_msg = bob.recv_msg(&sent_msg).await; + let claire_id = Contact::create(&bob, "claire", "claire@foo").await?; + let single_id = ChatId::create_for_contact(&bob, claire_id).await?; + let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?; + add_contact_to_chat(&bob, group_id, claire_id).await?; + let broadcast_id = create_broadcast_list(&bob).await?; + add_contact_to_chat(&bob, broadcast_id, claire_id).await?; + for chat_id in &[single_id, group_id, broadcast_id] { + forward_msgs(&bob, &[orig_msg.id], *chat_id).await?; + let sent_msg = bob.pop_sent_msg().await; + assert!(sent_msg + .payload() + .contains("---------- Forwarded message ----------")); + assert!(!sent_msg.payload().contains("secretgrpname")); + assert!(!sent_msg.payload().contains("secretname")); + assert!(!sent_msg.payload().contains("alice")); + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_save_msgs() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + + let sent = alice.send_text(alice_chat.get_id(), "hi, bob").await; + let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?; + assert!(sent_msg.get_saved_msg_id(&alice).await?.is_none()); + assert!(sent_msg.get_original_msg_id(&alice).await?.is_none()); + + let self_chat = alice.get_self_chat().await; + save_msgs(&alice, &[sent.sender_msg_id]).await?; + + let saved_msg = alice.get_last_msg_in(self_chat.id).await; + assert_ne!(saved_msg.get_id(), sent.sender_msg_id); + assert!(saved_msg.get_saved_msg_id(&alice).await?.is_none()); + assert_eq!( + saved_msg.get_original_msg_id(&alice).await?.unwrap(), + sent.sender_msg_id + ); + assert_eq!(saved_msg.get_text(), "hi, bob"); + assert!(!saved_msg.is_forwarded()); // UI should not flag "saved messages" as "forwarded" + assert_eq!(saved_msg.is_dc_message, MessengerMessage::Yes); + assert_eq!(saved_msg.get_from_id(), ContactId::SELF); + assert_eq!(saved_msg.get_state(), MessageState::OutDelivered); + assert_ne!(saved_msg.rfc724_mid(), sent_msg.rfc724_mid()); + + let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?; + assert_eq!( + sent_msg.get_saved_msg_id(&alice).await?.unwrap(), + saved_msg.id + ); + assert!(sent_msg.get_original_msg_id(&alice).await?.is_none()); + + let rcvd_msg = bob.recv_msg(&sent).await; + let self_chat = bob.get_self_chat().await; + save_msgs(&bob, &[rcvd_msg.id]).await?; + let saved_msg = bob.get_last_msg_in(self_chat.id).await; + assert_ne!(saved_msg.get_id(), rcvd_msg.id); + assert_eq!( + saved_msg.get_original_msg_id(&bob).await?.unwrap(), + rcvd_msg.id + ); + assert_eq!(saved_msg.get_text(), "hi, bob"); + assert!(!saved_msg.is_forwarded()); + assert_eq!(saved_msg.is_dc_message, MessengerMessage::Yes); + assert_ne!(saved_msg.get_from_id(), ContactId::SELF); + assert_eq!(saved_msg.get_state(), MessageState::InSeen); + assert_ne!(saved_msg.rfc724_mid(), rcvd_msg.rfc724_mid()); + + // delete original message + delete_msgs(&bob, &[rcvd_msg.id]).await?; + let saved_msg = Message::load_from_db(&bob, saved_msg.id).await?; + assert!(saved_msg.get_original_msg_id(&bob).await?.is_none()); + + // delete original chat + rcvd_msg.chat_id.delete(&bob).await?; + let msg = Message::load_from_db(&bob, saved_msg.id).await?; + assert!(msg.get_original_msg_id(&bob).await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_saved_msgs_not_added_to_shared_chats() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + let msg = tcm.send_recv_accept(&alice, &bob, "hi, bob").await; + + let self_chat = bob.get_self_chat().await; + save_msgs(&bob, &[msg.id]).await?; + let msg = bob.get_last_msg_in(self_chat.id).await; + let contact = Contact::get_by_id(&bob, msg.get_from_id()).await?; + assert_eq!(contact.get_addr(), "alice@example.org"); + + let shared_chats = Chatlist::try_load(&bob, 0, None, Some(contact.id)).await?; + assert_eq!(shared_chats.len(), 1); + assert_eq!( + shared_chats.get_chat_id(0).unwrap(), + bob.get_chat(&alice).await.id + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forward_from_saved_to_saved() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let sent = alice.send_text(alice.create_chat(&bob).await.id, "k").await; + + bob.recv_msg(&sent).await; + let orig = bob.get_last_msg().await; + let self_chat = bob.get_self_chat().await; + save_msgs(&bob, &[orig.id]).await?; + let saved1 = bob.get_last_msg().await; + assert_eq!( + saved1.get_original_msg_id(&bob).await?.unwrap(), + sent.sender_msg_id + ); + assert_ne!(saved1.from_id, ContactId::SELF); + + forward_msgs(&bob, &[saved1.id], self_chat.id).await?; + let saved2 = bob.get_last_msg().await; + assert!(saved2.get_original_msg_id(&bob).await?.is_none(),); + assert_eq!(saved2.from_id, ContactId::SELF); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_save_from_saved_to_saved_failing() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let sent = alice.send_text(alice.create_chat(&bob).await.id, "k").await; + + bob.recv_msg(&sent).await; + let orig = bob.get_last_msg().await; + save_msgs(&bob, &[orig.id]).await?; + let saved1 = bob.get_last_msg().await; + + let result = save_msgs(&bob, &[saved1.id]).await; + assert!(result.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_resend_own_message() -> Result<()> { + // Alice creates group with Bob and sends an initial message + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + let sent1 = alice.send_text(alice_grp, "alice->bob").await; + + // Alice adds Claire to group and resends her own initial message + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "claire@example.org").await?, + ) + .await?; + let sent2 = alice.pop_sent_msg().await; + let resent_msg_id = sent1.sender_msg_id; + resend_msgs(&alice, &[resent_msg_id]).await?; + assert_eq!( + resent_msg_id.get_state(&alice).await?, + MessageState::OutPending + ); + resend_msgs(&alice, &[resent_msg_id]).await?; + // Message can be re-sent multiple times. + assert_eq!( + resent_msg_id.get_state(&alice).await?, + MessageState::OutPending + ); + alice.pop_sent_msg().await; + // There's still one more pending SMTP job. + assert_eq!( + resent_msg_id.get_state(&alice).await?, + MessageState::OutPending + ); + let sent3 = alice.pop_sent_msg().await; + assert_eq!( + resent_msg_id.get_state(&alice).await?, + MessageState::OutDelivered + ); + + // Bob receives all messages + let bob = TestContext::new_bob().await; + let msg = bob.recv_msg(&sent1).await; + let sent1_ts_sent = msg.timestamp_sent; + assert_eq!(msg.get_text(), "alice->bob"); + assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2); + assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1); + bob.recv_msg(&sent2).await; + assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); + assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2); + let received = bob.recv_msg_opt(&sent3).await; + // No message should actually be added since we already know this message: + assert!(received.is_none()); + assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); + assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2); + + // Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob + let claire = TestContext::new().await; + claire.configure_addr("claire@example.org").await; + claire.recv_msg(&sent2).await; + let msg = claire.recv_msg(&sent3).await; + assert_eq!(msg.get_text(), "alice->bob"); + assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3); + assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2); + let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?; + assert_eq!(msg_from.get_addr(), "alice@example.org"); + assert!(sent1_ts_sent < msg.timestamp_sent); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_resend_foreign_message_fails() -> Result<()> { + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + let sent1 = alice.send_text(alice_grp, "alice->bob").await; + + let bob = TestContext::new_bob().await; + let msg = bob.recv_msg(&sent1).await; + assert!(resend_msgs(&bob, &[msg.id]).await.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_resend_opportunistically_encryption() -> Result<()> { + // Alice creates group with Bob and sends an initial message + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + let sent1 = alice.send_text(alice_grp, "alice->bob").await; + + // Bob now can send an encrypted message + let bob = TestContext::new_bob().await; + let msg = bob.recv_msg(&sent1).await; + assert!(!msg.get_showpadlock()); + + msg.chat_id.accept(&bob).await?; + let sent2 = bob.send_text(msg.chat_id, "bob->alice").await; + let msg = bob.get_last_msg().await; + assert!(msg.get_showpadlock()); + + // Bob adds Claire and resends his last message: this will drop encryption in opportunistic chats + add_contact_to_chat( + &bob, + msg.chat_id, + Contact::create(&bob, "", "claire@example.org").await?, + ) + .await?; + let _sent3 = bob.pop_sent_msg().await; + resend_msgs(&bob, &[sent2.sender_msg_id]).await?; + let _sent4 = bob.pop_sent_msg().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_resend_info_message_fails() -> Result<()> { + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + alice.send_text(alice_grp, "alice->bob").await; + + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "claire@example.org").await?, + ) + .await?; + let sent2 = alice.pop_sent_msg().await; + assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_can_send_group() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = Contact::create(&alice, "", "bob@f.br").await?; + let chat_id = ChatId::create_for_contact(&alice, bob).await?; + let chat = Chat::load_from_db(&alice, chat_id).await?; + assert!(chat.can_send(&alice).await?); + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + assert_eq!( + Chat::load_from_db(&alice, chat_id) + .await? + .can_send(&alice) + .await?, + true + ); + remove_contact_from_chat(&alice, chat_id, ContactId::SELF).await?; + assert_eq!( + Chat::load_from_db(&alice, chat_id) + .await? + .can_send(&alice) + .await?, + false + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_broadcast() -> Result<()> { + // create two context, send two messages so both know the other + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let chat_alice = alice.create_chat(&bob).await; + send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; + bob.recv_msg(&alice.pop_sent_msg().await).await; + + let chat_bob = bob.create_chat(&alice).await; + send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; + let msg = alice.recv_msg(&bob.pop_sent_msg().await).await; + assert!(msg.get_showpadlock()); + + // test broadcast list + let broadcast_id = create_broadcast_list(&alice).await?; + add_contact_to_chat( + &alice, + broadcast_id, + get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(), + ) + .await?; + set_chat_name(&alice, broadcast_id, "Broadcast list").await?; + { + let chat = Chat::load_from_db(&alice, broadcast_id).await?; + assert_eq!(chat.typ, Chattype::Broadcast); + assert_eq!(chat.name, "Broadcast list"); + assert!(!chat.is_self_talk()); + + send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.chat_id, chat.id); + } + + { + let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), "ola!"); + assert_eq!(msg.subject, "Broadcast list"); + assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data + let chat = Chat::load_from_db(&bob, msg.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_ne!(chat.id, chat_bob.id); + assert_eq!(chat.name, "Broadcast list"); + assert!(!chat.is_self_talk()); + } + + { + // Alice changes the name: + set_chat_name(&alice, broadcast_id, "My great broadcast").await?; + let sent = alice.send_text(broadcast_id, "I changed the title!").await; + + let msg = bob.recv_msg(&sent).await; + assert_eq!(msg.subject, "Re: My great broadcast"); + let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?; + assert_eq!(bob_chat.name, "My great broadcast"); + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_broadcast_multidev() -> Result<()> { + let alices = [ + TestContext::new_alice().await, + TestContext::new_alice().await, + ]; + let bob = TestContext::new_bob().await; + let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id; + + let a0_broadcast_id = create_broadcast_list(&alices[0]).await?; + let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?; + set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?; + let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await; + let msg = alices[1].recv_msg(&sent_msg).await; + let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid) + .await? + .unwrap() + .0; + assert_eq!(msg.chat_id, a1_broadcast_id); + let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?; + assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast); + assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42"); + assert!(get_chat_contacts(&alices[1], a1_broadcast_id) + .await? + .is_empty()); + + add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?; + set_chat_name(&alices[1], a1_broadcast_id, "Broadcast list 43").await?; + let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await; + let msg = alices[0].recv_msg(&sent_msg).await; + assert_eq!(msg.chat_id, a0_broadcast_id); + let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?; + assert_eq!(a0_broadcast_chat.get_type(), Chattype::Broadcast); + assert_eq!(a0_broadcast_chat.get_name(), "Broadcast list 42"); + assert!(get_chat_contacts(&alices[0], a0_broadcast_id) + .await? + .is_empty()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_for_contact_with_blocked() -> Result<()> { + let t = TestContext::new().await; + let (contact_id, _) = Contact::add_or_lookup( + &t, + "", + &ContactAddress::new("foo@bar.org")?, + Origin::ManuallyCreated, + ) + .await?; + + // create a blocked chat + let chat_id_orig = + ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; + assert!(!chat_id_orig.is_special()); + let chat = Chat::load_from_db(&t, chat_id_orig).await?; + assert_eq!(chat.blocked, Blocked::Yes); + + // repeating the call, the same chat must still be blocked + let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; + assert_eq!(chat_id, chat_id_orig); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.blocked, Blocked::Yes); + + // already created chats are unblocked if requested + let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Not).await?; + assert_eq!(chat_id, chat_id_orig); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.blocked, Blocked::Not); + + // however, already created chats are not re-blocked + let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; + assert_eq!(chat_id, chat_id_orig); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.blocked, Blocked::Not); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_get_encryption_info() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?; + let contact_fiona = Contact::create(&alice, "", "fiona@example.net").await?; + + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + assert_eq!(chat_id.get_encryption_info(&alice).await?, ""); + + add_contact_to_chat(&alice, chat_id, contact_bob).await?; + assert_eq!( + chat_id.get_encryption_info(&alice).await?, + "No encryption:\n\ + bob@example.net" + ); + + add_contact_to_chat(&alice, chat_id, contact_fiona).await?; + assert_eq!( + chat_id.get_encryption_info(&alice).await?, + "No encryption:\n\ + fiona@example.net\n\ + bob@example.net" + ); + + let direct_chat = bob.create_chat(&alice).await; + send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?; + alice.recv_msg(&bob.pop_sent_msg().await).await; + + assert_eq!( + chat_id.get_encryption_info(&alice).await?, + "No encryption:\n\ + fiona@example.net\n\ + \n\ + End-to-end encryption preferred:\n\ + bob@example.net" + ); + + bob.set_config(Config::E2eeEnabled, Some("0")).await?; + send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?; + alice.recv_msg(&bob.pop_sent_msg().await).await; + + assert_eq!( + chat_id.get_encryption_info(&alice).await?, + "No encryption:\n\ + fiona@example.net\n\ + \n\ + End-to-end encryption available:\n\ + bob@example.net" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_chat_media() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?; + + assert_eq!( + get_chat_media( + &t, + Some(chat_id1), + Viewtype::Image, + Viewtype::Sticker, + Viewtype::Unknown + ) + .await? + .len(), + 0 + ); + + async fn send_media( + t: &TestContext, + chat_id: ChatId, + msg_type: Viewtype, + name: &str, + bytes: &[u8], + ) -> Result { + let file = t.get_blobdir().join(name); + tokio::fs::write(&file, bytes).await?; + let mut msg = Message::new(msg_type); + msg.set_file(file.to_str().unwrap(), None); + send_msg(t, chat_id, &mut msg).await + } + + send_media( + &t, + chat_id1, + Viewtype::Image, + "a.jpg", + include_bytes!("../../test-data/image/rectangle200x180-rotated.jpg"), + ) + .await?; + send_media( + &t, + chat_id1, + Viewtype::Sticker, + "b.png", + include_bytes!("../../test-data/image/logo.png"), + ) + .await?; + let second_image_msg_id = send_media( + &t, + chat_id2, + Viewtype::Image, + "c.jpg", + include_bytes!("../../test-data/image/avatar64x64.png"), + ) + .await?; + send_media( + &t, + chat_id2, + Viewtype::Webxdc, + "d.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + ) + .await?; + + assert_eq!( + get_chat_media( + &t, + Some(chat_id1), + Viewtype::Image, + Viewtype::Unknown, + Viewtype::Unknown, + ) + .await? + .len(), + 1 + ); + assert_eq!( + get_chat_media( + &t, + Some(chat_id1), + Viewtype::Sticker, + Viewtype::Unknown, + Viewtype::Unknown, + ) + .await? + .len(), + 1 + ); + assert_eq!( + get_chat_media( + &t, + Some(chat_id1), + Viewtype::Sticker, + Viewtype::Image, + Viewtype::Unknown, + ) + .await? + .len(), + 2 + ); + assert_eq!( + get_chat_media( + &t, + Some(chat_id2), + Viewtype::Webxdc, + Viewtype::Unknown, + Viewtype::Unknown, + ) + .await? + .len(), + 1 + ); + assert_eq!( + get_chat_media( + &t, + None, + Viewtype::Image, + Viewtype::Unknown, + Viewtype::Unknown, + ) + .await? + .len(), + 2 + ); + assert_eq!( + get_chat_media( + &t, + None, + Viewtype::Image, + Viewtype::Sticker, + Viewtype::Unknown, + ) + .await? + .len(), + 3 + ); + assert_eq!( + get_chat_media( + &t, + None, + Viewtype::Image, + Viewtype::Sticker, + Viewtype::Webxdc, + ) + .await? + .len(), + 4 + ); + + // Delete an image. + delete_msgs(&t, &[second_image_msg_id]).await?; + assert_eq!( + get_chat_media( + &t, + None, + Viewtype::Image, + Viewtype::Sticker, + Viewtype::Webxdc, + ) + .await? + .len(), + 3 + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_blob_renaming() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat( + &alice, + chat_id, + Contact::create(&alice, "bob", "bob@example.net").await?, + ) + .await?; + let dir = tempfile::tempdir()?; + let file = dir.path().join("harmless_file.\u{202e}txt.exe"); + fs::write(&file, "aaa").await?; + let mut msg = Message::new(Viewtype::File); + msg.set_file(file.to_str().unwrap(), None); + let msg = bob.recv_msg(&alice.send_msg(chat_id, &mut msg).await).await; + + // the file bob receives should not contain BIDI-control characters + assert_eq!( + Some("$BLOBDIR/harmless_file.txt.exe"), + msg.param.get(Param::File), + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_blocked() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let bob = TestContext::new_bob().await; + + let ba_chat = bob.create_chat(alice0).await; + let sent_msg = bob.send_text(ba_chat.id, "hi").await; + let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; + alice1.recv_msg(&sent_msg).await; + let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id; + + assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request); + a0b_chat_id.accept(alice0).await?; + sync(alice0, alice1).await; + assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not); + a0b_chat_id.block(alice0).await?; + sync(alice0, alice1).await; + assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Yes); + a0b_chat_id.unblock(alice0).await?; + sync(alice0, alice1).await; + assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not); + + // Unblocking a 1:1 chat doesn't unblock the contact currently. + Contact::unblock(alice0, a0b_contact_id).await?; + + assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked()); + Contact::block(alice0, a0b_contact_id).await?; + sync(alice0, alice1).await; + assert!(alice1.add_or_lookup_contact(&bob).await.is_blocked()); + Contact::unblock(alice0, a0b_contact_id).await?; + sync(alice0, alice1).await; + assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked()); + + // Test accepting and blocking groups. This way we test: + // - Group chats synchronisation. + // - That blocking a group deletes it on other devices. + let fiona = TestContext::new_fiona().await; + let fiona_grp_chat_id = fiona + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0]) + .await; + let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await; + let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; + let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id; + let a1_grp_chat = Chat::load_from_db(alice1, a1_grp_chat_id).await?; + assert_eq!(a1_grp_chat.blocked, Blocked::Request); + a0_grp_chat_id.accept(alice0).await?; + sync(alice0, alice1).await; + let a1_grp_chat = Chat::load_from_db(alice1, a1_grp_chat_id).await?; + assert_eq!(a1_grp_chat.blocked, Blocked::Not); + a0_grp_chat_id.block(alice0).await?; + sync(alice0, alice1).await; + assert!(Chat::load_from_db(alice1, a1_grp_chat_id).await.is_err()); + assert!( + !alice1 + .sql + .exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,)) + .await? + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_accept_before_first_msg() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let bob = TestContext::new_bob().await; + + let ba_chat = bob.create_chat(alice0).await; + let sent_msg = bob.send_text(ba_chat.id, "hi").await; + let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; + assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request); + a0b_chat_id.accept(alice0).await?; + let a0b_contact = alice0.add_or_lookup_contact(&bob).await; + assert_eq!(a0b_contact.origin, Origin::CreateChat); + assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not); + + sync(alice0, alice1).await; + let a1b_contact = alice1.add_or_lookup_contact(&bob).await; + assert_eq!(a1b_contact.origin, Origin::CreateChat); + let a1b_chat = alice1.get_chat(&bob).await; + assert_eq!(a1b_chat.blocked, Blocked::Not); + let chats = Chatlist::try_load(alice1, 0, None, None).await?; + assert_eq!(chats.len(), 1); + + let rcvd_msg = alice1.recv_msg(&sent_msg).await; + assert_eq!(rcvd_msg.chat_id, a1b_chat.id); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_block_before_first_msg() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let bob = TestContext::new_bob().await; + + let ba_chat = bob.create_chat(alice0).await; + let sent_msg = bob.send_text(ba_chat.id, "hi").await; + let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; + assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request); + a0b_chat_id.block(alice0).await?; + let a0b_contact = alice0.add_or_lookup_contact(&bob).await; + assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom); + assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes); + + sync(alice0, alice1).await; + let a1b_contact = alice1.add_or_lookup_contact(&bob).await; + assert_eq!(a1b_contact.origin, Origin::Hidden); + assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id) + .await? + .is_none()); + + let rcvd_msg = alice1.recv_msg(&sent_msg).await; + let a1b_contact = alice1.add_or_lookup_contact(&bob).await; + assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom); + let a1b_chat = alice1.get_chat(&bob).await; + assert_eq!(a1b_chat.blocked, Blocked::Yes); + assert_eq!(rcvd_msg.chat_id, a1b_chat.id); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_adhoc_grp() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + + let mut chat_ids = Vec::new(); + for a in [alice0, alice1] { + let msg = receive_imf( + a, + b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\ + From: alice@example.org\r\n\ + To: , \r\n\ + Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\ + Message-ID: \r\n\ + Chat-Version: 1.0\r\n\ + \r\n\ + hi\r\n", + false, + ) + .await? + .unwrap(); + chat_ids.push(msg.chat_id); + } + let chat1 = Chat::load_from_db(alice1, chat_ids[1]).await?; + assert_eq!(chat1.typ, Chattype::Group); + assert!(chat1.grpid.is_empty()); + + // Test synchronisation on chat blocking because it causes chat deletion currently and thus + // requires generating a sync message in advance. + chat_ids[0].block(alice0).await?; + sync(alice0, alice1).await; + assert!(Chat::load_from_db(alice1, chat_ids[1]).await.is_err()); + assert!( + !alice1 + .sql + .exists("SELECT COUNT(*) FROM chats WHERE id=?", (chat_ids[1],)) + .await? + ); + + Ok(()) +} + +/// Tests syncing of chat visibility on a self-chat. This way we test: +/// - Self-chat synchronisation. +/// - That sync messages don't unarchive the self-chat. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_visibility() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let a0self_chat_id = alice0.get_self_chat().await.id; + + assert_eq!( + alice1.get_self_chat().await.get_visibility(), + ChatVisibility::Normal + ); + let mut visibilities = ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal)); + visibilities.next(); + for v in visibilities { + a0self_chat_id.set_visibility(alice0, v).await?; + sync(alice0, alice1).await; + for a in [alice0, alice1] { + assert_eq!(a.get_self_chat().await.get_visibility(), v); + } + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_muted() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let bob = TestContext::new_bob().await; + let a0b_chat_id = alice0.create_chat(&bob).await.id; + alice1.create_chat(&bob).await; + + assert_eq!( + alice1.get_chat(&bob).await.mute_duration, + MuteDuration::NotMuted + ); + let mute_durations = [ + MuteDuration::Forever, + MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)), + MuteDuration::NotMuted, + ]; + for m in mute_durations { + set_muted(alice0, a0b_chat_id, m).await?; + sync(alice0, alice1).await; + let m = match m { + MuteDuration::Until(time) => MuteDuration::Until( + SystemTime::UNIX_EPOCH + + Duration::from_secs(time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs()), + ), + _ => m, + }; + assert_eq!(alice1.get_chat(&bob).await.mute_duration, m); + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_broadcast() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let bob = TestContext::new_bob().await; + let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id; + + let a0_broadcast_id = create_broadcast_list(alice0).await?; + sync(alice0, alice1).await; + let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?; + let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid) + .await? + .unwrap() + .0; + let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?; + assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast); + assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name()); + assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty()); + add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?; + sync(alice0, alice1).await; + let a1b_contact_id = Contact::lookup_id_by_addr( + alice1, + &bob.get_config(Config::Addr).await?.unwrap(), + Origin::Hidden, + ) + .await? + .unwrap(); + assert_eq!( + get_chat_contacts(alice1, a1_broadcast_id).await?, + vec![a1b_contact_id] + ); + let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await; + let msg = bob.recv_msg(&sent_msg).await; + let chat = Chat::load_from_db(&bob, msg.chat_id).await?; + assert_eq!(chat.get_type(), Chattype::Mailinglist); + let msg = alice0.recv_msg(&sent_msg).await; + assert_eq!(msg.chat_id, a0_broadcast_id); + remove_contact_from_chat(alice0, a0_broadcast_id, a0b_contact_id).await?; + sync(alice0, alice1).await; + assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_name() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let a0_broadcast_id = create_broadcast_list(alice0).await?; + sync(alice0, alice1).await; + let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?; + set_chat_name(alice0, a0_broadcast_id, "Broadcast list 42").await?; + sync(alice0, alice1).await; + let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid) + .await? + .unwrap() + .0; + let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?; + assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast); + assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42"); + Ok(()) +} + +/// Tests sending JPEG image with .png extension. +/// +/// This is a regression test, previously sending failed +/// because image was passed to PNG decoder +/// and it failed to decode image. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_jpeg_with_png_ext() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let bytes = include_bytes!("../../test-data/image/screenshot.jpg"); + let file = alice.get_blobdir().join("screenshot.png"); + tokio::fs::write(&file, bytes).await?; + let mut msg = Message::new(Viewtype::Image); + msg.set_file(file.to_str().unwrap(), None); + + let alice_chat = alice.create_chat(&bob).await; + let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; + let _msg = bob.recv_msg(&sent_msg).await; + + Ok(()) +} + +/// Tests that info message is ignored when constructing `In-Reply-To`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_info_not_referenced() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let bob_received_message = tcm.send_recv_accept(alice, bob, "Hi!").await; + let bob_chat_id = bob_received_message.chat_id; + add_info_msg(bob, bob_chat_id, "Some info", create_smeared_timestamp(bob)).await?; + + // Bob sends a message. + // This message should reference Alice's "Hi!" message and not the info message. + let sent = bob.send_text(bob_chat_id, "Hi hi!").await; + let mime_message = alice.parse_msg(&sent).await; + + let in_reply_to = mime_message.get_header(HeaderDef::InReplyTo).unwrap(); + assert_eq!( + in_reply_to, + format!("<{}>", bob_received_message.rfc724_mid) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_do_not_overwrite_draft() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let mut msg = Message::new_text("This is a draft message".to_string()); + let self_chat = alice.get_self_chat().await.id; + self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap(); + let draft1 = self_chat.get_draft(&alice).await?.unwrap(); + SystemTime::shift(Duration::from_secs(1)); + self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap(); + let draft2 = self_chat.get_draft(&alice).await?.unwrap(); + assert_eq!(draft1.timestamp_sort, draft2.timestamp_sort); + + Ok(()) +} + +/// Test group consistency. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_member_bug() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?; + let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; + + // Create a group. + let alice_chat_id = + create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; + + // Promote the group. + let alice_sent_msg = alice + .send_text(alice_chat_id, "Hi! I created a group.") + .await; + let bob_received_msg = bob.recv_msg(&alice_sent_msg).await; + + let bob_chat_id = bob_received_msg.get_chat_id(); + bob_chat_id.accept(bob).await?; + + // Alice removes Fiona from the chat. + remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; + let _alice_sent_add_msg = alice.pop_sent_msg().await; + + SystemTime::shift(Duration::from_secs(3600)); + + // Bob sends a message + // to Alice and Fiona because he still has not received + // a message about Fiona being removed. + let bob_sent_msg = bob.send_text(bob_chat_id, "Hi Alice!").await; + + // Alice receives a message. + // This should not add Fiona back. + let _alice_received_msg = alice.recv_msg(&bob_sent_msg).await; + + assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 2); + + Ok(()) +} + +/// Test that tombstones for past members are added to chats_contacts table +/// even if the row did not exist before. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_past_members() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = &tcm.alice().await; + let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; + + let alice_chat_id = + create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; + alice + .send_text(alice_chat_id, "Hi! I created a group.") + .await; + remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; + assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1); + + let bob = &tcm.bob().await; + let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); + let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; + add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + + let add_message = alice.pop_sent_msg().await; + let bob_add_message = bob.recv_msg(&add_message).await; + let bob_chat_id = bob_add_message.chat_id; + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); + assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn non_member_cannot_modify_member_list() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); + let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; + + let alice_chat_id = + create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + let alice_sent_msg = alice + .send_text(alice_chat_id, "Hi! I created a group.") + .await; + let bob_received_msg = bob.recv_msg(&alice_sent_msg).await; + let bob_chat_id = bob_received_msg.get_chat_id(); + bob_chat_id.accept(bob).await?; + + let bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?; + + // Alice removes Bob and Bob adds Fiona at the same time. + remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?; + + let bob_sent_add_msg = bob.pop_sent_msg().await; + + // Alice ignores Bob's message because Bob is not a member. + assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1); + alice.recv_msg_trash(&bob_sent_add_msg).await; + assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn unpromoted_group_no_tombstones() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); + let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; + let fiona_addr = "fiona@example.net"; + let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?; + + let alice_chat_id = + create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; + assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3); + assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0); + + remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; + assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 2); + + // There should be no tombstone because the group is not promoted yet. + assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0); + + let sent = alice.send_text(alice_chat_id, "Hello group!").await; + let payload = sent.payload(); + assert_eq!(payload.contains("Hello group!"), true); + assert_eq!(payload.contains(&bob_addr), true); + assert_eq!(payload.contains(fiona_addr), false); + + let bob_msg = bob.recv_msg(&sent).await; + let bob_chat_id = bob_msg.chat_id; + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); + assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 0); + + Ok(()) +} diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 8e0625957..41be42714 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -2345,1826 +2345,4 @@ async fn handle_ndn( } #[cfg(test)] -mod tests { - use mailparse::ParsedMail; - - use super::*; - use crate::{ - chat, - chatlist::Chatlist, - constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS}, - message::{MessageState, MessengerMessage}, - receive_imf::receive_imf, - test_utils::{TestContext, TestContextManager}, - tools::time, - }; - - impl AvatarAction { - pub fn is_change(&self) -> bool { - match self { - AvatarAction::Delete => false, - AvatarAction::Change(_) => true, - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mimeparser_fromheader() { - let ctx = TestContext::new_alice().await; - - let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi", None) - .await - .unwrap(); - let contact = mimemsg.from; - assert_eq!(contact.addr, "g@c.de"); - assert_eq!(contact.display_name, None); - - let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi", None) - .await - .unwrap(); - let contact = mimemsg.from; - assert_eq!(contact.addr, "g@c.de"); - assert_eq!(contact.display_name, None); - - let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \n\nhi", None) - .await - .unwrap(); - let contact = mimemsg.from; - assert_eq!(contact.addr, "g@c.de"); - assert_eq!(contact.display_name, None); - - let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C \n\nhi", None) - .await - .unwrap(); - let contact = mimemsg.from; - assert_eq!(contact.addr, "g@c.de"); - assert_eq!(contact.display_name, Some("Goetz C".to_string())); - - let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" \n\nhi", None) - .await - .unwrap(); - let contact = mimemsg.from; - assert_eq!(contact.addr, "g@c.de"); - assert_eq!(contact.display_name, Some("Goetz C".to_string())); - - let mimemsg = - MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C \n\nhi", None) - .await - .unwrap(); - let contact = mimemsg.from; - assert_eq!(contact.addr, "g@c.de"); - assert_eq!(contact.display_name, Some("Götz C".to_string())); - - // although RFC 2047 says, encoded-words shall not appear inside quoted-string, - // this combination is used in the wild eg. by MailMate - let mimemsg = MimeMessage::from_bytes( - &ctx, - b"From: \"=?utf-8?q?G=C3=B6tz?= C\" \n\nhi", - None, - ) - .await - .unwrap(); - let contact = mimemsg.from; - assert_eq!(contact.addr, "g@c.de"); - assert_eq!(contact.display_name, Some("Götz C".to_string())); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mimeparser_crash() { - let context = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/issue_523.txt"); - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - - assert_eq!(mimeparser.get_subject(), None); - assert_eq!(mimeparser.parts.len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_rfc724_mid_exists() { - let context = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/mail_with_message_id.txt"); - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - - assert_eq!( - mimeparser.get_rfc724_mid(), - Some("2dfdbde7@example.org".into()) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_rfc724_mid_not_exists() { - let context = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/issue_523.txt"); - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(mimeparser.get_rfc724_mid(), None); - } - - #[test] - fn test_get_recipients() { - let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); - let mail = mailparse::parse_mail(&raw[..]).unwrap(); - let recipients = get_recipients(&mail.headers); - assert!(recipients.iter().any(|info| info.addr == "abc@bcd.com")); - assert!(recipients.iter().any(|info| info.addr == "def@def.de")); - assert_eq!(recipients.len(), 2); - - // If some header is present multiple times, - // only the last one must be used. - let raw = b"From: alice@example.org\n\ - TO: mallory@example.com\n\ - To: mallory@example.net\n\ - To: bob@example.net\n\ - Content-Type: text/plain\n\ - Chat-Version: 1.0\n\ - \n\ - Hello\n\ - "; - let mail = mailparse::parse_mail(&raw[..]).unwrap(); - let recipients = get_recipients(&mail.headers); - assert!(recipients.iter().any(|info| info.addr == "bob@example.net")); - assert_eq!(recipients.len(), 1); - } - - #[test] - fn test_is_attachment() { - let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); - let mail = mailparse::parse_mail(raw).unwrap(); - assert!(!is_attachment_disposition(&mail)); - - let raw = include_bytes!("../test-data/message/mail_attach_txt.eml"); - let mail = mailparse::parse_mail(raw).unwrap(); - assert!(!is_attachment_disposition(&mail)); - assert!(!is_attachment_disposition(&mail.subparts[0])); - assert!(is_attachment_disposition(&mail.subparts[1])); - } - - fn load_mail_with_attachment<'a>(t: &'a TestContext, raw: &'a [u8]) -> ParsedMail<'a> { - let mail = mailparse::parse_mail(raw).unwrap(); - assert!(get_attachment_filename(t, &mail).unwrap().is_none()); - assert!(get_attachment_filename(t, &mail.subparts[0]) - .unwrap() - .is_none()); - mail - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_simple.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("test.html".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_encoded_words() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_encoded_words.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("Maßnahmen Okt. 2020.html".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_encoded_words_binary() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_encoded_words_binary.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some(" § 165 Abs".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_encoded_words_windows1251() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_encoded_words_windows1251.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("file Что нового 2020.pdf".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_encoded_words_cont() { - // test continued encoded-words and also test apostropes work that way - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_encoded_words_cont.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("Maßn'ah'men Okt. 2020.html".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_encoded_words_bad_delimiter() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_encoded_words_bad_delimiter.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - // not decoded as a space is missing after encoded-words part - assert_eq!(filename, Some("=?utf-8?q?foo?=.bar".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_apostrophed() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_apostrophed.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("Maßnahmen Okt. 2021.html".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_apostrophed_cont() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_apostrophed_cont.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("Maßnahmen März 2022.html".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_apostrophed_windows1251() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_apostrophed_windows1251.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("программирование.HTM".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_apostrophed_cp1252() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_apostrophed_cp1252.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("Auftragsbestätigung.pdf".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_apostrophed_invalid() { - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_apostrophed_invalid.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("somedäüta.html.zip".to_string())) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_attachment_filename_combined() { - // test that if `filename` and `filename*0` are given, the filename is not doubled - let t = TestContext::new().await; - let mail = load_mail_with_attachment( - &t, - include_bytes!("../test-data/message/attach_filename_combined.eml"), - ); - let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); - assert_eq!(filename, Some("Maßnahmen Okt. 2020.html".to_string())) - } - - #[test] - fn test_mailparse_content_type() { - let ctype = - mailparse::parse_content_type("text/plain; charset=utf-8; protected-headers=v1;"); - - assert_eq!(ctype.mimetype, "text/plain"); - assert_eq!(ctype.charset, "utf-8"); - assert_eq!( - ctype.params.get("protected-headers"), - Some(&"v1".to_string()) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_first_addr() { - let context = TestContext::new().await; - let raw = b"From: hello@one.org, world@two.org\n\ - Chat-Disposition-Notification-To: wrong\n\ - Content-Type: text/plain\n\ - Chat-Version: 1.0\n\ - \n\ - test1\n\ - "; - - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await; - - assert!(mimeparser.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_parent_timestamp() { - let context = TestContext::new_alice().await; - let raw = b"From: foo@example.org\n\ - Content-Type: text/plain\n\ - Chat-Version: 1.0\n\ - In-Reply-To: \n\ - \n\ - Some reply\n\ - "; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(), - None - ); - let timestamp = 1570435529; - context - .ctx - .sql - .execute( - "INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)", - ("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp), - ) - .await - .expect("Failed to write to the database"); - assert_eq!( - mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(), - Some(timestamp) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mimeparser_with_context() { - let context = TestContext::new_alice().await; - let raw = b"From: hello@example.org\n\ - Content-Type: multipart/mixed; boundary=\"==break==\";\n\ - Subject: outer-subject\n\ - Secure-Join-Group: no\n\ - Secure-Join-Fingerprint: 123456\n\ - Test-Header: Bar\n\ - chat-VERSION: 0.0\n\ - \n\ - --==break==\n\ - Content-Type: text/plain; protected-headers=\"v1\";\n\ - Subject: inner-subject\n\ - SecureBar-Join-Group: yes\n\ - Test-Header: Xy\n\ - chat-VERSION: 1.0\n\ - \n\ - test1\n\ - \n\ - --==break==--\n\ - \n"; - - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - - // non-overwritten headers do not bubble up - let of = mimeparser.get_header(HeaderDef::SecureJoinGroup).unwrap(); - assert_eq!(of, "no"); - - // unknown headers do not bubble upwards - let of = mimeparser.get_header(HeaderDef::TestHeader).unwrap(); - assert_eq!(of, "Bar"); - - // the following fields would bubble up - // if the test would really use encryption for the protected part - // however, as this is not the case, the outer things stay valid. - // for Chat-Version, also the case-insensivity is tested. - assert_eq!(mimeparser.get_subject(), Some("outer-subject".into())); - - let of = mimeparser.get_header(HeaderDef::ChatVersion).unwrap(); - assert_eq!(of, "0.0"); - assert_eq!(mimeparser.parts.len(), 1); - - // make sure, headers that are only allowed in the encrypted part - // cannot be set from the outer part - assert!(mimeparser - .get_header(HeaderDef::SecureJoinFingerprint) - .is_none()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mimeparser_with_avatars() { - let t = TestContext::new_alice().await; - - let raw = include_bytes!("../test-data/message/mail_attach_txt.eml"); - let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); - assert_eq!(mimeparser.user_avatar, None); - assert_eq!(mimeparser.group_avatar, None); - - let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml"); - let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); - assert_eq!(mimeparser.parts.len(), 1); - assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); - assert!(mimeparser.user_avatar.unwrap().is_change()); - assert_eq!(mimeparser.group_avatar, None); - - let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml"); - let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); - assert_eq!(mimeparser.parts.len(), 1); - assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); - assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete)); - assert_eq!(mimeparser.group_avatar, None); - - let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml"); - let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); - assert_eq!(mimeparser.parts.len(), 1); - assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); - assert!(mimeparser.user_avatar.unwrap().is_change()); - assert!(mimeparser.group_avatar.unwrap().is_change()); - - // if the Chat-User-Avatar header is missing, the avatar become a normal attachment - let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml"); - let raw = String::from_utf8_lossy(raw).to_string(); - let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:"); - let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes(), None) - .await - .unwrap(); - assert_eq!(mimeparser.parts.len(), 1); - assert_eq!(mimeparser.parts[0].typ, Viewtype::Image); - assert_eq!(mimeparser.user_avatar, None); - assert!(mimeparser.group_avatar.unwrap().is_change()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mimeparser_with_videochat() { - let t = TestContext::new_alice().await; - - let raw = include_bytes!("../test-data/message/videochat_invitation.eml"); - let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); - assert_eq!(mimeparser.parts.len(), 1); - assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation); - assert_eq!( - mimeparser.parts[0] - .param - .get(Param::WebrtcRoom) - .unwrap_or_default(), - "https://example.org/p2p/?roomname=6HiduoAn4xN" - ); - assert!(mimeparser.parts[0] - .msg - .contains("https://example.org/p2p/?roomname=6HiduoAn4xN")); - assert_eq!(mimeparser.user_avatar, None); - assert_eq!(mimeparser.group_avatar, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mimeparser_message_kml() { - let context = TestContext::new_alice().await; - let raw = b"Chat-Version: 1.0\n\ -From: foo \n\ -To: bar \n\ -Subject: Location streaming\n\ -Content-Type: multipart/mixed; boundary=\"==break==\"\n\ -\n\ -\n\ ---==break==\n\ -Content-Type: text/plain; charset=utf-8\n\ -\n\ ---\n\ -Sent with my Delta Chat Messenger: https://delta.chat\n\ -\n\ ---==break==\n\ -Content-Type: application/vnd.google-earth.kml+xml\n\ -Content-Disposition: attachment; filename=\"message.kml\"\n\ -\n\ -\n\ -\n\ -\n\ -XXX0.0,0.0\n\ -\n\ -\n\ -\n\ ---==break==--\n\ -;"; - - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - mimeparser.get_subject(), - Some("Location streaming".to_string()) - ); - assert!(mimeparser.location_kml.is_none()); - assert!(mimeparser.message_kml.is_some()); - - // There is only one part because message.kml attachment is special - // and only goes into message_kml. - assert_eq!(mimeparser.parts.len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_mdn() { - let context = TestContext::new_alice().await; - let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ -Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ -Chat-Version: 1.0\n\ -Message-ID: \n\ -To: Alice \n\ -From: Bob \n\ -Auto-Submitted: auto-replied\n\ -Content-Type: multipart/report; report-type=disposition-notification;\n\t\ -boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ -Content-Type: text/plain; charset=utf-8\n\ -\n\ -The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ -\n\ -This is no guarantee the content was read.\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ -Content-Type: message/disposition-notification\n\ -\n\ -Reporting-UA: Delta Chat 1.0.0-beta.22\n\ -Original-Recipient: rfc822;bob@example.org\n\ -Final-Recipient: rfc822;bob@example.org\n\ -Original-Message-ID: \n\ -Disposition: manual-action/MDN-sent-automatically; displayed\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ -"; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - message.get_subject(), - Some("Chat: Message opened".to_string()) - ); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.mdn_reports.len(), 1); - assert_eq!(message.is_bot, None); - } - - /// Test parsing multiple MDNs combined in a single message. - /// - /// RFC 6522 specifically allows MDNs to be nested inside - /// multipart MIME messages. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_multiple_mdns() { - let context = TestContext::new_alice().await; - let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ -Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ -Chat-Version: 1.0\n\ -Message-ID: \n\ -To: Alice \n\ -From: Bob \n\ -Content-Type: multipart/parallel; boundary=outer\n\ -\n\ -This is a multipart MDN.\n\ -\n\ ---outer\n\ -Content-Type: multipart/report; report-type=disposition-notification;\n\t\ -boundary=kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ -Content-Type: text/plain; charset=utf-8\n\ -\n\ -The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ -\n\ -This is no guarantee the content was read.\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ -Content-Type: message/disposition-notification\n\ -\n\ -Reporting-UA: Delta Chat 1.0.0-beta.22\n\ -Original-Recipient: rfc822;bob@example.org\n\ -Final-Recipient: rfc822;bob@example.org\n\ -Original-Message-ID: \n\ -Disposition: manual-action/MDN-sent-automatically; displayed\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ ---outer\n\ -Content-Type: multipart/report; report-type=disposition-notification;\n\t\ -boundary=zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\ -\n\ -\n\ ---zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\ -Content-Type: text/plain; charset=utf-8\n\ -\n\ -The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ -\n\ -This is no guarantee the content was read.\n\ -\n\ -\n\ ---zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\ -Content-Type: message/disposition-notification\n\ -\n\ -Reporting-UA: Delta Chat 1.0.0-beta.22\n\ -Original-Recipient: rfc822;bob@example.org\n\ -Final-Recipient: rfc822;bob@example.org\n\ -Original-Message-ID: \n\ -Disposition: manual-action/MDN-sent-automatically; displayed\n\ -\n\ -\n\ ---zuOJlsTfZAukyawEPVdIgqWjaM9w2W--\n\ ---outer--\n\ -"; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - message.get_subject(), - Some("Chat: Message opened".to_string()) - ); - - assert_eq!(message.parts.len(), 2); - assert_eq!(message.mdn_reports.len(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_mdn_with_additional_message_ids() { - let context = TestContext::new_alice().await; - let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ -Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ -Chat-Version: 1.0\n\ -Message-ID: \n\ -To: Alice \n\ -From: Bob \n\ -Content-Type: multipart/report; report-type=disposition-notification;\n\t\ -boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ -Content-Type: text/plain; charset=utf-8\n\ -\n\ -The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ -\n\ -This is no guarantee the content was read.\n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ -Content-Type: message/disposition-notification\n\ -\n\ -Reporting-UA: Delta Chat 1.0.0-beta.22\n\ -Original-Recipient: rfc822;bob@example.org\n\ -Final-Recipient: rfc822;bob@example.org\n\ -Original-Message-ID: \n\ -Disposition: manual-action/MDN-sent-automatically; displayed\n\ -Additional-Message-IDs: \n\ -\n\ -\n\ ---kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ -"; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - message.get_subject(), - Some("Chat: Message opened".to_string()) - ); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.mdn_reports.len(), 1); - assert_eq!( - message.mdn_reports[0].original_message_id, - Some("foo@example.org".to_string()) - ); - assert_eq!( - &message.mdn_reports[0].additional_message_ids, - &["foo@example.com", "foo@example.net"] - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_inline_attachment() { - let context = TestContext::new_alice().await; - let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) -From: sender@example.com -To: receiver@example.com -Subject: Mail with inline attachment -MIME-Version: 1.0 -Content-Type: multipart/mixed; - boundary="----=_Part_25_46172632.1581201680436" - -------=_Part_25_46172632.1581201680436 -Content-Type: text/plain; charset=utf-8 - -Hello! - -------=_Part_25_46172632.1581201680436 -Content-Type: application/pdf; name="some_pdf.pdf" -Content-Transfer-Encoding: base64 -Content-Disposition: inline; filename="some_pdf.pdf" - -JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl -Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM -MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== -------=_Part_25_46172632.1581201680436-- -"#; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - message.get_subject(), - Some("Mail with inline attachment".to_string()) - ); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::File); - assert_eq!(message.parts[0].msg, "Mail with inline attachment – Hello!"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_hide_html_without_content() { - let t = TestContext::new_alice().await; - let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) -From: sender@example.com -To: receiver@example.com -Subject: Mail with inline attachment -MIME-Version: 1.0 -Content-Type: multipart/mixed; - boundary="----=_Part_25_46172632.1581201680436" - -------=_Part_25_46172632.1581201680436 -Content-Type: text/html; charset=utf-8 - - - - - - - -------=_Part_25_46172632.1581201680436 -Content-Type: application/pdf; name="some_pdf.pdf" -Content-Transfer-Encoding: base64 -Content-Disposition: inline; filename="some_pdf.pdf" - -JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl -Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM -MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== -------=_Part_25_46172632.1581201680436-- -"#; - - let message = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::File); - assert_eq!(message.parts[0].msg, ""); - - // Make sure the file is there even though the html is wrong: - let param = &message.parts[0].param; - let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap(); - let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap(); - let size = f.metadata().await.unwrap().len(); - assert_eq!(size, 154); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn parse_inline_image() { - let context = TestContext::new_alice().await; - let raw = br#"Message-ID: -From: foo -Subject: example -To: bar@example.org -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="--11019878869865180" - -----11019878869865180 -Content-Type: text/plain; charset=utf-8 - -Test - -----11019878869865180 -Content-Type: image/jpeg; - name="JPEG_filename.jpg" -Content-Transfer-Encoding: base64 -Content-Disposition: inline; - filename="JPEG_filename.jpg" - -ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG -acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT -/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal -n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C -xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2 -rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU -8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI -K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4 -CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= - - -----11019878869865180-- -"#; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(message.get_subject(), Some("example".to_string())); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::Image); - assert_eq!(message.parts[0].msg, "example – Test"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn parse_thunderbird_html_embedded_image() { - let context = TestContext::new_alice().await; - let raw = br#"To: Alice -From: Bob -Subject: Test subject -Message-ID: -User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 - Thunderbird/68.7.0 -MIME-Version: 1.0 -Content-Type: multipart/alternative; - boundary="------------779C1631600DF3DB8C02E53A" -Content-Language: en-US - -This is a multi-part message in MIME format. ---------------779C1631600DF3DB8C02E53A -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: 7bit - -Test - - ---------------779C1631600DF3DB8C02E53A -Content-Type: multipart/related; - boundary="------------10CC6C2609EB38DA782C5CA9" - - ---------------10CC6C2609EB38DA782C5CA9 -Content-Type: text/html; charset=utf-8 -Content-Transfer-Encoding: 7bit - - - - - - -Test
-

- - - ---------------10CC6C2609EB38DA782C5CA9 -Content-Type: image/png; - name="1.png" -Content-Transfer-Encoding: base64 -Content-ID: -Content-Disposition: inline; - filename="1.png" - -ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG -acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT -/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal -n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C -xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2 -rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU -8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI -K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4 -CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= ---------------10CC6C2609EB38DA782C5CA9-- - ---------------779C1631600DF3DB8C02E53A--"#; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(message.get_subject(), Some("Test subject".to_string())); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::Image); - assert_eq!(message.parts[0].msg, "Test subject – Test"); - } - - // Outlook specifies filename in the "name" attribute of Content-Type - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn parse_outlook_html_embedded_image() { - let context = TestContext::new_alice().await; - let raw = br#"From: Anonymous -To: Anonymous -Subject: Delta Chat is great stuff! -Date: Tue, 5 May 2020 01:23:45 +0000 -MIME-Version: 1.0 -Content-Type: multipart/related; - boundary="----=_NextPart_000_0003_01D622B3.CA753E60" -X-Mailer: Microsoft Outlook 15.0 - -This is a multipart message in MIME format. - -------=_NextPart_000_0003_01D622B3.CA753E60 -Content-Type: multipart/alternative; - boundary="----=_NextPart_001_0004_01D622B3.CA753E60" - - -------=_NextPart_001_0004_01D622B3.CA753E60 -Content-Type: text/plain; - charset="us-ascii" -Content-Transfer-Encoding: 7bit - - - - -------=_NextPart_001_0004_01D622B3.CA753E60 -Content-Type: text/html; - charset="us-ascii" -Content-Transfer-Encoding: quoted-printable - - - -

-Test -

- - -------=_NextPart_001_0004_01D622B3.CA753E60-- - -------=_NextPart_000_0003_01D622B3.CA753E60 -Content-Type: image/jpeg; - name="image001.jpg" -Content-Transfer-Encoding: base64 -Content-ID: - -ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG -acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT -/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal -n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C -xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2 -rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU -8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI -K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4 -CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= - -------=_NextPart_000_0003_01D622B3.CA753E60-- -"#; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - message.get_subject(), - Some("Delta Chat is great stuff!".to_string()) - ); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::Image); - assert_eq!(message.parts[0].msg, "Delta Chat is great stuff! – Test"); - } - - #[test] - fn test_parse_message_id() { - let test = parse_message_id(""); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "foobar"); - - let test = parse_message_id(" "); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "foo"); - - let test = parse_message_id(" < foo > "); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "foo"); - - let test = parse_message_id("foo"); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "foo"); - - let test = parse_message_id(" foo "); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "foo"); - - let test = parse_message_id("foo bar"); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "foo"); - - let test = parse_message_id(" foo bar "); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "foo"); - - let test = parse_message_id(""); - assert!(test.is_err()); - - let test = parse_message_id(" "); - assert!(test.is_err()); - - let test = parse_message_id("<>"); - assert!(test.is_err()); - - let test = parse_message_id("<> bar"); - assert!(test.is_ok()); - assert_eq!(test.unwrap(), "bar"); - } - - #[test] - fn test_parse_message_ids() { - let test = parse_message_ids(" foo bar "); - assert_eq!(test.len(), 3); - assert_eq!(test[0], "foo"); - assert_eq!(test[1], "bar"); - assert_eq!(test[2], "foobar"); - - let test = parse_message_ids(" < foobar >"); - assert_eq!(test.len(), 1); - assert_eq!(test[0], "foobar"); - - let test = parse_message_ids(""); - assert!(test.is_empty()); - - let test = parse_message_ids(" "); - assert!(test.is_empty()); - - let test = parse_message_ids(" < "); - assert!(test.is_empty()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn parse_format_flowed_quote() { - let context = TestContext::new_alice().await; - let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Subject: Re: swipe-to-reply -MIME-Version: 1.0 -In-Reply-To: -Date: Tue, 06 Oct 2020 00:00:00 +0000 -Chat-Version: 1.0 -Message-ID: -To: bob -From: alice - -> Long -> quote. - -Reply -"##; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - message.get_subject(), - Some("Re: swipe-to-reply".to_string()) - ); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::Text); - assert_eq!( - message.parts[0].param.get(Param::Quote).unwrap(), - "Long quote." - ); - assert_eq!(message.parts[0].msg, "Reply"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn parse_quote_without_reply() { - let context = TestContext::new_alice().await; - let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Subject: Re: swipe-to-reply -MIME-Version: 1.0 -In-Reply-To: -Date: Tue, 06 Oct 2020 00:00:00 +0000 -Message-ID: -To: bob -From: alice - -> Just a quote. -"##; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - message.get_subject(), - Some("Re: swipe-to-reply".to_string()) - ); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::Text); - assert_eq!( - message.parts[0].param.get(Param::Quote).unwrap(), - "Just a quote." - ); - assert_eq!(message.parts[0].msg, ""); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn parse_quote_top_posting() { - let context = TestContext::new_alice().await; - let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Subject: Re: top posting -MIME-Version: 1.0 -In-Reply-To: -Message-ID: -To: bob -From: alice - -A reply. - -On 2020-10-25, Bob wrote: -> A quote. -"##; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(message.get_subject(), Some("Re: top posting".to_string())); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::Text); - assert_eq!( - message.parts[0].param.get(Param::Quote).unwrap(), - "A quote." - ); - assert_eq!(message.parts[0].msg, "A reply."); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_attachment_quote() { - let context = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/quote_attach.eml"); - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - - assert_eq!(mimeparser.get_subject().unwrap(), "Message from Alice"); - assert_eq!(mimeparser.parts.len(), 1); - assert_eq!(mimeparser.parts[0].msg, "Reply"); - assert_eq!( - mimeparser.parts[0].param.get(Param::Quote).unwrap(), - "Quote" - ); - assert_eq!(mimeparser.parts[0].typ, Viewtype::File); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_quote_div() { - let t = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/gmx-quote.eml"); - let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap(); - assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line"); - assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_allinkl_blockquote() { - // all-inkl.com puts quotes into `
`. - let t = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/allinkl-quote.eml"); - let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap(); - assert!(mimeparser.parts[0].msg.starts_with("It's 1.0.")); - assert_eq!( - mimeparser.parts[0].param.get(Param::Quote).unwrap(), - "What's the version?" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_subj_to_multimedia_msg() { - let t = TestContext::new_alice().await; - receive_imf( - &t.ctx, - include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"), - false, - ) - .await - .unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - let msg_id = chats.get_msg_id(0).unwrap().unwrap(); - let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap(); - - assert_eq!(msg.text, "subj with important info – body text"); - assert_eq!(msg.viewtype, Viewtype::Image); - assert_eq!(msg.error(), None); - assert_eq!(msg.is_dc_message, MessengerMessage::No); - assert_eq!(msg.chat_blocked, Blocked::Request); - assert_eq!(msg.state, MessageState::InFresh); - assert_eq!(msg.get_filebytes(&t).await.unwrap().unwrap(), 2115); - assert!(msg.get_file(&t).is_some()); - assert_eq!(msg.get_filename().unwrap(), "avatar64x64.png"); - assert_eq!(msg.get_width(), 64); - assert_eq!(msg.get_height(), 64); - assert_eq!(msg.get_filemime().unwrap(), "image/png"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mime_modified_plain() { - let t = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); - assert!(!mimeparser.is_mime_modified); - assert_eq!( - mimeparser.parts[0].msg, - "This message does not have Content-Type nor Subject." - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mime_modified_alt_plain_html() { - let t = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); - assert!(mimeparser.is_mime_modified); - assert_eq!( - mimeparser.parts[0].msg, - "mime-modified test – this is plain" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mime_modified_alt_plain() { - let t = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/text_alt_plain.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); - assert!(!mimeparser.is_mime_modified); - assert_eq!( - mimeparser.parts[0].msg, - "mime-modified test – \ - mime-modified should not be set set as there is no html and no special stuff;\n\ - although not being a delta-message.\n\ - test some special html-characters as < > and & but also \" and ' :)" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mime_modified_alt_html() { - let t = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/text_alt_html.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); - assert!(mimeparser.is_mime_modified); - assert_eq!( - mimeparser.parts[0].msg, - "mime-modified test – mime-modified *set*; simplify is always regarded as lossy." - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mime_modified_html() { - let t = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/text_html.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); - assert!(mimeparser.is_mime_modified); - assert_eq!( - mimeparser.parts[0].msg, - "mime-modified test – mime-modified *set*; simplify is always regarded as lossy." - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mime_modified_large_plain() -> Result<()> { - let t = TestContext::new_alice().await; - let t1 = TestContext::new_alice().await; - - static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n"; - static REPEAT_CNT: usize = DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2; - let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT)); - assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT); - assert!(long_txt.len() > DC_DESIRED_TEXT_LEN); - - { - let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?; - assert!(mimemsg.is_mime_modified); - assert!( - mimemsg.parts[0].msg.matches("just repeated").count() - <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() - ); - assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()); - } - - for draft in [false, true] { - let chat = t.get_self_chat().await; - let mut msg = Message::new_text(long_txt.clone()); - if draft { - chat.id.set_draft(&t, Some(&mut msg)).await?; - } - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let msg = t.get_last_msg_in(chat.id).await; - assert!(msg.has_html()); - let html = msg.id.get_html(&t).await?.unwrap(); - assert_eq!(html.matches("").count(), 1); - assert_eq!(html.matches("just repeated.
").count(), REPEAT_CNT); - assert!( - msg.text.matches("just repeated.").count() - <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() - ); - assert!(msg.text.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()); - - let msg = t1.recv_msg(&sent_msg).await; - assert!(msg.has_html()); - assert_eq!(msg.id.get_html(&t1).await?.unwrap(), html); - } - - t.set_config(Config::Bot, Some("1")).await?; - { - let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?; - assert!(!mimemsg.is_mime_modified); - assert_eq!( - format!("{}\n", mimemsg.parts[0].msg), - REPEAT_TXT.repeat(REPEAT_CNT) - ); - } - - Ok(()) - } - - /// Tests that sender status (signature) does not appear - /// in HTML view of a long message. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_large_message_no_signature() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - alice - .set_config(Config::Selfstatus, Some("Some signature")) - .await?; - let chat = alice.create_chat(bob).await; - let txt = "Hello!\n".repeat(500); - let sent = alice.send_text(chat.id, &txt).await; - let msg = bob.recv_msg(&sent).await; - - assert_eq!(msg.has_html(), true); - let html = msg.id.get_html(bob).await?.unwrap(); - assert_eq!(html.contains("Some signature"), false); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_x_microsoft_original_message_id() { - let t = TestContext::new_alice().await; - let message = MimeMessage::from_bytes(&t, b"Date: Wed, 17 Feb 2021 15:45:15 +0000\n\ - Chat-Version: 1.0\n\ - Message-ID: \n\ - To: Bob \n\ - From: Alice \n\ - Subject: Message from Alice\n\ - Content-Type: text/plain\n\ - X-Microsoft-Original-Message-ID: \n\ - MIME-Version: 1.0\n\ - \n\ - Does it work with outlook now?\n\ - ", None) - .await - .unwrap(); - assert_eq!( - message.get_rfc724_mid(), - Some("Mr.6Dx7ITn4w38.n9j7epIcuQI@outlook.com".to_string()) - ); - } - - /// Tests that X-Microsoft-Original-Message-ID does not overwrite encrypted Message-ID. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_x_microsoft_original_message_id_precedence() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - let bob_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id; - chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?; - let mut sent_msg = bob.pop_sent_msg().await; - - // Insert X-Microsoft-Original-Message-ID. - // It should be ignored because there is a Message-ID in the encrypted part. - sent_msg.payload = sent_msg.payload.replace( - "Message-ID:", - "X-Microsoft-Original-Message-ID: \r\nMessage-ID:", - ); - - let msg = alice.recv_msg(&sent_msg).await; - assert!(!msg.rfc724_mid.contains("fake-message-id")); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_long_in_reply_to() -> Result<()> { - let t = TestContext::new_alice().await; - - // A message with a long Message-ID. - // Long message-IDs are generated by Mailjet. - let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000 -Chat-Version: 1.0\n\ -Message-ID: -To: Bob -From: Alice -Subject: ... - -Some quote. -"; - receive_imf(&t, raw, false).await?; - - // Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long. - let raw = br"In-Reply-To: - -Date: Thu, 28 Jan 2021 00:26:57 +0000 -Chat-Version: 1.0\n\ -Message-ID: -To: Alice -From: Bob -Subject: ... - -> Some quote. - -Some reply -"; - - receive_imf(&t, raw, false).await?; - - let msg = t.get_last_msg().await; - assert_eq!(msg.get_text(), "Some reply"); - let quoted_message = msg.quoted_message(&t).await?.unwrap(); - assert_eq!(quoted_message.get_text(), "Some quote."); - - Ok(()) - } - - // Test that WantsMdn parameter is not set on outgoing messages. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_outgoing_wants_mdn() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000 -Chat-Version: 1.0\n\ -Message-ID: -To: Bob -From: Alice -Subject: subject -Chat-Disposition-Notification-To: alice@example.org - -Message. -"; - - // Bob receives message. - receive_imf(&bob, raw, false).await?; - let msg = bob.get_last_msg().await; - // Message is incoming. - assert!(msg.param.get_bool(Param::WantsMdn).unwrap()); - - // Alice receives copy-to-self. - receive_imf(&alice, raw, false).await?; - let msg = alice.get_last_msg().await; - // Message is outgoing, don't send read receipt to self. - assert!(msg.param.get_bool(Param::WantsMdn).is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ignore_read_receipt_to_self() -> Result<()> { - let alice = TestContext::new_alice().await; - - // Alice receives BCC-self copy of a message sent to Bob. - receive_imf( - &alice, - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.net\n\ - Subject: foo\n\ - Message-ID: first@example.com\n\ - Chat-Version: 1.0\n\ - Chat-Disposition-Notification-To: alice@example.org\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n" - .as_bytes(), - false, - ) - .await?; - let msg = alice.get_last_msg().await; - assert_eq!(msg.state, MessageState::OutDelivered); - - // Due to a bug in the old version running on the other device, Alice receives a read - // receipt from self. - receive_imf( - &alice, - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: alice@example.org\n\ - Subject: message opened\n\ - Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ - Chat-Version: 1.0\n\ - Message-ID: second@example.com\n\ - Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: text/plain; charset=utf-8\n\ - \n\ - Read receipts do not guarantee sth. was read.\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: message/disposition-notification\n\ - \n\ - Original-Recipient: rfc822;bob@example.com\n\ - Final-Recipient: rfc822;bob@example.com\n\ - Original-Message-ID: \n\ - Disposition: manual-action/MDN-sent-automatically; displayed\n\ - \n\ - \n\ - --SNIPP--" - .as_bytes(), - false, - ) - .await?; - - // Check that the state has not changed to `MessageState::OutMdnRcvd`. - let msg = Message::load_from_db(&alice, msg.id).await?; - assert_eq!(msg.state, MessageState::OutDelivered); - - Ok(()) - } - - /// Test parsing of MDN sent by MS Exchange. - /// - /// It does not have required Original-Message-ID field, so it is useless, but we want to - /// recognize it as MDN nevertheless to avoid displaying it in the chat as normal message. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ms_exchange_mdn() -> Result<()> { - let t = TestContext::new_alice().await; - - let original = - include_bytes!("../test-data/message/ms_exchange_report_original_message.eml"); - receive_imf(&t, original, false).await?; - let original_msg_id = t.get_last_msg().await.id; - - // 1. Test mimeparser directly - let mdn = - include_bytes!("../test-data/message/ms_exchange_report_disposition_notification.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn, None).await?; - assert_eq!(mimeparser.mdn_reports.len(), 1); - assert_eq!( - mimeparser.mdn_reports[0].original_message_id.as_deref(), - Some("d5904dc344eeb5deaf9bb44603f0c716@posteo.de") - ); - assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty()); - - // 2. Test that marking the original msg as read works - receive_imf(&t, mdn, false).await?; - - assert_eq!( - original_msg_id.get_state(&t).await?, - MessageState::OutMdnRcvd - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_receive_eml() -> Result<()> { - let alice = TestContext::new_alice().await; - - let mime_message = MimeMessage::from_bytes( - &alice, - include_bytes!("../test-data/message/attached-eml.eml"), - None, - ) - .await?; - - assert_eq!(mime_message.parts.len(), 1); - assert_eq!(mime_message.parts[0].typ, Viewtype::File); - assert_eq!( - mime_message.parts[0].mimetype, - Some("message/rfc822".parse().unwrap(),) - ); - assert_eq!( - mime_message.parts[0].msg, - "this is a classic email – I attached the .EML file".to_string() - ); - assert_eq!( - mime_message.parts[0].param.get(Param::File), - Some("$BLOBDIR/.eml") - ); - - assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string())); - - Ok(()) - } - - /// Tests parsing of MIME message containing RFC 9078 reaction. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_reaction() -> Result<()> { - let alice = TestContext::new_alice().await; - - let mime_message = MimeMessage::from_bytes( - &alice, - "To: alice@example.org\n\ -From: bob@example.net\n\ -Date: Today, 29 February 2021 00:00:10 -800\n\ -Message-ID: 56789@example.net\n\ -In-Reply-To: 12345@example.org\n\ -Subject: Meeting\n\ -Mime-Version: 1.0 (1.0)\n\ -Content-Type: text/plain; charset=utf-8\n\ -Content-Disposition: reaction\n\ -\n\ -\u{1F44D}" - .as_bytes(), - None, - ) - .await?; - - assert_eq!(mime_message.parts.len(), 1); - assert_eq!(mime_message.parts[0].is_reaction, true); - assert_eq!( - mime_message - .get_header(HeaderDef::InReplyTo) - .and_then(|msgid| parse_message_id(msgid).ok()) - .unwrap(), - "12345@example.org" - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_jpeg_as_application_octet_stream() -> Result<()> { - let context = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/jpeg-as-application-octet-stream.eml"); - - let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(msg.parts.len(), 1); - assert_eq!(msg.parts[0].typ, Viewtype::Image); - - receive_imf(&context, &raw[..], false).await?; - let msg = context.get_last_msg().await; - assert_eq!(msg.get_viewtype(), Viewtype::Image); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_schleuder() -> Result<()> { - let context = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/schleuder.eml"); - - let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(msg.parts.len(), 2); - - // Header part. - assert_eq!(msg.parts[0].typ, Viewtype::Text); - - // Actual contents part. - assert_eq!(msg.parts[1].typ, Viewtype::Text); - assert_eq!(msg.parts[1].msg, "hello,\nbye"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_tlsrpt() -> Result<()> { - let context = TestContext::new_alice().await; - let raw = include_bytes!("../test-data/message/tlsrpt.eml"); - - let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(msg.parts.len(), 1); - - assert_eq!(msg.parts[0].typ, Viewtype::File); - assert_eq!(msg.parts[0].msg, "Report Domain: nine.testrun.org Submitter: google.com Report-ID: <2024.01.20T00.00.00Z+nine.testrun.org@google.com> – This is an aggregate TLS report from google.com"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_time_in_future() -> Result<()> { - let alice = TestContext::new_alice().await; - - let beginning_time = time(); - - // Receive a message with a date far in the future (year 3004) - // I'm just going to assume that no one uses this code after the year 3000 - let mime_message = MimeMessage::from_bytes( - &alice, - b"To: alice@example.org\n\ - From: bob@example.net\n\ - Date: Today, 29 February 3004 00:00:10 -800\n\ - Message-ID: 56789@example.net\n\ - Subject: Meeting\n\ - Mime-Version: 1.0 (1.0)\n\ - Content-Type: text/plain; charset=utf-8\n\ - \n\ - Hi", - None, - ) - .await?; - - // We do allow the time to be in the future a bit (because of unsynchronized clocks), - // but only 60 seconds: - assert!(mime_message.timestamp_sent <= time() + 60); - assert!(mime_message.timestamp_sent >= beginning_time + 60); - assert!(mime_message.timestamp_rcvd <= time()); - - Ok(()) - } - - /// Tests that subject is not prepended to the message - /// when bot receives it. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_bot_no_subject() { - let context = TestContext::new().await; - context.set_config(Config::Bot, Some("1")).await.unwrap(); - let raw = br#"Message-ID: -From: foo -Subject: Some subject -To: bar@example.org -MIME-Version: 1.0 -Content-Type: text/plain; charset=utf-8 - -/help -"#; - - let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!(message.get_subject(), Some("Some subject".to_string())); - - assert_eq!(message.parts.len(), 1); - assert_eq!(message.parts[0].typ, Viewtype::Text); - // Not "Some subject – /help" - assert_eq!(message.parts[0].msg, "/help"); - } - - /// Tests that Delta Chat takes the last header value - /// rather than the first one if multiple headers - /// are present. - /// - /// DKIM signature applies to the last N headers - /// if header name is included N times in - /// DKIM-Signature. - /// - /// If the client takes the first header - /// rather than the last, it can be fooled - /// into using unsigned header - /// when signed one is present - /// but not protected by oversigning. - /// - /// See - /// - /// for reference. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_take_last_header() { - let context = TestContext::new().await; - - // Mallory added second From: header. - let raw = b"From: mallory@example.org\n\ - From: alice@example.org\n\ - Content-Type: text/plain\n\ - Chat-Version: 1.0\n\ - \n\ - Hello\n\ - "; - - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) - .await - .unwrap(); - assert_eq!( - mimeparser.get_header(HeaderDef::From_).unwrap(), - "alice@example.org" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_protect_autocrypt() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - alice - .set_config_bool(Config::ProtectAutocrypt, true) - .await?; - bob.set_config_bool(Config::ProtectAutocrypt, true).await?; - - let msg = tcm.send_recv_accept(alice, bob, "Hello!").await; - assert_eq!(msg.get_showpadlock(), false); - - let msg = tcm.send_recv(bob, alice, "Hi!").await; - assert_eq!(msg.get_showpadlock(), true); - - Ok(()) - } -} +mod mimeparser_tests; diff --git a/src/mimeparser/mimeparser_tests.rs b/src/mimeparser/mimeparser_tests.rs new file mode 100644 index 000000000..8c77aa3f2 --- /dev/null +++ b/src/mimeparser/mimeparser_tests.rs @@ -0,0 +1,1819 @@ +use mailparse::ParsedMail; + +use super::*; +use crate::{ + chat, + chatlist::Chatlist, + constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS}, + message::{MessageState, MessengerMessage}, + receive_imf::receive_imf, + test_utils::{TestContext, TestContextManager}, + tools::time, +}; + +impl AvatarAction { + pub fn is_change(&self) -> bool { + match self { + AvatarAction::Delete => false, + AvatarAction::Change(_) => true, + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mimeparser_fromheader() { + let ctx = TestContext::new_alice().await; + + let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi", None) + .await + .unwrap(); + let contact = mimemsg.from; + assert_eq!(contact.addr, "g@c.de"); + assert_eq!(contact.display_name, None); + + let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi", None) + .await + .unwrap(); + let contact = mimemsg.from; + assert_eq!(contact.addr, "g@c.de"); + assert_eq!(contact.display_name, None); + + let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \n\nhi", None) + .await + .unwrap(); + let contact = mimemsg.from; + assert_eq!(contact.addr, "g@c.de"); + assert_eq!(contact.display_name, None); + + let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C \n\nhi", None) + .await + .unwrap(); + let contact = mimemsg.from; + assert_eq!(contact.addr, "g@c.de"); + assert_eq!(contact.display_name, Some("Goetz C".to_string())); + + let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" \n\nhi", None) + .await + .unwrap(); + let contact = mimemsg.from; + assert_eq!(contact.addr, "g@c.de"); + assert_eq!(contact.display_name, Some("Goetz C".to_string())); + + let mimemsg = + MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C \n\nhi", None) + .await + .unwrap(); + let contact = mimemsg.from; + assert_eq!(contact.addr, "g@c.de"); + assert_eq!(contact.display_name, Some("Götz C".to_string())); + + // although RFC 2047 says, encoded-words shall not appear inside quoted-string, + // this combination is used in the wild eg. by MailMate + let mimemsg = MimeMessage::from_bytes( + &ctx, + b"From: \"=?utf-8?q?G=C3=B6tz?= C\" \n\nhi", + None, + ) + .await + .unwrap(); + let contact = mimemsg.from; + assert_eq!(contact.addr, "g@c.de"); + assert_eq!(contact.display_name, Some("Götz C".to_string())); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mimeparser_crash() { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/issue_523.txt"); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + + assert_eq!(mimeparser.get_subject(), None); + assert_eq!(mimeparser.parts.len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_rfc724_mid_exists() { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/mail_with_message_id.txt"); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + + assert_eq!( + mimeparser.get_rfc724_mid(), + Some("2dfdbde7@example.org".into()) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_rfc724_mid_not_exists() { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/issue_523.txt"); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(mimeparser.get_rfc724_mid(), None); +} + +#[test] +fn test_get_recipients() { + let raw = include_bytes!("../../test-data/message/mail_with_cc.txt"); + let mail = mailparse::parse_mail(&raw[..]).unwrap(); + let recipients = get_recipients(&mail.headers); + assert!(recipients.iter().any(|info| info.addr == "abc@bcd.com")); + assert!(recipients.iter().any(|info| info.addr == "def@def.de")); + assert_eq!(recipients.len(), 2); + + // If some header is present multiple times, + // only the last one must be used. + let raw = b"From: alice@example.org\n\ + TO: mallory@example.com\n\ + To: mallory@example.net\n\ + To: bob@example.net\n\ + Content-Type: text/plain\n\ + Chat-Version: 1.0\n\ + \n\ + Hello\n\ + "; + let mail = mailparse::parse_mail(&raw[..]).unwrap(); + let recipients = get_recipients(&mail.headers); + assert!(recipients.iter().any(|info| info.addr == "bob@example.net")); + assert_eq!(recipients.len(), 1); +} + +#[test] +fn test_is_attachment() { + let raw = include_bytes!("../../test-data/message/mail_with_cc.txt"); + let mail = mailparse::parse_mail(raw).unwrap(); + assert!(!is_attachment_disposition(&mail)); + + let raw = include_bytes!("../../test-data/message/mail_attach_txt.eml"); + let mail = mailparse::parse_mail(raw).unwrap(); + assert!(!is_attachment_disposition(&mail)); + assert!(!is_attachment_disposition(&mail.subparts[0])); + assert!(is_attachment_disposition(&mail.subparts[1])); +} + +fn load_mail_with_attachment<'a>(t: &'a TestContext, raw: &'a [u8]) -> ParsedMail<'a> { + let mail = mailparse::parse_mail(raw).unwrap(); + assert!(get_attachment_filename(t, &mail).unwrap().is_none()); + assert!(get_attachment_filename(t, &mail.subparts[0]) + .unwrap() + .is_none()); + mail +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_simple.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("test.html".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_encoded_words() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_encoded_words.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("Maßnahmen Okt. 2020.html".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_encoded_words_binary() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_encoded_words_binary.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some(" § 165 Abs".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_encoded_words_windows1251() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_encoded_words_windows1251.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("file Что нового 2020.pdf".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_encoded_words_cont() { + // test continued encoded-words and also test apostropes work that way + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_encoded_words_cont.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("Maßn'ah'men Okt. 2020.html".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_encoded_words_bad_delimiter() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_encoded_words_bad_delimiter.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + // not decoded as a space is missing after encoded-words part + assert_eq!(filename, Some("=?utf-8?q?foo?=.bar".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_apostrophed() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_apostrophed.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("Maßnahmen Okt. 2021.html".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_apostrophed_cont() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_apostrophed_cont.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("Maßnahmen März 2022.html".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_apostrophed_windows1251() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_apostrophed_windows1251.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("программирование.HTM".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_apostrophed_cp1252() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_apostrophed_cp1252.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("Auftragsbestätigung.pdf".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_apostrophed_invalid() { + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_apostrophed_invalid.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("somedäüta.html.zip".to_string())) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_attachment_filename_combined() { + // test that if `filename` and `filename*0` are given, the filename is not doubled + let t = TestContext::new().await; + let mail = load_mail_with_attachment( + &t, + include_bytes!("../../test-data/message/attach_filename_combined.eml"), + ); + let filename = get_attachment_filename(&t, &mail.subparts[1]).unwrap(); + assert_eq!(filename, Some("Maßnahmen Okt. 2020.html".to_string())) +} + +#[test] +fn test_mailparse_content_type() { + let ctype = mailparse::parse_content_type("text/plain; charset=utf-8; protected-headers=v1;"); + + assert_eq!(ctype.mimetype, "text/plain"); + assert_eq!(ctype.charset, "utf-8"); + assert_eq!( + ctype.params.get("protected-headers"), + Some(&"v1".to_string()) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_first_addr() { + let context = TestContext::new().await; + let raw = b"From: hello@one.org, world@two.org\n\ + Chat-Disposition-Notification-To: wrong\n\ + Content-Type: text/plain\n\ + Chat-Version: 1.0\n\ + \n\ + test1\n\ + "; + + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await; + + assert!(mimeparser.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_parent_timestamp() { + let context = TestContext::new_alice().await; + let raw = b"From: foo@example.org\n\ + Content-Type: text/plain\n\ + Chat-Version: 1.0\n\ + In-Reply-To: \n\ + \n\ + Some reply\n\ + "; + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(), + None + ); + let timestamp = 1570435529; + context + .ctx + .sql + .execute( + "INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)", + ("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp), + ) + .await + .expect("Failed to write to the database"); + assert_eq!( + mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(), + Some(timestamp) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mimeparser_with_context() { + let context = TestContext::new_alice().await; + let raw = b"From: hello@example.org\n\ + Content-Type: multipart/mixed; boundary=\"==break==\";\n\ + Subject: outer-subject\n\ + Secure-Join-Group: no\n\ + Secure-Join-Fingerprint: 123456\n\ + Test-Header: Bar\n\ + chat-VERSION: 0.0\n\ + \n\ + --==break==\n\ + Content-Type: text/plain; protected-headers=\"v1\";\n\ + Subject: inner-subject\n\ + SecureBar-Join-Group: yes\n\ + Test-Header: Xy\n\ + chat-VERSION: 1.0\n\ + \n\ + test1\n\ + \n\ + --==break==--\n\ + \n"; + + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + + // non-overwritten headers do not bubble up + let of = mimeparser.get_header(HeaderDef::SecureJoinGroup).unwrap(); + assert_eq!(of, "no"); + + // unknown headers do not bubble upwards + let of = mimeparser.get_header(HeaderDef::TestHeader).unwrap(); + assert_eq!(of, "Bar"); + + // the following fields would bubble up + // if the test would really use encryption for the protected part + // however, as this is not the case, the outer things stay valid. + // for Chat-Version, also the case-insensivity is tested. + assert_eq!(mimeparser.get_subject(), Some("outer-subject".into())); + + let of = mimeparser.get_header(HeaderDef::ChatVersion).unwrap(); + assert_eq!(of, "0.0"); + assert_eq!(mimeparser.parts.len(), 1); + + // make sure, headers that are only allowed in the encrypted part + // cannot be set from the outer part + assert!(mimeparser + .get_header(HeaderDef::SecureJoinFingerprint) + .is_none()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mimeparser_with_avatars() { + let t = TestContext::new_alice().await; + + let raw = include_bytes!("../../test-data/message/mail_attach_txt.eml"); + let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); + assert_eq!(mimeparser.user_avatar, None); + assert_eq!(mimeparser.group_avatar, None); + + let raw = include_bytes!("../../test-data/message/mail_with_user_avatar.eml"); + let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); + assert_eq!(mimeparser.parts.len(), 1); + assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); + assert!(mimeparser.user_avatar.unwrap().is_change()); + assert_eq!(mimeparser.group_avatar, None); + + let raw = include_bytes!("../../test-data/message/mail_with_user_avatar_deleted.eml"); + let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); + assert_eq!(mimeparser.parts.len(), 1); + assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); + assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete)); + assert_eq!(mimeparser.group_avatar, None); + + let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml"); + let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); + assert_eq!(mimeparser.parts.len(), 1); + assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); + assert!(mimeparser.user_avatar.unwrap().is_change()); + assert!(mimeparser.group_avatar.unwrap().is_change()); + + // if the Chat-User-Avatar header is missing, the avatar become a normal attachment + let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml"); + let raw = String::from_utf8_lossy(raw).to_string(); + let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:"); + let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes(), None) + .await + .unwrap(); + assert_eq!(mimeparser.parts.len(), 1); + assert_eq!(mimeparser.parts[0].typ, Viewtype::Image); + assert_eq!(mimeparser.user_avatar, None); + assert!(mimeparser.group_avatar.unwrap().is_change()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mimeparser_with_videochat() { + let t = TestContext::new_alice().await; + + let raw = include_bytes!("../../test-data/message/videochat_invitation.eml"); + let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); + assert_eq!(mimeparser.parts.len(), 1); + assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation); + assert_eq!( + mimeparser.parts[0] + .param + .get(Param::WebrtcRoom) + .unwrap_or_default(), + "https://example.org/p2p/?roomname=6HiduoAn4xN" + ); + assert!(mimeparser.parts[0] + .msg + .contains("https://example.org/p2p/?roomname=6HiduoAn4xN")); + assert_eq!(mimeparser.user_avatar, None); + assert_eq!(mimeparser.group_avatar, None); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mimeparser_message_kml() { + let context = TestContext::new_alice().await; + let raw = b"Chat-Version: 1.0\n\ +From: foo \n\ +To: bar \n\ +Subject: Location streaming\n\ +Content-Type: multipart/mixed; boundary=\"==break==\"\n\ +\n\ +\n\ +--==break==\n\ +Content-Type: text/plain; charset=utf-8\n\ +\n\ +--\n\ +Sent with my Delta Chat Messenger: https://delta.chat\n\ +\n\ +--==break==\n\ +Content-Type: application/vnd.google-earth.kml+xml\n\ +Content-Disposition: attachment; filename=\"message.kml\"\n\ +\n\ +\n\ +\n\ +\n\ +XXX0.0,0.0\n\ +\n\ +\n\ +\n\ +--==break==--\n\ +;"; + + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + mimeparser.get_subject(), + Some("Location streaming".to_string()) + ); + assert!(mimeparser.location_kml.is_none()); + assert!(mimeparser.message_kml.is_some()); + + // There is only one part because message.kml attachment is special + // and only goes into message_kml. + assert_eq!(mimeparser.parts.len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_mdn() { + let context = TestContext::new_alice().await; + let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ +Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ +Chat-Version: 1.0\n\ +Message-ID: \n\ +To: Alice \n\ +From: Bob \n\ +Auto-Submitted: auto-replied\n\ +Content-Type: multipart/report; report-type=disposition-notification;\n\t\ +boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ +Content-Type: text/plain; charset=utf-8\n\ +\n\ +The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ +\n\ +This is no guarantee the content was read.\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ +Content-Type: message/disposition-notification\n\ +\n\ +Reporting-UA: Delta Chat 1.0.0-beta.22\n\ +Original-Recipient: rfc822;bob@example.org\n\ +Final-Recipient: rfc822;bob@example.org\n\ +Original-Message-ID: \n\ +Disposition: manual-action/MDN-sent-automatically; displayed\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ +"; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + message.get_subject(), + Some("Chat: Message opened".to_string()) + ); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.mdn_reports.len(), 1); + assert_eq!(message.is_bot, None); +} + +/// Test parsing multiple MDNs combined in a single message. +/// +/// RFC 6522 specifically allows MDNs to be nested inside +/// multipart MIME messages. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_multiple_mdns() { + let context = TestContext::new_alice().await; + let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ +Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ +Chat-Version: 1.0\n\ +Message-ID: \n\ +To: Alice \n\ +From: Bob \n\ +Content-Type: multipart/parallel; boundary=outer\n\ +\n\ +This is a multipart MDN.\n\ +\n\ +--outer\n\ +Content-Type: multipart/report; report-type=disposition-notification;\n\t\ +boundary=kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ +Content-Type: text/plain; charset=utf-8\n\ +\n\ +The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ +\n\ +This is no guarantee the content was read.\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ +Content-Type: message/disposition-notification\n\ +\n\ +Reporting-UA: Delta Chat 1.0.0-beta.22\n\ +Original-Recipient: rfc822;bob@example.org\n\ +Final-Recipient: rfc822;bob@example.org\n\ +Original-Message-ID: \n\ +Disposition: manual-action/MDN-sent-automatically; displayed\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ +--outer\n\ +Content-Type: multipart/report; report-type=disposition-notification;\n\t\ +boundary=zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\ +\n\ +\n\ +--zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\ +Content-Type: text/plain; charset=utf-8\n\ +\n\ +The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ +\n\ +This is no guarantee the content was read.\n\ +\n\ +\n\ +--zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\ +Content-Type: message/disposition-notification\n\ +\n\ +Reporting-UA: Delta Chat 1.0.0-beta.22\n\ +Original-Recipient: rfc822;bob@example.org\n\ +Final-Recipient: rfc822;bob@example.org\n\ +Original-Message-ID: \n\ +Disposition: manual-action/MDN-sent-automatically; displayed\n\ +\n\ +\n\ +--zuOJlsTfZAukyawEPVdIgqWjaM9w2W--\n\ +--outer--\n\ +"; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + message.get_subject(), + Some("Chat: Message opened".to_string()) + ); + + assert_eq!(message.parts.len(), 2); + assert_eq!(message.mdn_reports.len(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_mdn_with_additional_message_ids() { + let context = TestContext::new_alice().await; + let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ +Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ +Chat-Version: 1.0\n\ +Message-ID: \n\ +To: Alice \n\ +From: Bob \n\ +Content-Type: multipart/report; report-type=disposition-notification;\n\t\ +boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ +Content-Type: text/plain; charset=utf-8\n\ +\n\ +The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\ +\n\ +This is no guarantee the content was read.\n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\ +Content-Type: message/disposition-notification\n\ +\n\ +Reporting-UA: Delta Chat 1.0.0-beta.22\n\ +Original-Recipient: rfc822;bob@example.org\n\ +Final-Recipient: rfc822;bob@example.org\n\ +Original-Message-ID: \n\ +Disposition: manual-action/MDN-sent-automatically; displayed\n\ +Additional-Message-IDs: \n\ +\n\ +\n\ +--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ +"; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + message.get_subject(), + Some("Chat: Message opened".to_string()) + ); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.mdn_reports.len(), 1); + assert_eq!( + message.mdn_reports[0].original_message_id, + Some("foo@example.org".to_string()) + ); + assert_eq!( + &message.mdn_reports[0].additional_message_ids, + &["foo@example.com", "foo@example.net"] + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_inline_attachment() { + let context = TestContext::new_alice().await; + let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) +From: sender@example.com +To: receiver@example.com +Subject: Mail with inline attachment +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_25_46172632.1581201680436" + +------=_Part_25_46172632.1581201680436 +Content-Type: text/plain; charset=utf-8 + +Hello! + +------=_Part_25_46172632.1581201680436 +Content-Type: application/pdf; name="some_pdf.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="some_pdf.pdf" + +JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl +Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM +MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== +------=_Part_25_46172632.1581201680436-- +"#; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + message.get_subject(), + Some("Mail with inline attachment".to_string()) + ); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::File); + assert_eq!(message.parts[0].msg, "Mail with inline attachment – Hello!"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_hide_html_without_content() { + let t = TestContext::new_alice().await; + let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) +From: sender@example.com +To: receiver@example.com +Subject: Mail with inline attachment +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_25_46172632.1581201680436" + +------=_Part_25_46172632.1581201680436 +Content-Type: text/html; charset=utf-8 + + + + + + + +------=_Part_25_46172632.1581201680436 +Content-Type: application/pdf; name="some_pdf.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="some_pdf.pdf" + +JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl +Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM +MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== +------=_Part_25_46172632.1581201680436-- +"#; + + let message = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::File); + assert_eq!(message.parts[0].msg, ""); + + // Make sure the file is there even though the html is wrong: + let param = &message.parts[0].param; + let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap(); + let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap(); + let size = f.metadata().await.unwrap().len(); + assert_eq!(size, 154); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn parse_inline_image() { + let context = TestContext::new_alice().await; + let raw = br#"Message-ID: +From: foo +Subject: example +To: bar@example.org +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="--11019878869865180" + +----11019878869865180 +Content-Type: text/plain; charset=utf-8 + +Test + +----11019878869865180 +Content-Type: image/jpeg; + name="JPEG_filename.jpg" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="JPEG_filename.jpg" + +ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG +acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT +/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal +n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C +xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2 +rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU +8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI +K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4 +CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= + + +----11019878869865180-- +"#; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(message.get_subject(), Some("example".to_string())); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Image); + assert_eq!(message.parts[0].msg, "example – Test"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn parse_thunderbird_html_embedded_image() { + let context = TestContext::new_alice().await; + let raw = br#"To: Alice +From: Bob +Subject: Test subject +Message-ID: +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 + Thunderbird/68.7.0 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="------------779C1631600DF3DB8C02E53A" +Content-Language: en-US + +This is a multi-part message in MIME format. +--------------779C1631600DF3DB8C02E53A +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +Test + + +--------------779C1631600DF3DB8C02E53A +Content-Type: multipart/related; + boundary="------------10CC6C2609EB38DA782C5CA9" + + +--------------10CC6C2609EB38DA782C5CA9 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + + + + + + +Test
+

+ + + +--------------10CC6C2609EB38DA782C5CA9 +Content-Type: image/png; + name="1.png" +Content-Transfer-Encoding: base64 +Content-ID: +Content-Disposition: inline; + filename="1.png" + +ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG +acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT +/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal +n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C +xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2 +rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU +8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI +K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4 +CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= +--------------10CC6C2609EB38DA782C5CA9-- + +--------------779C1631600DF3DB8C02E53A--"#; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(message.get_subject(), Some("Test subject".to_string())); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Image); + assert_eq!(message.parts[0].msg, "Test subject – Test"); +} + +// Outlook specifies filename in the "name" attribute of Content-Type +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn parse_outlook_html_embedded_image() { + let context = TestContext::new_alice().await; + let raw = br#"From: Anonymous +To: Anonymous +Subject: Delta Chat is great stuff! +Date: Tue, 5 May 2020 01:23:45 +0000 +MIME-Version: 1.0 +Content-Type: multipart/related; + boundary="----=_NextPart_000_0003_01D622B3.CA753E60" +X-Mailer: Microsoft Outlook 15.0 + +This is a multipart message in MIME format. + +------=_NextPart_000_0003_01D622B3.CA753E60 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_0004_01D622B3.CA753E60" + + +------=_NextPart_001_0004_01D622B3.CA753E60 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + + + + +------=_NextPart_001_0004_01D622B3.CA753E60 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + + + +

+Test +

+ + +------=_NextPart_001_0004_01D622B3.CA753E60-- + +------=_NextPart_000_0003_01D622B3.CA753E60 +Content-Type: image/jpeg; + name="image001.jpg" +Content-Transfer-Encoding: base64 +Content-ID: + +ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG +acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT +/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal +n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C +xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2 +rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU +8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI +K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4 +CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= + +------=_NextPart_000_0003_01D622B3.CA753E60-- +"#; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + message.get_subject(), + Some("Delta Chat is great stuff!".to_string()) + ); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Image); + assert_eq!(message.parts[0].msg, "Delta Chat is great stuff! – Test"); +} + +#[test] +fn test_parse_message_id() { + let test = parse_message_id(""); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "foobar"); + + let test = parse_message_id(" "); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "foo"); + + let test = parse_message_id(" < foo > "); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "foo"); + + let test = parse_message_id("foo"); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "foo"); + + let test = parse_message_id(" foo "); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "foo"); + + let test = parse_message_id("foo bar"); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "foo"); + + let test = parse_message_id(" foo bar "); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "foo"); + + let test = parse_message_id(""); + assert!(test.is_err()); + + let test = parse_message_id(" "); + assert!(test.is_err()); + + let test = parse_message_id("<>"); + assert!(test.is_err()); + + let test = parse_message_id("<> bar"); + assert!(test.is_ok()); + assert_eq!(test.unwrap(), "bar"); +} + +#[test] +fn test_parse_message_ids() { + let test = parse_message_ids(" foo bar "); + assert_eq!(test.len(), 3); + assert_eq!(test[0], "foo"); + assert_eq!(test[1], "bar"); + assert_eq!(test[2], "foobar"); + + let test = parse_message_ids(" < foobar >"); + assert_eq!(test.len(), 1); + assert_eq!(test[0], "foobar"); + + let test = parse_message_ids(""); + assert!(test.is_empty()); + + let test = parse_message_ids(" "); + assert!(test.is_empty()); + + let test = parse_message_ids(" < "); + assert!(test.is_empty()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn parse_format_flowed_quote() { + let context = TestContext::new_alice().await; + let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Subject: Re: swipe-to-reply +MIME-Version: 1.0 +In-Reply-To: +Date: Tue, 06 Oct 2020 00:00:00 +0000 +Chat-Version: 1.0 +Message-ID: +To: bob +From: alice + +> Long +> quote. + +Reply +"##; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + message.get_subject(), + Some("Re: swipe-to-reply".to_string()) + ); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Text); + assert_eq!( + message.parts[0].param.get(Param::Quote).unwrap(), + "Long quote." + ); + assert_eq!(message.parts[0].msg, "Reply"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn parse_quote_without_reply() { + let context = TestContext::new_alice().await; + let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Subject: Re: swipe-to-reply +MIME-Version: 1.0 +In-Reply-To: +Date: Tue, 06 Oct 2020 00:00:00 +0000 +Message-ID: +To: bob +From: alice + +> Just a quote. +"##; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + message.get_subject(), + Some("Re: swipe-to-reply".to_string()) + ); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Text); + assert_eq!( + message.parts[0].param.get(Param::Quote).unwrap(), + "Just a quote." + ); + assert_eq!(message.parts[0].msg, ""); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn parse_quote_top_posting() { + let context = TestContext::new_alice().await; + let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Subject: Re: top posting +MIME-Version: 1.0 +In-Reply-To: +Message-ID: +To: bob +From: alice + +A reply. + +On 2020-10-25, Bob wrote: +> A quote. +"##; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(message.get_subject(), Some("Re: top posting".to_string())); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Text); + assert_eq!( + message.parts[0].param.get(Param::Quote).unwrap(), + "A quote." + ); + assert_eq!(message.parts[0].msg, "A reply."); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_attachment_quote() { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/quote_attach.eml"); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + + assert_eq!(mimeparser.get_subject().unwrap(), "Message from Alice"); + assert_eq!(mimeparser.parts.len(), 1); + assert_eq!(mimeparser.parts[0].msg, "Reply"); + assert_eq!( + mimeparser.parts[0].param.get(Param::Quote).unwrap(), + "Quote" + ); + assert_eq!(mimeparser.parts[0].typ, Viewtype::File); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_quote_div() { + let t = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/gmx-quote.eml"); + let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap(); + assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line"); + assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_allinkl_blockquote() { + // all-inkl.com puts quotes into `
`. + let t = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/allinkl-quote.eml"); + let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap(); + assert!(mimeparser.parts[0].msg.starts_with("It's 1.0.")); + assert_eq!( + mimeparser.parts[0].param.get(Param::Quote).unwrap(), + "What's the version?" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_subj_to_multimedia_msg() { + let t = TestContext::new_alice().await; + receive_imf( + &t.ctx, + include_bytes!("../../test-data/message/subj_with_multimedia_msg.eml"), + false, + ) + .await + .unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + let msg_id = chats.get_msg_id(0).unwrap().unwrap(); + let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap(); + + assert_eq!(msg.text, "subj with important info – body text"); + assert_eq!(msg.viewtype, Viewtype::Image); + assert_eq!(msg.error(), None); + assert_eq!(msg.is_dc_message, MessengerMessage::No); + assert_eq!(msg.chat_blocked, Blocked::Request); + assert_eq!(msg.state, MessageState::InFresh); + assert_eq!(msg.get_filebytes(&t).await.unwrap().unwrap(), 2115); + assert!(msg.get_file(&t).is_some()); + assert_eq!(msg.get_filename().unwrap(), "avatar64x64.png"); + assert_eq!(msg.get_width(), 64); + assert_eq!(msg.get_height(), 64); + assert_eq!(msg.get_filemime().unwrap(), "image/png"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mime_modified_plain() { + let t = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/text_plain_unspecified.eml"); + let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); + assert!(!mimeparser.is_mime_modified); + assert_eq!( + mimeparser.parts[0].msg, + "This message does not have Content-Type nor Subject." + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mime_modified_alt_plain_html() { + let t = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/text_alt_plain_html.eml"); + let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); + assert!(mimeparser.is_mime_modified); + assert_eq!( + mimeparser.parts[0].msg, + "mime-modified test – this is plain" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mime_modified_alt_plain() { + let t = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/text_alt_plain.eml"); + let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); + assert!(!mimeparser.is_mime_modified); + assert_eq!( + mimeparser.parts[0].msg, + "mime-modified test – \ + mime-modified should not be set set as there is no html and no special stuff;\n\ + although not being a delta-message.\n\ + test some special html-characters as < > and & but also \" and ' :)" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mime_modified_alt_html() { + let t = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/text_alt_html.eml"); + let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); + assert!(mimeparser.is_mime_modified); + assert_eq!( + mimeparser.parts[0].msg, + "mime-modified test – mime-modified *set*; simplify is always regarded as lossy." + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mime_modified_html() { + let t = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/text_html.eml"); + let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); + assert!(mimeparser.is_mime_modified); + assert_eq!( + mimeparser.parts[0].msg, + "mime-modified test – mime-modified *set*; simplify is always regarded as lossy." + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mime_modified_large_plain() -> Result<()> { + let t = TestContext::new_alice().await; + let t1 = TestContext::new_alice().await; + + static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n"; + static REPEAT_CNT: usize = DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2; + let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT)); + assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT); + assert!(long_txt.len() > DC_DESIRED_TEXT_LEN); + + { + let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?; + assert!(mimemsg.is_mime_modified); + assert!( + mimemsg.parts[0].msg.matches("just repeated").count() + <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + ); + assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()); + } + + for draft in [false, true] { + let chat = t.get_self_chat().await; + let mut msg = Message::new_text(long_txt.clone()); + if draft { + chat.id.set_draft(&t, Some(&mut msg)).await?; + } + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let msg = t.get_last_msg_in(chat.id).await; + assert!(msg.has_html()); + let html = msg.id.get_html(&t).await?.unwrap(); + assert_eq!(html.matches("").count(), 1); + assert_eq!(html.matches("just repeated.
").count(), REPEAT_CNT); + assert!( + msg.text.matches("just repeated.").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + ); + assert!(msg.text.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()); + + let msg = t1.recv_msg(&sent_msg).await; + assert!(msg.has_html()); + assert_eq!(msg.id.get_html(&t1).await?.unwrap(), html); + } + + t.set_config(Config::Bot, Some("1")).await?; + { + let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?; + assert!(!mimemsg.is_mime_modified); + assert_eq!( + format!("{}\n", mimemsg.parts[0].msg), + REPEAT_TXT.repeat(REPEAT_CNT) + ); + } + + Ok(()) +} + +/// Tests that sender status (signature) does not appear +/// in HTML view of a long message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_large_message_no_signature() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + alice + .set_config(Config::Selfstatus, Some("Some signature")) + .await?; + let chat = alice.create_chat(bob).await; + let txt = "Hello!\n".repeat(500); + let sent = alice.send_text(chat.id, &txt).await; + let msg = bob.recv_msg(&sent).await; + + assert_eq!(msg.has_html(), true); + let html = msg.id.get_html(bob).await?.unwrap(); + assert_eq!(html.contains("Some signature"), false); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_x_microsoft_original_message_id() { + let t = TestContext::new_alice().await; + let message = MimeMessage::from_bytes(&t, b"Date: Wed, 17 Feb 2021 15:45:15 +0000\n\ + Chat-Version: 1.0\n\ + Message-ID: \n\ + To: Bob \n\ + From: Alice \n\ + Subject: Message from Alice\n\ + Content-Type: text/plain\n\ + X-Microsoft-Original-Message-ID: \n\ + MIME-Version: 1.0\n\ + \n\ + Does it work with outlook now?\n\ + ", None) + .await + .unwrap(); + assert_eq!( + message.get_rfc724_mid(), + Some("Mr.6Dx7ITn4w38.n9j7epIcuQI@outlook.com".to_string()) + ); +} + +/// Tests that X-Microsoft-Original-Message-ID does not overwrite encrypted Message-ID. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_x_microsoft_original_message_id_precedence() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + let bob_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id; + chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?; + let mut sent_msg = bob.pop_sent_msg().await; + + // Insert X-Microsoft-Original-Message-ID. + // It should be ignored because there is a Message-ID in the encrypted part. + sent_msg.payload = sent_msg.payload.replace( + "Message-ID:", + "X-Microsoft-Original-Message-ID: \r\nMessage-ID:", + ); + + let msg = alice.recv_msg(&sent_msg).await; + assert!(!msg.rfc724_mid.contains("fake-message-id")); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_long_in_reply_to() -> Result<()> { + let t = TestContext::new_alice().await; + + // A message with a long Message-ID. + // Long message-IDs are generated by Mailjet. + let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000 +Chat-Version: 1.0\n\ +Message-ID: +To: Bob +From: Alice +Subject: ... + +Some quote. +"; + receive_imf(&t, raw, false).await?; + + // Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long. + let raw = br"In-Reply-To: + +Date: Thu, 28 Jan 2021 00:26:57 +0000 +Chat-Version: 1.0\n\ +Message-ID: +To: Alice +From: Bob +Subject: ... + +> Some quote. + +Some reply +"; + + receive_imf(&t, raw, false).await?; + + let msg = t.get_last_msg().await; + assert_eq!(msg.get_text(), "Some reply"); + let quoted_message = msg.quoted_message(&t).await?.unwrap(); + assert_eq!(quoted_message.get_text(), "Some quote."); + + Ok(()) +} + +// Test that WantsMdn parameter is not set on outgoing messages. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_wants_mdn() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000 +Chat-Version: 1.0\n\ +Message-ID: +To: Bob +From: Alice +Subject: subject +Chat-Disposition-Notification-To: alice@example.org + +Message. +"; + + // Bob receives message. + receive_imf(&bob, raw, false).await?; + let msg = bob.get_last_msg().await; + // Message is incoming. + assert!(msg.param.get_bool(Param::WantsMdn).unwrap()); + + // Alice receives copy-to-self. + receive_imf(&alice, raw, false).await?; + let msg = alice.get_last_msg().await; + // Message is outgoing, don't send read receipt to self. + assert!(msg.param.get_bool(Param::WantsMdn).is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ignore_read_receipt_to_self() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Alice receives BCC-self copy of a message sent to Bob. + receive_imf( + &alice, + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.net\n\ + Subject: foo\n\ + Message-ID: first@example.com\n\ + Chat-Version: 1.0\n\ + Chat-Disposition-Notification-To: alice@example.org\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n" + .as_bytes(), + false, + ) + .await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.state, MessageState::OutDelivered); + + // Due to a bug in the old version running on the other device, Alice receives a read + // receipt from self. + receive_imf( + &alice, + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: alice@example.org\n\ + Subject: message opened\n\ + Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ + Chat-Version: 1.0\n\ + Message-ID: second@example.com\n\ + Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: text/plain; charset=utf-8\n\ + \n\ + Read receipts do not guarantee sth. was read.\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: message/disposition-notification\n\ + \n\ + Original-Recipient: rfc822;bob@example.com\n\ + Final-Recipient: rfc822;bob@example.com\n\ + Original-Message-ID: \n\ + Disposition: manual-action/MDN-sent-automatically; displayed\n\ + \n\ + \n\ + --SNIPP--" + .as_bytes(), + false, + ) + .await?; + + // Check that the state has not changed to `MessageState::OutMdnRcvd`. + let msg = Message::load_from_db(&alice, msg.id).await?; + assert_eq!(msg.state, MessageState::OutDelivered); + + Ok(()) +} + +/// Test parsing of MDN sent by MS Exchange. +/// +/// It does not have required Original-Message-ID field, so it is useless, but we want to +/// recognize it as MDN nevertheless to avoid displaying it in the chat as normal message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ms_exchange_mdn() -> Result<()> { + let t = TestContext::new_alice().await; + + let original = + include_bytes!("../../test-data/message/ms_exchange_report_original_message.eml"); + receive_imf(&t, original, false).await?; + let original_msg_id = t.get_last_msg().await.id; + + // 1. Test mimeparser directly + let mdn = + include_bytes!("../../test-data/message/ms_exchange_report_disposition_notification.eml"); + let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn, None).await?; + assert_eq!(mimeparser.mdn_reports.len(), 1); + assert_eq!( + mimeparser.mdn_reports[0].original_message_id.as_deref(), + Some("d5904dc344eeb5deaf9bb44603f0c716@posteo.de") + ); + assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty()); + + // 2. Test that marking the original msg as read works + receive_imf(&t, mdn, false).await?; + + assert_eq!( + original_msg_id.get_state(&t).await?, + MessageState::OutMdnRcvd + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_receive_eml() -> Result<()> { + let alice = TestContext::new_alice().await; + + let mime_message = MimeMessage::from_bytes( + &alice, + include_bytes!("../../test-data/message/attached-eml.eml"), + None, + ) + .await?; + + assert_eq!(mime_message.parts.len(), 1); + assert_eq!(mime_message.parts[0].typ, Viewtype::File); + assert_eq!( + mime_message.parts[0].mimetype, + Some("message/rfc822".parse().unwrap(),) + ); + assert_eq!( + mime_message.parts[0].msg, + "this is a classic email – I attached the .EML file".to_string() + ); + assert_eq!( + mime_message.parts[0].param.get(Param::File), + Some("$BLOBDIR/.eml") + ); + + assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string())); + + Ok(()) +} + +/// Tests parsing of MIME message containing RFC 9078 reaction. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_reaction() -> Result<()> { + let alice = TestContext::new_alice().await; + + let mime_message = MimeMessage::from_bytes( + &alice, + "To: alice@example.org\n\ +From: bob@example.net\n\ +Date: Today, 29 February 2021 00:00:10 -800\n\ +Message-ID: 56789@example.net\n\ +In-Reply-To: 12345@example.org\n\ +Subject: Meeting\n\ +Mime-Version: 1.0 (1.0)\n\ +Content-Type: text/plain; charset=utf-8\n\ +Content-Disposition: reaction\n\ +\n\ +\u{1F44D}" + .as_bytes(), + None, + ) + .await?; + + assert_eq!(mime_message.parts.len(), 1); + assert_eq!(mime_message.parts[0].is_reaction, true); + assert_eq!( + mime_message + .get_header(HeaderDef::InReplyTo) + .and_then(|msgid| parse_message_id(msgid).ok()) + .unwrap(), + "12345@example.org" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_jpeg_as_application_octet_stream() -> Result<()> { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/jpeg-as-application-octet-stream.eml"); + + let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(msg.parts.len(), 1); + assert_eq!(msg.parts[0].typ, Viewtype::Image); + + receive_imf(&context, &raw[..], false).await?; + let msg = context.get_last_msg().await; + assert_eq!(msg.get_viewtype(), Viewtype::Image); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_schleuder() -> Result<()> { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/schleuder.eml"); + + let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(msg.parts.len(), 2); + + // Header part. + assert_eq!(msg.parts[0].typ, Viewtype::Text); + + // Actual contents part. + assert_eq!(msg.parts[1].typ, Viewtype::Text); + assert_eq!(msg.parts[1].msg, "hello,\nbye"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_tlsrpt() -> Result<()> { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/tlsrpt.eml"); + + let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(msg.parts.len(), 1); + + assert_eq!(msg.parts[0].typ, Viewtype::File); + assert_eq!(msg.parts[0].msg, "Report Domain: nine.testrun.org Submitter: google.com Report-ID: <2024.01.20T00.00.00Z+nine.testrun.org@google.com> – This is an aggregate TLS report from google.com"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_time_in_future() -> Result<()> { + let alice = TestContext::new_alice().await; + + let beginning_time = time(); + + // Receive a message with a date far in the future (year 3004) + // I'm just going to assume that no one uses this code after the year 3000 + let mime_message = MimeMessage::from_bytes( + &alice, + b"To: alice@example.org\n\ + From: bob@example.net\n\ + Date: Today, 29 February 3004 00:00:10 -800\n\ + Message-ID: 56789@example.net\n\ + Subject: Meeting\n\ + Mime-Version: 1.0 (1.0)\n\ + Content-Type: text/plain; charset=utf-8\n\ + \n\ + Hi", + None, + ) + .await?; + + // We do allow the time to be in the future a bit (because of unsynchronized clocks), + // but only 60 seconds: + assert!(mime_message.timestamp_sent <= time() + 60); + assert!(mime_message.timestamp_sent >= beginning_time + 60); + assert!(mime_message.timestamp_rcvd <= time()); + + Ok(()) +} + +/// Tests that subject is not prepended to the message +/// when bot receives it. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_bot_no_subject() { + let context = TestContext::new().await; + context.set_config(Config::Bot, Some("1")).await.unwrap(); + let raw = br#"Message-ID: +From: foo +Subject: Some subject +To: bar@example.org +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 + +/help +"#; + + let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(message.get_subject(), Some("Some subject".to_string())); + + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Text); + // Not "Some subject – /help" + assert_eq!(message.parts[0].msg, "/help"); +} + +/// Tests that Delta Chat takes the last header value +/// rather than the first one if multiple headers +/// are present. +/// +/// DKIM signature applies to the last N headers +/// if header name is included N times in +/// DKIM-Signature. +/// +/// If the client takes the first header +/// rather than the last, it can be fooled +/// into using unsigned header +/// when signed one is present +/// but not protected by oversigning. +/// +/// See +/// +/// for reference. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_take_last_header() { + let context = TestContext::new().await; + + // Mallory added second From: header. + let raw = b"From: mallory@example.org\n\ + From: alice@example.org\n\ + Content-Type: text/plain\n\ + Chat-Version: 1.0\n\ + \n\ + Hello\n\ + "; + + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!( + mimeparser.get_header(HeaderDef::From_).unwrap(), + "alice@example.org" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_protect_autocrypt() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + alice + .set_config_bool(Config::ProtectAutocrypt, true) + .await?; + bob.set_config_bool(Config::ProtectAutocrypt, true).await?; + + let msg = tcm.send_recv_accept(alice, bob, "Hello!").await; + assert_eq!(msg.get_showpadlock(), false); + + let msg = tcm.send_recv(bob, alice, "Hi!").await; + assert_eq!(msg.get_showpadlock(), true); + + Ok(()) +} diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 1fdbe9890..8b7f4c7fb 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3028,4 +3028,4 @@ async fn add_or_lookup_contacts_by_address_list( } #[cfg(test)] -mod tests; +mod receive_imf_tests; diff --git a/src/receive_imf/tests.rs b/src/receive_imf/receive_imf_tests.rs similarity index 100% rename from src/receive_imf/tests.rs rename to src/receive_imf/receive_imf_tests.rs