mirror of
https://github.com/chatmail/core.git
synced 2026-10-03 11:40:38 +03:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b24f24f5d | ||
|
|
736979b451 | ||
|
|
5a882b0aca | ||
|
|
1e36fb74be | ||
|
|
ebf1c87483 | ||
|
|
172ebc08f9 | ||
|
|
eeb9337a56 | ||
|
|
a1eb593d81 | ||
|
|
7e070efc28 | ||
|
|
5d3145d165 | ||
|
|
1605b971ba | ||
|
|
22578ea4b6 | ||
|
|
076f83f320 | ||
|
|
8ab98019a7 | ||
|
|
76d664f9cb | ||
|
|
f0e8e80894 | ||
|
|
3756537a13 | ||
|
|
2ad4301f44 | ||
|
|
6f4587a030 |
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.98.1
|
||||
RUST_VERSION: 1.99.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
|
||||
Generated
+6
-6
@@ -1327,7 +1327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1435,7 +1435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1456,14 +1456,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
dependencies = [
|
||||
"deltachat-jsonrpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1479,7 +1479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1508,7 +1508,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -21,9 +21,9 @@ fn main() {
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
||||
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
||||
prefix = option_env!("PREFIX").unwrap_or_else(|| "/usr/local"),
|
||||
libdir = option_env!("LIBDIR").unwrap_or_else(|| "/usr/local/lib"),
|
||||
includedir = option_env!("INCLUDEDIR").unwrap_or_else(|| "/usr/local/include"),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
|
||||
@@ -6178,6 +6178,17 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_CHAT_DELETED 2023
|
||||
|
||||
|
||||
/**
|
||||
* The list of pinned messages for the chat has changed.
|
||||
*
|
||||
* Some message got pinned, or pinned message is unpinned or deleted.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) 0
|
||||
*/
|
||||
#define DC_EVENT_PINNED_MESSAGES_CHANGED 2024
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
*
|
||||
|
||||
@@ -514,6 +514,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ChatDeleted { .. } => 2023,
|
||||
EventType::PinnedMessagesChanged { .. } => 2024,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
EventType::LocationChanged(_) => 2035,
|
||||
EventType::ConfigureProgress { .. } => 2041,
|
||||
@@ -587,7 +588,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgReadCountChanged { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. }
|
||||
| EventType::ChatDeleted { chat_id } => chat_id.to_u32() as libc::c_int,
|
||||
| EventType::ChatDeleted { chat_id }
|
||||
| EventType::PinnedMessagesChanged { chat_id } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id.to_u32() as libc::c_int
|
||||
@@ -661,7 +663,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
| EventType::TransportsModified => 0,
|
||||
| EventType::TransportsModified
|
||||
| EventType::PinnedMessagesChanged { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
@@ -762,7 +765,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::TransportsModified => ptr::null_mut(),
|
||||
| EventType::TransportsModified
|
||||
| EventType::PinnedMessagesChanged { .. } => ptr::null_mut(),
|
||||
EventType::IncomingCall {
|
||||
place_call_info, ..
|
||||
} => place_call_info.strdup(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.62.0"
|
||||
"version": "2.63.0-dev"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -258,6 +258,15 @@ pub enum EventType {
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// The list of pinned messages for the chat has changed.
|
||||
///
|
||||
/// Some message got pinned, or pinned message is unpinned or deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PinnedMessagesChanged {
|
||||
/// ID of the chat where the list of pinned messages changed.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged {
|
||||
@@ -512,6 +521,9 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
CoreEventType::PinnedMessagesChanged { chat_id } => PinnedMessagesChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingReaction {
|
||||
chat_id,
|
||||
contact_id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -27,55 +27,6 @@ use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
@@ -304,7 +255,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
============================================="
|
||||
),
|
||||
@@ -444,15 +394,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
const IMEX_COMMANDS: [&str; 9] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -156,7 +156,6 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -24,8 +24,16 @@ class DirectImap:
|
||||
|
||||
def __init__(self, account: Account, addr=None, password=None) -> None:
|
||||
self.account = account
|
||||
self.addr = addr or account.get_config("addr")
|
||||
self.password = password or account.get_config("mail_pw")
|
||||
if addr is None or password is None:
|
||||
transport = account.list_transports()[-1]
|
||||
if addr is None:
|
||||
self.addr = transport["addr"]
|
||||
else:
|
||||
self.addr = addr
|
||||
if password is None:
|
||||
self.password = transport["password"]
|
||||
else:
|
||||
self.password = password
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
self.connect()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -27,7 +28,7 @@ def test_qr_setup_contact(acf) -> None:
|
||||
|
||||
def test_qr_setup_contact_svg(acf) -> None:
|
||||
alice = acf.new_configured_account()
|
||||
_, _, domain = alice.get_config("addr").rpartition("@")
|
||||
_, _, domain = alice.list_transports()[0]["addr"].rpartition("@")
|
||||
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
|
||||
@@ -43,6 +44,7 @@ def test_qr_setup_contact_svg(acf) -> None:
|
||||
def test_qr_securejoin(acf):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
|
||||
alice.set_config("displayname", "Alice")
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
alice2 = alice.clone()
|
||||
@@ -67,7 +69,7 @@ def test_qr_securejoin(acf):
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "You were added by Alice."
|
||||
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
@@ -89,6 +91,30 @@ def test_qr_securejoin(acf):
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_qr_securejoin_request_deleted_on_all_transports(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice_chat = alice.create_group("Group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
alice2 = alice.clone()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
alice_chat.remove_contact(bob)
|
||||
# Stop and start io in order to wait until the remove message is sent out.
|
||||
alice.stop_io()
|
||||
alice.bring_online()
|
||||
|
||||
# Membership timestamps have a resolution of one second.
|
||||
time.sleep(1)
|
||||
# By now, the securejoin message sent by Bob must be deleted on Alice's relays.
|
||||
# Otherwise, alice2 would execute securejoin again and re-add Bob.
|
||||
alice2.bring_online()
|
||||
alice2_chat = alice2.get_chat_by_id(alice_chat.id)
|
||||
assert alice2.create_contact(bob) not in alice2_chat.get_contacts()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
@@ -499,11 +525,9 @@ def test_aeap_flow(acf):
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
logging.info("changing email account")
|
||||
ac1.set_config("addr", addr)
|
||||
ac1.set_config("mail_pw", password)
|
||||
ac1.stop_io()
|
||||
ac1.configure()
|
||||
ac1.start_io()
|
||||
old_addr = ac1.list_transports()[0]["addr"]
|
||||
ac1.add_transport_from_qr(acf.get_account_qr())
|
||||
ac1.delete_transport(old_addr)
|
||||
|
||||
logging.info("sending second message")
|
||||
msg_out = chat.send_text("changed address").get_snapshot()
|
||||
@@ -525,6 +549,7 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
but different key fingerprint while a securejoin with that contact is still pending.
|
||||
"""
|
||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
||||
ac3.set_config("displayname", "ac3")
|
||||
|
||||
# ac3 creates a group with ac1.
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
@@ -536,7 +561,7 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
|
||||
assert snapshot.text == "You were added by ac3."
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 sets up contact with ac1
|
||||
@@ -576,6 +601,8 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
|
||||
def test_withdraw_securejoin_qr(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.set_config("displayname", "Alice")
|
||||
bob.set_config("displayname", "Bob")
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
@@ -588,11 +615,11 @@ def test_withdraw_securejoin_qr(acf):
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "You were added by Alice."
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||
assert snapshot.text == "Group left by Bob."
|
||||
|
||||
logging.info("Alice withdraws QR code.")
|
||||
qr = alice.check_qr(qr_code)
|
||||
|
||||
@@ -155,7 +155,7 @@ def test_list_transports(acf) -> None:
|
||||
def test_account(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
@@ -318,7 +318,7 @@ def test_chat(acf) -> None:
|
||||
def test_contact(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
@@ -604,7 +604,6 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
||||
(ac1, some1) = acf.get_online_accounts(2)
|
||||
|
||||
log.section("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
@@ -622,7 +621,6 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
||||
contacts = ac.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.get_snapshot().address == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.62.0"
|
||||
"version": "2.63.0-dev"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.62.0"
|
||||
version = "2.63.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
+3
-1
@@ -34,6 +34,8 @@ const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||
"chat.nuvon.app",
|
||||
"chat.tinydispatch.org",
|
||||
"chat.vim.wtf",
|
||||
"chatmail.au",
|
||||
"chatmail.cc",
|
||||
"chatmail.uk",
|
||||
"chtml.ca",
|
||||
"deltachat.me",
|
||||
@@ -163,7 +165,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
for _ in 0..NUM_TRANSPORTS_TARGET {
|
||||
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
|
||||
context
|
||||
.set_config_internal(Config::AutorelayFinished, config::from_bool(true))
|
||||
.set_config_internal(Config::AutorelayFinished, Some(config::from_bool(true)))
|
||||
.await?;
|
||||
|
||||
return Ok(relay_added);
|
||||
|
||||
+3
-3
@@ -164,9 +164,9 @@ impl<'a> BlobObject<'a> {
|
||||
/// you want to create a [BlobObject] for a filename read from the
|
||||
/// database.
|
||||
pub fn from_name(context: &'a Context, name: &str) -> Result<BlobObject<'a>> {
|
||||
let name = match name.starts_with("$BLOBDIR/") {
|
||||
true => name.splitn(2, '/').last().unwrap(),
|
||||
false => name,
|
||||
let name = match name.strip_prefix("$BLOBDIR/") {
|
||||
Some(name) => name,
|
||||
None => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {name}"));
|
||||
|
||||
+11
-4
@@ -3639,7 +3639,7 @@ pub(crate) async fn create_out_broadcast_ext(
|
||||
)?;
|
||||
ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}");
|
||||
let mut params: Params = Params::new();
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time())?;
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time());
|
||||
|
||||
t.execute(
|
||||
"INSERT INTO chats
|
||||
@@ -4795,12 +4795,14 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
///
|
||||
/// Optional `label` can be provided to ensure that message is added only once.
|
||||
/// If `important` is true, a notification will be sent.
|
||||
/// `timestamp_sent` is the time shown on the message; it does not affect ordering.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn add_device_msg_with_importance(
|
||||
context: &Context,
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
important: bool,
|
||||
timestamp_sent: i64,
|
||||
) -> Result<MsgId> {
|
||||
ensure!(
|
||||
label.is_some() || msg.is_some(),
|
||||
@@ -4820,11 +4822,10 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let timestamp_sent = time();
|
||||
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
msg.timestamp_sort = timestamp_sent;
|
||||
msg.timestamp_sort = time();
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
|
||||
&& msg.timestamp_sort <= last_msg_time
|
||||
{
|
||||
@@ -4892,7 +4893,7 @@ pub async fn add_device_msg(
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
) -> Result<MsgId> {
|
||||
add_device_msg_with_importance(context, label, msg, false).await
|
||||
add_device_msg_with_importance(context, label, msg, false, time()).await
|
||||
}
|
||||
|
||||
/// Returns true if device message with a given label was ever added to the device chat.
|
||||
@@ -4920,6 +4921,12 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
|
||||
.await?;
|
||||
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
|
||||
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
|
||||
context
|
||||
|
||||
+14
-3
@@ -336,6 +336,17 @@ pub enum Config {
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// ID of the device message added after backup transfer.
|
||||
///
|
||||
/// If the message does not exist on the first housekeeping
|
||||
/// after `READD_BACKUP_TRANSFER_MSG_DELAY`, it is re-added.
|
||||
BackupTransferMsgId,
|
||||
|
||||
/// Timestamp of the device message added after backup transfer.
|
||||
///
|
||||
/// Needed to check against `READD_BACKUP_TRANSFER_MSG_DELAY`.
|
||||
BackupTransferTimestamp,
|
||||
|
||||
/// Timestamp of the last time accumulated broadcast channel reactions were sent
|
||||
LastReactionsBroadcast,
|
||||
|
||||
@@ -809,7 +820,7 @@ impl Context {
|
||||
|
||||
/// Set the given config to a boolean value.
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, from_bool(value)).await?;
|
||||
self.set_config(key, Some(from_bool(value))).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -830,8 +841,8 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
|
||||
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
|
||||
Some(if val { "1" } else { "0" })
|
||||
pub(crate) fn from_bool(val: bool) -> &'static str {
|
||||
if val { "1" } else { "0" }
|
||||
}
|
||||
|
||||
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
|
||||
|
||||
+1
-3
@@ -162,9 +162,7 @@ impl Context {
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
res?;
|
||||
|
||||
param.save_legacy(self).await
|
||||
res
|
||||
}
|
||||
|
||||
/// Adds a new email account as a transport
|
||||
|
||||
@@ -29,7 +29,7 @@ impl ServerParams {
|
||||
if self.username.is_empty() {
|
||||
vec![Self {
|
||||
username: addr.to_string(),
|
||||
..self.clone()
|
||||
..self
|
||||
}]
|
||||
} else {
|
||||
vec![self]
|
||||
|
||||
@@ -294,6 +294,8 @@ async fn test_get_info_completeness() {
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"backup_transfer_msg_id",
|
||||
"backup_transfer_timestamp",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
|
||||
+23
-9
@@ -363,7 +363,7 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
|
||||
/// Selects messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// For each message a row ID, chat id, viewtype and location ID is returned.
|
||||
/// For each message a row ID, chat id, viewtype, whether the message is pinned and location ID is returned.
|
||||
///
|
||||
/// Unknown viewtypes are returned as `Viewtype::Unknown`
|
||||
/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
|
||||
@@ -371,12 +371,12 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
|
||||
async fn select_expired_messages(
|
||||
context: &Context,
|
||||
now: i64,
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype, u32)>> {
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype, bool, u32)>> {
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
SELECT id, chat_id, type, pinned, location_id
|
||||
FROM msgs
|
||||
WHERE
|
||||
ephemeral_timestamp != 0
|
||||
@@ -392,8 +392,9 @@ WHERE
|
||||
.context("Using default viewtype for ephemeral handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let pinned: bool = row.get("pinned")?;
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
Ok((id, chat_id, viewtype, pinned, location_id))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -414,7 +415,7 @@ WHERE
|
||||
.sql
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
SELECT id, chat_id, type, pinned, location_id
|
||||
FROM msgs
|
||||
WHERE
|
||||
timestamp < ?1
|
||||
@@ -437,8 +438,9 @@ WHERE
|
||||
.context("Using default viewtype for delete-old handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let pinned: bool = row.get("pinned")?;
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
Ok((id, chat_id, viewtype, pinned, location_id))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -463,11 +465,15 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
|
||||
if !rows.is_empty() {
|
||||
info!(context, "Attempting to delete {} messages.", rows.len());
|
||||
|
||||
let (msgs_changed, webxdc_deleted) = context
|
||||
let (msgs_changed, webxdc_deleted, pinned_chat_ids) = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut msgs_changed = Vec::with_capacity(rows.len());
|
||||
let mut webxdc_deleted = Vec::new();
|
||||
|
||||
// IDs of the chats in which pinned messages were deleted.
|
||||
let mut pinned_chat_ids = BTreeSet::new();
|
||||
|
||||
// If you change which information is preserved here, also change `MsgId::trash()`
|
||||
// and other places it references.
|
||||
let mut del_msg_stmt = transaction.prepare(
|
||||
@@ -478,7 +484,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
|
||||
)?;
|
||||
let mut del_location_stmt =
|
||||
transaction.prepare("DELETE FROM locations WHERE independent=1 AND id=?")?;
|
||||
for (msg_id, chat_id, viewtype, location_id) in rows {
|
||||
for (msg_id, chat_id, viewtype, is_pinned, location_id) in rows {
|
||||
del_msg_stmt.execute((msg_id, ChatId::TRASH))?;
|
||||
if location_id > 0 {
|
||||
del_location_stmt.execute((location_id,))?;
|
||||
@@ -488,8 +494,12 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
webxdc_deleted.push(msg_id)
|
||||
}
|
||||
|
||||
if is_pinned {
|
||||
pinned_chat_ids.insert(chat_id);
|
||||
}
|
||||
}
|
||||
Ok((msgs_changed, webxdc_deleted))
|
||||
Ok((msgs_changed, webxdc_deleted, pinned_chat_ids))
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -504,6 +514,10 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
}
|
||||
|
||||
for chat_id in pinned_chat_ids {
|
||||
context.emit_event(EventType::PinnedMessagesChanged { chat_id });
|
||||
}
|
||||
|
||||
for msg_id in webxdc_deleted {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
|
||||
}
|
||||
|
||||
@@ -93,6 +93,14 @@ pub enum EventType {
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// The list of pinned messages for the chat has changed.
|
||||
///
|
||||
/// Some message got pinned, or pinned message is unpinned or deleted.
|
||||
PinnedMessagesChanged {
|
||||
/// ID of the chat where the list of pinned messages changed.
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// A reaction to one's own sent message received.
|
||||
/// Typically, the UI will show a notification for that.
|
||||
///
|
||||
|
||||
+2
-2
@@ -1454,7 +1454,7 @@ impl Session {
|
||||
/// or flags have been changed.
|
||||
/// In this case we may want to skip next IDLE and do a round
|
||||
/// of fetching new messages and synchronizing seen flags.
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> bool {
|
||||
use UnsolicitedResponse::*;
|
||||
use async_imap::imap_proto::Response;
|
||||
use async_imap::imap_proto::ResponseCode;
|
||||
@@ -1499,7 +1499,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(should_refetch)
|
||||
should_refetch
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl Session {
|
||||
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
if self.drain_unsolicited_responses(context) {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::tools::{
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub(crate) use transfer::maybe_readd_backup_transfer_msg;
|
||||
pub use transfer::{BackupProvider, get_backup};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
|
||||
+97
-7
@@ -40,21 +40,68 @@ use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::EventType;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::key;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{TempPathGuard, create_id};
|
||||
use crate::tools::{TempPathGuard, create_id, time};
|
||||
|
||||
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
|
||||
|
||||
/// ALPN protocol identifier for the backup transfer protocol.
|
||||
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
|
||||
/// Minimum time after a backup transfer before we check
|
||||
/// whether the "second device added" device message still exists.
|
||||
///
|
||||
/// The check is done in the first housekeeping after this delay;
|
||||
/// if the message was deleted until then, it is re-added with the original timestamp.
|
||||
/// After the check, the user can delete the message as usual without it being re-added.
|
||||
///
|
||||
/// This makes it easier to spot unwanted "add second device" actions.
|
||||
/// It is clear that this does not catch all eventualities, it is best-effort.
|
||||
/// First line of defense is device locking and asking for secret explicitly before adding a second device.
|
||||
const READD_BACKUP_TRANSFER_MSG_DELAY: i64 = 60 * 60;
|
||||
|
||||
/// Checks `Config::BackupTransferMsgId`
|
||||
/// and re-adds the backup transfer device message if it does not exist and some time passed.
|
||||
pub(crate) async fn maybe_readd_backup_transfer_msg(context: &Context) -> Result<()> {
|
||||
let Some(msg_id) = context
|
||||
.get_config_parsed::<u32>(Config::BackupTransferMsgId)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let timestamp = context
|
||||
.get_config_i64(Config::BackupTransferTimestamp)
|
||||
.await?;
|
||||
if time() < timestamp.saturating_add(READD_BACKUP_TRANSFER_MSG_DELAY) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
if Message::load_from_db_optional(context, MsgId::new(msg_id))
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(context));
|
||||
add_device_msg_with_importance(context, None, Some(&mut msg), false, timestamp).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provide or send a backup of this device.
|
||||
///
|
||||
/// This creates a backup of the current device and starts a service which offers another
|
||||
@@ -212,8 +259,23 @@ impl BackupProvider {
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let timestamp = time();
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
let msg_id =
|
||||
add_device_msg_with_importance(&context, None, Some(&mut msg), false, timestamp)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferTimestamp,
|
||||
Some(×tamp.to_string()),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferMsgId,
|
||||
Some(&msg_id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -393,14 +455,17 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::message::Viewtype;
|
||||
use crate::chat::{ChatId, ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{Viewtype, delete_msgs};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_receive() {
|
||||
async fn test_send_receive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Create first device.
|
||||
@@ -472,6 +537,31 @@ mod tests {
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
|
||||
// When deleting the backup transfer message on the sending device,
|
||||
// it is re-added once on housekeeping,
|
||||
// but not within the first hour after the transfer.
|
||||
let device_chat_id = ChatId::get_for_contact(&ctx0, ContactId::DEVICE).await?;
|
||||
let original_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_eq!(original_msg.text, backup_transfer_msg_body(&ctx0));
|
||||
delete_msgs(&ctx0, &[original_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
assert!(get_chat_msgs(&ctx0, device_chat_id).await?.is_empty()); // re-adding not done in first hour
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60 * 60));
|
||||
housekeeping(&ctx0).await?;
|
||||
let readded_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_ne!(readded_msg.id, original_msg.id);
|
||||
assert_eq!(readded_msg.get_text(), original_msg.get_text());
|
||||
assert_eq!(readded_msg.get_timestamp(), original_msg.get_timestamp());
|
||||
assert!(readded_msg.get_sort_timestamp() > original_msg.get_sort_timestamp());
|
||||
|
||||
delete_msgs(&ctx0, &[readded_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
let device_msgs = get_chat_msgs(&ctx0, device_chat_id).await?;
|
||||
assert!(device_msgs.is_empty()); // re-adding is done only once
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that trying to accidentally overwrite a profile
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let mut buf = Vec::new();
|
||||
self.to_writer(&mut buf).unwrap();
|
||||
self.to_writer(&mut buf)
|
||||
.expect("Writing to Vec<u8> cannot fail");
|
||||
buf
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied,
|
||||
clippy::manual_is_variant_and
|
||||
clippy::manual_is_variant_and,
|
||||
clippy::unnecessary_wraps
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
|
||||
@@ -9,14 +9,12 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use num_traits::ToPrimitive as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
pub use crate::net::proxy::ProxyConfig;
|
||||
pub use crate::provider::Socket;
|
||||
use crate::tools::ToOption;
|
||||
|
||||
/// User-entered setting for certificate checks.
|
||||
///
|
||||
@@ -232,62 +230,6 @@ impl EnteredLoginParam {
|
||||
oauth2: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Saves entered account settings,
|
||||
/// so that they can be prefilled if the user wants to configure the server again.
|
||||
///
|
||||
/// This is needed in case a UI is not yet updated, and still uses `get_config("mail_pw")` etc.
|
||||
/// in order to prefill the entered account settings.
|
||||
pub(crate) async fn save_legacy(&self, context: &Context) -> Result<()> {
|
||||
context.set_config(Config::Addr, Some(&self.addr)).await?;
|
||||
|
||||
context
|
||||
.set_config(Config::MailServer, self.imap.server.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailPort, self.imap.port.to_option().as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(
|
||||
Config::MailSecurity,
|
||||
self.imap.security.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailUser, self.imap.user.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailPw, self.imap.password.to_option())
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(Config::SendServer, self.smtp.server.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendPort, self.smtp.port.to_option().as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(
|
||||
Config::SendSecurity,
|
||||
self.smtp.security.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendUser, self.smtp.user.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendPw, self.smtp.password.to_option())
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(
|
||||
Config::ImapCertificateChecks,
|
||||
self.certificate_checks.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EnteredLoginParam {
|
||||
@@ -369,41 +311,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_entered_login_param() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let param = EnteredLoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
imap: EnteredImapLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 0,
|
||||
folder: "".to_string(),
|
||||
security: Socket::Starttls,
|
||||
user: "".to_string(),
|
||||
password: "foobar".to_string(),
|
||||
},
|
||||
smtp: EnteredSmtpLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 2947,
|
||||
security: Socket::default(),
|
||||
user: "".to_string(),
|
||||
password: "".to_string(),
|
||||
},
|
||||
certificate_checks: Default::default(),
|
||||
oauth2: false,
|
||||
};
|
||||
param.save_legacy(&t).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
assert_eq!(t.get_config(Config::MailPw).await?.unwrap(), "foobar");
|
||||
assert_eq!(t.get_config(Config::SendPw).await?, None);
|
||||
assert_eq!(t.get_config_int(Config::SendPort).await?, 2947);
|
||||
|
||||
assert_eq!(EnteredLoginParam::load_legacy(&t).await?, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+28
-9
@@ -580,7 +580,7 @@ impl Message {
|
||||
|
||||
if let Some(msg) = &mut msg {
|
||||
msg.additional_text =
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param)?;
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param);
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
@@ -618,7 +618,7 @@ impl Message {
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
param: &Params,
|
||||
) -> Result<String> {
|
||||
) -> String {
|
||||
if download_state != DownloadState::Done {
|
||||
let file_size = param
|
||||
.get(Param::PostMessageFileBytes)
|
||||
@@ -635,14 +635,14 @@ impl Message {
|
||||
.unwrap_or("?".to_owned());
|
||||
|
||||
return match viewtype {
|
||||
Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
|
||||
Viewtype::File => format!(" [{file_name} – {file_size}]"),
|
||||
_ => {
|
||||
let translated_viewtype = viewtype.to_locale_string(context);
|
||||
Ok(format!(" [{translated_viewtype} – {file_size}]"))
|
||||
format!(" [{translated_viewtype} – {file_size}]")
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(String::new())
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Returns the MIME type of an attached file if it exists.
|
||||
@@ -1641,11 +1641,15 @@ pub(crate) async fn delete_msgs_locally_done(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
modified_chat_ids: BTreeSet<ChatId>,
|
||||
pinned_messages_changed_chat_ids: BTreeSet<ChatId>,
|
||||
) -> Result<()> {
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
|
||||
}
|
||||
for chat_id in pinned_messages_changed_chat_ids {
|
||||
context.emit_event(EventType::PinnedMessagesChanged { chat_id });
|
||||
}
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
@@ -1671,6 +1675,7 @@ pub async fn delete_msgs_ext(
|
||||
delete_for_all: bool,
|
||||
) -> Result<()> {
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
let mut pinned_messages_changed_chat_ids = BTreeSet::new();
|
||||
let mut deleted_rfc724_mid = Vec::new();
|
||||
let mut res = Ok(());
|
||||
|
||||
@@ -1686,6 +1691,9 @@ pub async fn delete_msgs_ext(
|
||||
);
|
||||
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
if msg.is_pinned() {
|
||||
pinned_messages_changed_chat_ids.insert(msg.chat_id);
|
||||
}
|
||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||
|
||||
let update_db = |trans: &mut rusqlite::Transaction| {
|
||||
@@ -1743,7 +1751,13 @@ pub async fn delete_msgs_ext(
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
delete_msg_locally(context, &msg).await?;
|
||||
}
|
||||
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
|
||||
delete_msgs_locally_done(
|
||||
context,
|
||||
msg_ids,
|
||||
modified_chat_ids,
|
||||
pinned_messages_changed_chat_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
@@ -2003,14 +2017,19 @@ pub(crate) async fn set_msg_failed(
|
||||
|
||||
/// Inserts a tombstone into `msgs` table
|
||||
/// to prevent downloading the same message in the future.
|
||||
/// With `on_server`, copies seen on IMAP later are deleted on the server.
|
||||
///
|
||||
/// Returns tombstone database row ID.
|
||||
pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
|
||||
pub(crate) async fn insert_tombstone(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
on_server: bool,
|
||||
) -> Result<MsgId> {
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, ChatId::TRASH),
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
|
||||
(rfc724_mid, ChatId::TRASH, on_server),
|
||||
)
|
||||
.await?;
|
||||
let msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
|
||||
@@ -784,7 +784,7 @@ async fn test_get_existing_msg_ids() -> Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_fail() -> Result<()> {
|
||||
fn test_can_fail() {
|
||||
use MessageState::*;
|
||||
|
||||
// states that are not allowed to transition to OutFailed
|
||||
@@ -799,6 +799,4 @@ fn test_can_fail() -> Result<()> {
|
||||
assert!(OutPending.can_fail());
|
||||
assert!(OutDelivered.can_fail());
|
||||
assert!(OutFailed.can_fail());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+6
-5
@@ -511,11 +511,6 @@ impl MimeFactory {
|
||||
let mut member_fingerprints = Vec::new();
|
||||
let mut member_timestamps = Vec::new();
|
||||
let mut recipient_ids = HashSet::new();
|
||||
let req_mdn = !chat.is_self_talk()
|
||||
&& !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
&& context.should_request_mdns().await?;
|
||||
|
||||
let self_fingerprint = self_fingerprint(context).await?;
|
||||
|
||||
let encryption = if chat.is_self_talk() {
|
||||
@@ -796,6 +791,12 @@ impl MimeFactory {
|
||||
}
|
||||
};
|
||||
|
||||
let req_mdn = encryption.is_encrypted()
|
||||
&& !chat.is_self_talk()
|
||||
&& !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
&& context.should_request_mdns().await?;
|
||||
|
||||
let (in_reply_to, references) = context
|
||||
.sql
|
||||
.query_row(
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::key::{load_self_secret_key, secret_key_to_public_key};
|
||||
use crate::message;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{self, SentMessage};
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
@@ -795,7 +795,7 @@ async fn test_protected_headers_directive() -> Result<()> {
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presence of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", b"content", None)?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
|
||||
@@ -1022,7 +1022,7 @@ END:VCARD";
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_outer_headers() -> Result<()> {
|
||||
async fn test_render_outer_headers_of_encrypted_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
@@ -1030,26 +1030,9 @@ async fn test_render_outer_headers() -> Result<()> {
|
||||
let chat_id = alice.create_chat_id(bob).await;
|
||||
let sent = alice.send_text(chat_id, "Hello!").await;
|
||||
|
||||
let (unencrypted, _encrypted) = sent
|
||||
.payload()
|
||||
.split_once("-----BEGIN PGP MESSAGE-----")
|
||||
.unwrap();
|
||||
let payload = normalized_payload(sent).await;
|
||||
|
||||
// Normalize the parts of the message that vary between runs
|
||||
// (MIME boundary, Date, Message-ID)
|
||||
let boundary = unencrypted
|
||||
.split_once("boundary=\"")
|
||||
.and_then(|(_, rest)| rest.split_once('"'))
|
||||
.map(|(b, _)| b)
|
||||
.unwrap_or_default();
|
||||
let unencrypted = unencrypted.replace(boundary, "BOUNDARY");
|
||||
|
||||
let rfc724_mid = sent.load_from_db().await.rfc724_mid;
|
||||
let unencrypted = unencrypted.replace(&rfc724_mid, "MESSAGE_ID@localhost");
|
||||
|
||||
let unencrypted = regex!(r"Date:[^\r\n]*")
|
||||
.replace(&unencrypted, "Date: DATE")
|
||||
.to_string();
|
||||
let (unencrypted, _encrypted) = payload.split_once("-----BEGIN PGP MESSAGE-----").unwrap();
|
||||
|
||||
let expected = r#"From: <alice@example.org>
|
||||
Date: DATE
|
||||
@@ -1085,3 +1068,138 @@ expected (debug print): {expected:?}"
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_unencrypted_msg_basic() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
let sent = alice.send_text(chat.id, "Hello!").await;
|
||||
let unencrypted = normalized_payload(sent).await;
|
||||
|
||||
let expected = r#"From: <alice@example.org>
|
||||
Message-ID: <MESSAGE_ID@localhost>
|
||||
MIME-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
|
||||
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
|
||||
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
|
||||
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
|
||||
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
|
||||
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
|
||||
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
|
||||
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
|
||||
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
|
||||
3jsMAg==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Date: DATE
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello!"#
|
||||
.replace("\n", "\r\n");
|
||||
assert_eq!(
|
||||
unencrypted, expected,
|
||||
"---------------- Actual: ----------------
|
||||
{unencrypted}
|
||||
-----------------------------------------
|
||||
actual (debug print): {unencrypted:?}
|
||||
expected (debug print): {expected:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_unencrypted_msg_with_attachment() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text("Hello!".to_string());
|
||||
msg.set_file_from_bytes(alice, "foo.bar", b"content", None)?;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let unencrypted = normalized_payload(sent).await;
|
||||
|
||||
let expected = r#"From: <alice@example.org>
|
||||
Message-ID: <MESSAGE_ID@localhost>
|
||||
MIME-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
|
||||
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
|
||||
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
|
||||
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
|
||||
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
|
||||
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
|
||||
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
|
||||
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
|
||||
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
|
||||
3jsMAg==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="BOUNDARY"
|
||||
Date: DATE
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
|
||||
|
||||
--BOUNDARY
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello!
|
||||
--BOUNDARY
|
||||
Content-Type: application/octet-stream
|
||||
Content-Disposition: attachment; filename="foo.bar"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
Y29udGVudA==
|
||||
|
||||
--BOUNDARY--
|
||||
"#
|
||||
.replace("\n", "\r\n");
|
||||
assert_eq!(
|
||||
unencrypted, expected,
|
||||
"---------------- Actual: ----------------
|
||||
{unencrypted}
|
||||
-----------------------------------------
|
||||
actual (debug print): {unencrypted:?}
|
||||
expected (debug print): {expected:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize the parts of the message that vary between runs
|
||||
/// (MIME boundary, Date, Message-ID)
|
||||
async fn normalized_payload(sent: SentMessage<'_>) -> String {
|
||||
let rfc724_mid = sent.load_from_db().await.rfc724_mid;
|
||||
|
||||
let mut payload = sent.payload;
|
||||
|
||||
if let Some(boundary) = payload
|
||||
.split_once("boundary=\"")
|
||||
.and_then(|(_, rest)| rest.split_once('"'))
|
||||
.map(|(b, _)| b)
|
||||
{
|
||||
payload = payload.replace(boundary, "BOUNDARY");
|
||||
}
|
||||
|
||||
payload = payload.replace(&rfc724_mid, "MESSAGE_ID@localhost");
|
||||
|
||||
payload = regex!(r"Date:[^\r\n]*")
|
||||
.replace(&payload, "Date: DATE")
|
||||
.to_string();
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
+7
-10
@@ -294,11 +294,7 @@ impl MimeMessage {
|
||||
&mut wants_mdn,
|
||||
&mail,
|
||||
);
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| is_hidden(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
);
|
||||
headers_removed.extend(headers.extract_if(|k, _v| is_hidden(k)).map(|(k, _v)| k));
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -935,9 +931,10 @@ impl MimeMessage {
|
||||
// See if an MDN is requested from the other side
|
||||
if self.decryption_error.is_none()
|
||||
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
|
||||
&& self.wants_mdn
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
&& self.wants_mdn
|
||||
&& self.was_encrypted() // Do not send MDNs for unencrypted messages.
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
{
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
@@ -951,7 +948,7 @@ impl MimeMessage {
|
||||
typ: Viewtype::Text,
|
||||
..Default::default()
|
||||
};
|
||||
if self.wants_mdn && self.incoming {
|
||||
if self.wants_mdn && self.was_encrypted() && self.incoming {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
if let Some(ref subject) = self.get_subject()
|
||||
@@ -1751,7 +1748,7 @@ impl MimeMessage {
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| has_header_protection || is_protected(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
.map(|(k, _v)| k),
|
||||
);
|
||||
|
||||
if has_header_protection {
|
||||
|
||||
@@ -46,7 +46,7 @@ use crate::mimeparser::SystemMessage;
|
||||
|
||||
/// The length of an ed25519 `PublicKey`, in bytes.
|
||||
const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
const PUBLIC_KEY_STUB: &[u8] = b"static_string";
|
||||
|
||||
/// Store Iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -86,14 +86,14 @@ async fn test_can_communicate() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -107,14 +107,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -149,14 +149,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice 2".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice 2".as_bytes() {
|
||||
if data == b"bob -> alice 2" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -314,14 +314,14 @@ async fn test_can_reconnect() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -373,14 +373,14 @@ async fn test_can_reconnect() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
|
||||
+1
-1
@@ -401,7 +401,7 @@ pub fn merge_openpgp_certificates(
|
||||
// such as Alice's key in `test-data/key/alice-secret.asc`.
|
||||
let best_user: Option<SignedUser> = old_users
|
||||
.into_iter()
|
||||
.chain(new_users.clone())
|
||||
.chain(new_users)
|
||||
.filter_map(|SignedUser { id, signatures }| {
|
||||
// Select the best signature for each User ID.
|
||||
// If User ID has no valid signatures, it is filtered out.
|
||||
|
||||
@@ -384,7 +384,7 @@ fn test_merge_openpgp_certificates() {
|
||||
|
||||
// Cannot merge certificates with different primary key.
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob, alice).is_err());
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
|
||||
+6
-223
@@ -13,6 +13,7 @@ use anyhow::{Result, ensure};
|
||||
use crate::chat::{ChatId, send_msg};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -78,7 +79,11 @@ async fn update_pinned_state_in_db(
|
||||
(new_pinned_state, msg.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
context.emit_event(EventType::PinnedMessagesChanged {
|
||||
chat_id: msg.chat_id,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -140,226 +145,4 @@ pub(crate) async fn handle_pinned_state_from_wire(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{ChatItem, add_info_msg, create_broadcast, get_chat_msgs};
|
||||
use crate::config::Config;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{TestContextManager, sync};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pinned_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await; // Alice's second device
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice creates all chat types upfront, with Bob as member if possible
|
||||
let single_chat_id = alice.create_chat(bob).await.id;
|
||||
let group_chat_id = alice.create_group_with_members("Group", &[bob]).await;
|
||||
let broadcast_chat_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(broadcast_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let self_chat_id = alice.get_self_chat().await.id;
|
||||
sync(alice, alice2).await;
|
||||
|
||||
for alice_chat_id in [
|
||||
single_chat_id,
|
||||
group_chat_id,
|
||||
broadcast_chat_id,
|
||||
self_chat_id,
|
||||
] {
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Alice sends message "Foo" and pins it
|
||||
let sent1 = alice.send_text(alice_chat_id, "Foo").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(!msg1.is_pinned());
|
||||
|
||||
set_pinned_state(alice, msg1.id, true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert!(sent1.load_from_db().await.is_pinned());
|
||||
|
||||
let info_msg = sent2.load_from_db().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(alice).await?.is_none()); // contact not needed, tapping shall jump to message
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
// Pinning an info message does not work
|
||||
assert!(set_pinned_state(alice, info_msg.id, true).await.is_err());
|
||||
|
||||
// Unpin the initially pinned message.
|
||||
// Before, send another message "Bar". To test, no visible info message is added this time,
|
||||
let sent3 = alice.send_text(alice_chat_id, "Bar").await;
|
||||
|
||||
set_pinned_state(alice, msg1.id, false).await?;
|
||||
let sent4 = alice.pop_sent_msg().await;
|
||||
assert!(!sent1.load_from_db().await.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let msg3 = sent3.load_from_db().await;
|
||||
assert!(!msg3.is_info());
|
||||
assert!(!msg3.is_pinned());
|
||||
assert_eq!(alice.get_last_msg_id_in(msg3.chat_id).await, msg3.id); // last message is still "Bar", not an info message
|
||||
|
||||
if alice_chat_id != self_chat_id {
|
||||
// Bob receives message "Foo"
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
assert!(!msg1.is_pinned());
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Bob receives info message to pin "Foo"
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert!(Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
let info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(bob).await?.is_none());
|
||||
|
||||
// Bob receives message "Bar" and hidden message to unpin message "Foo"
|
||||
bob.recv_msg(&sent3).await;
|
||||
bob.recv_msg_trash(&sent4).await;
|
||||
assert!(!Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
// Alice's second device receives all four messages and ends up in the same state
|
||||
let msg1 = alice2.recv_msg(&sent1).await;
|
||||
alice2.recv_msg(&sent2).await;
|
||||
assert!(Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
alice2.recv_msg(&sent3).await;
|
||||
alice2.recv_msg_trash(&sent4).await;
|
||||
assert!(!Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(alice2, alice2.get_last_msg_id_in(msg1.chat_id).await)
|
||||
.await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_pinned_messages_order() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
let boilerplate_msg_count = get_chat_msgs(alice, chat_id).await?.len();
|
||||
|
||||
// create three messages, sent1 and sent2 have different timestamp, sent2 and sent3 may differ by ID only
|
||||
let sent1 = alice.send_text(chat_id, "1").await;
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
let sent2 = alice.send_text(chat_id, "2").await;
|
||||
let sent3 = alice.send_text(chat_id, "3").await;
|
||||
|
||||
// get_chat_msgs() start with the oldest message
|
||||
let chat_msgs = get_chat_msgs(alice, chat_id).await?;
|
||||
let msg_ids: Vec<_> = chat_msgs
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
ChatItem::Message { msg_id } => Some(msg_id),
|
||||
ChatItem::DayMarker { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
&msg_ids[boilerplate_msg_count..],
|
||||
&[
|
||||
sent1.sender_msg_id,
|
||||
sent2.sender_msg_id,
|
||||
sent3.sender_msg_id
|
||||
]
|
||||
);
|
||||
|
||||
// get_pinned_messages() has the same order, also starting with the oldest message
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
// order of pinning does not affect the order of pinned messages.
|
||||
// this is to keep scrolling direction of chat bubbles and pinned banner scrollbar in sync,
|
||||
// and not jumping wildly around.
|
||||
// this is also what most other messengers are doing.
|
||||
set_pinned_state(alice, sent1.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, false).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 0);
|
||||
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_pinned_state_from_wire() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
|
||||
let sent1 = alice.send_text(chat_id, "pinnable").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(is_pinnable(&msg1));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg1, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// For not-pinnable messages, handle_pinned_state_from_wire() logs a warning and returns "ok".
|
||||
// otherwise if there is an incompatibility in which messages are treated as "pinnable",
|
||||
// this error will bubble up and user will get a device message saying "please report a bug".
|
||||
let msg2_id = add_info_msg(alice, chat_id, "not pinnable").await?;
|
||||
let msg2 = Message::load_from_db(alice, msg2_id).await?;
|
||||
assert!(!is_pinnable(&msg2));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg2, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
alice.assert_warn("Message is not pinnable").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod pinned_messages_tests;
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
use super::*;
|
||||
use crate::chat::{ChatItem, add_info_msg, create_broadcast, get_chat_msgs};
|
||||
use crate::config::Config;
|
||||
use crate::ephemeral;
|
||||
use crate::message;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{TestContext, TestContextManager, sync};
|
||||
use crate::tools::{SystemTime, time};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Waits for a PinnedMessagesChanged for a given `chat_id`.
|
||||
///
|
||||
/// Panics if event arrives for the wrong `chat_id`.
|
||||
async fn expect_pinned_message_event(context: &TestContext, chat_id: ChatId) {
|
||||
let EventType::PinnedMessagesChanged {
|
||||
chat_id: event_chat_id,
|
||||
} = context
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::PinnedMessagesChanged { .. }))
|
||||
.await
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
assert_eq!(event_chat_id, chat_id);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pinned_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await; // Alice's second device
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice creates all chat types upfront, with Bob as member if possible
|
||||
let single_chat_id = alice.create_chat(bob).await.id;
|
||||
let group_chat_id = alice.create_group_with_members("Group", &[bob]).await;
|
||||
let broadcast_chat_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(broadcast_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let self_chat_id = alice.get_self_chat().await.id;
|
||||
sync(alice, alice2).await;
|
||||
|
||||
for alice_chat_id in [
|
||||
single_chat_id,
|
||||
group_chat_id,
|
||||
broadcast_chat_id,
|
||||
self_chat_id,
|
||||
] {
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Alice sends message "Foo" and pins it
|
||||
let sent1 = alice.send_text(alice_chat_id, "Foo").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(!msg1.is_pinned());
|
||||
|
||||
set_pinned_state(alice, msg1.id, true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
expect_pinned_message_event(alice, msg1.chat_id).await;
|
||||
assert!(sent1.load_from_db().await.is_pinned());
|
||||
|
||||
let info_msg = sent2.load_from_db().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(alice).await?.is_none()); // contact not needed, tapping shall jump to message
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
// Pinning an info message does not work
|
||||
assert!(set_pinned_state(alice, info_msg.id, true).await.is_err());
|
||||
|
||||
// Unpin the initially pinned message.
|
||||
// Before, send another message "Bar". To test, no visible info message is added this time,
|
||||
let sent3 = alice.send_text(alice_chat_id, "Bar").await;
|
||||
|
||||
set_pinned_state(alice, msg1.id, false).await?;
|
||||
let sent4 = alice.pop_sent_msg().await;
|
||||
assert!(!sent1.load_from_db().await.is_pinned());
|
||||
expect_pinned_message_event(alice, msg1.chat_id).await;
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let msg3 = sent3.load_from_db().await;
|
||||
assert!(!msg3.is_info());
|
||||
assert!(!msg3.is_pinned());
|
||||
assert_eq!(alice.get_last_msg_id_in(msg3.chat_id).await, msg3.id); // last message is still "Bar", not an info message
|
||||
|
||||
if alice_chat_id != self_chat_id {
|
||||
// Bob receives message "Foo"
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
assert!(!msg1.is_pinned());
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Bob receives info message to pin "Foo"
|
||||
bob.recv_msg(&sent2).await;
|
||||
expect_pinned_message_event(bob, msg1.chat_id).await;
|
||||
assert!(Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
let info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(bob).await?.is_none());
|
||||
|
||||
// Bob receives message "Bar" and hidden message to unpin message "Foo"
|
||||
bob.recv_msg(&sent3).await;
|
||||
bob.recv_msg_trash(&sent4).await;
|
||||
expect_pinned_message_event(bob, msg1.chat_id).await;
|
||||
assert!(!Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
// Alice's second device receives all four messages and ends up in the same state
|
||||
let msg1 = alice2.recv_msg(&sent1).await;
|
||||
alice2.recv_msg(&sent2).await;
|
||||
expect_pinned_message_event(alice2, msg1.chat_id).await;
|
||||
assert!(Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
alice2.recv_msg(&sent3).await;
|
||||
alice2.recv_msg_trash(&sent4).await;
|
||||
expect_pinned_message_event(alice2, msg1.chat_id).await;
|
||||
assert!(!Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(alice2, alice2.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_pinned_messages_order() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
let boilerplate_msg_count = get_chat_msgs(alice, chat_id).await?.len();
|
||||
|
||||
// create three messages, sent1 and sent2 have different timestamp, sent2 and sent3 may differ by ID only
|
||||
let sent1 = alice.send_text(chat_id, "1").await;
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
let sent2 = alice.send_text(chat_id, "2").await;
|
||||
let sent3 = alice.send_text(chat_id, "3").await;
|
||||
|
||||
// get_chat_msgs() start with the oldest message
|
||||
let chat_msgs = get_chat_msgs(alice, chat_id).await?;
|
||||
let msg_ids: Vec<_> = chat_msgs
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
ChatItem::Message { msg_id } => Some(msg_id),
|
||||
ChatItem::DayMarker { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
&msg_ids[boilerplate_msg_count..],
|
||||
&[
|
||||
sent1.sender_msg_id,
|
||||
sent2.sender_msg_id,
|
||||
sent3.sender_msg_id
|
||||
]
|
||||
);
|
||||
|
||||
// get_pinned_messages() has the same order, also starting with the oldest message
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
// order of pinning does not affect the order of pinned messages.
|
||||
// this is to keep scrolling direction of chat bubbles and pinned banner scrollbar in sync,
|
||||
// and not jumping wildly around.
|
||||
// this is also what most other messengers are doing.
|
||||
set_pinned_state(alice, sent1.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, false).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 0);
|
||||
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_pinned_state_from_wire() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
|
||||
let sent1 = alice.send_text(chat_id, "pinnable").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(is_pinnable(&msg1));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg1, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// For not-pinnable messages, handle_pinned_state_from_wire() logs a warning and returns "ok".
|
||||
// otherwise if there is an incompatibility in which messages are treated as "pinnable",
|
||||
// this error will bubble up and user will get a device message saying "please report a bug".
|
||||
let msg2_id = add_info_msg(alice, chat_id, "not pinnable").await?;
|
||||
let msg2 = Message::load_from_db(alice, msg2_id).await?;
|
||||
assert!(!is_pinnable(&msg2));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg2, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
alice.assert_warn("Message is not pinnable").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that disappearing pinned message expires and emits `PinnedMessagesChanged` event.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_pinned_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = alice.create_chat_id(bob).await;
|
||||
|
||||
// Alice sends ephemeral timer in single chat with Bob.
|
||||
alice_chat_id
|
||||
.set_ephemeral_timer(alice, ephemeral::Timer::from_u32(60))
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Alice sends "Hello!" message to Bob.
|
||||
let bob_msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
|
||||
// Bob reads the message, so the timer starts.
|
||||
message::markseen_msgs(bob, vec![bob_msg.id]).await?;
|
||||
|
||||
// Bob pins "Hello!" message received from Alice.
|
||||
set_pinned_state(bob, bob_msg.id, true).await?;
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
|
||||
// Wait until the message expires.
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
ephemeral::delete_expired_messages(bob, time()).await?;
|
||||
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `PinnedMessagesChanged` event is emitted when pinned message is deleted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_pinned_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let bob_msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
|
||||
// Bob pins "Hello!" message received from Alice.
|
||||
set_pinned_state(bob, bob_msg.id, true).await?;
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
|
||||
message::delete_msgs(bob, &[bob_msg.id]).await?;
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+5
-9
@@ -1,6 +1,6 @@
|
||||
//! Handle plain text together with some attributes.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use regex::regex;
|
||||
|
||||
use crate::simplify::remove_message_footer;
|
||||
|
||||
@@ -25,12 +25,8 @@ impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
let linkify_mail_re = regex!(r"\b([\w.\-+]+@[\w.\-]+)\b");
|
||||
let linkify_url_re = regex!(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)");
|
||||
|
||||
let lines: Vec<&str> = self.text.lines().collect();
|
||||
let (lines, _footer) = remove_message_footer(&lines);
|
||||
@@ -52,12 +48,12 @@ impl PlainText {
|
||||
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
|
||||
let line = line.to_string().replace('\r', "");
|
||||
|
||||
let mut line = LINKIFY_MAIL_RE
|
||||
let mut line = linkify_mail_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
line = LINKIFY_URL_RE
|
||||
line = linkify_url_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
mod dclogin_scheme;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
pub(crate) use dclogin_scheme::login_param_from_login_qr;
|
||||
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
|
||||
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
||||
use regex::regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::autorelay::login_param_from_host;
|
||||
@@ -764,7 +764,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
|
||||
let addr = server_config.addr();
|
||||
let host = addr.host().to_string();
|
||||
let host = addr.host();
|
||||
let port = addr.port();
|
||||
Ok(Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
@@ -1071,16 +1071,11 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
}
|
||||
|
||||
static VCARD_NAME_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
|
||||
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
|
||||
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let name = VCARD_NAME_RE
|
||||
let name = regex!(r"(?m)^N:([^;]*);([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
@@ -1090,7 +1085,10 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
|
||||
let addr = if let Some(cap) = regex!(r"(?m)^EMAIL([^:\n]*):([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| caps.get(2))
|
||||
{
|
||||
normalize_address(cap.as_str().trim())?
|
||||
} else {
|
||||
bail!("Bad e-mail address");
|
||||
@@ -1124,7 +1122,7 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
|
||||
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
|
||||
|
||||
Ok(new_addr.to_string())
|
||||
Ok(new_addr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -120,7 +120,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
};
|
||||
|
||||
Ok(Qr::Login {
|
||||
address: addr.to_owned(),
|
||||
address: addr,
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ async fn set_msg_id_reaction(
|
||||
.await?;
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)
|
||||
{
|
||||
chat.param
|
||||
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
|
||||
|
||||
+18
-7
@@ -483,7 +483,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
let trash = || async {
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid, false).await?];
|
||||
Ok(Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::Undefined,
|
||||
@@ -668,14 +668,15 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
match res {
|
||||
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
|
||||
let msg_id = insert_tombstone(context, rfc724_mid).await?;
|
||||
let needs_delete_job = res == securejoin::HandshakeMessage::Done;
|
||||
let msg_id = insert_tombstone(context, rfc724_mid, needs_delete_job).await?;
|
||||
received_msg = Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::InSeen,
|
||||
hidden: false,
|
||||
sort_timestamp: mime_parser.timestamp_sent,
|
||||
msg_ids: vec![msg_id],
|
||||
needs_delete_job: res == securejoin::HandshakeMessage::Done,
|
||||
needs_delete_job,
|
||||
});
|
||||
}
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
@@ -2311,7 +2312,7 @@ INSERT INTO msgs
|
||||
// This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)
|
||||
{
|
||||
// write the last subject even if empty -
|
||||
// otherwise a reply may get an outdated subject.
|
||||
@@ -2392,6 +2393,7 @@ async fn handle_edit_delete(
|
||||
}
|
||||
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
let mut pinned_messages_changed_chat_ids = BTreeSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
@@ -2400,7 +2402,7 @@ async fn handle_edit_delete(
|
||||
let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
|
||||
insert_tombstone(context, rfc724_mid).await?;
|
||||
insert_tombstone(context, rfc724_mid, false).await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -2416,8 +2418,17 @@ async fn handle_edit_delete(
|
||||
message::delete_msg_locally(context, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
if msg.is_pinned() {
|
||||
pinned_messages_changed_chat_ids.insert(msg.chat_id);
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
message::delete_msgs_locally_done(
|
||||
context,
|
||||
&msg_ids,
|
||||
modified_chat_ids,
|
||||
pinned_messages_changed_chat_ids,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3404,7 +3415,7 @@ async fn apply_chat_name_avatar_and_description_changes(
|
||||
&& is_from_in_chat
|
||||
&& chat
|
||||
.param
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)
|
||||
{
|
||||
info!(context, "Group-avatar change for {}.", chat.id);
|
||||
match avatar_action {
|
||||
|
||||
@@ -416,7 +416,9 @@ async fn test_escaped_from() {
|
||||
);
|
||||
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.text, "hello");
|
||||
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||
|
||||
// MDN request from unencrypted message is ignored.
|
||||
assert!(msg.param.get_int(Param::WantsMdn).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+14
-15
@@ -33,14 +33,14 @@ pub(crate) use qrinvite::QrInvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_').remove(b'@');
|
||||
|
||||
fn inviter_progress(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
// No other values are used.
|
||||
let progress = 1000;
|
||||
context.emit_event(EventType::SecurejoinInviterProgress {
|
||||
@@ -49,8 +49,6 @@ fn inviter_progress(
|
||||
chat_type,
|
||||
progress,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shorten name to max. `length` characters.
|
||||
@@ -124,18 +122,19 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
|
||||
|
||||
let r_param = context
|
||||
let encoded_extra_relays: Vec<String> = context
|
||||
.get_self_addrs()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|addr| *addr != self_addr)
|
||||
.reduce(|acc, addr| {
|
||||
format!(
|
||||
"{acc},{}",
|
||||
utf8_percent_encode(&addr, DISALLOWED_CHARACTERS)
|
||||
)
|
||||
})
|
||||
.map_or(String::default(), |addrs| format!("&r={addrs}"));
|
||||
.map(|addr| utf8_percent_encode(&addr, DISALLOWED_CHARACTERS).to_string())
|
||||
.collect();
|
||||
|
||||
let r_param = if encoded_extra_relays.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("&r={}", encoded_extra_relays.join(","))
|
||||
};
|
||||
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
@@ -659,7 +658,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ);
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
@@ -670,7 +669,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single);
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
}
|
||||
@@ -817,7 +816,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
// and tests which don't care about the chat ID,
|
||||
// so we pass invalid chat ID here.
|
||||
let chat_id = ChatId::new(0);
|
||||
inviter_progress(context, contact_id, chat_id, chat_type)?;
|
||||
inviter_progress(context, contact_id, chat_id, chat_type);
|
||||
}
|
||||
|
||||
if matches!(step, SecureJoinStep::MemberAdded) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::message::rfc724_mid_exists_ext;
|
||||
use crate::qr::Qr;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, messages_e2ee_info_msg};
|
||||
@@ -13,6 +14,7 @@ use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, get_chat_msg, sync,
|
||||
};
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SetupContactCase {
|
||||
@@ -1142,6 +1144,43 @@ async fn test_get_securejoin_qr_name_is_last() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that addresses in QR codes are percent-encoded.
|
||||
/// `@` should not be encoded unnecessarily,
|
||||
/// since this would just make the QR code longer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_securejoin_qr_encoding() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// `@` in email addresses must not be percent-encoded:
|
||||
add_pseudo_transport(alice, "asdf@example.org").await?;
|
||||
// But `%` does need percent-encoding:
|
||||
add_pseudo_transport(alice, "jk%l@example.net").await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
assert!(
|
||||
qr.contains("a=alice@example.org"),
|
||||
"{qr} doesn't contain 'a=alice@example.org'"
|
||||
);
|
||||
assert!(
|
||||
qr.contains("r=jk%25l@example.net,asdf@example.org"),
|
||||
"{qr} doesn't contain 'r=jk%25l@example.net,asdf@example.org'"
|
||||
);
|
||||
|
||||
let qr = check_qr(bob, &qr).await?;
|
||||
let Qr::AskVerifyContact { mut addrs, .. } = qr else {
|
||||
unreachable!()
|
||||
};
|
||||
addrs.sort();
|
||||
assert_eq!(
|
||||
addrs,
|
||||
vec!["alice@example.org", "asdf@example.org", "jk%l@example.net",]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// QR codes should not get arbitrary big because of long names.
|
||||
/// The truncation, however, should not let the url end with a `.`, which is a call for trouble in linkfiers.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1525,3 +1564,29 @@ async fn test_deduplicate_member_added() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a handled join request is also marked as "deleted"
|
||||
/// in the database, so that if a copy of the request arrives via
|
||||
/// other relays in the future, then this copy will also be deleted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_join_request_deleted_on_all_relays() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = chat::create_group(alice, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
bob.add_or_lookup_contact_id(alice).await;
|
||||
join_securejoin(bob, &qr).await?;
|
||||
let request = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&request).await;
|
||||
|
||||
let rfc724_mid = Message::load_from_db(bob, request.sender_msg_id)
|
||||
.await?
|
||||
.rfc724_mid;
|
||||
let (_, deleted) = rfc724_mid_exists_ext(alice, &rfc724_mid, "deleted=1")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(deleted);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
-2
@@ -356,7 +356,7 @@ pub(crate) async fn insert_into_smtp(
|
||||
queued_msg: &QueuedMail,
|
||||
) -> Result<()> {
|
||||
let now = tools::time();
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid).await?;
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid, false).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
|
||||
@@ -720,7 +720,7 @@ async fn send_mdn_rfc724_mid(
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid).await?;
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid, false).await?;
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
|
||||
+7
-1
@@ -14,7 +14,7 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::imex::{self, BLOBS_BACKUP_NAME};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
@@ -827,6 +827,12 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
imex::maybe_readd_backup_transfer_msg(context)
|
||||
.await
|
||||
.context("Failed to re-add backup transfer message")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
if let Err(err) = incremental_vacuum(context).await {
|
||||
warn!(context, "Failed to run incremental vacuum: {err:#}.");
|
||||
}
|
||||
|
||||
+4
-12
@@ -32,8 +32,6 @@ fn migrate_key_contacts(
|
||||
context: &Context,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
) -> std::result::Result<(), anyhow::Error> {
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 1: ===============================
|
||||
// Alter tables
|
||||
transaction.execute_batch(
|
||||
@@ -79,13 +77,12 @@ fn migrate_key_contacts(
|
||||
.optional()
|
||||
.context("Step 0")?
|
||||
else {
|
||||
info!(
|
||||
context,
|
||||
"Not yet configured, no need to migrate key-contacts"
|
||||
);
|
||||
// Not yet configured, no need to migrate key-contacts.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 2: ===============================
|
||||
// Create up to 3 new contacts for every contact that has a peerstate:
|
||||
// one from the Autocrypt key fingerprint, one from the verified key fingerprint,
|
||||
@@ -1936,14 +1933,9 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
|
||||
inc_and_check(&mut migration_version, 132)?;
|
||||
if dbversion < migration_version {
|
||||
let start = Time::now();
|
||||
sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"key-contacts migration took {:?} in total.",
|
||||
time_elapsed(&start),
|
||||
);
|
||||
|
||||
// Schedule `msgs_to_key_contacts()`.
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
|
||||
+11
-1
@@ -384,6 +384,7 @@ impl Context {
|
||||
|
||||
async fn sync_message_deletion(&self, msgs: &[String]) -> Result<()> {
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
let mut pinned_messages_changed_chat_ids = BTreeSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
for rfc724_mid in msgs {
|
||||
if let Some(msg_id) = message::rfc724_mid_exists(self, rfc724_mid).await? {
|
||||
@@ -391,6 +392,9 @@ impl Context {
|
||||
message::delete_msg_locally(self, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
if msg.is_pinned() {
|
||||
pinned_messages_changed_chat_ids.insert(msg.chat_id);
|
||||
}
|
||||
} else {
|
||||
warn!(self, "Sync message delete: Database entry does not exist.");
|
||||
}
|
||||
@@ -398,7 +402,13 @@ impl Context {
|
||||
warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;
|
||||
message::delete_msgs_locally_done(
|
||||
self,
|
||||
&msg_ids,
|
||||
modified_chat_ids,
|
||||
pinned_messages_changed_chat_ids,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-26
@@ -220,6 +220,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
|
||||
),
|
||||
Some(&mut msg),
|
||||
true,
|
||||
time(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
@@ -520,32 +521,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToOption<T> {
|
||||
fn to_option(self) -> Option<T>;
|
||||
}
|
||||
impl<'a> ToOption<&'a str> for &'a String {
|
||||
fn to_option(self) -> Option<&'a str> {
|
||||
if self.is_empty() { None } else { Some(self) }
|
||||
}
|
||||
}
|
||||
impl ToOption<String> for u16 {
|
||||
fn to_option(self) -> Option<String> {
|
||||
if self == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ToOption<String> for Option<i32> {
|
||||
fn to_option(self) -> Option<String> {
|
||||
match self {
|
||||
None | Some(0) => None,
|
||||
Some(v) => Some(v.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) fn remove_subject_prefix(last_subject: &str) -> String {
|
||||
let subject_start = if last_subject.starts_with("Chat:") {
|
||||
|
||||
+13
-13
@@ -27,7 +27,7 @@ impl Context {
|
||||
Ok(param.parse().unwrap_or_default())
|
||||
},
|
||||
)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp);
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
@@ -57,7 +57,7 @@ impl ChatId {
|
||||
let param: String = row.get(0)?;
|
||||
Ok(param.parse().unwrap_or_default())
|
||||
})?;
|
||||
let update = param.update_timestamp(scope, new_timestamp)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp);
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE chats SET param=? WHERE id=?",
|
||||
@@ -73,13 +73,13 @@ impl ChatId {
|
||||
impl Params {
|
||||
/// Updates a param's timestamp in memory, if reasonable.
|
||||
/// Returns true if the caller shall update the settings belonging to the scope.
|
||||
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> Result<bool> {
|
||||
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> bool {
|
||||
let old_timestamp = self.get_i64(scope).unwrap_or_default();
|
||||
if new_timestamp >= old_timestamp {
|
||||
self.set_i64(scope, new_timestamp);
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
Ok(false)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,18 +96,18 @@ mod tests {
|
||||
let mut params = Params::new();
|
||||
let ts = time();
|
||||
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?);
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?); // same timestamp -> update
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts + 10)?);
|
||||
assert!(!params.update_timestamp(Param::LastSubject, ts)?); // `ts` is now too old
|
||||
assert!(!params.update_timestamp(Param::LastSubject, 0)?);
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts));
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)); // same timestamp -> update
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts + 10));
|
||||
assert!(!params.update_timestamp(Param::LastSubject, ts)); // `ts` is now too old
|
||||
assert!(!params.update_timestamp(Param::LastSubject, 0));
|
||||
assert_eq!(params.get_i64(Param::LastSubject).unwrap(), ts + 10);
|
||||
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // stay unset -> update ...
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // ... also on multiple calls
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // stay unset -> update ...
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // ... also on multiple calls
|
||||
assert_eq!(params.get_i64(Param::GroupNameTimestamp).unwrap(), 0);
|
||||
|
||||
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1)?);
|
||||
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1));
|
||||
assert_eq!(params.get_i64(Param::AvatarTimestamp), None);
|
||||
|
||||
Ok(())
|
||||
|
||||
+3
-3
@@ -350,7 +350,7 @@ impl Context {
|
||||
if let Some(ref document) = status_update_item.document
|
||||
&& instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
|
||||
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)
|
||||
{
|
||||
instance.param.set(Param::WebxdcDocument, document);
|
||||
param_changed = true;
|
||||
@@ -359,10 +359,10 @@ impl Context {
|
||||
if let Some(ref summary) = status_update_item.summary
|
||||
&& instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)
|
||||
{
|
||||
let summary = sanitize_bidi_characters(summary);
|
||||
instance.param.set(Param::WebxdcSummary, summary.clone());
|
||||
instance.param.set(Param::WebxdcSummary, summary);
|
||||
param_changed = true;
|
||||
}
|
||||
|
||||
|
||||
+12
-14
@@ -1201,30 +1201,28 @@ async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"key = syntax error"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"no_name = "no name, no icon""#)?;
|
||||
assert_eq!(manifest.name, None);
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"name = "name, no icon""#)?;
|
||||
assert_eq!(manifest.name, Some("name, no icon".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
r#"name = "foo"
|
||||
icon = "bar""#
|
||||
.as_bytes(),
|
||||
br#"name = "foo"
|
||||
icon = "bar""#,
|
||||
)?;
|
||||
assert_eq!(manifest.name, Some("foo".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
r#"name = "foz"
|
||||
br#"name = "foz"
|
||||
icon = "baz"
|
||||
add_item = "that should be just ignored"
|
||||
|
||||
[section]
|
||||
sth_for_the = "future""#
|
||||
.as_bytes(),
|
||||
sth_for_the = "future""#,
|
||||
)?;
|
||||
assert_eq!(manifest.name, Some("foz".to_string()));
|
||||
Ok(())
|
||||
@@ -1232,13 +1230,13 @@ sth_for_the = "future""#
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
||||
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"min_api = 3"#)?;
|
||||
assert_eq!(manifest.min_api, Some(3));
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"min_api = "1""#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"min_api = 1.2"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -1246,10 +1244,10 @@ async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"source_code_url = 3"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"source_code_url = "https://foo.bar""#)?;
|
||||
assert_eq!(
|
||||
manifest.source_code_url,
|
||||
Some("https://foo.bar".to_string())
|
||||
|
||||
Reference in New Issue
Block a user