diff --git a/src/config.rs b/src/config.rs index b0e3191372..e317c7a4ed 100644 --- a/src/config.rs +++ b/src/config.rs @@ -963,397 +963,4 @@ fn get_config_keys_string() -> String { } #[cfg(test)] -mod tests { - use num_traits::FromPrimitive; - - use super::*; - use crate::test_utils::{sync, TestContext, TestContextManager}; - - #[test] - fn test_to_string() { - assert_eq!(Config::MailServer.to_string(), "mail_server"); - assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer)); - - assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys"); - assert_eq!( - Config::from_str("sys.config_keys"), - Ok(Config::SysConfigKeys) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_config_addr() { - let t = TestContext::new().await; - - // Test that uppercase address get lowercased. - assert!(t - .set_config(Config::Addr, Some("Foobar@eXample.oRg")) - .await - .is_ok()); - assert_eq!( - t.get_config(Config::Addr).await.unwrap().unwrap(), - "foobar@example.org" - ); - } - - /// Tests that "bot" config can only be set to "0" or "1". - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_config_bot() { - let t = TestContext::new().await; - - assert!(t.set_config(Config::Bot, None).await.is_ok()); - assert!(t.set_config(Config::Bot, Some("0")).await.is_ok()); - assert!(t.set_config(Config::Bot, Some("1")).await.is_ok()); - assert!(t.set_config(Config::Bot, Some("2")).await.is_err()); - assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_media_quality_config_option() { - let t = TestContext::new().await; - let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); - assert_eq!(media_quality, 0); - let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); - assert_eq!(media_quality, constants::MediaQuality::Balanced); - - t.set_config(Config::MediaQuality, Some("1")).await.unwrap(); - - let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); - assert_eq!(media_quality, 1); - assert_eq!(constants::MediaQuality::Worse as i32, 1); - let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); - assert_eq!(media_quality, constants::MediaQuality::Worse); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ui_config() -> Result<()> { - let t = TestContext::new().await; - - assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None); - - t.set_ui_config("ui.android.screen_security", Some("safe")) - .await?; - assert_eq!( - t.get_ui_config("ui.android.screen_security").await?, - Some("safe".to_string()) - ); - - t.set_ui_config("ui.android.screen_security", None).await?; - assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None); - - assert!(t.set_ui_config("configured", Some("bar")).await.is_err()); - - Ok(()) - } - - /// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012 - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_config_bool() -> Result<()> { - let t = TestContext::new().await; - - // We need some config that defaults to true - let c = Config::E2eeEnabled; - assert_eq!(t.get_config_bool(c).await?, true); - t.set_config_bool(c, false).await?; - assert_eq!(t.get_config_bool(c).await?, false); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_self_addrs() -> Result<()> { - let alice = TestContext::new_alice().await; - - assert!(alice.is_self_addr("alice@example.org").await?); - assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]); - assert!(!alice.is_self_addr("alice@alice.com").await?); - - // Test adding the same primary address - alice.set_primary_self_addr("alice@example.org").await?; - alice.set_primary_self_addr("Alice@Example.Org").await?; - assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]); - - // Test adding a new (primary) self address - // The address is trimmed during configure by `LoginParam::from_database()`, - // so `set_primary_self_addr()` doesn't have to trim it. - alice.set_primary_self_addr("Alice@alice.com").await?; - assert!(alice.is_self_addr("aliCe@example.org").await?); - assert!(alice.is_self_addr("alice@alice.com").await?); - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["Alice@alice.com", "Alice@Example.Org"] - ); - - // Check that the entry is not duplicated - alice.set_primary_self_addr("alice@alice.com").await?; - alice.set_primary_self_addr("alice@alice.com").await?; - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["alice@alice.com", "Alice@Example.Org"] - ); - - // Test switching back - alice.set_primary_self_addr("alice@example.org").await?; - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["alice@example.org", "alice@alice.com"] - ); - - // Test setting a new primary self address, the previous self address - // should be kept as a secondary self address - alice.set_primary_self_addr("alice@alice.xyz").await?; - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"] - ); - assert!(alice.is_self_addr("alice@example.org").await?); - assert!(alice.is_self_addr("alice@alice.com").await?); - assert!(alice.is_self_addr("Alice@alice.xyz").await?); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mdns_default_behaviour() -> Result<()> { - let t = &TestContext::new_alice().await; - assert!(t.should_request_mdns().await?); - assert!(t.should_send_mdns().await?); - assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); - // The setting should be displayed correctly. - assert!(t.get_config_bool(Config::MdnsEnabled).await?); - - t.set_config_bool(Config::Bot, true).await?; - assert!(!t.should_request_mdns().await?); - assert!(t.should_send_mdns().await?); - assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); - assert!(t.get_config_bool(Config::MdnsEnabled).await?); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_server_after_default() -> Result<()> { - let t = &TestContext::new_alice().await; - - // Check that the settings are displayed correctly. - assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string())); - assert_eq!( - t.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) - ); - - // Leaving emails on the server even w/o `BccSelf` is a good default at least because other - // MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail - // does). - t.set_config_bool(Config::BccSelf, false).await?; - assert_eq!( - t.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) - ); - Ok(()) - } - - const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync() -> 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 mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?; - // Alice1 has a different config value. - alice1 - .set_config_bool(Config::MdnsEnabled, !mdns_enabled) - .await?; - // This changes nothing, but still sends a sync message. - alice0 - .set_config_bool(Config::MdnsEnabled, mdns_enabled) - .await?; - sync(&alice0, &alice1).await; - assert_eq!( - alice1.get_config_bool(Config::MdnsEnabled).await?, - mdns_enabled - ); - - // Reset to default. Test that it's not synced because defaults may differ across client - // versions. - alice0.set_config(Config::MdnsEnabled, None).await?; - alice0.set_config_bool(Config::MdnsEnabled, false).await?; - sync(&alice0, &alice1).await; - assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false); - - for key in [Config::ShowEmails, Config::MvboxMove] { - let val = alice0.get_config_bool(key).await?; - alice0.set_config_bool(key, !val).await?; - sync(&alice0, &alice1).await; - assert_eq!(alice1.get_config_bool(key).await?, !val); - } - - // `Config::SyncMsgs` mustn't be synced. - alice0.set_config_bool(Config::SyncMsgs, false).await?; - alice0.set_config_bool(Config::SyncMsgs, true).await?; - alice0.set_config_bool(Config::MdnsEnabled, true).await?; - sync(&alice0, &alice1).await; - assert!(alice1.get_config_bool(Config::MdnsEnabled).await?); - - // Usual sync scenario. - async fn test_config_str( - alice0: &TestContext, - alice1: &TestContext, - key: Config, - val: &str, - ) -> Result<()> { - alice0.set_config(key, Some(val)).await?; - sync(alice0, alice1).await; - assert_eq!(alice1.get_config(key).await?, Some(val.to_string())); - Ok(()) - } - test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?; - test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?; - - assert!(alice0.get_config(Config::Selfavatar).await?.is_none()); - let file = alice0.dir.path().join("avatar.png"); - let bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await?; - alice0 - .set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await?; - sync(&alice0, &alice1).await; - // There was a bug that a sync message creates the self-chat with the user avatar instead of - // the special icon and that remains so when the self-chat becomes user-visible. Let's check - // this. - let self_chat = alice0.get_self_chat().await; - let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap(); - assert_eq!( - self_chat_avatar_path, - alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE) - ); - assert!(alice1 - .get_config(Config::Selfavatar) - .await? - .filter(|path| path.ends_with(".png")) - .is_some()); - alice0.set_config(Config::Selfavatar, None).await?; - sync(&alice0, &alice1).await; - assert!(alice1.get_config(Config::Selfavatar).await?.is_none()); - - Ok(()) - } - - /// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_sync_on_self_sent_msg() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice0 = &tcm.alice().await; - let alice1 = &tcm.alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - - let status = "Synced via usual message"; - alice0.set_config(Config::Selfstatus, Some(status)).await?; - alice0.send_sync_msg().await?; - alice0.pop_sent_sync_msg().await; - let status1 = "Synced via sync message"; - alice1.set_config(Config::Selfstatus, Some(status1)).await?; - tcm.send_recv(alice0, alice1, "hi Alice!").await; - assert_eq!( - alice1.get_config(Config::Selfstatus).await?, - Some(status.to_string()) - ); - sync(alice1, alice0).await; - assert_eq!( - alice0.get_config(Config::Selfstatus).await?, - Some(status1.to_string()) - ); - - // Need a chat with another contact to send self-avatar. - let bob = &tcm.bob().await; - let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id; - let file = alice0.dir.path().join("avatar.png"); - let bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await?; - alice0 - .set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await?; - alice0.send_sync_msg().await?; - alice0.pop_sent_sync_msg().await; - let file = alice1.dir.path().join("avatar.jpg"); - let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); - tokio::fs::write(&file, bytes).await?; - alice1 - .set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await?; - let sent_msg = alice0.send_text(a0b_chat_id, "hi").await; - alice1.recv_msg(&sent_msg).await; - assert!(alice1 - .get_config(Config::Selfavatar) - .await? - .filter(|path| path.ends_with(".png")) - .is_some()); - sync(alice1, alice0).await; - assert!(alice0 - .get_config(Config::Selfavatar) - .await? - .filter(|path| path.ends_with(".jpg")) - .is_some()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_event_config_synced() -> 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?; - } - - alice0 - .set_config(Config::Displayname, Some("Alice Sync")) - .await?; - alice0 - .evtracker - .get_matching(|e| { - matches!( - e, - EventType::ConfigSynced { - key: Config::Displayname - } - ) - }) - .await; - sync(&alice0, &alice1).await; - assert_eq!( - alice1.get_config(Config::Displayname).await?, - Some("Alice Sync".to_string()) - ); - alice1 - .evtracker - .get_matching(|e| { - matches!( - e, - EventType::ConfigSynced { - key: Config::Displayname - } - ) - }) - .await; - - alice0.set_config(Config::Displayname, None).await?; - alice0 - .evtracker - .get_matching(|e| { - matches!( - e, - EventType::ConfigSynced { - key: Config::Displayname - } - ) - }) - .await; - - Ok(()) - } -} +mod config_tests; diff --git a/src/config/config_tests.rs b/src/config/config_tests.rs new file mode 100644 index 0000000000..3cb218a183 --- /dev/null +++ b/src/config/config_tests.rs @@ -0,0 +1,392 @@ +use num_traits::FromPrimitive; + +use super::*; +use crate::test_utils::{sync, TestContext, TestContextManager}; + +#[test] +fn test_to_string() { + assert_eq!(Config::MailServer.to_string(), "mail_server"); + assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer)); + + assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys"); + assert_eq!( + Config::from_str("sys.config_keys"), + Ok(Config::SysConfigKeys) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_config_addr() { + let t = TestContext::new().await; + + // Test that uppercase address get lowercased. + assert!(t + .set_config(Config::Addr, Some("Foobar@eXample.oRg")) + .await + .is_ok()); + assert_eq!( + t.get_config(Config::Addr).await.unwrap().unwrap(), + "foobar@example.org" + ); +} + +/// Tests that "bot" config can only be set to "0" or "1". +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_config_bot() { + let t = TestContext::new().await; + + assert!(t.set_config(Config::Bot, None).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("0")).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("1")).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("2")).await.is_err()); + assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_media_quality_config_option() { + let t = TestContext::new().await; + let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); + assert_eq!(media_quality, 0); + let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); + assert_eq!(media_quality, constants::MediaQuality::Balanced); + + t.set_config(Config::MediaQuality, Some("1")).await.unwrap(); + + let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); + assert_eq!(media_quality, 1); + assert_eq!(constants::MediaQuality::Worse as i32, 1); + let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); + assert_eq!(media_quality, constants::MediaQuality::Worse); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ui_config() -> Result<()> { + let t = TestContext::new().await; + + assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None); + + t.set_ui_config("ui.android.screen_security", Some("safe")) + .await?; + assert_eq!( + t.get_ui_config("ui.android.screen_security").await?, + Some("safe".to_string()) + ); + + t.set_ui_config("ui.android.screen_security", None).await?; + assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None); + + assert!(t.set_ui_config("configured", Some("bar")).await.is_err()); + + Ok(()) +} + +/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012 +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_config_bool() -> Result<()> { + let t = TestContext::new().await; + + // We need some config that defaults to true + let c = Config::E2eeEnabled; + assert_eq!(t.get_config_bool(c).await?, true); + t.set_config_bool(c, false).await?; + assert_eq!(t.get_config_bool(c).await?, false); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_self_addrs() -> Result<()> { + let alice = TestContext::new_alice().await; + + assert!(alice.is_self_addr("alice@example.org").await?); + assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]); + assert!(!alice.is_self_addr("alice@alice.com").await?); + + // Test adding the same primary address + alice.set_primary_self_addr("alice@example.org").await?; + alice.set_primary_self_addr("Alice@Example.Org").await?; + assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]); + + // Test adding a new (primary) self address + // The address is trimmed during configure by `LoginParam::from_database()`, + // so `set_primary_self_addr()` doesn't have to trim it. + alice.set_primary_self_addr("Alice@alice.com").await?; + assert!(alice.is_self_addr("aliCe@example.org").await?); + assert!(alice.is_self_addr("alice@alice.com").await?); + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["Alice@alice.com", "Alice@Example.Org"] + ); + + // Check that the entry is not duplicated + alice.set_primary_self_addr("alice@alice.com").await?; + alice.set_primary_self_addr("alice@alice.com").await?; + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["alice@alice.com", "Alice@Example.Org"] + ); + + // Test switching back + alice.set_primary_self_addr("alice@example.org").await?; + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["alice@example.org", "alice@alice.com"] + ); + + // Test setting a new primary self address, the previous self address + // should be kept as a secondary self address + alice.set_primary_self_addr("alice@alice.xyz").await?; + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"] + ); + assert!(alice.is_self_addr("alice@example.org").await?); + assert!(alice.is_self_addr("alice@alice.com").await?); + assert!(alice.is_self_addr("Alice@alice.xyz").await?); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mdns_default_behaviour() -> Result<()> { + let t = &TestContext::new_alice().await; + assert!(t.should_request_mdns().await?); + assert!(t.should_send_mdns().await?); + assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); + // The setting should be displayed correctly. + assert!(t.get_config_bool(Config::MdnsEnabled).await?); + + t.set_config_bool(Config::Bot, true).await?; + assert!(!t.should_request_mdns().await?); + assert!(t.should_send_mdns().await?); + assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); + assert!(t.get_config_bool(Config::MdnsEnabled).await?); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_server_after_default() -> Result<()> { + let t = &TestContext::new_alice().await; + + // Check that the settings are displayed correctly. + assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string())); + assert_eq!( + t.get_config(Config::DeleteServerAfter).await?, + Some("0".to_string()) + ); + + // Leaving emails on the server even w/o `BccSelf` is a good default at least because other + // MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail + // does). + t.set_config_bool(Config::BccSelf, false).await?; + assert_eq!( + t.get_config(Config::DeleteServerAfter).await?, + Some("0".to_string()) + ); + Ok(()) +} + +const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync() -> 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 mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?; + // Alice1 has a different config value. + alice1 + .set_config_bool(Config::MdnsEnabled, !mdns_enabled) + .await?; + // This changes nothing, but still sends a sync message. + alice0 + .set_config_bool(Config::MdnsEnabled, mdns_enabled) + .await?; + sync(&alice0, &alice1).await; + assert_eq!( + alice1.get_config_bool(Config::MdnsEnabled).await?, + mdns_enabled + ); + + // Reset to default. Test that it's not synced because defaults may differ across client + // versions. + alice0.set_config(Config::MdnsEnabled, None).await?; + alice0.set_config_bool(Config::MdnsEnabled, false).await?; + sync(&alice0, &alice1).await; + assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false); + + for key in [Config::ShowEmails, Config::MvboxMove] { + let val = alice0.get_config_bool(key).await?; + alice0.set_config_bool(key, !val).await?; + sync(&alice0, &alice1).await; + assert_eq!(alice1.get_config_bool(key).await?, !val); + } + + // `Config::SyncMsgs` mustn't be synced. + alice0.set_config_bool(Config::SyncMsgs, false).await?; + alice0.set_config_bool(Config::SyncMsgs, true).await?; + alice0.set_config_bool(Config::MdnsEnabled, true).await?; + sync(&alice0, &alice1).await; + assert!(alice1.get_config_bool(Config::MdnsEnabled).await?); + + // Usual sync scenario. + async fn test_config_str( + alice0: &TestContext, + alice1: &TestContext, + key: Config, + val: &str, + ) -> Result<()> { + alice0.set_config(key, Some(val)).await?; + sync(alice0, alice1).await; + assert_eq!(alice1.get_config(key).await?, Some(val.to_string())); + Ok(()) + } + test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?; + test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?; + + assert!(alice0.get_config(Config::Selfavatar).await?.is_none()); + let file = alice0.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + alice0 + .set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + sync(&alice0, &alice1).await; + // There was a bug that a sync message creates the self-chat with the user avatar instead of + // the special icon and that remains so when the self-chat becomes user-visible. Let's check + // this. + let self_chat = alice0.get_self_chat().await; + let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap(); + assert_eq!( + self_chat_avatar_path, + alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE) + ); + assert!(alice1 + .get_config(Config::Selfavatar) + .await? + .filter(|path| path.ends_with(".png")) + .is_some()); + alice0.set_config(Config::Selfavatar, None).await?; + sync(&alice0, &alice1).await; + assert!(alice1.get_config(Config::Selfavatar).await?.is_none()); + + Ok(()) +} + +/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_sync_on_self_sent_msg() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice0 = &tcm.alice().await; + let alice1 = &tcm.alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + + let status = "Synced via usual message"; + alice0.set_config(Config::Selfstatus, Some(status)).await?; + alice0.send_sync_msg().await?; + alice0.pop_sent_sync_msg().await; + let status1 = "Synced via sync message"; + alice1.set_config(Config::Selfstatus, Some(status1)).await?; + tcm.send_recv(alice0, alice1, "hi Alice!").await; + assert_eq!( + alice1.get_config(Config::Selfstatus).await?, + Some(status.to_string()) + ); + sync(alice1, alice0).await; + assert_eq!( + alice0.get_config(Config::Selfstatus).await?, + Some(status1.to_string()) + ); + + // Need a chat with another contact to send self-avatar. + let bob = &tcm.bob().await; + let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id; + let file = alice0.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + alice0 + .set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + alice0.send_sync_msg().await?; + alice0.pop_sent_sync_msg().await; + let file = alice1.dir.path().join("avatar.jpg"); + let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg"); + tokio::fs::write(&file, bytes).await?; + alice1 + .set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + let sent_msg = alice0.send_text(a0b_chat_id, "hi").await; + alice1.recv_msg(&sent_msg).await; + assert!(alice1 + .get_config(Config::Selfavatar) + .await? + .filter(|path| path.ends_with(".png")) + .is_some()); + sync(alice1, alice0).await; + assert!(alice0 + .get_config(Config::Selfavatar) + .await? + .filter(|path| path.ends_with(".jpg")) + .is_some()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_event_config_synced() -> 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?; + } + + alice0 + .set_config(Config::Displayname, Some("Alice Sync")) + .await?; + alice0 + .evtracker + .get_matching(|e| { + matches!( + e, + EventType::ConfigSynced { + key: Config::Displayname + } + ) + }) + .await; + sync(&alice0, &alice1).await; + assert_eq!( + alice1.get_config(Config::Displayname).await?, + Some("Alice Sync".to_string()) + ); + alice1 + .evtracker + .get_matching(|e| { + matches!( + e, + EventType::ConfigSynced { + key: Config::Displayname + } + ) + }) + .await; + + alice0.set_config(Config::Displayname, None).await?; + alice0 + .evtracker + .get_matching(|e| { + matches!( + e, + EventType::ConfigSynced { + key: Config::Displayname + } + ) + }) + .await; + + Ok(()) +} diff --git a/src/mimefactory.rs b/src/mimefactory.rs index b418f73a63..f0a3671487 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1732,870 +1732,4 @@ fn encode_words(word: &str) -> String { } #[cfg(test)] -mod tests { - use deltachat_contact_tools::ContactAddress; - use mail_builder::headers::Header; - use mailparse::{addrparse_header, MailHeaderMap}; - use std::str; - - use super::*; - use crate::chat::{ - add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId, - ProtectionStatus, - }; - use crate::chatlist::Chatlist; - use crate::constants; - use crate::contact::Origin; - use crate::headerdef::HeaderDef; - use crate::mimeparser::MimeMessage; - use crate::receive_imf::receive_imf; - use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; - - fn render_email_address(display_name: &str, addr: &str) -> String { - let mut output = Vec::::new(); - new_address_with_name(display_name, addr.to_string()) - .unwrap_address() - .write_header(&mut output, 0) - .unwrap(); - - String::from_utf8(output).unwrap() - } - - #[test] - fn test_render_email_address() { - let display_name = "ä space"; - let addr = "x@y.org"; - - assert!(!display_name.is_ascii()); - assert!(!display_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == ' ')); - - let s = render_email_address(display_name, addr); - - println!("{s}"); - - assert_eq!(s, r#""=?utf-8?B?w6Qgc3BhY2U=?=" "#); - } - - #[test] - fn test_render_email_address_noescape() { - let display_name = "a space"; - let addr = "x@y.org"; - - assert!(display_name.is_ascii()); - assert!(display_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == ' ')); - - let s = render_email_address(display_name, addr); - - // Addresses should not be unnecessarily be encoded, see : - assert_eq!(s, r#""a space" "#); - } - - #[test] - fn test_render_email_address_duplicated_as_name() { - let addr = "x@y.org"; - let s = render_email_address(addr, addr); - assert_eq!(s, ""); - } - - #[test] - fn test_render_rfc724_mid() { - assert_eq!( - render_rfc724_mid("kqjwle123@qlwe"), - "".to_string() - ); - assert_eq!( - render_rfc724_mid(" kqjwle123@qlwe "), - "".to_string() - ); - assert_eq!( - render_rfc724_mid(""), - "".to_string() - ); - } - - fn render_header_text(text: &str) -> String { - let mut output = Vec::::new(); - mail_builder::headers::text::Text::new(text.to_string()) - .write_header(&mut output, 0) - .unwrap(); - - String::from_utf8(output).unwrap() - } - - #[test] - fn test_header_encoding() { - assert_eq!(render_header_text("foobar"), "foobar\r\n"); - assert_eq!(render_header_text("-_.~%"), "-_.~%\r\n"); - assert_eq!(render_header_text("äöü"), "=?utf-8?B?w6TDtsO8?=\r\n"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_manually_set_subject() -> Result<()> { - let t = TestContext::new_alice().await; - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - let mut msg = Message::new(Viewtype::Text); - msg.set_subject("Subjeeeeect".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let payload = sent_msg.payload(); - - assert_eq!(payload.match_indices("Subject: Subjeeeeect").count(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_from_mua() { - // 1.: Receive a mail from an MUA - assert_eq!( - msg_to_subject_str( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Antw: Chat: hello\n\ - Message-ID: <2222@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - ) - .await, - "Re: Chat: hello" - ); - - assert_eq!( - msg_to_subject_str( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Infos: 42\n\ - Message-ID: <2222@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - ) - .await, - "Re: Infos: 42" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_from_dc() { - // 2. Receive a message from Delta Chat - assert_eq!( - msg_to_subject_str( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: Chat: hello\n\ - Chat-Version: 1.0\n\ - Message-ID: <2223@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - ) - .await, - "Re: Chat: hello" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_outgoing() { - // 3. Send the first message to a new contact - let t = TestContext::new_alice().await; - - assert_eq!(first_subject_str(t).await, "Message from alice@example.org"); - - let t = TestContext::new_alice().await; - t.set_config(Config::Displayname, Some("Alice")) - .await - .unwrap(); - assert_eq!(first_subject_str(t).await, "Message from Alice"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_unicode() { - // 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result) - msg_to_subject_str( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: äääää\n\ - Chat-Version: 1.0\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - .as_bytes(), - ) - .await; - - msg_to_subject_str( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: aäääää\n\ - Chat-Version: 1.0\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - .as_bytes(), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_mdn() { - // 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used - let t = TestContext::new_alice().await; - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Hello, Bob\n\ - Chat-Version: 1.0\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let mut new_msg = incoming_msg_to_reply_msg( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\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: \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\ - Reporting-UA: Delta Chat 1.28.0\n\ - Original-Recipient: rfc822;bob@example.com\n\ - Final-Recipient: rfc822;bob@example.com\n\ - Original-Message-ID: <2893@example.com>\n\ - Disposition: manual-action/MDN-sent-automatically; displayed\n\ - \n", &t).await; - chat::send_msg(&t, new_msg.chat_id, &mut new_msg) - .await - .unwrap(); - let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); - // The subject string should not be "Re: message opened" - assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mdn_create_encrypted() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - alice - .set_config(Config::Displayname, Some("Alice Exampleorg")) - .await?; - let bob = tcm.bob().await; - bob.set_config(Config::Displayname, Some("Bob Examplenet")) - .await?; - bob.set_config(Config::Selfstatus, Some("Bob Examplenet")) - .await?; - bob.set_config_bool(Config::MdnsEnabled, true).await?; - - let mut msg = Message::new(Viewtype::Text); - msg.param.set_int(Param::SkipAutocrypt, 1); - let chat_alice = alice.create_chat(&bob).await.id; - let sent = alice.send_msg(chat_alice, &mut msg).await; - - let rcvd = bob.recv_msg(&sent).await; - message::markseen_msgs(&bob, vec![rcvd.id]).await?; - let mimefactory = - MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?; - let rendered_msg = mimefactory.render(&bob).await?; - - assert!(!rendered_msg.is_encrypted); - assert!(!rendered_msg.message.contains("Bob Examplenet")); - assert!(!rendered_msg.message.contains("Alice Exampleorg")); - let bob_alice_contact = bob.add_or_lookup_contact(&alice).await; - assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg"); - - let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await; - message::markseen_msgs(&bob, vec![rcvd.id]).await?; - - let mimefactory = - MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?; - let rendered_msg = mimefactory.render(&bob).await?; - - // When encrypted, the MDN should be encrypted as well - assert!(rendered_msg.is_encrypted); - assert!(!rendered_msg.message.contains("Bob Examplenet")); - assert!(!rendered_msg.message.contains("Alice Exampleorg")); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_in_group() -> Result<()> { - async fn send_msg_get_subject( - t: &TestContext, - group_id: ChatId, - quote: Option<&Message>, - ) -> Result { - let mut new_msg = Message::new_text("Hi".to_string()); - if let Some(q) = quote { - new_msg.set_quote(t, Some(q)).await?; - } - let sent = t.send_msg(group_id, &mut new_msg).await; - get_subject(t, sent).await - } - async fn get_subject( - t: &TestContext, - sent: crate::test_utils::SentMessage<'_>, - ) -> Result { - let parsed_subject = t.parse_msg(&sent).await.get_subject().unwrap(); - - let sent_msg = sent.load_from_db().await; - assert_eq!(parsed_subject, sent_msg.subject); - - Ok(parsed_subject) - } - - // 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject - let t = TestContext::new_alice().await; - let group_id = - chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä - .await - .unwrap(); - let bob = Contact::create(&t, "", "bob@example.org").await?; - chat::add_contact_to_chat(&t, group_id, bob).await?; - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "groupname"); - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "Re: groupname"); - - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: Different subject\n\ - In-Reply-To: {}\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n", - t.get_last_msg().await.rfc724_mid - ) - .as_bytes(), - false, - ) - .await?; - let message_from_bob = t.get_last_msg().await; - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "Re: groupname"); - - let subject = send_msg_get_subject(&t, group_id, Some(&message_from_bob)).await?; - let outgoing_quoting_msg = t.get_last_msg().await; - assert_eq!(subject, "Re: Different subject"); - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "Re: groupname"); - - let subject = send_msg_get_subject(&t, group_id, Some(&outgoing_quoting_msg)).await?; - assert_eq!(subject, "Re: Different subject"); - - chat::forward_msgs(&t, &[message_from_bob.id], group_id).await?; - let subject = get_subject(&t, t.pop_sent_msg().await).await?; - assert_eq!(subject, "Re: groupname"); - Ok(()) - } - - async fn first_subject_str(t: TestContext) -> String { - let contact_id = Contact::add_or_lookup( - &t, - "Dave", - &ContactAddress::new("dave@example.com").unwrap(), - Origin::ManuallyCreated, - ) - .await - .unwrap() - .0; - - let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); - - let mut new_msg = Message::new_text("Hi".to_string()); - new_msg.chat_id = chat_id; - chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap(); - - let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); - - mf.subject_str(&t).await.unwrap() - } - - // In `imf_raw`, From has to be bob@example.com, To has to be alice@example.org - async fn msg_to_subject_str(imf_raw: &[u8]) -> String { - let subject_str = msg_to_subject_str_inner(imf_raw, false, false, false).await; - - // Check that combinations of true and false reproduce the same subject_str: - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, true, false, false).await - ); - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, false, true, false).await - ); - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, false, true, true).await - ); - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, true, true, false).await - ); - - // These two combinations are different: If `message_arrives_inbetween` is true, but - // `reply` is false, the core is actually expected to use the subject of the message - // that arrived in between. - assert_eq!( - "Re: Some other, completely unrelated subject", - msg_to_subject_str_inner(imf_raw, false, false, true).await - ); - assert_eq!( - "Re: Some other, completely unrelated subject", - msg_to_subject_str_inner(imf_raw, true, false, true).await - ); - - // We leave away the combination (true, true, true) here: - // It would mean that the original message is quoted without sending the quoting message - // out yet, then the original message is deleted, then another unrelated message arrives - // and then the message with the quote is sent out. Not very realistic. - - subject_str - } - - async fn msg_to_subject_str_inner( - imf_raw: &[u8], - delete_original_msg: bool, - reply: bool, - message_arrives_inbetween: bool, - ) -> String { - let t = TestContext::new_alice().await; - let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await; - let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 1).await; - - if delete_original_msg { - incoming_msg.id.trash(&t, false).await.unwrap(); - } - - if message_arrives_inbetween { - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Some other, completely unrelated subject\n\ - Message-ID: <3cl4@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - Some other, completely unrelated content\n", - false, - ) - .await - .unwrap(); - - let arrived_msg = t.get_last_msg().await; - assert_eq!(arrived_msg.chat_id, incoming_msg.chat_id); - } - - if reply { - new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap(); - } - - chat::send_msg(&t, new_msg.chat_id, &mut new_msg) - .await - .unwrap(); - let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); - mf.subject_str(&t).await.unwrap() - } - - // Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`. - async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message { - context - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(context, imf_raw, false).await.unwrap(); - - let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); - - let chat_id = chats.get_chat_id(0).unwrap(); - chat_id.accept(context).await.unwrap(); - - let mut new_msg = Message::new_text("Hi".to_string()); - new_msg.chat_id = chat_id; - - new_msg - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - // This test could still be extended - async fn test_render_reply() { - let t = TestContext::new_alice().await; - let context = &t; - - let mut msg = incoming_msg_to_reply_msg( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Charlie \n\ - To: alice@example.org\n\ - Subject: Chat: hello\n\ - Chat-Version: 1.0\n\ - Message-ID: <2223@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n", - context, - ) - .await; - chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap(); - - let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap(); - - let recipients = mimefactory.recipients(); - assert_eq!(recipients, vec!["charlie@example.com"]); - - let rendered_msg = mimefactory.render(context).await.unwrap(); - - let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap(); - assert_eq!( - mail.headers - .iter() - .find(|h| h.get_key() == "MIME-Version") - .unwrap() - .get_value(), - "1.0" - ); - - let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes(), None) - .await - .unwrap(); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { - // create chat with bob, set selfavatar - let t = TestContext::new_alice().await; - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - let file = t.dir.path().join("avatar.png"); - let bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await?; - t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await?; - - // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; - // make sure, `Subject:` stays in the outer header (imf header) - let mut msg = Message::new_text("this is the text!".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); - - let outer = payload.next().unwrap(); - let inner = payload.next().unwrap(); - let body = payload.next().unwrap(); - - assert_eq!(outer.match_indices("multipart/mixed").count(), 1); - assert_eq!(outer.match_indices("Message-ID:").count(), 1); - assert_eq!(outer.match_indices("Subject:").count(), 1); - assert_eq!(outer.match_indices("Autocrypt:").count(), 1); - assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); - - assert_eq!(inner.match_indices("text/plain").count(), 1); - assert_eq!(inner.match_indices("Message-ID:").count(), 1); - assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); - assert_eq!(inner.match_indices("Subject:").count(), 0); - - assert_eq!(body.match_indices("this is the text!").count(), 1); - - // if another message is sent, that one must not contain the avatar - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); - let outer = payload.next().unwrap(); - let inner = payload.next().unwrap(); - let body = payload.next().unwrap(); - - assert_eq!(outer.match_indices("multipart/mixed").count(), 1); - assert_eq!(outer.match_indices("Message-ID:").count(), 1); - assert_eq!(outer.match_indices("Subject:").count(), 1); - assert_eq!(outer.match_indices("Autocrypt:").count(), 1); - assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); - - assert_eq!(inner.match_indices("text/plain").count(), 1); - assert_eq!(inner.match_indices("Message-ID:").count(), 1); - assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); - assert_eq!(inner.match_indices("Subject:").count(), 0); - - assert_eq!(body.match_indices("this is the text!").count(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_selfavatar_unencrypted_signed() { - // create chat with bob, set selfavatar - let t = TestContext::new_alice().await; - t.set_config(Config::SignUnencrypted, Some("1")) - .await - .unwrap(); - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - let file = t.dir.path().join("avatar.png"); - let bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await.unwrap(); - t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await - .unwrap(); - - // send message to bob: that should get multipart/signed. - // `Subject:` is protected by copying it. - // make sure, `Subject:` stays in the outer header (imf header) - let mut msg = Message::new_text("this is the text!".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("multipart/signed").count(), 1); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!( - part.match_indices("multipart/mixed; protected-headers=\"v1\"") - .count(), - 1 - ); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 0); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("text/plain").count(), 1); - assert_eq!(part.match_indices("From:").count(), 0); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1); - assert_eq!(part.match_indices("Subject:").count(), 0); - - let body = payload.next().unwrap(); - assert_eq!(body.match_indices("this is the text!").count(), 1); - - let bob = TestContext::new_bob().await; - bob.recv_msg(&sent_msg).await; - let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .unwrap() - .unwrap(); - let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); - assert!(alice_contact - .get_profile_image(&bob.ctx) - .await - .unwrap() - .is_some()); - - // if another message is sent, that one must not contain the avatar - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("multipart/signed").count(), 1); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!( - part.match_indices("multipart/mixed; protected-headers=\"v1\"") - .count(), - 1 - ); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 0); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("text/plain").count(), 1); - assert_eq!(body.match_indices("From:").count(), 0); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 0); - - let body = payload.next().unwrap(); - assert_eq!(body.match_indices("this is the text!").count(), 1); - - bob.recv_msg(&sent_msg).await; - let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); - assert!(alice_contact - .get_profile_image(&bob.ctx) - .await - .unwrap() - .is_some()); - } - - /// Test that removed member address does not go into the `To:` field. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_remove_member_bcc() -> Result<()> { - // Alice creates a group with Bob and Claire and then removes Bob. - let alice = TestContext::new_alice().await; - - let claire_addr = "claire@foo.de"; - let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "Claire", claire_addr).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?; - send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?; - - remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; - let remove = alice.pop_sent_msg().await; - let remove_payload = remove.payload(); - let parsed = mailparse::parse_mail(remove_payload.as_bytes())?; - let to = parsed - .headers - .get_first_header("To") - .context("no To: header parsed")?; - let to = addrparse_header(to)?; - for to_addr in to.iter() { - match to_addr { - mailparse::MailAddr::Single(ref info) => { - // Addresses should be of existing members (Alice and Bob) and not Claire. - assert_ne!(info.addr, claire_addr); - } - mailparse::MailAddr::Group(_) => { - panic!("Group addresses are not expected here"); - } - } - } - - Ok(()) - } - - /// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_from_before_autocrypt() -> Result<()> { - // create chat with bob - let t = TestContext::new_alice().await; - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; - // make sure, `Subject:` stays in the outer header (imf header) - let mut msg = Message::new_text("this is the text!".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let payload = sent_msg.payload(); - - assert_eq!(payload.match_indices("Autocrypt:").count(), 1); - assert_eq!(payload.match_indices("From:").count(), 1); - - assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_protected_headers_directive() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let chat = tcm - .send_recv_accept(&alice, &bob, "alice->bob") - .await - .chat_id; - - // Now Bob can send an encrypted message to Alice. - let mut msg = Message::new(Viewtype::File); - // Long messages are truncated and MimeMessage::decoded_data is set for them. We need - // decoded_data to check presence of the necessary headers. - msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1)); - msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?; - let sent = bob.send_msg(chat, &mut msg).await; - assert!(msg.get_showpadlock()); - assert!(sent.payload.contains("\r\nSubject: [...]\r\n")); - - let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?; - let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n"); - let part = payload.next().unwrap(); - assert_eq!( - part.match_indices("multipart/mixed; protected-headers=\"v1\"") - .count(), - 1 - ); - assert_eq!(part.match_indices("Subject:").count(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_remove_self() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let first_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) - .await; - alice.send_text(first_group, "Hi! I created a group.").await; - remove_contact_from_chat(alice, first_group, ContactId::SELF).await?; - alice.pop_sent_msg().await; - - let second_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) - .await; - let sent = alice - .send_text(second_group, "Hi! I created another group.") - .await; - - println!("{}", sent.payload); - let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None) - .await - .unwrap(); - assert_eq!( - mime_message.get_header(HeaderDef::ChatGroupPastMembers), - None - ); - assert_eq!( - mime_message.chat_group_member_timestamps().unwrap().len(), - 1 // There is a timestamp for Bob, not for Alice - ); - - Ok(()) - } -} +mod mimefactory_tests; diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs new file mode 100644 index 0000000000..bf099cda6d --- /dev/null +++ b/src/mimefactory/mimefactory_tests.rs @@ -0,0 +1,863 @@ +use deltachat_contact_tools::ContactAddress; +use mail_builder::headers::Header; +use mailparse::{addrparse_header, MailHeaderMap}; +use std::str; + +use super::*; +use crate::chat::{ + add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId, + ProtectionStatus, +}; +use crate::chatlist::Chatlist; +use crate::constants; +use crate::contact::Origin; +use crate::headerdef::HeaderDef; +use crate::mimeparser::MimeMessage; +use crate::receive_imf::receive_imf; +use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; + +fn render_email_address(display_name: &str, addr: &str) -> String { + let mut output = Vec::::new(); + new_address_with_name(display_name, addr.to_string()) + .unwrap_address() + .write_header(&mut output, 0) + .unwrap(); + + String::from_utf8(output).unwrap() +} + +#[test] +fn test_render_email_address() { + let display_name = "ä space"; + let addr = "x@y.org"; + + assert!(!display_name.is_ascii()); + assert!(!display_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == ' ')); + + let s = render_email_address(display_name, addr); + + println!("{s}"); + + assert_eq!(s, r#""=?utf-8?B?w6Qgc3BhY2U=?=" "#); +} + +#[test] +fn test_render_email_address_noescape() { + let display_name = "a space"; + let addr = "x@y.org"; + + assert!(display_name.is_ascii()); + assert!(display_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == ' ')); + + let s = render_email_address(display_name, addr); + + // Addresses should not be unnecessarily be encoded, see : + assert_eq!(s, r#""a space" "#); +} + +#[test] +fn test_render_email_address_duplicated_as_name() { + let addr = "x@y.org"; + let s = render_email_address(addr, addr); + assert_eq!(s, ""); +} + +#[test] +fn test_render_rfc724_mid() { + assert_eq!( + render_rfc724_mid("kqjwle123@qlwe"), + "".to_string() + ); + assert_eq!( + render_rfc724_mid(" kqjwle123@qlwe "), + "".to_string() + ); + assert_eq!( + render_rfc724_mid(""), + "".to_string() + ); +} + +fn render_header_text(text: &str) -> String { + let mut output = Vec::::new(); + mail_builder::headers::text::Text::new(text.to_string()) + .write_header(&mut output, 0) + .unwrap(); + + String::from_utf8(output).unwrap() +} + +#[test] +fn test_header_encoding() { + assert_eq!(render_header_text("foobar"), "foobar\r\n"); + assert_eq!(render_header_text("-_.~%"), "-_.~%\r\n"); + assert_eq!(render_header_text("äöü"), "=?utf-8?B?w6TDtsO8?=\r\n"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_manually_set_subject() -> Result<()> { + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + let mut msg = Message::new(Viewtype::Text); + msg.set_subject("Subjeeeeect".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let payload = sent_msg.payload(); + + assert_eq!(payload.match_indices("Subject: Subjeeeeect").count(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_from_mua() { + // 1.: Receive a mail from an MUA + assert_eq!( + msg_to_subject_str( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Antw: Chat: hello\n\ + Message-ID: <2222@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + ) + .await, + "Re: Chat: hello" + ); + + assert_eq!( + msg_to_subject_str( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Infos: 42\n\ + Message-ID: <2222@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + ) + .await, + "Re: Infos: 42" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_from_dc() { + // 2. Receive a message from Delta Chat + assert_eq!( + msg_to_subject_str( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: Chat: hello\n\ + Chat-Version: 1.0\n\ + Message-ID: <2223@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + ) + .await, + "Re: Chat: hello" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_outgoing() { + // 3. Send the first message to a new contact + let t = TestContext::new_alice().await; + + assert_eq!(first_subject_str(t).await, "Message from alice@example.org"); + + let t = TestContext::new_alice().await; + t.set_config(Config::Displayname, Some("Alice")) + .await + .unwrap(); + assert_eq!(first_subject_str(t).await, "Message from Alice"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_unicode() { + // 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result) + msg_to_subject_str( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: äääää\n\ + Chat-Version: 1.0\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + .as_bytes(), + ) + .await; + + msg_to_subject_str( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: aäääää\n\ + Chat-Version: 1.0\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + .as_bytes(), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_mdn() { + // 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used + let t = TestContext::new_alice().await; + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Hello, Bob\n\ + Chat-Version: 1.0\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let mut new_msg = incoming_msg_to_reply_msg( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\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: \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\ + Reporting-UA: Delta Chat 1.28.0\n\ + Original-Recipient: rfc822;bob@example.com\n\ + Final-Recipient: rfc822;bob@example.com\n\ + Original-Message-ID: <2893@example.com>\n\ + Disposition: manual-action/MDN-sent-automatically; displayed\n\ + \n", &t).await; + chat::send_msg(&t, new_msg.chat_id, &mut new_msg) + .await + .unwrap(); + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); + // The subject string should not be "Re: message opened" + assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mdn_create_encrypted() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + alice + .set_config(Config::Displayname, Some("Alice Exampleorg")) + .await?; + let bob = tcm.bob().await; + bob.set_config(Config::Displayname, Some("Bob Examplenet")) + .await?; + bob.set_config(Config::Selfstatus, Some("Bob Examplenet")) + .await?; + bob.set_config_bool(Config::MdnsEnabled, true).await?; + + let mut msg = Message::new(Viewtype::Text); + msg.param.set_int(Param::SkipAutocrypt, 1); + let chat_alice = alice.create_chat(&bob).await.id; + let sent = alice.send_msg(chat_alice, &mut msg).await; + + let rcvd = bob.recv_msg(&sent).await; + message::markseen_msgs(&bob, vec![rcvd.id]).await?; + let mimefactory = + MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?; + let rendered_msg = mimefactory.render(&bob).await?; + + assert!(!rendered_msg.is_encrypted); + assert!(!rendered_msg.message.contains("Bob Examplenet")); + assert!(!rendered_msg.message.contains("Alice Exampleorg")); + let bob_alice_contact = bob.add_or_lookup_contact(&alice).await; + assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg"); + + let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await; + message::markseen_msgs(&bob, vec![rcvd.id]).await?; + + let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?; + let rendered_msg = mimefactory.render(&bob).await?; + + // When encrypted, the MDN should be encrypted as well + assert!(rendered_msg.is_encrypted); + assert!(!rendered_msg.message.contains("Bob Examplenet")); + assert!(!rendered_msg.message.contains("Alice Exampleorg")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_in_group() -> Result<()> { + async fn send_msg_get_subject( + t: &TestContext, + group_id: ChatId, + quote: Option<&Message>, + ) -> Result { + let mut new_msg = Message::new_text("Hi".to_string()); + if let Some(q) = quote { + new_msg.set_quote(t, Some(q)).await?; + } + let sent = t.send_msg(group_id, &mut new_msg).await; + get_subject(t, sent).await + } + async fn get_subject( + t: &TestContext, + sent: crate::test_utils::SentMessage<'_>, + ) -> Result { + let parsed_subject = t.parse_msg(&sent).await.get_subject().unwrap(); + + let sent_msg = sent.load_from_db().await; + assert_eq!(parsed_subject, sent_msg.subject); + + Ok(parsed_subject) + } + + // 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject + let t = TestContext::new_alice().await; + let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä + .await + .unwrap(); + let bob = Contact::create(&t, "", "bob@example.org").await?; + chat::add_contact_to_chat(&t, group_id, bob).await?; + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "groupname"); + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "Re: groupname"); + + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: Different subject\n\ + In-Reply-To: {}\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n", + t.get_last_msg().await.rfc724_mid + ) + .as_bytes(), + false, + ) + .await?; + let message_from_bob = t.get_last_msg().await; + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "Re: groupname"); + + let subject = send_msg_get_subject(&t, group_id, Some(&message_from_bob)).await?; + let outgoing_quoting_msg = t.get_last_msg().await; + assert_eq!(subject, "Re: Different subject"); + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "Re: groupname"); + + let subject = send_msg_get_subject(&t, group_id, Some(&outgoing_quoting_msg)).await?; + assert_eq!(subject, "Re: Different subject"); + + chat::forward_msgs(&t, &[message_from_bob.id], group_id).await?; + let subject = get_subject(&t, t.pop_sent_msg().await).await?; + assert_eq!(subject, "Re: groupname"); + Ok(()) +} + +async fn first_subject_str(t: TestContext) -> String { + let contact_id = Contact::add_or_lookup( + &t, + "Dave", + &ContactAddress::new("dave@example.com").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap() + .0; + + let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); + + let mut new_msg = Message::new_text("Hi".to_string()); + new_msg.chat_id = chat_id; + chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap(); + + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); + + mf.subject_str(&t).await.unwrap() +} + +// In `imf_raw`, From has to be bob@example.com, To has to be alice@example.org +async fn msg_to_subject_str(imf_raw: &[u8]) -> String { + let subject_str = msg_to_subject_str_inner(imf_raw, false, false, false).await; + + // Check that combinations of true and false reproduce the same subject_str: + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, true, false, false).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, false, true, false).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, false, true, true).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, true, true, false).await + ); + + // These two combinations are different: If `message_arrives_inbetween` is true, but + // `reply` is false, the core is actually expected to use the subject of the message + // that arrived in between. + assert_eq!( + "Re: Some other, completely unrelated subject", + msg_to_subject_str_inner(imf_raw, false, false, true).await + ); + assert_eq!( + "Re: Some other, completely unrelated subject", + msg_to_subject_str_inner(imf_raw, true, false, true).await + ); + + // We leave away the combination (true, true, true) here: + // It would mean that the original message is quoted without sending the quoting message + // out yet, then the original message is deleted, then another unrelated message arrives + // and then the message with the quote is sent out. Not very realistic. + + subject_str +} + +async fn msg_to_subject_str_inner( + imf_raw: &[u8], + delete_original_msg: bool, + reply: bool, + message_arrives_inbetween: bool, +) -> String { + let t = TestContext::new_alice().await; + let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await; + let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 1).await; + + if delete_original_msg { + incoming_msg.id.trash(&t, false).await.unwrap(); + } + + if message_arrives_inbetween { + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Some other, completely unrelated subject\n\ + Message-ID: <3cl4@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + Some other, completely unrelated content\n", + false, + ) + .await + .unwrap(); + + let arrived_msg = t.get_last_msg().await; + assert_eq!(arrived_msg.chat_id, incoming_msg.chat_id); + } + + if reply { + new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap(); + } + + chat::send_msg(&t, new_msg.chat_id, &mut new_msg) + .await + .unwrap(); + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); + mf.subject_str(&t).await.unwrap() +} + +// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`. +async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message { + context + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(context, imf_raw, false).await.unwrap(); + + let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); + + let chat_id = chats.get_chat_id(0).unwrap(); + chat_id.accept(context).await.unwrap(); + + let mut new_msg = Message::new_text("Hi".to_string()); + new_msg.chat_id = chat_id; + + new_msg +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +// This test could still be extended +async fn test_render_reply() { + let t = TestContext::new_alice().await; + let context = &t; + + let mut msg = incoming_msg_to_reply_msg( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Charlie \n\ + To: alice@example.org\n\ + Subject: Chat: hello\n\ + Chat-Version: 1.0\n\ + Message-ID: <2223@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n", + context, + ) + .await; + chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap(); + + let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap(); + + let recipients = mimefactory.recipients(); + assert_eq!(recipients, vec!["charlie@example.com"]); + + let rendered_msg = mimefactory.render(context).await.unwrap(); + + let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap(); + assert_eq!( + mail.headers + .iter() + .find(|h| h.get_key() == "MIME-Version") + .unwrap() + .get_value(), + "1.0" + ); + + let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes(), None) + .await + .unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { + // create chat with bob, set selfavatar + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + let file = t.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + + // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; + // make sure, `Subject:` stays in the outer header (imf header) + let mut msg = Message::new_text("this is the text!".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); + + let outer = payload.next().unwrap(); + let inner = payload.next().unwrap(); + let body = payload.next().unwrap(); + + assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Message-ID:").count(), 1); + assert_eq!(outer.match_indices("Subject:").count(), 1); + assert_eq!(outer.match_indices("Autocrypt:").count(), 1); + assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); + + assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Message-ID:").count(), 1); + assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); + assert_eq!(inner.match_indices("Subject:").count(), 0); + + assert_eq!(body.match_indices("this is the text!").count(), 1); + + // if another message is sent, that one must not contain the avatar + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); + let outer = payload.next().unwrap(); + let inner = payload.next().unwrap(); + let body = payload.next().unwrap(); + + assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Message-ID:").count(), 1); + assert_eq!(outer.match_indices("Subject:").count(), 1); + assert_eq!(outer.match_indices("Autocrypt:").count(), 1); + assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); + + assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Message-ID:").count(), 1); + assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(inner.match_indices("Subject:").count(), 0); + + assert_eq!(body.match_indices("this is the text!").count(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_selfavatar_unencrypted_signed() { + // create chat with bob, set selfavatar + let t = TestContext::new_alice().await; + t.set_config(Config::SignUnencrypted, Some("1")) + .await + .unwrap(); + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + let file = t.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await.unwrap(); + t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await + .unwrap(); + + // send message to bob: that should get multipart/signed. + // `Subject:` is protected by copying it. + // make sure, `Subject:` stays in the outer header (imf header) + let mut msg = Message::new_text("this is the text!".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("multipart/signed").count(), 1); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!( + part.match_indices("multipart/mixed; protected-headers=\"v1\"") + .count(), + 1 + ); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 0); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 0); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("text/plain").count(), 1); + assert_eq!(part.match_indices("From:").count(), 0); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1); + assert_eq!(part.match_indices("Subject:").count(), 0); + + let body = payload.next().unwrap(); + assert_eq!(body.match_indices("this is the text!").count(), 1); + + let bob = TestContext::new_bob().await; + bob.recv_msg(&sent_msg).await; + let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) + .await + .unwrap() + .unwrap(); + let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); + assert!(alice_contact + .get_profile_image(&bob.ctx) + .await + .unwrap() + .is_some()); + + // if another message is sent, that one must not contain the avatar + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("multipart/signed").count(), 1); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!( + part.match_indices("multipart/mixed; protected-headers=\"v1\"") + .count(), + 1 + ); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 0); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 0); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("text/plain").count(), 1); + assert_eq!(body.match_indices("From:").count(), 0); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(part.match_indices("Subject:").count(), 0); + + let body = payload.next().unwrap(); + assert_eq!(body.match_indices("this is the text!").count(), 1); + + bob.recv_msg(&sent_msg).await; + let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); + assert!(alice_contact + .get_profile_image(&bob.ctx) + .await + .unwrap() + .is_some()); +} + +/// Test that removed member address does not go into the `To:` field. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_remove_member_bcc() -> Result<()> { + // Alice creates a group with Bob and Claire and then removes Bob. + let alice = TestContext::new_alice().await; + + let claire_addr = "claire@foo.de"; + let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?; + let claire_id = Contact::create(&alice, "Claire", claire_addr).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?; + send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?; + + remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; + let remove = alice.pop_sent_msg().await; + let remove_payload = remove.payload(); + let parsed = mailparse::parse_mail(remove_payload.as_bytes())?; + let to = parsed + .headers + .get_first_header("To") + .context("no To: header parsed")?; + let to = addrparse_header(to)?; + for to_addr in to.iter() { + match to_addr { + mailparse::MailAddr::Single(ref info) => { + // Addresses should be of existing members (Alice and Bob) and not Claire. + assert_ne!(info.addr, claire_addr); + } + mailparse::MailAddr::Group(_) => { + panic!("Group addresses are not expected here"); + } + } + } + + Ok(()) +} + +/// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_from_before_autocrypt() -> Result<()> { + // create chat with bob + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; + // make sure, `Subject:` stays in the outer header (imf header) + let mut msg = Message::new_text("this is the text!".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let payload = sent_msg.payload(); + + assert_eq!(payload.match_indices("Autocrypt:").count(), 1); + assert_eq!(payload.match_indices("From:").count(), 1); + + assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_protected_headers_directive() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let chat = tcm + .send_recv_accept(&alice, &bob, "alice->bob") + .await + .chat_id; + + // Now Bob can send an encrypted message to Alice. + let mut msg = Message::new(Viewtype::File); + // Long messages are truncated and MimeMessage::decoded_data is set for them. We need + // decoded_data to check presence of the necessary headers. + msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1)); + msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?; + let sent = bob.send_msg(chat, &mut msg).await; + assert!(msg.get_showpadlock()); + assert!(sent.payload.contains("\r\nSubject: [...]\r\n")); + + let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?; + let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n"); + let part = payload.next().unwrap(); + assert_eq!( + part.match_indices("multipart/mixed; protected-headers=\"v1\"") + .count(), + 1 + ); + assert_eq!(part.match_indices("Subject:").count(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_remove_self() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let first_group = alice + .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) + .await; + alice.send_text(first_group, "Hi! I created a group.").await; + remove_contact_from_chat(alice, first_group, ContactId::SELF).await?; + alice.pop_sent_msg().await; + + let second_group = alice + .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) + .await; + let sent = alice + .send_text(second_group, "Hi! I created another group.") + .await; + + println!("{}", sent.payload); + let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None) + .await + .unwrap(); + assert_eq!( + mime_message.get_header(HeaderDef::ChatGroupPastMembers), + None + ); + assert_eq!( + mime_message.chat_group_member_timestamps().unwrap().len(), + 1 // There is a timestamp for Bob, not for Alice + ); + + Ok(()) +} diff --git a/src/tools.rs b/src/tools.rs index b23f3b4820..7ba3ae6762 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -740,567 +740,4 @@ pub(crate) fn inc_and_check( } #[cfg(test)] -mod tests { - use chrono::NaiveDate; - use proptest::prelude::*; - - use super::*; - use crate::chatlist::Chatlist; - use crate::{chat, test_utils}; - use crate::{receive_imf::receive_imf, test_utils::TestContext}; - - #[test] - fn test_parse_receive_headers() { - // Test `parse_receive_headers()` with some more-or-less random emails from the test-data - let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); - let expected = - "Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\ - Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000"; - check_parse_receive_headers(raw, expected); - - let raw = include_bytes!("../test-data/message/wrong-html.eml"); - let expected = - "Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 6 Aug 2020 16:40:31 +0000\n\ - Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 6 Aug 2020 16:40:32 +0000"; - check_parse_receive_headers(raw, expected); - - let raw = include_bytes!("../test-data/message/posteo_ndn.eml"); - let expected = - "Hop: By: mout01.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ - Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ - Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ - Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ - Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ - Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 9 Jun 2020 18:44:24 +0000"; - check_parse_receive_headers(raw, expected); - } - - fn check_parse_receive_headers(raw: &[u8], expected: &str) { - let mail = mailparse::parse_mail(raw).unwrap(); - let hop_info = parse_receive_headers(&mail.get_headers()); - assert_eq!(hop_info, expected) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_receive_headers_integration() { - let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); - let expected = r"State: Fresh - -hi - -Message-ID: 2dfdbde7@example.org - -Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000 -Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000 - -DKIM Results: Passed=true"; - check_parse_receive_headers_integration(raw, expected).await; - - let raw = include_bytes!("../test-data/message/encrypted_with_received_headers.eml"); - let expected = "State: Fresh, Encrypted - -Re: Message from alice@example.org - -hi back\r\n\ -\r\n\ --- \r\n\ -Sent with my Delta Chat Messenger: https://delta.chat - -Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net - -Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000 -Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 -Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 - -DKIM Results: Passed=true"; - check_parse_receive_headers_integration(raw, expected).await; - } - - async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) { - let t = TestContext::new_alice().await; - receive_imf(&t, raw, false).await.unwrap(); - let msg = t.get_last_msg().await; - let msg_info = msg.id.get_info(&t).await.unwrap(); - - // Ignore the first rows of the msg_info because they contain a - // received time that depends on the test time which makes it impossible to - // compare with a static string - let capped_result = &msg_info[msg_info.find("State").unwrap()..]; - assert_eq!(expected, capped_result); - } - - #[test] - fn test_rust_ftoa() { - assert_eq!("1.22", format!("{}", 1.22)); - } - - #[test] - fn test_truncate_1() { - let s = "this is a little test string"; - assert_eq!(truncate(s, 16), "this is a [...]"); - } - - #[test] - fn test_truncate_2() { - assert_eq!(truncate("1234", 2), "1234"); - } - - #[test] - fn test_truncate_3() { - assert_eq!(truncate("1234567", 1), "1[...]"); - } - - #[test] - fn test_truncate_4() { - assert_eq!(truncate("123456", 4), "123456"); - } - - #[test] - fn test_truncate_edge() { - assert_eq!(truncate("", 4), ""); - - assert_eq!(truncate("\n hello \n world", 4), "\n [...]"); - - assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]"); - assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]"); - - // 9 characters, so no truncation - assert_eq!(truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",); - - // 12 characters, truncation - assert_eq!( - truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6), - "𑒀ὐ¢🜀\u{1e01b}A[...]", - ); - } - - mod truncate_by_lines { - use super::*; - - #[test] - fn test_just_text() { - let s = "this is a little test string".to_string(); - assert_eq!( - truncate_by_lines(s, 4, 6), - ("this is a little test [...]".to_string(), true) - ); - } - - #[test] - fn test_with_linebreaks() { - let s = "this\n is\n a little test string".to_string(); - assert_eq!( - truncate_by_lines(s, 4, 6), - ("this\n is\n a little [...]".to_string(), true) - ); - } - - #[test] - fn test_only_linebreaks() { - let s = "\n\n\n\n\n\n\n".to_string(); - assert_eq!( - truncate_by_lines(s, 4, 5), - ("\n\n\n[...]".to_string(), true) - ); - } - - #[test] - fn limit_hits_end() { - let s = "hello\n world !".to_string(); - assert_eq!( - truncate_by_lines(s, 2, 8), - ("hello\n world !".to_string(), false) - ); - } - - #[test] - fn test_edge() { - assert_eq!( - truncate_by_lines("".to_string(), 2, 4), - ("".to_string(), false) - ); - - assert_eq!( - truncate_by_lines("\n hello \n world".to_string(), 2, 4), - ("\n [...]".to_string(), true) - ); - assert_eq!( - truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 2), - ("𐠈0[...]".to_string(), true) - ); - assert_eq!( - truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 0), - ("[...]".to_string(), true) - ); - - // 9 characters, so no truncation - assert_eq!( - truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), 1, 12), - ("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), false), - ); - - // 12 characters, truncation - assert_eq!( - truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd".to_string(), 1, 7), - ("𑒀ὐ¢🜀\u{1e01b}A [...]".to_string(), true), - ); - } - } - - #[test] - fn test_create_id() { - let buf = create_id(); - assert_eq!(buf.len(), 24); - } - - #[test] - fn test_validate_id() { - for _ in 0..10 { - assert!(validate_id(&create_id())); - } - - assert_eq!(validate_id("aaaaaaaaaaaa"), true); - assert_eq!(validate_id("aa-aa_aaaXaa"), true); - - // ID cannot contain whitespace. - assert_eq!(validate_id("aaaaa aaaaaa"), false); - assert_eq!(validate_id("aaaaa\naaaaaa"), false); - - // ID cannot contain "/", "+". - assert_eq!(validate_id("aaaaa/aaaaaa"), false); - assert_eq!(validate_id("aaaaaaaa+aaa"), false); - - // Too long ID. - assert_eq!(validate_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), false); - } - - #[test] - fn test_create_id_invalid_chars() { - for _ in 1..1000 { - let buf = create_id(); - assert!(!buf.contains('/')); // `/` must not be used to be URL-safe - assert!(!buf.contains('.')); // `.` is used as a delimiter when extracting grpid from Message-ID - } - } - - #[test] - fn test_create_outgoing_rfc724_mid() { - let mid = create_outgoing_rfc724_mid(); - assert_eq!(mid.len(), 46); - assert!(mid.contains("-")); // It has an UUID inside. - assert!(mid.ends_with("@localhost")); - } - - proptest! { - #[test] - fn test_truncate( - buf: String, - approx_chars in 0..100usize - ) { - let res = truncate(&buf, approx_chars); - let el_len = 5; - let l = res.chars().count(); - assert!( - l <= approx_chars + el_len, - "buf: '{}' - res: '{}' - len {}, approx {}", - &buf, &res, res.len(), approx_chars - ); - - if buf.chars().count() > approx_chars + el_len { - let l = res.len(); - assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res); - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_file_handling() { - let t = TestContext::new().await; - let context = &t; - macro_rules! file_exist { - ($ctx:expr, $fname:expr) => { - $ctx.get_blobdir() - .join(Path::new($fname).file_name().unwrap()) - .exists() - }; - } - - assert!(delete_file(context, "$BLOBDIR/lkqwjelqkwlje") - .await - .is_err()); - assert!(write_file(context, "$BLOBDIR/foobar", b"content") - .await - .is_ok()); - assert!(file_exist!(context, "$BLOBDIR/foobar")); - assert!(!file_exist!(context, "$BLOBDIR/foobarx")); - assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await.unwrap(), 7); - - let abs_path = context - .get_blobdir() - .join("foobar") - .to_string_lossy() - .to_string(); - - assert!(file_exist!(context, &abs_path)); - - assert!(delete_file(context, "$BLOBDIR/foobar").await.is_ok()); - assert!(create_folder(context, "$BLOBDIR/foobar-folder") - .await - .is_ok()); - assert!(file_exist!(context, "$BLOBDIR/foobar-folder")); - assert!(delete_file(context, "$BLOBDIR/foobar-folder") - .await - .is_err()); - - let fn0 = "$BLOBDIR/data.data"; - assert!(write_file(context, &fn0, b"content").await.is_ok()); - - assert!(delete_file(context, &fn0).await.is_ok()); - assert!(!file_exist!(context, &fn0)); - } - - #[test] - fn test_duration_to_str() { - assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s"); - assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s"); - assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s"); - assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s"); - assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s"); - assert_eq!( - duration_to_str(Duration::from_secs(59 * 60 + 59)), - "0h 59m 59s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(59 * 60 + 60)), - "1h 0m 0s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)), - "2h 59m 59s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)), - "3h 0m 0s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)), - "3h 0m 59s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)), - "3h 1m 0s" - ); - } - - #[test] - fn test_get_filemeta() { - let (w, h) = get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap(); - assert_eq!(w, 900); - assert_eq!(h, 900); - - let data = include_bytes!("../test-data/image/avatar1000x1000.jpg"); - let (w, h) = get_filemeta(data).unwrap(); - assert_eq!(w, 1000); - assert_eq!(h, 1000); - - let data = include_bytes!("../test-data/image/image100x50.gif"); - let (w, h) = get_filemeta(data).unwrap(); - assert_eq!(w, 100); - assert_eq!(h, 50); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_maybe_warn_on_bad_time() { - let t = TestContext::new().await; - let timestamp_now = time(); - let timestamp_future = timestamp_now + 60 * 60 * 24 * 7; - let timestamp_past = NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 9, 1).unwrap(), - NaiveTime::from_hms_opt(0, 0, 0).unwrap(), - ) - .and_utc() - .timestamp_millis() - / 1_000; - - // a correct time must not add a device message - maybe_warn_on_bad_time(&t, timestamp_now, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // we cannot find out if a date in the future is wrong - a device message is not added - maybe_warn_on_bad_time(&t, timestamp_future, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // a date in the past must add a device message - maybe_warn_on_bad_time(&t, timestamp_past, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - // the message should be added only once a day - test that an hour later and nearly a day later - maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60, get_release_timestamp()).await; - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - maybe_warn_on_bad_time( - &t, - timestamp_past + 60 * 60 * 24 - 1, - get_release_timestamp(), - ) - .await; - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - // next day, there should be another device message - maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60 * 24, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap()); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_maybe_warn_on_outdated() { - let t = TestContext::new().await; - let timestamp_now: i64 = time(); - - // in about 6 months, the app should not be outdated - // (if this fails, provider-db is not updated since 6 months) - maybe_warn_on_outdated( - &t, - timestamp_now + 180 * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // in 1 year, the app should be considered as outdated - maybe_warn_on_outdated( - &t, - timestamp_now + 365 * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - // do not repeat the warning every day ... - // (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message) - maybe_warn_on_outdated( - &t, - timestamp_now + (365 + 1) * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - maybe_warn_on_outdated( - &t, - timestamp_now + (365 + 2) * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - let test_len = msgs.len(); - assert!(test_len == 1 || test_len == 2); - - // ... but every month - // (forward generous 33 days to avoid being in the same month as in the previous check) - maybe_warn_on_outdated( - &t, - timestamp_now + (365 + 33) * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), test_len + 1); - } - - #[test] - fn test_get_release_timestamp() { - let timestamp_past = NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(), - NaiveTime::from_hms_opt(0, 0, 0).unwrap(), - ) - .and_utc() - .timestamp_millis() - / 1_000; - assert!(get_release_timestamp() <= time()); - assert!(get_release_timestamp() > timestamp_past); - } - - #[test] - fn test_remove_subject_prefix() { - assert_eq!(remove_subject_prefix("Subject"), "Subject"); - assert_eq!( - remove_subject_prefix("Chat: Re: Subject"), - "Chat: Re: Subject" - ); - assert_eq!(remove_subject_prefix("Re: Subject"), "Subject"); - assert_eq!(remove_subject_prefix("Fwd: Subject"), "Subject"); - assert_eq!(remove_subject_prefix("Fw: Subject"), "Subject"); - } - - #[test] - fn test_parse_mailto() { - let mailto_url = "mailto:someone@example.com"; - let reps = parse_mailto(mailto_url); - assert_eq!( - Some(MailTo { - to: vec![EmailAddress { - local: "someone".to_string(), - domain: "example.com".to_string() - }], - subject: None, - body: None - }), - reps - ); - - let mailto_url = "mailto:someone@example.com?subject=Hello%20World"; - let reps = parse_mailto(mailto_url); - assert_eq!( - Some(MailTo { - to: vec![EmailAddress { - local: "someone".to_string(), - domain: "example.com".to_string() - }], - subject: Some("Hello World".to_string()), - body: None - }), - reps - ); - - let mailto_url = "mailto:someone@example.com,someoneelse@example.com?subject=Hello%20World&body=This%20is%20a%20test"; - let reps = parse_mailto(mailto_url); - assert_eq!( - Some(MailTo { - to: vec![ - EmailAddress { - local: "someone".to_string(), - domain: "example.com".to_string() - }, - EmailAddress { - local: "someoneelse".to_string(), - domain: "example.com".to_string() - } - ], - subject: Some("Hello World".to_string()), - body: Some("This is a test".to_string()) - }), - reps - ); - } -} +mod tools_tests; diff --git a/src/tools/tools_tests.rs b/src/tools/tools_tests.rs new file mode 100644 index 0000000000..62c40bb752 --- /dev/null +++ b/src/tools/tools_tests.rs @@ -0,0 +1,562 @@ +use chrono::NaiveDate; +use proptest::prelude::*; + +use super::*; +use crate::chatlist::Chatlist; +use crate::{chat, test_utils}; +use crate::{receive_imf::receive_imf, test_utils::TestContext}; + +#[test] +fn test_parse_receive_headers() { + // Test `parse_receive_headers()` with some more-or-less random emails from the test-data + let raw = include_bytes!("../../test-data/message/mail_with_cc.txt"); + let expected = + "Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\ + Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000"; + check_parse_receive_headers(raw, expected); + + let raw = include_bytes!("../../test-data/message/wrong-html.eml"); + let expected = + "Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 6 Aug 2020 16:40:31 +0000\n\ + Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 6 Aug 2020 16:40:32 +0000"; + check_parse_receive_headers(raw, expected); + + let raw = include_bytes!("../../test-data/message/posteo_ndn.eml"); + let expected = + "Hop: By: mout01.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ + Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ + Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ + Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ + Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ + Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 9 Jun 2020 18:44:24 +0000"; + check_parse_receive_headers(raw, expected); +} + +fn check_parse_receive_headers(raw: &[u8], expected: &str) { + let mail = mailparse::parse_mail(raw).unwrap(); + let hop_info = parse_receive_headers(&mail.get_headers()); + assert_eq!(hop_info, expected) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_receive_headers_integration() { + let raw = include_bytes!("../../test-data/message/mail_with_cc.txt"); + let expected = r"State: Fresh + +hi + +Message-ID: 2dfdbde7@example.org + +Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000 +Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000 + +DKIM Results: Passed=true"; + check_parse_receive_headers_integration(raw, expected).await; + + let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml"); + let expected = "State: Fresh, Encrypted + +Re: Message from alice@example.org + +hi back\r\n\ +\r\n\ +-- \r\n\ +Sent with my Delta Chat Messenger: https://delta.chat + +Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net + +Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000 +Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 +Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 + +DKIM Results: Passed=true"; + check_parse_receive_headers_integration(raw, expected).await; +} + +async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) { + let t = TestContext::new_alice().await; + receive_imf(&t, raw, false).await.unwrap(); + let msg = t.get_last_msg().await; + let msg_info = msg.id.get_info(&t).await.unwrap(); + + // Ignore the first rows of the msg_info because they contain a + // received time that depends on the test time which makes it impossible to + // compare with a static string + let capped_result = &msg_info[msg_info.find("State").unwrap()..]; + assert_eq!(expected, capped_result); +} + +#[test] +fn test_rust_ftoa() { + assert_eq!("1.22", format!("{}", 1.22)); +} + +#[test] +fn test_truncate_1() { + let s = "this is a little test string"; + assert_eq!(truncate(s, 16), "this is a [...]"); +} + +#[test] +fn test_truncate_2() { + assert_eq!(truncate("1234", 2), "1234"); +} + +#[test] +fn test_truncate_3() { + assert_eq!(truncate("1234567", 1), "1[...]"); +} + +#[test] +fn test_truncate_4() { + assert_eq!(truncate("123456", 4), "123456"); +} + +#[test] +fn test_truncate_edge() { + assert_eq!(truncate("", 4), ""); + + assert_eq!(truncate("\n hello \n world", 4), "\n [...]"); + + assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]"); + assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]"); + + // 9 characters, so no truncation + assert_eq!(truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",); + + // 12 characters, truncation + assert_eq!( + truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6), + "𑒀ὐ¢🜀\u{1e01b}A[...]", + ); +} + +mod truncate_by_lines { + use super::*; + + #[test] + fn test_just_text() { + let s = "this is a little test string".to_string(); + assert_eq!( + truncate_by_lines(s, 4, 6), + ("this is a little test [...]".to_string(), true) + ); + } + + #[test] + fn test_with_linebreaks() { + let s = "this\n is\n a little test string".to_string(); + assert_eq!( + truncate_by_lines(s, 4, 6), + ("this\n is\n a little [...]".to_string(), true) + ); + } + + #[test] + fn test_only_linebreaks() { + let s = "\n\n\n\n\n\n\n".to_string(); + assert_eq!( + truncate_by_lines(s, 4, 5), + ("\n\n\n[...]".to_string(), true) + ); + } + + #[test] + fn limit_hits_end() { + let s = "hello\n world !".to_string(); + assert_eq!( + truncate_by_lines(s, 2, 8), + ("hello\n world !".to_string(), false) + ); + } + + #[test] + fn test_edge() { + assert_eq!( + truncate_by_lines("".to_string(), 2, 4), + ("".to_string(), false) + ); + + assert_eq!( + truncate_by_lines("\n hello \n world".to_string(), 2, 4), + ("\n [...]".to_string(), true) + ); + assert_eq!( + truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 2), + ("𐠈0[...]".to_string(), true) + ); + assert_eq!( + truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 0), + ("[...]".to_string(), true) + ); + + // 9 characters, so no truncation + assert_eq!( + truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), 1, 12), + ("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), false), + ); + + // 12 characters, truncation + assert_eq!( + truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd".to_string(), 1, 7), + ("𑒀ὐ¢🜀\u{1e01b}A [...]".to_string(), true), + ); + } +} + +#[test] +fn test_create_id() { + let buf = create_id(); + assert_eq!(buf.len(), 24); +} + +#[test] +fn test_validate_id() { + for _ in 0..10 { + assert!(validate_id(&create_id())); + } + + assert_eq!(validate_id("aaaaaaaaaaaa"), true); + assert_eq!(validate_id("aa-aa_aaaXaa"), true); + + // ID cannot contain whitespace. + assert_eq!(validate_id("aaaaa aaaaaa"), false); + assert_eq!(validate_id("aaaaa\naaaaaa"), false); + + // ID cannot contain "/", "+". + assert_eq!(validate_id("aaaaa/aaaaaa"), false); + assert_eq!(validate_id("aaaaaaaa+aaa"), false); + + // Too long ID. + assert_eq!(validate_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), false); +} + +#[test] +fn test_create_id_invalid_chars() { + for _ in 1..1000 { + let buf = create_id(); + assert!(!buf.contains('/')); // `/` must not be used to be URL-safe + assert!(!buf.contains('.')); // `.` is used as a delimiter when extracting grpid from Message-ID + } +} + +#[test] +fn test_create_outgoing_rfc724_mid() { + let mid = create_outgoing_rfc724_mid(); + assert_eq!(mid.len(), 46); + assert!(mid.contains("-")); // It has an UUID inside. + assert!(mid.ends_with("@localhost")); +} + +proptest! { + #[test] + fn test_truncate( + buf: String, + approx_chars in 0..100usize + ) { + let res = truncate(&buf, approx_chars); + let el_len = 5; + let l = res.chars().count(); + assert!( + l <= approx_chars + el_len, + "buf: '{}' - res: '{}' - len {}, approx {}", + &buf, &res, res.len(), approx_chars + ); + + if buf.chars().count() > approx_chars + el_len { + let l = res.len(); + assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_file_handling() { + let t = TestContext::new().await; + let context = &t; + macro_rules! file_exist { + ($ctx:expr, $fname:expr) => { + $ctx.get_blobdir() + .join(Path::new($fname).file_name().unwrap()) + .exists() + }; + } + + assert!(delete_file(context, "$BLOBDIR/lkqwjelqkwlje") + .await + .is_err()); + assert!(write_file(context, "$BLOBDIR/foobar", b"content") + .await + .is_ok()); + assert!(file_exist!(context, "$BLOBDIR/foobar")); + assert!(!file_exist!(context, "$BLOBDIR/foobarx")); + assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await.unwrap(), 7); + + let abs_path = context + .get_blobdir() + .join("foobar") + .to_string_lossy() + .to_string(); + + assert!(file_exist!(context, &abs_path)); + + assert!(delete_file(context, "$BLOBDIR/foobar").await.is_ok()); + assert!(create_folder(context, "$BLOBDIR/foobar-folder") + .await + .is_ok()); + assert!(file_exist!(context, "$BLOBDIR/foobar-folder")); + assert!(delete_file(context, "$BLOBDIR/foobar-folder") + .await + .is_err()); + + let fn0 = "$BLOBDIR/data.data"; + assert!(write_file(context, &fn0, b"content").await.is_ok()); + + assert!(delete_file(context, &fn0).await.is_ok()); + assert!(!file_exist!(context, &fn0)); +} + +#[test] +fn test_duration_to_str() { + assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s"); + assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s"); + assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s"); + assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s"); + assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s"); + assert_eq!( + duration_to_str(Duration::from_secs(59 * 60 + 59)), + "0h 59m 59s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(59 * 60 + 60)), + "1h 0m 0s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)), + "2h 59m 59s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)), + "3h 0m 0s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)), + "3h 0m 59s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)), + "3h 1m 0s" + ); +} + +#[test] +fn test_get_filemeta() { + let (w, h) = get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap(); + assert_eq!(w, 900); + assert_eq!(h, 900); + + let data = include_bytes!("../../test-data/image/avatar1000x1000.jpg"); + let (w, h) = get_filemeta(data).unwrap(); + assert_eq!(w, 1000); + assert_eq!(h, 1000); + + let data = include_bytes!("../../test-data/image/image100x50.gif"); + let (w, h) = get_filemeta(data).unwrap(); + assert_eq!(w, 100); + assert_eq!(h, 50); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_warn_on_bad_time() { + let t = TestContext::new().await; + let timestamp_now = time(); + let timestamp_future = timestamp_now + 60 * 60 * 24 * 7; + let timestamp_past = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 9, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + .and_utc() + .timestamp_millis() + / 1_000; + + // a correct time must not add a device message + maybe_warn_on_bad_time(&t, timestamp_now, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // we cannot find out if a date in the future is wrong - a device message is not added + maybe_warn_on_bad_time(&t, timestamp_future, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // a date in the past must add a device message + maybe_warn_on_bad_time(&t, timestamp_past, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + // the message should be added only once a day - test that an hour later and nearly a day later + maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60, get_release_timestamp()).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + maybe_warn_on_bad_time( + &t, + timestamp_past + 60 * 60 * 24 - 1, + get_release_timestamp(), + ) + .await; + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + // next day, there should be another device message + maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60 * 24, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap()); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_warn_on_outdated() { + let t = TestContext::new().await; + let timestamp_now: i64 = time(); + + // in about 6 months, the app should not be outdated + // (if this fails, provider-db is not updated since 6 months) + maybe_warn_on_outdated( + &t, + timestamp_now + 180 * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // in 1 year, the app should be considered as outdated + maybe_warn_on_outdated( + &t, + timestamp_now + 365 * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + // do not repeat the warning every day ... + // (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message) + maybe_warn_on_outdated( + &t, + timestamp_now + (365 + 1) * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + maybe_warn_on_outdated( + &t, + timestamp_now + (365 + 2) * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + let test_len = msgs.len(); + assert!(test_len == 1 || test_len == 2); + + // ... but every month + // (forward generous 33 days to avoid being in the same month as in the previous check) + maybe_warn_on_outdated( + &t, + timestamp_now + (365 + 33) * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), test_len + 1); +} + +#[test] +fn test_get_release_timestamp() { + let timestamp_past = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + .and_utc() + .timestamp_millis() + / 1_000; + assert!(get_release_timestamp() <= time()); + assert!(get_release_timestamp() > timestamp_past); +} + +#[test] +fn test_remove_subject_prefix() { + assert_eq!(remove_subject_prefix("Subject"), "Subject"); + assert_eq!( + remove_subject_prefix("Chat: Re: Subject"), + "Chat: Re: Subject" + ); + assert_eq!(remove_subject_prefix("Re: Subject"), "Subject"); + assert_eq!(remove_subject_prefix("Fwd: Subject"), "Subject"); + assert_eq!(remove_subject_prefix("Fw: Subject"), "Subject"); +} + +#[test] +fn test_parse_mailto() { + let mailto_url = "mailto:someone@example.com"; + let reps = parse_mailto(mailto_url); + assert_eq!( + Some(MailTo { + to: vec![EmailAddress { + local: "someone".to_string(), + domain: "example.com".to_string() + }], + subject: None, + body: None + }), + reps + ); + + let mailto_url = "mailto:someone@example.com?subject=Hello%20World"; + let reps = parse_mailto(mailto_url); + assert_eq!( + Some(MailTo { + to: vec![EmailAddress { + local: "someone".to_string(), + domain: "example.com".to_string() + }], + subject: Some("Hello World".to_string()), + body: None + }), + reps + ); + + let mailto_url = "mailto:someone@example.com,someoneelse@example.com?subject=Hello%20World&body=This%20is%20a%20test"; + let reps = parse_mailto(mailto_url); + assert_eq!( + Some(MailTo { + to: vec![ + EmailAddress { + local: "someone".to_string(), + domain: "example.com".to_string() + }, + EmailAddress { + local: "someoneelse".to_string(), + domain: "example.com".to_string() + } + ], + subject: Some("Hello World".to_string()), + body: Some("This is a test".to_string()) + }), + reps + ); +}