feat: add "e2ee encrypted" info message to all e2ee chats (#7008)

this PR adds a info message "messages are end-to-end-encrypted" also for
chats created by eg. vcards. by the removal of lock icons, this is a
good place to hint for that in addition; this is also what eg. whatsapp
and others are doing

the wording itself is tweaked at
https://github.com/deltachat/deltachat-android/pull/3817 (and there is
also the rough idea to make the message a little more outstanding, by
some more dedicated colors)

~~did not test in practise, if this leads to double "e2ee info messages"
on secure join, tests look good, however.~~ EDIT: did lots of practise
tests meanwhile :)

most of the changes in this PR are about test ...

ftr, in another PR, after 2.0 reeases, there could probably quite some
code cleanup wrt set-protection, protection-disabled etc.

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
This commit is contained in:
bjoern
2025-07-18 22:08:33 +02:00
committed by GitHub
parent a2df29515a
commit 2c7d51f98f
29 changed files with 261 additions and 122 deletions

View File

@@ -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_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_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_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_ENABLED (11) - Info-message for "Chat is protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet", * - 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 * 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 * 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_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, * For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID. * 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_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10 #define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11 #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_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32 #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. /// Used in summaries.
#define DC_STR_GIF 23 #define DC_STR_GIF 23
/// "Encrypted message" /// @deprecated 2025-07, this string is no longer needed.
///
/// Used in subjects of outgoing messages.
#define DC_STR_ENCRYPTEDMSG 24 #define DC_STR_ENCRYPTEDMSG 24
/// "End-to-end encryption available." /// "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. /// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163 #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. /// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170 #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." /// "%1$s sent a message from another device."
/// ///
/// Used in info messages. /// Used in info messages.
/// @deprecated 2025-07
#define DC_STR_CHAT_PROTECTION_DISABLED 171 #define DC_STR_CHAT_PROTECTION_DISABLED 171
/// "Others will only see this group after you sent a first message." /// "Others will only see this group after you sent a first message."

View File

@@ -416,6 +416,9 @@ pub enum SystemMessageType {
/// Chat ephemeral message timer is changed. /// Chat ephemeral message timer is changed.
EphemeralTimerChanged, EphemeralTimerChanged,
// Chat is e2ee
ChatE2ee,
// Chat protection state changed // Chat protection state changed
ChatProtectionEnabled, ChatProtectionEnabled,
ChatProtectionDisabled, ChatProtectionDisabled,
@@ -450,6 +453,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled, SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
SystemMessage::LocationOnly => SystemMessageType::LocationOnly, SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged, SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled, SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled, SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync, SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,

View File

@@ -95,8 +95,10 @@ describe("online tests", function () {
false, false,
); );
expect(messageList).have.length(1); // There are 2 messages in the chat:
const message = await dc.rpc.getMessage(accountId2, messageList[0]); // '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.text).equal("Hello");
expect(message.showPadlock).equal(true); expect(message.showPadlock).equal(true);
}); });

View File

@@ -13,6 +13,12 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
from ._utils import futuremethod from ._utils import futuremethod
from .rpc import Rpc 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: class ACFactory:
"""Test account factory.""" """Test account factory."""

View File

@@ -36,6 +36,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
assert ac1.get_config("bcc_self") == "1" assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first. # 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) 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 assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text

View File

@@ -12,6 +12,7 @@ import pytest
from deltachat_rpc_client import Contact, EventType, Message, events from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState 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 from deltachat_rpc_client.rpc import JsonRpcError
@@ -457,8 +458,12 @@ def test_wait_next_messages(acfactory) -> None:
alice_chat_bot.send_text("Hello!") alice_chat_bot.send_text("Hello!")
next_messages = next_messages_task.result() 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!" assert snapshot.text == "Hello!"

View File

@@ -20,6 +20,12 @@ import deltachat
from . import Account, account_hookimpl, const, get_core_info from . import Account, account_hookimpl, const, get_core_info
from .events import FFIEventLogger, FFIEventTracker 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): def pytest_addoption(parser):
group = parser.getgroup("deltachat testplugin options") group = parser.getgroup("deltachat testplugin options")
@@ -606,7 +612,7 @@ class ACFactory:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2) msg = ac2.get_message_by_id(ev.data2)
assert msg is not None 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() msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text assert "Member Me " in msg.text and " added by " in msg.text

View File

@@ -133,8 +133,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert "added" in msg.text.lower() assert "added" in msg.text.lower()
assert any( assert any(
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on." m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
for m in msg.chat.get_messages()
) )
lp.sec("ac1: send message") lp.sec("ac1: send message")
msg_out = chat1.send_text("hello") 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") assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat chat2 = msg_in.chat
assert chat2.is_protected() 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() assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message") 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") ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2) msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message() 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 # We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")

View File

@@ -10,6 +10,7 @@ from imap_tools import AND, U
import deltachat as dc import deltachat as dc
from deltachat import account_hookimpl, Message from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker from deltachat.tracker import ImexTracker
from deltachat.testplugin import E2EE_INFO_MSGS
def test_basic_imap_api(acfactory, tmp_path): def test_basic_imap_api(acfactory, tmp_path):
@@ -408,6 +409,10 @@ def test_forward_messages(acfactory, lp):
msg_out = chat.send_text("message2") msg_out = chat.send_text("message2")
lp.sec("ac2: wait for receive") 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") ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == msg_out.id assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(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: with ac2.direct_imap.idle() as idle2:
ac2.start_io() 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") ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2) 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") lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1") 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") lp.sec("disable ac1 MDNs")
ac1.set_config("mdns_enabled", "0") ac1.set_config("mdns_enabled", "0")
@@ -746,7 +756,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("wait for ac2 to receive message") lp.sec("wait for ac2 to receive message")
msg = ac2._evtracker.wait_next_incoming_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") lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg]) 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 # MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED") 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. # 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.") 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 assert m == msg_out
lp.sec("wait for ac2 to receive message") 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") ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
assert ev.data2 == msg_out.id assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(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 assert contact2.addr == some1_addr
chat2 = contact2.create_chat() chat2 = contact2.create_chat()
messages = chat2.get_messages() messages = chat2.get_messages()
assert len(messages) == 3 assert len(messages) == 3 + E2EE_INFO_MSGS
assert messages[0].text == "msg1" assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert messages[1].filemime == "image/png" assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname") ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname" assert ac.get_config("displayname") == "new displayname"
@@ -1414,8 +1429,8 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network() ac1.maybe_network()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED) ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages() msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1 assert len(msgs) == 1 + E2EE_INFO_MSGS
assert msgs[0].text == "Hi" assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched") 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) ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages() msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2 assert len(msgs) == 2 + E2EE_INFO_MSGS
assert msgs[1].text == "Hi 2" assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
def test_fetch_deleted_msg(acfactory, lp): def test_fetch_deleted_msg(acfactory, lp):

View File

@@ -6,6 +6,7 @@ import pytest
import deltachat as dc import deltachat as dc
from deltachat.tracker import ImexFailed from deltachat.tracker import ImexFailed
from deltachat import Account, Message from deltachat import Account, Message
from deltachat.testplugin import E2EE_INFO_MSGS
class TestOfflineAccountBasic: class TestOfflineAccountBasic:
@@ -461,9 +462,9 @@ class TestOfflineChat:
assert contact2.addr == ac_contact.get_config("addr") assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat() chat2 = contact2.create_chat()
messages = chat2.get_messages() messages = chat2.get_messages()
assert len(messages) == 2 assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0].text == "msg1" assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1].filename) assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path): def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
passphrase1 = "passphrase1" passphrase1 = "passphrase1"
@@ -500,9 +501,9 @@ class TestOfflineChat:
contact2_addr = contact2.addr contact2_addr = contact2.addr
chat2 = contact2.create_chat() chat2 = contact2.create_chat()
messages = chat2.get_messages() messages = chat2.get_messages()
assert len(messages) == 2 assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0].text == "msg1" assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1].filename) assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
ac2.shutdown() ac2.shutdown()
@@ -517,9 +518,9 @@ class TestOfflineChat:
assert contact2.addr == contact2_addr assert contact2.addr == contact2_addr
chat2 = contact2.create_chat() chat2 = contact2.create_chat()
messages = chat2.get_messages() messages = chat2.get_messages()
assert len(messages) == 2 assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0].text == "msg1" assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1].filename) assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_import_export_with_passphrase(self, acfactory, tmp_path): def test_import_export_with_passphrase(self, acfactory, tmp_path):
passphrase = "test_passphrase" passphrase = "test_passphrase"
@@ -557,9 +558,9 @@ class TestOfflineChat:
assert contact2.addr == ac_contact.get_config("addr") assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat() chat2 = contact2.create_chat()
messages = chat2.get_messages() messages = chat2.get_messages()
assert len(messages) == 2 assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0].text == "msg1" assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1].filename) assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path): 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") assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat() chat2 = contact2.create_chat()
messages = chat2.get_messages() messages = chat2.get_messages()
assert len(messages) == 2 assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0].text == "msg1" assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1].filename) assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
ac2.shutdown() ac2.shutdown()
@@ -620,9 +621,9 @@ class TestOfflineChat:
assert contact2.addr == ac_contact.get_config("addr") assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat() chat2 = contact2.create_chat()
messages = chat2.get_messages() messages = chat2.get_messages()
assert len(messages) == 2 assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0].text == "msg1" assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1].filename) assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_set_get_draft(self, chat1): def test_set_get_draft(self, chat1):
msg1 = Message.new_empty(chat1.account, "text") msg1 = Message.new_empty(chat1.account, "text")

View File

@@ -349,6 +349,8 @@ impl ChatId {
chat_id chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, timestamp) .add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
.await?; .await?;
} else {
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
} }
info!( info!(
@@ -604,6 +606,42 @@ impl ChatId {
Ok(()) 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. /// Sets protection and adds a message.
/// ///
/// `timestamp_sort` is used as the timestamp of the added message /// `timestamp_sort` is used as the timestamp of the added message
@@ -2673,6 +2711,10 @@ impl ChatIdBlocked {
smeared_time, smeared_time,
) )
.await?; .await?;
} else {
chat_id
.maybe_add_encrypted_msg(context, smeared_time)
.await?;
} }
Ok(Self { Ok(Self {

View File

@@ -8,7 +8,7 @@ use crate::message::{MessengerMessage, delete_msgs};
use crate::mimeparser::{self, MimeMessage}; use crate::mimeparser::{self, MimeMessage};
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
use crate::test_utils::{ 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, TimeShiftFalsePositiveNote, sync,
}; };
use pretty_assertions::assert_eq; 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?; forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
let forwarded_msg = bob.pop_sent_msg().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!( assert_ne!(
forwarded_msg.load_from_db().await.rfc724_mid, forwarded_msg.load_from_db().await.rfc724_mid,
msg.rfc724_mid, msg.rfc724_mid,
@@ -2132,7 +2132,7 @@ async fn test_forward_info_msg() -> Result<()> {
assert!(msg1.get_text().contains("bob@example.net")); assert!(msg1.get_text().contains("bob@example.net"));
let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?; 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?; forward_msgs(alice, &[msg1.id], chat_id2).await?;
let msg2 = alice.get_last_msg_in(chat_id2).await; let msg2 = alice.get_last_msg_in(chat_id2).await;
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state 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; let sent1_ts_sent = msg.timestamp_sent;
assert_eq!(msg.get_text(), "alice->bob"); assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2); 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; bob.recv_msg(&sent2).await;
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); 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; let received = bob.recv_msg_opt(&sent3).await;
// No message should actually be added since we already know this message: // No message should actually be added since we already know this message:
assert!(received.is_none()); assert!(received.is_none());
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); 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 does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
fiona.recv_msg(&sent2).await; fiona.recv_msg(&sent2).await;
let msg = fiona.recv_msg(&sent3).await; let msg = fiona.recv_msg(&sent3).await;
assert_eq!(msg.get_text(), "alice->bob"); assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3); 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?; let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
assert_eq!(msg_from.get_addr(), "alice@example.org"); assert_eq!(msg_from.get_addr(), "alice@example.org");
assert!(sent1_ts_sent < msg.timestamp_sent); 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_msg = bob.recv_msg(&sent1).await;
let bob_chat_id = bob_msg.chat_id; let bob_chat_id = bob_msg.chat_id;
assert_eq!(bob_msg.text, "zext me in delra.cat"); 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?; 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; 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(()) Ok(())
} }
@@ -4549,28 +4561,34 @@ async fn test_send_delete_request() -> Result<()> {
// Alice sends a message, then sends a deletion request // Alice sends a message, then sends a deletion request
let sent1 = alice.send_text(alice_chat.id, "wtf").await; let sent1 = alice.send_text(alice_chat.id, "wtf").await;
let alice_msg = sent1.load_from_db().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?; message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
let sent2 = alice.pop_sent_msg().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 // Bob receives both messages and has nothing the end
let bob_msg = bob.recv_msg(&sent1).await; let bob_msg = bob.recv_msg(&sent1).await;
assert_eq!(bob_msg.text, "wtf"); 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; 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 // Alice has another device, and there is also nothing at the end
let alice2 = &tcm.alice().await; let alice2 = &tcm.alice().await;
alice2.recv_msg(&sent0).await; alice2.recv_msg(&sent0).await;
let alice2_msg = alice2.recv_msg(&sent1).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; 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(()) Ok(())
} }

View File

@@ -8,7 +8,7 @@ use crate::chatlist::Chatlist;
use crate::constants::Chattype; use crate::constants::Chattype;
use crate::mimeparser::SystemMessage; use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf; 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}; use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[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; 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()); assert!(bob.get_next_msgs().await?.is_empty());
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;

View File

@@ -277,7 +277,7 @@ mod tests {
use crate::chat::{get_chat_msgs, send_msg}; use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer; use crate::ephemeral::Timer;
use crate::receive_imf::receive_imf_from_inbox; use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::TestContext; use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
#[test] #[test]
fn test_downloadstate_values() { fn test_downloadstate_values() {
@@ -459,7 +459,10 @@ mod tests {
.await?; .await?;
let msg = bob.get_last_msg().await; let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id; 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); assert_eq!(msg.download_state(), DownloadState::Available);
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat // downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
@@ -472,7 +475,7 @@ mod tests {
None, None,
) )
.await?; .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!( assert!(
Message::load_from_db_optional(&bob, msg.id) Message::load_from_db_optional(&bob, msg.id)
.await? .await?

View File

@@ -963,6 +963,7 @@ impl Message {
| SystemMessage::SecurejoinMessage | SystemMessage::SecurejoinMessage
| SystemMessage::LocationStreamingEnabled | SystemMessage::LocationStreamingEnabled
| SystemMessage::LocationOnly | SystemMessage::LocationOnly
| SystemMessage::ChatE2ee
| SystemMessage::ChatProtectionEnabled | SystemMessage::ChatProtectionEnabled
| SystemMessage::ChatProtectionDisabled | SystemMessage::ChatProtectionDisabled
| SystemMessage::InvalidUnencryptedMail | SystemMessage::InvalidUnencryptedMail

View File

@@ -7,7 +7,7 @@ use crate::config::Config;
use crate::reaction::send_reaction; use crate::reaction::send_reaction;
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
use crate::test_utils; use crate::test_utils;
use crate::test_utils::{TestContext, TestContextManager}; use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
#[test] #[test]
fn test_guess_msgtype_from_suffix() { 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?; let chats = Chatlist::try_load(&bob, 0, None, None).await?;
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; 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); assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
// that has no effect in contact request // that has no effect in contact request
@@ -358,7 +358,7 @@ async fn test_markseen_msgs() -> Result<()> {
assert_eq!(bob_chat.blocked, Blocked::Request); assert_eq!(bob_chat.blocked, Blocked::Request);
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; 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_chat_id.accept(&bob).await.unwrap();
// bob sends to alice, // 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 // Alice sends a messsage and receives it on the other device
let sent1 = alice.send_text(alice_chat_id, "foo").await; 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 msg = alice2.recv_msg(&sent1).await;
let alice2_chat_id = msg.chat_id; let alice2_chat_id = msg.chat_id;
assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.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 // Alice deletes the message; this should happen on both devices as well
delete_msgs(alice, &[sent1.sender_msg_id]).await?; 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; 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(()) Ok(())
} }

View File

@@ -181,10 +181,10 @@ pub enum SystemMessage {
/// Chat ephemeral message timer is changed. /// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10, EphemeralTimerChanged = 10,
/// "Messages are guaranteed to be end-to-end encrypted from now on." /// "Messages are end-to-end encrypted."
ChatProtectionEnabled = 11, ChatProtectionEnabled = 11,
/// "%1$s sent a message from another device." /// "%1$s sent a message from another device.", deprecated 2025-07
ChatProtectionDisabled = 12, ChatProtectionDisabled = 12,
/// Message can't be sent because of `Invalid unencrypted mail to <>` /// 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. /// This message contains a users iroh node address.
IrohNodeAddr = 40, IrohNodeAddr = 40,
/// "Messages are end-to-end encrypted."
ChatE2ee = 50,
} }
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup"; const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";

View File

@@ -407,6 +407,7 @@ mod tests {
use crate::message::{MessageState, delete_msgs}; use crate::message::{MessageState, delete_msgs};
use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::sql::housekeeping; use crate::sql::housekeeping;
use crate::test_utils::E2EE_INFO_MSGS;
use crate::test_utils::TestContext; use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager; use crate::test_utils::TestContextManager;
use crate::tools::SystemTime; use crate::tools::SystemTime;
@@ -653,13 +654,25 @@ Here's my footer -- bob@example.net"
let chat_alice = alice.create_chat(&bob).await; let chat_alice = alice.create_chat(&bob).await;
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await; let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
let bob_msg = bob.recv_msg(&alice_msg).await; let bob_msg = bob.recv_msg(&alice_msg).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 1); assert_eq!(
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 1); 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; let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
bob.recv_msg(&alice_msg2).await; bob.recv_msg(&alice_msg2).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2); assert_eq!(
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2); 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?; 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(); send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?; expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
expect_no_unwanted_events(&bob).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 bob_reaction_msg = bob.pop_sent_msg().await;
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await; let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
assert_eq!(alice_reaction_msg.state, MessageState::InFresh); 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?; let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1"); assert_eq!(reactions.to_string(), "👍1");

View File

@@ -15,8 +15,9 @@ use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::imap::prefetch_should_download; use crate::imap::prefetch_should_download;
use crate::imex::{ImexMode, imex}; use crate::imex::{ImexMode, imex};
use crate::securejoin::get_securejoin_qr; use crate::securejoin::get_securejoin_qr;
use crate::test_utils::mark_as_verified; use crate::test_utils::{
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg}; E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
};
use crate::tools::{SystemTime, time}; use crate::tools::{SystemTime, time};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[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?; let chats = Chatlist::try_load(bob, 0, None, None).await?;
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0)?; 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(()) 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. // The big message must go away from the 1:1 chat.
let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?; let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?;
assert!(msgs.is_empty()); assert_eq!(msgs.len(), E2EE_INFO_MSGS);
Ok(()) Ok(())
} }

View File

@@ -6,7 +6,7 @@ use crate::chatlist::Chatlist;
use crate::constants::Chattype; use crate::constants::Chattype;
use crate::key::self_fingerprint; use crate::key::self_fingerprint;
use crate::receive_imf::receive_imf; 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::{ use crate::test_utils::{
TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg, 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 chat = alice.get_chat(&bob).await;
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await; let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
assert!(msg.is_info()); 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); assert_eq!(msg.get_text(), expected_text);
if case == SetupContactCase::CheckProtectionTimestamp { if case == SetupContactCase::CheckProtectionTimestamp {
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1); 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); 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; let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info()); 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -540,7 +540,7 @@ async fn test_secure_join() -> Result<()> {
// - You added member bob@example.net // - You added member bob@example.net
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await; let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
assert!(msg.is_info()); 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); assert_eq!(msg.get_text(), expected_text);
} }

File diff suppressed because one or more lines are too long

View File

@@ -65,9 +65,6 @@ pub enum StockMessage {
#[strum(props(fallback = "GIF"))] #[strum(props(fallback = "GIF"))]
Gif = 23, Gif = 23,
#[strum(props(fallback = "Encrypted message"))]
EncryptedMsg = 24,
#[strum(props(fallback = "End-to-end encryption available"))] #[strum(props(fallback = "End-to-end encryption available"))]
E2eAvailable = 25, E2eAvailable = 25,
@@ -380,9 +377,10 @@ pub enum StockMessage {
#[strum(props(fallback = "I left the group."))] #[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166, 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, ChatProtectionEnabled = 170,
// deprecated 2025-07
#[strum(props(fallback = "%1$s sent a message from another device."))] #[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171, ChatProtectionDisabled = 171,
@@ -1031,8 +1029,8 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
translated(context, StockMessage::ErrorNoNetwork).await translated(context, StockMessage::ErrorNoNetwork).await
} }
/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.` /// Stock string: `Messages are end-to-end encrypted.`
pub(crate) async fn chat_protection_enabled(context: &Context) -> String { pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await translated(context, StockMessage::ChatProtectionEnabled).await
} }
@@ -1303,7 +1301,7 @@ impl Context {
"[Error] No contact_id given".to_string() "[Error] No contact_id given".to_string()
} }
} }
ProtectionStatus::Protected => chat_protection_enabled(self).await, ProtectionStatus::Protected => messages_e2e_encrypted(self).await,
} }
} }

View File

@@ -43,6 +43,10 @@ use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str::StockStrings; use crate::stock_str::StockStrings;
use crate::tools::time; 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)] #[allow(non_upper_case_globals)]
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png"); pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");

View File

@@ -15,7 +15,9 @@ use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str; 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::tools::SystemTime;
use crate::{e2ee, message}; use crate::{e2ee, message};
@@ -132,7 +134,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
assert!(chat.is_protected()); assert!(chat.is_protected());
let msg = get_chat_msg(&alice, chat.id, 0, 1).await; 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); assert_eq!(msg.text, expected_text);
} }
@@ -142,7 +144,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
assert!(chat.is_protected()); assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await; 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); 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; let chat = alice.get_chat(&fiona_new).await;
assert!(!chat.is_protected()); 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"); assert_eq!(msg.text, "I have a new device");
// After recreating the chat, it should still be unprotected // After recreating the chat, it should still be unprotected
@@ -268,7 +270,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
.await?; .await?;
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).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.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled); assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);

View File

@@ -13,7 +13,7 @@ use crate::config::Config;
use crate::download::DownloadState; use crate::download::DownloadState;
use crate::ephemeral; use crate::ephemeral;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; 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::tools::{self, SystemTime};
use crate::{message, sql}; use crate::{message, sql};
@@ -250,7 +250,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> {
); );
let bob_grp = bob_instance.chat_id; let bob_grp = bob_instance.chat_id;
assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.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(()) Ok(())
} }
@@ -869,14 +869,14 @@ async fn test_send_big_webxdc_status_update() -> Result<()> {
let sent2 = &alice.pop_sent_msg().await; let sent2 = &alice.pop_sent_msg().await;
let alice_update = sent2.load_from_db().await; let alice_update = sent2.load_from_db().await;
assert_eq!(alice_update.text, BODY_DESCR.to_string()); 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. // Bob receives the instance.
let bob_instance = bob.recv_msg(sent1).await; let bob_instance = bob.recv_msg(sent1).await;
let bob_chat_id = bob_instance.chat_id; let bob_chat_id = bob_instance.chat_id;
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid); assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); 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 receives the status updates.
bob.recv_msg_trash(sent2).await; 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}, r#"[{"payload":{"foo":"bar2"},"serial":2,"max_serial":3},
{"payload":{"foo":"bar3"},"serial":3,"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(()) Ok(())
} }
@@ -1485,7 +1485,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
let alice_chat = alice.create_chat(&bob).await; let alice_chat = alice.create_chat(&bob).await;
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
let sent1 = &alice.pop_sent_msg().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 alice
.send_webxdc_status_update( .send_webxdc_status_update(
@@ -1495,7 +1495,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
.await?; .await?;
alice.flush_status_updates().await?; alice.flush_status_updates().await?;
let sent2 = &alice.pop_sent_msg().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; let info_msg = alice.get_last_msg().await;
assert!(info_msg.is_info()); assert!(info_msg.is_info());
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); 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_instance = bob.recv_msg(sent1).await;
let bob_chat_id = bob_instance.chat_id; let bob_chat_id = bob_instance.chat_id;
bob.recv_msg_trash(sent2).await; 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; let info_msg = bob.get_last_msg().await;
assert!(info_msg.is_info()); assert!(info_msg.is_info());
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); 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_instance = alice2.recv_msg(sent1).await;
let alice2_chat_id = alice2_instance.chat_id; let alice2_chat_id = alice2_instance.chat_id;
alice2.recv_msg_trash(sent2).await; 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; let info_msg = alice2.get_last_msg().await;
assert!(info_msg.is_info()); assert!(info_msg.is_info());
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
@@ -1572,13 +1575,13 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
.await?; .await?;
alice.flush_status_updates().await?; alice.flush_status_updates().await?;
let sent2 = &alice.pop_sent_msg().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 alice
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#) .send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#)
.await?; .await?;
alice.flush_status_updates().await?; alice.flush_status_updates().await?;
let sent3 = &alice.pop_sent_msg().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; let info_msg = alice.get_last_msg().await;
assert_eq!(info_msg.get_text(), "i2"); 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_instance = bob.recv_msg(sent1).await;
let bob_chat_id = bob_instance.chat_id; let bob_chat_id = bob_instance.chat_id;
bob.recv_msg_trash(sent2).await; 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; 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; let info_msg = bob.get_last_msg().await;
assert_eq!(info_msg.get_text(), "i2"); assert_eq!(info_msg.get_text(), "i2");

View File

@@ -1,7 +1,8 @@
Group#Chat#10: Group chat [3 member(s)] Group#Chat#10: Group chat [3 member(s)]
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
Msg#10🔒: (Contact#Contact#10): Hi! I created a group. [FRESH] Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#11🔒: Me (Contact#Contact#Self): You left. [INFO] √ Msg#11🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#12🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO] Msg#12🔒: Me (Contact#Contact#Self): You left. [INFO]
Msg#13🔒: (Contact#Contact#10): What a silence! [FRESH] 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]
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️ 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. √ Msg#11🔒: Me (Contact#Contact#Self): Test This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️ 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#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √ Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@@ -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] 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#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] Msg#18🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------