diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index ee30f2b3b..965ca9aa7 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4535,12 +4535,12 @@ int dc_msg_is_info (const dc_msg_t* msg); * - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT" * - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT" * - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT" - * - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected" - * - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected" + * - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected" * - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet", * the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL * and also offer a way to fix the encryption, eg. by a button offering a QR scan * - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info` + * - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted" * * For the messages that refer to a CONTACT, * dc_msg_get_info_contact_id() returns the contact ID. @@ -4593,9 +4593,10 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg); #define DC_INFO_LOCATION_ONLY 9 #define DC_INFO_EPHEMERAL_TIMER_CHANGED 10 #define DC_INFO_PROTECTION_ENABLED 11 -#define DC_INFO_PROTECTION_DISABLED 12 +#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07 #define DC_INFO_INVALID_UNENCRYPTED_MAIL 13 #define DC_INFO_WEBXDC_INFO_MESSAGE 32 +#define DC_INFO_CHAT_E2EE 50 /** @@ -6898,9 +6899,7 @@ void dc_event_unref(dc_event_t* event); /// Used in summaries. #define DC_STR_GIF 23 -/// "Encrypted message" -/// -/// Used in subjects of outgoing messages. +/// @deprecated 2025-07, this string is no longer needed. #define DC_STR_ENCRYPTEDMSG 24 /// "End-to-end encryption available." @@ -7605,7 +7604,7 @@ void dc_event_unref(dc_event_t* event); /// Used as a device message after a successful backup transfer. #define DC_STR_BACKUP_TRANSFER_MSG_BODY 163 -/// "Messages are guaranteed to be end-to-end encrypted from now on." +/// "Messages are end-to-end encrypted." /// /// Used in info messages. #define DC_STR_CHAT_PROTECTION_ENABLED 170 @@ -7613,6 +7612,7 @@ void dc_event_unref(dc_event_t* event); /// "%1$s sent a message from another device." /// /// Used in info messages. +/// @deprecated 2025-07 #define DC_STR_CHAT_PROTECTION_DISABLED 171 /// "Others will only see this group after you sent a first message." diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 3ad81bd12..af3c45d31 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -416,6 +416,9 @@ pub enum SystemMessageType { /// Chat ephemeral message timer is changed. EphemeralTimerChanged, + // Chat is e2ee + ChatE2ee, + // Chat protection state changed ChatProtectionEnabled, ChatProtectionDisabled, @@ -450,6 +453,7 @@ impl From for SystemMessageType { SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled, SystemMessage::LocationOnly => SystemMessageType::LocationOnly, SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged, + SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee, SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled, SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled, SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync, diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index 91238d6f0..c633fc291 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -95,8 +95,10 @@ describe("online tests", function () { false, ); - expect(messageList).have.length(1); - const message = await dc.rpc.getMessage(accountId2, messageList[0]); + // There are 2 messages in the chat: + // 'Messages are end-to-end encrypted' (info message) and 'Hello' + expect(messageList).have.length(2); + const message = await dc.rpc.getMessage(accountId2, messageList[1]); expect(message.text).equal("Hello"); expect(message.showPadlock).equal(true); }); diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 31577dbe9..89610382e 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -13,6 +13,12 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag from ._utils import futuremethod from .rpc import Rpc +E2EE_INFO_MSGS = 1 +""" +The number of info messages added to new e2ee chats. +Currently this is "End-to-end encryption available". +""" + class ACFactory: """Test account factory.""" diff --git a/deltachat-rpc-client/tests/test_multidevice.py b/deltachat-rpc-client/tests/test_multidevice.py index 663542aa0..008ed4657 100644 --- a/deltachat-rpc-client/tests/test_multidevice.py +++ b/deltachat-rpc-client/tests/test_multidevice.py @@ -36,6 +36,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap): assert ac1.get_config("bcc_self") == "1" # Second client receives only second message, but not the first. + ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED) + assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted." + ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED) assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 01b1c37ea..45914a7b1 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -12,6 +12,7 @@ import pytest from deltachat_rpc_client import Contact, EventType, Message, events from deltachat_rpc_client.const import ChatType, DownloadState, MessageState +from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS from deltachat_rpc_client.rpc import JsonRpcError @@ -457,8 +458,12 @@ def test_wait_next_messages(acfactory) -> None: alice_chat_bot.send_text("Hello!") next_messages = next_messages_task.result() - assert len(next_messages) == 1 - snapshot = next_messages[0].get_snapshot() + + if len(next_messages) == E2EE_INFO_MSGS: + next_messages += bot.wait_next_messages() + + assert len(next_messages) == 1 + E2EE_INFO_MSGS + snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot() assert snapshot.text == "Hello!" diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index aecd333ff..15bdb6035 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -20,6 +20,12 @@ import deltachat from . import Account, account_hookimpl, const, get_core_info from .events import FFIEventLogger, FFIEventTracker +E2EE_INFO_MSGS = 1 +""" +The number of info messages added to new e2ee chats. +Currently this is "End-to-end encryption available". +""" + def pytest_addoption(parser): group = parser.getgroup("deltachat testplugin options") @@ -606,7 +612,7 @@ class ACFactory: ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") msg = ac2.get_message_by_id(ev.data2) assert msg is not None - assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on." + assert msg.text == "Messages are end-to-end encrypted." msg = ac2._evtracker.wait_next_incoming_message() assert msg is not None assert "Member Me " in msg.text and " added by " in msg.text diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 86bb8d7f7..d92dbcacc 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -133,8 +133,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp): assert "added" in msg.text.lower() assert any( - m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on." - for m in msg.chat.get_messages() + m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages() ) lp.sec("ac1: send message") msg_out = chat1.send_text("hello") @@ -338,7 +337,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp assert contact.addr == ac1.get_config("addr") chat2 = msg_in.chat assert chat2.is_protected() - assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on." + assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted." assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read() lp.sec("ac2_offl: sending message") @@ -412,7 +411,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") msg_in = ac2_offl.get_message_by_id(ev.data2) assert msg_in.is_system_message() - assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on." + assert msg_in.text == "Messages are end-to-end encrypted." # We need to consume one event that has data2=0 ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 2a29a54fa..9ae861eb9 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -10,6 +10,7 @@ from imap_tools import AND, U import deltachat as dc from deltachat import account_hookimpl, Message from deltachat.tracker import ImexTracker +from deltachat.testplugin import E2EE_INFO_MSGS def test_basic_imap_api(acfactory, tmp_path): @@ -408,6 +409,10 @@ def test_forward_messages(acfactory, lp): msg_out = chat.send_text("message2") lp.sec("ac2: wait for receive") + ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") + msg_in = ac2.get_message_by_id(ev.data2) + assert msg_in.text == "Messages are end-to-end encrypted." + ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") assert ev.data2 == msg_out.id msg_in = ac2.get_message_by_id(msg_out.id) @@ -622,6 +627,11 @@ def test_moved_markseen(acfactory): with ac2.direct_imap.idle() as idle2: ac2.start_io() + + ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") + msg = ac2.get_message_by_id(ev.data2) + assert msg.text == "Messages are end-to-end encrypted." + ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") msg = ac2.get_message_by_id(ev.data2) @@ -738,7 +748,7 @@ def test_mdn_asymmetric(acfactory, lp): lp.sec("sending text message from ac1 to ac2") msg_out = chat.send_text("message1") - assert len(chat.get_messages()) == 1 + assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS lp.sec("disable ac1 MDNs") ac1.set_config("mdns_enabled", "0") @@ -746,7 +756,7 @@ def test_mdn_asymmetric(acfactory, lp): lp.sec("wait for ac2 to receive message") msg = ac2._evtracker.wait_next_incoming_message() - assert len(msg.chat.get_messages()) == 1 + assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS lp.sec("ac2: mark incoming message as seen") ac2.mark_seen_messages([msg]) @@ -755,7 +765,7 @@ def test_mdn_asymmetric(acfactory, lp): # MDN should be moved even though MDNs are already disabled ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED") - assert len(chat.get_messages()) == 1 + assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS # Wait for the message to be marked as seen on IMAP. ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.") @@ -1123,6 +1133,11 @@ def test_send_and_receive_image(acfactory, lp, data): assert m == msg_out lp.sec("wait for ac2 to receive message") + + ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG") + msg_in = ac2.get_message_by_id(ev.data2) + assert msg_in.text == "Messages are end-to-end encrypted." + ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG") assert ev.data2 == msg_out.id msg_in = ac2.get_message_by_id(msg_out.id) @@ -1158,10 +1173,10 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp): assert contact2.addr == some1_addr chat2 = contact2.create_chat() messages = chat2.get_messages() - assert len(messages) == 3 - assert messages[0].text == "msg1" - assert messages[1].filemime == "image/png" - assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size + assert len(messages) == 3 + E2EE_INFO_MSGS + assert messages[0 + E2EE_INFO_MSGS].text == "msg1" + assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png" + assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size ac.set_config("displayname", "new displayname") assert ac.get_config("displayname") == "new displayname" @@ -1414,8 +1429,8 @@ def test_connectivity(acfactory, lp): ac1.maybe_network() ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED) msgs = ac1.create_chat(ac2).get_messages() - assert len(msgs) == 1 - assert msgs[0].text == "Hi" + assert len(msgs) == 1 + E2EE_INFO_MSGS + assert msgs[0 + E2EE_INFO_MSGS].text == "Hi" lp.sec("Test that the connectivity changes to WORKING while new messages are fetched") @@ -1425,8 +1440,8 @@ def test_connectivity(acfactory, lp): ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED) msgs = ac1.create_chat(ac2).get_messages() - assert len(msgs) == 2 - assert msgs[1].text == "Hi 2" + assert len(msgs) == 2 + E2EE_INFO_MSGS + assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2" def test_fetch_deleted_msg(acfactory, lp): diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 6a0a5ac40..850baba71 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -6,6 +6,7 @@ import pytest import deltachat as dc from deltachat.tracker import ImexFailed from deltachat import Account, Message +from deltachat.testplugin import E2EE_INFO_MSGS class TestOfflineAccountBasic: @@ -461,9 +462,9 @@ class TestOfflineChat: assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() - assert len(messages) == 2 - assert messages[0].text == "msg1" - assert os.path.exists(messages[1].filename) + assert len(messages) == 2 + E2EE_INFO_MSGS + assert messages[0 + E2EE_INFO_MSGS].text == "msg1" + assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename) def test_import_export_on_encrypted_acct(self, acfactory, tmp_path): passphrase1 = "passphrase1" @@ -500,9 +501,9 @@ class TestOfflineChat: contact2_addr = contact2.addr chat2 = contact2.create_chat() messages = chat2.get_messages() - assert len(messages) == 2 - assert messages[0].text == "msg1" - assert os.path.exists(messages[1].filename) + assert len(messages) == 2 + E2EE_INFO_MSGS + assert messages[0 + E2EE_INFO_MSGS].text == "msg1" + assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename) ac2.shutdown() @@ -517,9 +518,9 @@ class TestOfflineChat: assert contact2.addr == contact2_addr chat2 = contact2.create_chat() messages = chat2.get_messages() - assert len(messages) == 2 - assert messages[0].text == "msg1" - assert os.path.exists(messages[1].filename) + assert len(messages) == 2 + E2EE_INFO_MSGS + assert messages[0 + E2EE_INFO_MSGS].text == "msg1" + assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename) def test_import_export_with_passphrase(self, acfactory, tmp_path): passphrase = "test_passphrase" @@ -557,9 +558,9 @@ class TestOfflineChat: assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() - assert len(messages) == 2 - assert messages[0].text == "msg1" - assert os.path.exists(messages[1].filename) + assert len(messages) == 2 + E2EE_INFO_MSGS + assert messages[0 + E2EE_INFO_MSGS].text == "msg1" + assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename) def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path): """ @@ -603,9 +604,9 @@ class TestOfflineChat: assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() - assert len(messages) == 2 - assert messages[0].text == "msg1" - assert os.path.exists(messages[1].filename) + assert len(messages) == 2 + E2EE_INFO_MSGS + assert messages[0 + E2EE_INFO_MSGS].text == "msg1" + assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename) ac2.shutdown() @@ -620,9 +621,9 @@ class TestOfflineChat: assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() - assert len(messages) == 2 - assert messages[0].text == "msg1" - assert os.path.exists(messages[1].filename) + assert len(messages) == 2 + E2EE_INFO_MSGS + assert messages[0 + E2EE_INFO_MSGS].text == "msg1" + assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename) def test_set_get_draft(self, chat1): msg1 = Message.new_empty(chat1.account, "text") diff --git a/src/chat.rs b/src/chat.rs index 2112cc8f7..4b13f07e9 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -349,6 +349,8 @@ impl ChatId { chat_id .add_protection_msg(context, ProtectionStatus::Protected, None, timestamp) .await?; + } else { + chat_id.maybe_add_encrypted_msg(context, timestamp).await?; } info!( @@ -604,6 +606,42 @@ impl ChatId { Ok(()) } + /// Adds message "Messages are end-to-end encrypted" if appropriate. + /// + /// This function is rather slow because it does a lot of database queries, + /// but this is fine because it is only called on chat creation. + async fn maybe_add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> { + let chat = Chat::load_from_db(context, self).await?; + + // as secure-join adds its own message on success (after some other messasges), + // we do not want to add "Messages are end-to-end encrypted" on chat creation. + // we detect secure join by `can_send` (for Bob, scanner side) and by `blocked` (for Alice, inviter side) below. + if !chat.is_encrypted(context).await? + || self <= DC_CHAT_ID_LAST_SPECIAL + || chat.is_device_talk() + || chat.is_self_talk() + || (!chat.can_send(context).await? && !chat.is_contact_request()) + || chat.blocked == Blocked::Yes + { + return Ok(()); + } + + let text = stock_str::messages_e2e_encrypted(context).await; + add_info_msg_with_cmd( + context, + self, + &text, + SystemMessage::ChatE2ee, + timestamp_sort, + None, + None, + None, + None, + ) + .await?; + Ok(()) + } + /// Sets protection and adds a message. /// /// `timestamp_sort` is used as the timestamp of the added message @@ -2673,6 +2711,10 @@ impl ChatIdBlocked { smeared_time, ) .await?; + } else { + chat_id + .maybe_add_encrypted_msg(context, smeared_time) + .await?; } Ok(Self { diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 8bf741904..b6c263ba4 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -8,7 +8,7 @@ use crate::message::{MessengerMessage, delete_msgs}; use crate::mimeparser::{self, MimeMessage}; use crate::receive_imf::receive_imf; use crate::test_utils::{ - AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager, + AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync, }; use pretty_assertions::assert_eq; @@ -2104,7 +2104,7 @@ async fn test_forward_basic() -> Result<()> { forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?; let forwarded_msg = bob.pop_sent_msg().await; - assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2); + assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2); assert_ne!( forwarded_msg.load_from_db().await.rfc724_mid, msg.rfc724_mid, @@ -2132,7 +2132,7 @@ async fn test_forward_info_msg() -> Result<()> { assert!(msg1.get_text().contains("bob@example.net")); let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?; - assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0); + assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), E2EE_INFO_MSGS); forward_msgs(alice, &[msg1.id], chat_id2).await?; let msg2 = alice.get_last_msg_in(chat_id2).await; assert!(!msg2.is_info()); // forwarded info-messages lose their info-state @@ -2518,22 +2518,34 @@ async fn test_resend_own_message() -> Result<()> { let sent1_ts_sent = msg.timestamp_sent; assert_eq!(msg.get_text(), "alice->bob"); assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2); - assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1); + assert_eq!( + get_chat_msgs(&bob, msg.chat_id).await?.len(), + E2EE_INFO_MSGS + 1 + ); bob.recv_msg(&sent2).await; assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); - assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2); + assert_eq!( + get_chat_msgs(&bob, msg.chat_id).await?.len(), + E2EE_INFO_MSGS + 2 + ); let received = bob.recv_msg_opt(&sent3).await; // No message should actually be added since we already know this message: assert!(received.is_none()); assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); - assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2); + assert_eq!( + get_chat_msgs(&bob, msg.chat_id).await?.len(), + E2EE_INFO_MSGS + 2 + ); // Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob fiona.recv_msg(&sent2).await; let msg = fiona.recv_msg(&sent3).await; assert_eq!(msg.get_text(), "alice->bob"); assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3); - assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2); + assert_eq!( + get_chat_msgs(&fiona, msg.chat_id).await?.len(), + E2EE_INFO_MSGS + 2 + ); let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?; assert_eq!(msg_from.get_addr(), "alice@example.org"); assert!(sent1_ts_sent < msg.timestamp_sent); @@ -4454,13 +4466,13 @@ async fn test_receive_edit_request_after_removal() -> Result<()> { let bob_msg = bob.recv_msg(&sent1).await; let bob_chat_id = bob_msg.chat_id; assert_eq!(bob_msg.text, "zext me in delra.cat"); - assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1); + assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1); delete_msgs(bob, &[bob_msg.id]).await?; - assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0); + assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS); bob.recv_msg_trash(&sent2).await; - assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0); + assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS); Ok(()) } @@ -4549,28 +4561,34 @@ async fn test_send_delete_request() -> Result<()> { // Alice sends a message, then sends a deletion request let sent1 = alice.send_text(alice_chat.id, "wtf").await; let alice_msg = sent1.load_from_db().await; - assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2); + assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 2); message::delete_msgs_ex(alice, &[alice_msg.id], true).await?; let sent2 = alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1); + assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1); // Bob receives both messages and has nothing the end let bob_msg = bob.recv_msg(&sent1).await; assert_eq!(bob_msg.text, "wtf"); - assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2); + assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2); bob.recv_msg_opt(&sent2).await; - assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1); + assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1); // Alice has another device, and there is also nothing at the end let alice2 = &tcm.alice().await; alice2.recv_msg(&sent0).await; let alice2_msg = alice2.recv_msg(&sent1).await; - assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2); + assert_eq!( + alice2_msg.chat_id.get_msg_cnt(alice2).await?, + E2EE_INFO_MSGS + 2 + ); alice2.recv_msg_opt(&sent2).await; - assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1); + assert_eq!( + alice2_msg.chat_id.get_msg_cnt(alice2).await?, + E2EE_INFO_MSGS + 1 + ); Ok(()) } diff --git a/src/context/context_tests.rs b/src/context/context_tests.rs index 885001284..e80c17448 100644 --- a/src/context/context_tests.rs +++ b/src/context/context_tests.rs @@ -8,7 +8,7 @@ use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::mimeparser::SystemMessage; use crate::receive_imf::receive_imf; -use crate::test_utils::{TestContext, get_chat_msg}; +use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg}; use crate::tools::{SystemTime, create_outgoing_rfc724_mid}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -571,7 +571,7 @@ async fn test_get_next_msgs() -> Result<()> { let alice_chat = alice.create_chat(&bob).await; - assert!(alice.get_next_msgs().await?.is_empty()); + assert_eq!(alice.get_next_msgs().await?.len(), E2EE_INFO_MSGS); assert!(bob.get_next_msgs().await?.is_empty()); let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; diff --git a/src/download.rs b/src/download.rs index a95ec104b..57086a43d 100644 --- a/src/download.rs +++ b/src/download.rs @@ -277,7 +277,7 @@ mod tests { use crate::chat::{get_chat_msgs, send_msg}; use crate::ephemeral::Timer; use crate::receive_imf::receive_imf_from_inbox; - use crate::test_utils::TestContext; + use crate::test_utils::{E2EE_INFO_MSGS, TestContext}; #[test] fn test_downloadstate_values() { @@ -459,7 +459,10 @@ mod tests { .await?; let msg = bob.get_last_msg().await; let chat_id = msg.chat_id; - assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1); + assert_eq!( + get_chat_msgs(&bob, chat_id).await?.len(), + E2EE_INFO_MSGS + 1 + ); assert_eq!(msg.download_state(), DownloadState::Available); // downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat @@ -472,7 +475,7 @@ mod tests { None, ) .await?; - assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0); + assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), E2EE_INFO_MSGS); assert!( Message::load_from_db_optional(&bob, msg.id) .await? diff --git a/src/message.rs b/src/message.rs index e385ce9d5..4325295e1 100644 --- a/src/message.rs +++ b/src/message.rs @@ -963,6 +963,7 @@ impl Message { | SystemMessage::SecurejoinMessage | SystemMessage::LocationStreamingEnabled | SystemMessage::LocationOnly + | SystemMessage::ChatE2ee | SystemMessage::ChatProtectionEnabled | SystemMessage::ChatProtectionDisabled | SystemMessage::InvalidUnencryptedMail diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index 35c64ba90..08b312aa5 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -7,7 +7,7 @@ use crate::config::Config; use crate::reaction::send_reaction; use crate::receive_imf::receive_imf; use crate::test_utils; -use crate::test_utils::{TestContext, TestContextManager}; +use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager}; #[test] fn test_guess_msgtype_from_suffix() { @@ -347,7 +347,7 @@ async fn test_markseen_msgs() -> Result<()> { let chats = Chatlist::try_load(&bob, 0, None, None).await?; assert_eq!(chats.len(), 1); let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; - assert_eq!(msgs.len(), 2); + assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2); assert_eq!(bob.get_fresh_msgs().await?.len(), 0); // that has no effect in contact request @@ -358,7 +358,7 @@ async fn test_markseen_msgs() -> Result<()> { assert_eq!(bob_chat.blocked, Blocked::Request); let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; - assert_eq!(msgs.len(), 2); + assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2); bob_chat_id.accept(&bob).await.unwrap(); // bob sends to alice, @@ -761,19 +761,22 @@ async fn test_delete_msgs_sync() -> Result<()> { // Alice sends a messsage and receives it on the other device let sent1 = alice.send_text(alice_chat_id, "foo").await; - assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1); + assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1); let msg = alice2.recv_msg(&sent1).await; let alice2_chat_id = msg.chat_id; assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id); - assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1); + assert_eq!( + alice2_chat_id.get_msg_cnt(alice2).await?, + E2EE_INFO_MSGS + 1 + ); // Alice deletes the message; this should happen on both devices as well delete_msgs(alice, &[sent1.sender_msg_id]).await?; - assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0); + assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS); test_utils::sync(alice, alice2).await; - assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0); + assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, E2EE_INFO_MSGS); Ok(()) } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 20923acef..5623abcc5 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -181,10 +181,10 @@ pub enum SystemMessage { /// Chat ephemeral message timer is changed. EphemeralTimerChanged = 10, - /// "Messages are guaranteed to be end-to-end encrypted from now on." + /// "Messages are end-to-end encrypted." ChatProtectionEnabled = 11, - /// "%1$s sent a message from another device." + /// "%1$s sent a message from another device.", deprecated 2025-07 ChatProtectionDisabled = 12, /// Message can't be sent because of `Invalid unencrypted mail to <>` @@ -213,6 +213,9 @@ pub enum SystemMessage { /// This message contains a users iroh node address. IrohNodeAddr = 40, + + /// "Messages are end-to-end encrypted." + ChatE2ee = 50, } const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup"; diff --git a/src/reaction.rs b/src/reaction.rs index 3a13f4e98..685f811c9 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -407,6 +407,7 @@ mod tests { use crate::message::{MessageState, delete_msgs}; use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; use crate::sql::housekeeping; + use crate::test_utils::E2EE_INFO_MSGS; use crate::test_utils::TestContext; use crate::test_utils::TestContextManager; use crate::tools::SystemTime; @@ -653,13 +654,25 @@ Here's my footer -- bob@example.net" let chat_alice = alice.create_chat(&bob).await; let alice_msg = alice.send_text(chat_alice.id, "Hi!").await; let bob_msg = bob.recv_msg(&alice_msg).await; - assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 1); - assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 1); + assert_eq!( + get_chat_msgs(&alice, chat_alice.id).await?.len(), + E2EE_INFO_MSGS + 1 + ); + assert_eq!( + get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), + E2EE_INFO_MSGS + 1 + ); let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await; bob.recv_msg(&alice_msg2).await; - assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2); - assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2); + assert_eq!( + get_chat_msgs(&alice, chat_alice.id).await?.len(), + E2EE_INFO_MSGS + 2 + ); + assert_eq!( + get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), + E2EE_INFO_MSGS + 2 + ); bob_msg.chat_id.accept(&bob).await?; @@ -667,12 +680,18 @@ Here's my footer -- bob@example.net" send_reaction(&bob, bob_msg.id, "πŸ‘").await.unwrap(); expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?; expect_no_unwanted_events(&bob).await; - assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2); + assert_eq!( + get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), + E2EE_INFO_MSGS + 2 + ); let bob_reaction_msg = bob.pop_sent_msg().await; let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await; assert_eq!(alice_reaction_msg.state, MessageState::InFresh); - assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2); + assert_eq!( + get_chat_msgs(&alice, chat_alice.id).await?.len(), + E2EE_INFO_MSGS + 2 + ); let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?; assert_eq!(reactions.to_string(), "πŸ‘1"); diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 8be0e2beb..1287d811e 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -15,8 +15,9 @@ use crate::download::MIN_DOWNLOAD_LIMIT; use crate::imap::prefetch_should_download; use crate::imex::{ImexMode, imex}; use crate::securejoin::get_securejoin_qr; -use crate::test_utils::mark_as_verified; -use crate::test_utils::{TestContext, TestContextManager, get_chat_msg}; +use crate::test_utils::{ + E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified, +}; use crate::tools::{SystemTime, time}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -133,7 +134,7 @@ async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result< let chats = Chatlist::try_load(bob, 0, None, None).await?; assert_eq!(chats.len(), 1); let chat_id = chats.get_chat_id(0)?; - assert_eq!(chat_id.get_msg_cnt(bob).await?, 1); + assert_eq!(chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1); Ok(()) } @@ -4410,7 +4411,7 @@ async fn test_create_group_with_big_msg() -> Result<()> { // The big message must go away from the 1:1 chat. let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?; - assert!(msgs.is_empty()); + assert_eq!(msgs.len(), E2EE_INFO_MSGS); Ok(()) } diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index fca280443..b5f3fcd84 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -6,7 +6,7 @@ use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::key::self_fingerprint; use crate::receive_imf::receive_imf; -use crate::stock_str::{self, chat_protection_enabled}; +use crate::stock_str::{self, messages_e2e_encrypted}; use crate::test_utils::{ TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg, }; @@ -246,7 +246,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { let chat = alice.get_chat(&bob).await; let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await; assert!(msg.is_info()); - let expected_text = chat_protection_enabled(&alice).await; + let expected_text = messages_e2e_encrypted(&alice).await; assert_eq!(msg.get_text(), expected_text); if case == SetupContactCase::CheckProtectionTimestamp { assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1); @@ -296,7 +296,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await); let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; assert!(msg.is_info()); - assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await); + assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -540,7 +540,7 @@ async fn test_secure_join() -> Result<()> { // - You added member bob@example.net let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await; assert!(msg.is_info()); - let expected_text = chat_protection_enabled(&alice).await; + let expected_text = messages_e2e_encrypted(&alice).await; assert_eq!(msg.get_text(), expected_text); } diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index c0029673d..8919dd35d 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -134,7 +134,7 @@ async fn test_key_contacts_migration_verified() -> Result<()> { t.sql.call_write(|conn| Ok(conn.execute_batch(r#" INSERT INTO acpeerstates VALUES(1,'bob@example.net',0,0,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680be34b021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487921c07ff4f16327797bc5836071b5fcf2ae269c609b697c38b579f2449d0ff07f3e04db07822bfa83a6ca85308d99328c765f9b37b883a3526d38c3c005810ee6d9064acae1c68784781b9688be535a03ed5902d9ab5c9e5d28fb04aa621cb294445b9eab122d86afc0e2a4fd9a6a9af82f50b49295a9852f35c9ed8d816218ba98bc047cfe5fb9432e45ea63140bd16263728b1d1dd18d143b677e1ddd9cb5e939dd51cd7f2c2037cc89b5cee26917ea949e31c808996a5b7efc73636511173f59e2ab025902d86085110ac22988e86e663f19514c559a3b5a52838d1d1fa263f065fddf5fc8c8a1b4dac51aece76d536a3426f133be204dcb03c4a84242137373e39e7cec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680be34b021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf7048798cf07fc09e848aa6595d435805efbbbd0b05bec2fffa88b3b1d6e0a3ba80e300bdd83aa5f03bfcc9361b2a90e9cf8980c775d707467c638a13d65a01eda4b57d3560fada0c675399c263e668a02b84733b2c8e71d7fb9f15cb076f933571d32fc3377bb59ff64da6808eeb96210776126504ae9d6916124d3c679ba810a6c92dfe7d58eba7df22e9f07241d343d3e1792fe48d36fd6ec7d1ed291eae5d5d688872f5c723d5a12c424ff32c25d1348d2b683c5cf9128efb957b0026e607d593528a01dea6458c4709779b8f99bd689ef1bfde7146461317ee2793a130663388977488a9fd1a652377445571b1c913ee14fe0b22d451943b4fe1d0578b71201f1ee106f4c',1,1745609547,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680be34b021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487921c07ff4f16327797bc5836071b5fcf2ae269c609b697c38b579f2449d0ff07f3e04db07822bfa83a6ca85308d99328c765f9b37b883a3526d38c3c005810ee6d9064acae1c68784781b9688be535a03ed5902d9ab5c9e5d28fb04aa621cb294445b9eab122d86afc0e2a4fd9a6a9af82f50b49295a9852f35c9ed8d816218ba98bc047cfe5fb9432e45ea63140bd16263728b1d1dd18d143b677e1ddd9cb5e939dd51cd7f2c2037cc89b5cee26917ea949e31c808996a5b7efc73636511173f59e2ab025902d86085110ac22988e86e663f19514c559a3b5a52838d1d1fa263f065fddf5fc8c8a1b4dac51aece76d536a3426f133be204dcb03c4a84242137373e39e7cec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680be34b021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf7048798cf07fc09e848aa6595d435805efbbbd0b05bec2fffa88b3b1d6e0a3ba80e300bdd83aa5f03bfcc9361b2a90e9cf8980c775d707467c638a13d65a01eda4b57d3560fada0c675399c263e668a02b84733b2c8e71d7fb9f15cb076f933571d32fc3377bb59ff64da6808eeb96210776126504ae9d6916124d3c679ba810a6c92dfe7d58eba7df22e9f07241d343d3e1792fe48d36fd6ec7d1ed291eae5d5d688872f5c723d5a12c424ff32c25d1348d2b683c5cf9128efb957b0026e607d593528a01dea6458c4709779b8f99bd689ef1bfde7146461317ee2793a130663388977488a9fd1a652377445571b1c913ee14fe0b22d451943b4fe1d0578b71201f1ee106f4c','CCCB5AA9F6E1141C943165F1DB18B18CBCF70487','CCCB5AA9F6E1141C943165F1DB18B18CBCF70487',X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680be34b021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487921c07ff4f16327797bc5836071b5fcf2ae269c609b697c38b579f2449d0ff07f3e04db07822bfa83a6ca85308d99328c765f9b37b883a3526d38c3c005810ee6d9064acae1c68784781b9688be535a03ed5902d9ab5c9e5d28fb04aa621cb294445b9eab122d86afc0e2a4fd9a6a9af82f50b49295a9852f35c9ed8d816218ba98bc047cfe5fb9432e45ea63140bd16263728b1d1dd18d143b677e1ddd9cb5e939dd51cd7f2c2037cc89b5cee26917ea949e31c808996a5b7efc73636511173f59e2ab025902d86085110ac22988e86e663f19514c559a3b5a52838d1d1fa263f065fddf5fc8c8a1b4dac51aece76d536a3426f133be204dcb03c4a84242137373e39e7cec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680be34b021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf7048798cf07fc09e848aa6595d435805efbbbd0b05bec2fffa88b3b1d6e0a3ba80e300bdd83aa5f03bfcc9361b2a90e9cf8980c775d707467c638a13d65a01eda4b57d3560fada0c675399c263e668a02b84733b2c8e71d7fb9f15cb076f933571d32fc3377bb59ff64da6808eeb96210776126504ae9d6916124d3c679ba810a6c92dfe7d58eba7df22e9f07241d343d3e1792fe48d36fd6ec7d1ed291eae5d5d688872f5c723d5a12c424ff32c25d1348d2b683c5cf9128efb957b0026e607d593528a01dea6458c4709779b8f99bd689ef1bfde7146461317ee2793a130663388977488a9fd1a652377445571b1c913ee14fe0b22d451943b4fe1d0578b71201f1ee106f4c','CCCB5AA9F6E1141C943165F1DB18B18CBCF70487','',NULL,NULL,'',1); INSERT INTO contacts VALUES(10,'','bob@example.net',16384,0,0,'','',1745609549,'',0); - INSERT INTO msgs VALUES(10,'29b4af31-1560-4bc8-9b2b-083f2a3d0432@localhost','',0,10,2,2,1745609547,10,13,1,0,'Messages are guaranteed to be end-to-end encrypted from now on.','','S=11',0,0,0,0,NULL,'',NULL,1,0,'',0,0,0,'',0,NULL,0,NULL,0); + INSERT INTO msgs VALUES(10,'29b4af31-1560-4bc8-9b2b-083f2a3d0432@localhost','',0,10,2,2,1745609547,10,13,1,0,'Messages are end-to-end encrypted.','','S=11',0,0,0,0,NULL,'',NULL,1,0,'',0,0,0,'',0,NULL,0,NULL,0); INSERT INTO msgs VALUES(11,'411b3fdd-a20c-48c7-b94d-19c04654a1c5@localhost','',0,10,1,0,1745609548,10,26,1,0,'Hello!','',replace('A=1\nc=1','\n',char(10)),0,0,0,0,X'','','411b3fdd-a20c-48c7-b94d-19c04654a1c5@localhost',1,0,'',0,0,0,'Group',0,NULL,1,NULL,0); INSERT INTO chats VALUES(10,120,'Group',0,'',0,'-PYdPTYhrEl9L_C6osfpEpQu','g=1745609548',0,0,0,0,0,1745609547,0,NULL,1); INSERT INTO chats_contacts VALUES(10,1,1745609547,0); diff --git a/src/stock_str.rs b/src/stock_str.rs index 4d295da62..fbb7bd507 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -65,9 +65,6 @@ pub enum StockMessage { #[strum(props(fallback = "GIF"))] Gif = 23, - #[strum(props(fallback = "Encrypted message"))] - EncryptedMsg = 24, - #[strum(props(fallback = "End-to-end encryption available"))] E2eAvailable = 25, @@ -380,9 +377,10 @@ pub enum StockMessage { #[strum(props(fallback = "I left the group."))] MsgILeftGroup = 166, - #[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))] + #[strum(props(fallback = "Messages are end-to-end encrypted."))] ChatProtectionEnabled = 170, + // deprecated 2025-07 #[strum(props(fallback = "%1$s sent a message from another device."))] ChatProtectionDisabled = 171, @@ -1031,8 +1029,8 @@ pub(crate) async fn error_no_network(context: &Context) -> String { translated(context, StockMessage::ErrorNoNetwork).await } -/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.` -pub(crate) async fn chat_protection_enabled(context: &Context) -> String { +/// Stock string: `Messages are end-to-end encrypted.` +pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String { translated(context, StockMessage::ChatProtectionEnabled).await } @@ -1303,7 +1301,7 @@ impl Context { "[Error] No contact_id given".to_string() } } - ProtectionStatus::Protected => chat_protection_enabled(self).await, + ProtectionStatus::Protected => messages_e2e_encrypted(self).await, } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 5498ca5d0..573ef65df 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -43,6 +43,10 @@ use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::stock_str::StockStrings; use crate::tools::time; +/// The number of info messages added to new e2ee chats. +/// Currently this is "End-to-end encryption available", string `E2eAvailable`. +pub const E2EE_INFO_MSGS: usize = 1; + #[allow(non_upper_case_globals)] pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png"); diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 7b6b18178..d5595e22c 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -15,7 +15,9 @@ use crate::mimeparser::SystemMessage; use crate::receive_imf::receive_imf; use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::stock_str; -use crate::test_utils::{TestContext, TestContextManager, get_chat_msg, mark_as_verified}; +use crate::test_utils::{ + E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified, +}; use crate::tools::SystemTime; use crate::{e2ee, message}; @@ -132,7 +134,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { assert!(chat.is_protected()); let msg = get_chat_msg(&alice, chat.id, 0, 1).await; - let expected_text = stock_str::chat_protection_enabled(&alice).await; + let expected_text = stock_str::messages_e2e_encrypted(&alice).await; assert_eq!(msg.text, expected_text); } @@ -142,7 +144,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { assert!(chat.is_protected()); let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await; - let expected_text = stock_str::chat_protection_enabled(&fiona).await; + let expected_text = stock_str::messages_e2e_encrypted(&fiona).await; assert_eq!(msg0.text, expected_text); } @@ -162,7 +164,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { let chat = alice.get_chat(&fiona_new).await; assert!(!chat.is_protected()); - let msg = get_chat_msg(&alice, chat.id, 0, 1).await; + let msg = get_chat_msg(&alice, chat.id, 1, E2EE_INFO_MSGS + 1).await; assert_eq!(msg.text, "I have a new device"); // After recreating the chat, it should still be unprotected @@ -268,7 +270,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> { .await?; let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await; - let enabled = stock_str::chat_protection_enabled(&alice).await; + let enabled = stock_str::messages_e2e_encrypted(&alice).await; assert_eq!(msg0.text, enabled); assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled); diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index 61fca71b8..35039459e 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -13,7 +13,7 @@ use crate::config::Config; use crate::download::DownloadState; use crate::ephemeral; use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; -use crate::test_utils::{TestContext, TestContextManager}; +use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager}; use crate::tools::{self, SystemTime}; use crate::{message, sql}; @@ -250,7 +250,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> { ); let bob_grp = bob_instance.chat_id; assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.id); - assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 1); + assert_eq!(bob_grp.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1); Ok(()) } @@ -869,14 +869,14 @@ async fn test_send_big_webxdc_status_update() -> Result<()> { let sent2 = &alice.pop_sent_msg().await; let alice_update = sent2.load_from_db().await; assert_eq!(alice_update.text, BODY_DESCR.to_string()); - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 1); // Bob receives the instance. let bob_instance = bob.recv_msg(sent1).await; let bob_chat_id = bob_instance.chat_id; assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid); assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1); // Bob receives the status updates. bob.recv_msg_trash(sent2).await; @@ -896,7 +896,7 @@ async fn test_send_big_webxdc_status_update() -> Result<()> { r#"[{"payload":{"foo":"bar2"},"serial":2,"max_serial":3}, {"payload":{"foo":"bar3"},"serial":3,"max_serial":3}]"# ); - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1); Ok(()) } @@ -1485,7 +1485,7 @@ async fn test_webxdc_info_msg() -> Result<()> { let alice_chat = alice.create_chat(&bob).await; let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; let sent1 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 1); alice .send_webxdc_status_update( @@ -1495,7 +1495,7 @@ async fn test_webxdc_info_msg() -> Result<()> { .await?; alice.flush_status_updates().await?; let sent2 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2); let info_msg = alice.get_last_msg().await; assert!(info_msg.is_info()); assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); @@ -1517,7 +1517,7 @@ async fn test_webxdc_info_msg() -> Result<()> { let bob_instance = bob.recv_msg(sent1).await; let bob_chat_id = bob_instance.chat_id; bob.recv_msg_trash(sent2).await; - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2); let info_msg = bob.get_last_msg().await; assert!(info_msg.is_info()); assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); @@ -1536,7 +1536,10 @@ async fn test_webxdc_info_msg() -> Result<()> { let alice2_instance = alice2.recv_msg(sent1).await; let alice2_chat_id = alice2_instance.chat_id; alice2.recv_msg_trash(sent2).await; - assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2); + assert_eq!( + alice2_chat_id.get_msg_cnt(&alice2).await?, + E2EE_INFO_MSGS + 2 + ); let info_msg = alice2.get_last_msg().await; assert!(info_msg.is_info()); assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); @@ -1572,13 +1575,13 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> { .await?; alice.flush_status_updates().await?; let sent2 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2); alice .send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#) .await?; alice.flush_status_updates().await?; let sent3 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2); let info_msg = alice.get_last_msg().await; assert_eq!(info_msg.get_text(), "i2"); @@ -1586,9 +1589,9 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> { let bob_instance = bob.recv_msg(sent1).await; let bob_chat_id = bob_instance.chat_id; bob.recv_msg_trash(sent2).await; - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2); bob.recv_msg_trash(sent3).await; - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2); let info_msg = bob.get_last_msg().await; assert_eq!(info_msg.get_text(), "i2"); diff --git a/test-data/golden/chat_test_parallel_member_remove b/test-data/golden/chat_test_parallel_member_remove index 449f89a51..b2442e855 100644 --- a/test-data/golden/chat_test_parallel_member_remove +++ b/test-data/golden/chat_test_parallel_member_remove @@ -1,7 +1,8 @@ Group#Chat#10: Group chat [3 member(s)] -------------------------------------------------------------------------------- -Msg#10πŸ”’: (Contact#Contact#10): Hi! I created a group. [FRESH] -Msg#11πŸ”’: Me (Contact#Contact#Self): You left. [INFO] √ -Msg#12πŸ”’: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO] -Msg#13πŸ”’: (Contact#Contact#10): What a silence! [FRESH] +Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] +Msg#11πŸ”’: (Contact#Contact#10): Hi! I created a group. [FRESH] +Msg#12πŸ”’: Me (Contact#Contact#Self): You left. [INFO] √ +Msg#13πŸ”’: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO] +Msg#14πŸ”’: (Contact#Contact#10): What a silence! [FRESH] -------------------------------------------------------------------------------- diff --git a/test-data/golden/test_outgoing_encrypted_msg b/test-data/golden/test_outgoing_encrypted_msg index cd3b205be..06cecece6 100644 --- a/test-data/golden/test_outgoing_encrypted_msg +++ b/test-data/golden/test_outgoing_encrypted_msg @@ -1,5 +1,5 @@ Single#Chat#10: bob@example.net [KEY bob@example.net] πŸ›‘οΈ -------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO πŸ›‘οΈ] Msg#11πŸ”’: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √ -------------------------------------------------------------------------------- diff --git a/test-data/golden/test_outgoing_mua_msg_pgp b/test-data/golden/test_outgoing_mua_msg_pgp index b30278135..1a128d528 100644 --- a/test-data/golden/test_outgoing_mua_msg_pgp +++ b/test-data/golden/test_outgoing_mua_msg_pgp @@ -1,6 +1,6 @@ Single#Chat#10: bob@example.net [KEY bob@example.net] πŸ›‘οΈ -------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO πŸ›‘οΈ] Msg#11πŸ”’: (Contact#Contact#10): Heyho from DC [FRESH] Msg#13πŸ”’: Me (Contact#Contact#Self): Sending with DC again √ -------------------------------------------------------------------------------- diff --git a/test-data/golden/two_group_securejoins b/test-data/golden/two_group_securejoins index f93d0bbe6..3684034fd 100644 --- a/test-data/golden/two_group_securejoins +++ b/test-data/golden/two_group_securejoins @@ -4,6 +4,6 @@ Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this Waiting for the device of alice@example.org to reply… [NOTICED][INFO] Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO] -Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#17: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO πŸ›‘οΈ] Msg#18πŸ”’: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO] --------------------------------------------------------------------------------