diff --git a/CHANGELOG.md b/CHANGELOG.md
index c492f4e3e..63af2105d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -289,8 +289,8 @@
- Use vCard in TestContext.add_or_lookup_contact().
- Remove test_group_with_removed_message_id.
-- Use add_or_lookup_email_contact() in get_chat().
-- Use add_or_lookup_email_contact in test_setup_contact_ex.
+- Use add_or_lookup_address_contact() in get_chat().
+- Use add_or_lookup_address_contact in test_setup_contact_ex.
- Use vCards more in Python tests.
- Use TestContextManager in more tests.
- Use vCards to create contacts in more Rust tests.
diff --git a/assets/icon-address-contact.png b/assets/icon-address-contact.png
new file mode 100644
index 000000000..9262dbdac
Binary files /dev/null and b/assets/icon-address-contact.png differ
diff --git a/assets/icon-address-contact.svg b/assets/icon-address-contact.svg
new file mode 100644
index 000000000..0cb9d97a8
--- /dev/null
+++ b/assets/icon-address-contact.svg
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
diff --git a/assets/self-reporting-bot.vcf b/assets/self-reporting-bot.vcf
new file mode 100644
index 000000000..0e38035fe
--- /dev/null
+++ b/assets/self-reporting-bot.vcf
@@ -0,0 +1,7 @@
+BEGIN:VCARD
+VERSION:4.0
+EMAIL:self_reporting@testrun.org
+FN:Statistics bot
+KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4=
+REV:20250412T195751Z
+END:VCARD
diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h
index b2fca4d1b..233f19242 100644
--- a/deltachat-ffi/deltachat.h
+++ b/deltachat-ffi/deltachat.h
@@ -3838,6 +3838,21 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
+/**
+ * Check if the chat is encrypted.
+ *
+ * 1:1 chats with key-contacts and group chats with key-contacts
+ * are encrypted.
+ * 1:1 chats with emails contacts and ad-hoc groups
+ * created for email threads are not encrypted.
+ *
+ * @memberof dc_chat_t
+ * @param chat The chat object.
+ * @return 1=chat is encrypted, 0=chat is not encrypted.
+ */
+int dc_chat_is_encrypted (const dc_chat_t *chat);
+
+
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
@@ -6886,6 +6901,7 @@ void dc_event_unref(dc_event_t* event);
/// "End-to-end encryption preferred."
///
/// Used to build the string returned by dc_get_contact_encrinfo().
+/// @deprecated 2025-06-05
#define DC_STR_E2E_PREFERRED 34
/// "%1$s verified"
@@ -6898,12 +6914,14 @@ void dc_event_unref(dc_event_t* event);
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact that cannot be verified
+/// @deprecated 2025-06-05
#define DC_STR_CONTACT_NOT_VERIFIED 36
/// "Changed setup for %1$s."
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact with the changed setup
+/// @deprecated 2025-06-05
#define DC_STR_CONTACT_SETUP_CHANGED 37
/// "Archived chats"
@@ -7293,6 +7311,7 @@ void dc_event_unref(dc_event_t* event);
/// "%1$s changed their address from %2$s to %3$s"
///
/// Used as an info message to chats with contacts that changed their address.
+/// @deprecated 2025-06-05
#define DC_STR_AEAP_ADDR_CHANGED 122
/// "You changed your email address from %1$s to %2$s.
@@ -7599,6 +7618,7 @@ void dc_event_unref(dc_event_t* event);
/// "The contact must be online to proceed. This process will continue automatically in background."
///
/// Used as info message.
+/// @deprecated 2025-06-05
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
/// "Contact". Deprecated, currently unused.
diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs
index 884d89476..3567879cc 100644
--- a/deltachat-ffi/src/lib.rs
+++ b/deltachat-ffi/src/lib.rs
@@ -3153,6 +3153,18 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
ffi_chat.chat.is_protected() as libc::c_int
}
+#[no_mangle]
+pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_int {
+ if chat.is_null() {
+ eprintln!("ignoring careless call to dc_chat_is_encrypted()");
+ return 0;
+ }
+ let ffi_chat = &*chat;
+
+ block_on(ffi_chat.chat.is_encrypted(&ffi_chat.context))
+ .unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
+}
+
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
@@ -4303,6 +4315,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
.context("failed to get verifier")
.log_err(ctx)
.unwrap_or_default()
+ .unwrap_or_default()
.unwrap_or_default();
verifier_contact_id.to_u32()
diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs
index 3b4414297..558643df6 100644
--- a/deltachat-jsonrpc/src/api.rs
+++ b/deltachat-jsonrpc/src/api.rs
@@ -354,6 +354,20 @@ impl CommandApi {
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
+ /// If there was an error while the account was opened
+ /// and migrated to the current version,
+ /// then this function returns it.
+ ///
+ /// This function is useful because the key-contacts migration could fail due to bugs
+ /// and then the account will not work properly.
+ ///
+ /// After opening an account, the UI should call this function
+ /// and show the error string if one is returned.
+ async fn get_migration_error(&self, account_id: u32) -> Result> {
+ let ctx = self.get_context(account_id).await?;
+ Ok(ctx.get_migration_error())
+ }
+
/// Copy file to blob dir.
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result {
let ctx = self.get_context(account_id).await?;
@@ -1542,15 +1556,6 @@ impl CommandApi {
Ok(())
}
- /// Resets contact encryption.
- async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
- let ctx = self.get_context(account_id).await?;
- let contact_id = ContactId::new(contact_id);
-
- contact_id.reset_encryption(&ctx).await?;
- Ok(())
- }
-
/// Sets display name for existing contact.
async fn change_contact_name(
&self,
diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs
index b1ee3802c..777defe76 100644
--- a/deltachat-jsonrpc/src/api/types/chat.rs
+++ b/deltachat-jsonrpc/src/api/types/chat.rs
@@ -30,6 +30,29 @@ pub struct FullChat {
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
+ /// True if the chat is encrypted.
+ /// This means that all messages in the chat are encrypted,
+ /// and all contacts in the chat are "key-contacts",
+ /// i.e. identified by the PGP key fingerprint.
+ ///
+ /// False if the chat is unencrypted.
+ /// This means that all messages in the chat are unencrypted,
+ /// and all contacts in the chat are "address-contacts",
+ /// i.e. identified by the email address.
+ /// The UI should mark this chat e.g. with a mail-letter icon.
+ ///
+ /// Unencrypted groups are called "ad-hoc groups"
+ /// and the user can't add/remove members,
+ /// create a QR invite code,
+ /// or set an avatar.
+ /// These options should therefore be disabled in the UI.
+ ///
+ /// Note that it can happen that an encrypted chat
+ /// contains unencrypted messages that were received in core <= v1.159.*
+ /// and vice versa.
+ ///
+ /// See also `is_key_contact` on `Contact`.
+ is_encrypted: bool,
profile_image: Option, //BLOBS ?
archived: bool,
pinned: bool,
@@ -108,6 +131,7 @@ impl FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
+ is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
@@ -159,6 +183,30 @@ pub struct BasicChat {
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
+
+ /// True if the chat is encrypted.
+ /// This means that all messages in the chat are encrypted,
+ /// and all contacts in the chat are "key-contacts",
+ /// i.e. identified by the PGP key fingerprint.
+ ///
+ /// False if the chat is unencrypted.
+ /// This means that all messages in the chat are unencrypted,
+ /// and all contacts in the chat are "address-contacts",
+ /// i.e. identified by the email address.
+ /// The UI should mark this chat e.g. with a mail-letter icon.
+ ///
+ /// Unencrypted groups are called "ad-hoc groups"
+ /// and the user can't add/remove members,
+ /// create a QR invite code,
+ /// or set an avatar.
+ /// These options should therefore be disabled in the UI.
+ ///
+ /// Note that it can happen that an encrypted chat
+ /// contains unencrypted messages that were received in core <= v1.159.*
+ /// and vice versa.
+ ///
+ /// See also `is_key_contact` on `Contact`.
+ is_encrypted: bool,
profile_image: Option, //BLOBS ?
archived: bool,
pinned: bool,
@@ -187,6 +235,7 @@ impl BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
+ is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs
index 5a4e2cf26..d229d8c1f 100644
--- a/deltachat-jsonrpc/src/api/types/chat_list.rs
+++ b/deltachat-jsonrpc/src/api/types/chat_list.rs
@@ -30,6 +30,30 @@ pub enum ChatListItemFetchResult {
/// showing preview if last chat message is image
summary_preview_image: Option,
is_protected: bool,
+
+ /// True if the chat is encrypted.
+ /// This means that all messages in the chat are encrypted,
+ /// and all contacts in the chat are "key-contacts",
+ /// i.e. identified by the PGP key fingerprint.
+ ///
+ /// False if the chat is unencrypted.
+ /// This means that all messages in the chat are unencrypted,
+ /// and all contacts in the chat are "address-contacts",
+ /// i.e. identified by the email address.
+ /// The UI should mark this chat e.g. with a mail-letter icon.
+ ///
+ /// Unencrypted groups are called "ad-hoc groups"
+ /// and the user can't add/remove members,
+ /// create a QR invite code,
+ /// or set an avatar.
+ /// These options should therefore be disabled in the UI.
+ ///
+ /// Note that it can happen that an encrypted chat
+ /// contains unencrypted messages that were received in core <= v1.159.*
+ /// and vice versa.
+ ///
+ /// See also `is_key_contact` on `Contact`.
+ is_encrypted: bool,
is_group: bool,
fresh_message_counter: usize,
is_self_talk: bool,
@@ -137,6 +161,7 @@ pub(crate) async fn get_chat_list_item_by_id(
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
+ is_encrypted: chat.is_encrypted(ctx).await?,
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
is_self_talk: chat.is_self_talk(),
diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs
index eb85cf35c..3d8ce98df 100644
--- a/deltachat-jsonrpc/src/api/types/contact.rs
+++ b/deltachat-jsonrpc/src/api/types/contact.rs
@@ -19,6 +19,16 @@ pub struct ContactObject {
profile_image: Option, // BLOBS
name_and_addr: String,
is_blocked: bool,
+
+ /// Is the contact a key contact.
+ is_key_contact: bool,
+
+ /// Is encryption available for this contact.
+ ///
+ /// This can only be true for key-contacts.
+ /// However, it is possible to have a key-contact
+ /// for which encryption is not available because we don't have a key yet,
+ /// e.g. if we just scanned the fingerprint from a QR code.
e2ee_avail: bool,
/// True if the contact can be added to verified groups.
@@ -67,6 +77,7 @@ impl ContactObject {
let verifier_id = contact
.get_verifier_id(context)
.await?
+ .flatten()
.map(|contact_id| contact_id.to_u32());
Ok(ContactObject {
@@ -80,6 +91,7 @@ impl ContactObject {
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
+ is_key_contact: contact.is_key_contact(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
is_profile_verified,
diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs
index 511d79f02..3ad81bd12 100644
--- a/deltachat-jsonrpc/src/api/types/message.rs
+++ b/deltachat-jsonrpc/src/api/types/message.rs
@@ -59,6 +59,13 @@ pub struct MessageObject {
// summary - use/create another function if you need it
subject: String,
+
+ /// True if the message was correctly encrypted&signed, false otherwise.
+ /// Historically, UIs showed a small padlock on the message then.
+ ///
+ /// Today, the UIs should instead show a small email-icon on the message
+ /// if `show_padlock` is `false`,
+ /// and nothing if it is `true`.
show_padlock: bool,
is_setupmessage: bool,
is_info: bool,
diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs
index 2f5188f75..2c9080050 100644
--- a/deltachat-repl/src/cmdline.rs
+++ b/deltachat-repl/src/cmdline.rs
@@ -20,7 +20,6 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
-use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::reaction::send_reaction;
@@ -35,14 +34,6 @@ use tokio::fs;
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})...");
- if 0 != bits & 2 {
- context
- .sql()
- .execute("DELETE FROM acpeerstates;", ())
- .await
- .unwrap();
- println!("(2) Peerstates reset.");
- }
if 0 != bits & 4 {
context
.sql()
@@ -277,7 +268,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
for contact_id in contacts {
- let mut line2 = "".to_string();
+ let line2 = "".to_string();
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
@@ -296,15 +287,6 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
verified_str,
if !addr.is_empty() { addr } else { "addr unset" }
);
- let peerstate = Peerstate::from_addr(context, addr)
- .await
- .expect("peerstate error");
- if peerstate.is_some() && *contact_id != ContactId::SELF {
- line2 = format!(
- ", prefer-encrypt={}",
- peerstate.as_ref().unwrap().prefer_encrypt
- );
- }
println!("Contact#{}: {}{}", *contact_id, line, line2);
}
@@ -514,7 +496,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
"reset" => {
- ensure!(!arg1.is_empty(), "Argument missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
+ ensure!(
+ !arg1.is_empty(),
+ "Argument missing: 4=private keys, 8=rest but server config"
+ );
let bits: i32 = arg1.parse()?;
ensure!(bits < 16, " must be lower than 16.");
reset_tables(&context, bits).await;
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
index c2444afbd..1c0389064 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
@@ -37,10 +37,6 @@ class Contact:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
- def reset_encryption(self) -> None:
- """Reset contact encryption."""
- self._rpc.reset_contact_encryption(self.account.id, self.id)
-
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)
diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py
index 03ade4d04..31f483520 100644
--- a/deltachat-rpc-client/tests/test_securejoin.py
+++ b/deltachat-rpc-client/tests/test_securejoin.py
@@ -1,5 +1,4 @@
import logging
-import time
import pytest
@@ -16,14 +15,14 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
- alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
+ alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
- bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
+ bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
@@ -84,7 +83,7 @@ def test_qr_securejoin(acfactory, protect):
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
- alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
+ alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
@@ -93,7 +92,7 @@ def test_qr_securejoin(acfactory, protect):
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
- bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
+ bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
@@ -101,7 +100,7 @@ def test_qr_securejoin(acfactory, protect):
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
- alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
+ alice2_contact_bob = alice2.create_contact(bob)
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.is_verified
@@ -213,72 +212,8 @@ def test_setup_contact_resetup(acfactory) -> None:
bob.wait_for_securejoin_joiner_success()
-def test_verified_group_recovery(acfactory) -> None:
- """Tests verified group recovery by reverifying a member and sending a message in a group."""
- ac1, ac2, ac3 = acfactory.get_online_accounts(3)
-
- logging.info("ac1 creates verified group")
- chat = ac1.create_group("Verified group", protect=True)
- assert chat.get_basic_snapshot().is_protected
-
- logging.info("ac2 joins verified group")
- qr_code = chat.get_qr_code()
- ac2.secure_join(qr_code)
- ac2.wait_for_securejoin_joiner_success()
-
- # ac1 has ac2 directly verified.
- ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
- assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
-
- logging.info("ac3 joins verified group")
- ac3_chat = ac3.secure_join(qr_code)
- ac3.wait_for_securejoin_joiner_success()
- ac3.wait_for_incoming_msg_event() # Member added
-
- logging.info("ac2 logs in on a new device")
- ac2 = acfactory.resetup_account(ac2)
-
- logging.info("ac2 reverifies with ac3")
- qr_code = ac3.get_qr_code()
- ac2.secure_join(qr_code)
- ac2.wait_for_securejoin_joiner_success()
-
- logging.info("ac3 sends a message to the group")
- assert len(ac3_chat.get_contacts()) == 3
- ac3_chat.send_text("Hi!")
-
- snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
- assert snapshot.text == "Hi!"
-
- msg_id = ac2.wait_for_incoming_msg_event().msg_id
- message = ac2.get_message_by_id(msg_id)
- snapshot = message.get_snapshot()
- assert snapshot.text == "Hi!"
-
- # ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
- ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
- assert ac1_contact.get_snapshot().is_verified
-
- # ac2 can write messages to the group.
- snapshot.chat.send_text("Works again!")
-
- snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
- assert snapshot.text == "Works again!"
-
- snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
- assert snapshot.text == "Works again!"
-
- ac1_chat_messages = snapshot.chat.get_messages()
- ac2_addr = ac2.get_config("addr")
- assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
-
- # ac2 is now verified by ac3 for ac1
- ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
- assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
-
-
def test_verified_group_member_added_recovery(acfactory) -> None:
- """Tests verified group recovery by reverifiying than removing and adding a member back."""
+ """Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
@@ -291,7 +226,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
- ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
+ ac1_contact_ac2 = ac1.create_contact(ac2)
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
@@ -299,6 +234,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
+ ac3_contact_ac2_old = ac3.create_contact(ac2)
+
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -311,21 +248,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
- msg_id = ac2.wait_for_incoming_msg_event().msg_id
- message = ac2.get_message_by_id(msg_id)
- snapshot = message.get_snapshot()
- logging.info("Received message %s", snapshot.text)
- assert snapshot.text == "Hi!"
-
ac1.wait_for_incoming_msg_event() # Hi!
- ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
- ac3_chat.remove_contact(ac3_contact_ac2)
-
- msg_id = ac2.wait_for_incoming_msg_event().msg_id
- message = ac2.get_message_by_id(msg_id)
- snapshot = message.get_snapshot()
- assert "removed" in snapshot.text
+ ac3_contact_ac2 = ac3.create_contact(ac2)
+ ac3_chat.remove_contact(ac3_contact_ac2_old)
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
@@ -354,19 +280,16 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
- ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
+ ac1_contact_ac2 = ac1.create_contact(ac2)
+ ac1_contact_ac3 = ac1.create_contact(ac3)
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
assert ac1_contact_ac2_snapshot.is_verified
- assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
-
- # ac2 is now verified by ac3 for ac1
- ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
- assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
+ assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
- issue .
+ issue .
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
@@ -400,12 +323,12 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
- assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
- assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
+ assert ac3.create_contact(ac2).get_snapshot().is_verified
+ assert ac2.create_contact(ac3).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
- vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
+ vg.add_contact(ac3.create_contact(ac2))
# ensure ac2 receives message in VG
vg.send_text("hello")
@@ -443,7 +366,7 @@ def test_qr_new_group_unblocked(acfactory):
ac1.wait_for_securejoin_inviter_success()
ac1_new_chat = ac1.create_group("Another group")
- ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
+ ac1_new_chat.add_contact(ac1.create_contact(ac2))
# Receive "Member added" message.
ac2.wait_for_incoming_msg_event()
@@ -577,30 +500,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 resetups the account.
ac1 = acfactory.resetup_account(ac1)
-
- # Loop sending message from ac1 to ac2
- # until ac2 accepts new ac1 key.
- #
- # This may not happen immediately because resetup of ac1
- # rewinds "smeared timestamp" so Date: header for messages
- # sent by new ac1 are in the past compared to the last Date:
- # header sent by old ac1.
- while True:
- # ac1 sends a message to ac2.
- ac1_contact_ac2 = ac1.create_contact(ac2, "")
- ac1_chat_ac2 = ac1_contact_ac2.create_chat()
- ac1_chat_ac2.send_text("Hello!")
-
- # ac2 receives a message.
- snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
- assert snapshot.text == "Hello!"
- logging.info("ac2 received Hello!")
-
- # ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
- logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
- if not ac2_contact_ac1.get_snapshot().is_verified:
- break
- time.sleep(1)
+ ac2_contact_ac1 = ac2.create_contact(ac1, "")
+ assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()
diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py
index d859140f7..b7b2b683c 100644
--- a/deltachat-rpc-client/tests/test_something.py
+++ b/deltachat-rpc-client/tests/test_something.py
@@ -170,7 +170,7 @@ def test_account(acfactory) -> None:
assert alice.get_size()
assert alice.is_configured()
assert not alice.get_avatar()
- assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
+ assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
assert alice.get_contacts()
assert alice.get_contacts(snapshot=True)
assert alice.self_contact
@@ -287,7 +287,6 @@ def test_contact(acfactory) -> None:
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
- alice_contact_bob.reset_encryption()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
diff --git a/python/examples/group_tracking.py b/python/examples/group_tracking.py
deleted file mode 100644
index 51f63cb74..000000000
--- a/python/examples/group_tracking.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# content of group_tracking.py
-
-from deltachat import account_hookimpl, run_cmdline
-
-
-class GroupTrackingPlugin:
- @account_hookimpl
- def ac_incoming_message(self, message):
- print("process_incoming message", message)
- if message.text.strip() == "/quit":
- message.account.shutdown()
- else:
- # unconditionally accept the chat
- message.create_chat()
- addr = message.get_sender_contact().addr
- text = message.text
- message.chat.send_text(f"echoing from {addr}:\n{text}")
-
- @account_hookimpl
- def ac_outgoing_message(self, message):
- print("ac_outgoing_message:", message)
-
- @account_hookimpl
- def ac_configure_completed(self, success):
- print("ac_configure_completed:", success)
-
- @account_hookimpl
- def ac_chat_modified(self, chat):
- print("ac_chat_modified:", chat.id, chat.get_name())
- for member in chat.get_contacts():
- print(f"chat member: {member.addr}")
-
- @account_hookimpl
- def ac_member_added(self, chat, contact, actor, message):
- print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
- for member in chat.get_contacts():
- print(f"chat member: {member.addr}")
-
- @account_hookimpl
- def ac_member_removed(self, chat, contact, actor, message):
- print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
-
-
-def main(argv=None):
- run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
-
-
-if __name__ == "__main__":
- main()
diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py
index 7e55d6617..58fac9c65 100644
--- a/python/examples/test_examples.py
+++ b/python/examples/test_examples.py
@@ -1,10 +1,7 @@
import echo_and_quit
-import group_tracking
import py
import pytest
-from deltachat.events import FFIEventLogger
-
@pytest.fixture(scope="session")
def datadir():
@@ -36,55 +33,3 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("send quit sequence")
bot_chat.send_text("/quit")
botproc.wait()
-
-
-def test_group_tracking_plugin(acfactory, lp):
- lp.sec("creating one group-tracking bot and two temp accounts")
- botproc = acfactory.run_bot_process(group_tracking)
-
- ac1, ac2 = acfactory.get_online_accounts(2)
-
- ac1.add_account_plugin(FFIEventLogger(ac1))
- ac2.add_account_plugin(FFIEventLogger(ac2))
-
- lp.sec("creating bot test group with bot")
- bot_chat = ac1.qr_setup_contact(botproc.qr)
- ac1._evtracker.wait_securejoin_joiner_progress(1000)
- bot_contact = bot_chat.get_contacts()[0]
- ch = ac1.create_group_chat("bot test group")
- ch.add_contact(bot_contact)
- ch.send_text("hello")
-
- botproc.fnmatch_lines(
- """
- *ac_chat_modified*bot test group*
- """,
- )
-
- lp.sec("adding third member {}".format(ac2.get_config("addr")))
- contact3 = ac1.create_contact(ac2)
- ch.add_contact(contact3)
-
- reply = ac1._evtracker.wait_next_incoming_message()
- assert "hello" in reply.text
-
- lp.sec("now looking at what the bot received")
- botproc.fnmatch_lines(
- """
- *ac_member_added {}*from*{}*
- """.format(
- contact3.addr,
- ac1.get_config("addr"),
- ),
- )
-
- lp.sec("contact successfully added, now removing")
- ch.remove_contact(contact3)
- botproc.fnmatch_lines(
- """
- *ac_member_removed {}*from*{}*
- """.format(
- contact3.addr,
- ac1.get_config("addr"),
- ),
- )
diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py
index 8bc199ce9..c40382db6 100644
--- a/python/src/deltachat/account.py
+++ b/python/src/deltachat/account.py
@@ -293,6 +293,8 @@ class Account:
return Contact(self, contact_id)
def get_contact(self, obj) -> Optional[Contact]:
+ if isinstance(obj, Account):
+ return self.create_contact(obj)
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py
index b5cfa9235..edc2d5ba7 100644
--- a/python/src/deltachat/chat.py
+++ b/python/src/deltachat/chat.py
@@ -417,7 +417,13 @@ class Chat:
:raises ValueError: if contact could not be added
:returns: None
"""
- contact = self.account.create_contact(obj)
+ from .contact import Contact
+
+ if isinstance(obj, Contact):
+ contact = obj
+ else:
+ contact = self.account.create_contact(obj)
+
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError(f"could not add contact {contact!r} to chat")
diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py
index b9fcb4f64..f559e9fad 100644
--- a/python/src/deltachat/events.py
+++ b/python/src/deltachat/events.py
@@ -13,7 +13,6 @@ from .account import Account
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
-from .message import map_system_message
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
@@ -304,21 +303,15 @@ class EventThread(threading.Thread):
elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
- yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
+ yield ("ac_incoming_message", {"message": msg})
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
if msg.is_outgoing():
- res = map_system_message(msg)
- if res and res[0].startswith("ac_member"):
- yield res
yield "ac_outgoing_message", {"message": msg}
elif msg.is_in_fresh():
- yield map_system_message(msg) or (
- "ac_incoming_message",
- {"message": msg},
- )
+ yield "ac_incoming_message", {"message": msg}
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)
diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py
index d5447d76c..d15c1696e 100644
--- a/python/src/deltachat/message.py
+++ b/python/src/deltachat/message.py
@@ -2,7 +2,6 @@
import json
import os
-import re
from datetime import datetime, timezone
from typing import Optional, Union
@@ -504,56 +503,3 @@ def get_viewtype_code_from_name(view_type_name):
raise ValueError(
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
)
-
-
-#
-# some helper code for turning system messages into hook events
-#
-
-
-def map_system_message(msg):
- if msg.is_system_message():
- res = parse_system_add_remove(msg.text)
- if not res:
- return None
- action, affected, actor = res
- affected = msg.account.get_contact_by_addr(affected)
- actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
- d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
- return "ac_member_" + res[0], d
-
-
-def extract_addr(text):
- m = re.match(r".*\((.+@.+)\)", text)
- if m:
- text = m.group(1)
- text = text.rstrip(".")
- return text.strip()
-
-
-def parse_system_add_remove(text):
- """return add/remove info from parsing the given system message text.
-
- returns a (action, affected, actor) triple
- """
- # You removed member a@b.
- # You added member a@b.
- # Member Me (x@y) removed by a@b.
- # Member x@y added by a@b
- # Member With space (tmp1@x.org) removed by tmp2@x.org.
- # Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
- # Group left by some one (tmp1@x.org).
- # Group left by tmp1@x.org.
- text = text.lower()
- m = re.match(r"member (.+) (removed|added) by (.+)", text)
- if m:
- affected, action, actor = m.groups()
- return action, extract_addr(affected), extract_addr(actor)
- m = re.match(r"you (removed|added) member (.+)", text)
- if m:
- action, affected = m.groups()
- return action, extract_addr(affected), "me"
- if text.startswith("group left by "):
- addr = extract_addr(text[13:])
- if addr:
- return "removed", addr, addr
diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py
index a61515187..86bb8d7f7 100644
--- a/python/tests/test_0_complex_or_slow.py
+++ b/python/tests/test_0_complex_or_slow.py
@@ -187,83 +187,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
-def test_undecipherable_group(acfactory, lp):
- """Test how group messages that cannot be decrypted are
- handled.
-
- Group name is encrypted and plaintext subject is set to "..." in
- this case, so we should assign the messages to existing chat
- instead of creating a new one. Since there is no existing group
- chat, the messages should be assigned to 1-1 chat with the sender
- of the message.
- """
-
- lp.sec("creating and configuring three accounts")
- ac1, ac2, ac3 = acfactory.get_online_accounts(3)
-
- acfactory.introduce_each_other([ac1, ac2, ac3])
-
- lp.sec("ac3 reinstalls DC and generates a new key")
- ac3.stop_io()
- acfactory.remove_preconfigured_keys()
- ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
- acfactory.wait_configured(ac4)
- # Create contacts to make sure incoming messages are not treated as contact requests
- chat41 = ac4.create_chat(ac1)
- chat42 = ac4.create_chat(ac2)
- ac4.start_io()
- ac4._evtracker.wait_idle_inbox_ready()
-
- lp.sec("ac1: creating group chat with 2 other members")
- chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
-
- lp.sec("ac1: send message to new group chat")
- msg = chat.send_text("hello")
-
- lp.sec("ac2: checking that the chat arrived correctly")
- msg = ac2._evtracker.wait_next_incoming_message()
- assert msg.text == "hello"
- assert msg.is_encrypted(), "Message is not encrypted"
-
- # ac4 cannot decrypt the message.
- # Error message should be assigned to the chat with ac1.
- lp.sec("ac4: checking that message is assigned to the sender chat")
- error_msg = ac4._evtracker.wait_next_incoming_message()
- assert error_msg.error # There is an error decrypting the message
- assert error_msg.chat == chat41
-
- lp.sec("ac2: sending a reply to the chat")
- msg.chat.send_text("reply")
- reply = ac1._evtracker.wait_next_incoming_message()
- assert reply.text == "reply"
- assert reply.is_encrypted(), "Reply is not encrypted"
-
- lp.sec("ac4: checking that reply is assigned to ac2 chat")
- error_reply = ac4._evtracker.wait_next_incoming_message()
- assert error_reply.error # There is an error decrypting the message
- assert error_reply.chat == chat42
-
- # Test that ac4 replies to error messages don't appear in the
- # group chat on ac1 and ac2.
- lp.sec("ac4: replying to ac1 and ac2")
-
- # Otherwise reply becomes a contact request.
- chat41.send_text("I can't decrypt your message, ac1!")
- chat42.send_text("I can't decrypt your message, ac2!")
-
- msg = ac1._evtracker.wait_next_incoming_message()
- assert msg.error is None
- assert msg.text == "I can't decrypt your message, ac1!"
- assert msg.is_encrypted(), "Message is not encrypted"
- assert msg.chat == ac1.create_chat(ac3)
-
- msg = ac2._evtracker.wait_next_incoming_message()
- assert msg.error is None
- assert msg.text == "I can't decrypt your message, ac2!"
- assert msg.is_encrypted(), "Message is not encrypted"
- assert msg.chat == ac2.create_chat(ac4)
-
-
def test_ephemeral_timer(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py
index e75f36a86..2a29a54fa 100644
--- a/python/tests/test_1_online.py
+++ b/python/tests/test_1_online.py
@@ -767,7 +767,7 @@ def test_mdn_asymmetric(acfactory, lp):
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
-def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
+def test_send_receive_encrypt(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.get_device_chat().mark_noticed()
@@ -798,12 +798,11 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
msg3.mark_seen()
assert not list(ac1.get_fresh_messages())
- lp.sec("create group chat with two members, one of which has no encrypt state")
+ lp.sec("create group chat with two members")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac2)
- chat.add_contact(ac1.create_contact("notexisting@testrun.org"))
msg = chat.send_text("test not encrypt")
- assert not msg.is_encrypted()
+ assert msg.is_encrypted()
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
@@ -1139,9 +1138,9 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
lp.sec("create some chat content")
some1_addr = some1.get_config("addr")
- chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
+ chat1 = ac1.create_contact(some1).create_chat()
chat1.send_text("msg1")
- assert len(ac1.get_contacts(query="some1")) == 1
+ assert len(ac1.get_contacts()) == 1
original_image_path = data.get_path("d.png")
chat1.send_image(original_image_path)
@@ -1153,7 +1152,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
chat1.send_file(str(path))
def assert_account_is_proper(ac):
- contacts = ac.get_contacts(query="some1")
+ contacts = ac.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == some1_addr
@@ -1288,79 +1287,6 @@ def test_set_get_contact_avatar(acfactory, data, lp):
assert msg6.get_sender_contact().get_profile_image() is None
-def test_add_remove_member_remote_events(acfactory, lp):
- ac1, ac2, ac3 = acfactory.get_online_accounts(3)
- ac1_addr = ac1.get_config("addr")
- ac3_addr = ac3.get_config("addr")
- # activate local plugin for ac2
- in_list = queue.Queue()
-
- class EventHolder:
- def __init__(self, **kwargs) -> None:
- self.__dict__.update(kwargs)
-
- class InPlugin:
- @account_hookimpl
- def ac_incoming_message(self, message):
- # we immediately accept the sender because
- # otherwise we won't see member_added contacts
- message.create_chat()
-
- @account_hookimpl
- def ac_chat_modified(self, chat):
- in_list.put(EventHolder(action="chat-modified", chat=chat))
-
- @account_hookimpl
- def ac_member_added(self, chat, contact, message):
- in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message))
-
- @account_hookimpl
- def ac_member_removed(self, chat, contact, message):
- in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message))
-
- ac2.add_account_plugin(InPlugin())
-
- lp.sec("ac1: create group chat with ac2")
- chat = ac1.create_group_chat("hello", contacts=[ac2])
-
- lp.sec("ac1: send a message to group chat to promote the group")
- chat.send_text("afterwards promoted")
- ev = in_list.get()
- assert ev.action == "chat-modified"
- assert chat.is_promoted()
- assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
-
- lp.sec("ac1: add address2")
- # note that if the above create_chat() would not
- # happen we would not receive a proper member_added event
- contact2 = chat.add_contact(ac3)
- ev = in_list.get()
- assert ev.action == "chat-modified"
- ev = in_list.get()
- assert ev.action == "chat-modified"
- ev = in_list.get()
- assert ev.action == "added"
- assert ev.message.get_sender_contact().addr == ac1_addr
- assert ev.contact.addr == ac3_addr
-
- lp.sec("ac1: remove address2")
- chat.remove_contact(contact2)
- ev = in_list.get()
- assert ev.action == "chat-modified"
- ev = in_list.get()
- assert ev.action == "removed"
- assert ev.contact.addr == contact2.addr
- assert ev.message.get_sender_contact().addr == ac1_addr
-
- lp.sec("ac1: remove ac2 contact from chat")
- chat.remove_contact(ac2)
- ev = in_list.get()
- assert ev.action == "chat-modified"
- ev = in_list.get()
- assert ev.action == "removed"
- assert ev.message.get_sender_contact().addr == ac1_addr
-
-
def test_system_group_msg_from_blocked_user(acfactory, lp):
"""
Tests that a blocked user removes you from a group.
@@ -1760,44 +1686,6 @@ def test_configure_error_msgs_invalid_server(acfactory):
assert "configuration" not in ev.data2.lower()
-def test_name_changes(acfactory):
- ac1, ac2 = acfactory.get_online_accounts(2)
- ac1.set_config("displayname", "Account 1")
-
- chat12 = acfactory.get_accepted_chat(ac1, ac2)
- contact = None
-
- def update_name():
- """Send a message from ac1 to ac2 to update the name"""
- nonlocal contact
- chat12.send_text("Hello")
- msg = ac2._evtracker.wait_next_incoming_message()
- contact = msg.get_sender_contact()
- return contact.name
-
- assert update_name() == "Account 1"
-
- ac1.set_config("displayname", "Account 1 revision 2")
- assert update_name() == "Account 1 revision 2"
-
- # Explicitly rename contact on ac2 to "Renamed"
- ac2.create_contact(contact, name="Renamed")
- assert contact.name == "Renamed"
- ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
- assert ev.data1 == contact.id
-
- # ac1 also renames itself into "Renamed"
- assert update_name() == "Renamed"
- ac1.set_config("displayname", "Renamed")
- assert update_name() == "Renamed"
-
- # Contact name was set to "Renamed" explicitly before,
- # so it should not be changed.
- ac1.set_config("displayname", "Renamed again")
- updated_name = update_name()
- assert updated_name == "Renamed"
-
-
def test_status(acfactory):
"""Test that status is transferred over the network."""
ac1, ac2 = acfactory.get_online_accounts(2)
diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py
index aae958216..6a0a5ac40 100644
--- a/python/tests/test_3_offline.py
+++ b/python/tests/test_3_offline.py
@@ -1,51 +1,11 @@
import os
-import time
from datetime import datetime, timedelta, timezone
import pytest
import deltachat as dc
from deltachat.tracker import ImexFailed
-from deltachat import Account, account_hookimpl, Message
-
-
-@pytest.mark.parametrize(
- ("msgtext", "res"),
- [
- (
- "Member Me (tmp1@x.org) removed by tmp2@x.org.",
- ("removed", "tmp1@x.org", "tmp2@x.org"),
- ),
- (
- "Member With space (tmp1@x.org) removed by tmp2@x.org.",
- ("removed", "tmp1@x.org", "tmp2@x.org"),
- ),
- (
- "Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
- ("removed", "tmp1@x.org", "tmp2@x.org"),
- ),
- (
- "Member With space (tmp1@x.org) removed by me",
- ("removed", "tmp1@x.org", "me"),
- ),
- (
- "Group left by some one (tmp1@x.org).",
- ("removed", "tmp1@x.org", "tmp1@x.org"),
- ),
- ("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")),
- (
- "Member tmp1@x.org added by tmp2@x.org.",
- ("added", "tmp1@x.org", "tmp2@x.org"),
- ),
- ("Member nothing bla bla", None),
- ("Another unknown system message", None),
- ],
-)
-def test_parse_system_add_remove(msgtext, res):
- from deltachat.message import parse_system_add_remove
-
- out = parse_system_add_remove(msgtext)
- assert out == res
+from deltachat import Account, Message
class TestOfflineAccountBasic:
@@ -177,14 +137,15 @@ class TestOfflineContact:
def test_get_contacts_and_delete(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
- contact1 = ac1.create_contact("some1@example.org", name="some1")
+ ac2 = acfactory.get_pseudo_configured_account()
+ contact1 = ac1.create_contact(ac2)
contacts = ac1.get_contacts()
assert len(contacts) == 1
- assert contact1 in contacts
assert not ac1.get_contacts(query="some2")
- assert ac1.get_contacts(query="some1")
+ assert not ac1.get_contacts(query="some1")
assert len(ac1.get_contacts(with_self=True)) == 2
+ assert contact1 in ac1.get_contacts()
assert ac1.delete_contact(contact1)
assert contact1 not in ac1.get_contacts()
@@ -199,9 +160,9 @@ class TestOfflineContact:
def test_create_chat_flexibility(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
- chat1 = ac1.create_chat(ac2)
- chat2 = ac1.create_chat(ac2.get_self_contact().addr)
- assert chat1 == chat2
+ chat1 = ac1.create_chat(ac2) # This creates a key-contact chat
+ chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates address-contact chat
+ assert chat1 != chat2
ac3 = acfactory.get_unconfigured_account()
with pytest.raises(ValueError):
ac1.create_chat(ac3)
@@ -259,17 +220,18 @@ class TestOfflineChat:
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1")
- with pytest.raises(ValueError):
- chat.add_contact(ac2.get_self_contact())
contact = chat.add_contact(ac2)
assert contact.addr == ac2.get_config("addr")
assert contact.name == ac2.get_config("displayname")
assert contact.account == ac1
chat.remove_contact(ac2)
- def test_group_chat_creation(self, ac1):
- contact1 = ac1.create_contact("some1@example.org", name="some1")
- contact2 = ac1.create_contact("some2@example.org", name="some2")
+ def test_group_chat_creation(self, acfactory):
+ ac1 = acfactory.get_pseudo_configured_account()
+ ac2 = acfactory.get_pseudo_configured_account()
+ ac3 = acfactory.get_pseudo_configured_account()
+ contact1 = ac1.create_contact(ac2)
+ contact2 = ac1.create_contact(ac3)
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
assert chat.get_name() == "title1"
assert contact1 in chat.get_contacts()
@@ -316,13 +278,14 @@ class TestOfflineChat:
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
- def test_removing_blocked_user_from_group(self, ac1, lp):
+ def test_removing_blocked_user_from_group(self, ac1, acfactory, lp):
"""
Test that blocked contact is not unblocked when removed from a group.
See https://github.com/deltachat/deltachat-core-rust/issues/2030
"""
lp.sec("Create a group chat with a contact")
- contact = ac1.create_contact("some1@example.org")
+ ac2 = acfactory.get_pseudo_configured_account()
+ contact = ac1.create_contact(ac2)
group = ac1.create_group_chat("title", contacts=[contact])
group.send_text("First group message")
@@ -334,10 +297,6 @@ class TestOfflineChat:
group.remove_contact(contact)
assert contact.is_blocked()
- lp.sec("ac1 adding blocked contact unblocks it")
- group.add_contact(contact)
- assert not contact.is_blocked()
-
def test_get_set_profile_image_simple(self, ac1, data):
chat = ac1.create_group_chat(name="title1")
p = data.get_path("d.png")
@@ -480,7 +439,8 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
- chat = ac1.create_contact("some1 ").create_chat()
+ ac_contact = acfactory.get_pseudo_configured_account()
+ chat = ac1.create_contact(ac_contact).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -495,10 +455,10 @@ class TestOfflineChat:
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
ac2.import_all(path)
- contacts = ac2.get_contacts(query="some1")
+ contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
- assert contact2.addr == "some1@example.org"
+ assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
@@ -511,8 +471,9 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
+ ac2 = acfactory.get_pseudo_configured_account()
- chat = ac1.create_contact("some1 ").create_chat()
+ chat = ac1.create_contact(ac2).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -533,10 +494,10 @@ class TestOfflineChat:
ac2.import_all(path)
# check data integrity
- contacts = ac2.get_contacts(query="some1")
+ contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
- assert contact2.addr == "some1@example.org"
+ contact2_addr = contact2.addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
@@ -550,10 +511,10 @@ class TestOfflineChat:
ac2.open(passphrase2)
# check data integrity
- contacts = ac2.get_contacts(query="some1")
+ contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
- assert contact2.addr == "some1@example.org"
+ assert contact2.addr == contact2_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
@@ -566,8 +527,9 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
+ ac_contact = acfactory.get_pseudo_configured_account()
- chat = ac1.create_contact("some1 ").create_chat()
+ chat = ac1.create_contact(ac_contact).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -589,10 +551,10 @@ class TestOfflineChat:
ac2.import_all(path, passphrase)
# check data integrity
- contacts = ac2.get_contacts(query="some1")
+ contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
- assert contact2.addr == "some1@example.org"
+ assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
@@ -611,7 +573,8 @@ class TestOfflineChat:
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
- chat = ac1.create_contact("some1 ").create_chat()
+ ac_contact = acfactory.get_pseudo_configured_account()
+ chat = ac1.create_contact(ac_contact).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -634,10 +597,10 @@ class TestOfflineChat:
ac2.import_all(path, bak_passphrase)
# check data integrity
- contacts = ac2.get_contacts(query="some1")
+ contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
- assert contact2.addr == "some1@example.org"
+ assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
@@ -651,10 +614,10 @@ class TestOfflineChat:
ac2.open(acct_passphrase)
# check data integrity
- contacts = ac2.get_contacts(query="some1")
+ contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
- assert contact2.addr == "some1@example.org"
+ assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
@@ -681,78 +644,10 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
- def test_group_chat_many_members_add_remove(self, ac1, lp):
- lp.sec("ac1: creating group chat with 10 other members")
- chat = ac1.create_group_chat(name="title1")
- # promote chat
- chat.send_text("hello")
- assert chat.is_promoted()
+ def test_audit_log_view_without_daymarker(self, acfactory, lp):
+ ac1 = acfactory.get_pseudo_configured_account()
+ ac2 = acfactory.get_pseudo_configured_account()
- # activate local plugin
- in_list = []
-
- class InPlugin:
- @account_hookimpl
- def ac_member_added(self, chat, contact, actor):
- in_list.append(("added", chat, contact, actor))
-
- @account_hookimpl
- def ac_member_removed(self, chat, contact, actor):
- in_list.append(("removed", chat, contact, actor))
-
- ac1.add_account_plugin(InPlugin())
-
- # perform add contact many times
- contacts = []
- for i in range(10):
- lp.sec("create contact")
- contact = ac1.create_contact(f"some{i}@example.org")
- contacts.append(contact)
- lp.sec("add contact")
- chat.add_contact(contact)
-
- assert chat.num_contacts() == 11
-
- # let's make sure the events perform plugin hooks
- def wait_events(cond):
- now = time.time()
- while time.time() < now + 5:
- if cond():
- break
- time.sleep(0.1)
- else:
- pytest.fail("failed to get events")
-
- wait_events(lambda: len(in_list) == 10)
-
- assert len(in_list) == 10
- chat_contacts = chat.get_contacts()
- for in_cmd, in_chat, in_contact, in_actor in in_list:
- assert in_cmd == "added"
- assert in_chat == chat
- assert in_contact in chat_contacts
- assert in_actor is None
- chat_contacts.remove(in_contact)
-
- assert chat_contacts[0].id == 1 # self contact
-
- in_list[:] = []
-
- lp.sec("ac1: removing two contacts and checking things are right")
- chat.remove_contact(contacts[9])
- chat.remove_contact(contacts[3])
- assert chat.num_contacts() == 9
-
- wait_events(lambda: len(in_list) == 2)
- assert len(in_list) == 2
- assert in_list[0][0] == "removed"
- assert in_list[0][1] == chat
- assert in_list[0][2] == contacts[9]
- assert in_list[1][0] == "removed"
- assert in_list[1][1] == chat
- assert in_list[1][2] == contacts[3]
-
- def test_audit_log_view_without_daymarker(self, ac1, lp):
lp.sec("ac1: test audit log (show only system messages)")
chat = ac1.create_group_chat(name="audit log sample data")
@@ -761,7 +656,7 @@ class TestOfflineChat:
assert chat.is_promoted()
lp.sec("create test data")
- chat.add_contact(ac1.create_contact("some-1@example.org"))
+ chat.add_contact(ac2)
chat.set_name("audit log test group")
chat.send_text("a message in between")
diff --git a/src/authres.rs b/src/authres.rs
index 21879b0f4..0ac928434 100644
--- a/src/authres.rs
+++ b/src/authres.rs
@@ -266,7 +266,6 @@ mod tests {
use super::*;
use crate::mimeparser;
- use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
@@ -520,41 +519,6 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
- // Test that Autocrypt works with mailing list.
- //
- // Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
- // This is not needed: comparing of the From address to Autocrypt header address is enough.
- // If the mailing list is not rewriting the From header, Autocrypt should be applied.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
- let mut tcm = TestContextManager::new();
- let alice = tcm.alice().await;
- let bob = tcm.bob().await;
-
- let alice_bob_chat = alice.create_chat(&bob).await;
- let bob_alice_chat = bob.create_chat(&alice).await;
- let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
- sent.payload
- .insert_str(0, "List-Post: \n");
- bob.recv_msg(&sent).await;
- let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
- assert!(peerstate.is_some());
-
- // Bob can now write encrypted to Alice:
- let mut sent = bob
- .send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
- .await;
- assert!(sent.load_from_db().await.get_showpadlock());
-
- sent.payload
- .insert_str(0, "List-Post: \n");
- let rcvd = alice.recv_msg(&sent).await;
- assert!(rcvd.get_showpadlock());
- assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
-
- Ok(())
- }
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
diff --git a/src/chat.rs b/src/chat.rs
index d79a76ec8..d8ed7073c 100644
--- a/src/chat.rs
+++ b/src/chat.rs
@@ -15,17 +15,14 @@ use deltachat_derive::{FromSql, ToSql};
use mail_builder::mime::MimePart;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
-use tokio::task;
-use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
- self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
- DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
- TIMESTAMP_SENT_TOLERANCE,
+ Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
+ DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
};
use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context;
@@ -39,7 +36,6 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
-use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
@@ -130,8 +126,8 @@ pub(crate) enum CantSendReason {
/// Not a member of the chat.
NotAMember,
- /// Temporary state for 1:1 chats while SecureJoin is in progress.
- SecurejoinWait,
+ /// State for 1:1 chat with a key-contact that does not have a key.
+ MissingKey,
}
impl fmt::Display for CantSendReason {
@@ -151,7 +147,7 @@ impl fmt::Display for CantSendReason {
write!(f, "mailing list does not have a know post address")
}
Self::NotAMember => write!(f, "not a member of the chat"),
- Self::SecurejoinWait => write!(f, "awaiting SecureJoin for 1:1 chat"),
+ Self::MissingKey => write!(f, "key is missing"),
}
}
}
@@ -1326,8 +1322,12 @@ impl ChatId {
///
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
pub async fn get_encryption_info(self, context: &Context) -> Result {
- let mut ret_available = String::new();
- let mut ret_reset = String::new();
+ let chat = Chat::load_from_db(context, self).await?;
+ if !chat.is_encrypted(context).await? {
+ return Ok(stock_str::encr_none(context).await);
+ }
+
+ let mut ret = stock_str::e2e_available(context).await + "\n";
for contact_id in get_chat_contacts(context, self)
.await?
@@ -1336,34 +1336,15 @@ impl ChatId {
{
let contact = Contact::get_by_id(context, *contact_id).await?;
let addr = contact.get_addr();
- let peerstate = Peerstate::from_addr(context, addr).await?;
-
- match peerstate
- .filter(|peerstate| peerstate.peek_key(false).is_some())
- .map(|peerstate| peerstate.prefer_encrypt)
- {
- Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => {
- ret_available += &format!("{addr}\n")
- }
- Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
- };
- }
-
- let mut ret = String::new();
- if !ret_reset.is_empty() {
- ret += &stock_str::encr_none(context).await;
- ret.push(':');
- ret.push('\n');
- ret += &ret_reset;
- }
- if !ret_available.is_empty() {
- if !ret.is_empty() {
- ret.push('\n');
+ debug_assert!(contact.is_key_contact());
+ let fingerprint = contact
+ .fingerprint()
+ .context("Contact does not have a fingerprint in encrypted chat")?;
+ if contact.public_key(context).await?.is_some() {
+ ret += &format!("\n{addr}\n{fingerprint}\n");
+ } else {
+ ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n");
}
- ret += &stock_str::e2e_available(context).await;
- ret.push(':');
- ret.push('\n');
- ret += &ret_available;
}
Ok(ret.trim().to_string())
@@ -1473,18 +1454,6 @@ impl ChatId {
Ok(sort_timestamp)
}
-
- /// Spawns a task checking after a timeout whether the SecureJoin has finished for the 1:1 chat
- /// and otherwise notifying the user accordingly.
- pub(crate) fn spawn_securejoin_wait(self, context: &Context, timeout: u64) {
- let context = context.clone();
- task::spawn(async move {
- tokio::time::sleep(Duration::from_secs(timeout)).await;
- let chat = Chat::load_from_db(&context, self).await?;
- chat.check_securejoin_wait(&context, 0).await?;
- Result::<()>::Ok(())
- });
- }
}
impl std::fmt::Display for ChatId {
@@ -1696,15 +1665,18 @@ impl Chat {
if !skip_fn(&reason) && !self.is_self_in_chat(context).await? {
return Ok(Some(reason));
}
- let reason = SecurejoinWait;
- if !skip_fn(&reason)
- && self
- .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
- .await?
- > 0
- {
- return Ok(Some(reason));
+
+ let reason = MissingKey;
+ if !skip_fn(&reason) && self.typ == Chattype::Single {
+ let contact_ids = get_chat_contacts(context, self.id).await?;
+ if let Some(contact_id) = contact_ids.first() {
+ let contact = Contact::get_by_id(context, *contact_id).await?;
+ if contact.is_key_contact() && contact.public_key(context).await?.is_none() {
+ return Ok(Some(reason));
+ }
+ }
}
+
Ok(None)
}
@@ -1715,74 +1687,6 @@ impl Chat {
Ok(self.why_cant_send(context).await?.is_none())
}
- /// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
- ///
- /// If the timeout has expired, adds an info message with additional information.
- /// See also [`CantSendReason::SecurejoinWait`].
- pub(crate) async fn check_securejoin_wait(
- &self,
- context: &Context,
- timeout: u64,
- ) -> Result {
- if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
- return Ok(0);
- }
-
- // chat is single and unprotected:
- // get last info message of type SecurejoinWait or SecurejoinWaitTimeout
- let (mut param_wait, mut param_timeout) = (Params::new(), Params::new());
- param_wait.set_cmd(SystemMessage::SecurejoinWait);
- param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout);
- let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string());
- let Some((param, ts_sort, ts_start)) = context
- .sql
- .query_row_optional(
- "SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
- (SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
- (self.id, ¶m_wait, ¶m_timeout),
- |row| {
- let param: String = row.get(0)?;
- let ts_sort: i64 = row.get(1)?;
- let ts_start: i64 = row.get(2)?;
- Ok((param, ts_sort, ts_start))
- },
- )
- .await?
- else {
- return Ok(0);
- };
- if param == param_timeout {
- return Ok(0);
- }
-
- let now = time();
- // Don't await SecureJoin if the clock was set back.
- if ts_start <= now {
- let timeout = ts_start
- .saturating_add(timeout.try_into()?)
- .saturating_sub(now);
- if timeout > 0 {
- return Ok(timeout as u64);
- }
- }
- add_info_msg_with_cmd(
- context,
- self.id,
- &stock_str::securejoin_takes_longer(context).await,
- SystemMessage::SecurejoinWaitTimeout,
- // Use the sort timestamp of the "please wait" message, this way the added message is
- // never sorted below the protection message if the SecureJoin finishes in parallel.
- ts_sort,
- Some(now),
- None,
- None,
- None,
- )
- .await?;
- context.emit_event(EventType::ChatModified(self.id));
- Ok(0)
- }
-
/// Checks if the user is part of a chat
/// and has basically the permissions to edit the chat therefore.
/// The function does not check if the chat type allows editing of concrete elements.
@@ -1826,25 +1730,36 @@ impl Chat {
/// Returns profile image path for the chat.
pub async fn get_profile_image(&self, context: &Context) -> Result> {
- if let Some(image_rel) = self.param.get(Param::ProfileImage) {
+ if self.id.is_archived_link() {
+ // This is not a real chat, but the "Archive" button
+ // that is shown at the top of the chats list
+ return Ok(Some(get_archive_icon(context).await?));
+ } else if self.is_device_talk() {
+ return Ok(Some(get_device_icon(context).await?));
+ } else if self.is_self_talk() {
+ return Ok(Some(get_saved_messages_icon(context).await?));
+ } else if self.typ == Chattype::Single {
+ // For 1:1 chats, we always use the same avatar as for the contact
+ // This is before the `self.is_encrypted()` check, because that function
+ // has two database calls, i.e. it's slow
+ let contacts = get_chat_contacts(context, self.id).await?;
+ if let Some(contact_id) = contacts.first() {
+ let contact = Contact::get_by_id(context, *contact_id).await?;
+ return contact.get_profile_image(context).await;
+ }
+ } else if !self.is_encrypted(context).await? {
+ // This is an address-contact chat, show a special avatar that marks it as such
+ return Ok(Some(get_abs_path(
+ context,
+ Path::new(&get_address_contact_icon(context).await?),
+ )));
+ } else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
+ // Load the group avatar, or the device-chat / saved-messages icon
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
}
- } else if self.id.is_archived_link() {
- if let Ok(image_rel) = get_archive_icon(context).await {
- return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
- }
- } else if self.typ == Chattype::Single {
- let contacts = get_chat_contacts(context, self.id).await?;
- if let Some(contact_id) = contacts.first() {
- if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
- return contact.get_profile_image(context).await;
- }
- }
} else if self.typ == Chattype::Broadcast {
- if let Ok(image_rel) = get_broadcast_icon(context).await {
- return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
- }
+ return Ok(Some(get_broadcast_icon(context).await?));
}
Ok(None)
}
@@ -1935,6 +1850,33 @@ impl Chat {
self.protected == ProtectionStatus::Protected
}
+ /// Returns true if the chat is encrypted.
+ pub async fn is_encrypted(&self, context: &Context) -> Result {
+ let is_encrypted = self.is_protected()
+ || match self.typ {
+ Chattype::Single => {
+ let chat_contact_ids = get_chat_contacts(context, self.id).await?;
+ if let Some(contact_id) = chat_contact_ids.first() {
+ if *contact_id == ContactId::DEVICE {
+ true
+ } else {
+ let contact = Contact::get_by_id(context, *contact_id).await?;
+ contact.is_key_contact()
+ }
+ } else {
+ true
+ }
+ }
+ Chattype::Group => {
+ // Do not encrypt ad-hoc groups.
+ !self.grpid.is_empty()
+ }
+ Chattype::Mailinglist => false,
+ Chattype::Broadcast => true,
+ };
+ Ok(is_encrypted)
+ }
+
/// Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
@@ -2287,19 +2229,41 @@ impl Chat {
/// Sends a `SyncAction` synchronising chat contacts to other devices.
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
- let addrs = context
- .sql
- .query_map(
- "SELECT c.addr \
- FROM contacts c INNER JOIN chats_contacts cc \
- ON c.id=cc.contact_id \
- WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
- (self.id,),
- |row| row.get::<_, String>(0),
- |addrs| addrs.collect::, _>>().map_err(Into::into),
- )
- .await?;
- self.sync(context, SyncAction::SetContacts(addrs)).await
+ if self.is_encrypted(context).await? {
+ let fingerprint_addrs = context
+ .sql
+ .query_map(
+ "SELECT c.fingerprint, c.addr
+ FROM contacts c INNER JOIN chats_contacts cc
+ ON c.id=cc.contact_id
+ WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
+ (self.id,),
+ |row| {
+ let fingerprint = row.get(0)?;
+ let addr = row.get(1)?;
+ Ok((fingerprint, addr))
+ },
+ |addrs| addrs.collect::, _>>().map_err(Into::into),
+ )
+ .await?;
+ self.sync(context, SyncAction::SetPgpContacts(fingerprint_addrs))
+ .await?;
+ } else {
+ let addrs = context
+ .sql
+ .query_map(
+ "SELECT c.addr \
+ FROM contacts c INNER JOIN chats_contacts cc \
+ ON c.id=cc.contact_id \
+ WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
+ (self.id,),
+ |row| row.get::<_, String>(0),
+ |addrs| addrs.collect::, _>>().map_err(Into::into),
+ )
+ .await?;
+ self.sync(context, SyncAction::SetContacts(addrs)).await?;
+ }
+ Ok(())
}
/// Returns chat id for the purpose of synchronisation across devices.
@@ -2319,7 +2283,11 @@ impl Chat {
return Ok(None);
}
let contact = Contact::get_by_id(context, contact_id).await?;
- r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
+ if let Some(fingerprint) = contact.fingerprint() {
+ r = Some(SyncId::ContactFingerprint(fingerprint.hex()));
+ } else {
+ r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
+ }
}
Ok(r)
}
@@ -2465,69 +2433,63 @@ pub struct ChatInfo {
// - [ ] email
}
-pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> {
- if let Some(ChatIdBlocked { id: chat_id, .. }) =
- ChatIdBlocked::lookup_by_contact(context, ContactId::SELF).await?
- {
- let icon = include_bytes!("../assets/icon-saved-messages.png");
- let blob =
- BlobObject::create_and_deduplicate_from_bytes(context, icon, "saved-messages.png")?;
- let icon = blob.as_name().to_string();
-
- let mut chat = Chat::load_from_db(context, chat_id).await?;
- chat.param.set(Param::ProfileImage, icon);
- chat.update_param(context).await?;
- }
- Ok(())
-}
-
-pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
- if let Some(ChatIdBlocked { id: chat_id, .. }) =
- ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE).await?
- {
- let icon = include_bytes!("../assets/icon-device.png");
- let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "device.png")?;
- let icon = blob.as_name().to_string();
-
- let mut chat = Chat::load_from_db(context, chat_id).await?;
- chat.param.set(Param::ProfileImage, &icon);
- chat.update_param(context).await?;
-
- let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?;
- contact.param.set(Param::ProfileImage, icon);
- contact.update_param(context).await?;
- }
- Ok(())
-}
-
-pub(crate) async fn get_broadcast_icon(context: &Context) -> Result {
- if let Some(icon) = context.sql.get_raw_config("icon-broadcast").await? {
- return Ok(icon);
+async fn get_asset_icon(context: &Context, name: &str, bytes: &[u8]) -> Result {
+ ensure!(name.starts_with("icon-"));
+ if let Some(icon) = context.sql.get_raw_config(name).await? {
+ return Ok(get_abs_path(context, Path::new(&icon)));
}
- let icon = include_bytes!("../assets/icon-broadcast.png");
- let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "broadcast.png")?;
+ let blob =
+ BlobObject::create_and_deduplicate_from_bytes(context, bytes, &format!("{name}.png"))?;
let icon = blob.as_name().to_string();
- context
- .sql
- .set_raw_config("icon-broadcast", Some(&icon))
- .await?;
- Ok(icon)
+ context.sql.set_raw_config(name, Some(&icon)).await?;
+
+ Ok(get_abs_path(context, Path::new(&icon)))
}
-pub(crate) async fn get_archive_icon(context: &Context) -> Result {
- if let Some(icon) = context.sql.get_raw_config("icon-archive").await? {
- return Ok(icon);
- }
+pub(crate) async fn get_saved_messages_icon(context: &Context) -> Result {
+ get_asset_icon(
+ context,
+ "icon-saved-messages",
+ include_bytes!("../assets/icon-saved-messages.png"),
+ )
+ .await
+}
- let icon = include_bytes!("../assets/icon-archive.png");
- let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "archive.png")?;
- let icon = blob.as_name().to_string();
- context
- .sql
- .set_raw_config("icon-archive", Some(&icon))
- .await?;
- Ok(icon)
+pub(crate) async fn get_device_icon(context: &Context) -> Result {
+ get_asset_icon(
+ context,
+ "icon-device",
+ include_bytes!("../assets/icon-device.png"),
+ )
+ .await
+}
+
+pub(crate) async fn get_broadcast_icon(context: &Context) -> Result {
+ get_asset_icon(
+ context,
+ "icon-broadcast",
+ include_bytes!("../assets/icon-broadcast.png"),
+ )
+ .await
+}
+
+pub(crate) async fn get_archive_icon(context: &Context) -> Result {
+ get_asset_icon(
+ context,
+ "icon-archive",
+ include_bytes!("../assets/icon-archive.png"),
+ )
+ .await
+}
+
+pub(crate) async fn get_address_contact_icon(context: &Context) -> Result {
+ get_asset_icon(
+ context,
+ "icon-address-contact",
+ include_bytes!("../assets/icon-address-contact.png"),
+ )
+ .await
}
async fn update_special_chat_name(
@@ -2566,34 +2528,6 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
Ok(())
}
-/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
-/// unblocking the chat and notifying the user accordingly.
-pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
- let chat_ids: Vec = context
- .sql
- .query_map(
- "SELECT chat_id FROM bobstate",
- (),
- |row| {
- let chat_id: ChatId = row.get(0)?;
- Ok(chat_id)
- },
- |rows| rows.collect::, _>>().map_err(Into::into),
- )
- .await?;
-
- for chat_id in chat_ids {
- let chat = Chat::load_from_db(context, chat_id).await?;
- let timeout = chat
- .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
- .await?;
- if timeout > 0 {
- chat_id.spawn_securejoin_wait(context, timeout);
- }
- }
- Ok(())
-}
-
/// Handle a [`ChatId`] and its [`Blocked`] status at once.
///
/// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once
@@ -2677,12 +2611,7 @@ impl ChatIdBlocked {
_ => (),
}
- let protected = contact_id == ContactId::SELF || {
- let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
- peerstate.is_some_and(|p| {
- p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
- })
- };
+ let protected = contact_id == ContactId::SELF || contact.is_verified(context).await?;
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
@@ -2734,12 +2663,6 @@ impl ChatIdBlocked {
.await?;
}
- match contact_id {
- ContactId::SELF => update_saved_messages_icon(context).await?,
- ContactId::DEVICE => update_device_icon(context).await?,
- _ => (),
- }
-
Ok(Self {
id: chat_id,
blocked: create_blocked,
@@ -2919,9 +2842,7 @@ async fn prepare_send_msg(
let mut chat = Chat::load_from_db(context, chat_id).await?;
let skip_fn = |reason: &CantSendReason| match reason {
- CantSendReason::ProtectionBroken
- | CantSendReason::ContactRequest
- | CantSendReason::SecurejoinWait => {
+ CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
// Allow securejoin messages, they are supposed to repair the verification.
// If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
@@ -2930,6 +2851,10 @@ async fn prepare_send_msg(
// Necessary checks should be made anyway before removing contact
// from the chat.
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
+ CantSendReason::MissingKey => msg
+ .param
+ .get_bool(Param::ForcePlaintext)
+ .unwrap_or_default(),
_ => false,
};
if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
@@ -3898,6 +3823,10 @@ pub(crate) async fn add_contact_to_chat_ex(
chat.typ != Chattype::Broadcast || contact_id != ContactId::SELF,
"Cannot add SELF to broadcast."
);
+ ensure!(
+ chat.is_encrypted(context).await? == contact.is_key_contact(),
+ "Only key-contacts can be added to encrypted chats"
+ );
if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
@@ -3949,7 +3878,7 @@ pub(crate) async fn add_contact_to_chat_ex(
msg.viewtype = Viewtype::Text;
let contact_addr = contact.get_addr().to_lowercase();
- msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
+ msg.text = stock_str::msg_add_member_local(context, contact.id, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
@@ -4143,12 +4072,9 @@ pub async fn remove_contact_from_chat(
if contact_id == ContactId::SELF {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
- msg.text = stock_str::msg_del_member_local(
- context,
- contact.get_addr(),
- ContactId::SELF,
- )
- .await;
+ msg.text =
+ stock_str::msg_del_member_local(context, contact_id, ContactId::SELF)
+ .await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
@@ -4283,8 +4209,12 @@ pub async fn set_chat_profile_image(
ensure!(!chat_id.is_special(), "Invalid chat ID");
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(
- chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist,
- "Failed to set profile image; group does not exist"
+ chat.typ == Chattype::Group,
+ "Can only set profile image for group chats"
+ );
+ ensure!(
+ !chat.grpid.is_empty(),
+ "Cannot set profile image for ad hoc groups"
);
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
@@ -4849,6 +4779,10 @@ pub(crate) async fn update_msg_text_and_timestamp(
/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
let chat = Chat::load_from_db(context, id).await?;
+ ensure!(
+ !chat.is_encrypted(context).await?,
+ "Cannot add address-contacts to encrypted chat {id}"
+ );
ensure!(
chat.typ == Chattype::Broadcast,
"{id} is not a broadcast list",
@@ -4884,10 +4818,64 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
Ok(())
}
+/// Set chat contacts by their fingerprints creating the corresponding contacts if necessary.
+///
+/// `fingerprint_addrs` is a list of pairs of fingerprint and address.
+async fn set_contacts_by_fingerprints(
+ context: &Context,
+ id: ChatId,
+ fingerprint_addrs: &[(String, String)],
+) -> Result<()> {
+ let chat = Chat::load_from_db(context, id).await?;
+ ensure!(
+ chat.is_encrypted(context).await?,
+ "Cannot add key-contacts to unencrypted chat {id}"
+ );
+ ensure!(
+ chat.typ == Chattype::Broadcast,
+ "{id} is not a broadcast list",
+ );
+ let mut contacts = HashSet::new();
+ for (fingerprint, addr) in fingerprint_addrs {
+ let contact_addr = ContactAddress::new(addr)?;
+ let contact =
+ Contact::add_or_lookup_ex(context, "", &contact_addr, fingerprint, Origin::Hidden)
+ .await?
+ .0;
+ contacts.insert(contact);
+ }
+ let contacts_old = HashSet::::from_iter(get_chat_contacts(context, id).await?);
+ if contacts == contacts_old {
+ return Ok(());
+ }
+ context
+ .sql
+ .transaction(move |transaction| {
+ transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
+
+ // We do not care about `add_timestamp` column
+ // because timestamps are not used for broadcast lists.
+ let mut statement = transaction
+ .prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
+ for contact_id in &contacts {
+ statement.execute((id, contact_id))?;
+ }
+ Ok(())
+ })
+ .await?;
+ context.emit_event(EventType::ChatModified(id));
+ Ok(())
+}
+
/// A cross-device chat id used for synchronisation.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum SyncId {
+ /// E-mail address of the contact.
ContactAddr(String),
+
+ /// OpenPGP key fingerprint of the contact.
+ ContactFingerprint(String),
+
Grpid(String),
/// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
Msgids(Vec),
@@ -4909,6 +4897,10 @@ pub(crate) enum SyncAction {
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec),
+ /// Set chat contacts by their fingerprints.
+ ///
+ /// The list is a list of pairs of fingerprint and address.
+ SetPgpContacts(Vec<(String, String)>),
Delete,
}
@@ -4939,6 +4931,30 @@ impl Context {
.await?
.id
}
+ SyncId::ContactFingerprint(fingerprint) => {
+ let name = "";
+ let addr = "";
+ let (contact_id, _) =
+ Contact::add_or_lookup_ex(self, name, addr, fingerprint, Origin::Hidden)
+ .await?;
+ match action {
+ SyncAction::Rename(to) => {
+ contact_id.set_name_ex(self, Nosync, to).await?;
+ self.emit_event(EventType::ContactsChanged(Some(contact_id)));
+ return Ok(());
+ }
+ SyncAction::Block => {
+ return contact::set_blocked(self, Nosync, contact_id, true).await
+ }
+ SyncAction::Unblock => {
+ return contact::set_blocked(self, Nosync, contact_id, false).await
+ }
+ _ => (),
+ }
+ ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
+ .await?
+ .id
+ }
SyncId::Grpid(grpid) => {
if let SyncAction::CreateBroadcast(name) = action {
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
@@ -4969,6 +4985,9 @@ impl Context {
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
+ SyncAction::SetPgpContacts(fingerprint_addrs) => {
+ set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await
+ }
SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
}
}
diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs
index 908ac29af..6cf5c0368 100644
--- a/src/chat/chat_tests.rs
+++ b/src/chat/chat_tests.rs
@@ -7,6 +7,7 @@ use crate::imex::{has_backup, imex, ImexMode};
use crate::message::{delete_msgs, MessengerMessage};
use crate::receive_imf::receive_imf;
use crate::test_utils::{sync, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
+use pretty_assertions::assert_eq;
use strum::IntoEnumIterator;
use tokio::fs;
@@ -19,31 +20,39 @@ async fn test_chat_info() {
// Ensure we can serialize this.
println!("{}", serde_json::to_string_pretty(&info).unwrap());
- let expected = r#"
- {
- "id": 10,
- "type": 100,
- "name": "bob",
- "archived": false,
- "param": "",
- "gossiped_timestamp": 0,
- "is_sending_locations": false,
- "color": 35391,
- "profile_image": "",
- "draft": "",
- "is_muted": false,
- "ephemeral_timer": "Disabled"
- }
- "#;
+ let expected = format!(
+ r#"{{
+ "id": 10,
+ "type": 100,
+ "name": "bob",
+ "archived": false,
+ "param": "",
+ "is_sending_locations": false,
+ "color": 35391,
+ "profile_image": {},
+ "draft": "",
+ "is_muted": false,
+ "ephemeral_timer": "Disabled"
+}}"#,
+ // We need to do it like this so that the test passes on Windows:
+ serde_json::to_string(
+ t.get_blobdir()
+ .join("4138c52e5bc1c576cda7dd44d088c07.png")
+ .to_str()
+ .unwrap()
+ )
+ .unwrap()
+ );
// Ensure we can deserialize this.
- let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
- assert_eq!(info, loaded);
+ serde_json::from_str::(&expected).unwrap();
+
+ assert_eq!(serde_json::to_string_pretty(&info).unwrap(), expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_draft_no_draft() {
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
let chat = t.get_self_chat().await;
let draft = chat.id.get_draft(&t).await.unwrap();
assert!(draft.is_none());
@@ -60,14 +69,14 @@ async fn test_get_draft_special_chat_id() {
async fn test_get_draft_no_chat() {
// This is a weird case, maybe this should be an error but we
// do not get this info from the database currently.
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
let draft = ChatId::new(42).get_draft(&t).await.unwrap();
assert!(draft.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_draft() {
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new_text("hello".to_string());
@@ -320,18 +329,20 @@ async fn test_member_add_remove() -> Result<()> {
assert_eq!(alice_bob_contact.get_display_name(), "robert");
}
- // Create and promote a group.
+ tcm.section("Create and promote a group.");
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
- alice
+ let sent = alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
+ let fiona_chat_id = fiona.recv_msg(&sent).await.chat_id;
- // Alice adds Bob to the chat.
+ tcm.section("Alice adds Bob to the chat.");
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
+ fiona.recv_msg(&sent).await;
// Locally set name "robert" should not leak.
assert!(!sent.payload.contains("robert"));
@@ -339,8 +350,15 @@ async fn test_member_add_remove() -> Result<()> {
sent.load_from_db().await.get_text(),
"You added member robert."
);
+ let fiona_contact_ids = get_chat_contacts(&fiona, fiona_chat_id).await?;
+ assert_eq!(fiona_contact_ids.len(), 3);
+ for contact_id in fiona_contact_ids {
+ let contact = Contact::get_by_id(&fiona, contact_id).await?;
+ assert_ne!(contact.get_name(), "robert");
+ assert!(contact.is_key_contact());
+ }
- // Alice removes Bob from the chat.
+ tcm.section("Alice removes Bob from the chat.");
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
assert!(!sent.payload.contains("robert"));
@@ -371,7 +389,7 @@ async fn test_parallel_member_remove() -> Result<()> {
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
- // Create and promote a group.
+ tcm.section("Alice creates and promotes a group");
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
@@ -384,31 +402,31 @@ async fn test_parallel_member_remove() -> Result<()> {
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(&bob).await?;
- // Alice adds Charlie to the chat.
+ tcm.section("Alice adds Charlie to the chat");
add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?;
let alice_sent_add_msg = alice.pop_sent_msg().await;
- // Bob leaves the chat.
+ tcm.section("Bob leaves the chat");
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
bob.pop_sent_msg().await;
- // Bob receives a msg about Alice adding Claire to the group.
+ tcm.section("Bob receives a message about Alice adding Charlie to the group");
bob.recv_msg(&alice_sent_add_msg).await;
SystemTime::shift(Duration::from_secs(3600));
- // Alice sends a message to Bob because the message about leaving is lost.
+ tcm.section("Alice sends a message to Bob because the message about leaving is lost");
let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await;
bob.recv_msg(&alice_sent_msg).await;
bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove")
.await;
- // Alice removes Bob from the chat.
+ tcm.section("Alice removes Bob from the chat");
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let alice_sent_remove_msg = alice.pop_sent_msg().await;
- // Bob receives a msg about Alice removing him from the group.
+ tcm.section("Bob receives a msg about Alice removing him from the group");
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
// Test that remove message is rewritten.
@@ -621,6 +639,7 @@ async fn test_lost_member_added() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
+ let charlie = &tcm.charlie().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
.await;
@@ -629,8 +648,8 @@ async fn test_lost_member_added() -> Result<()> {
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
// Attempt to add member, but message is lost.
- let claire_id = Contact::create(alice, "", "claire@foo.de").await?;
- add_contact_to_chat(alice, alice_chat_id, claire_id).await?;
+ let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
+ add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
alice.pop_sent_msg().await;
let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await;
@@ -693,12 +712,12 @@ async fn test_leave_group() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
- // Create group chat with Bob.
+ tcm.section("Alice creates group chat with Bob.");
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
- // Alice sends first message to group.
+ tcm.section("Alice sends first message to group.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
let bob_msg = bob.recv_msg(&sent_msg).await;
@@ -711,7 +730,7 @@ async fn test_leave_group() -> Result<()> {
// Shift the time so that we can later check the 'Group left' message's timestamp:
SystemTime::shift(Duration::from_secs(60));
- // Bob leaves the group.
+ tcm.section("Bob leaves the group.");
let bob_chat_id = bob_msg.chat_id;
bob_chat_id.accept(&bob).await?;
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
@@ -897,7 +916,11 @@ async fn test_add_device_msg_labelled() -> Result<()> {
assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat));
assert_eq!(chat.name, stock_str::device_messages(&t).await);
- assert!(chat.get_profile_image(&t).await?.is_some());
+ let device_msg_icon = chat.get_profile_image(&t).await?.unwrap();
+ assert_eq!(
+ device_msg_icon.metadata()?.len(),
+ include_bytes!("../../assets/icon-device.png").len() as u64
+ );
// delete device message, make sure it is not added again
message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()]).await?;
@@ -968,7 +991,7 @@ async fn test_delete_device_chat() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_device_chat_cannot_sent() {
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
t.update_device_chats().await.unwrap();
let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
.await
@@ -981,6 +1004,18 @@ async fn test_device_chat_cannot_sent() {
assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err());
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_device_chat_is_encrypted() {
+ let t = TestContext::new_alice().await;
+ t.update_device_chats().await.unwrap();
+ let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
+ .await
+ .unwrap();
+
+ let device_chat = Chat::load_from_db(&t, device_chat_id).await.unwrap();
+ assert!(device_chat.is_encrypted(&t).await.unwrap());
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_and_reset_all_device_msgs() {
let t = TestContext::new().await;
@@ -1015,7 +1050,7 @@ async fn chatlist_len(ctx: &Context, listflags: usize) -> usize {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_archive() {
// create two chats
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
let mut msg = Message::new_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
@@ -1313,7 +1348,7 @@ async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec Result<()> {
- let t = TestContext::new().await;
- let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
- assert!(!shall_attach_selfavatar(&t, chat_id).await?);
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
- let (contact_id, _) = Contact::add_or_lookup(
- &t,
- "",
- &ContactAddress::new("foo@bar.org")?,
- Origin::IncomingUnknownTo,
- )
- .await?;
- add_contact_to_chat(&t, chat_id, contact_id).await?;
- assert!(shall_attach_selfavatar(&t, chat_id).await?);
+ let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
+ assert!(!shall_attach_selfavatar(alice, chat_id).await?);
- chat_id.set_selfavatar_timestamp(&t, time()).await?;
- assert!(!shall_attach_selfavatar(&t, chat_id).await?);
+ let contact_id = alice.add_or_lookup_contact_id(bob).await;
+ add_contact_to_chat(alice, chat_id, contact_id).await?;
+ assert!(shall_attach_selfavatar(alice, chat_id).await?);
- t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
- assert!(shall_attach_selfavatar(&t, chat_id).await?);
+ chat_id.set_selfavatar_timestamp(alice, time()).await?;
+ assert!(!shall_attach_selfavatar(alice, chat_id).await?);
+
+ alice.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
+ assert!(shall_attach_selfavatar(alice, chat_id).await?);
Ok(())
}
@@ -1509,15 +1541,10 @@ async fn test_shall_attach_selfavatar() -> Result<()> {
async fn test_profile_data_on_group_leave() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
+ let bob = &tcm.bob().await;
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").await?;
- let (contact_id, _) = Contact::add_or_lookup(
- t,
- "",
- &ContactAddress::new("foo@bar.org")?,
- Origin::IncomingUnknownTo,
- )
- .await?;
+ let contact_id = t.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(t, chat_id, contact_id).await?;
send_text_msg(t, chat_id, "populate".to_string()).await?;
@@ -1532,7 +1559,8 @@ async fn test_profile_data_on_group_leave() -> Result<()> {
remove_contact_from_chat(t, chat_id, ContactId::SELF).await?;
let sent_msg = t.pop_sent_msg().await;
- assert!(sent_msg.payload().contains("Chat-User-Avatar"));
+ let msg = bob.parse_msg(&sent_msg).await;
+ assert!(msg.header_exists(HeaderDef::ChatUserAvatar));
Ok(())
}
@@ -2183,40 +2211,46 @@ async fn test_forward_group() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_only_minimal_data_are_forwarded() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
+ let charlie = tcm.charlie().await;
+
// send a message from Alice to a group with Bob
- let alice = TestContext::new_alice().await;
alice
.set_config(Config::Displayname, Some("secretname"))
.await?;
- let bob_id = Contact::create(&alice, "bob", "bob@example.net").await?;
+ let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let group_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
add_contact_to_chat(&alice, group_id, bob_id).await?;
let mut msg = Message::new_text("bla foo".to_owned());
let sent_msg = alice.send_msg(group_id, &mut msg).await;
- assert!(sent_msg.payload().contains("secretgrpname"));
- assert!(sent_msg.payload().contains("secretname"));
- assert!(sent_msg.payload().contains("alice"));
+ let parsed_msg = alice.parse_msg(&sent_msg).await;
+ let encrypted_payload = String::from_utf8(parsed_msg.decoded_data.clone()).unwrap();
+ assert!(encrypted_payload.contains("secretgrpname"));
+ assert!(encrypted_payload.contains("secretname"));
+ assert!(encrypted_payload.contains("alice"));
// Bob forwards that message to Claire -
// Claire should not get information about Alice for the original Group
- let bob = TestContext::new_bob().await;
let orig_msg = bob.recv_msg(&sent_msg).await;
- let claire_id = Contact::create(&bob, "claire", "claire@foo").await?;
- let single_id = ChatId::create_for_contact(&bob, claire_id).await?;
+ let charlie_id = bob.add_or_lookup_contact_id(&charlie).await;
+ let single_id = ChatId::create_for_contact(&bob, charlie_id).await?;
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
- add_contact_to_chat(&bob, group_id, claire_id).await?;
+ add_contact_to_chat(&bob, group_id, charlie_id).await?;
let broadcast_id = create_broadcast_list(&bob).await?;
- add_contact_to_chat(&bob, broadcast_id, claire_id).await?;
+ add_contact_to_chat(&bob, broadcast_id, charlie_id).await?;
for chat_id in &[single_id, group_id, broadcast_id] {
forward_msgs(&bob, &[orig_msg.id], *chat_id).await?;
let sent_msg = bob.pop_sent_msg().await;
- assert!(sent_msg
- .payload()
- .contains("---------- Forwarded message ----------"));
- assert!(!sent_msg.payload().contains("secretgrpname"));
- assert!(!sent_msg.payload().contains("secretname"));
- assert!(!sent_msg.payload().contains("alice"));
+ let parsed_msg = bob.parse_msg(&sent_msg).await;
+ let encrypted_payload = String::from_utf8(parsed_msg.decoded_data.clone()).unwrap();
+
+ assert!(encrypted_payload.contains("---------- Forwarded message ----------"));
+ assert!(!encrypted_payload.contains("secretgrpname"));
+ assert!(!encrypted_payload.contains("secretname"));
+ assert!(!encrypted_payload.contains("alice"));
}
Ok(())
@@ -2500,43 +2534,6 @@ async fn test_resend_foreign_message_fails() -> Result<()> {
Ok(())
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_resend_opportunistically_encryption() -> Result<()> {
- // Alice creates group with Bob and sends an initial message
- let alice = TestContext::new_alice().await;
- let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
- add_contact_to_chat(
- &alice,
- alice_grp,
- Contact::create(&alice, "", "bob@example.net").await?,
- )
- .await?;
- let sent1 = alice.send_text(alice_grp, "alice->bob").await;
-
- // Bob now can send an encrypted message
- let bob = TestContext::new_bob().await;
- let msg = bob.recv_msg(&sent1).await;
- assert!(!msg.get_showpadlock());
-
- msg.chat_id.accept(&bob).await?;
- let sent2 = bob.send_text(msg.chat_id, "bob->alice").await;
- let msg = bob.get_last_msg().await;
- assert!(msg.get_showpadlock());
-
- // Bob adds Claire and resends his last message: this will drop encryption in opportunistic chats
- add_contact_to_chat(
- &bob,
- msg.chat_id,
- Contact::create(&bob, "", "claire@example.org").await?,
- )
- .await?;
- let _sent3 = bob.pop_sent_msg().await;
- resend_msgs(&bob, &[sent2.sender_msg_id]).await?;
- let _sent4 = bob.pop_sent_msg().await;
-
- Ok(())
-}
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_info_message_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -2737,53 +2734,64 @@ async fn test_create_for_contact_with_blocked() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_get_encryption_info() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let fiona = &tcm.fiona().await;
- let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?;
- let contact_fiona = Contact::create(&alice, "", "fiona@example.net").await?;
+ let contact_bob = alice.add_or_lookup_contact_id(bob).await;
+ let contact_fiona = alice.add_or_lookup_contact_id(fiona).await;
- let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
- assert_eq!(chat_id.get_encryption_info(&alice).await?, "");
-
- add_contact_to_chat(&alice, chat_id, contact_bob).await?;
+ let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
assert_eq!(
- chat_id.get_encryption_info(&alice).await?,
- "No encryption:\n\
- bob@example.net"
+ chat_id.get_encryption_info(alice).await?,
+ "End-to-end encryption available"
);
- add_contact_to_chat(&alice, chat_id, contact_fiona).await?;
+ add_contact_to_chat(alice, chat_id, contact_bob).await?;
assert_eq!(
- chat_id.get_encryption_info(&alice).await?,
- "No encryption:\n\
- fiona@example.net\n\
- bob@example.net"
+ chat_id.get_encryption_info(alice).await?,
+ "End-to-end encryption available\n\
+ \n\
+ bob@example.net\n\
+ CCCB 5AA9 F6E1 141C 9431\n\
+ 65F1 DB18 B18C BCF7 0487"
);
- let direct_chat = bob.create_chat(&alice).await;
- send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
- alice.recv_msg(&bob.pop_sent_msg().await).await;
-
+ add_contact_to_chat(alice, chat_id, contact_fiona).await?;
assert_eq!(
- chat_id.get_encryption_info(&alice).await?,
- "No encryption:\n\
- fiona@example.net\n\
- \n\
- End-to-end encryption available:\n\
- bob@example.net"
+ chat_id.get_encryption_info(alice).await?,
+ "End-to-end encryption available\n\
+ \n\
+ fiona@example.net\n\
+ C8BA 50BF 4AC1 2FAF 38D7\n\
+ F657 DDFC 8E9F 3C79 9195\n\
+ \n\
+ bob@example.net\n\
+ CCCB 5AA9 F6E1 141C 9431\n\
+ 65F1 DB18 B18C BCF7 0487"
);
- send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
- alice.recv_msg(&bob.pop_sent_msg().await).await;
-
+ let email_chat = alice.create_email_chat(bob).await;
assert_eq!(
- chat_id.get_encryption_info(&alice).await?,
- "No encryption:\n\
- fiona@example.net\n\
- \n\
- End-to-end encryption available:\n\
- bob@example.net"
+ email_chat.id.get_encryption_info(alice).await?,
+ "No encryption"
+ );
+
+ alice.sql.execute("DELETE FROM public_keys", ()).await?;
+ assert_eq!(
+ chat_id.get_encryption_info(alice).await?,
+ "End-to-end encryption available\n\
+ \n\
+ fiona@example.net\n\
+ (key missing)\n\
+ C8BA 50BF 4AC1 2FAF 38D7\n\
+ F657 DDFC 8E9F 3C79 9195\n\
+ \n\
+ bob@example.net\n\
+ (key missing)\n\
+ CCCB 5AA9 F6E1 141C 9431\n\
+ 65F1 DB18 B18C BCF7 0487"
);
Ok(())
@@ -3039,40 +3047,41 @@ async fn test_blob_renaming() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_blocked() -> Result<()> {
- let alice0 = &TestContext::new_alice().await;
- let alice1 = &TestContext::new_alice().await;
+ 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 bob = TestContext::new_bob().await;
+ let bob = &tcm.bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
alice1.recv_msg(&sent_msg).await;
- let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await;
+ let a0b_contact_id = alice0.add_or_lookup_contact_id(bob).await;
- assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
+ assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
sync(alice0, alice1).await;
- assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not);
+ assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Not);
a0b_chat_id.block(alice0).await?;
sync(alice0, alice1).await;
- assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Yes);
+ assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Yes);
a0b_chat_id.unblock(alice0).await?;
sync(alice0, alice1).await;
- assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not);
+ assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Not);
// Unblocking a 1:1 chat doesn't unblock the contact currently.
Contact::unblock(alice0, a0b_contact_id).await?;
- assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked());
+ assert!(!alice1.add_or_lookup_contact(bob).await.is_blocked());
Contact::block(alice0, a0b_contact_id).await?;
sync(alice0, alice1).await;
- assert!(alice1.add_or_lookup_contact(&bob).await.is_blocked());
+ assert!(alice1.add_or_lookup_contact(bob).await.is_blocked());
Contact::unblock(alice0, a0b_contact_id).await?;
sync(alice0, alice1).await;
- assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked());
+ assert!(!alice1.add_or_lookup_contact(bob).await.is_blocked());
// Test accepting and blocking groups. This way we test:
// - Group chats synchronisation.
@@ -3132,7 +3141,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
assert_eq!(alice1_contacts.len(), 1);
let a1b_contact_id = alice1_contacts[0];
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
- assert_eq!(a1b_contact.get_addr(), "bob@example.net");
+ assert_eq!(a1b_contact.get_addr(), "");
assert_eq!(a1b_contact.origin, Origin::CreateChat);
let a1b_chat = alice1.get_chat(bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Not);
@@ -3146,23 +3155,30 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_block_before_first_msg() -> Result<()> {
- let alice0 = &TestContext::new_alice().await;
- let alice1 = &TestContext::new_alice().await;
+ 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 bob = TestContext::new_bob().await;
+ let bob = &tcm.bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let rcvd_msg = alice0.recv_msg(&sent_msg).await;
let a0b_chat_id = rcvd_msg.chat_id;
let a0b_contact_id = rcvd_msg.from_id;
- assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
+ assert_eq!(
+ Chat::load_from_db(alice0, a0b_chat_id).await?.blocked,
+ Blocked::Request
+ );
a0b_chat_id.block(alice0).await?;
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
- assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
+ assert_eq!(
+ Chat::load_from_db(alice0, a0b_chat_id).await?.blocked,
+ Blocked::Yes
+ );
sync(alice0, alice1).await;
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
@@ -3172,9 +3188,14 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
let a1b_contact_id = rcvd_msg.from_id;
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
- let a1b_chat = alice1.get_chat(&bob).await;
- assert_eq!(a1b_chat.blocked, Blocked::Yes);
- assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
+ let ChatIdBlocked {
+ id: a1b_chat_id,
+ blocked: a1b_chat_blocked,
+ } = ChatIdBlocked::lookup_by_contact(alice1, a1b_contact_id)
+ .await?
+ .unwrap();
+ assert_eq!(a1b_chat_blocked, Blocked::Yes);
+ assert_eq!(rcvd_msg.chat_id, a1b_chat_id);
Ok(())
}
@@ -3366,8 +3387,8 @@ async fn test_sync_broadcast() -> Result<()> {
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
- let bob = TestContext::new_bob().await;
- let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
+ let bob = &tcm.bob().await;
+ let a0b_contact_id = alice0.add_or_lookup_contact(bob).await.id;
let a0_broadcast_id = create_broadcast_list(alice0).await?;
sync(alice0, alice1).await;
@@ -3382,20 +3403,19 @@ async fn test_sync_broadcast() -> Result<()> {
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
sync(alice0, alice1).await;
- let a1b_contact_id = Contact::lookup_id_by_addr(
- alice1,
- &bob.get_config(Config::Addr).await?.unwrap(),
- Origin::Hidden,
- )
- .await?
- .unwrap();
+
+ // This also imports Bob's key from the vCard.
+ // Otherwise it is possible that second device
+ // does not have Bob's key as only the fingerprint
+ // is transferred in the sync message.
+ let a1b_contact_id = alice1.add_or_lookup_contact(bob).await.id;
assert_eq!(
get_chat_contacts(alice1, a1_broadcast_id).await?,
vec![a1b_contact_id]
);
let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await;
let msg = bob.recv_msg(&sent_msg).await;
- let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
+ let chat = Chat::load_from_db(bob, msg.chat_id).await?;
assert_eq!(chat.get_type(), Chattype::Mailinglist);
let msg = alice0.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, a0_broadcast_id);
@@ -3514,7 +3534,7 @@ async fn test_info_contact_id() -> Result<()> {
expected_bob_id: ContactId,
) -> Result<()> {
let sent_msg = alice.pop_sent_msg().await;
- let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
+ let msg = sent_msg.load_from_db().await;
assert_eq!(msg.get_info_type(), expected_type);
assert_eq!(
msg.get_info_contact_id(alice).await?,
@@ -3677,19 +3697,24 @@ async fn test_past_members() -> Result<()> {
let fiona = &tcm.fiona().await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
+ tcm.section("Alice creates a chat.");
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
+
+ tcm.section("Alice removes Fiona from the chat.");
remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
+ tcm.section("Alice adds Bob to the chat.");
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
+ tcm.section("Bob receives a message.");
let add_message = alice.pop_sent_msg().await;
let bob_add_message = bob.recv_msg(&add_message).await;
let bob_chat_id = bob_add_message.chat_id;
@@ -4212,5 +4237,108 @@ async fn test_oneone_gossip() -> Result<()> {
assert_eq!(rcvd_msg2.get_showpadlock(), true);
assert_eq!(rcvd_msg2.text, "Hello from second device!");
+ tcm.section("Alice sends another message from the first devicer");
+ let sent_msg3 = alice.send_text(alice_chat.id, "Hello again, Bob!").await;
+
+ // This message has no Autocrypt-Gossip header,
+ // but should still be assigned to key-contact.
+ tcm.section("Alice receives a copy of another message on second device");
+ let rcvd_msg3 = alice2.recv_msg(&sent_msg3).await;
+ assert_eq!(rcvd_msg3.get_showpadlock(), true);
+ assert_eq!(rcvd_msg3.chat_id, rcvd_msg.chat_id);
+
+ // Check that there was no gossip.
+ let parsed_msg3 = alice2.parse_msg(&sent_msg3).await;
+ assert!(!parsed_msg3.header_exists(HeaderDef::AutocryptGossip));
+
+ Ok(())
+}
+
+/// Tests that address-contacts cannot be added to encrypted group chats.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_no_address_contacts_in_group_chats() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let charlie = &tcm.charlie().await;
+
+ let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
+ let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
+ let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
+
+ // key-contact should be added successfully.
+ add_contact_to_chat(alice, chat_id, bob_key_contact_id).await?;
+
+ // Adding address-contact should fail.
+ let res = add_contact_to_chat(alice, chat_id, charlie_address_contact_id).await;
+ assert!(res.is_err());
+
+ Ok(())
+}
+
+/// Tests that key-contacts cannot be added to ad hoc groups.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let charlie = &tcm.charlie().await;
+
+ let chat_id = receive_imf(
+ alice,
+ b"Subject: Email thread\r\n\
+ From: alice@example.org\r\n\
+ To: Bob , Fiona \r\n\
+ Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\
+ Message-ID: \r\n\
+ \r\n\
+ Starting a new thread\r\n",
+ false,
+ )
+ .await?
+ .unwrap()
+ .chat_id;
+
+ let bob_address_contact_id = alice.add_or_lookup_address_contact_id(bob).await;
+ let charlie_key_contact_id = alice.add_or_lookup_contact_id(charlie).await;
+
+ // Address-contact should be added successfully.
+ add_contact_to_chat(alice, chat_id, bob_address_contact_id).await?;
+
+ // Adding key-contact should fail.
+ let res = add_contact_to_chat(alice, chat_id, charlie_key_contact_id).await;
+ assert!(res.is_err());
+
+ Ok(())
+}
+
+/// Tests that avatar cannot be set in ad hoc groups.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_no_avatar_in_adhoc_chats() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+
+ let chat_id = receive_imf(
+ alice,
+ b"Subject: Email thread\r\n\
+ From: alice@example.org\r\n\
+ To: Bob , Fiona \r\n\
+ Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\
+ Message-ID: \r\n\
+ \r\n\
+ Starting a new thread\r\n",
+ false,
+ )
+ .await?
+ .unwrap()
+ .chat_id;
+
+ // Test that setting avatar in ad hoc group is not possible.
+ let file = alice.dir.path().join("avatar.png");
+ let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
+ tokio::fs::write(&file, bytes).await?;
+ let res = set_chat_profile_image(alice, chat_id, file.to_str().unwrap()).await;
+ assert!(res.is_err());
+
Ok(())
}
diff --git a/src/chatlist.rs b/src/chatlist.rs
index d38a17784..5c36f1a90 100644
--- a/src/chatlist.rs
+++ b/src/chatlist.rs
@@ -572,7 +572,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sort_self_talk_up_on_forward() {
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
t.update_device_chats().await.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
@@ -605,7 +605,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_search_special_chat_names() {
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
diff --git a/src/constants.rs b/src/constants.rs
index 522d67502..884b498dc 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -210,11 +210,6 @@ pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60;
/// in the group membership consistency algo to reject outdated membership changes.
pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
-/// How long a 1:1 chat can't be used for sending while the SecureJoin is in progress. This should
-/// be 10-20 seconds so that we are reasonably sure that the app remains active and receiving also
-/// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`].
-pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
-
// To make text edits clearer for Non-Delta-MUA or old Delta Chats, edited text will be prefixed by EDITED_PREFIX.
// Newer Delta Chats will remove the prefix as needed.
pub(crate) const EDITED_PREFIX: &str = "✏️";
diff --git a/src/contact.rs b/src/contact.rs
index cc6976c2e..e702f7b2a 100644
--- a/src/contact.rs
+++ b/src/contact.rs
@@ -1,6 +1,6 @@
//! Contacts module
-use std::cmp::{min, Reverse};
+use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
@@ -11,8 +11,8 @@ use async_channel::{self as channel, Receiver, Sender};
use base64::Engine as _;
pub use deltachat_contact_tools::may_be_valid_addr;
use deltachat_contact_tools::{
- self as contact_tools, addr_cmp, addr_normalize, sanitize_name, sanitize_name_and_addr,
- ContactAddress, VcardContact,
+ self as contact_tools, addr_normalize, sanitize_name, sanitize_name_and_addr, ContactAddress,
+ VcardContact,
};
use deltachat_derive::{FromSql, ToSql};
use rusqlite::OptionalExtension;
@@ -20,7 +20,6 @@ use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{timeout, Duration};
-use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::color::str_to_color;
@@ -28,14 +27,16 @@ use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF};
use crate::context::Context;
use crate::events::EventType;
-use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
+use crate::key::{
+ load_self_public_key, self_fingerprint, self_fingerprint_opt, DcKey, Fingerprint,
+ SignedPublicKey,
+};
use crate::log::{info, warn, LogExt};
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
-use crate::peerstate::Peerstate;
use crate::sync::{self, Sync::*};
-use crate::tools::{duration_to_str, get_abs_path, smeared_time, time, SystemTime};
+use crate::tools::{duration_to_str, get_abs_path, time, SystemTime};
use crate::{chat, chatlist_events, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -102,7 +103,16 @@ impl ContactId {
/// for this contact will switch to the
/// contact's authorized name.
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
- let addr = context
+ self.set_name_ex(context, Sync, name).await
+ }
+
+ pub(crate) async fn set_name_ex(
+ self,
+ context: &Context,
+ sync: sync::Sync,
+ name: &str,
+ ) -> Result<()> {
+ let row = context
.sql
.transaction(|transaction| {
let is_changed = transaction.execute(
@@ -111,30 +121,45 @@ impl ContactId {
)? > 0;
if is_changed {
update_chat_names(context, transaction, self)?;
- let addr = transaction.query_row(
- "SELECT addr FROM contacts WHERE id=?",
+ let (addr, fingerprint) = transaction.query_row(
+ "SELECT addr, fingerprint FROM contacts WHERE id=?",
(self,),
|row| {
let addr: String = row.get(0)?;
- Ok(addr)
+ let fingerprint: String = row.get(1)?;
+ Ok((addr, fingerprint))
},
)?;
- Ok(Some(addr))
+ context.emit_event(EventType::ContactsChanged(Some(self)));
+ Ok(Some((addr, fingerprint)))
} else {
Ok(None)
}
})
.await?;
- if let Some(addr) = addr {
- chat::sync(
- context,
- chat::SyncId::ContactAddr(addr.to_string()),
- chat::SyncAction::Rename(name.to_string()),
- )
- .await
- .log_err(context)
- .ok();
+ if sync.into() {
+ if let Some((addr, fingerprint)) = row {
+ if fingerprint.is_empty() {
+ chat::sync(
+ context,
+ chat::SyncId::ContactAddr(addr),
+ chat::SyncAction::Rename(name.to_string()),
+ )
+ .await
+ .log_err(context)
+ .ok();
+ } else {
+ chat::sync(
+ context,
+ chat::SyncId::ContactFingerprint(fingerprint),
+ chat::SyncAction::Rename(name.to_string()),
+ )
+ .await
+ .log_err(context)
+ .ok();
+ }
+ }
}
Ok(())
}
@@ -196,31 +221,6 @@ impl ContactId {
.await?;
Ok(addr)
}
-
- /// Resets encryption with the contact.
- ///
- /// Effect is similar to receiving a message without Autocrypt header
- /// from the contact, but this action is triggered manually by the user.
- ///
- /// For example, this will result in sending the next message
- /// to 1:1 chat unencrypted, but will not remove existing verified keys.
- pub async fn reset_encryption(self, context: &Context) -> Result<()> {
- let now = time();
-
- let addr = self.addr(context).await?;
- if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? {
- peerstate.degrade_encryption(now);
- peerstate.save_to_db(&context.sql).await?;
- }
-
- // Reset 1:1 chat protection.
- if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? {
- chat_id
- .set_protection(context, ProtectionStatus::Unprotected, now, Some(self))
- .await?;
- }
- Ok(())
- }
}
impl fmt::Display for ContactId {
@@ -267,14 +267,8 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result Some(load_self_public_key(context).await?),
- _ => Peerstate::from_addr(context, &c.addr)
- .await?
- .and_then(|peerstate| peerstate.take_key(false)),
- };
- let key = key.map(|k| k.to_base64());
- let profile_image = match c.get_profile_image(context).await? {
+ let key = c.public_key(context).await?.map(|k| k.to_base64());
+ let profile_image = match c.get_profile_image_ex(context, false).await? {
None => None,
Some(path) => tokio::fs::read(path)
.await
@@ -330,15 +324,6 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
// mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we
// want `contact.authname` to be saved as the authname and not a locally given name.
let origin = Origin::CreateChat;
- let (id, modified) =
- match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await {
- Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
- Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
- Ok(val) => val,
- };
- if modified != Modifier::None {
- context.emit_event(EventType::ContactsChanged(Some(id)));
- }
let key = contact.key.as_ref().and_then(|k| {
SignedPublicKey::from_base64(k)
.with_context(|| {
@@ -350,50 +335,35 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
.log_err(context)
.ok()
});
+
+ let fingerprint;
if let Some(public_key) = key {
- let timestamp = contact
- .timestamp
- .as_ref()
- .map_or(0, |&t| min(t, smeared_time(context)));
- let aheader = Aheader {
- addr: contact.addr.clone(),
- public_key,
- prefer_encrypt: EncryptPreference::Mutual,
- };
- let peerstate = match Peerstate::from_addr(context, &aheader.addr).await {
- Err(e) => {
- warn!(
- context,
- "import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr
- );
- return Ok(id);
- }
- Ok(p) => p,
- };
- let peerstate = if let Some(mut p) = peerstate {
- p.apply_gossip(&aheader, timestamp);
- p
- } else {
- Peerstate::from_gossip(&aheader, timestamp)
- };
- if let Err(e) = peerstate.save_to_db(&context.sql).await {
- warn!(
- context,
- "import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr
- );
- return Ok(id);
- }
- if let Err(e) = peerstate
- .handle_fingerprint_change(context, timestamp)
+ fingerprint = public_key.dc_fingerprint().hex();
+
+ context
+ .sql
+ .execute(
+ "INSERT INTO public_keys (fingerprint, public_key)
+ VALUES (?, ?)
+ ON CONFLICT (fingerprint)
+ DO NOTHING",
+ (&fingerprint, public_key.to_bytes()),
+ )
+ .await?;
+ } else {
+ fingerprint = String::new();
+ }
+
+ let (id, modified) =
+ match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin)
.await
{
- warn!(
- context,
- "import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.",
- contact.addr
- );
- return Ok(id);
- }
+ Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
+ Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
+ Ok(val) => val,
+ };
+ if modified != Modifier::None {
+ context.emit_event(EventType::ContactsChanged(Some(id)));
}
if modified != Modifier::Created {
return Ok(id);
@@ -465,6 +435,11 @@ pub struct Contact {
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
addr: String,
+ /// OpenPGP key fingerprint.
+ /// Non-empty iff the contact is a key-contact,
+ /// identified by this fingerprint.
+ fingerprint: Option,
+
/// Blocked state. Use contact_is_blocked to access this field.
pub blocked: bool,
@@ -614,7 +589,7 @@ impl Contact {
.sql
.query_row_optional(
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
- c.authname, c.param, c.status, c.is_bot
+ c.authname, c.param, c.status, c.is_bot, c.fingerprint
FROM contacts c
WHERE c.id=?;",
(contact_id,),
@@ -628,11 +603,14 @@ impl Contact {
let param: String = row.get(6)?;
let status: Option = row.get(7)?;
let is_bot: bool = row.get(8)?;
+ let fingerprint: Option =
+ Some(row.get(9)?).filter(|s: &String| !s.is_empty());
let contact = Self {
id: contact_id,
name,
authname,
addr,
+ fingerprint,
blocked: blocked.unwrap_or_default(),
last_seen,
origin,
@@ -655,6 +633,9 @@ impl Contact {
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
+ if let Some(self_fp) = self_fingerprint_opt(context).await? {
+ contact.fingerprint = Some(self_fp.to_string());
+ }
contact.status = context
.get_config(Config::Selfstatus)
.await?
@@ -812,9 +793,10 @@ impl Contact {
let id = context
.sql
.query_get_value(
- "SELECT id FROM contacts \
- WHERE addr=?1 COLLATE NOCASE \
- AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
+ "SELECT id FROM contacts
+ WHERE addr=?1 COLLATE NOCASE
+ AND fingerprint='' -- Do not lookup key-contacts
+ AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
(
&addr_normalized,
ContactId::LAST_SPECIAL,
@@ -827,8 +809,19 @@ impl Contact {
Ok(id)
}
+ pub(crate) async fn add_or_lookup(
+ context: &Context,
+ name: &str,
+ addr: &ContactAddress,
+ origin: Origin,
+ ) -> Result<(ContactId, Modifier)> {
+ Self::add_or_lookup_ex(context, name, addr, "", origin).await
+ }
+
/// Lookup a contact and create it if it does not exist yet.
- /// The contact is identified by the email-address, a name and an "origin" can be given.
+ /// If `fingerprint` is non-empty, a key-contact with this fingerprint is added / looked up.
+ /// Otherwise, an address-contact with `addr` is added / looked up.
+ /// A name and an "origin" can be given.
///
/// The "origin" is where the address comes from -
/// from-header, cc-header, addressbook, qr, manual-edit etc.
@@ -852,21 +845,32 @@ impl Contact {
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
- pub(crate) async fn add_or_lookup(
+ pub(crate) async fn add_or_lookup_ex(
context: &Context,
name: &str,
- addr: &ContactAddress,
+ addr: &str,
+ fingerprint: &str,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
- ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
+ ensure!(
+ !addr.is_empty() || !fingerprint.is_empty(),
+ "Can not add_or_lookup empty address"
+ );
ensure!(origin != Origin::Unknown, "Missing valid origin");
if context.is_self_addr(addr).await? {
return Ok((ContactId::SELF, sth_modified));
}
+ if !fingerprint.is_empty() {
+ let fingerprint_self = self_fingerprint(context).await?;
+ if fingerprint == fingerprint_self {
+ return Ok((ContactId::SELF, sth_modified));
+ }
+ }
+
let mut name = sanitize_name(name);
if origin <= Origin::OutgoingTo {
// The user may accidentally have written to a "noreply" address with another MUA:
@@ -902,8 +906,10 @@ impl Contact {
let row = transaction
.query_row(
"SELECT id, name, addr, origin, authname
- FROM contacts WHERE addr=? COLLATE NOCASE",
- (addr,),
+ FROM contacts
+ WHERE fingerprint=?1 AND
+ (?1<>'' OR addr=?2 COLLATE NOCASE)",
+ (fingerprint, addr),
|row| {
let row_id: u32 = row.get(0)?;
let row_name: String = row.get(1)?;
@@ -927,7 +933,7 @@ impl Contact {
|| row_authname.is_empty());
row_id = id;
- if origin >= row_origin && addr.as_ref() != row_addr {
+ if origin >= row_origin && addr != row_addr {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
@@ -971,11 +977,12 @@ impl Contact {
let update_authname = !manual;
transaction.execute(
- "INSERT INTO contacts (name, addr, origin, authname)
- VALUES (?, ?, ?, ?);",
+ "INSERT INTO contacts (name, addr, fingerprint, origin, authname)
+ VALUES (?, ?, ?, ?, ?);",
(
if update_name { &name } else { "" },
&addr,
+ fingerprint,
origin,
if update_authname { &name } else { "" },
),
@@ -983,7 +990,14 @@ impl Contact {
sth_modified = Modifier::Created;
row_id = u32::try_from(transaction.last_insert_rowid())?;
- info!(context, "Added contact id={row_id} addr={addr}.");
+ if fingerprint.is_empty() {
+ info!(context, "Added contact id={row_id} addr={addr}.");
+ } else {
+ info!(
+ context,
+ "Added contact id={row_id} fpr={fingerprint} addr={addr}."
+ );
+ }
}
Ok(row_id)
})
@@ -1076,8 +1090,8 @@ impl Contact {
.sql
.query_map(
"SELECT c.id, c.addr FROM contacts c
- LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
WHERE c.id>?
+ AND c.fingerprint!='' \
AND c.origin>=? \
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
@@ -1133,6 +1147,7 @@ impl Contact {
.query_map(
"SELECT id, addr FROM contacts
WHERE id>?
+ AND fingerprint!=''
AND origin>=?
AND blocked=0
ORDER BY last_seen DESC, id DESC;",
@@ -1253,17 +1268,16 @@ impl Contact {
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
- let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
- let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
- else {
+ let Some(fingerprint_other) = contact.fingerprint() else {
return Ok(stock_str::encr_none(context).await);
};
+ let fingerprint_other = fingerprint_other.to_string();
- let stock_message = match peerstate.prefer_encrypt {
- EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
- EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
- EncryptPreference::Reset => stock_str::encr_none(context).await,
+ let stock_message = if contact.public_key(context).await?.is_some() {
+ stock_str::e2e_available(context).await
+ } else {
+ stock_str::encr_none(context).await
};
let finger_prints = stock_str::finger_prints(context).await;
@@ -1273,43 +1287,31 @@ impl Contact {
.await?
.dc_fingerprint()
.to_string();
- let fingerprint_other_verified = peerstate
- .peek_key(true)
- .map(|k| k.dc_fingerprint().to_string())
- .unwrap_or_default();
- let fingerprint_other_unverified = peerstate
- .peek_key(false)
- .map(|k| k.dc_fingerprint().to_string())
- .unwrap_or_default();
- if addr < peerstate.addr {
+ if addr < contact.addr {
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context).await,
&addr,
&fingerprint_self,
- "",
);
cat_fingerprint(
&mut ret,
contact.get_display_name(),
- &peerstate.addr,
- &fingerprint_other_verified,
- &fingerprint_other_unverified,
+ &contact.addr,
+ &fingerprint_other,
);
} else {
cat_fingerprint(
&mut ret,
contact.get_display_name(),
- &peerstate.addr,
- &fingerprint_other_verified,
- &fingerprint_other_unverified,
+ &contact.addr,
+ &fingerprint_other,
);
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context).await,
&addr,
&fingerprint_self,
- "",
);
}
@@ -1382,6 +1384,59 @@ impl Contact {
&self.addr
}
+ /// Returns true if the contact is a key-contact.
+ /// Otherwise it is an addresss-contact.
+ pub fn is_key_contact(&self) -> bool {
+ self.fingerprint.is_some()
+ }
+
+ /// Returns OpenPGP fingerprint of a contact.
+ ///
+ /// `None` for address-contacts.
+ pub fn fingerprint(&self) -> Option {
+ if let Some(fingerprint) = &self.fingerprint {
+ fingerprint.parse().ok()
+ } else {
+ None
+ }
+ }
+
+ /// Returns OpenPGP public key of a contact.
+ ///
+ /// Returns `None` if the contact is not a key-contact
+ /// or if the key is not available.
+ /// It is possible for a key-contact to not have a key,
+ /// e.g. if only the fingerprint is known from a QR-code.
+ pub async fn public_key(&self, context: &Context) -> Result> {
+ if self.id == ContactId::SELF {
+ return Ok(Some(load_self_public_key(context).await?));
+ }
+
+ if let Some(fingerprint) = &self.fingerprint {
+ if let Some(public_key_bytes) = context
+ .sql
+ .query_row_optional(
+ "SELECT public_key
+ FROM public_keys
+ WHERE fingerprint=?",
+ (fingerprint,),
+ |row| {
+ let bytes: Vec = row.get(0)?;
+ Ok(bytes)
+ },
+ )
+ .await?
+ {
+ let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
+ Ok(Some(public_key))
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+ }
+
/// Get name authorized by the contact.
pub fn get_authname(&self) -> &str {
&self.authname
@@ -1447,11 +1502,28 @@ impl Contact {
/// This is the image set by each remote user on their own
/// using set_config(context, "selfavatar", image).
pub async fn get_profile_image(&self, context: &Context) -> Result> {
+ self.get_profile_image_ex(context, true).await
+ }
+
+ /// Get the contact's profile image.
+ /// This is the image set by each remote user on their own
+ /// using set_config(context, "selfavatar", image).
+ async fn get_profile_image_ex(
+ &self,
+ context: &Context,
+ show_fallback_icon: bool,
+ ) -> Result > {
if self.id == ContactId::SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await? {
return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already
}
- } else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
+ } else if self.id == ContactId::DEVICE {
+ return Ok(Some(chat::get_device_icon(context).await?));
+ }
+ if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
+ return Ok(Some(chat::get_address_contact_icon(context).await?));
+ }
+ if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
}
@@ -1477,25 +1549,21 @@ impl Contact {
/// Returns whether end-to-end encryption to the contact is available.
pub async fn e2ee_avail(&self, context: &Context) -> Result {
if self.id == ContactId::SELF {
+ // We don't need to check if we have our own key.
return Ok(true);
}
- let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
- return Ok(false);
- };
- Ok(peerstate.peek_key(false).is_some())
+ Ok(self.public_key(context).await?.is_some())
}
/// Returns true if the contact
- /// can be added to verified chats,
- /// i.e. has a verified key
- /// and Autocrypt key matches the verified key.
+ /// can be added to verified chats.
///
/// If contact is verified
/// UI should display green checkmark after the contact name
/// in contact list items and
/// in chat member list items.
///
- /// In contact profile view, us this function only if there is no chat with the contact,
+ /// In contact profile view, use this function only if there is no chat with the contact,
/// otherwise use is_chat_protected().
/// Use [Self::get_verifier_id] to display the verifier contact
/// in the info section of the contact profile.
@@ -1506,64 +1574,31 @@ impl Contact {
return Ok(true);
}
- let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
- return Ok(false);
- };
-
- let forward_verified = peerstate.is_using_verified_key();
- let backward_verified = peerstate.is_backward_verified(context).await?;
- Ok(forward_verified && backward_verified)
- }
-
- /// Returns true if we have a verified key for the contact
- /// and it is the same as Autocrypt key.
- /// This is enough to send messages to the contact in verified chat
- /// and verify received messages, but not enough to display green checkmark
- /// or add the contact to verified groups.
- pub async fn is_forward_verified(&self, context: &Context) -> Result {
- if self.id == ContactId::SELF {
- return Ok(true);
- }
-
- let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
- return Ok(false);
- };
-
- Ok(peerstate.is_using_verified_key())
+ Ok(self.get_verifier_id(context).await?.is_some())
}
/// Returns the `ContactId` that verified the contact.
///
- /// If the function returns non-zero result,
+ /// If this returns Some(_),
/// display green checkmark in the profile and "Introduced by ..." line
/// with the name and address of the contact
/// formatted by [Self::get_name_n_addr].
///
- /// If this function returns a verifier,
- /// this does not necessarily mean
- /// you can add the contact to verified chats.
- /// Use [Self::is_verified] to check
- /// if a contact can be added to a verified chat instead.
- pub async fn get_verifier_id(&self, context: &Context) -> Result> {
- let Some(verifier_addr) = Peerstate::from_addr(context, self.get_addr())
+ /// If this returns `Some(None)`, then the contact is verified,
+ /// but it's unclear by whom.
+ pub async fn get_verifier_id(&self, context: &Context) -> Result >> {
+ let verifier_id: u32 = context
+ .sql
+ .query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,))
.await?
- .and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))
- else {
- return Ok(None);
- };
+ .with_context(|| format!("Contact {} does not exist", self.id))?;
- if addr_cmp(&verifier_addr, &self.addr) {
- // Contact is directly verified via QR code.
- return Ok(Some(ContactId::SELF));
- }
-
- match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? {
- Some(contact_id) => Ok(Some(contact_id)),
- None => {
- let addr = &self.addr;
- warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
- Ok(None)
- }
+ if verifier_id == 0 {
+ Ok(None)
+ } else if verifier_id == self.id.to_u32() {
+ Ok(Some(None))
+ } else {
+ Ok(Some(Some(ContactId::new(verifier_id))))
}
}
@@ -1735,14 +1770,16 @@ WHERE type=? AND id IN (
true => chat::SyncAction::Block,
false => chat::SyncAction::Unblock,
};
- chat::sync(
- context,
- chat::SyncId::ContactAddr(contact.addr.clone()),
- action,
- )
- .await
- .log_err(context)
- .ok();
+ let sync_id = if let Some(fingerprint) = contact.fingerprint() {
+ chat::SyncId::ContactFingerprint(fingerprint.hex())
+ } else {
+ chat::SyncId::ContactAddr(contact.addr.clone())
+ };
+
+ chat::sync(context, sync_id, action)
+ .await
+ .log_err(context)
+ .ok();
}
}
@@ -1862,29 +1899,51 @@ pub(crate) async fn update_last_seen(
Ok(())
}
-fn cat_fingerprint(
- ret: &mut String,
- name: &str,
- addr: &str,
- fingerprint_verified: &str,
- fingerprint_unverified: &str,
-) {
- *ret += &format!(
- "\n\n{} ({}):\n{}",
- name,
- addr,
- if !fingerprint_verified.is_empty() {
- fingerprint_verified
- } else {
- fingerprint_unverified
- },
+/// Marks contact `contact_id` as verified by `verifier_id`.
+pub(crate) async fn mark_contact_id_as_verified(
+ context: &Context,
+ contact_id: ContactId,
+ verifier_id: ContactId,
+) -> Result<()> {
+ debug_assert_ne!(
+ contact_id, verifier_id,
+ "Contact cannot be verified by self"
);
- if !fingerprint_verified.is_empty()
- && !fingerprint_unverified.is_empty()
- && fingerprint_verified != fingerprint_unverified
- {
- *ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}");
- }
+ context
+ .sql
+ .transaction(|transaction| {
+ let contact_fingerprint: String = transaction.query_row(
+ "SELECT fingerprint FROM contacts WHERE id=?",
+ (contact_id,),
+ |row| row.get(0),
+ )?;
+ if contact_fingerprint.is_empty() {
+ bail!("Non-key-contact {contact_id} cannot be verified");
+ }
+ if verifier_id != ContactId::SELF {
+ let verifier_fingerprint: String = transaction.query_row(
+ "SELECT fingerprint FROM contacts WHERE id=?",
+ (verifier_id,),
+ |row| row.get(0),
+ )?;
+ if verifier_fingerprint.is_empty() {
+ bail!(
+ "Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
+ );
+ }
+ }
+ transaction.execute(
+ "UPDATE contacts SET verifier=? WHERE id=?",
+ (verifier_id, contact_id),
+ )?;
+ Ok(())
+ })
+ .await?;
+ Ok(())
+}
+
+fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
+ *ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs
index 2acf699f1..ae07c65d4 100644
--- a/src/contact/contact_tests.rs
+++ b/src/contact/contact_tests.rs
@@ -1,4 +1,4 @@
-use deltachat_contact_tools::may_be_valid_addr;
+use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
@@ -56,58 +56,49 @@ fn test_split_address_book() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_contacts() -> Result<()> {
- let context = TestContext::new().await;
+ let mut tcm = TestContextManager::new();
+ let context = tcm.bob().await;
+ let alice = tcm.alice().await;
+ alice
+ .set_config(Config::Displayname, Some("MyName"))
+ .await?;
- assert!(context.get_all_self_addrs().await?.is_empty());
-
- // Bob is not in the contacts yet.
- let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
+ // Alice is not in the contacts yet.
+ let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?;
+ assert_eq!(contacts.len(), 0);
+ let contacts = Contact::get_all(&context.ctx, 0, Some("MyName")).await?;
assert_eq!(contacts.len(), 0);
- let (id, _modified) = Contact::add_or_lookup(
- &context.ctx,
- "bob",
- &ContactAddress::new("user@example.org")?,
- Origin::IncomingReplyTo,
- )
- .await?;
+ let id = context.add_or_lookup_contact_id(&alice).await;
assert_ne!(id, ContactId::UNDEFINED);
- let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
+ let contact = Contact::get_by_id(&context, id).await.unwrap();
assert_eq!(contact.get_name(), "");
- assert_eq!(contact.get_authname(), "bob");
- assert_eq!(contact.get_display_name(), "bob");
+ assert_eq!(contact.get_authname(), "MyName");
+ assert_eq!(contact.get_display_name(), "MyName");
// Search by name.
- let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
+ let contacts = Contact::get_all(&context, 0, Some("myname")).await?;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.first(), Some(&id));
// Search by address.
- let contacts = Contact::get_all(&context.ctx, 0, Some("user")).await?;
+ let contacts = Contact::get_all(&context, 0, Some("alice@example.org")).await?;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.first(), Some(&id));
- let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).await?;
+ let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?;
assert_eq!(contacts.len(), 0);
- // Set Bob name to "someone" manually.
- let (contact_bob_id, modified) = Contact::add_or_lookup(
- &context.ctx,
- "someone",
- &ContactAddress::new("user@example.org")?,
- Origin::ManuallyCreated,
- )
- .await?;
- assert_eq!(contact_bob_id, id);
- assert_eq!(modified, Modifier::Modified);
+ // Set Alice name to "someone" manually.
+ id.set_name(&context, "someone").await?;
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "someone");
- assert_eq!(contact.get_authname(), "bob");
+ assert_eq!(contact.get_authname(), "MyName");
assert_eq!(contact.get_display_name(), "someone");
// Not searchable by authname, because it is not displayed.
- let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
+ let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
assert_eq!(contacts.len(), 0);
// Search by display name (same as manually set name).
@@ -133,7 +124,7 @@ async fn test_is_self_addr() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
let book = concat!(
" Name one \n one@eins.org \n",
"Name two\ntwo@deux.net\n",
@@ -247,7 +238,7 @@ async fn test_add_or_lookup() {
// check SELF
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
- assert_eq!(contact.get_addr(), ""); // we're not configured
+ assert_eq!(contact.get_addr(), "alice@example.org");
assert!(!contact.is_blocked());
}
@@ -282,7 +273,7 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "f@example.org");
assert_eq!(contact.get_name_n_addr(), "f@example.org");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
- assert_eq!(contacts.len(), 1);
+ assert_eq!(contacts.len(), 0);
// second message inits the name
receive_imf(
@@ -308,9 +299,9 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "Flobbyfoo");
assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
- assert_eq!(contacts.len(), 1);
+ assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
- assert_eq!(contacts.len(), 1);
+ assert_eq!(contacts.len(), 0);
// third message changes the name
receive_imf(
@@ -338,11 +329,11 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "Foo Flobby");
assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
- assert_eq!(contacts.len(), 1);
+ assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("Foo Flobby")).await?;
- assert_eq!(contacts.len(), 1);
+ assert_eq!(contacts.len(), 0);
// change name manually
let test_id = Contact::create(&t, "Falk", "f@example.org").await?;
@@ -356,9 +347,9 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "Falk");
assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
- assert_eq!(contacts.len(), 1);
+ assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
- assert_eq!(contacts.len(), 1);
+ assert_eq!(contacts.len(), 0);
Ok(())
}
@@ -366,20 +357,13 @@ async fn test_contact_name_changes() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete() -> Result<()> {
let alice = TestContext::new_alice().await;
+ let bob = TestContext::new_bob().await;
assert!(Contact::delete(&alice, ContactId::SELF).await.is_err());
// Create Bob contact
- let (contact_id, _) = Contact::add_or_lookup(
- &alice,
- "Bob",
- &ContactAddress::new("bob@example.net")?,
- Origin::ManuallyCreated,
- )
- .await?;
- let chat = alice
- .create_chat_with_contact("Bob", "bob@example.net")
- .await;
+ let contact_id = alice.add_or_lookup_contact_id(&bob).await;
+ let chat = alice.create_chat(&bob).await;
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
@@ -416,30 +400,57 @@ async fn test_delete() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_and_recreate_contact() -> Result<()> {
- let t = TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let t = tcm.alice().await;
+ let bob = tcm.bob().await;
// test recreation after physical deletion
- let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
- assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
+ let contact_id1 = t.add_or_lookup_contact_id(&bob).await;
+ assert_eq!(
+ Contact::get_all(&t, 0, Some("bob@example.net"))
+ .await?
+ .len(),
+ 1
+ );
Contact::delete(&t, contact_id1).await?;
assert!(Contact::get_by_id(&t, contact_id1).await.is_err());
- assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
- let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
+ assert_eq!(
+ Contact::get_all(&t, 0, Some("bob@example.net"))
+ .await?
+ .len(),
+ 0
+ );
+ let contact_id2 = t.add_or_lookup_contact_id(&bob).await;
assert_ne!(contact_id2, contact_id1);
- assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
+ assert_eq!(
+ Contact::get_all(&t, 0, Some("bob@example.net"))
+ .await?
+ .len(),
+ 1
+ );
// test recreation after hiding
- t.create_chat_with_contact("Foo", "foo@bar.de").await;
+ t.create_chat(&bob).await;
Contact::delete(&t, contact_id2).await?;
let contact = Contact::get_by_id(&t, contact_id2).await?;
assert_eq!(contact.origin, Origin::Hidden);
- assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
+ assert_eq!(
+ Contact::get_all(&t, 0, Some("bob@example.net"))
+ .await?
+ .len(),
+ 0
+ );
- let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
+ let contact_id3 = t.add_or_lookup_contact_id(&bob).await;
let contact = Contact::get_by_id(&t, contact_id3).await?;
- assert_eq!(contact.origin, Origin::ManuallyCreated);
+ assert_eq!(contact.origin, Origin::CreateChat);
assert_eq!(contact_id3, contact_id2);
- assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
+ assert_eq!(
+ Contact::get_all(&t, 0, Some("bob@example.net"))
+ .await?
+ .len(),
+ 1
+ );
Ok(())
}
@@ -728,51 +739,59 @@ async fn test_contact_get_color() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_contact_get_encrinfo() -> Result<()> {
- let alice = TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
// Return error for special IDs
- let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await;
+ let encrinfo = Contact::get_encrinfo(alice, ContactId::SELF).await;
assert!(encrinfo.is_err());
- let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await;
+ let encrinfo = Contact::get_encrinfo(alice, ContactId::DEVICE).await;
assert!(encrinfo.is_err());
- let (contact_bob_id, _modified) = Contact::add_or_lookup(
- &alice,
- "Bob",
- &ContactAddress::new("bob@example.net")?,
- Origin::ManuallyCreated,
- )
- .await?;
-
- let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
+ let address_contact_bob_id = alice.add_or_lookup_address_contact_id(bob).await;
+ let encrinfo = Contact::get_encrinfo(alice, address_contact_bob_id).await?;
assert_eq!(encrinfo, "No encryption");
- let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
- assert!(!contact.e2ee_avail(&alice).await?);
- let bob = TestContext::new_bob().await;
- let chat_alice = bob
- .create_chat_with_contact("Alice", "alice@example.org")
- .await;
- send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?;
- let msg = bob.pop_sent_msg().await;
- alice.recv_msg(&msg).await;
+ let contact = Contact::get_by_id(alice, address_contact_bob_id).await?;
+ assert!(!contact.e2ee_avail(alice).await?);
- let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
+ let contact_bob_id = alice.add_or_lookup_contact_id(bob).await;
+ let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
assert_eq!(
encrinfo,
- "End-to-end encryption preferred.
+ "End-to-end encryption available.
Fingerprints:
Me (alice@example.org):
2E6F A2CB 23B5 32D7 2863
4B58 64B0 8F61 A9ED 9443
-Bob (bob@example.net):
+bob@example.net (bob@example.net):
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
- let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
- assert!(contact.e2ee_avail(&alice).await?);
+ let contact = Contact::get_by_id(alice, contact_bob_id).await?;
+ assert!(contact.e2ee_avail(alice).await?);
+
+ alice.sql.execute("DELETE FROM public_keys", ()).await?;
+ let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
+ assert_eq!(
+ encrinfo,
+ "No encryption.
+Fingerprints:
+
+Me (alice@example.org):
+2E6F A2CB 23B5 32D7 2863
+4B58 64B0 8F61 A9ED 9443
+
+bob@example.net (bob@example.net):
+CCCB 5AA9 F6E1 141C 9431
+65F1 DB18 B18C BCF7 0487"
+ );
+ let contact = Contact::get_by_id(alice, contact_bob_id).await?;
+ assert!(!contact.e2ee_avail(alice).await?);
+
Ok(())
}
@@ -780,24 +799,24 @@ CCCB 5AA9 F6E1 141C 9431
/// synchronized when the message is not encrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_synchronize_status() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+
// Alice has two devices.
- let alice1 = TestContext::new_alice().await;
- let alice2 = TestContext::new_alice().await;
+ let alice1 = &tcm.alice().await;
+ let alice2 = &tcm.alice().await;
// Bob has one device.
- let bob = TestContext::new_bob().await;
+ let bob = &tcm.bob().await;
let default_status = alice1.get_config(Config::Selfstatus).await?;
alice1
.set_config(Config::Selfstatus, Some("New status"))
.await?;
- let chat = alice1
- .create_chat_with_contact("Bob", "bob@example.net")
- .await;
+ let chat = alice1.create_email_chat(bob).await;
// Alice sends a message to Bob from the first device.
- send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
+ send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Message is not encrypted.
@@ -813,18 +832,9 @@ async fn test_synchronize_status() -> Result<()> {
// Message was not encrypted, so status is not copied.
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
- // Bob replies.
- let chat = bob
- .create_chat_with_contact("Alice", "alice@example.org")
- .await;
-
- send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
- let sent_msg = bob.pop_sent_msg().await;
- alice1.recv_msg(&sent_msg).await;
- alice2.recv_msg(&sent_msg).await;
-
- // Alice sends second message.
- send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
+ // Alice sends encrypted message.
+ let chat = alice1.create_chat(bob).await;
+ send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Second message is encrypted.
@@ -845,12 +855,14 @@ async fn test_synchronize_status() -> Result<()> {
/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_changed_event() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+
// Alice has two devices.
- let alice1 = TestContext::new_alice().await;
- let alice2 = TestContext::new_alice().await;
+ let alice1 = &tcm.alice().await;
+ let alice2 = &tcm.alice().await;
// Bob has one device.
- let bob = TestContext::new_bob().await;
+ let bob = &tcm.bob().await;
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
@@ -866,20 +878,9 @@ async fn test_selfavatar_changed_event() -> Result<()> {
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
.await;
- // Bob sends a message so that Alice can encrypt to him.
- let chat = bob
- .create_chat_with_contact("Alice", "alice@example.org")
- .await;
-
- send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
- let sent_msg = bob.pop_sent_msg().await;
- alice1.recv_msg(&sent_msg).await;
- alice2.recv_msg(&sent_msg).await;
-
// Alice sends a message.
- let alice1_chat_id = alice1.get_last_msg().await.chat_id;
- alice1_chat_id.accept(&alice1).await?;
- send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?;
+ let alice1_chat_id = alice1.create_chat(bob).await.id;
+ send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// The message is encrypted.
@@ -1008,7 +1009,7 @@ async fn test_verified_by_none() -> Result<()> {
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_id(&alice).await?.is_none());
- // Receive a message from Bob to create a peerstate.
+ // Receive a message from Bob to save the public key.
let chat = bob.create_chat(&alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
alice.recv_msg(&sent_msg).await;
@@ -1066,14 +1067,9 @@ async fn test_make_n_import_vcard() -> Result<()> {
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
let chat = bob.create_chat(alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
- alice.recv_msg(&sent_msg).await;
- let bob_id = Contact::create(alice, "Some Bob", &bob_addr).await?;
- let key_base64 = Peerstate::from_addr(alice, &bob_addr)
- .await?
- .unwrap()
- .peek_key(false)
- .unwrap()
- .to_base64();
+ let bob_id = alice.recv_msg(&sent_msg).await.from_id;
+ let bob_contact = Contact::get_by_id(alice, bob_id).await?;
+ let key_base64 = bob_contact.public_key(alice).await?.unwrap().to_base64();
let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
assert_eq!(make_vcard(alice, &[]).await?, "".to_string());
@@ -1157,8 +1153,10 @@ async fn test_make_n_import_vcard() -> Result<()> {
Ok(())
}
+/// Tests importing a vCard with the same email address,
+/// but a new key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_import_vcard_updates_only_key() -> Result<()> {
+async fn test_import_vcard_key_change() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
@@ -1176,28 +1174,34 @@ async fn test_import_vcard_updates_only_key() -> Result<()> {
let msg = bob.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
- let bob = &TestContext::new().await;
- bob.configure_addr(bob_addr).await;
- bob.set_config(Config::Displayname, Some("Not Bob")).await?;
- let avatar_path = bob.dir.path().join("avatar.png");
+ let bob1 = &TestContext::new().await;
+ bob1.configure_addr(bob_addr).await;
+ bob1.set_config(Config::Displayname, Some("New Bob"))
+ .await?;
+ let avatar_path = bob1.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&avatar_path, avatar_bytes).await?;
- bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
+ bob1.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
.await?;
SystemTime::shift(Duration::from_secs(1));
- let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?;
- assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]);
+ let vcard1 = make_vcard(bob1, &[ContactId::SELF]).await?;
+ let alice_bob_id1 = import_vcard(alice, &vcard1).await?[0];
+ assert_ne!(alice_bob_id1, alice_bob_id);
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?;
assert_eq!(alice_bob_contact.get_authname(), "Bob");
assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None);
+ let alice_bob_contact1 = Contact::get_by_id(alice, alice_bob_id1).await?;
+ assert_eq!(alice_bob_contact1.get_authname(), "New Bob");
+ assert!(alice_bob_contact1.get_profile_image(alice).await?.is_some());
+
+ // Last message is still the same,
+ // no new messages are added.
let msg = alice.get_last_msg_in(chat_id).await;
- assert!(msg.is_info());
- assert_eq!(
- msg.get_text(),
- stock_str::contact_setup_changed(alice, bob_addr).await
- );
- let sent_msg = alice.send_text(chat_id, "moin").await;
- let msg = bob.recv_msg(&sent_msg).await;
+ assert_eq!(msg.get_text(), "moin");
+
+ let chat_id1 = ChatId::create_for_contact(alice, alice_bob_id1).await?;
+ let sent_msg = alice.send_text(chat_id1, "moin").await;
+ let msg = bob1.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
// The old vCard is imported, but doesn't change Bob's key for Alice.
@@ -1209,63 +1213,6 @@ async fn test_import_vcard_updates_only_key() -> Result<()> {
Ok(())
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_reset_encryption() -> Result<()> {
- let mut tcm = TestContextManager::new();
- let alice = &tcm.alice().await;
- let bob = &tcm.bob().await;
-
- let msg = tcm.send_recv_accept(bob, alice, "Hi!").await;
- assert_eq!(msg.get_showpadlock(), true);
-
- let alice_bob_chat_id = msg.chat_id;
- let alice_bob_contact_id = msg.from_id;
-
- alice_bob_contact_id.reset_encryption(alice).await?;
-
- let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
- let msg = bob.recv_msg(&sent).await;
- assert_eq!(msg.get_showpadlock(), false);
-
- Ok(())
-}
-
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_reset_verified_encryption() -> Result<()> {
- let mut tcm = TestContextManager::new();
- let alice = &tcm.alice().await;
- let bob = &tcm.bob().await;
-
- tcm.execute_securejoin(bob, alice).await;
-
- let msg = tcm.send_recv(bob, alice, "Encrypted").await;
- assert_eq!(msg.get_showpadlock(), true);
-
- let alice_bob_chat_id = msg.chat_id;
- let alice_bob_contact_id = msg.from_id;
-
- alice_bob_contact_id.reset_encryption(alice).await?;
-
- // Check that the contact is still verified after resetting encryption.
- let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?;
- assert_eq!(alice_bob_contact.is_verified(alice).await?, true);
-
- // 1:1 chat and profile is no longer verified.
- assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false);
-
- let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await;
- assert_eq!(
- info_msg.text,
- "bob@example.net sent a message from another device."
- );
-
- let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
- let msg = bob.recv_msg(&sent).await;
- assert_eq!(msg.get_showpadlock(), false);
-
- Ok(())
-}
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_is_verified() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -1275,9 +1222,87 @@ async fn test_self_is_verified() -> Result<()> {
assert_eq!(contact.is_verified(&alice).await?, true);
assert!(contact.is_profile_verified(&alice).await?);
assert!(contact.get_verifier_id(&alice).await?.is_none());
+ assert!(contact.is_key_contact());
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
Ok(())
}
+
+/// Tests that importing a vCard with a key creates a key-contact.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_vcard_creates_key_contact() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+
+ let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
+ let contact_ids = import_vcard(alice, &vcard).await?;
+ assert_eq!(contact_ids.len(), 1);
+ let contact_id = contact_ids.first().unwrap();
+ let contact = Contact::get_by_id(alice, *contact_id).await?;
+ assert!(contact.is_key_contact());
+
+ Ok(())
+}
+
+/// Tests changing display name by sending a message.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_name_changes() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+
+ alice
+ .set_config(Config::Displayname, Some("Alice Revision 1"))
+ .await?;
+ let alice_bob_chat = alice.create_chat(bob).await;
+ let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
+ let bob_alice_id = bob.recv_msg(&sent_msg).await.from_id;
+ let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
+ assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 1");
+
+ alice
+ .set_config(Config::Displayname, Some("Alice Revision 2"))
+ .await?;
+ let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
+ bob.recv_msg(&sent_msg).await;
+ let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
+ assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 2");
+
+ // Explicitly rename contact to "Renamed".
+ bob.evtracker.clear_events();
+ bob_alice_contact.id.set_name(bob, "Renamed").await?;
+ let event = bob
+ .evtracker
+ .get_matching(|e| matches!(e, EventType::ContactsChanged { .. }))
+ .await;
+ assert_eq!(
+ event,
+ EventType::ContactsChanged(Some(bob_alice_contact.id))
+ );
+ let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
+ assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
+
+ // Alice also renames self into "Renamed".
+ alice
+ .set_config(Config::Displayname, Some("Renamed"))
+ .await?;
+ let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
+ bob.recv_msg(&sent_msg).await;
+ let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
+ assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
+
+ // Contact name was set to "Renamed" explicitly before,
+ // so it should not be changed.
+ alice
+ .set_config(Config::Displayname, Some("Renamed again"))
+ .await?;
+ let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
+ bob.recv_msg(&sent_msg).await;
+ let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
+ assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
+
+ Ok(())
+}
diff --git a/src/context.rs b/src/context.rs
index 858689b8e..2d1ba7655 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -5,35 +5,32 @@ use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::Arc;
+use std::sync::{Arc, OnceLock};
use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
-use pgp::composed::SignedPublicKey;
use pgp::types::PublicKeyTrait;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
-use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
};
-use crate::contact::{Contact, ContactId};
+use crate::contact::{import_vcard, mark_contact_id_as_verified, Contact, ContactId};
use crate::debug_logging::DebugLogging;
use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
-use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
+use crate::key::{load_self_secret_key, self_fingerprint};
use crate::log::{info, warn};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
-use crate::peerstate::Peerstate;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{convert_folder_meaning, SchedulerState};
@@ -279,6 +276,13 @@ pub struct InnerContext {
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: parking_lot::RwLock,
+ /// It's not possible to emit migration errors as an event,
+ /// because at the time of the migration, there is no event emitter yet.
+ /// So, this holds the error that happened during migration, if any.
+ /// This is necessary for the possibly-failible PGP migration,
+ /// which happened 2025-05, and can be removed a few releases later.
+ pub(crate) migration_error: parking_lot::RwLock>,
+
/// If debug logging is enabled, this contains all necessary information
///
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
@@ -294,6 +298,11 @@ pub struct InnerContext {
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc>>,
+
+ /// The own fingerprint, if it was computed already.
+ /// tokio::sync::OnceCell would be possible to use, but overkill for our usecase;
+ /// the standard library's OnceLock is enough, and it's a lot smaller in memory.
+ pub(crate) self_fingerprint: OnceLock,
}
/// The state of ongoing process.
@@ -448,10 +457,12 @@ impl Context {
creation_time: tools::Time::now(),
last_full_folder_scan: Mutex::new(None),
last_error: parking_lot::RwLock::new("".to_string()),
+ migration_error: parking_lot::RwLock::new(None),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
+ self_fingerprint: OnceLock::new(),
};
let ctx = Context {
@@ -805,10 +816,10 @@ impl Context {
let pub_key_cnt = self
.sql
- .count("SELECT COUNT(*) FROM acpeerstates;", ())
+ .count("SELECT COUNT(*) FROM public_keys;", ())
.await?;
- let fingerprint_str = match load_self_public_key(self).await {
- Ok(key) => key.dc_fingerprint().hex(),
+ let fingerprint_str = match self_fingerprint(self).await {
+ Ok(fp) => fp.to_string(),
Err(err) => format!(""),
};
@@ -1164,32 +1175,14 @@ impl Context {
/// On the other end, a bot will receive the message and make it available
/// to Delta Chat's developers.
pub async fn draft_self_report(&self) -> Result {
- const SELF_REPORTING_BOT: &str = "self_reporting@testrun.org";
+ const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
+ let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
+ .await?
+ .first()
+ .context("Self reporting bot vCard does not contain a contact")?;
+ mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?;
- let contact_id = Contact::create(self, "Statistics bot", SELF_REPORTING_BOT).await?;
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
-
- // We're including the bot's public key in Delta Chat
- // so that the first message to the bot can directly be encrypted:
- let public_key = SignedPublicKey::from_base64(
- "xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCM\
- PNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUI\
- CQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+Nq\
- I4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARl\
- t8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGB\
- YIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj\
- 2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ\
- 4=",
- )?;
- let mut peerstate = Peerstate::from_public_key(
- SELF_REPORTING_BOT,
- 0,
- EncryptPreference::Mutual,
- &public_key,
- );
- let fingerprint = public_key.dc_fingerprint();
- peerstate.set_verified(public_key, fingerprint, "".to_string())?;
- peerstate.save_to_db(&self.sql).await?;
chat_id
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
.await?;
diff --git a/src/decrypt.rs b/src/decrypt.rs
index 2664ea701..8e242a57e 100644
--- a/src/decrypt.rs
+++ b/src/decrypt.rs
@@ -3,14 +3,9 @@
use std::collections::HashSet;
use anyhow::Result;
-use deltachat_contact_tools::addr_cmp;
use mailparse::ParsedMail;
-use crate::aheader::Aheader;
-use crate::context::Context;
-use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
-use crate::log::info;
-use crate::peerstate::Peerstate;
+use crate::key::{Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
@@ -144,83 +139,6 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
}
}
-/// Returns public keyring for `peerstate`.
-pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec {
- let mut public_keyring_for_validate = Vec::new();
- if let Some(peerstate) = peerstate {
- if let Some(key) = &peerstate.public_key {
- public_keyring_for_validate.push(key.clone());
- } else if let Some(key) = &peerstate.gossip_key {
- public_keyring_for_validate.push(key.clone());
- }
- }
- public_keyring_for_validate
-}
-
-/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
-///
-/// If we already know this fingerprint from another contact's peerstate, return that
-/// peerstate in order to make AEAP work, but don't save it into the db yet.
-///
-/// Returns updated peerstate.
-pub(crate) async fn get_autocrypt_peerstate(
- context: &Context,
- from: &str,
- autocrypt_header: Option<&Aheader>,
- message_time: i64,
- allow_aeap: bool,
-) -> Result> {
- let allow_change = !context.is_self_addr(from).await?;
- let mut peerstate;
-
- // Apply Autocrypt header
- if let Some(header) = autocrypt_header {
- if allow_aeap {
- // If we know this fingerprint from another addr,
- // we may want to do a transition from this other addr
- // (and keep its peerstate)
- // For security reasons, for now, we only do a transition
- // if the fingerprint is verified.
- peerstate = Peerstate::from_verified_fingerprint_or_addr(
- context,
- &header.public_key.dc_fingerprint(),
- from,
- )
- .await?;
- } else {
- peerstate = Peerstate::from_addr(context, from).await?;
- }
-
- if let Some(ref mut peerstate) = peerstate {
- if addr_cmp(&peerstate.addr, from) {
- if allow_change {
- peerstate.apply_header(context, header, message_time);
- peerstate.save_to_db(&context.sql).await?;
- } else {
- info!(
- context,
- "Refusing to update existing peerstate of {}", &peerstate.addr
- );
- }
- }
- // If `peerstate.addr` and `from` differ, this means that
- // someone is using the same key but a different addr, probably
- // because they made an AEAP transition.
- // But we don't know if that's legit until we checked the
- // signatures, so wait until then with writing anything
- // to the database.
- } else {
- let p = Peerstate::from_header(header, message_time);
- p.save_to_db(&context.sql).await?;
- peerstate = Some(p);
- }
- } else {
- peerstate = Peerstate::from_addr(context, from).await?;
- }
-
- Ok(peerstate)
-}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/e2ee.rs b/src/e2ee.rs
index f5bf44cc0..d4529bfaf 100644
--- a/src/e2ee.rs
+++ b/src/e2ee.rs
@@ -1,9 +1,8 @@
//! End-to-end encryption support.
-use std::collections::BTreeSet;
use std::io::Cursor;
-use anyhow::{bail, Result};
+use anyhow::Result;
use mail_builder::mime::MimePart;
use num_traits::FromPrimitive;
@@ -11,8 +10,6 @@ use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::context::Context;
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
-use crate::log::warn;
-use crate::peerstate::Peerstate;
use crate::pgp;
#[derive(Debug)]
@@ -43,93 +40,6 @@ impl EncryptHelper {
Aheader::new(addr, pk, self.prefer_encrypt)
}
- /// Determines if we can and should encrypt.
- pub(crate) async fn should_encrypt(
- &self,
- context: &Context,
- peerstates: &[(Option, String)],
- ) -> Result {
- let is_chatmail = context.is_chatmail().await?;
- for (peerstate, _addr) in peerstates {
- if let Some(peerstate) = peerstate {
- // For chatmail we ignore the encryption preference,
- // because we can either send encrypted or not at all.
- if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset {
- continue;
- }
- }
- return Ok(false);
- }
- Ok(true)
- }
-
- /// Constructs a vector of public keys for given peerstates.
- ///
- /// In addition returns the set of recipient addresses
- /// for which there is no key available.
- ///
- /// Returns an error if there are recipients
- /// other than self, but no recipient keys are available.
- pub(crate) fn encryption_keyring(
- &self,
- context: &Context,
- verified: bool,
- peerstates: &[(Option, String)],
- ) -> Result<(Vec, BTreeSet)> {
- // Encrypt to self unconditionally,
- // even for a single-device setup.
- let mut keyring = vec![self.public_key.clone()];
- let mut missing_key_addresses = BTreeSet::new();
-
- if peerstates.is_empty() {
- return Ok((keyring, missing_key_addresses));
- }
-
- let mut verifier_addresses: Vec<&str> = Vec::new();
-
- for (peerstate, addr) in peerstates {
- if let Some(peerstate) = peerstate {
- if let Some(key) = peerstate.clone().take_key(verified) {
- keyring.push(key);
- verifier_addresses.push(addr);
- } else {
- warn!(context, "Encryption key for {addr} is missing.");
- missing_key_addresses.insert(addr.clone());
- }
- } else {
- warn!(context, "Peerstate for {addr} is missing.");
- missing_key_addresses.insert(addr.clone());
- }
- }
-
- debug_assert!(
- !keyring.is_empty(),
- "At least our own key is in the keyring"
- );
- if keyring.len() <= 1 {
- bail!("No recipient keys are available, cannot encrypt");
- }
-
- // Encrypt to secondary verified keys
- // if we also encrypt to the introducer ("verifier") of the key.
- if verified {
- for (peerstate, _addr) in peerstates {
- if let Some(peerstate) = peerstate {
- if let (Some(key), Some(verifier)) = (
- peerstate.secondary_verified_key.as_ref(),
- peerstate.secondary_verifier.as_deref(),
- ) {
- if verifier_addresses.contains(&verifier) {
- keyring.push(key.clone());
- }
- }
- }
- }
- }
-
- Ok((keyring, missing_key_addresses))
- }
-
/// Tries to encrypt the passed in `mail`.
pub async fn encrypt(
self,
@@ -177,11 +87,9 @@ mod tests {
use super::*;
use crate::chat::send_text_msg;
use crate::config::Config;
- use crate::key::DcKey;
- use crate::message::{Message, Viewtype};
- use crate::param::Param;
+ use crate::message::Message;
use crate::receive_imf::receive_imf;
- use crate::test_utils::{bob_keypair, TestContext, TestContextManager};
+ use crate::test_utils::{TestContext, TestContextManager};
mod ensure_secret_key_exists {
use super::*;
@@ -223,130 +131,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
);
}
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> {
- let mut tcm = TestContextManager::new();
- let alice = tcm.alice().await;
- let bob = tcm.bob().await;
-
- let chat_alice = alice.create_email_chat(&bob).await.id;
- let chat_bob = bob.create_email_chat(&alice).await.id;
-
- // Alice sends unencrypted message to Bob
- let mut msg = Message::new(Viewtype::Text);
- let sent = alice.send_msg(chat_alice, &mut msg).await;
-
- // Bob receives unencrypted message from Alice
- let msg = bob.recv_msg(&sent).await;
- assert!(!msg.get_showpadlock());
-
- let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
- .await?
- .expect("no peerstate found in the database");
- assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
-
- // Bob sends empty encrypted message to Alice
- let mut msg = Message::new(Viewtype::Text);
- let sent = bob.send_msg(chat_bob, &mut msg).await;
-
- // Alice receives an empty encrypted message from Bob.
- // This is also a regression test for previously existing bug
- // that resulted in no padlock on encrypted empty messages.
- let msg = alice.recv_msg(&sent).await;
- assert!(msg.get_showpadlock());
-
- let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net")
- .await?
- .expect("no peerstate found in the database");
- assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual);
-
- // Now Alice and Bob have established keys.
-
- // Alice sends encrypted message without Autocrypt header.
- let mut msg = Message::new(Viewtype::Text);
- msg.param.set_int(Param::SkipAutocrypt, 1);
- let sent = alice.send_msg(chat_alice, &mut msg).await;
-
- let msg = bob.recv_msg(&sent).await;
- assert!(msg.get_showpadlock());
- let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
- .await?
- .expect("no peerstate found in the database");
- assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
-
- // Alice sends plaintext message with Autocrypt header.
- let mut msg = Message::new(Viewtype::Text);
- msg.force_plaintext();
- let sent = alice.send_msg(chat_alice, &mut msg).await;
-
- let msg = bob.recv_msg(&sent).await;
- assert!(!msg.get_showpadlock());
- let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
- .await?
- .expect("no peerstate found in the database");
- assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
-
- // Alice sends plaintext message without Autocrypt header.
- let mut msg = Message::new(Viewtype::Text);
- msg.force_plaintext();
- msg.param.set_int(Param::SkipAutocrypt, 1);
- let sent = alice.send_msg(chat_alice, &mut msg).await;
-
- let msg = bob.recv_msg(&sent).await;
- assert!(!msg.get_showpadlock());
- let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
- .await?
- .expect("no peerstate found in the database");
- assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
-
- Ok(())
- }
-
- fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option, String)> {
- let addr = "bob@foo.bar";
- let pub_key = bob_keypair().public;
- let peerstate = Peerstate {
- addr: addr.into(),
- last_seen: 13,
- last_seen_autocrypt: 14,
- prefer_encrypt,
- public_key: Some(pub_key.clone()),
- public_key_fingerprint: Some(pub_key.dc_fingerprint()),
- gossip_key: Some(pub_key.clone()),
- gossip_timestamp: 15,
- gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
- verified_key: Some(pub_key.clone()),
- verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- };
- vec![(Some(peerstate), addr.to_string())]
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_should_encrypt() -> Result<()> {
- let t = TestContext::new_alice().await;
- let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
-
- let ps = new_peerstates(EncryptPreference::NoPreference);
- assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
-
- let ps = new_peerstates(EncryptPreference::Reset);
- assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
-
- let ps = new_peerstates(EncryptPreference::Mutual);
- assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
-
- // test with missing peerstate
- let ps = vec![(None, "bob@foo.bar".to_string())];
- assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
- Ok(())
- }
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
diff --git a/src/events/chatlist_events.rs b/src/events/chatlist_events.rs
index 4b612f3ee..57d096825 100644
--- a/src/events/chatlist_events.rs
+++ b/src/events/chatlist_events.rs
@@ -247,8 +247,7 @@ mod test_chatlist_events {
bob.evtracker.clear_events();
// set name
- let addr = alice_on_bob.get_addr();
- Contact::create(&bob, "Alice2", addr).await?;
+ alice_on_bob.id.set_name(&bob, "Alice2").await?;
assert!(bob.add_or_lookup_contact(&alice).await.get_display_name() == "Alice2");
wait_for_chatlist_all_items(&bob).await;
diff --git a/src/headerdef.rs b/src/headerdef.rs
index 6cfd61af6..330a4d9ba 100644
--- a/src/headerdef.rs
+++ b/src/headerdef.rs
@@ -75,6 +75,12 @@ pub enum HeaderDef {
/// for members listed in the `Chat-Group-Past-Members` field.
ChatGroupMemberTimestamps,
+ /// Space-separated PGP key fingerprints
+ /// of group members listed in the `To` field
+ /// followed by fingerprints
+ /// of past members listed in the `Chat-Group-Past-Members` field.
+ ChatGroupMemberFpr,
+
/// Duration of the attached media file.
ChatDuration,
diff --git a/src/imap.rs b/src/imap.rs
index 53198f55c..5119c61d1 100644
--- a/src/imap.rs
+++ b/src/imap.rs
@@ -1884,7 +1884,7 @@ async fn should_move_out_of_spam(
};
// No chat found.
let (from_id, blocked_contact, _origin) =
- match from_field_to_contact_id(context, &from, true)
+ match from_field_to_contact_id(context, &from, None, true, true)
.await
.context("from_field_to_contact_id")?
{
@@ -2242,7 +2242,7 @@ pub(crate) async fn prefetch_should_download(
None => return Ok(false),
};
let (_from_id, blocked_contact, origin) =
- match from_field_to_contact_id(context, &from, true).await? {
+ match from_field_to_contact_id(context, &from, None, true, true).await? {
Some(res) => res,
None => return Ok(false),
};
diff --git a/src/imex/key_transfer.rs b/src/imex/key_transfer.rs
index d7726e8d8..273508b99 100644
--- a/src/imex/key_transfer.rs
+++ b/src/imex/key_transfer.rs
@@ -290,10 +290,12 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer() -> Result<()> {
- let alice = TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ tcm.section("Alice sends Autocrypt setup message");
alice.set_config(Config::BccSelf, Some("0")).await?;
- let setup_code = initiate_key_transfer(&alice).await?;
+ let setup_code = initiate_key_transfer(alice).await?;
// Test that sending Autocrypt Setup Message enables `bcc_self`.
assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true);
@@ -301,26 +303,21 @@ mod tests {
// Get Autocrypt Setup Message.
let sent = alice.pop_sent_msg().await;
- // Alice sets up a second device.
- let alice2 = TestContext::new().await;
+ tcm.section("Alice sets up a second device");
+ let alice2 = &tcm.unconfigured().await;
alice2.set_name("alice2");
alice2.configure_addr("alice@example.org").await;
alice2.recv_msg(&sent).await;
let msg = alice2.get_last_msg().await;
assert!(msg.is_setupmessage());
- assert_eq!(
- crate::key::load_self_secret_keyring(&alice2).await?.len(),
- 0
- );
+ assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 0);
// Transfer the key.
+ tcm.section("Alice imports a key from Autocrypt Setup Message");
alice2.set_config(Config::BccSelf, Some("0")).await?;
- continue_key_transfer(&alice2, msg.id, &setup_code).await?;
+ continue_key_transfer(alice2, msg.id, &setup_code).await?;
assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true);
- assert_eq!(
- crate::key::load_self_secret_keyring(&alice2).await?.len(),
- 1
- );
+ assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 1);
// Alice sends a message to self from the new device.
let sent = alice2.send_text(msg.chat_id, "Test").await;
diff --git a/src/key.rs b/src/key.rs
index dc44f0563..c39246033 100644
--- a/src/key.rs
+++ b/src/key.rs
@@ -129,8 +129,11 @@ pub(crate) trait DcKey: Serialize + Deserializable + Clone {
fn key_id(&self) -> KeyId;
}
-pub(crate) async fn load_self_public_key(context: &Context) -> Result {
- let public_key = context
+/// Attempts to load own public key.
+///
+/// Returns `None` if no key is generated yet.
+pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result> {
+ let Some(public_key_bytes) = context
.sql
.query_row_optional(
"SELECT public_key
@@ -142,9 +145,20 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result SignedPublicKey::from_slice(&bytes),
+ .await?
+ else {
+ return Ok(None);
+ };
+ let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
+ Ok(Some(public_key))
+}
+
+/// Loads own public key.
+///
+/// If no key is generated yet, generates a new one.
+pub(crate) async fn load_self_public_key(context: &Context) -> Result {
+ match load_self_public_key_opt(context).await? {
+ Some(public_key) => Ok(public_key),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
@@ -171,6 +185,38 @@ pub(crate) async fn load_self_public_keyring(context: &Context) -> Result Result<&str> {
+ if let Some(fp) = context.self_fingerprint.get() {
+ Ok(fp)
+ } else {
+ let fp = load_self_public_key(context).await?.dc_fingerprint().hex();
+ Ok(context.self_fingerprint.get_or_init(|| fp))
+ }
+}
+
+/// Returns own public key fingerprint in (not human-readable) hex representation.
+/// This is the fingerprint format that is used in the database.
+///
+/// Returns `None` if no key is generated yet.
+///
+/// For performance reasons, the fingerprint is cached after the first invocation.
+pub(crate) async fn self_fingerprint_opt(context: &Context) -> Result> {
+ if let Some(fp) = context.self_fingerprint.get() {
+ Ok(Some(fp))
+ } else if let Some(key) = load_self_public_key_opt(context).await? {
+ let fp = key.dc_fingerprint().hex();
+ Ok(Some(context.self_fingerprint.get_or_init(|| fp)))
+ } else {
+ Ok(None)
+ }
+}
+
pub(crate) async fn load_self_secret_key(context: &Context) -> Result {
let private_key = context
.sql
diff --git a/src/lib.rs b/src/lib.rs
index 9e9cc49cc..3c6402cbf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -74,7 +74,6 @@ mod mimefactory;
pub mod mimeparser;
pub mod oauth2;
mod param;
-pub mod peerstate;
mod pgp;
pub mod provider;
pub mod qr;
diff --git a/src/log.rs b/src/log.rs
index 2a81067fa..ab6b15494 100644
--- a/src/log.rs
+++ b/src/log.rs
@@ -68,6 +68,16 @@ impl Context {
let last_error = &*self.last_error.read();
last_error.clone()
}
+
+ pub fn set_migration_error(&self, error: &str) {
+ let mut migration_error = self.migration_error.write();
+ *migration_error = Some(error.to_string());
+ }
+
+ pub fn get_migration_error(&self) -> Option {
+ let migration_error = &*self.migration_error.read();
+ migration_error.clone()
+ }
}
pub trait LogExt
diff --git a/src/message.rs b/src/message.rs
index 0f1e9da1f..91351dd6f 100644
--- a/src/message.rs
+++ b/src/message.rs
@@ -835,6 +835,7 @@ impl Message {
/// Returns true if padlock indicating message encryption should be displayed in the UI.
pub fn get_showpadlock(&self) -> bool {
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
+ || self.from_id == ContactId::DEVICE
}
/// Returns true if message is auto-generated.
diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs
index 7c4bb0734..5846012bc 100644
--- a/src/message/message_tests.rs
+++ b/src/message/message_tests.rs
@@ -1,10 +1,7 @@
use num_traits::FromPrimitive;
use super::*;
-use crate::chat::{
- self, add_contact_to_chat, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem,
- ProtectionStatus,
-};
+use crate::chat::{self, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::reaction::send_reaction;
@@ -106,7 +103,7 @@ async fn test_create_webrtc_instance_noroom() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_width_height() {
- let t = TestContext::new().await;
+ let t = TestContext::new_alice().await;
// test that get_width() and get_height() are returning some dimensions for images;
// (as the device-chat contains a welcome-images, we check that)
@@ -183,6 +180,8 @@ async fn test_no_quote() {
assert!(msg.quoted_message(bob).await.unwrap().is_none());
}
+/// Tests that quote of encrypted message
+/// cannot be sent unencrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -190,40 +189,26 @@ async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
- let alice_group = alice
- .create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob])
+ tcm.section("Bob sends encrypted message to Alice");
+ let alice_chat = alice.create_chat(bob).await;
+ let sent = alice
+ .send_text(alice_chat.id, "Hi! This is encrypted.")
.await;
- let sent = alice.send_text(alice_group, "Hi! I created a group").await;
+
let bob_received_message = bob.recv_msg(&sent).await;
+ assert_eq!(bob_received_message.get_showpadlock(), true);
- let bob_group = bob_received_message.chat_id;
- bob_group.accept(bob).await?;
- let sent = bob.send_text(bob_group, "Encrypted message").await;
- let alice_received_message = alice.recv_msg(&sent).await;
- assert!(alice_received_message.get_showpadlock());
+ // Bob quotes encrypted message in unencrypted chat.
+ let bob_email_chat = bob.create_email_chat(alice).await;
+ let mut msg = Message::new_text("I am sending an unencrypted reply.".to_string());
+ msg.set_quote(bob, Some(&bob_received_message)).await?;
+ chat::send_msg(bob, bob_email_chat.id, &mut msg).await?;
- // Alice adds contact without key so chat becomes unencrypted.
- let alice_flubby_contact_id = Contact::create(alice, "Flubby", "flubby@example.org").await?;
- add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
-
- // Alice quotes encrypted message in unencrypted chat.
- let mut msg = Message::new_text("unencrypted".to_string());
- msg.set_quote(alice, Some(&alice_received_message)).await?;
- chat::send_msg(alice, alice_group, &mut msg).await?;
-
- let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
- assert_eq!(bob_received_message.quoted_text().unwrap(), "...");
- assert_eq!(bob_received_message.get_showpadlock(), false);
-
- // Alice replaces a quote of encrypted message with a quote of unencrypted one.
- let mut msg1 = Message::new(Viewtype::Text);
- msg1.set_quote(alice, Some(&alice_received_message)).await?;
- msg1.set_quote(alice, Some(&msg)).await?;
- chat::send_msg(alice, alice_group, &mut msg1).await?;
-
- let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
- assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted");
- assert_eq!(bob_received_message.get_showpadlock(), false);
+ // Alice receives unencrypted message,
+ // but the quote of encrypted message is replaced with "...".
+ let alice_received_message = alice.recv_msg(&bob.pop_sent_msg().await).await;
+ assert_eq!(alice_received_message.quoted_text().unwrap(), "...");
+ assert_eq!(alice_received_message.get_showpadlock(), false);
Ok(())
}
@@ -424,12 +409,16 @@ async fn test_markseen_not_downloaded_msg() -> Result<()> {
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
- let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
+ let bob_chat_id = bob.create_chat(alice).await.id;
+ alice.create_chat(bob).await; // Make sure the chat is accepted.
+ tcm.section("Bob sends a large message to Alice");
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
+
+ tcm.section("Alice receives a large message from Bob");
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
diff --git a/src/mimefactory.rs b/src/mimefactory.rs
index 14639b918..d301b2a53 100644
--- a/src/mimefactory.rs
+++ b/src/mimefactory.rs
@@ -1,6 +1,6 @@
//! # MIME message production.
-use std::collections::HashSet;
+use std::collections::{BTreeSet, HashSet};
use std::io::Cursor;
use std::path::Path;
@@ -23,14 +23,14 @@ use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
-use crate::key::DcKey;
+use crate::key::self_fingerprint;
+use crate::key::{DcKey, SignedPublicKey};
use crate::location;
use crate::log::{info, warn};
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{is_hidden, SystemMessage};
use crate::param::Param;
use crate::peer_channels::create_iroh_header;
-use crate::peerstate::Peerstate;
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use crate::tools::{
@@ -88,6 +88,13 @@ pub struct MimeFactory {
/// but `MimeFactory` is not responsible for this.
recipients: Vec,
+ /// Vector of pairs of recipient
+ /// addresses and OpenPGP keys
+ /// to use for encryption.
+ ///
+ /// `None` if the message is not encrypted.
+ encryption_keys: Option>,
+
/// Vector of pairs of recipient name and address that goes into the `To` field.
///
/// The list of actual message recipient addresses may be different,
@@ -99,11 +106,18 @@ pub struct MimeFactory {
/// Vector of pairs of past group member names and addresses.
past_members: Vec<(String, String)>,
+ /// Fingerprints of the members in the same order as in the `to`
+ /// followed by `past_members`.
+ ///
+ /// If this is not empty, its length
+ /// should be the sum of `to` and `past_members` length.
+ member_fingerprints: Vec,
+
/// Timestamps of the members in the same order as in the `to`
/// followed by `past_members`.
///
/// If this is not empty, its length
- /// should be the sum of `recipients` and `past_members` length.
+ /// should be the sum of `to` and `past_members` length.
member_timestamps: Vec,
timestamp: i64,
@@ -185,12 +199,24 @@ impl MimeFactory {
let mut recipients = Vec::new();
let mut to = Vec::new();
let mut past_members = Vec::new();
+ let mut member_fingerprints = Vec::new();
let mut member_timestamps = Vec::new();
let mut recipient_ids = HashSet::new();
let mut req_mdn = false;
+ let encryption_keys;
+
+ let self_fingerprint = self_fingerprint(context).await?;
+
if chat.is_self_talk() {
to.push((from_displayname.to_string(), from_addr.to_string()));
+
+ encryption_keys = if msg.param.get_bool(Param::ForcePlaintext).unwrap_or(false) {
+ None
+ } else {
+ // Encrypt, but only to self.
+ Some(Vec::new())
+ };
} else if chat.is_mailing_list() {
let list_post = chat
.param
@@ -198,6 +224,9 @@ impl MimeFactory {
.context("Can't write to mailinglist without ListPost param")?;
to.push(("".to_string(), list_post.to_string()));
recipients.push(list_post.to_string());
+
+ // Do not encrypt messages to mailing lists.
+ encryption_keys = None;
} else {
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
msg.param.get(Param::Arg)
@@ -205,27 +234,59 @@ impl MimeFactory {
None
};
+ let is_encrypted = if msg
+ .param
+ .get_bool(Param::ForcePlaintext)
+ .unwrap_or_default()
+ {
+ false
+ } else {
+ msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default()
+ || chat.is_encrypted(context).await?
+ };
+
+ let mut keys = Vec::new();
+ let mut missing_key_addresses = BTreeSet::new();
context
.sql
.query_map(
- "SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp
+ "SELECT
+ c.authname,
+ c.addr,
+ c.fingerprint,
+ c.id,
+ cc.add_timestamp,
+ cc.remove_timestamp,
+ k.public_key
FROM chats_contacts cc
LEFT JOIN contacts c ON cc.contact_id=c.id
- WHERE cc.chat_id=? AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
+ LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint
+ WHERE cc.chat_id=?
+ AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
(msg.chat_id, chat.typ == Chattype::Group),
|row| {
let authname: String = row.get(0)?;
let addr: String = row.get(1)?;
- let id: ContactId = row.get(2)?;
- let add_timestamp: i64 = row.get(3)?;
- let remove_timestamp: i64 = row.get(4)?;
- Ok((authname, addr, id, add_timestamp, remove_timestamp))
+ let fingerprint: String = row.get(2)?;
+ let id: ContactId = row.get(3)?;
+ let add_timestamp: i64 = row.get(4)?;
+ let remove_timestamp: i64 = row.get(5)?;
+ let public_key_bytes_opt: Option> = row.get(6)?;
+ Ok((authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt))
},
|rows| {
let mut past_member_timestamps = Vec::new();
+ let mut past_member_fingerprints = Vec::new();
for row in rows {
- let (authname, addr, id, add_timestamp, remove_timestamp) = row?;
+ let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?;
+
+ let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
+ Some(SignedPublicKey::from_slice(public_key_bytes)?)
+ } else {
+ None
+ };
+
let addr = if id == ContactId::SELF {
from_addr.to_string()
} else {
@@ -237,13 +298,34 @@ impl MimeFactory {
};
if add_timestamp >= remove_timestamp {
if !recipients_contain_addr(&to, &addr) {
- recipients.push(addr.clone());
+ if id != ContactId::SELF {
+ recipients.push(addr.clone());
+ }
if !undisclosed_recipients {
- to.push((name, addr));
+ to.push((name, addr.clone()));
+
+ if is_encrypted {
+ if !fingerprint.is_empty() {
+ member_fingerprints.push(fingerprint);
+ } else if id == ContactId::SELF {
+ member_fingerprints.push(self_fingerprint.to_string());
+ } else {
+ debug_assert!(member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
+ }
+ }
member_timestamps.push(add_timestamp);
}
}
recipient_ids.insert(id);
+
+ if let Some(public_key) = public_key_opt {
+ keys.push((addr.clone(), public_key))
+ } else if id != ContactId::SELF {
+ missing_key_addresses.insert(addr.clone());
+ if is_encrypted {
+ warn!(context, "Missing key for {addr}");
+ }
+ }
} else if remove_timestamp.saturating_add(60 * 24 * 3600) > now {
// Row is a tombstone,
// member is not actually part of the group.
@@ -253,27 +335,57 @@ impl MimeFactory {
// This is a "member removed" message,
// we need to notify removed member
// that it was removed.
- recipients.push(addr.clone());
+ if id != ContactId::SELF {
+ recipients.push(addr.clone());
+ }
+
+ if let Some(public_key) = public_key_opt {
+ keys.push((addr.clone(), public_key))
+ } else if id != ContactId::SELF {
+ missing_key_addresses.insert(addr.clone());
+ if is_encrypted {
+ warn!(context, "Missing key for {addr}");
+ }
+ }
}
}
if !undisclosed_recipients {
- past_members.push((name, addr));
+ past_members.push((name, addr.clone()));
past_member_timestamps.push(remove_timestamp);
+
+ if is_encrypted {
+ if !fingerprint.is_empty() {
+ past_member_fingerprints.push(fingerprint);
+ } else if id == ContactId::SELF {
+ // It's fine to have self in past members
+ // if we are leaving the group.
+ past_member_fingerprints.push(self_fingerprint.to_string());
+ } else {
+ debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
+ }
+ }
}
}
}
}
debug_assert!(member_timestamps.len() >= to.len());
+ debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
to.remove(position);
member_timestamps.remove(position);
+ if is_encrypted {
+ member_fingerprints.remove(position);
+ }
}
}
member_timestamps.extend(past_member_timestamps);
+ if is_encrypted {
+ member_fingerprints.extend(past_member_fingerprints);
+ }
Ok(())
},
)
@@ -287,7 +399,26 @@ impl MimeFactory {
{
req_mdn = true;
}
+
+ encryption_keys = if !is_encrypted {
+ None
+ } else {
+ if keys.is_empty() && !recipients.is_empty() {
+ bail!(
+ "No recipient keys are available, cannot encrypt to {:?}.",
+ recipients
+ );
+ }
+
+ // Remove recipients for which the key is missing.
+ if !missing_key_addresses.is_empty() {
+ recipients.retain(|addr| !missing_key_addresses.contains(addr));
+ }
+
+ Some(keys)
+ };
}
+
let (in_reply_to, references) = context
.sql
.query_row(
@@ -320,14 +451,17 @@ impl MimeFactory {
member_timestamps.is_empty()
|| to.len() + past_members.len() == member_timestamps.len()
);
+
let factory = MimeFactory {
from_addr,
from_displayname,
sender_displayname,
selfstatus,
recipients,
+ encryption_keys,
to,
past_members,
+ member_fingerprints,
member_timestamps,
timestamp: msg.timestamp_sort,
loaded: Loaded::Message { msg, chat },
@@ -351,14 +485,27 @@ impl MimeFactory {
let from_addr = context.get_primary_self_addr().await?;
let timestamp = create_smeared_timestamp(context);
+ let addr = contact.get_addr().to_string();
+ let encryption_keys = if contact.is_key_contact() {
+ if let Some(key) = contact.public_key(context).await? {
+ Some(vec![(addr.clone(), key)])
+ } else {
+ Some(Vec::new())
+ }
+ } else {
+ None
+ };
+
let res = MimeFactory {
from_addr,
from_displayname: "".to_string(),
sender_displayname: None,
selfstatus: "".to_string(),
- recipients: vec![contact.get_addr().to_string()],
+ recipients: vec![addr],
+ encryption_keys,
to: vec![("".to_string(), contact.get_addr().to_string())],
past_members: vec![],
+ member_fingerprints: vec![],
member_timestamps: vec![],
timestamp,
loaded: Loaded::Mdn {
@@ -376,57 +523,6 @@ impl MimeFactory {
Ok(res)
}
- async fn peerstates_for_recipients(
- &self,
- context: &Context,
- ) -> Result, String)>> {
- let self_addr = context.get_primary_self_addr().await?;
-
- let mut res = Vec::new();
- for addr in self.recipients.iter().filter(|&addr| *addr != self_addr) {
- res.push((Peerstate::from_addr(context, addr).await?, addr.clone()));
- }
-
- Ok(res)
- }
-
- fn is_e2ee_guaranteed(&self) -> bool {
- match &self.loaded {
- Loaded::Message { chat, msg } => {
- !msg.param
- .get_bool(Param::ForcePlaintext)
- .unwrap_or_default()
- && (chat.is_protected()
- || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default())
- }
- Loaded::Mdn { .. } => false,
- }
- }
-
- fn verified(&self) -> bool {
- match &self.loaded {
- Loaded::Message { chat, msg } => {
- chat.is_self_talk() ||
- // Securejoin messages are supposed to verify a key.
- // In order to do this, it is necessary that they can be sent
- // to a key that is not yet verified.
- // This has to work independently of whether the chat is protected right now.
- chat.is_protected() && msg.get_info_type() != SystemMessage::SecurejoinMessage
- }
- Loaded::Mdn { .. } => false,
- }
- }
-
- fn should_force_plaintext(&self) -> bool {
- match &self.loaded {
- Loaded::Message { msg, .. } => msg
- .param
- .get_bool(Param::ForcePlaintext)
- .unwrap_or_default(),
- Loaded::Mdn { .. } => false,
- }
- }
-
fn should_skip_autocrypt(&self) -> bool {
match &self.loaded {
Loaded::Message { msg, .. } => {
@@ -602,21 +698,35 @@ impl MimeFactory {
}
if let Loaded::Message { chat, .. } = &self.loaded {
- if chat.typ == Chattype::Group
- && !self.member_timestamps.is_empty()
- && !chat.member_list_is_stale(context).await?
- {
- headers.push((
- "Chat-Group-Member-Timestamps",
- mail_builder::headers::raw::Raw::new(
- self.member_timestamps
- .iter()
- .map(|ts| ts.to_string())
- .collect::>()
- .join(" "),
- )
- .into(),
- ));
+ if chat.typ == Chattype::Group {
+ if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await?
+ {
+ headers.push((
+ "Chat-Group-Member-Timestamps",
+ mail_builder::headers::raw::Raw::new(
+ self.member_timestamps
+ .iter()
+ .map(|ts| ts.to_string())
+ .collect::>()
+ .join(" "),
+ )
+ .into(),
+ ));
+ }
+
+ if !self.member_fingerprints.is_empty() {
+ headers.push((
+ "Chat-Group-Member-Fpr",
+ mail_builder::headers::raw::Raw::new(
+ self.member_fingerprints
+ .iter()
+ .map(|fp| fp.to_string())
+ .collect::>()
+ .join(" "),
+ )
+ .into(),
+ ));
+ }
}
}
@@ -727,10 +837,8 @@ impl MimeFactory {
));
}
- let verified = self.verified();
let grpimage = self.grpimage();
let skip_autocrypt = self.should_skip_autocrypt();
- let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(context).await?;
if !skip_autocrypt {
@@ -742,6 +850,8 @@ impl MimeFactory {
));
}
+ let is_encrypted = self.encryption_keys.is_some();
+
// Add ephemeral timer for non-MDN messages.
// For MDNs it does not matter because they are not visible
// and ignored by the receiver.
@@ -755,9 +865,6 @@ impl MimeFactory {
}
}
- let peerstates = self.peerstates_for_recipients(context).await?;
- let is_encrypted = !self.should_force_plaintext()
- && (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?);
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
} else {
@@ -904,7 +1011,7 @@ impl MimeFactory {
}
}
- let outer_message = if is_encrypted {
+ let outer_message = if let Some(encryption_keys) = self.encryption_keys {
// Store protected headers in the inner message.
let message = protected_headers
.into_iter()
@@ -921,7 +1028,7 @@ impl MimeFactory {
// Add gossip headers in chats with multiple recipients
let multiple_recipients =
- peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
+ encryption_keys.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
let now = time();
@@ -929,11 +1036,7 @@ impl MimeFactory {
match &self.loaded {
Loaded::Message { chat, msg } => {
if chat.typ != Chattype::Broadcast {
- for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
- let Some(key) = peerstate.peek_key(verified) else {
- continue;
- };
-
+ for (addr, key) in &encryption_keys {
let fingerprint = key.dc_fingerprint().hex();
let cmd = msg.param.get_cmd();
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
@@ -965,7 +1068,7 @@ impl MimeFactory {
}
let header = Aheader::new(
- peerstate.addr.clone(),
+ addr.clone(),
key.clone(),
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
@@ -1015,8 +1118,10 @@ impl MimeFactory {
Loaded::Mdn { .. } => true,
};
- let (encryption_keyring, missing_key_addresses) =
- encrypt_helper.encryption_keyring(context, verified, &peerstates)?;
+ // Encrypt to self unconditionally,
+ // even for a single-device setup.
+ let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
+ encryption_keyring.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
// XXX: additional newline is needed
// to pass filtermail at
@@ -1026,12 +1131,6 @@ impl MimeFactory {
.await?
+ "\n";
- // Remove recipients for which the key is missing.
- if !missing_key_addresses.is_empty() {
- self.recipients
- .retain(|addr| !missing_key_addresses.contains(addr));
- }
-
// Set the appropriate Content-Type for the outer message
MimePart::new(
"multipart/encrypted; protocol=\"application/pgp-encrypted\"",
@@ -1257,6 +1356,8 @@ impl MimeFactory {
}
}
SystemMessage::MemberAddedToGroup => {
+ // TODO: lookup the contact by ID rather than email address.
+ // We are adding key-contacts, the cannot be looked up by address.
let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
placeholdertext =
Some(stock_str::msg_add_member_remote(context, email_to_add).await);
@@ -1561,7 +1662,7 @@ impl MimeFactory {
// we do not piggyback sync-files to other self-sent-messages
// to not risk files becoming too larger and being skipped by download-on-demand.
- if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
+ if command == SystemMessage::MultiDeviceSync {
let json = msg.param.get(Param::Arg).unwrap_or_default();
let ids = msg.param.get(Param::Arg2).unwrap_or_default();
parts.push(context.build_sync_part(json.to_string()));
diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs
index 22306ddeb..97c1ee788 100644
--- a/src/mimefactory/mimefactory_tests.rs
+++ b/src/mimefactory/mimefactory_tests.rs
@@ -339,39 +339,31 @@ async fn test_subject_in_group() -> Result<()> {
}
// 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 bob = TestContext::new_bob().await;
- let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä
+ let mut tcm = TestContextManager::new();
+ let t = tcm.alice().await;
+ let bob = tcm.bob().await;
+ let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname")
.await
.unwrap();
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
- let subject = send_msg_get_subject(&t, group_id, None).await?;
- assert_eq!(subject, "groupname");
+ let sent_message = t.send_text(group_id, "Hello!").await;
+ let bob_received_message = bob.recv_msg(&sent_message).await;
+ let bob_group_id = bob_received_message.chat_id;
+ bob_group_id.accept(&bob).await.unwrap();
+ assert_eq!(get_subject(&t, sent_message).await?, "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 mut msg = Message::new(Viewtype::Text);
+ msg.set_subject("Different subject".to_string());
+ let bob_sent_msg = bob.send_msg(bob_group_id, &mut msg).await;
+ let message_from_bob = t.recv_msg(&bob_sent_msg).await;
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "Re: groupname");
@@ -623,43 +615,6 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
Ok(())
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_group_avatar_unencrypted() -> anyhow::Result<()> {
- let t = &TestContext::new_alice().await;
- let group_id = chat::create_group_chat(t, chat::ProtectionStatus::Unprotected, "Group")
- .await
- .unwrap();
- let bob = Contact::create(t, "", "bob@example.org").await?;
- chat::add_contact_to_chat(t, group_id, bob).await?;
-
- let file = t.dir.path().join("avatar.png");
- let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
- tokio::fs::write(&file, bytes).await?;
- chat::set_chat_profile_image(t, group_id, file.to_str().unwrap()).await?;
-
- // Send message to bob: that should get multipart/mixed because of the avatar moved to inner header.
- let mut msg = Message::new_text("this is the text!".to_string());
- let sent_msg = t.send_msg(group_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-Group-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-Group-Avatar:").count(), 1);
-
- 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
diff --git a/src/mimeparser.rs b/src/mimeparser.rs
index 92728c463..21600aa07 100644
--- a/src/mimeparser.rs
+++ b/src/mimeparser.rs
@@ -13,7 +13,7 @@ use format_flowed::unformat_flowed;
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use mime::Mime;
-use crate::aheader::{Aheader, EncryptPreference};
+use crate::aheader::Aheader;
use crate::authres::handle_authres;
use crate::blob::BlobObject;
use crate::chat::ChatId;
@@ -21,10 +21,7 @@ use crate::config::Config;
use crate::constants;
use crate::contact::ContactId;
use crate::context::Context;
-use crate::decrypt::{
- get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
- validate_detached_signature,
-};
+use crate::decrypt::{try_decrypt, validate_detached_signature};
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
@@ -32,7 +29,6 @@ use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPubli
use crate::log::{error, info, warn};
use crate::message::{self, get_vcard_summary, set_msg_failed, Message, MsgId, Viewtype};
use crate::param::{Param, Params};
-use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::sync::SyncItems;
use crate::tools::{
@@ -72,17 +68,12 @@ pub(crate) struct MimeMessage {
/// `From:` address.
pub from: SingleInfo,
- /// Whether the From address was repeated in the signed part
- /// (and we know that the signer intended to send from this address)
- pub from_is_signed: bool,
/// Whether the message is incoming or outgoing (self-sent).
pub incoming: bool,
/// The List-Post address is only set for mailing lists. Users can send
/// messages to this address to post them to the list.
pub list_post: Option,
pub chat_disposition_notification_to: Option,
- pub autocrypt_header: Option,
- pub peerstate: Option,
pub decrypting_failed: bool,
/// Set of valid signature fingerprints if a message is an
@@ -91,11 +82,16 @@ pub(crate) struct MimeMessage {
/// If a message is not encrypted or the signature is not valid,
/// this set is empty.
pub signatures: HashSet,
- /// The mail recipient addresses for which gossip headers were applied
- /// and their respective gossiped keys,
- /// regardless of whether they modified any peerstates.
+
+ /// The addresses for which there was a gossip header
+ /// and their respective gossiped keys.
pub gossiped_keys: HashMap,
+ /// Fingerprint of the key in the Autocrypt header.
+ ///
+ /// It is not verified that the sender can use this key.
+ pub autocrypt_fingerprint: Option,
+
/// True if the message is a forwarded message.
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
@@ -118,8 +114,7 @@ pub(crate) struct MimeMessage {
/// MIME message in this case.
pub is_mime_modified: bool,
- /// Decrypted, raw MIME structure. Nonempty iff `is_mime_modified` and the message was actually
- /// encrypted.
+ /// Decrypted raw MIME structure.
pub decoded_data: Vec,
/// Hop info for debugging.
@@ -260,7 +255,7 @@ impl MimeMessage {
);
headers.retain(|k, _| {
!is_hidden(k) || {
- headers_removed.insert(k.clone());
+ headers_removed.insert(k.to_string());
false
}
});
@@ -326,12 +321,9 @@ impl MimeMessage {
let mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?;
- let allow_aeap = get_encrypted_mime(&mail).is_some();
-
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
let mut gossiped_keys = Default::default();
- let mut from_is_signed = false;
hop_info += "\n\n";
hop_info += &dkim_results.to_string();
@@ -407,19 +399,37 @@ impl MimeMessage {
None
};
- // The peerstate that will be used to validate the signatures.
- let mut peerstate = get_autocrypt_peerstate(
- context,
- &from.addr,
- autocrypt_header.as_ref(),
- timestamp_sent,
- allow_aeap,
- )
- .await?;
+ let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
+ let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
+ let inserted = context
+ .sql
+ .execute(
+ "INSERT INTO public_keys (fingerprint, public_key)
+ VALUES (?, ?)
+ ON CONFLICT (fingerprint)
+ DO NOTHING",
+ (&fingerprint, autocrypt_header.public_key.to_bytes()),
+ )
+ .await?;
+ if inserted > 0 {
+ info!(
+ context,
+ "Saved key with fingerprint {fingerprint} from the Autocrypt header"
+ );
+ }
+ Some(fingerprint)
+ } else {
+ None
+ };
- let public_keyring = match peerstate.is_none() && !incoming {
- true => key::load_self_public_keyring(context).await?,
- false => keyring_from_peerstate(peerstate.as_ref()),
+ let public_keyring = if incoming {
+ if let Some(autocrypt_header) = autocrypt_header {
+ vec![autocrypt_header.public_key]
+ } else {
+ vec![]
+ }
+ } else {
+ key::load_self_public_keyring(context).await?
};
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
@@ -482,14 +492,8 @@ impl MimeMessage {
// but only if the mail was correctly signed. Probably it's ok to not require
// encryption here, but let's follow the standard.
let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
- gossiped_keys = update_gossip_peerstates(
- context,
- timestamp_sent,
- &from.addr,
- &recipients,
- gossip_headers,
- )
- .await?;
+ gossiped_keys =
+ parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
}
if let Some(inner_from) = inner_from {
@@ -514,30 +518,14 @@ impl MimeMessage {
bail!("From header is forged");
}
from = inner_from;
- from_is_signed = !signatures.is_empty();
}
}
if signatures.is_empty() {
Self::remove_secured_headers(&mut headers, &mut headers_removed);
-
- // If it is not a read receipt, degrade encryption.
- if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
- if timestamp_sent > peerstate.last_seen_autocrypt
- && mail.ctype.mimetype != "multipart/report"
- {
- peerstate.degrade_encryption(timestamp_sent);
- }
- }
}
if !is_encrypted {
signatures.clear();
}
- if let Some(peerstate) = &mut peerstate {
- if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
- peerstate.prefer_encrypt = EncryptPreference::Mutual;
- peerstate.save_to_db(&context.sql).await?;
- }
- }
let mut parser = MimeMessage {
parts: Vec::new(),
@@ -549,15 +537,13 @@ impl MimeMessage {
past_members,
list_post,
from,
- from_is_signed,
incoming,
chat_disposition_notification_to,
- autocrypt_header,
- peerstate,
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
signatures,
+ autocrypt_fingerprint,
gossiped_keys,
is_forwarded: false,
mdn_reports: Vec::new(),
@@ -620,10 +606,7 @@ impl MimeMessage {
parser.maybe_remove_inline_mailinglist_footer();
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context).await?;
-
- if parser.is_mime_modified {
- parser.decoded_data = mail_raw;
- }
+ parser.decoded_data = mail_raw;
Ok(parser)
}
@@ -1338,14 +1321,13 @@ impl MimeMessage {
if decoded_data.is_empty() {
return Ok(());
}
- if let Some(peerstate) = &mut self.peerstate {
- if peerstate.prefer_encrypt != EncryptPreference::Mutual
- && mime_type.type_() == mime::APPLICATION
- && mime_type.subtype().as_str() == "pgp-keys"
- && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await?
- {
- return Ok(());
- }
+
+ // Process attached PGP keys.
+ if mime_type.type_() == mime::APPLICATION
+ && mime_type.subtype().as_str() == "pgp-keys"
+ && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
+ {
+ return Ok(());
}
let mut part = Part::default();
let msg_type = if context
@@ -1438,10 +1420,9 @@ impl MimeMessage {
Ok(())
}
- /// Returns whether a key from the attachment was set as peer's pubkey.
+ /// Returns whether a key from the attachment was saved.
async fn try_set_peer_key_from_file_part(
context: &Context,
- peerstate: &mut Peerstate,
decoded_data: &[u8],
) -> Result {
let key = match str::from_utf8(decoded_data) {
@@ -1455,45 +1436,30 @@ impl MimeMessage {
Err(err) => {
warn!(
context,
- "PGP key attachment is not an ASCII-armored file: {:#}", err
+ "PGP key attachment is not an ASCII-armored file: {err:#}."
);
return Ok(false);
}
Ok((key, _)) => key,
};
if let Err(err) = key.verify() {
- warn!(context, "attached PGP key verification failed: {}", err);
+ warn!(context, "Attached PGP key verification failed: {err:#}.");
return Ok(false);
}
- if !key.details.users.iter().any(|user| {
- user.id
- .id()
- .ends_with((String::from("<") + &peerstate.addr + ">").as_bytes())
- }) {
- return Ok(false);
- }
- if let Some(curr_key) = &peerstate.public_key {
- if key != *curr_key && peerstate.prefer_encrypt != EncryptPreference::Reset {
- // We don't want to break the existing Autocrypt setup. Yes, it's unlikely that a
- // user have an Autocrypt-capable MUA and also attaches a key, but if that's the
- // case, let 'em first disable Autocrypt and then change the key by attaching it.
- warn!(
- context,
- "not using attached PGP key for peer '{}' because another one is already set \
- with prefer-encrypt={}",
- peerstate.addr,
- peerstate.prefer_encrypt,
- );
- return Ok(false);
- }
- }
- peerstate.public_key = Some(key);
- info!(
- context,
- "using attached PGP key for peer '{}' with prefer-encrypt=mutual", peerstate.addr,
- );
- peerstate.prefer_encrypt = EncryptPreference::Mutual;
- peerstate.save_to_db(&context.sql).await?;
+
+ let fingerprint = key.dc_fingerprint().hex();
+ context
+ .sql
+ .execute(
+ "INSERT INTO public_keys (fingerprint, public_key)
+ VALUES (?, ?)
+ ON CONFLICT (fingerprint)
+ DO NOTHING",
+ (&fingerprint, key.to_bytes()),
+ )
+ .await?;
+
+ info!(context, "Imported PGP key {fingerprint} from attachment.");
Ok(true)
}
@@ -1887,6 +1853,19 @@ impl MimeMessage {
.collect()
})
}
+
+ /// Returns list of fingerprints from
+ /// `Chat-Group-Member-Fpr` header.
+ pub fn chat_group_member_fingerprints(&self) -> Vec {
+ if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
+ header
+ .split_ascii_whitespace()
+ .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
+ .collect()
+ } else {
+ Vec::new()
+ }
+ }
}
fn remove_header(
@@ -1902,14 +1881,13 @@ fn remove_header(
}
}
-/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
-/// Params:
-/// from: The address which sent the message currently being parsed
+/// Parses `Autocrypt-Gossip` headers from the email,
+/// saves the keys into the `public_keys` table,
+/// and returns them in a HashMap.
///
-/// Returns the set of mail recipient addresses for which valid gossip headers were found.
-async fn update_gossip_peerstates(
+/// * `from`: The address which sent the message currently being parsed
+async fn parse_gossip_headers(
context: &Context,
- message_time: i64,
from: &str,
recipients: &[SingleInfo],
gossip_headers: Vec,
@@ -1937,7 +1915,7 @@ async fn update_gossip_peerstates(
continue;
}
if addr_cmp(from, &header.addr) {
- // Non-standard, but anyway we can't update the cached peerstate here.
+ // Non-standard, might not be necessary to have this check here
warn!(
context,
"Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
@@ -1945,18 +1923,16 @@ async fn update_gossip_peerstates(
continue;
}
- let peerstate;
- if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
- p.apply_gossip(&header, message_time);
- p.save_to_db(&context.sql).await?;
- peerstate = p;
- } else {
- let p = Peerstate::from_gossip(&header, message_time);
- p.save_to_db(&context.sql).await?;
- peerstate = p;
- };
- peerstate
- .handle_fingerprint_change(context, message_time)
+ let fingerprint = header.public_key.dc_fingerprint().hex();
+ context
+ .sql
+ .execute(
+ "INSERT INTO public_keys (fingerprint, public_key)
+ VALUES (?, ?)
+ ON CONFLICT (fingerprint)
+ DO NOTHING",
+ (&fingerprint, header.public_key.to_bytes()),
+ )
.await?;
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
diff --git a/src/param.rs b/src/param.rs
index 9500778a2..8fb5448fc 100644
--- a/src/param.rs
+++ b/src/param.rs
@@ -98,6 +98,11 @@ pub enum Param {
Cmd = b'S',
/// For Messages
+ ///
+ /// For "MemberRemovedFromGroup" this is the email address
+ /// removed from the group.
+ ///
+ /// For "MemberAddedToGroup" this is the email address added to the group.
Arg = b'E',
/// For Messages
diff --git a/src/peerstate.rs b/src/peerstate.rs
deleted file mode 100644
index 997bba6db..000000000
--- a/src/peerstate.rs
+++ /dev/null
@@ -1,1064 +0,0 @@
-//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module.
-
-use std::mem;
-
-use anyhow::{Context as _, Error, Result};
-use deltachat_contact_tools::{addr_cmp, ContactAddress};
-use num_traits::FromPrimitive;
-
-use crate::aheader::{Aheader, EncryptPreference};
-use crate::chat::{self, Chat};
-use crate::chatlist::Chatlist;
-use crate::config::Config;
-use crate::constants::Chattype;
-use crate::contact::{Contact, Origin};
-use crate::context::Context;
-use crate::events::EventType;
-use crate::key::{DcKey, Fingerprint, SignedPublicKey};
-use crate::log::{info, warn};
-use crate::message::Message;
-use crate::mimeparser::SystemMessage;
-use crate::sql::Sql;
-use crate::{chatlist_events, stock_str};
-
-/// Type of the public key stored inside the peerstate.
-#[derive(Debug)]
-pub enum PeerstateKeyType {
- /// Public key sent in the `Autocrypt-Gossip` header.
- GossipKey,
-
- /// Public key sent in the `Autocrypt` header.
- PublicKey,
-}
-
-/// Peerstate represents the state of an Autocrypt peer.
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct Peerstate {
- /// E-mail address of the contact.
- pub addr: String,
-
- /// Timestamp of the latest peerstate update.
- ///
- /// Updated when a message is received from a contact,
- /// either with or without `Autocrypt` header.
- pub last_seen: i64,
-
- /// Timestamp of the latest `Autocrypt` header reception.
- pub last_seen_autocrypt: i64,
-
- /// Encryption preference of the contact.
- pub prefer_encrypt: EncryptPreference,
-
- /// Public key of the contact received in `Autocrypt` header.
- pub public_key: Option,
-
- /// Fingerprint of the contact public key.
- pub public_key_fingerprint: Option,
-
- /// Public key of the contact received in `Autocrypt-Gossip` header.
- pub gossip_key: Option,
-
- /// Timestamp of the latest `Autocrypt-Gossip` header reception.
- ///
- /// It is stored to avoid applying outdated gossiped key
- /// from delayed or reordered messages.
- pub gossip_timestamp: i64,
-
- /// Fingerprint of the contact gossip key.
- pub gossip_key_fingerprint: Option,
-
- /// Public key of the contact at the time it was verified,
- /// either directly or via gossip from the verified contact.
- pub verified_key: Option,
-
- /// Fingerprint of the verified public key.
- pub verified_key_fingerprint: Option,
-
- /// The address that introduced this verified key.
- pub verifier: Option,
-
- /// Secondary public verified key of the contact.
- /// It could be a contact gossiped by another verified contact in a shared group
- /// or a key that was previously used as a verified key.
- pub secondary_verified_key: Option,
-
- /// Fingerprint of the secondary verified public key.
- pub secondary_verified_key_fingerprint: Option,
-
- /// The address that introduced secondary verified key.
- pub secondary_verifier: Option,
-
- /// Row ID of the key in the `keypairs` table
- /// that we think the peer knows as verified.
- pub backward_verified_key_id: Option,
-
- /// True if it was detected
- /// that the fingerprint of the key used in chats with
- /// opportunistic encryption was changed after Peerstate creation.
- pub fingerprint_changed: bool,
-}
-
-impl Peerstate {
- /// Creates a peerstate from the `Autocrypt` header.
- pub fn from_header(header: &Aheader, message_time: i64) -> Self {
- Self::from_public_key(
- &header.addr,
- message_time,
- header.prefer_encrypt,
- &header.public_key,
- )
- }
-
- /// Creates a peerstate from the given public key.
- pub fn from_public_key(
- addr: &str,
- last_seen: i64,
- prefer_encrypt: EncryptPreference,
- public_key: &SignedPublicKey,
- ) -> Self {
- Peerstate {
- addr: addr.to_string(),
- last_seen,
- last_seen_autocrypt: last_seen,
- prefer_encrypt,
- public_key: Some(public_key.clone()),
- public_key_fingerprint: Some(public_key.dc_fingerprint()),
- gossip_key: None,
- gossip_key_fingerprint: None,
- gossip_timestamp: 0,
- verified_key: None,
- verified_key_fingerprint: None,
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- }
- }
-
- /// Create a peerstate from the `Autocrypt-Gossip` header.
- pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self {
- Peerstate {
- addr: gossip_header.addr.clone(),
- last_seen: 0,
- last_seen_autocrypt: 0,
-
- // Non-standard extension. According to Autocrypt 1.1.0 gossip headers SHOULD NOT
- // contain encryption preference.
- //
- // Delta Chat includes encryption preference to ensure new users introduced to a group
- // learn encryption preferences of other members immediately and don't send unencrypted
- // messages to a group where everyone prefers encryption.
- prefer_encrypt: gossip_header.prefer_encrypt,
- public_key: None,
- public_key_fingerprint: None,
- gossip_key: Some(gossip_header.public_key.clone()),
- gossip_key_fingerprint: Some(gossip_header.public_key.dc_fingerprint()),
- gossip_timestamp: message_time,
- verified_key: None,
- verified_key_fingerprint: None,
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- }
- }
-
- /// Loads peerstate corresponding to the given address from the database.
- pub async fn from_addr(context: &Context, addr: &str) -> Result> {
- if context.is_self_addr(addr).await? {
- return Ok(None);
- }
- let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
- gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
- verified_key, verified_key_fingerprint, \
- verifier, \
- secondary_verified_key, secondary_verified_key_fingerprint, \
- secondary_verifier, \
- backward_verified_key_id \
- FROM acpeerstates \
- WHERE addr=? COLLATE NOCASE LIMIT 1;";
- Self::from_stmt(context, query, (addr,)).await
- }
-
- /// Loads peerstate corresponding to the given fingerprint from the database.
- pub async fn from_fingerprint(
- context: &Context,
- fingerprint: &Fingerprint,
- ) -> Result > {
- // NOTE: If it's our key fingerprint, this returns None currently.
- let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
- gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
- verified_key, verified_key_fingerprint, \
- verifier, \
- secondary_verified_key, secondary_verified_key_fingerprint, \
- secondary_verifier, \
- backward_verified_key_id \
- FROM acpeerstates \
- WHERE public_key_fingerprint=? \
- OR gossip_key_fingerprint=? \
- ORDER BY public_key_fingerprint=? DESC LIMIT 1;";
- let fp = fingerprint.hex();
- Self::from_stmt(context, query, (&fp, &fp, &fp)).await
- }
-
- /// Loads peerstate by address or verified fingerprint.
- ///
- /// If the address is different but verified fingerprint is the same,
- /// peerstate with corresponding verified fingerprint is preferred.
- pub async fn from_verified_fingerprint_or_addr(
- context: &Context,
- fingerprint: &Fingerprint,
- addr: &str,
- ) -> Result > {
- if context.is_self_addr(addr).await? {
- return Ok(None);
- }
- let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
- gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
- verified_key, verified_key_fingerprint, \
- verifier, \
- secondary_verified_key, secondary_verified_key_fingerprint, \
- secondary_verifier, \
- backward_verified_key_id \
- FROM acpeerstates \
- WHERE verified_key_fingerprint=? \
- OR addr=? COLLATE NOCASE \
- ORDER BY verified_key_fingerprint=? DESC, addr=? COLLATE NOCASE DESC, \
- last_seen DESC LIMIT 1;";
- let fp = fingerprint.hex();
- Self::from_stmt(context, query, (&fp, addr, &fp, addr)).await
- }
-
- async fn from_stmt(
- context: &Context,
- query: &str,
- params: impl rusqlite::Params + Send,
- ) -> Result > {
- let peerstate = context
- .sql
- .query_row_optional(query, params, |row| {
- let res = Peerstate {
- addr: row.get("addr")?,
- last_seen: row.get("last_seen")?,
- last_seen_autocrypt: row.get("last_seen_autocrypt")?,
- prefer_encrypt: EncryptPreference::from_i32(row.get("prefer_encrypted")?)
- .unwrap_or_default(),
- public_key: row
- .get("public_key")
- .ok()
- .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()),
- public_key_fingerprint: row
- .get::<_, Option>("public_key_fingerprint")?
- .map(|s| s.parse::())
- .transpose()
- .unwrap_or_default(),
- gossip_key: row
- .get("gossip_key")
- .ok()
- .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()),
- gossip_key_fingerprint: row
- .get::<_, Option>("gossip_key_fingerprint")?
- .map(|s| s.parse::())
- .transpose()
- .unwrap_or_default(),
- gossip_timestamp: row.get("gossip_timestamp")?,
- verified_key: row
- .get("verified_key")
- .ok()
- .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()),
- verified_key_fingerprint: row
- .get::<_, Option>("verified_key_fingerprint")?
- .map(|s| s.parse::())
- .transpose()
- .unwrap_or_default(),
- verifier: {
- let verifier: Option = row.get("verifier")?;
- verifier.filter(|s| !s.is_empty())
- },
- secondary_verified_key: row
- .get("secondary_verified_key")
- .ok()
- .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()),
- secondary_verified_key_fingerprint: row
- .get::<_, Option>("secondary_verified_key_fingerprint")?
- .map(|s| s.parse::())
- .transpose()
- .unwrap_or_default(),
- secondary_verifier: {
- let secondary_verifier: Option = row.get("secondary_verifier")?;
- secondary_verifier.filter(|s| !s.is_empty())
- },
- backward_verified_key_id: row.get("backward_verified_key_id")?,
- fingerprint_changed: false,
- };
-
- Ok(res)
- })
- .await?;
- Ok(peerstate)
- }
-
- /// Re-calculate `self.public_key_fingerprint` and `self.gossip_key_fingerprint`.
- /// If one of them was changed, `self.fingerprint_changed` is set to `true`.
- ///
- /// Call this after you changed `self.public_key` or `self.gossip_key`.
- pub fn recalc_fingerprint(&mut self) {
- if let Some(ref public_key) = self.public_key {
- let old_public_fingerprint = self.public_key_fingerprint.take();
- self.public_key_fingerprint = Some(public_key.dc_fingerprint());
-
- if old_public_fingerprint.is_some()
- && old_public_fingerprint != self.public_key_fingerprint
- {
- self.fingerprint_changed = true;
- }
- }
-
- if let Some(ref gossip_key) = self.gossip_key {
- let old_gossip_fingerprint = self.gossip_key_fingerprint.take();
- self.gossip_key_fingerprint = Some(gossip_key.dc_fingerprint());
-
- if old_gossip_fingerprint.is_none()
- || self.gossip_key_fingerprint.is_none()
- || old_gossip_fingerprint != self.gossip_key_fingerprint
- {
- // Warn about gossip key change only if there is no public key obtained from
- // Autocrypt header, which overrides gossip key.
- if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() {
- self.fingerprint_changed = true;
- }
- }
- }
- }
-
- /// Reset Autocrypt peerstate.
- ///
- /// Used when it is detected that the contact no longer uses Autocrypt.
- pub fn degrade_encryption(&mut self, message_time: i64) {
- self.prefer_encrypt = EncryptPreference::Reset;
- self.last_seen = message_time;
- }
-
- /// Updates peerstate according to the given `Autocrypt` header.
- pub fn apply_header(&mut self, context: &Context, header: &Aheader, message_time: i64) {
- if !addr_cmp(&self.addr, &header.addr) {
- return;
- }
-
- if message_time >= self.last_seen {
- self.last_seen = message_time;
- self.last_seen_autocrypt = message_time;
- if (header.prefer_encrypt == EncryptPreference::Mutual
- || header.prefer_encrypt == EncryptPreference::NoPreference)
- && header.prefer_encrypt != self.prefer_encrypt
- {
- self.prefer_encrypt = header.prefer_encrypt;
- }
-
- if self.public_key.as_ref() != Some(&header.public_key) {
- self.public_key = Some(header.public_key.clone());
- self.recalc_fingerprint();
- }
- } else {
- warn!(
- context,
- "Ignoring outdated Autocrypt header because message_time={} < last_seen={}.",
- message_time,
- self.last_seen
- );
- }
- }
-
- /// Updates peerstate according to the given `Autocrypt-Gossip` header.
- pub fn apply_gossip(&mut self, gossip_header: &Aheader, message_time: i64) {
- if self.addr.to_lowercase() != gossip_header.addr.to_lowercase() {
- return;
- }
-
- if message_time >= self.gossip_timestamp {
- self.gossip_timestamp = message_time;
- if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
- self.gossip_key = Some(gossip_header.public_key.clone());
- self.recalc_fingerprint();
- }
-
- // This is non-standard.
- //
- // According to Autocrypt 1.1.0 gossip headers SHOULD NOT
- // contain encryption preference, but we include it into
- // Autocrypt-Gossip and apply it one way (from
- // "nopreference" to "mutual").
- //
- // This is compatible to standard clients, because they
- // can't distinguish it from the case where we have
- // contacted the client in the past and received this
- // preference via Autocrypt header.
- if self.last_seen_autocrypt == 0
- && self.prefer_encrypt == EncryptPreference::NoPreference
- && gossip_header.prefer_encrypt == EncryptPreference::Mutual
- {
- self.prefer_encrypt = EncryptPreference::Mutual;
- }
- };
- }
-
- /// Converts the peerstate into the contact public key.
- ///
- /// Similar to [`Self::peek_key`], but consumes the peerstate and returns owned key.
- pub fn take_key(mut self, verified: bool) -> Option {
- if verified {
- self.verified_key.take()
- } else {
- self.public_key.take().or_else(|| self.gossip_key.take())
- }
- }
-
- /// Returns a reference to the contact public key.
- ///
- /// `verified` determines the required verification status of the key.
- /// If verified key is requested, returns the verified key,
- /// otherwise returns the Autocrypt key.
- ///
- /// Returned key is suitable for sending in `Autocrypt-Gossip` header.
- ///
- /// Returns `None` if there is no suitable public key.
- pub fn peek_key(&self, verified: bool) -> Option<&SignedPublicKey> {
- if verified {
- self.verified_key.as_ref()
- } else {
- self.public_key.as_ref().or(self.gossip_key.as_ref())
- }
- }
-
- /// Returns a reference to the contact's public key fingerprint.
- ///
- /// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key.
- fn peek_key_fingerprint(&self, verified: bool) -> Option<&Fingerprint> {
- if verified {
- self.verified_key_fingerprint.as_ref()
- } else {
- self.public_key_fingerprint
- .as_ref()
- .or(self.gossip_key_fingerprint.as_ref())
- }
- }
-
- /// Returns true if the key used for opportunistic encryption in the 1:1 chat
- /// is the same as the verified key.
- ///
- /// Note that verified groups always use the verified key no matter if the
- /// opportunistic key matches or not.
- pub(crate) fn is_using_verified_key(&self) -> bool {
- let verified = self.peek_key_fingerprint(true);
-
- verified.is_some() && verified == self.peek_key_fingerprint(false)
- }
-
- pub(crate) async fn is_backward_verified(&self, context: &Context) -> Result {
- let Some(backward_verified_key_id) = self.backward_verified_key_id else {
- return Ok(false);
- };
-
- let self_key_id = context.get_config_i64(Config::KeyId).await?;
-
- let backward_verified = backward_verified_key_id == self_key_id;
- Ok(backward_verified)
- }
-
- /// Set this peerstate to verified;
- /// make sure to call `self.save_to_db` to save these changes.
- ///
- /// Params:
- ///
- /// * key: The new verified key.
- /// * fingerprint: Only set to verified if the key's fingerprint matches this.
- /// * verifier:
- /// The address which introduces the given contact.
- /// If we are verifying the contact, use that contacts address.
- pub fn set_verified(
- &mut self,
- key: SignedPublicKey,
- fingerprint: Fingerprint,
- verifier: String,
- ) -> Result<()> {
- if key.dc_fingerprint() == fingerprint {
- self.verified_key = Some(key);
- self.verified_key_fingerprint = Some(fingerprint);
- self.verifier = Some(verifier);
- Ok(())
- } else {
- Err(Error::msg(format!(
- "{fingerprint} is not peer's key fingerprint",
- )))
- }
- }
-
- /// Sets the gossiped key as the secondary verified key.
- ///
- /// If gossiped key is the same as the current verified key,
- /// do nothing to avoid overwriting secondary verified key
- /// which may be different.
- pub fn set_secondary_verified_key(&mut self, gossip_key: SignedPublicKey, verifier: String) {
- let fingerprint = gossip_key.dc_fingerprint();
- if self.verified_key_fingerprint.as_ref() != Some(&fingerprint) {
- self.secondary_verified_key = Some(gossip_key);
- self.secondary_verified_key_fingerprint = Some(fingerprint);
- self.secondary_verifier = Some(verifier);
- }
- }
-
- /// Saves the peerstate to the database.
- pub async fn save_to_db(&self, sql: &Sql) -> Result<()> {
- self.save_to_db_ex(sql, None).await
- }
-
- /// Saves the peerstate to the database.
- ///
- /// * `old_addr`: Old address of the peerstate in case of an AEAP transition.
- pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> {
- let trans_fn = |t: &mut rusqlite::Transaction| {
- let verified_key_fingerprint =
- self.verified_key_fingerprint.as_ref().map(|fp| fp.hex());
- if let Some(old_addr) = old_addr {
- // We are doing an AEAP transition to the new address and the SQL INSERT below will
- // save the existing peerstate as belonging to this new address. We now need to
- // "unverify" the peerstate that belongs to the current address in case if the
- // contact later wants to move back to the current address. Otherwise the old entry
- // will be just found and updated instead of doing AEAP. We can't just delete the
- // existing peerstate as this would break encryption to it. This is critical for
- // non-verified groups -- if we can't encrypt to the old address, we can't securely
- // remove it from the group (to add the new one instead).
- //
- // NB: We check that `verified_key_fingerprint` hasn't changed to protect from
- // possible races.
- t.execute(
- "UPDATE acpeerstates
- SET verified_key=NULL, verified_key_fingerprint='', verifier=''
- WHERE addr=? AND verified_key_fingerprint=?",
- (old_addr, &verified_key_fingerprint),
- )?;
- }
- t.execute(
- "INSERT INTO acpeerstates (
- last_seen,
- last_seen_autocrypt,
- prefer_encrypted,
- public_key,
- gossip_timestamp,
- gossip_key,
- public_key_fingerprint,
- gossip_key_fingerprint,
- verified_key,
- verified_key_fingerprint,
- verifier,
- secondary_verified_key,
- secondary_verified_key_fingerprint,
- secondary_verifier,
- backward_verified_key_id,
- addr)
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
- ON CONFLICT (addr)
- DO UPDATE SET
- last_seen = excluded.last_seen,
- last_seen_autocrypt = excluded.last_seen_autocrypt,
- prefer_encrypted = excluded.prefer_encrypted,
- public_key = excluded.public_key,
- gossip_timestamp = excluded.gossip_timestamp,
- gossip_key = excluded.gossip_key,
- public_key_fingerprint = excluded.public_key_fingerprint,
- gossip_key_fingerprint = excluded.gossip_key_fingerprint,
- verified_key = excluded.verified_key,
- verified_key_fingerprint = excluded.verified_key_fingerprint,
- verifier = excluded.verifier,
- secondary_verified_key = excluded.secondary_verified_key,
- secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint,
- secondary_verifier = excluded.secondary_verifier,
- backward_verified_key_id = excluded.backward_verified_key_id",
- (
- self.last_seen,
- self.last_seen_autocrypt,
- self.prefer_encrypt as i64,
- self.public_key.as_ref().map(|k| k.to_bytes()),
- self.gossip_timestamp,
- self.gossip_key.as_ref().map(|k| k.to_bytes()),
- self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
- self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
- self.verified_key.as_ref().map(|k| k.to_bytes()),
- &verified_key_fingerprint,
- self.verifier.as_deref().unwrap_or(""),
- self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
- self.secondary_verified_key_fingerprint
- .as_ref()
- .map(|fp| fp.hex()),
- self.secondary_verifier.as_deref().unwrap_or(""),
- self.backward_verified_key_id,
- &self.addr,
- ),
- )?;
- Ok(())
- };
- sql.transaction(trans_fn).await
- }
-
- /// Returns the address that verified the contact
- pub fn get_verifier(&self) -> Option<&str> {
- self.verifier.as_deref()
- }
-
- /// Add an info message to all the chats with this contact, informing about
- /// a [`PeerstateChange`].
- ///
- /// Also, in the case of an address change (AEAP), replace the old address
- /// with the new address in all chats.
- async fn handle_setup_change(
- &self,
- context: &Context,
- timestamp: i64,
- change: PeerstateChange,
- ) -> Result<()> {
- if context.is_self_addr(&self.addr).await? {
- // Do not try to search all the chats with self.
- return Ok(());
- }
-
- let contact_id = context
- .sql
- .query_get_value(
- "SELECT id FROM contacts WHERE addr=? COLLATE NOCASE;",
- (&self.addr,),
- )
- .await?
- .with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?;
-
- let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
- let msg = match &change {
- PeerstateChange::FingerprintChange => {
- stock_str::contact_setup_changed(context, &self.addr).await
- }
- PeerstateChange::Aeap(new_addr) => {
- let old_contact = Contact::get_by_id(context, contact_id).await?;
- stock_str::aeap_addr_changed(
- context,
- old_contact.get_display_name(),
- &self.addr,
- new_addr,
- )
- .await
- }
- };
- for (chat_id, msg_id) in chats.iter() {
- let timestamp_sort = if let Some(msg_id) = msg_id {
- let lastmsg = Message::load_from_db(context, *msg_id).await?;
- lastmsg.timestamp_sort
- } else {
- chat_id.created_timestamp(context).await?
- };
-
- if let PeerstateChange::Aeap(new_addr) = &change {
- let chat = Chat::load_from_db(context, *chat_id).await?;
-
- if chat.typ == Chattype::Group && !chat.is_protected() {
- // Don't add an info_msg to the group, in order not to make the user think
- // that the address was automatically replaced in the group.
- continue;
- }
-
- // For security reasons, for now, we only do the AEAP transition if the fingerprint
- // is verified (that's what from_verified_fingerprint_or_addr() does).
- // In order to not have inconsistent group membership state, we then only do the
- // transition in verified groups and in broadcast lists.
- if (chat.typ == Chattype::Group && chat.is_protected())
- || chat.typ == Chattype::Broadcast
- {
- match ContactAddress::new(new_addr) {
- Ok(new_addr) => {
- let (new_contact_id, _) = Contact::add_or_lookup(
- context,
- "",
- &new_addr,
- Origin::IncomingUnknownFrom,
- )
- .await?;
- context
- .sql
- .transaction(|transaction| {
- transaction.execute(
- "UPDATE chats_contacts
- SET remove_timestamp=MAX(add_timestamp+1, ?)
- WHERE chat_id=? AND contact_id=?",
- (timestamp, chat_id, contact_id),
- )?;
- transaction.execute(
- "INSERT INTO chats_contacts
- (chat_id, contact_id, add_timestamp)
- VALUES (?1, ?2, ?3)
- ON CONFLICT (chat_id, contact_id)
- DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)",
- (chat_id, new_contact_id, timestamp),
- )?;
- Ok(())
- })
- .await?;
-
- context.emit_event(EventType::ChatModified(*chat_id));
- }
- Err(err) => {
- warn!(
- context,
- "New address {:?} is not valid, not doing AEAP: {:#}.",
- new_addr,
- err
- )
- }
- }
- }
- }
-
- chat::add_info_msg_with_cmd(
- context,
- *chat_id,
- &msg,
- SystemMessage::Unknown,
- timestamp_sort,
- Some(timestamp),
- None,
- None,
- None,
- )
- .await?;
- }
-
- chatlist_events::emit_chatlist_changed(context);
- // update the chats the contact is part of
- chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
- Ok(())
- }
-
- /// Adds a warning to all the chats corresponding to peerstate if fingerprint has changed.
- pub(crate) async fn handle_fingerprint_change(
- &self,
- context: &Context,
- timestamp: i64,
- ) -> Result<()> {
- if self.fingerprint_changed {
- self.handle_setup_change(context, timestamp, PeerstateChange::FingerprintChange)
- .await?;
- }
- Ok(())
- }
-}
-
-/// Do an AEAP transition, if necessary.
-/// AEAP stands for "Automatic Email Address Porting."
-///
-/// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP.
-pub(crate) async fn maybe_do_aeap_transition(
- context: &Context,
- mime_parser: &mut crate::mimeparser::MimeMessage,
-) -> Result<()> {
- let Some(peerstate) = &mime_parser.peerstate else {
- return Ok(());
- };
-
- // If the from addr is different from the peerstate address we know,
- // we may want to do an AEAP transition.
- if !addr_cmp(&peerstate.addr, &mime_parser.from.addr) {
- // Check if it's a chat message; we do this to avoid
- // some accidental transitions if someone writes from multiple
- // addresses with an MUA.
- if !mime_parser.has_chat_version() {
- info!(
- context,
- "Not doing AEAP from {} to {} because the message is not a chat message.",
- &peerstate.addr,
- &mime_parser.from.addr
- );
- return Ok(());
- }
-
- // Check if the message is encrypted and signed correctly. If it's not encrypted, it's
- // probably from a new contact sharing the same key.
- if mime_parser.signatures.is_empty() {
- info!(
- context,
- "Not doing AEAP from {} to {} because the message is not encrypted and signed.",
- &peerstate.addr,
- &mime_parser.from.addr
- );
- return Ok(());
- }
-
- // Check if the From: address was also in the signed part of the email.
- // Without this check, an attacker could replay a message from Alice
- // to Bob. Then Bob's device would do an AEAP transition from Alice's
- // to the attacker's address, allowing for easier phishing.
- if !mime_parser.from_is_signed {
- info!(
- context,
- "Not doing AEAP from {} to {} because From: is not signed.",
- &peerstate.addr,
- &mime_parser.from.addr
- );
- return Ok(());
- }
-
- // DC avoids sending messages with the same timestamp, that's why messages
- // with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
- if mime_parser.timestamp_sent <= peerstate.last_seen {
- info!(
- context,
- "Not doing AEAP from {} to {} because {} < {}.",
- &peerstate.addr,
- &mime_parser.from.addr,
- mime_parser.timestamp_sent,
- peerstate.last_seen
- );
- return Ok(());
- }
-
- info!(
- context,
- "Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
- );
-
- let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?;
- // Add info messages to chats with this (verified) contact
- //
- peerstate
- .handle_setup_change(
- context,
- mime_parser.timestamp_sent,
- PeerstateChange::Aeap(mime_parser.from.addr.clone()),
- )
- .await?;
-
- let old_addr = mem::take(&mut peerstate.addr);
- peerstate.addr.clone_from(&mime_parser.from.addr);
- let header = mime_parser.autocrypt_header.as_ref().context(
- "Internal error: Tried to do an AEAP transition without an autocrypt header??",
- )?;
- peerstate.apply_header(context, header, mime_parser.timestamp_sent);
-
- peerstate
- .save_to_db_ex(&context.sql, Some(&old_addr))
- .await?;
- }
-
- Ok(())
-}
-
-/// Type of the peerstate change.
-///
-/// Changes to the peerstate are notified to the user via a message
-/// explaining the happened change.
-enum PeerstateChange {
- /// The contact's public key fingerprint changed, likely because
- /// the contact uses a new device and didn't transfer their key.
- FingerprintChange,
- /// The contact changed their address to the given new address
- /// (Automatic Email Address Porting).
- Aeap(String),
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::test_utils::alice_keypair;
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_peerstate_save_to_db() {
- let ctx = crate::test_utils::TestContext::new().await;
- let addr = "hello@mail.com";
-
- let pub_key = alice_keypair().public;
-
- let peerstate = Peerstate {
- addr: addr.into(),
- last_seen: 10,
- last_seen_autocrypt: 11,
- prefer_encrypt: EncryptPreference::Mutual,
- public_key: Some(pub_key.clone()),
- public_key_fingerprint: Some(pub_key.dc_fingerprint()),
- gossip_key: Some(pub_key.clone()),
- gossip_timestamp: 12,
- gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
- verified_key: Some(pub_key.clone()),
- verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- };
-
- assert!(
- peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
- "failed to save to db"
- );
-
- let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
- .await
- .expect("failed to load peerstate from db")
- .expect("no peerstate found in the database");
-
- assert_eq!(peerstate, peerstate_new);
- let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.dc_fingerprint())
- .await
- .expect("failed to load peerstate from db")
- .expect("no peerstate found in the database");
- assert_eq!(peerstate, peerstate_new2);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_peerstate_double_create() {
- let ctx = crate::test_utils::TestContext::new().await;
- let addr = "hello@mail.com";
- let pub_key = alice_keypair().public;
-
- let peerstate = Peerstate {
- addr: addr.into(),
- last_seen: 10,
- last_seen_autocrypt: 11,
- prefer_encrypt: EncryptPreference::Mutual,
- public_key: Some(pub_key.clone()),
- public_key_fingerprint: Some(pub_key.dc_fingerprint()),
- gossip_key: None,
- gossip_timestamp: 12,
- gossip_key_fingerprint: None,
- verified_key: None,
- verified_key_fingerprint: None,
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- };
-
- assert!(
- peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
- "failed to save"
- );
- assert!(
- peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
- "double-call with create failed"
- );
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_peerstate_with_empty_gossip_key_save_to_db() {
- let ctx = crate::test_utils::TestContext::new().await;
- let addr = "hello@mail.com";
-
- let pub_key = alice_keypair().public;
-
- let peerstate = Peerstate {
- addr: addr.into(),
- last_seen: 10,
- last_seen_autocrypt: 11,
- prefer_encrypt: EncryptPreference::Mutual,
- public_key: Some(pub_key.clone()),
- public_key_fingerprint: Some(pub_key.dc_fingerprint()),
- gossip_key: None,
- gossip_timestamp: 12,
- gossip_key_fingerprint: None,
- verified_key: None,
- verified_key_fingerprint: None,
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- };
-
- assert!(
- peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
- "failed to save"
- );
-
- let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
- .await
- .expect("failed to load peerstate from db");
-
- assert_eq!(Some(peerstate), peerstate_new);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_peerstate_load_db_defaults() {
- let ctx = crate::test_utils::TestContext::new().await;
- let addr = "hello@mail.com";
-
- // Old code created peerstates with this code and updated
- // other values later. If UPDATE failed, other columns had
- // default values, in particular fingerprints were set to
- // empty strings instead of NULL. This should not be the case
- // anymore, but the regression test still checks that defaults
- // can be loaded without errors.
- ctx.ctx
- .sql
- .execute("INSERT INTO acpeerstates (addr) VALUES(?)", (addr,))
- .await
- .expect("Failed to write to the database");
-
- let peerstate = Peerstate::from_addr(&ctx.ctx, addr)
- .await
- .expect("Failed to load peerstate from db")
- .expect("Loaded peerstate is empty");
-
- // Check that default values for fingerprints are treated like
- // NULL.
- assert_eq!(peerstate.public_key_fingerprint, None);
- assert_eq!(peerstate.gossip_key_fingerprint, None);
- assert_eq!(peerstate.verified_key_fingerprint, None);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_peerstate_degrade_reordering() {
- let ctx = crate::test_utils::TestContext::new().await;
-
- let addr = "example@example.org";
- let pub_key = alice_keypair().public;
- let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
-
- let mut peerstate = Peerstate {
- addr: addr.to_string(),
- last_seen: 0,
- last_seen_autocrypt: 0,
- prefer_encrypt: EncryptPreference::NoPreference,
- public_key: None,
- public_key_fingerprint: None,
- gossip_key: None,
- gossip_timestamp: 0,
- gossip_key_fingerprint: None,
- verified_key: None,
- verified_key_fingerprint: None,
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- };
-
- peerstate.apply_header(&ctx, &header, 100);
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
-
- peerstate.degrade_encryption(300);
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
-
- // This has message time 200, while encryption was degraded at timestamp 300.
- // Because of reordering, header should not be applied.
- peerstate.apply_header(&ctx, &header, 200);
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
-
- // Same header will be applied in the future.
- peerstate.apply_header(&ctx, &header, 300);
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
- }
-}
diff --git a/src/qr.rs b/src/qr.rs
index edfd62c12..0afe6c667 100644
--- a/src/qr.rs
+++ b/src/qr.rs
@@ -11,9 +11,7 @@ use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
pub(crate) use self::dclogin_scheme::configure_from_login_qr;
-use crate::chat::ChatIdBlocked;
use crate::config::Config;
-use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::events::EventType;
@@ -21,7 +19,6 @@ use crate::key::Fingerprint;
use crate::message::Message;
use crate::net::http::post_empty;
use crate::net::proxy::{ProxyConfig, DEFAULT_SOCKS_PORT};
-use crate::peerstate::Peerstate;
use crate::token;
use crate::tools::validate_id;
@@ -44,7 +41,7 @@ pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
/// Version written to Backups and Backup-QR-Codes.
/// Imports will fail when they have a larger version.
-pub(crate) const DCBACKUP_VERSION: i32 = 2;
+pub(crate) const DCBACKUP_VERSION: i32 = 3;
/// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -457,17 +454,17 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result {
None
};
- // retrieve known state for this fingerprint
- let peerstate = Peerstate::from_fingerprint(context, &fingerprint)
- .await
- .context("Can't load peerstate")?;
-
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
let addr = ContactAddress::new(addr)?;
- let (contact_id, _) =
- Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledSecurejoinQrScan)
- .await
- .with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
+ let (contact_id, _) = Contact::add_or_lookup_ex(
+ context,
+ &name,
+ &addr,
+ &fingerprint.hex(),
+ Origin::UnhandledSecurejoinQrScan,
+ )
+ .await
+ .with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
if let (Some(grpid), Some(grpname)) = (grpid, grpname) {
if context
@@ -529,21 +526,18 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result {
})
}
} else if let Some(addr) = addr {
- if let Some(peerstate) = peerstate {
- let peerstate_addr = ContactAddress::new(&peerstate.addr)?;
- let (contact_id, _) =
- Contact::add_or_lookup(context, &name, &peerstate_addr, Origin::UnhandledQrScan)
- .await
- .context("add_or_lookup")?;
- ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
- .await
- .context("Failed to create (new) chat for contact")?;
+ let fingerprint = fingerprint.hex();
+ let (contact_id, _) =
+ Contact::add_or_lookup_ex(context, "", &addr, &fingerprint, Origin::UnhandledQrScan)
+ .await?;
+ let contact = Contact::get_by_id(context, contact_id).await?;
+
+ if contact.public_key(context).await?.is_some() {
Ok(Qr::FprOk { contact_id })
} else {
- let contact_id = Contact::lookup_id_by_addr(context, &addr, Origin::Unknown)
- .await
- .with_context(|| format!("Error looking up contact {addr:?}"))?;
- Ok(Qr::FprMismatch { contact_id })
+ Ok(Qr::FprMismatch {
+ contact_id: Some(contact_id),
+ })
}
} else {
Ok(Qr::FprWithoutAddr {
diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs
index fa507b1bd..ca96f6b3d 100644
--- a/src/qr/qr_tests.rs
+++ b/src/qr/qr_tests.rs
@@ -1,10 +1,8 @@
use super::*;
-use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::config::Config;
-use crate::key::DcKey;
use crate::securejoin::get_securejoin_qr;
-use crate::test_utils::{alice_keypair, TestContext};
+use crate::test_utils::{TestContext, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_http() -> Result<()> {
@@ -212,7 +210,7 @@ async fn test_decode_smtp() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_ideltachat_link() -> Result<()> {
- let ctx = TestContext::new().await;
+ let ctx = TestContext::new_alice().await;
let qr = check_qr(
&ctx.ctx,
@@ -233,7 +231,7 @@ async fn test_decode_ideltachat_link() -> Result<()> {
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
- let ctx = TestContext::new().await;
+ let ctx = TestContext::new_alice().await;
let qr = check_qr(
&ctx.ctx,
@@ -246,7 +244,7 @@ async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_group() -> Result<()> {
- let ctx = TestContext::new().await;
+ let ctx = TestContext::new_alice().await;
let qr = check_qr(
&ctx.ctx,
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
@@ -264,7 +262,7 @@ async fn test_decode_openpgp_group() -> Result<()> {
}
// Test it again with lowercased "openpgp4fpr:" uri scheme
- let ctx = TestContext::new().await;
+ let ctx = TestContext::new_alice().await;
let qr = check_qr(
&ctx.ctx,
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
@@ -289,7 +287,7 @@ async fn test_decode_openpgp_group() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_invalid_token() -> Result<()> {
- let ctx = TestContext::new().await;
+ let ctx = TestContext::new_alice().await;
// Token cannot contain "/"
let qr = check_qr(
@@ -304,7 +302,7 @@ async fn test_decode_openpgp_invalid_token() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_secure_join() -> Result<()> {
- let ctx = TestContext::new().await;
+ let ctx = TestContext::new_alice().await;
let qr = check_qr(
&ctx.ctx,
@@ -333,7 +331,7 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
}
// Regression test
- let ctx = TestContext::new().await;
+ let ctx = TestContext::new_alice().await;
let qr = check_qr(
&ctx.ctx,
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
@@ -353,52 +351,29 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_fingerprint() -> Result<()> {
- let ctx = TestContext::new().await;
+ let mut tcm = TestContextManager::new();
+ let bob = &tcm.bob().await;
+ let alice = &tcm.alice().await;
- let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
- .await
- .context("failed to create contact")?;
- let pub_key = alice_keypair().public;
- let peerstate = Peerstate {
- addr: "alice@example.org".to_string(),
- last_seen: 1,
- last_seen_autocrypt: 1,
- prefer_encrypt: EncryptPreference::Mutual,
- public_key: Some(pub_key.clone()),
- public_key_fingerprint: Some(pub_key.dc_fingerprint()),
- gossip_key: None,
- gossip_timestamp: 0,
- gossip_key_fingerprint: None,
- verified_key: None,
- verified_key_fingerprint: None,
- verifier: None,
- secondary_verified_key: None,
- secondary_verified_key_fingerprint: None,
- secondary_verifier: None,
- backward_verified_key_id: None,
- fingerprint_changed: false,
- };
- assert!(
- peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
- "failed to save peerstate"
- );
+ let alice_contact = bob.add_or_lookup_contact(alice).await;
+ let alice_contact_id = alice_contact.id;
let qr = check_qr(
- &ctx.ctx,
+ bob,
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
)
.await?;
if let Qr::FprMismatch { contact_id, .. } = qr {
- assert_eq!(contact_id, Some(alice_contact_id));
+ assert_ne!(contact_id.unwrap(), alice_contact_id);
} else {
bail!("Wrong QR code type");
}
let qr = check_qr(
- &ctx.ctx,
+ bob,
&format!(
"OPENPGP4FPR:{}#a=alice@example.org",
- pub_key.dc_fingerprint()
+ alice_contact.fingerprint().unwrap()
),
)
.await?;
@@ -408,14 +383,14 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
bail!("Wrong QR code type");
}
- assert_eq!(
+ assert!(matches!(
check_qr(
- &ctx.ctx,
+ bob,
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
)
.await?,
- Qr::FprMismatch { contact_id: None }
- );
+ Qr::FprMismatch { .. }
+ ));
Ok(())
}
diff --git a/src/reaction.rs b/src/reaction.rs
index 545b31ba8..a612eb862 100644
--- a/src/reaction.rs
+++ b/src/reaction.rs
@@ -981,10 +981,11 @@ Here's my footer -- bob@example.net"
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction_multidevice() -> Result<()> {
- let alice0 = TestContext::new_alice().await;
- let alice1 = TestContext::new_alice().await;
- let bob_id = Contact::create(&alice0, "", "bob@example.net").await?;
- let chat_id = ChatId::create_for_contact(&alice0, bob_id).await?;
+ let mut tcm = TestContextManager::new();
+ let alice0 = tcm.alice().await;
+ let alice1 = tcm.alice().await;
+ let bob = tcm.bob().await;
+ let chat_id = alice0.create_chat(&bob).await.id;
let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
diff --git a/src/receive_imf.rs b/src/receive_imf.rs
index 479fd25b0..e0264d605 100644
--- a/src/receive_imf.rs
+++ b/src/receive_imf.rs
@@ -1,22 +1,23 @@
//! Internet Message Format reception pipeline.
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
use std::iter;
use std::sync::LazyLock;
use anyhow::{Context as _, Result};
use data_encoding::BASE32_NOPAD;
-use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
+use deltachat_contact_tools::{
+ addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line, ContactAddress,
+};
use iroh_gossip::proto::TopicId;
use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use regex::Regex;
-use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH, EDITED_PREFIX};
-use crate::contact::{Contact, ContactId, Origin};
+use crate::contact::{mark_contact_id_as_verified, Contact, ContactId, Origin};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
use crate::download::DownloadState;
@@ -24,7 +25,8 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
-use crate::key::DcKey;
+use crate::key::self_fingerprint_opt;
+use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::log::LogExt;
use crate::log::{info, warn};
use crate::message::{
@@ -33,7 +35,6 @@ use crate::message::{
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
-use crate::peerstate::Peerstate;
use crate::reaction::{set_msg_reaction, Reaction};
use crate::rusqlite::OptionalExtension;
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
@@ -67,11 +68,77 @@ pub struct ReceivedMsg {
/// Whether IMAP messages should be immediately deleted.
pub needs_delete_job: bool,
+}
- /// Whether the From address was repeated in the signed part
- /// (and we know that the signer intended to send from this address).
- #[cfg(test)]
- pub(crate) from_is_signed: bool,
+/// Decision on which kind of chat the message
+/// should be assigned in.
+///
+/// This is done before looking up contact IDs
+/// so we know in advance whether to lookup
+/// key-contacts or email address contacts.
+///
+/// Once this decision is made,
+/// it should not be changed so we
+/// don't assign the message to an encrypted
+/// group after looking up key-contacts
+/// or vice versa.
+#[derive(Debug)]
+enum ChatAssignment {
+ /// Trash the message.
+ Trash,
+
+ /// Group chat with a Group ID.
+ ///
+ /// Lookup key-contacts and
+ /// assign to encrypted group.
+ GroupChat { grpid: String },
+
+ /// Mailing list or broadcast list.
+ ///
+ /// Mailing lists don't have members.
+ /// Broadcast lists have members
+ /// on the sender side,
+ /// but their addresses don't go into
+ /// the `To` field.
+ ///
+ /// In any case, the `To`
+ /// field should be ignored
+ /// and no contact IDs should be looked
+ /// up except the `from_id`
+ /// which may be an email address contact
+ /// or a key-contact.
+ MailingList,
+
+ /// Group chat without a Group ID.
+ ///
+ /// This is not encrypted.
+ AdHocGroup,
+
+ /// Assign the message to existing chat
+ /// with a known `chat_id`.
+ ExistingChat {
+ /// ID of existing chat
+ /// which the message should be assigned to.
+ chat_id: ChatId,
+
+ /// Whether existing chat is blocked.
+ /// This is loaded together with a chat ID
+ /// reduce the number of database calls.
+ ///
+ /// We may want to unblock the chat
+ /// after adding the message there
+ /// if the chat is currently blocked.
+ chat_id_blocked: Blocked,
+ },
+
+ /// 1:1 chat with a single contact.
+ ///
+ /// The chat may be encrypted or not,
+ /// it does not matter.
+ /// It is not possible to mix
+ /// email address contacts
+ /// with key-contacts in a single 1:1 chat anyway.
+ OneOneChat,
}
/// Emulates reception of a message from the network.
@@ -147,6 +214,243 @@ async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result
Ok(msg_id)
}
+async fn get_to_and_past_contact_ids(
+ context: &Context,
+ mime_parser: &MimeMessage,
+ chat_assignment: &ChatAssignment,
+ is_partial_download: Option,
+ parent_message: &Option,
+ incoming_origin: Origin,
+) -> Result<(Vec>, Vec >)> {
+ // `None` means that the chat is encrypted,
+ // but we were not able to convert the address
+ // to key-contact, e.g.
+ // because there was no corresponding
+ // Autocrypt-Gossip header.
+ //
+ // This way we still preserve remaining
+ // number of contacts and their positions
+ // so we can match the contacts to
+ // e.g. Chat-Group-Member-Timestamps
+ // header.
+ let to_ids: Vec >;
+ let past_ids: Vec >;
+
+ // ID of the chat to look up the addresses in.
+ //
+ // Note that this is not necessarily the chat we want to assign the message to.
+ // In case of an outgoing private reply to a group message we may
+ // lookup the address of receipient in the list of addresses used in the group,
+ // but want to assign the message to 1:1 chat.
+ let chat_id = match chat_assignment {
+ ChatAssignment::Trash => None,
+ ChatAssignment::GroupChat { ref grpid } => {
+ if let Some((chat_id, _protected, _blocked)) =
+ chat::get_chat_id_by_grpid(context, grpid).await?
+ {
+ Some(chat_id)
+ } else {
+ None
+ }
+ }
+ ChatAssignment::AdHocGroup => {
+ // If we are going to assign a message to ad hoc group,
+ // we can just convert the email addresses
+ // to e-mail address contacts and don't need a `ChatId`
+ // to lookup key-contacts.
+ None
+ }
+ ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
+ ChatAssignment::MailingList => None,
+ ChatAssignment::OneOneChat => {
+ if is_partial_download.is_none() && !mime_parser.incoming {
+ parent_message.as_ref().map(|m| m.chat_id)
+ } else {
+ None
+ }
+ }
+ };
+
+ let member_fingerprints = mime_parser.chat_group_member_fingerprints();
+ let to_member_fingerprints;
+ let past_member_fingerprints;
+
+ if !member_fingerprints.is_empty() {
+ if member_fingerprints.len() >= mime_parser.recipients.len() {
+ (to_member_fingerprints, past_member_fingerprints) =
+ member_fingerprints.split_at(mime_parser.recipients.len());
+ } else {
+ warn!(
+ context,
+ "Unexpected length of the fingerprint header, expected at least {}, got {}.",
+ mime_parser.recipients.len(),
+ member_fingerprints.len()
+ );
+ to_member_fingerprints = &[];
+ past_member_fingerprints = &[];
+ }
+ } else {
+ to_member_fingerprints = &[];
+ past_member_fingerprints = &[];
+ }
+
+ let pgp_to_ids = add_or_lookup_key_contacts_by_address_list(
+ context,
+ &mime_parser.recipients,
+ &mime_parser.gossiped_keys,
+ to_member_fingerprints,
+ Origin::Hidden,
+ )
+ .await?;
+
+ match chat_assignment {
+ ChatAssignment::GroupChat { .. } => {
+ to_ids = pgp_to_ids;
+
+ if let Some(chat_id) = chat_id {
+ past_ids = lookup_key_contacts_by_address_list(
+ context,
+ &mime_parser.past_members,
+ past_member_fingerprints,
+ Some(chat_id),
+ )
+ .await?;
+ } else {
+ past_ids = add_or_lookup_key_contacts_by_address_list(
+ context,
+ &mime_parser.past_members,
+ &mime_parser.gossiped_keys,
+ past_member_fingerprints,
+ Origin::Hidden,
+ )
+ .await?;
+ }
+ }
+ ChatAssignment::Trash | ChatAssignment::MailingList => {
+ to_ids = Vec::new();
+ past_ids = Vec::new();
+ }
+ ChatAssignment::ExistingChat { chat_id, .. } => {
+ let chat = Chat::load_from_db(context, *chat_id).await?;
+ if chat.is_encrypted(context).await? {
+ to_ids = pgp_to_ids;
+ past_ids = lookup_key_contacts_by_address_list(
+ context,
+ &mime_parser.past_members,
+ past_member_fingerprints,
+ Some(*chat_id),
+ )
+ .await?;
+ } else {
+ to_ids = add_or_lookup_contacts_by_address_list(
+ context,
+ &mime_parser.recipients,
+ if !mime_parser.incoming {
+ Origin::OutgoingTo
+ } else if incoming_origin.is_known() {
+ Origin::IncomingTo
+ } else {
+ Origin::IncomingUnknownTo
+ },
+ )
+ .await?;
+
+ past_ids = add_or_lookup_contacts_by_address_list(
+ context,
+ &mime_parser.past_members,
+ Origin::Hidden,
+ )
+ .await?;
+ }
+ }
+ ChatAssignment::AdHocGroup => {
+ to_ids = add_or_lookup_contacts_by_address_list(
+ context,
+ &mime_parser.recipients,
+ if !mime_parser.incoming {
+ Origin::OutgoingTo
+ } else if incoming_origin.is_known() {
+ Origin::IncomingTo
+ } else {
+ Origin::IncomingUnknownTo
+ },
+ )
+ .await?;
+
+ past_ids = add_or_lookup_contacts_by_address_list(
+ context,
+ &mime_parser.past_members,
+ Origin::Hidden,
+ )
+ .await?;
+ }
+ ChatAssignment::OneOneChat => {
+ if pgp_to_ids
+ .first()
+ .is_some_and(|contact_id| contact_id.is_some())
+ {
+ // There is a single recipient and we have
+ // mapped it to a key contact.
+ // This is an encrypted 1:1 chat.
+ to_ids = pgp_to_ids
+ } else if let Some(chat_id) = chat_id {
+ to_ids = lookup_key_contacts_by_address_list(
+ context,
+ &mime_parser.recipients,
+ to_member_fingerprints,
+ Some(chat_id),
+ )
+ .await?;
+ } else {
+ let ids = match mime_parser.was_encrypted() {
+ true => {
+ lookup_key_contacts_by_address_list(
+ context,
+ &mime_parser.recipients,
+ to_member_fingerprints,
+ chat_id,
+ )
+ .await?
+ }
+ false => vec![],
+ };
+ if chat_id.is_some()
+ || (mime_parser.was_encrypted() && !ids.contains(&None))
+ // Prefer creating PGP chats if there are any key-contacts. At least this prevents
+ // from replying unencrypted.
+ || ids
+ .iter()
+ .any(|&c| c.is_some() && c != Some(ContactId::SELF))
+ {
+ to_ids = ids;
+ } else {
+ to_ids = add_or_lookup_contacts_by_address_list(
+ context,
+ &mime_parser.recipients,
+ if !mime_parser.incoming {
+ Origin::OutgoingTo
+ } else if incoming_origin.is_known() {
+ Origin::IncomingTo
+ } else {
+ Origin::IncomingUnknownTo
+ },
+ )
+ .await?;
+ }
+ }
+
+ past_ids = add_or_lookup_contacts_by_address_list(
+ context,
+ &mime_parser.past_members,
+ Origin::Hidden,
+ )
+ .await?;
+ }
+ };
+
+ Ok((to_ids, past_ids))
+}
+
/// Receive a message and add it to the database.
///
/// Returns an error on database failure or if the message is broken,
@@ -197,27 +501,11 @@ pub(crate) async fn receive_imf_inner(
sort_timestamp: 0,
msg_ids,
needs_delete_job: false,
- #[cfg(test)]
- from_is_signed: false,
}));
}
Ok(mime_parser) => mime_parser,
};
- crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?;
- if let Some(peerstate) = &mime_parser.peerstate {
- peerstate
- .handle_fingerprint_change(context, mime_parser.timestamp_sent)
- .await?;
- // When peerstate is set to Mutual, it's saved immediately to not lose that fact in case
- // of an error. Otherwise we don't save peerstate until get here to reduce the number of
- // calls to save_to_db() and not to degrade encryption if a mail wasn't parsed
- // successfully.
- if peerstate.prefer_encrypt != EncryptPreference::Mutual {
- peerstate.save_to_db(&context.sql).await?;
- }
- }
-
let rfc724_mid_orig = &mime_parser
.get_rfc724_mid()
.unwrap_or(rfc724_mid.to_string());
@@ -323,63 +611,82 @@ pub(crate) async fn receive_imf_inner(
// For example, GitHub sends messages from `notifications@github.com`,
// but uses display name of the user whose action generated the notification
// as the display name.
- let (from_id, _from_id_blocked, incoming_origin) =
- match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? {
- Some(contact_id_res) => contact_id_res,
- None => {
- warn!(
- context,
- "receive_imf: From field does not contain an acceptable address."
- );
- return Ok(None);
- }
- };
-
- let to_ids = add_or_lookup_contacts_by_address_list(
+ let fingerprint = mime_parser.signatures.iter().next();
+ let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id(
context,
- &mime_parser.recipients,
- if !mime_parser.incoming {
- Origin::OutgoingTo
- } else if incoming_origin.is_known() {
- Origin::IncomingTo
- } else {
- Origin::IncomingUnknownTo
- },
+ &mime_parser.from,
+ fingerprint,
+ prevent_rename,
+ is_partial_download.is_some()
+ && mime_parser
+ .get_header(HeaderDef::ContentType)
+ .unwrap_or_default()
+ .starts_with("multipart/encrypted"),
+ )
+ .await?
+ {
+ Some(contact_id_res) => contact_id_res,
+ None => {
+ warn!(
+ context,
+ "receive_imf: From field does not contain an acceptable address."
+ );
+ return Ok(None);
+ }
+ };
+
+ // Lookup parent message.
+ //
+ // This may be useful to assign the message to
+ // group chats without Chat-Group-ID
+ // when a message is sent by Thunderbird.
+ //
+ // This can be also used to lookup
+ // key-contact by email address
+ // when receiving a private 1:1 reply
+ // to a group chat message.
+ let parent_message = get_parent_message(
+ context,
+ mime_parser.get_header(HeaderDef::References),
+ mime_parser.get_header(HeaderDef::InReplyTo),
+ )
+ .await?
+ .filter(|p| Some(p.id) != replace_msg_id);
+
+ let chat_assignment = decide_chat_assignment(
+ context,
+ &mime_parser,
+ &parent_message,
+ rfc724_mid,
+ from_id,
+ &is_partial_download,
)
.await?;
- let past_ids = add_or_lookup_contacts_by_address_list(
+ info!(context, "Chat assignment is {chat_assignment:?}.");
+
+ let (to_ids, past_ids) = get_to_and_past_contact_ids(
context,
- &mime_parser.past_members,
- if !mime_parser.incoming {
- Origin::OutgoingTo
- } else if incoming_origin.is_known() {
- Origin::IncomingTo
- } else {
- Origin::IncomingUnknownTo
- },
+ &mime_parser,
+ &chat_assignment,
+ is_partial_download,
+ &parent_message,
+ incoming_origin,
)
.await?;
- update_verified_keys(context, &mut mime_parser, from_id).await?;
-
let received_msg;
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
- let res;
- if mime_parser.incoming {
- res = handle_securejoin_handshake(context, &mut mime_parser, from_id)
+ let res = if mime_parser.incoming {
+ handle_securejoin_handshake(context, &mut mime_parser, from_id)
.await
- .context("error in Secure-Join message handling")?;
-
- // Peerstate could be updated by handling the Securejoin handshake.
- let contact = Contact::get_by_id(context, from_id).await?;
- mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
+ .context("error in Secure-Join message handling")?
} else {
- let to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
+ let to_id = to_ids.first().copied().flatten().unwrap_or(ContactId::SELF);
// handshake may mark contacts as verified and must be processed before chats are created
- res = observe_securejoin_on_other_device(context, &mime_parser, to_id)
+ observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await
.context("error in Secure-Join watching")?
- }
+ };
match res {
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
@@ -391,8 +698,6 @@ pub(crate) async fn receive_imf_inner(
sort_timestamp: mime_parser.timestamp_sent,
msg_ids: vec![msg_id],
needs_delete_job: res == securejoin::HandshakeMessage::Done,
- #[cfg(test)]
- from_is_signed: mime_parser.from_is_signed,
});
}
securejoin::HandshakeMessage::Propagate => {
@@ -403,33 +708,67 @@ pub(crate) async fn receive_imf_inner(
received_msg = None;
}
- let verified_encryption = has_verified_encryption(&mime_parser, from_id)?;
+ let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
if verified_encryption == VerifiedEncryption::Verified {
mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?;
}
- if verified_encryption == VerifiedEncryption::Verified
- && mime_parser.get_header(HeaderDef::ChatVerified).is_some()
- {
- if let Some(peerstate) = &mut mime_parser.peerstate {
- // NOTE: it might be better to remember ID of the key
- // that we used to decrypt the message, but
- // it is unlikely that default key ever changes
- // as it only happens when user imports a new default key.
- //
- // Backward verification is not security-critical,
- // it is only needed to avoid adding user who does not
- // have our key as verified to protected chats.
- peerstate.backward_verified_key_id =
- Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
- peerstate.save_to_db(&context.sql).await?;
- }
- }
-
let received_msg = if let Some(received_msg) = received_msg {
received_msg
} else {
+ let is_dc_message = if mime_parser.has_chat_version() {
+ MessengerMessage::Yes
+ } else if let Some(parent_message) = &parent_message {
+ match parent_message.is_dc_message {
+ MessengerMessage::No => MessengerMessage::No,
+ MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
+ }
+ } else {
+ MessengerMessage::No
+ };
+
+ let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
+ .unwrap_or_default();
+
+ let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
+ let allow_creation = if mime_parser.decrypting_failed {
+ false
+ } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
+ && is_dc_message == MessengerMessage::No
+ && !context.get_config_bool(Config::IsChatmail).await?
+ {
+ // the message is a classic email in a classic profile
+ // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
+ match show_emails {
+ ShowEmails::Off | ShowEmails::AcceptedContacts => false,
+ ShowEmails::All => true,
+ }
+ } else {
+ !is_reaction
+ };
+
+ let to_id = if mime_parser.incoming {
+ ContactId::SELF
+ } else {
+ to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
+ };
+
+ let (chat_id, chat_id_blocked) = do_chat_assignment(
+ context,
+ chat_assignment,
+ from_id,
+ &to_ids,
+ &past_ids,
+ to_id,
+ allow_creation,
+ &mut mime_parser,
+ is_partial_download,
+ &verified_encryption,
+ parent_message,
+ )
+ .await?;
+
// Add parts
add_parts(
context,
@@ -444,6 +783,9 @@ pub(crate) async fn receive_imf_inner(
replace_msg_id,
prevent_rename,
verified_encryption,
+ chat_id,
+ chat_id_blocked,
+ is_dc_message,
)
.await
.context("add_parts error")?
@@ -663,12 +1005,22 @@ pub(crate) async fn receive_imf_inner(
/// display names. We don't want the display name to change every time the user gets a new email from
/// a mailing list.
///
+/// * `find_key_contact_by_addr`: if true, we only know the e-mail address
+/// of the contact, but not the fingerprint,
+/// yet want to assign the message to some key-contact.
+/// This can happen during prefetch or when the message is partially downloaded.
+/// If we get it wrong, the message will be placed into the correct
+/// chat after downloading.
+///
/// Returns `None` if From field does not contain a valid contact address.
pub async fn from_field_to_contact_id(
context: &Context,
from: &SingleInfo,
+ fingerprint: Option<&Fingerprint>,
prevent_rename: bool,
+ find_key_contact_by_addr: bool,
) -> Result > {
+ let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
let display_name = if prevent_rename {
Some("")
} else {
@@ -685,10 +1037,42 @@ pub async fn from_field_to_contact_id(
}
};
- let (from_id, _) = Contact::add_or_lookup(
+ if fingerprint.is_empty() && find_key_contact_by_addr {
+ let addr_normalized = addr_normalize(&from_addr);
+
+ // Try to assign to some key-contact.
+ if let Some((from_id, origin)) = context
+ .sql
+ .query_row_optional(
+ "SELECT id, origin FROM contacts
+ WHERE addr=?1 COLLATE NOCASE
+ AND fingerprint<>'' -- Only key-contacts
+ AND id>?2 AND origin>=?3 AND blocked=?4
+ ORDER BY last_seen DESC
+ LIMIT 1",
+ (
+ &addr_normalized,
+ ContactId::LAST_SPECIAL,
+ Origin::IncomingUnknownFrom,
+ Blocked::Not,
+ ),
+ |row| {
+ let id: ContactId = row.get(0)?;
+ let origin: Origin = row.get(1)?;
+ Ok((id, origin))
+ },
+ )
+ .await?
+ {
+ return Ok(Some((from_id, false, origin)));
+ }
+ }
+
+ let (from_id, _) = Contact::add_or_lookup_ex(
context,
display_name.unwrap_or_default(),
&from_addr,
+ &fingerprint,
Origin::IncomingUnknownFrom,
)
.await?;
@@ -699,126 +1083,200 @@ pub async fn from_field_to_contact_id(
let contact = Contact::get_by_id(context, from_id).await?;
let from_id_blocked = contact.blocked;
let incoming_origin = contact.origin;
+
+ context
+ .sql
+ .execute(
+ "UPDATE contacts SET addr=? WHERE id=?",
+ (from_addr, from_id),
+ )
+ .await?;
+
Ok(Some((from_id, from_id_blocked, incoming_origin)))
}
}
-/// Creates a `ReceivedMsg` from given parts which might consist of
-/// multiple messages (if there are multiple attachments).
-/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table.
-#[expect(clippy::too_many_arguments)]
-async fn add_parts(
+async fn decide_chat_assignment(
context: &Context,
- mime_parser: &mut MimeMessage,
- imf_raw: &[u8],
- to_ids: &[ContactId],
- past_ids: &[ContactId],
+ mime_parser: &MimeMessage,
+ parent_message: &Option,
rfc724_mid: &str,
from_id: ContactId,
- seen: bool,
+ is_partial_download: &Option,
+) -> Result {
+ let should_trash = if !mime_parser.mdn_reports.is_empty() {
+ info!(context, "Message is an MDN (TRASH).");
+ true
+ } else if mime_parser.delivery_report.is_some() {
+ info!(context, "Message is a DSN (TRASH).");
+ markseen_on_imap_table(context, rfc724_mid).await.ok();
+ true
+ } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
+ || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
+ || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
+ || mime_parser.sync_items.is_some()
+ {
+ info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
+ true
+ } else if mime_parser.decrypting_failed && !mime_parser.incoming {
+ // Outgoing undecryptable message.
+ let last_time = context
+ .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
+ .await?;
+ let now = tools::time();
+ let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
+ let mut msg = Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
+ chat::add_device_msg(context, None, Some(&mut msg))
+ .await
+ .log_err(context)
+ .ok();
+ true
+ } else {
+ last_time > now
+ };
+ if update_config {
+ context
+ .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
+ .await?;
+ }
+ info!(context, "Outgoing undecryptable message (TRASH).");
+ true
+ } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
+ && !mime_parser.has_chat_version()
+ && parent_message
+ .as_ref()
+ .is_none_or(|p| p.is_dc_message == MessengerMessage::No)
+ && !context.get_config_bool(Config::IsChatmail).await?
+ && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
+ .unwrap_or_default()
+ == ShowEmails::Off
+ {
+ info!(context, "Classical email not shown (TRASH).");
+ // the message is a classic email in a classic profile
+ // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
+ true
+ } else if mime_parser
+ .get_header(HeaderDef::XMozillaDraftInfo)
+ .is_some()
+ {
+ // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
+ // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
+ // created by Thunderbird.
+
+ // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
+ info!(context, "Email is probably just a draft (TRASH).");
+ true
+ } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
+ if let Some(part) = mime_parser.parts.first() {
+ if part.typ == Viewtype::Text && part.msg.is_empty() {
+ info!(context, "Message is a status update only (TRASH).");
+ markseen_on_imap_table(context, rfc724_mid).await.ok();
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ } else {
+ false
+ };
+
+ // Decide on the type of chat we assign the message to.
+ //
+ // The chat may not exist yet, i.e. there may be
+ // no database row and ChatId yet.
+ let mut num_recipients = mime_parser.recipients.len();
+ if from_id != ContactId::SELF {
+ let mut has_self_addr = false;
+ for recipient in &mime_parser.recipients {
+ if context.is_self_addr(&recipient.addr).await? {
+ has_self_addr = true;
+ }
+ }
+ if !has_self_addr {
+ num_recipients += 1;
+ }
+ }
+
+ let chat_assignment = if should_trash {
+ ChatAssignment::Trash
+ } else if let Some(grpid) = mime_parser.get_chat_group_id() {
+ if mime_parser.was_encrypted() {
+ ChatAssignment::GroupChat {
+ grpid: grpid.to_string(),
+ }
+ } else if let Some(parent) = &parent_message {
+ if let Some((chat_id, chat_id_blocked)) =
+ lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
+ {
+ // Try to assign to a chat based on In-Reply-To/References.
+ ChatAssignment::ExistingChat {
+ chat_id,
+ chat_id_blocked,
+ }
+ } else {
+ ChatAssignment::AdHocGroup
+ }
+ } else {
+ // Could be a message from old version
+ // with opportunistic encryption.
+ //
+ // We still want to assign this to a group
+ // even if it had only two members.
+ //
+ // Group ID is ignored, however.
+ ChatAssignment::AdHocGroup
+ }
+ } else if mime_parser.get_mailinglist_header().is_some() {
+ ChatAssignment::MailingList
+ } else if let Some(parent) = &parent_message {
+ if let Some((chat_id, chat_id_blocked)) =
+ lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
+ {
+ // Try to assign to a chat based on In-Reply-To/References.
+ ChatAssignment::ExistingChat {
+ chat_id,
+ chat_id_blocked,
+ }
+ } else if num_recipients <= 1 {
+ ChatAssignment::OneOneChat
+ } else {
+ ChatAssignment::AdHocGroup
+ }
+ } else if num_recipients <= 1 {
+ ChatAssignment::OneOneChat
+ } else {
+ ChatAssignment::AdHocGroup
+ };
+ Ok(chat_assignment)
+}
+
+/// Assigns the message to a chat.
+///
+/// Creates a new chat if necessary.
+#[expect(clippy::too_many_arguments)]
+async fn do_chat_assignment(
+ context: &Context,
+ chat_assignment: ChatAssignment,
+ from_id: ContactId,
+ to_ids: &[Option],
+ past_ids: &[Option],
+ to_id: ContactId,
+ allow_creation: bool,
+ mime_parser: &mut MimeMessage,
is_partial_download: Option,
- mut replace_msg_id: Option,
- prevent_rename: bool,
- verified_encryption: VerifiedEncryption,
-) -> Result {
+ verified_encryption: &VerifiedEncryption,
+ parent_message: Option,
+) -> Result<(ChatId, Blocked)> {
let is_bot = context.get_config_bool(Config::Bot).await?;
- let rfc724_mid_orig = &mime_parser
- .get_rfc724_mid()
- .unwrap_or(rfc724_mid.to_string());
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
- let mut better_msg = None;
- let mut group_changes = GroupChangesInfo::default();
- if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
- better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
- }
-
- let parent = get_parent_message(
- context,
- mime_parser.get_header(HeaderDef::References),
- mime_parser.get_header(HeaderDef::InReplyTo),
- )
- .await?
- .filter(|p| Some(p.id) != replace_msg_id);
-
- let is_dc_message = if mime_parser.has_chat_version() {
- MessengerMessage::Yes
- } else if let Some(parent) = &parent {
- match parent.is_dc_message {
- MessengerMessage::No => MessengerMessage::No,
- MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
- }
- } else {
- MessengerMessage::No
- };
- // incoming non-chat messages may be discarded
-
- let is_location_kml = mime_parser.location_kml.is_some();
- let is_mdn = !mime_parser.mdn_reports.is_empty();
- let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
- let show_emails =
- ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
-
- let allow_creation;
- if mime_parser.decrypting_failed {
- allow_creation = false;
- } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
- && is_dc_message == MessengerMessage::No
- && !context.get_config_bool(Config::IsChatmail).await?
- {
- // the message is a classic email in a classic profile
- // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
- match show_emails {
- ShowEmails::Off => {
- info!(context, "Classical email not shown (TRASH).");
- chat_id = Some(DC_CHAT_ID_TRASH);
- allow_creation = false;
- }
- ShowEmails::AcceptedContacts => allow_creation = false,
- ShowEmails::All => allow_creation = !is_mdn,
- }
- } else {
- allow_creation = !is_mdn && !is_reaction;
- }
-
- // check if the message introduces a new chat:
- // - outgoing messages introduce a chat with the first to: address if they are sent by a messenger
- // - incoming messages introduce a chat only for known contacts if they are sent by a messenger
- // (of course, the user can add other chats manually later)
- let to_id: ContactId;
- let state: MessageState;
- let mut hidden = is_reaction;
- let mut needs_delete_job = false;
- let mut restore_protection = false;
-
- // if contact renaming is prevented (for mailinglists and bots),
- // we use name from From:-header as override name
- if prevent_rename {
- if let Some(name) = &mime_parser.from.display_name {
- for part in &mut mime_parser.parts {
- part.param.set(Param::OverrideSenderDisplayname, name);
- }
- }
- }
-
- if chat_id.is_none() && is_mdn {
- chat_id = Some(DC_CHAT_ID_TRASH);
- info!(context, "Message is an MDN (TRASH).",);
- }
-
if mime_parser.incoming {
- to_id = ContactId::SELF;
-
let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
- if chat_id.is_none() && mime_parser.delivery_report.is_some() {
- chat_id = Some(DC_CHAT_ID_TRASH);
- info!(context, "Message is a DSN (TRASH).",);
- markseen_on_imap_table(context, rfc724_mid).await.ok();
- }
-
let create_blocked_default = if is_bot {
Blocked::Not
} else {
@@ -845,11 +1303,14 @@ async fn add_parts(
create_blocked_default
};
- // Try to assign to a chat based on Chat-Group-ID.
- if chat_id.is_none() {
- if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
+ match &chat_assignment {
+ ChatAssignment::Trash => {
+ chat_id = Some(DC_CHAT_ID_TRASH);
+ }
+ ChatAssignment::GroupChat { grpid } => {
+ // Try to assign to a chat based on Chat-Group-ID.
if let Some((id, _protected, blocked)) =
- chat::get_chat_id_by_grpid(context, &grpid).await?
+ chat::get_chat_id_by_grpid(context, grpid).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
@@ -862,8 +1323,8 @@ async fn add_parts(
from_id,
to_ids,
past_ids,
- &verified_encryption,
- &grpid,
+ verified_encryption,
+ grpid,
)
.await?
{
@@ -872,80 +1333,39 @@ async fn add_parts(
}
}
}
- }
+ ChatAssignment::MailingList => {
+ if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
+ if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
+ context,
+ allow_creation,
+ mailinglist_header,
+ mime_parser,
+ )
+ .await?
+ {
+ chat_id = Some(new_chat_id);
+ chat_id_blocked = new_chat_id_blocked;
- if chat_id.is_none() {
- if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
- context,
- mime_parser,
- &parent,
- to_ids,
- from_id,
- allow_creation || test_normal_chat.is_some(),
- create_blocked,
- is_partial_download.is_some(),
- )
- .await?
- {
- chat_id = Some(new_chat_id);
- chat_id_blocked = new_chat_id_blocked;
- }
- }
-
- // if the chat is somehow blocked but we want to create a non-blocked chat,
- // unblock the chat
- if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes {
- if let Some(chat_id) = chat_id {
- chat_id.set_blocked(context, create_blocked).await?;
- chat_id_blocked = create_blocked;
- }
- }
-
- // In lookup_chat_by_reply() and create_group(), it can happen that the message is put into a chat
- // but the From-address is not a member of this chat.
- if let Some(group_chat_id) = chat_id {
- if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
- let chat = Chat::load_from_db(context, group_chat_id).await?;
- if chat.is_protected() && chat.typ == Chattype::Single {
- // Just assign the message to the 1:1 chat with the actual sender instead.
- chat_id = None;
- } else {
- // In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
- // to the sender's name, indicating to the user that he/she is not part of the group.
- let from = &mime_parser.from;
- let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
- for part in &mut mime_parser.parts {
- part.param.set(Param::OverrideSenderDisplayname, name);
-
- if chat.is_protected() {
- // In protected chat, also mark the message with an error.
- let s = stock_str::unknown_sender_for_chat(context).await;
- part.error = Some(s);
- }
+ apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
}
}
}
-
- group_changes = apply_group_changes(
- context,
- mime_parser,
- group_chat_id,
- from_id,
- to_ids,
- past_ids,
- &verified_encryption,
- )
- .await?;
- }
-
- if chat_id.is_none() {
- // check if the message belongs to a mailing list
- if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
- if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
+ ChatAssignment::ExistingChat {
+ chat_id: new_chat_id,
+ chat_id_blocked: new_chat_id_blocked,
+ } => {
+ chat_id = Some(*new_chat_id);
+ chat_id_blocked = *new_chat_id_blocked;
+ }
+ ChatAssignment::AdHocGroup => {
+ if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group(
context,
- allow_creation,
- mailinglist_header,
mime_parser,
+ to_ids,
+ from_id,
+ allow_creation || test_normal_chat.is_some(),
+ create_blocked,
+ is_partial_download.is_some(),
)
.await?
{
@@ -953,14 +1373,23 @@ async fn add_parts(
chat_id_blocked = new_chat_id_blocked;
}
}
+ ChatAssignment::OneOneChat => {}
}
- if let Some(chat_id) = chat_id {
- apply_mailinglist_changes(context, mime_parser, chat_id).await?;
+ // if the chat is somehow blocked but we want to create a non-blocked chat,
+ // unblock the chat
+ if chat_id_blocked != Blocked::Not
+ && create_blocked != Blocked::Yes
+ && !matches!(chat_assignment, ChatAssignment::MailingList)
+ {
+ if let Some(chat_id) = chat_id {
+ chat_id.set_blocked(context, create_blocked).await?;
+ chat_id_blocked = create_blocked;
+ }
}
if chat_id.is_none() {
- // try to create a normal chat
+ // Try to create a 1:1 chat.
let contact = Contact::get_by_id(context, from_id).await?;
let create_blocked = match contact.is_blocked() {
true => Blocked::Yes,
@@ -984,7 +1413,7 @@ async fn add_parts(
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
}
- if create_blocked == Blocked::Request && parent.is_some() {
+ if create_blocked == Blocked::Request && parent_message.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
@@ -1033,60 +1462,25 @@ async fn add_parts(
)
.await?;
}
- if let Some(peerstate) = &mime_parser.peerstate {
- restore_protection = new_protection != ProtectionStatus::Protected
- && peerstate.prefer_encrypt == EncryptPreference::Mutual
- // Check that the contact still has the Autocrypt key same as the
- // verified key, see also `Peerstate::is_using_verified_key()`.
- && contact.is_verified(context).await?;
- }
}
}
}
-
- state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
- // No check for `hidden` because only reactions are such and they should be `InFresh`.
- {
- MessageState::InSeen
- } else {
- MessageState::InFresh
- };
} else {
// Outgoing
- // the mail is on the IMAP server, probably it is also delivered.
- // We cannot recreate other states (read, error).
- state = MessageState::OutDelivered;
- to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
-
// Older Delta Chat versions with core <=1.152.2 only accepted
// self-sent messages in Saved Messages with own address in the `To` field.
// New Delta Chat versions may use empty `To` field
// with only a single `hidden-recipients` group in this case.
let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
- if mime_parser.sync_items.is_some() && self_sent {
- chat_id = Some(DC_CHAT_ID_TRASH);
- }
-
- // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
- // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
- // created by Thunderbird.
- let is_draft = mime_parser
- .get_header(HeaderDef::XMozillaDraftInfo)
- .is_some();
-
- if is_draft {
- // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
- info!(context, "Email is probably just a draft (TRASH).");
- chat_id = Some(DC_CHAT_ID_TRASH);
- }
-
- // Try to assign to a chat based on Chat-Group-ID.
- if chat_id.is_none() {
- if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
+ match &chat_assignment {
+ ChatAssignment::Trash => {
+ chat_id = Some(DC_CHAT_ID_TRASH);
+ }
+ ChatAssignment::GroupChat { grpid } => {
if let Some((id, _protected, blocked)) =
- chat::get_chat_id_by_grpid(context, &grpid).await?
+ chat::get_chat_id_by_grpid(context, grpid).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
@@ -1099,8 +1493,8 @@ async fn add_parts(
from_id,
to_ids,
past_ids,
- &verified_encryption,
- &grpid,
+ verified_encryption,
+ grpid,
)
.await?
{
@@ -1109,55 +1503,46 @@ async fn add_parts(
}
}
}
- }
-
- if mime_parser.decrypting_failed {
- if chat_id.is_none() {
- chat_id = Some(DC_CHAT_ID_TRASH);
- } else {
- hidden = true;
+ ChatAssignment::ExistingChat {
+ chat_id: new_chat_id,
+ chat_id_blocked: new_chat_id_blocked,
+ } => {
+ chat_id = Some(*new_chat_id);
+ chat_id_blocked = *new_chat_id_blocked;
}
- let last_time = context
- .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
- .await?;
- let now = tools::time();
- let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
- let mut msg =
- Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
- chat::add_device_msg(context, None, Some(&mut msg))
- .await
- .log_err(context)
- .ok();
- true
- } else {
- last_time > now
- };
- if update_config {
- context
- .set_config_internal(
- Config::LastCantDecryptOutgoingMsgs,
- Some(&now.to_string()),
- )
- .await?;
+ ChatAssignment::MailingList => {
+ // Check if the message belongs to a broadcast list.
+ if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
+ let listid = mailinglist_header_listid(mailinglist_header)?;
+ chat_id = Some(
+ if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await?
+ {
+ id
+ } else {
+ let name =
+ compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
+ chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
+ },
+ );
+ }
}
- }
-
- if chat_id.is_none() {
- if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
- context,
- mime_parser,
- &parent,
- to_ids,
- from_id,
- allow_creation,
- Blocked::Not,
- is_partial_download.is_some(),
- )
- .await?
- {
- chat_id = Some(new_chat_id);
- chat_id_blocked = new_chat_id_blocked;
+ ChatAssignment::AdHocGroup => {
+ if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group(
+ context,
+ mime_parser,
+ to_ids,
+ from_id,
+ allow_creation,
+ Blocked::Not,
+ is_partial_download.is_some(),
+ )
+ .await?
+ {
+ chat_id = Some(new_chat_id);
+ chat_id_blocked = new_chat_id_blocked;
+ }
}
+ ChatAssignment::OneOneChat => {}
}
if !to_ids.is_empty() {
@@ -1176,49 +1561,12 @@ async fn add_parts(
chat_id_blocked = chat.blocked;
}
}
- if chat_id.is_none() && is_dc_message == MessengerMessage::Yes {
+ if chat_id.is_none() && mime_parser.has_chat_version() {
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
-
- // automatically unblock chat when the user sends a message
- if chat_id_blocked != Blocked::Not {
- if let Some(chat_id) = chat_id {
- chat_id.unblock_ex(context, Nosync).await?;
- // Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
- }
- }
- }
-
- if let Some(chat_id) = chat_id {
- group_changes = apply_group_changes(
- context,
- mime_parser,
- chat_id,
- from_id,
- to_ids,
- past_ids,
- &verified_encryption,
- )
- .await?;
- }
-
- if chat_id.is_none() {
- // Check if the message belongs to a broadcast list.
- if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
- let listid = mailinglist_header_listid(mailinglist_header)?;
- chat_id = Some(
- if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
- id
- } else {
- let name =
- compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
- chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
- },
- );
- }
}
if chat_id.is_none() && self_sent {
@@ -1229,29 +1577,105 @@ async fn add_parts(
.context("Failed to get (new) chat for contact")?;
chat_id = Some(chat.id);
- // Not assigning `chat_id_blocked = chat.blocked` to avoid unused_assignments warning.
+ chat_id_blocked = chat.blocked;
if Blocked::Not != chat.blocked {
chat.id.unblock_ex(context, Nosync).await?;
}
}
- }
- if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
- if let Some(part) = mime_parser.parts.first() {
- if part.typ == Viewtype::Text && part.msg.is_empty() {
- chat_id = Some(DC_CHAT_ID_TRASH);
- info!(context, "Message is a status update only (TRASH).");
- markseen_on_imap_table(context, rfc724_mid).await.ok();
+ // automatically unblock chat when the user sends a message
+ if chat_id_blocked != Blocked::Not {
+ if let Some(chat_id) = chat_id {
+ chat_id.unblock_ex(context, Nosync).await?;
+ chat_id_blocked = Blocked::Not;
+ }
+ }
+ }
+ let chat_id = chat_id.unwrap_or_else(|| {
+ info!(context, "No chat id for message (TRASH).");
+ DC_CHAT_ID_TRASH
+ });
+ Ok((chat_id, chat_id_blocked))
+}
+
+/// Creates a `ReceivedMsg` from given parts which might consist of
+/// multiple messages (if there are multiple attachments).
+/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table.
+#[expect(clippy::too_many_arguments)]
+async fn add_parts(
+ context: &Context,
+ mime_parser: &mut MimeMessage,
+ imf_raw: &[u8],
+ to_ids: &[Option],
+ past_ids: &[Option],
+ rfc724_mid: &str,
+ from_id: ContactId,
+ seen: bool,
+ is_partial_download: Option,
+ mut replace_msg_id: Option,
+ prevent_rename: bool,
+ verified_encryption: VerifiedEncryption,
+ chat_id: ChatId,
+ chat_id_blocked: Blocked,
+ is_dc_message: MessengerMessage,
+) -> Result {
+ let to_id = if mime_parser.incoming {
+ ContactId::SELF
+ } else {
+ to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
+ };
+
+ // if contact renaming is prevented (for mailinglists and bots),
+ // we use name from From:-header as override name
+ if prevent_rename {
+ if let Some(name) = &mime_parser.from.display_name {
+ for part in &mut mime_parser.parts {
+ part.param.set(Param::OverrideSenderDisplayname, name);
}
}
}
- let orig_chat_id = chat_id;
- let mut chat_id = chat_id.unwrap_or_else(|| {
- info!(context, "No chat id for message (TRASH).");
- DC_CHAT_ID_TRASH
- });
+ if mime_parser.incoming && !chat_id.is_trash() {
+ // It can happen that the message is put into a chat
+ // but the From-address is not a member of this chat.
+ if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
+ let chat = Chat::load_from_db(context, chat_id).await?;
+
+ // Mark the sender as overridden.
+ // The UI will prepend `~` to the sender's name,
+ // indicating that the sender is not part of the group.
+ let from = &mime_parser.from;
+ let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
+ for part in &mut mime_parser.parts {
+ part.param.set(Param::OverrideSenderDisplayname, name);
+
+ if chat.is_protected() {
+ // In protected chat, also mark the message with an error.
+ let s = stock_str::unknown_sender_for_chat(context).await;
+ part.error = Some(s);
+ }
+ }
+ }
+ }
+
+ let is_location_kml = mime_parser.location_kml.is_some();
+ let is_mdn = !mime_parser.mdn_reports.is_empty();
+
+ let mut group_changes = apply_group_changes(
+ context,
+ mime_parser,
+ chat_id,
+ from_id,
+ to_ids,
+ past_ids,
+ &verified_encryption,
+ )
+ .await?;
+
+ let rfc724_mid_orig = &mime_parser
+ .get_rfc724_mid()
+ .unwrap_or(rfc724_mid.to_string());
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
let mut ephemeral_timer = if is_partial_download.is_some() {
@@ -1268,7 +1692,17 @@ async fn add_parts(
EphemeralTimer::Disabled
};
+ let state = if !mime_parser.incoming {
+ MessageState::OutDelivered
+ } else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
+ // No check for `hidden` because only reactions are such and they should be `InFresh`.
+ {
+ MessageState::InSeen
+ } else {
+ MessageState::InFresh
+ };
let in_fresh = state == MessageState::InFresh;
+
let sort_to_bottom = false;
let received = true;
let sort_timestamp = chat_id
@@ -1357,9 +1791,10 @@ async fn add_parts(
}
}
- if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
- better_msg = Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await);
-
+ let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
+ {
+ Some(stock_str::msg_location_enabled_by(context, from_id).await)
+ } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
// Do not delete the system message itself.
//
// This prevents confusion when timer is changed
@@ -1367,7 +1802,11 @@ async fn add_parts(
// hour, only the message about the change to 1
// week is left.
ephemeral_timer = EphemeralTimer::Disabled;
- }
+
+ Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await)
+ } else {
+ None
+ };
// if a chat is protected and the message is fully downloaded, check additional properties
if !chat_id.is_special() && is_partial_download.is_none() {
@@ -1448,6 +1887,16 @@ async fn add_parts(
}
}
+ let chat_id = if better_msg
+ .as_ref()
+ .is_some_and(|better_msg| better_msg.is_empty())
+ && is_partial_download.is_none()
+ {
+ DC_CHAT_ID_TRASH
+ } else {
+ chat_id
+ };
+
for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
chat::add_info_msg_with_cmd(
context,
@@ -1464,7 +1913,6 @@ async fn add_parts(
}
if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
- chat_id = DC_CHAT_ID_TRASH;
match mime_parser.get_header(HeaderDef::InReplyTo) {
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
Some((instance_id, _ts_sent)) => {
@@ -1490,11 +1938,10 @@ async fn add_parts(
}
}
- if handle_edit_delete(context, mime_parser, from_id).await? {
- chat_id = DC_CHAT_ID_TRASH;
- info!(context, "Message edits/deletes existing message (TRASH).");
- }
+ handle_edit_delete(context, mime_parser, from_id).await?;
+ let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
+ let hidden = is_reaction;
let mut parts = mime_parser.parts.iter().peekable();
while let Some(part) = parts.next() {
if part.is_reaction {
@@ -1503,7 +1950,7 @@ async fn add_parts(
set_msg_reaction(
context,
mime_in_reply_to,
- orig_chat_id.unwrap_or_default(),
+ chat_id,
from_id,
sort_timestamp,
Reaction::from(reaction_str.as_str()),
@@ -1532,9 +1979,6 @@ async fn add_parts(
}
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
- if better_msg.is_empty() && is_partial_download.is_none() {
- chat_id = DC_CHAT_ID_TRASH;
- }
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
@@ -1729,22 +2173,12 @@ RETURNING id
}
}
- if !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes {
- // Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
- // outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
- // delete it.
- needs_delete_job = true;
- }
- if restore_protection {
- chat_id
- .set_protection(
- context,
- ProtectionStatus::Protected,
- mime_parser.timestamp_rcvd,
- Some(from_id),
- )
- .await?;
- }
+ // Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
+ // outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
+ // delete it.
+ let needs_delete_job =
+ !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes;
+
Ok(ReceivedMsg {
chat_id,
state,
@@ -1752,8 +2186,6 @@ RETURNING id
sort_timestamp,
msg_ids: created_db_entries,
needs_delete_job,
- #[cfg(test)]
- from_is_signed: mime_parser.from_is_signed,
})
}
@@ -1765,7 +2197,7 @@ async fn handle_edit_delete(
context: &Context,
mime_parser: &MimeMessage,
from_id: ContactId,
-) -> Result {
+) -> Result<()> {
if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? {
if let Some(mut original_msg) =
@@ -1798,8 +2230,6 @@ async fn handle_edit_delete(
"Edit message: rfc724_mid {rfc724_mid:?} not found."
);
}
-
- Ok(true)
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
if let Some(part) = mime_parser.parts.first() {
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
@@ -1833,11 +2263,8 @@ async fn handle_edit_delete(
warn!(context, "Delete message: Not encrypted.");
}
}
-
- Ok(true)
- } else {
- Ok(false)
}
+ Ok(())
}
async fn tweak_sort_timestamp(
@@ -1929,61 +2356,58 @@ async fn save_locations(
async fn lookup_chat_by_reply(
context: &Context,
mime_parser: &MimeMessage,
- parent: &Option,
- to_ids: &[ContactId],
- from_id: ContactId,
+ parent: &Message,
+ is_partial_download: &Option,
) -> Result> {
- // Try to assign message to the same chat as the parent message.
+ // If the message is encrypted and has group ID,
+ // lookup by reply should never be needed
+ // as we can directly assign the message to the chat
+ // by its group ID.
+ debug_assert!(mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted());
- let Some(parent) = parent else {
- return Ok(None);
- };
+ // Try to assign message to the same chat as the parent message.
let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
return Ok(None);
};
- let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
// If this was a private message just to self, it was probably a private reply.
// It should not go into the group then, but into the private chat.
- if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
+ if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
return Ok(None);
}
- // If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
+ // If the parent chat is a 1:1 chat, and the sender added
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
// newly created ad-hoc group.
- if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
- let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
- chat_contacts.push(ContactId::SELF);
- if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
- return Ok(None);
- }
+ let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
+ if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
+ return Ok(None);
+ }
+
+ // Do not assign unencrypted messages to encrypted chats.
+ if is_partial_download.is_none()
+ && parent_chat.is_encrypted(context).await?
+ && !mime_parser.was_encrypted()
+ {
+ return Ok(None);
}
info!(
context,
- "Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
+ "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
);
Ok(Some((parent_chat.id, parent_chat.blocked)))
}
-#[expect(clippy::too_many_arguments)]
-async fn lookup_chat_or_create_adhoc_group(
+async fn lookup_or_create_adhoc_group(
context: &Context,
mime_parser: &MimeMessage,
- parent: &Option,
- to_ids: &[ContactId],
+ to_ids: &[Option],
from_id: ContactId,
allow_creation: bool,
create_blocked: Blocked,
is_partial_download: bool,
) -> Result> {
- if let Some((new_chat_id, new_chat_id_blocked)) =
- // Try to assign to a chat based on In-Reply-To/References.
- lookup_chat_by_reply(context, mime_parser, parent, to_ids, from_id).await?
- {
- return Ok(Some((new_chat_id, new_chat_id_blocked)));
- }
// Partial download may be an encrypted message with protected Subject header. We do not want to
// create a group with "..." or "Encrypted message" as a subject. The same is for undecipherable
// messages. Instead, assign the message to 1:1 chat with the sender.
@@ -2006,8 +2430,9 @@ async fn lookup_chat_or_create_adhoc_group(
.get_subject()
.map(|s| remove_subject_prefix(&s))
.unwrap_or_else(|| "👥📧".to_string());
+ let to_ids: Vec = to_ids.iter().filter_map(|x| *x).collect();
let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
- contact_ids.extend(to_ids);
+ contact_ids.extend(&to_ids);
if !contact_ids.contains(&from_id) {
contact_ids.push(from_id);
}
@@ -2063,7 +2488,7 @@ async fn lookup_chat_or_create_adhoc_group(
mime_parser,
create_blocked,
from_id,
- to_ids,
+ &to_ids,
&grpname,
)
.await
@@ -2074,11 +2499,14 @@ async fn lookup_chat_or_create_adhoc_group(
/// If it returns false, it shall be assigned to the parent chat.
async fn is_probably_private_reply(
context: &Context,
- to_ids: &[ContactId],
- from_id: ContactId,
mime_parser: &MimeMessage,
parent_chat_id: ChatId,
) -> Result {
+ // Message cannot be a private reply if it has an explicit Chat-Group-ID header.
+ if mime_parser.get_chat_group_id().is_some() {
+ return Ok(false);
+ }
+
// Usually we don't want to show private replies in the parent chat, but in the
// 1:1 chat with the sender.
//
@@ -2086,14 +2514,7 @@ async fn is_probably_private_reply(
// should be assigned to the group chat. We restrict this exception to classical emails, as chat-group-messages
// contain a Chat-Group-Id header and can be sorted into the correct chat this way.
- let private_message =
- (to_ids == [ContactId::SELF]) || (from_id == ContactId::SELF && to_ids.len() == 1);
- if !private_message {
- return Ok(false);
- }
-
- // Message cannot be a private reply if it has an explicit Chat-Group-ID header.
- if mime_parser.get_chat_group_id().is_some() {
+ if mime_parser.recipients.len() != 1 {
return Ok(false);
}
@@ -2119,24 +2540,15 @@ async fn create_group(
is_partial_download: bool,
create_blocked: Blocked,
from_id: ContactId,
- to_ids: &[ContactId],
- past_ids: &[ContactId],
+ to_ids: &[Option],
+ past_ids: &[Option],
verified_encryption: &VerifiedEncryption,
grpid: &str,
) -> Result> {
+ let to_ids_flat: Vec = to_ids.iter().filter_map(|x| *x).collect();
let mut chat_id = None;
let mut chat_id_blocked = Default::default();
- // For chat messages, we don't have to guess (is_*probably*_private_reply()) but we know for sure that
- // they belong to the group because of the Chat-Group-Id or Message-Id header
- if let Some(chat_id) = chat_id {
- if !mime_parser.has_chat_version()
- && is_probably_private_reply(context, to_ids, from_id, mime_parser, chat_id).await?
- {
- return Ok(None);
- }
- }
-
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(
@@ -2199,8 +2611,8 @@ async fn create_group(
// Create initial member list.
if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
let mut new_to_ids = to_ids.to_vec();
- if !new_to_ids.contains(&from_id) {
- new_to_ids.insert(0, from_id);
+ if !new_to_ids.contains(&Some(from_id)) {
+ new_to_ids.insert(0, Some(from_id));
chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
}
@@ -2218,7 +2630,7 @@ async fn create_group(
if !from_id.is_special() {
members.push(from_id);
}
- members.extend(to_ids);
+ members.extend(to_ids_flat);
// Add all members with 0 timestamp
// because we don't know the real timestamp of their addition.
@@ -2257,8 +2669,8 @@ async fn update_chats_contacts_timestamps(
context: &Context,
chat_id: ChatId,
ignored_id: Option,
- to_ids: &[ContactId],
- past_ids: &[ContactId],
+ to_ids: &[Option],
+ past_ids: &[Option],
chat_group_member_timestamps: &[i64],
) -> Result {
let expected_timestamps_count = to_ids.len() + past_ids.len();
@@ -2291,11 +2703,13 @@ async fn update_chats_contacts_timestamps(
to_ids.iter(),
chat_group_member_timestamps.iter().take(to_ids.len()),
) {
- if Some(*contact_id) != ignored_id {
- // It could be that member was already added,
- // but updated addition timestamp
- // is also a modification worth notifying about.
- modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
+ if let Some(contact_id) = contact_id {
+ if Some(*contact_id) != ignored_id {
+ // It could be that member was already added,
+ // but updated addition timestamp
+ // is also a modification worth notifying about.
+ modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
+ }
}
}
@@ -2312,10 +2726,12 @@ async fn update_chats_contacts_timestamps(
past_ids.iter(),
chat_group_member_timestamps.iter().skip(to_ids.len()),
) {
- // It could be that member was already removed,
- // but updated removal timestamp
- // is also a modification worth notifying about.
- modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
+ if let Some(contact_id) = contact_id {
+ // It could be that member was already removed,
+ // but updated removal timestamp
+ // is also a modification worth notifying about.
+ modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
+ }
}
Ok(())
@@ -2352,10 +2768,11 @@ async fn apply_group_changes(
mime_parser: &mut MimeMessage,
chat_id: ChatId,
from_id: ContactId,
- to_ids: &[ContactId],
- past_ids: &[ContactId],
+ to_ids: &[Option],
+ past_ids: &[Option],
verified_encryption: &VerifiedEncryption,
) -> Result {
+ let to_ids_flat: Vec = to_ids.iter().filter_map(|x| *x).collect();
if chat_id.is_special() {
// Do not apply group changes to the trash chat.
return Ok(GroupChangesInfo::default());
@@ -2408,27 +2825,46 @@ async fn apply_group_changes(
}
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
- removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
+ // TODO: if address "alice@example.org" is a member of the group twice,
+ // with old and new key,
+ // and someone (maybe Alice's new contact) just removed Alice's old contact,
+ // we may lookup the wrong contact because we only look up by the address.
+ // The result is that info message may contain the new Alice's display name
+ // rather than old display name.
+ // This could be fixed by looking up the contact with the highest
+ // `remove_timestamp` after applying Chat-Group-Member-Timestamps.
+ removed_id = lookup_key_contact_by_address(context, removed_addr, Some(chat_id)).await?;
if let Some(id) = removed_id {
better_msg = if id == from_id {
silent = true;
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
- Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
+ Some(stock_str::msg_del_member_local(context, id, from_id).await)
};
} else {
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
- if let Some(contact_id) =
- Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
- {
- added_id = Some(contact_id);
+ if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
+ // TODO: if gossiped keys contain the same address multiple times,
+ // we may lookup the wrong contact.
+ // This could be fixed by looking up the contact with
+ // highest `add_timestamp` to disambiguate.
+ // The result of the error is that info message
+ // may contain display name of the wrong contact.
+ let fingerprint = key.dc_fingerprint().hex();
+ if let Some(contact_id) =
+ lookup_key_contact_by_fingerprint(context, &fingerprint).await?
+ {
+ added_id = Some(contact_id);
+ better_msg =
+ Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
+ } else {
+ warn!(context, "Added {added_addr:?} has no contact id.");
+ }
} else {
- warn!(context, "Added {added_addr:?} has no contact id.");
+ warn!(context, "Added {added_addr:?} has no gossiped key.");
}
-
- better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
}
let group_name_timestamp = mime_parser
@@ -2499,7 +2935,8 @@ async fn apply_group_changes(
if is_from_in_chat {
if chat.member_list_is_stale(context).await? {
info!(context, "Member list is stale.");
- let mut new_members: HashSet = HashSet::from_iter(to_ids.iter().copied());
+ let mut new_members: HashSet =
+ HashSet::from_iter(to_ids_flat.iter().copied());
new_members.insert(ContactId::SELF);
if !from_id.is_special() {
new_members.insert(from_id);
@@ -2541,9 +2978,9 @@ async fn apply_group_changes(
)
.await?;
} else {
- let mut new_members;
+ let mut new_members: HashSet;
if self_added {
- new_members = HashSet::from_iter(to_ids.iter().copied());
+ new_members = HashSet::from_iter(to_ids_flat.iter().copied());
new_members.insert(ContactId::SELF);
if !from_id.is_special() {
new_members.insert(from_id);
@@ -2556,7 +2993,7 @@ async fn apply_group_changes(
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
// Don't delete any members locally, but instead add absent ones to provide group
// membership consistency for all members:
- new_members.extend(to_ids.iter());
+ new_members.extend(to_ids_flat.iter());
}
// Apply explicit addition if any.
@@ -2678,7 +3115,7 @@ async fn group_changes_msgs(
removed_ids: &HashSet,
chat_id: ChatId,
) -> Result)>> {
- let mut group_changes_msgs = Vec::new();
+ let mut group_changes_msgs: Vec<(String, SystemMessage, Option)> = Vec::new();
if !added_ids.is_empty() {
warn!(
context,
@@ -2693,21 +3130,17 @@ async fn group_changes_msgs(
}
group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
for contact_id in added_ids {
- let contact = Contact::get_by_id(context, *contact_id).await?;
group_changes_msgs.push((
- stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
- .await,
+ stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
SystemMessage::MemberAddedToGroup,
- Some(contact.id),
+ Some(*contact_id),
));
}
for contact_id in removed_ids {
- let contact = Contact::get_by_id(context, *contact_id).await?;
group_changes_msgs.push((
- stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
- .await,
+ stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
SystemMessage::MemberRemovedFromGroup,
- Some(contact.id),
+ Some(*contact_id),
));
}
@@ -2964,7 +3397,11 @@ async fn create_adhoc_group(
);
return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
}
- if member_ids.len() < 3 {
+ if member_ids.len() < 2 {
+ info!(
+ context,
+ "Not creating ad hoc group with less than 2 members."
+ );
return Ok(None);
}
@@ -3005,70 +3442,11 @@ enum VerifiedEncryption {
NotVerified(String), // The string contains the reason why it's not verified
}
-/// Moves secondary verified key to primary verified key
-/// if the message is signed with a secondary verified key.
-/// Removes secondary verified key if the message is signed with primary key.
-async fn update_verified_keys(
- context: &Context,
- mimeparser: &mut MimeMessage,
- from_id: ContactId,
-) -> Result> {
- if from_id == ContactId::SELF {
- return Ok(None);
- }
-
- if !mimeparser.was_encrypted() {
- return Ok(None);
- }
-
- let Some(peerstate) = &mut mimeparser.peerstate else {
- // No peerstate means no verified keys.
- return Ok(None);
- };
-
- let signed_with_primary_verified_key = peerstate
- .verified_key_fingerprint
- .as_ref()
- .filter(|fp| mimeparser.signatures.contains(fp))
- .is_some();
- let signed_with_secondary_verified_key = peerstate
- .secondary_verified_key_fingerprint
- .as_ref()
- .filter(|fp| mimeparser.signatures.contains(fp))
- .is_some();
-
- if signed_with_primary_verified_key {
- // Remove secondary key if it exists.
- if peerstate.secondary_verified_key.is_some()
- || peerstate.secondary_verified_key_fingerprint.is_some()
- || peerstate.secondary_verifier.is_some()
- {
- peerstate.secondary_verified_key = None;
- peerstate.secondary_verified_key_fingerprint = None;
- peerstate.secondary_verifier = None;
- peerstate.save_to_db(&context.sql).await?;
- }
-
- // No need to notify about secondary key removal.
- Ok(None)
- } else if signed_with_secondary_verified_key {
- peerstate.verified_key = peerstate.secondary_verified_key.take();
- peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
- peerstate.verifier = peerstate.secondary_verifier.take();
- peerstate.fingerprint_changed = true;
- peerstate.save_to_db(&context.sql).await?;
-
- // Primary verified key changed.
- Ok(None)
- } else {
- Ok(None)
- }
-}
-
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted and signed with a verified key.
-fn has_verified_encryption(
+async fn has_verified_encryption(
+ context: &Context,
mimeparser: &MimeMessage,
from_id: ContactId,
) -> Result {
@@ -3078,107 +3456,50 @@ fn has_verified_encryption(
return Ok(NotVerified("This message is not encrypted".to_string()));
};
- // ensure, the contact is verified
- // and the message is signed with a verified key of the sender.
- // this check is skipped for SELF as there is no proper SELF-peerstate
- // and results in group-splits otherwise.
- if from_id != ContactId::SELF {
- let Some(peerstate) = &mimeparser.peerstate else {
- return Ok(NotVerified(
- "No peerstate, the contact isn't verified".to_string(),
- ));
- };
-
- let signed_with_verified_key = peerstate
- .verified_key_fingerprint
- .as_ref()
- .filter(|fp| mimeparser.signatures.contains(fp))
- .is_some();
-
- if !signed_with_verified_key {
- return Ok(NotVerified(
- "The message was sent with non-verified encryption".to_string(),
- ));
- }
+ if from_id == ContactId::SELF {
+ return Ok(Verified);
}
- Ok(Verified)
+ let from_contact = Contact::get_by_id(context, from_id).await?;
+
+ let Some(fingerprint) = from_contact.fingerprint() else {
+ return Ok(NotVerified(
+ "The message was sent without encryption".to_string(),
+ ));
+ };
+
+ if from_contact.get_verifier_id(context).await?.is_none() {
+ return Ok(NotVerified(
+ "The message was sent by non-verified contact".to_string(),
+ ));
+ }
+
+ let signed_with_verified_key = mimeparser.signatures.contains(&fingerprint);
+ if signed_with_verified_key {
+ Ok(Verified)
+ } else {
+ Ok(NotVerified(
+ "The message was sent with non-verified encryption".to_string(),
+ ))
+ }
}
async fn mark_recipients_as_verified(
context: &Context,
from_id: ContactId,
- to_ids: &[ContactId],
+ to_ids: &[Option],
mimeparser: &MimeMessage,
) -> Result<()> {
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
return Ok(());
}
- let contact = Contact::get_by_id(context, from_id).await?;
- for &id in to_ids {
- if id == ContactId::SELF {
+ for to_id in to_ids.iter().filter_map(|&x| x) {
+ if to_id == ContactId::SELF {
continue;
}
- let Some((to_addr, is_verified)) = context
- .sql
- .query_row_optional(
- "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c
- LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id=?",
- (id,),
- |row| {
- let to_addr: String = row.get(0)?;
- let is_verified: i32 = row.get(1).unwrap_or(0);
- Ok((to_addr, is_verified != 0))
- },
- )
- .await?
- else {
- continue;
- };
- // mark gossiped keys (if any) as verified
- if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) {
- if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
- // If we're here, we know the gossip key is verified.
- //
- // Use the gossip-key as verified-key if there is no verified-key.
- //
- // Store gossip key as secondary verified key if there is a verified key and
- // gossiped key is different.
- //
- // See
- // and for discussion.
- let verifier_addr = contact.get_addr().to_owned();
- if !is_verified {
- info!(context, "{verifier_addr} has verified {to_addr}.");
- if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
- peerstate.set_verified(gossiped_key.clone(), fp, verifier_addr)?;
- peerstate.backward_verified_key_id =
- Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
- peerstate.save_to_db(&context.sql).await?;
-
- let (to_contact_id, _) = Contact::add_or_lookup(
- context,
- "",
- &ContactAddress::new(&to_addr)?,
- Origin::Hidden,
- )
- .await?;
- ChatId::set_protection_for_contact(
- context,
- to_contact_id,
- mimeparser.timestamp_sent,
- )
- .await?;
- }
- } else {
- // The contact already has a verified key.
- // Store gossiped key as the secondary verified key.
- peerstate.set_secondary_verified_key(gossiped_key.clone(), verifier_addr);
- peerstate.save_to_db(&context.sql).await?;
- }
- }
- }
+ mark_contact_id_as_verified(context, to_id, from_id).await?;
+ ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?;
}
Ok(())
@@ -3237,11 +3558,12 @@ async fn add_or_lookup_contacts_by_address_list(
context: &Context,
address_list: &[SingleInfo],
origin: Origin,
-) -> Result> {
+) -> Result>> {
let mut contact_ids = Vec::new();
for info in address_list {
let addr = &info.addr;
if !may_be_valid_addr(addr) {
+ contact_ids.push(None);
continue;
}
let display_name = info.display_name.as_deref();
@@ -3249,14 +3571,215 @@ async fn add_or_lookup_contacts_by_address_list(
let (contact_id, _) =
Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
.await?;
- contact_ids.push(contact_id);
+ contact_ids.push(Some(contact_id));
} else {
warn!(context, "Contact with address {:?} cannot exist.", addr);
+ contact_ids.push(None);
}
}
Ok(contact_ids)
}
+/// Looks up contact IDs from the database given the list of recipients.
+async fn add_or_lookup_key_contacts_by_address_list(
+ context: &Context,
+ address_list: &[SingleInfo],
+ gossiped_keys: &HashMap,
+ fingerprints: &[Fingerprint],
+ origin: Origin,
+) -> Result>> {
+ let mut contact_ids = Vec::new();
+ let mut fingerprint_iter = fingerprints.iter();
+ for info in address_list {
+ let addr = &info.addr;
+ if !may_be_valid_addr(addr) {
+ contact_ids.push(None);
+ continue;
+ }
+ let fingerprint: String = if let Some(fp) = fingerprint_iter.next() {
+ // Iterator has not ran out of fingerprints yet.
+ fp.hex()
+ } else if let Some(key) = gossiped_keys.get(addr) {
+ key.dc_fingerprint().hex()
+ } else {
+ contact_ids.push(None);
+ continue;
+ };
+ let display_name = info.display_name.as_deref();
+ if let Ok(addr) = ContactAddress::new(addr) {
+ let (contact_id, _) = Contact::add_or_lookup_ex(
+ context,
+ display_name.unwrap_or_default(),
+ &addr,
+ &fingerprint,
+ origin,
+ )
+ .await?;
+ contact_ids.push(Some(contact_id));
+ } else {
+ warn!(context, "Contact with address {:?} cannot exist.", addr);
+ contact_ids.push(None);
+ }
+ }
+
+ debug_assert_eq!(contact_ids.len(), address_list.len());
+ Ok(contact_ids)
+}
+
+/// Looks up a key-contact by email address.
+///
+/// If provided, `chat_id` must be an encrypted chat ID that has key-contacts inside.
+/// Otherwise the function searches in all contacts, returning the recently seen one.
+async fn lookup_key_contact_by_address(
+ context: &Context,
+ addr: &str,
+ chat_id: Option,
+) -> Result> {
+ if context.is_self_addr(addr).await? {
+ let is_self_in_chat = context
+ .sql
+ .exists(
+ "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
+ (chat_id,),
+ )
+ .await?;
+ if is_self_in_chat {
+ return Ok(Some(ContactId::SELF));
+ }
+ }
+ let contact_id: Option = match chat_id {
+ Some(chat_id) => {
+ context
+ .sql
+ .query_row_optional(
+ "SELECT id FROM contacts
+ WHERE contacts.addr=?
+ AND EXISTS (SELECT 1 FROM chats_contacts
+ WHERE contact_id=contacts.id
+ AND chat_id=?)
+ AND fingerprint<>'' -- Should always be true
+ ",
+ (addr, chat_id),
+ |row| {
+ let contact_id: ContactId = row.get(0)?;
+ Ok(contact_id)
+ },
+ )
+ .await?
+ }
+ None => {
+ context
+ .sql
+ .query_row_optional(
+ "SELECT id FROM contacts
+ WHERE contacts.addr=?1
+ AND fingerprint<>''
+ ORDER BY last_seen DESC, id DESC
+ ",
+ (addr,),
+ |row| {
+ let contact_id: ContactId = row.get(0)?;
+ Ok(contact_id)
+ },
+ )
+ .await?
+ }
+ };
+ Ok(contact_id)
+}
+
+async fn lookup_key_contact_by_fingerprint(
+ context: &Context,
+ fingerprint: &str,
+) -> Result> {
+ debug_assert!(!fingerprint.is_empty());
+ if fingerprint.is_empty() {
+ // Avoid accidentally looking up a non-key-contact.
+ return Ok(None);
+ }
+ if let Some(contact_id) = context
+ .sql
+ .query_row_optional(
+ "SELECT id FROM contacts
+ WHERE fingerprint=? AND fingerprint!=''",
+ (fingerprint,),
+ |row| {
+ let contact_id: ContactId = row.get(0)?;
+ Ok(contact_id)
+ },
+ )
+ .await?
+ {
+ Ok(Some(contact_id))
+ } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
+ if self_fp == fingerprint {
+ Ok(Some(ContactId::SELF))
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+}
+
+/// Looks up key-contacts by email addresses.
+///
+/// `fingerprints` may be empty.
+/// This is used as a fallback when email addresses are available,
+/// but not the fingerprints, e.g. when core 1.157.3
+/// client sends the `To` and `Chat-Group-Past-Members` header
+/// but not the corresponding fingerprint list.
+///
+/// Lookup is restricted to the chat ID.
+///
+/// If contact cannot be found, `None` is returned.
+/// This ensures that the length of the result vector
+/// is the same as the number of addresses in the header
+/// and it is possible to find corresponding
+/// `Chat-Group-Member-Timestamps` items.
+async fn lookup_key_contacts_by_address_list(
+ context: &Context,
+ address_list: &[SingleInfo],
+ fingerprints: &[Fingerprint],
+ chat_id: Option,
+) -> Result>> {
+ let mut contact_ids = Vec::new();
+ let mut fingerprint_iter = fingerprints.iter();
+ for info in address_list {
+ let addr = &info.addr;
+ if !may_be_valid_addr(addr) {
+ contact_ids.push(None);
+ continue;
+ }
+
+ if let Some(fp) = fingerprint_iter.next() {
+ // Iterator has not ran out of fingerprints yet.
+ let display_name = info.display_name.as_deref();
+ let fingerprint: String = fp.hex();
+
+ if let Ok(addr) = ContactAddress::new(addr) {
+ let (contact_id, _) = Contact::add_or_lookup_ex(
+ context,
+ display_name.unwrap_or_default(),
+ &addr,
+ &fingerprint,
+ Origin::Hidden,
+ )
+ .await?;
+ contact_ids.push(Some(contact_id));
+ } else {
+ warn!(context, "Contact with address {:?} cannot exist.", addr);
+ contact_ids.push(None);
+ }
+ } else {
+ let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
+ contact_ids.push(contact_id);
+ }
+ }
+ debug_assert_eq!(address_list.len(), contact_ids.len());
+ Ok(contact_ids)
+}
+
#[cfg(test)]
mod receive_imf_tests;
diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs
index 5b1ce99e6..1555edf33 100644
--- a/src/receive_imf/receive_imf_tests.rs
+++ b/src/receive_imf/receive_imf_tests.rs
@@ -10,13 +10,14 @@ use crate::chat::{
ChatVisibility,
};
use crate::chatlist::Chatlist;
-use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
+use crate::constants::DC_GCL_FOR_FORWARDING;
use crate::contact;
use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::imap::prefetch_should_download;
use crate::imex::{imex, ImexMode};
use crate::securejoin::get_securejoin_qr;
-use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
+use crate::test_utils::mark_as_verified;
+use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
use crate::tools::{time, SystemTime};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -270,120 +271,6 @@ async fn test_adhoc_groups_merge() -> Result<()> {
Ok(())
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_read_receipt_and_unarchive() -> Result<()> {
- // create alice's account
- let t = TestContext::new_alice().await;
-
- let bob_id = Contact::create(&t, "bob", "bob@example.com").await?;
- let one2one_id = ChatId::create_for_contact(&t, bob_id).await?;
- one2one_id
- .set_visibility(&t, ChatVisibility::Archived)
- .await
- .unwrap();
- let one2one = Chat::load_from_db(&t, one2one_id).await?;
- assert!(one2one.get_visibility() == ChatVisibility::Archived);
-
- // create a group with bob, archive group
- let group_id = chat::create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
- chat::add_contact_to_chat(&t, group_id, bob_id).await?;
- assert_eq!(chat::get_chat_msgs(&t, group_id).await.unwrap().len(), 0);
- group_id
- .set_visibility(&t, ChatVisibility::Archived)
- .await?;
- let group = Chat::load_from_db(&t, group_id).await?;
- assert!(group.get_visibility() == ChatVisibility::Archived);
-
- // everything archived, chatlist should be empty
- assert_eq!(
- Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None)
- .await?
- .len(),
- 0
- );
-
- // send a message to group with bob
- receive_imf(
- &t,
- format!(
- "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: foo\n\
- Message-ID: \n\
- Chat-Version: 1.0\n\
- Chat-Group-ID: {}\n\
- Chat-Group-Name: foo\n\
- Chat-Disposition-Notification-To: alice@example.org\n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- hello\n",
- group.grpid, group.grpid
- )
- .as_bytes(),
- false,
- )
- .await?;
- let msg = get_chat_msg(&t, group_id, 0, 1).await;
- assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
- assert_eq!(msg.text, "hello");
- assert_eq!(msg.state, MessageState::OutDelivered);
- let group = Chat::load_from_db(&t, group_id).await?;
- assert!(group.get_visibility() == ChatVisibility::Normal);
-
- // bob sends a read receipt to the group
- 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: 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: \n\
- Disposition: manual-action/MDN-sent-automatically; displayed\n\
- \n\
- \n\
- --SNIPP--",
- group.grpid
- )
- .as_bytes(),
- false,
- )
- .await?;
- assert_eq!(chat::get_chat_msgs(&t, group_id).await?.len(), 1);
- let msg = message::Message::load_from_db(&t, msg.id).await?;
- assert_eq!(msg.state, MessageState::OutMdnRcvd);
-
- // check, the read-receipt has not unarchived the one2one
- assert_eq!(
- Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None)
- .await?
- .len(),
- 1
- );
- let one2one = Chat::load_from_db(&t, one2one_id).await?;
- assert!(one2one.get_visibility() == ChatVisibility::Archived);
- Ok(())
-}
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_and_alias() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -1529,7 +1416,7 @@ async fn test_mailing_list_with_mimepart_footer_signed() {
}
/// Test that the changes from apply_mailinglist_changes() are also applied
-/// if the message is assigned to the chat by In-Reply-To
+/// if the message is a reply.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_apply_mailinglist_changes_assigned_by_reply() {
let t = TestContext::new_alice().await;
@@ -1548,10 +1435,6 @@ async fn test_apply_mailinglist_changes_assigned_by_reply() {
t.get_last_msg().await.in_reply_to.unwrap(),
"3333@example.org"
);
- // `Assigning message to Chat#... as it's a reply to 3333@example.org`
- t.evtracker
- .get_info_contains("as it's a reply to 3333@example.org")
- .await;
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(!chat.can_send(&t).await.unwrap());
@@ -1782,110 +1665,6 @@ async fn test_in_reply_to() {
assert!(!msg.chat_id.is_special());
}
-/// Test that classical MUA messages are assigned to group chats
-/// based on the `In-Reply-To` header for two-member groups.
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_in_reply_to_two_member_group() {
- let t = TestContext::new().await;
- t.configure_addr("bob@example.com").await;
-
- // Receive message from Alice about group "foo".
- 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: foo\n\
- Message-ID: \n\
- Chat-Version: 1.0\n\
- Chat-Group-ID: foobarbaz12\n\
- Chat-Group-Name: foo\n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- hello foo\n",
- false,
- )
- .await
- .unwrap();
-
- // Receive a classic MUA reply from Alice.
- // It is assigned to the group chat.
- 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: Re: foo\n\
- Message-ID: \n\
- In-Reply-To: \n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- classic reply\n",
- false,
- )
- .await
- .unwrap();
-
- // Ensure message is assigned to group chat.
- let msg = t.get_last_msg().await;
- let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
- assert_eq!(chat.typ, Chattype::Group);
- assert_eq!(msg.get_text(), "classic reply");
-
- // Receive a Delta Chat reply from Alice.
- // It is assigned to group chat, because it has a group ID.
- 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: Re: foo\n\
- Message-ID: \n\
- In-Reply-To: \n\
- Chat-Version: 1.0\n\
- Chat-Group-ID: foobarbaz12\n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- chat reply\n",
- false,
- )
- .await
- .unwrap();
-
- // Ensure message is assigned to group chat.
- let msg = t.get_last_msg().await;
- let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
- assert_eq!(chat.typ, Chattype::Group);
- assert_eq!(msg.get_text(), "chat reply");
-
- // Receive a private Delta Chat reply from Alice.
- // It is assigned to 1:1 chat, because it has no group ID,
- // which means it was created using "reply privately" feature.
- // Normally it contains a quote, but it should not matter.
- 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: Re: foo\n\
- Message-ID: \n\
- In-Reply-To: \n\
- Chat-Version: 1.0\n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- private reply\n",
- false,
- )
- .await
- .unwrap();
-
- // Ensure message is assigned to a 1:1 chat.
- let msg = t.get_last_msg().await;
- let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
- assert_eq!(chat.typ, Chattype::Single);
- assert_eq!(msg.get_text(), "private reply");
-}
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_mime_headers_off() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
@@ -2053,17 +1832,21 @@ async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: boo
"bob@example.net"
); // Bob is not part of the group, so override-sender-name should be set
- // Check that Claire also gets the message in the same chat.
+ // Claire gets the reply as ad hoc group.
let request = claire.get_last_msg().await;
receive_imf(&claire, reply.as_bytes(), false).await.unwrap();
let answer = claire.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().contains("the version is 1.0"));
- assert_eq!(answer.chat_id, request.chat_id);
- assert_eq!(
- answer.get_override_sender_name().unwrap(),
- "bob@example.net"
- );
+ if group_request {
+ assert_eq!(answer.chat_id, request.chat_id);
+ assert_eq!(
+ answer.get_override_sender_name().unwrap(),
+ "bob@example.net"
+ );
+ } else {
+ assert_ne!(answer.chat_id, request.chat_id);
+ }
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2561,7 +2344,7 @@ async fn test_chat_assignment_private_chat_reply() {
Subject: =?utf-8?q?single_reply-to?=
{}
Date: Fri, 28 May 2021 10:15:05 +0000
-To: Bob {}
+To: Bob , Charlie {}
From: Alice
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Content-Transfer-Encoding: quoted-printable
@@ -2752,8 +2535,10 @@ Reply to all"#,
/// headers.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_assignment_adhoc() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
+ let mut tcm = TestContextManager::new();
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
+ let fiona = tcm.fiona().await;
let first_thread_mime = br#"Subject: First thread
Message-ID: first@example.org
@@ -2783,8 +2568,8 @@ Second thread."#;
let bob_second_msg = bob.get_last_msg().await;
// Messages go to separate chats both for Alice and Bob.
- assert!(alice_first_msg.chat_id != alice_second_msg.chat_id);
- assert!(bob_first_msg.chat_id != bob_second_msg.chat_id);
+ assert_ne!(alice_first_msg.chat_id, alice_second_msg.chat_id);
+ assert_ne!(bob_first_msg.chat_id, bob_second_msg.chat_id);
// Alice replies to both chats. Bob receives two messages and assigns them to corresponding
// chats.
@@ -2803,8 +2588,7 @@ Second thread."#;
assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id);
// Alice adds Fiona to both ad hoc groups.
- let fiona = TestContext::new_fiona().await;
- let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await;
+ let alice_fiona_contact = alice.add_or_lookup_address_contact(&fiona).await;
let alice_fiona_contact_id = alice_fiona_contact.id;
chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?;
@@ -2817,104 +2601,7 @@ Second thread."#;
// Fiona was added to two separate chats and should see two separate chats, even though they
// don't have different group IDs to distinguish them.
- assert!(fiona_first_invite.chat_id != fiona_second_invite.chat_id);
-
- Ok(())
-}
-
-/// Test that `Chat-Group-ID` is preferred over `In-Reply-To` and `References`.
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_chat_assignment_chat_group_id_preference() -> Result<()> {
- let t = &TestContext::new_alice().await;
-
- receive_imf(
- t,
- br#"Subject: Hello
-Chat-Group-ID: eJ_llQIXf0K
-Chat-Group-Name: Group name
-Chat-Version: 1.0
-Message-ID:
-References:
-Date: Fri, 28 May 2021 10:15:05 +0000
-From: Alice
-To: Bob ,
-Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
-Content-Transfer-Encoding: quoted-printable
-
-Hello, I've just created a group for us."#,
- false,
- )
- .await?;
- let group_msg = t.get_last_msg().await;
-
- receive_imf(
- t,
- br#"Subject: Hello
-Chat-Version: 1.0
-Message-ID:
-References:
-Date: Fri, 28 May 2021 10:15:05 +0000
-From: Bob
-To: Alice
-Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
-Content-Transfer-Encoding: quoted-printable
-
-Hello from Bob in 1:1 chat."#,
- false,
- )
- .await?;
-
- // References and In-Reply-To point to a message
- // already assigned to 1:1 chat, but Chat-Group-ID is
- // a stronger signal to assign message to a group.
- receive_imf(
- t,
- br#"Subject: Hello
-Chat-Group-ID: eJ_llQIXf0K
-Chat-Group-Name: Group name
-Chat-Version: 1.0
-Message-ID:
-In-Reply-To:
-References:
-Date: Fri, 28 May 2021 10:15:05 +0000
-From: Bob
-To: Alice ,
-Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
-Content-Transfer-Encoding: quoted-printable
-
-Hello from Bob in a group."#,
- false,
- )
- .await?;
-
- let msg = t.get_last_msg().await;
- assert_eq!(msg.text, "Hello from Bob in a group.");
- assert_eq!(msg.chat_id, group_msg.chat_id);
-
- // Test outgoing message as well.
- receive_imf(
- t,
- br#"Subject: Hello
-Chat-Group-ID: eJ_llQIXf0K
-Chat-Group-Name: Group name
-Chat-Version: 1.0
-Message-ID:
-In-Reply-To:
-References:
-Date: Fri, 28 May 2021 10:15:05 +0000
-From: Alice
-To: Bob ,
-Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
-Content-Transfer-Encoding: quoted-printable
-
-Hello from Alice in a group."#,
- false,
- )
- .await?;
-
- let msg_outgoing = t.get_last_msg().await;
- assert_eq!(msg_outgoing.text, "Hello from Alice in a group.");
- assert_eq!(msg_outgoing.chat_id, group_msg.chat_id);
+ assert_ne!(fiona_first_invite.chat_id, fiona_second_invite.chat_id);
Ok(())
}
@@ -3111,20 +2798,16 @@ Message with references."#;
/// Test a message with RFC 1847 encapsulation as created by Thunderbird.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_rfc1847_encapsulation() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
- // Alice sends an Autocrypt message to Bob so Bob gets Alice's key.
- let chat_alice = alice.create_chat(&bob).await;
- let first_msg = alice
- .send_text(chat_alice.id, "Sending Alice key to Bob.")
- .await;
- bob.recv_msg(&first_msg).await;
- message::delete_msgs(&bob, &[bob.get_last_msg().await.id]).await?;
+ // Bob gets Alice's key via vCard.
+ bob.add_or_lookup_contact_id(alice).await;
// Alice sends a message to Bob using Thunderbird.
let raw = include_bytes!("../../test-data/message/rfc1847_encapsulation.eml");
- receive_imf(&bob, raw, false).await?;
+ receive_imf(bob, raw, false).await?;
let msg = bob.get_last_msg().await;
assert!(msg.get_showpadlock());
@@ -3144,70 +2827,6 @@ async fn test_invalid_to_address() -> Result<()> {
Ok(())
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_reply_from_different_addr() -> Result<()> {
- let t = TestContext::new_alice().await;
-
- // Alice creates a 2-person-group with Bob
- receive_imf(
- &t,
- br#"Subject: =?utf-8?q?Januar_13-19?=
-Chat-Group-ID: qetqsutor7a
-Chat-Group-Name: =?utf-8?q?Januar_13-19?=
-MIME-Version: 1.0
-References:
-Date: Mon, 20 Dec 2021 12:15:01 +0000
-Chat-Version: 1.0
-Message-ID:
-To:
-From:
-Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
-
-Hi, I created a group"#,
- false,
- )
- .await?;
- let msg_out = t.get_last_msg().await;
- assert_eq!(msg_out.from_id, ContactId::SELF);
- assert_eq!(msg_out.text, "Hi, I created a group");
- assert_eq!(msg_out.in_reply_to, None);
-
- // Bob replies from a different address
- receive_imf(
- &t,
- b"Content-Type: text/plain; charset=utf-8
-Content-Transfer-Encoding: quoted-printable
-From:
-Mime-Version: 1.0 (1.0)
-Subject: Re: Januar 13-19
-Date: Mon, 20 Dec 2021 13:54:55 +0100
-Message-Id:
-References:
-In-Reply-To:
-To: holger
-
-Reply from different address
-",
- false,
- )
- .await?;
- let msg_in = t.get_last_msg().await;
- assert_eq!(msg_in.to_id, ContactId::SELF);
- assert_eq!(msg_in.text, "Reply from different address");
- assert_eq!(
- msg_in.in_reply_to.unwrap(),
- "Gr.qetqsutor7a.Aresxresy-4@deltachat.de"
- );
- assert_eq!(
- msg_in.param.get(Param::OverrideSenderDisplayname),
- Some("bob-alias@example.com")
- );
-
- assert_eq!(msg_in.chat_id, msg_out.chat_id);
-
- Ok(())
-}
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_weird_and_duplicated_filenames() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -3358,7 +2977,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
// =============== Alice's second device receives the message ===============
let received = alice2.get_last_msg().await;
- // That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949:
+ // That's a regression test for https://github.com/chatmail/core/issues/2949:
assert_eq!(received.chat_id, alice2.get_chat(&bob).await.id);
let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await;
@@ -3382,13 +3001,15 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_auto_accept_for_bots() -> Result<()> {
- let t = TestContext::new_alice().await;
- t.set_config(Config::Bot, Some("1")).await.unwrap();
- receive_imf(&t, MSGRMSG, false).await?;
- let msg = t.get_last_msg().await;
- let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ alice.set_config(Config::Bot, Some("1")).await.unwrap();
+ let msg = tcm.send_recv(bob, alice, "Hello!").await;
+ let chat = chat::Chat::load_from_db(alice, msg.chat_id).await?;
assert!(!chat.is_contact_request());
- assert!(Contact::get_all(&t, 0, None).await?.len() == 1);
+
+ assert_eq!(Contact::get_all(alice, 0, None).await?.len(), 1);
Ok(())
}
@@ -3602,12 +3223,10 @@ async fn test_outgoing_undecryptable() -> Result<()> {
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
receive_imf(alice, raw, false).await?;
- let bob_contact_id = Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo)
- .await?
- .unwrap();
- assert!(ChatId::lookup_by_contact(alice, bob_contact_id)
- .await?
- .is_none());
+ // Undecryptable message does not even create a contact.
+ let bob_contact_id =
+ Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo).await?;
+ assert!(bob_contact_id.is_none());
let dev_chat_id = ChatId::lookup_by_contact(alice, ContactId::DEVICE)
.await?
@@ -3621,67 +3240,85 @@ async fn test_outgoing_undecryptable() -> Result<()> {
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
receive_imf(alice, raw, false).await?;
- assert!(ChatId::lookup_by_contact(alice, bob_contact_id)
- .await?
- .is_none());
+ let bob_contact_id =
+ Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo).await?;
+ assert!(bob_contact_id.is_none());
// The device message mustn't be added too frequently.
assert_eq!(alice.get_last_msg_in(dev_chat_id).await.id, dev_msg.id);
Ok(())
}
+/// Tests that a message from Thunderbird with an Autocrypt header is assigned to the key-contact.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_thunderbird_autocrypt() -> Result<()> {
let t = TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
let received_msg = receive_imf(&t, raw, false).await?.unwrap();
- assert!(received_msg.from_is_signed);
- let peerstate = Peerstate::from_addr(&t, "alice@example.org")
- .await?
- .unwrap();
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
+ assert_eq!(received_msg.msg_ids.len(), 1);
+ let msg_id = received_msg.msg_ids[0];
+
+ let message = Message::load_from_db(&t, msg_id).await?;
+ assert!(message.get_showpadlock());
+
+ let from_id = message.from_id;
+
+ let from_contact = Contact::get_by_id(&t, from_id).await?;
+ assert!(from_contact.is_key_contact());
Ok(())
}
+/// Tests reception of a message from Thunderbird with attached key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
let t = TestContext::new_bob().await;
+ // The message has public key attached *and* Autocrypt header.
+ //
+ // Autocrypt header is used to check the signature.
+ //
+ // At the time of the writing (2025-04-30, introduction of key-contacts)
+ // signature checking does not work without the Autocrypt header.
let raw =
include_bytes!("../../test-data/message/thunderbird_encrypted_signed_with_pubkey.eml");
- receive_imf(&t, raw, false).await?;
- let peerstate = Peerstate::from_addr(&t, "alice@example.org")
- .await?
- .unwrap();
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
+ let received_msg = receive_imf(&t, raw, false).await?.unwrap();
- receive_imf(
- &t,
- b"From: alice@example.org\n\
- To: bob@example.net\n\
- Subject: foo\n\
- Message-ID: \n\
- Date: Thu, 2 Nov 2023 02:20:28 -0300\n\
- \n\
- unencrypted\n",
- false,
- )
- .await?;
- let peerstate = Peerstate::from_addr(&t, "alice@example.org")
- .await?
- .unwrap();
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
+ // Attached key does not appear as an attachment,
+ // there is only one part.
+ assert_eq!(received_msg.msg_ids.len(), 1);
+ let msg_id = received_msg.msg_ids[0];
+ let message = Message::load_from_db(&t, msg_id).await?;
+ assert!(message.get_showpadlock());
+
+ let alice_id = message.from_id;
+ let alice_contact = Contact::get_by_id(&t, alice_id).await?;
+ assert!(alice_contact.is_key_contact());
+
+ // The message without the Autocrypt header
+ // cannot be assigned to the contact even if it
+ // is encrypted and signed.
+ //
+ // This could be fixed by looking up
+ // the key by the issuer fingerprint
+ // which is present in the detached signature,
+ // but this is not done yet.
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
- receive_imf(&t, raw, false).await?;
- let peerstate = Peerstate::from_addr(&t, "alice@example.org")
- .await?
- .unwrap();
- assert!(peerstate.public_key.is_some());
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
+ let received_msg = receive_imf(&t, raw, false).await?.unwrap();
+
+ assert_eq!(received_msg.msg_ids.len(), 1);
+ let msg_id = received_msg.msg_ids[0];
+
+ let message = Message::load_from_db(&t, msg_id).await?;
+ assert!(!message.get_showpadlock());
+
+ let alice_email_id = message.from_id;
+ assert_ne!(alice_email_id, alice_id);
+ let alice_address_contact = Contact::get_by_id(&t, alice_email_id).await?;
+ assert!(!alice_address_contact.is_key_contact());
Ok(())
}
@@ -3691,8 +3328,10 @@ async fn test_forged_from_and_no_valid_signatures() -> Result<()> {
let t = &TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
let received_msg = receive_imf(t, raw, false).await?.unwrap();
- assert!(!received_msg.from_is_signed);
- let msg = t.get_last_msg().await;
+
+ assert_eq!(received_msg.msg_ids.len(), 1);
+ let msg_id = received_msg.msg_ids[0];
+ let msg = Message::load_from_db(t, msg_id).await?;
assert!(!msg.chat_id.is_trash());
assert!(!msg.get_showpadlock());
@@ -3709,8 +3348,7 @@ async fn test_wrong_from_name_and_no_valid_signatures() -> Result<()> {
let t = &TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
let raw = String::from_utf8(raw.to_vec())?.replace("From: Alice", "From: A");
- let received_msg = receive_imf(t, raw.as_bytes(), false).await?.unwrap();
- assert!(!received_msg.from_is_signed);
+ receive_imf(t, raw.as_bytes(), false).await?.unwrap();
let msg = t.get_last_msg().await;
assert!(!msg.chat_id.is_trash());
assert!(!msg.get_showpadlock());
@@ -3721,21 +3359,34 @@ async fn test_wrong_from_name_and_no_valid_signatures() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> {
- let t = TestContext::new_bob().await;
+ let bob = &TestContext::new_bob().await;
+ // Thunderbird message with Autocrypt header and a signature,
+ // but not encrypted.
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt_unencrypted.eml");
- receive_imf(&t, raw, false).await?;
- let peerstate = Peerstate::from_addr(&t, "alice@example.org")
- .await?
- .unwrap();
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
+ let received_msg = receive_imf(bob, raw, false).await?.unwrap();
+
+ assert_eq!(received_msg.msg_ids.len(), 1);
+ let msg_id = received_msg.msg_ids[0];
+ let msg = Message::load_from_db(bob, msg_id).await?;
+ assert!(!msg.get_showpadlock());
+
+ // The message should arrive as address-contact
+ let alice_id = msg.from_id;
+ let alice_contact = Contact::get_by_id(bob, alice_id).await?;
+ assert!(!alice_contact.is_key_contact());
let raw = include_bytes!("../../test-data/message/thunderbird_signed_unencrypted.eml");
- receive_imf(&t, raw, false).await?;
- let peerstate = Peerstate::from_addr(&t, "alice@example.org")
- .await?
- .unwrap();
- assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
+ let received_msg = receive_imf(bob, raw, false).await?.unwrap();
+
+ assert_eq!(received_msg.msg_ids.len(), 1);
+ let msg_id = received_msg.msg_ids[0];
+ let msg = Message::load_from_db(bob, msg_id).await?;
+ assert!(!msg.get_showpadlock());
+
+ let alice_id = msg.from_id;
+ let alice_contact = Contact::get_by_id(bob, alice_id).await?;
+ assert!(!alice_contact.is_key_contact());
Ok(())
}
@@ -3750,8 +3401,7 @@ async fn test_thunderbird_unsigned() -> Result<()> {
// Alice receives an unsigned message from Bob.
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_unsigned.eml");
- let received_msg = receive_imf(&alice, raw, false).await?.unwrap();
- assert!(!received_msg.from_is_signed);
+ receive_imf(&alice, raw, false).await?.unwrap();
let msg = alice.get_last_msg().await;
assert!(!msg.get_showpadlock());
@@ -3852,6 +3502,7 @@ async fn test_big_forwarded_with_big_attachment() -> Result<()> {
Ok(())
}
+/// Tests that MUA user can add members to ad-hoc group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mua_user_adds_member() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -3859,42 +3510,41 @@ async fn test_mua_user_adds_member() -> Result<()> {
receive_imf(
&t,
b"From: alice@example.org\n\
- To: bob@example.com\n\
- Subject: foo\n\
- Message-ID: \n\
- Chat-Version: 1.0\n\
- Chat-Group-ID: gggroupiddd\n\
- Chat-Group-Name: foo\n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- hello\n",
+ To: bob@example.com, charlie@example.net\n\
+ Subject: foo\n\
+ Message-ID: \n\
+ Chat-Version: 1.0\n\
+ Chat-Group-ID: gggroupiddd\n\
+ Chat-Group-Name: foo\n\
+ Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
+ \n\
+ hello\n",
false,
)
.await?
.unwrap();
- receive_imf(
+ let msg = receive_imf(
&t,
b"From: bob@example.com\n\
- To: alice@example.org, fiona@example.net\n\
- Subject: foo\n\
- Message-ID: \n\
- In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- hello\n",
+ To: alice@example.org, charlie@example.net, fiona@example.net\n\
+ Subject: foo\n\
+ Message-ID: \n\
+ In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\
+ Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
+ \n\
+ hello\n",
false,
)
.await?
.unwrap();
- let (chat_id, _, _) = chat::get_chat_id_by_grpid(&t, "gggroupiddd")
- .await?
- .unwrap();
+ let chat_id = msg.chat_id;
let mut actual_chat_contacts = chat::get_chat_contacts(&t, chat_id).await?;
actual_chat_contacts.sort();
let mut expected_chat_contacts = vec![
Contact::create(&t, "", "bob@example.com").await?,
+ Contact::create(&t, "", "charlie@example.net").await?,
Contact::create(&t, "", "fiona@example.net").await?,
ContactId::SELF,
];
@@ -4663,12 +4313,9 @@ async fn test_create_group_with_big_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
- let ba_contact = Contact::create(
- &bob,
- "alice",
- &alice.get_config(Config::Addr).await?.unwrap(),
- )
- .await?;
+ let ba_contact = bob.add_or_lookup_contact_id(&alice).await;
+ let ab_chat_id = alice.create_chat(&bob).await.id;
+
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
@@ -4676,26 +4323,22 @@ async fn test_create_group_with_big_msg() -> Result<()> {
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
- assert!(!msg.get_showpadlock());
+ assert!(msg.get_showpadlock());
alice.set_config(Config::DownloadLimit, Some("1")).await?;
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
- let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
- assert_eq!(alice_grp.typ, Chattype::Group);
- assert_eq!(alice_grp.name, "Group");
- assert_eq!(
- chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
- 2
- );
+ let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
+ // Incomplete message is assigned to 1:1 chat.
+ assert_eq!(alice_chat.typ, Chattype::Single);
alice.set_config(Config::DownloadLimit, None).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(msg.viewtype, Viewtype::Image);
- assert_eq!(msg.chat_id, alice_grp.id);
+ assert_ne!(msg.chat_id, alice_chat.id);
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_grp.typ, Chattype::Group);
assert_eq!(alice_grp.name, "Group");
@@ -4704,7 +4347,6 @@ async fn test_create_group_with_big_msg() -> Result<()> {
2
);
- let ab_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
// Now Bob can send encrypted messages to Alice.
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group1").await?;
@@ -4735,7 +4377,8 @@ async fn test_create_group_with_big_msg() -> Result<()> {
);
// The big message must go away from the 1:1 chat.
- assert_eq!(alice.get_last_msg_in(ab_chat_id).await.text, "hi");
+ let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?;
+ assert!(msgs.is_empty());
Ok(())
}
@@ -4821,52 +4464,19 @@ Chat-Group-Member-Added: charlie@example.com",
Ok(())
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_leave_protected_group_missing_member_key() -> Result<()> {
- let mut tcm = TestContextManager::new();
- let alice = &tcm.alice().await;
- let bob = &tcm.bob().await;
- mark_as_verified(alice, bob).await;
- let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
- let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
- add_contact_to_chat(alice, group_id, alice_bob_id).await?;
- alice.send_text(group_id, "Hello!").await;
- alice
- .sql
- .execute(
- "UPDATE acpeerstates SET addr=? WHERE addr=?",
- ("b@b", "bob@example.net"),
- )
- .await?;
-
- // We fail to send the message.
- assert!(remove_contact_from_chat(alice, group_id, ContactId::SELF)
- .await
- .is_err());
-
- // The contact is already removed anyway.
- assert!(!is_contact_in_chat(alice, group_id, ContactId::SELF).await?);
- Ok(())
-}
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
- let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
mark_as_verified(alice, bob).await;
let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
add_contact_to_chat(alice, group_id, alice_bob_id).await?;
alice.send_text(group_id, "Hello!").await;
- alice
- .sql
- .execute("DELETE FROM acpeerstates WHERE addr=?", (&bob_addr,))
- .await?;
+ alice.sql.execute("DELETE FROM public_keys", ()).await?;
let fiona = &tcm.fiona().await;
- let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
mark_as_verified(alice, fiona).await;
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
add_contact_to_chat(alice, group_id, alice_fiona_id).await?;
@@ -4878,7 +4488,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
assert!(msg.is_info());
assert_eq!(
msg.get_text(),
- stock_str::msg_add_member_local(alice, &fiona_addr, ContactId::SELF).await
+ stock_str::msg_add_member_local(alice, alice_fiona_id, ContactId::SELF).await
);
remove_contact_from_chat(alice, group_id, alice_bob_id).await?;
@@ -4887,42 +4497,11 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
assert!(msg.is_info());
assert_eq!(
msg.get_text(),
- stock_str::msg_del_member_local(alice, &bob_addr, ContactId::SELF,).await
+ stock_str::msg_del_member_local(alice, alice_bob_id, ContactId::SELF).await
);
Ok(())
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_protected_group_reply_from_mua() -> Result<()> {
- let mut tcm = TestContextManager::new();
- let alice = &tcm.alice().await;
- let bob = &tcm.bob().await;
- let fiona = &tcm.fiona().await;
- mark_as_verified(alice, bob).await;
- mark_as_verified(alice, fiona).await;
- mark_as_verified(bob, alice).await;
- let alice_chat_id = alice
- .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob, fiona])
- .await;
- let sent = alice.send_text(alice_chat_id, "Hello!").await;
- let bob_msg = bob.recv_msg(&sent).await;
- bob_msg.chat_id.accept(bob).await?;
- // This is hacky, but i don't know other simple way to simulate a MUA reply. It works because
- // the message is correctly assigned to the chat by `References:`.
- bob.sql
- .execute(
- "UPDATE chats SET protected=0, grpid='' WHERE id=?",
- (bob_msg.chat_id,),
- )
- .await?;
- let sent = bob
- .send_text(bob_msg.chat_id, "/me replying from MUA")
- .await;
- let alice_msg = alice.recv_msg(&sent).await;
- assert_eq!(alice_msg.chat_id, alice_chat_id);
- Ok(())
-}
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_older_message_from_2nd_device() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -5020,6 +4599,7 @@ async fn test_no_op_member_added_is_trash() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
+ let fiona = &tcm.fiona().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob])
.await;
@@ -5029,11 +4609,11 @@ async fn test_no_op_member_added_is_trash() -> Result<()> {
let bob_chat_id = bob.get_last_msg().await.chat_id;
bob_chat_id.accept(bob).await?;
- let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
+ let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
let msg = alice.pop_sent_msg().await;
- let fiona_id = Contact::create(bob, "", "fiona@example.net").await?;
+ let fiona_id = bob.add_or_lookup_contact_id(fiona).await;
add_contact_to_chat(bob, bob_chat_id, fiona_id).await?;
bob.recv_msg_trash(&msg).await;
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
@@ -5291,8 +4871,12 @@ async fn test_make_n_send_vcard() -> Result<()> {
Ok(())
}
+/// Tests that group is not created if the message
+/// has no recipients even if it has unencrypted Chat-Group-ID.
+///
+/// Chat-Group-ID in unencrypted messages should be ignored.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_group_no_recipients() -> Result<()> {
+async fn test_unencrypted_group_id_no_recipients() -> Result<()> {
let t = &TestContext::new_alice().await;
let raw = "From: alice@example.org
Subject: Group
@@ -5307,7 +4891,7 @@ Hello!"
let received = receive_imf(t, raw, false).await?.unwrap();
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
let chat = Chat::load_from_db(t, msg.chat_id).await?;
- assert_eq!(chat.typ, Chattype::Group);
+ assert_eq!(chat.typ, Chattype::Single);
// Check that the weird group name is sanitzied correctly:
let mail = mailparse::parse_mail(raw).unwrap();
@@ -5318,38 +4902,30 @@ Hello!"
.get_value_raw(),
"Group\n name\u{202B}".as_bytes()
);
- assert_eq!(chat.name, "Group name");
+ assert_eq!(chat.name, "Saved messages");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_name_with_newline() -> Result<()> {
- let t = &TestContext::new_alice().await;
- let raw = "From: alice@example.org
-Subject: Group
-Chat-Version: 1.0
-Chat-Group-Name: =?utf-8?q?Delta=0D=0AChat?=
-Chat-Group-ID: GePFDkwEj2K
-Message-ID:
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
-Hello!"
- .as_bytes();
- let received = receive_imf(t, raw, false).await?.unwrap();
- let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
- let chat = Chat::load_from_db(t, msg.chat_id).await?;
+ let chat_id = create_group_chat(
+ alice,
+ ProtectionStatus::Unprotected,
+ "Group\r\nwith\nnewlines",
+ )
+ .await?;
+ add_contact_to_chat(alice, chat_id, alice.add_or_lookup_contact_id(bob).await).await?;
+ send_text_msg(alice, chat_id, "populate".to_string()).await?;
+ let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
+
+ let chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(chat.typ, Chattype::Group);
-
- // Check that the weird group name is sanitzied correctly:
- let mail = mailparse::parse_mail(raw).unwrap();
- assert_eq!(
- mail.headers
- .get_header(HeaderDef::ChatGroupName)
- .unwrap()
- .get_value(),
- "Delta\r\nChat"
- );
- assert_eq!(chat.name, "Delta Chat");
+ assert_eq!(chat.name, "Group with newlines");
Ok(())
}
@@ -5413,133 +4989,6 @@ async fn test_rename_chat_after_creating_invite() -> Result<()> {
Ok(())
}
-/// Tests that creating a group
-/// is preferred over assigning message to existing
-/// chat based on `In-Reply-To` and `References`.
-///
-/// Referenced message itself may be incorrectly assigned,
-/// but `Chat-Group-ID` uniquely identifies the chat.
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_prefer_chat_group_id_over_references() -> Result<()> {
- let t = &TestContext::new_alice().await;
-
- // Alice receives 1:1 message from Bob.
- let raw = b"From: bob@example.net\n\
- To: alice@example.org\n\
- Subject: Hi\n\
- Message-ID: \n\
- \n\
- Hello!";
- receive_imf(t, raw, false).await?.unwrap();
-
- // Alice receives a group message from Bob.
- // This references 1:1 message,
- // but should create a group.
- let raw = b"From: bob@example.net\n\
- To: alice@example.org\n\
- Subject: Group\n\
- Chat-Version: 1.0\n\
- Chat-Group-Name: Group 1\n\
- Chat-Group-ID: GePFDkwEj2K\n\
- Message-ID: \n\
- References: \n\
- In-Reply-To: \n\
- \n\
- Group 1";
- let received1 = receive_imf(t, raw, false).await?.unwrap();
- let msg1 = Message::load_from_db(t, *received1.msg_ids.last().unwrap()).await?;
- let chat1 = Chat::load_from_db(t, msg1.chat_id).await?;
- assert_eq!(chat1.typ, Chattype::Group);
-
- // Alice receives outgoing group message.
- // This references 1:1 message,
- // but should create another group.
- let raw = b"From: alice@example.org\n\
- To: bob@example.net
- Subject: Group\n\
- Chat-Version: 1.0\n\
- Chat-Group-Name: Group 2\n\
- Chat-Group-ID: Abcdexyzfoo\n\
- Message-ID: \n\
- References: \n\
- In-Reply-To: \n\
- \n\
- Group 2";
- let received2 = receive_imf(t, raw, false).await?.unwrap();
- let msg2 = Message::load_from_db(t, *received2.msg_ids.last().unwrap()).await?;
- let chat2 = Chat::load_from_db(t, msg2.chat_id).await?;
- assert_eq!(chat2.typ, Chattype::Group);
-
- assert_ne!(chat1.id, chat2.id);
- Ok(())
-}
-
-/// Tests that if member timestamps are unknown
-/// because of the missing `Chat-Group-Member-Timestamps` header,
-/// then timestamps default to zero.
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_default_member_timestamps_to_zero() -> Result<()> {
- let bob = &TestContext::new_bob().await;
-
- let now = time();
-
- let date = chrono::DateTime::::from_timestamp(now - 1000, 0)
- .unwrap()
- .to_rfc2822();
- let msg = receive_imf(
- bob,
- format!(
- "Subject: Some group\r\n\
- From: \r\n\
- To: , , \r\n\
- Date: {date}\r\n\
- Message-ID: \r\n\
- Chat-Group-ID: foobarbaz12\n\
- Chat-Group-Name: foo\n\
- Chat-Version: 1.0\r\n\
- \r\n\
- Hi!\r\n"
- )
- .as_bytes(),
- false,
- )
- .await?
- .unwrap();
- let chat = Chat::load_from_db(bob, msg.chat_id).await?;
- assert_eq!(chat.typ, Chattype::Group);
- assert_eq!(chat::get_chat_contacts(bob, chat.id).await?.len(), 4);
-
- let date = chrono::DateTime::::from_timestamp(now, 0)
- .unwrap()
- .to_rfc2822();
- receive_imf(
- bob,
- format!(
- "Subject: Some group\r\n\
- From: