mirror of
https://github.com/chatmail/core.git
synced 2026-10-17 10:23:01 +03:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20bb3235dc | ||
|
|
84161f4202 | ||
|
|
4af9463a91 | ||
|
|
ddd4fc49a2 | ||
|
|
7d5bedde4d | ||
|
|
e34fee72a0 | ||
|
|
7ba4a43253 | ||
|
|
a09fd4577a | ||
|
|
525a3539d2 | ||
|
|
fbcdd45015 | ||
|
|
1ea8ed6442 | ||
|
|
f6817131b8 | ||
|
|
28fc1d2ff2 | ||
|
|
5925f72316 | ||
|
|
8dfa5fc37e | ||
|
|
49b04e8789 | ||
|
|
d87d87f467 | ||
|
|
bf72b3ad49 | ||
|
|
30f2981259 | ||
|
|
121bfd1fa8 | ||
|
|
9e2a4325e9 | ||
|
|
4509c1bd06 | ||
|
|
3133d89dcc | ||
|
|
99775458c4 | ||
|
|
e432960246 | ||
|
|
58cd133b5c | ||
|
|
3d234e7fc7 | ||
|
|
595258ae05 | ||
|
|
06b2a890da | ||
|
|
95ed31391d | ||
|
|
98944efdb8 | ||
|
|
3f27be9bcb | ||
|
|
5902fe2cbe | ||
|
|
73e0f81e83 | ||
|
|
cbe842735e | ||
|
|
72bc9f0ae4 | ||
|
|
0defa117a0 | ||
|
|
3821cfab0c | ||
|
|
09f159991e | ||
|
|
014d2ace76 | ||
|
|
646728372b |
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.91.0
|
||||
RUST_VERSION: 1.92.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.88.0
|
||||
|
||||
@@ -1,5 +1,81 @@
|
||||
# Changelog
|
||||
|
||||
## [2.35.0] - 2025-12-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add blob dir size to storage info ([#7605](https://github.com/chatmail/core/pull/7605)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Use `turn.delta.chat` as fallback TURN server ([#7382](https://github.com/chatmail/core/pull/7382)).
|
||||
- Add ip addresses of known public chatmail relays from https://chatmail.at/relays to DNS cache ([#7607](https://github.com/chatmail/core/pull/7607)).
|
||||
- Improve error messages on adding relays.
|
||||
- Add transport addresses to IMAP URLs in message info.
|
||||
- `lookup_host_with_cache()`: Don't return empty address list ([#7596](https://github.com/chatmail/core/pull/7596)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `get_chat_msgs_ex()`: Don't match on "S=" (Cmd) in param payload.
|
||||
- Remove `SecurejoinWait` info message when received Alice's key ([#7585](https://github.com/chatmail/core/pull/7585)).
|
||||
- Do not set normalized name for existing chats and contacts in a migration.
|
||||
- Remove now redundant "used_account_settings" and "entered_account_settings" from `Context.get_info()` ([#7587](https://github.com/chatmail/core/pull/7587)).
|
||||
- Don't use fallback servers if got TURN servers from IMAP METADATA.
|
||||
- Use fallback ICE servers if server can't IMAP METADATA ([#7382](https://github.com/chatmail/core/pull/7382)).
|
||||
- Add explicit limit for adding relays (5 at the moment) ([#7611](https://github.com/chatmail/core/pull/7611)).
|
||||
- Take `transport_id` into account when using `imap` table.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.92.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Apply Rust 1.92.0 clippy suggestions.
|
||||
|
||||
### Other
|
||||
|
||||
- Log entered login params and actual used params on configuration failure ([#7610](https://github.com/chatmail/core/pull/7610)).
|
||||
|
||||
## [2.34.0] - 2025-12-11
|
||||
|
||||
### API-Changes
|
||||
|
||||
- rpc-client: Accept `Account` for `Chat.{add,remove}_contact()`.
|
||||
- rpc-client: Add `Chat.num_contacts()`.
|
||||
- Forwarding messages to another profile ([#7491](https://github.com/chatmail/core/pull/7491)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Double ringing time to 120 seconds.
|
||||
- Better logging for failing securejoin messages ([#7593](https://github.com/chatmail/core/pull/7593)).
|
||||
- Add multi-transport information to `Context.get_info` ([#7583](https://github.com/chatmail/core/pull/7583))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Multi-transport: all transports were shown as "inbox" in connectivity view, now they are shown by their hostname ([#7582](https://github.com/chatmail/core/pull/7582)).
|
||||
- Multi-transport: Synchronize primary transport immediately after changing it.
|
||||
- Use u64 instead of usize to calculate storage usage.
|
||||
- Use u64 to represent the number of bytes in backup files.
|
||||
- Use u64 to count the number of bytes sent/received over the network.
|
||||
- Use logging macros instead of emitting event directly, so that it is also logged by tracing ([#7459](https://github.com/chatmail/core/pull/7459)).
|
||||
- Let securejoin succeed even if the chat was deleted in the meantime ([#7594](https://github.com/chatmail/core/pull/7594)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Add RUSTSEC-2025-0134 exception to deny.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use u16 instead of usize to represent progress bar.
|
||||
- Remove EncryptHelper.prefer_encrypt.
|
||||
- Add params when forwarding message instead of removing unneeded ones.
|
||||
|
||||
### Tests
|
||||
|
||||
- Port test_synchronize_member_list_on_group_rejoin to JSON-RPC.
|
||||
- Test setting up second device between core versions.
|
||||
|
||||
## [2.33.0] - 2025-12-05
|
||||
|
||||
### Features / Changes
|
||||
@@ -7371,3 +7447,5 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.31.0]: https://github.com/chatmail/core/compare/v2.30.0..v2.31.0
|
||||
[2.32.0]: https://github.com/chatmail/core/compare/v2.31.0..v2.32.0
|
||||
[2.33.0]: https://github.com/chatmail/core/compare/v2.32.0..v2.33.0
|
||||
[2.34.0]: https://github.com/chatmail/core/compare/v2.33.0..v2.34.0
|
||||
[2.35.0]: https://github.com/chatmail/core/compare/v2.34.0..v2.35.0
|
||||
|
||||
Generated
+6
-6
@@ -1304,7 +1304,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1388,6 +1388,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
@@ -1413,7 +1414,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1429,13 +1430,12 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"typescript-type-def",
|
||||
"walkdir",
|
||||
"yerpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1451,7 +1451,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1480,7 +1480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
@@ -111,6 +111,7 @@ toml = "0.9"
|
||||
tracing = "0.1.41"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
walkdir = "2.5.0"
|
||||
webpki-roots = "0.26.8"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -19,7 +19,6 @@ yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
||||
tokio = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
walkdir = "2.5.0"
|
||||
base64 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -10,8 +10,9 @@ pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem,
|
||||
MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::Config;
|
||||
@@ -34,14 +35,13 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::storage_usage::get_storage_usage;
|
||||
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
pub mod types;
|
||||
@@ -329,13 +329,7 @@ impl CommandApi {
|
||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let dbfile = ctx.get_dbfile().metadata()?.len();
|
||||
let total_size = WalkDir::new(ctx.get_blobdir())
|
||||
.max_depth(2)
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.metadata().ok())
|
||||
.filter(|metadata| metadata.is_file())
|
||||
.fold(0, |acc, m| acc + m.len());
|
||||
let total_size = get_blobdir_storage_usage(&ctx);
|
||||
|
||||
Ok(dbfile + total_size)
|
||||
}
|
||||
@@ -2208,6 +2202,27 @@ impl CommandApi {
|
||||
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Forward messages to a chat in another account.
|
||||
/// See [`Self::forward_messages`] for more info.
|
||||
async fn forward_messages_to_account(
|
||||
&self,
|
||||
src_account_id: u32,
|
||||
src_message_ids: Vec<u32>,
|
||||
dst_account_id: u32,
|
||||
dst_chat_id: u32,
|
||||
) -> Result<()> {
|
||||
let src_ctx = self.get_context(src_account_id).await?;
|
||||
let dst_ctx = self.get_context(dst_account_id).await?;
|
||||
let src_message_ids: Vec<MsgId> = src_message_ids.into_iter().map(MsgId::new).collect();
|
||||
forward_msgs_2ctx(
|
||||
&src_ctx,
|
||||
&src_message_ids,
|
||||
&dst_ctx,
|
||||
ChatId::new(dst_chat_id),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Resend messages and make information available for newly added chat members.
|
||||
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
/// Clients that already have the original message can still ignore the resent message as
|
||||
|
||||
@@ -271,7 +271,7 @@ pub enum EventType {
|
||||
/// Progress.
|
||||
///
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
|
||||
/// Progress comment or error, something to display to the user.
|
||||
comment: Option<String>,
|
||||
@@ -282,7 +282,7 @@ pub enum EventType {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexProgress {
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
@@ -313,7 +313,7 @@ pub enum EventType {
|
||||
chat_id: u32,
|
||||
|
||||
/// Progress, always 1000.
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
@@ -329,7 +329,7 @@ pub enum EventType {
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.33.0"
|
||||
"version": "2.35.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -430,12 +430,12 @@ async fn handle_cmd(
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
if let Some(oauth2_url) =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
||||
{
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||
} else {
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap());
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
}
|
||||
} else {
|
||||
println!("oauth2: set addr first.");
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -219,10 +219,12 @@ class Chat:
|
||||
"""Mark all messages in this chat as noticed."""
|
||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
|
||||
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||
"""Add contacts to this group."""
|
||||
from .account import Account
|
||||
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
if isinstance(cnt, (str, Account)):
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
@@ -230,10 +232,12 @@ class Chat:
|
||||
contact_id = cnt
|
||||
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
def remove_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||
"""Remove members from this group."""
|
||||
from .account import Account
|
||||
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
if isinstance(cnt, (str, Account)):
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
@@ -249,6 +253,10 @@ class Chat:
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
def num_contacts(self) -> int:
|
||||
"""Return number of contacts in this chat."""
|
||||
return len(self.get_contacts())
|
||||
|
||||
def get_past_contacts(self) -> list[Contact]:
|
||||
"""Get past contacts for this chat."""
|
||||
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
||||
|
||||
@@ -42,3 +42,16 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
assert msg.get_snapshot().text == "hello"
|
||||
|
||||
|
||||
def test_second_device(acfactory, alice_and_remote_bob) -> None:
|
||||
"""Test setting up current version as a second device for old version."""
|
||||
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||
|
||||
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
|
||||
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
|
||||
new_account = acfactory.get_unconfigured_account()
|
||||
new_account._rpc.get_backup(new_account.id, qr)
|
||||
remote_eval("locals()['future']()")
|
||||
|
||||
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
||||
|
||||
@@ -188,6 +188,10 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport3["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
[transport1, transport3] = ac1_clone.list_transports()
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
log.section("ac1 removes the first transport")
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
|
||||
@@ -217,3 +221,58 @@ def test_recognize_self_address(acfactory) -> None:
|
||||
bob_chat.send_text("Hello!")
|
||||
msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.chat == alice.create_chat(bob)
|
||||
|
||||
|
||||
def test_transport_limit(acfactory) -> None:
|
||||
"""Test transports limit."""
|
||||
account = acfactory.get_online_account()
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
limit = 5
|
||||
|
||||
for _ in range(1, limit):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
assert len(account.list_transports()) == limit
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
account.delete_transport(second_addr)
|
||||
|
||||
# test that adding a transport after deleting one works again
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory, log) -> None:
|
||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
log.section("Alice adds ac1 clone removes second transport")
|
||||
qr = acfactory.get_account_qr()
|
||||
for i in range(3):
|
||||
alice.add_transport_from_qr(qr)
|
||||
# Wait for all transports to go IDLE after adding each one.
|
||||
for _ in range(i + 1):
|
||||
alice.bring_online()
|
||||
|
||||
new_alice_addr = alice.list_transports()[2]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
|
||||
# Enable multi-device mode so messages are not deleted immediately.
|
||||
alice.set_config("bcc_self", "1")
|
||||
|
||||
# Bob creates chat, learning about Alice's currently selected transport.
|
||||
# This is where he will send the message.
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
# Alice changes the transport again.
|
||||
alice.set_config("configured_addr", alice.list_transports()[3]["addr"])
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
for alice_transport in alice.list_transports():
|
||||
addr = alice_transport["addr"]
|
||||
assert (addr == new_alice_addr) == (addr in msg.get_info())
|
||||
|
||||
@@ -90,12 +90,9 @@ def test_lowercase_address(acfactory) -> None:
|
||||
assert account.get_config("configured_addr") == addr
|
||||
assert account.list_transports()[0]["addr"] == addr
|
||||
|
||||
for param in [
|
||||
account.get_info()["used_account_settings"],
|
||||
account.get_info()["entered_account_settings"],
|
||||
]:
|
||||
assert addr in param
|
||||
assert addr_upper not in param
|
||||
param = account.get_info()["used_transport_settings"]
|
||||
assert addr in param
|
||||
assert addr_upper not in param
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
@@ -733,7 +730,7 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert "cert_strict" in alice.get_info().used_account_settings
|
||||
assert "cert_strict" in alice.get_info().used_transport_settings
|
||||
|
||||
# "cert_old_automatic" is the value old Delta Chat core versions used
|
||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||
@@ -746,7 +743,7 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
#
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert "cert_old_automatic" not in alice.get_info().used_account_settings
|
||||
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory):
|
||||
@@ -1012,3 +1009,47 @@ def test_message_exists(acfactory):
|
||||
ac1.remove()
|
||||
assert not message1.exists()
|
||||
assert not message2.exists()
|
||||
|
||||
|
||||
def test_synchronize_member_list_on_group_rejoin(acfactory, log):
|
||||
"""
|
||||
Test that user recreates group member list when it joins the group again.
|
||||
ac1 creates a group with two other accounts: ac2 and ac3
|
||||
Then it removes ac2, removes ac3 and adds ac2 back.
|
||||
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
||||
"""
|
||||
log.section("setting up accounts, accepted with each other")
|
||||
ac1, ac2, ac3 = accounts = acfactory.get_online_accounts(3)
|
||||
|
||||
log.section("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group("title1")
|
||||
chat.add_contact(ac2)
|
||||
chat.add_contact(ac3)
|
||||
|
||||
log.section("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
assert chat.num_contacts() == 3
|
||||
|
||||
log.section("checking that the chat arrived correctly")
|
||||
for ac in accounts[1:]:
|
||||
msg = ac.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
assert msg.chat.num_contacts() == 3
|
||||
msg.chat.accept()
|
||||
|
||||
log.section("ac1: removing ac2")
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
log.section("ac2: wait for a message about removal from the chat")
|
||||
ac2.wait_for_incoming_msg()
|
||||
log.section("ac1: removing ac3")
|
||||
chat.remove_contact(ac3)
|
||||
|
||||
log.section("ac1: adding ac2 back")
|
||||
chat.add_contact(ac2)
|
||||
|
||||
log.section("ac2: check that ac3 is removed")
|
||||
msg = ac2.wait_for_incoming_msg()
|
||||
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.get_snapshot().chat.num_contacts() == 2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.33.0"
|
||||
"version": "2.35.0"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ ignore = [
|
||||
|
||||
# Unmaintained paste
|
||||
"RUSTSEC-2024-0436",
|
||||
|
||||
# Unmaintained rustls-pemfile
|
||||
# It is a transitive dependency of iroh 0.35.0,
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134"
|
||||
]
|
||||
|
||||
[bans]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.33.0"
|
||||
version = "2.35.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
import deltachat as dc
|
||||
@@ -63,56 +62,6 @@ class TestGroupStressTests:
|
||||
# Message should be encrypted because keys of other members are gossiped
|
||||
assert msg.is_encrypted()
|
||||
|
||||
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
|
||||
"""
|
||||
Test that user recreates group member list when it joins the group again.
|
||||
ac1 creates a group with two other accounts: ac2 and ac3
|
||||
Then it removes ac2, removes ac3 and adds ac2 back.
|
||||
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
||||
"""
|
||||
lp.sec("setting up accounts, accepted with each other")
|
||||
accounts = acfactory.get_online_accounts(3)
|
||||
acfactory.introduce_each_other(accounts)
|
||||
ac1, ac2, ac3 = accounts
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title1", contacts=[ac2, ac3])
|
||||
assert not chat.is_promoted()
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
assert chat.is_promoted() and msg.is_encrypted()
|
||||
|
||||
assert chat.num_contacts() == 3
|
||||
|
||||
lp.sec("checking that the chat arrived correctly")
|
||||
for ac in accounts[1:]:
|
||||
msg = ac._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
print("chat is", msg.chat)
|
||||
assert msg.chat.num_contacts() == 3
|
||||
|
||||
lp.sec("ac1: removing ac2")
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
lp.sec("ac2: wait for a message about removal from the chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: removing ac3")
|
||||
chat.remove_contact(ac3)
|
||||
|
||||
lp.sec("ac1: adding ac2 back")
|
||||
# Group is promoted, message is sent automatically
|
||||
assert chat.is_promoted()
|
||||
chat.add_contact(ac2)
|
||||
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
@@ -1295,16 +1295,17 @@ def test_configure_error_msgs_invalid_server(acfactory):
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||
if ev.data1 == 0:
|
||||
break
|
||||
err_lower = ev.data2.lower()
|
||||
# Can't connect so it probably should say something about "internet"
|
||||
# again, should not repeat itself
|
||||
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
|
||||
# in configure.rs returned false because the error message was changed
|
||||
# (i.e. did not contain "could not resolve" anymore)
|
||||
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
|
||||
assert (err_lower.count("internet") + err_lower.count("network")) == 1
|
||||
# Should mention that it can't connect:
|
||||
assert ev.data2.count("connect") == 1
|
||||
assert err_lower.count("connect") == 1
|
||||
# The users do not know what "configuration" is
|
||||
assert "configuration" not in ev.data2.lower()
|
||||
assert "configuration" not in err_lower
|
||||
|
||||
|
||||
def test_status(acfactory):
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2025-12-05
|
||||
2025-12-16
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.91.0
|
||||
RUST_VERSION=1.92.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -377,6 +377,11 @@ impl Accounts {
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::INFO,
|
||||
account_id = 0,
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
);
|
||||
let mut set = JoinSet::new();
|
||||
for account in accounts {
|
||||
set.spawn(async move {
|
||||
@@ -392,6 +397,11 @@ impl Accounts {
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::INFO,
|
||||
account_id = 0,
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
);
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
@@ -429,6 +439,11 @@ impl Accounts {
|
||||
id: 0,
|
||||
typ: EventType::Warning("Background fetch timed out.".to_string()),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::WARN,
|
||||
account_id = 0,
|
||||
"Background fetch timed out."
|
||||
);
|
||||
}
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
|
||||
+17
-6
@@ -33,7 +33,7 @@ use tokio::time::sleep;
|
||||
///
|
||||
/// For the caller, this means they should also not wait longer,
|
||||
/// as the callee won't start the call afterwards.
|
||||
const RINGING_SECONDS: i64 = 60;
|
||||
const RINGING_SECONDS: i64 = 120;
|
||||
|
||||
// For persisting parameters in the call, we use Param::Arg*
|
||||
|
||||
@@ -663,9 +663,7 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
// We use nine.testrun.org for a default STUN server.
|
||||
let hostname = "nine.testrun.org";
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||
@@ -673,14 +671,27 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
let stun_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
let hostname = "turn.delta.chat";
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
let turn_server = IceServer {
|
||||
urls,
|
||||
username: Some("public".to_string()),
|
||||
credential: Some("o4tR7yG4rG2slhXqRUf9zgmHz".to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[stun_server, turn_server])?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
|
||||
+42
-26
@@ -432,14 +432,18 @@ impl ChatId {
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
// Previously accepting a chat literally created a chat because unaccepted chats
|
||||
// went to "contact requests" list rather than normal chatlist.
|
||||
// But for groups we use lower origin because users don't always check all members
|
||||
// before accepting a chat and may not want to have the group members mixed with
|
||||
// existing contacts. `IncomingTo` fits here by its definition.
|
||||
let origin = match chat.typ {
|
||||
Chattype::Group => Origin::IncomingTo,
|
||||
_ => Origin::CreateChat,
|
||||
};
|
||||
for contact_id in get_chat_contacts(context, self).await? {
|
||||
if contact_id != ContactId::SELF {
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
|
||||
.await?;
|
||||
ContactId::scaleup_origin(context, &[contact_id], origin).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3090,7 +3094,7 @@ pub async fn get_chat_msgs_ex(
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
AND (
|
||||
m.param GLOB \"*S=*\"
|
||||
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
|
||||
OR m.from_id == ?
|
||||
OR m.to_id == ?
|
||||
);",
|
||||
@@ -4203,6 +4207,16 @@ pub async fn set_chat_profile_image(
|
||||
|
||||
/// Forwards multiple messages to a chat.
|
||||
pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Result<()> {
|
||||
forward_msgs_2ctx(context, msg_ids, context, chat_id).await
|
||||
}
|
||||
|
||||
/// Forwards multiple messages to a chat in another context.
|
||||
pub async fn forward_msgs_2ctx(
|
||||
ctx_src: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
ctx_dst: &Context,
|
||||
chat_id: ChatId,
|
||||
) -> Result<()> {
|
||||
ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward");
|
||||
ensure!(!chat_id.is_special(), "can not forward to special chat");
|
||||
|
||||
@@ -4210,16 +4224,16 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
let mut curr_timestamp: i64;
|
||||
|
||||
chat_id
|
||||
.unarchive_if_not_muted(context, MessageState::Undefined)
|
||||
.unarchive_if_not_muted(ctx_dst, MessageState::Undefined)
|
||||
.await?;
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
let mut chat = Chat::load_from_db(ctx_dst, chat_id).await?;
|
||||
if let Some(reason) = chat.why_cant_send(ctx_dst).await? {
|
||||
bail!("cannot send to {chat_id}: {reason}");
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
|
||||
curr_timestamp = create_smeared_timestamps(ctx_dst, msg_ids.len());
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for id in msg_ids {
|
||||
let ts: i64 = context
|
||||
let ts: i64 = ctx_src
|
||||
.sql
|
||||
.query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
|
||||
.await?
|
||||
@@ -4229,11 +4243,14 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msgs.sort_unstable();
|
||||
for (_, id) in msgs {
|
||||
let src_msg_id: MsgId = id;
|
||||
let mut msg = Message::load_from_db(context, src_msg_id).await?;
|
||||
let mut msg = Message::load_from_db(ctx_src, src_msg_id).await?;
|
||||
if msg.state == MessageState::OutDraft {
|
||||
bail!("cannot forward drafts.");
|
||||
}
|
||||
|
||||
let mut param = msg.param;
|
||||
msg.param = Params::new();
|
||||
|
||||
if msg.get_viewtype() != Viewtype::Sticker {
|
||||
msg.param
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
@@ -4243,17 +4260,16 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.viewtype = Viewtype::Text;
|
||||
}
|
||||
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcDocument);
|
||||
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.param.remove(Param::IsEdited);
|
||||
msg.param.remove(Param::WebrtcRoom);
|
||||
msg.param.remove(Param::WebrtcAccepted);
|
||||
let param = &mut param;
|
||||
msg.param.steal(param, Param::File);
|
||||
msg.param.steal(param, Param::Filename);
|
||||
msg.param.steal(param, Param::Width);
|
||||
msg.param.steal(param, Param::Height);
|
||||
msg.param.steal(param, Param::Duration);
|
||||
msg.param.steal(param, Param::MimeType);
|
||||
msg.param.steal(param, Param::ProtectQuote);
|
||||
msg.param.steal(param, Param::Quote);
|
||||
msg.param.steal(param, Param::Summary1);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
@@ -4262,16 +4278,16 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.state = MessageState::OutPending;
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
msg.timestamp_sort = curr_timestamp;
|
||||
chat.prepare_msg_raw(context, &mut msg, None).await?;
|
||||
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
||||
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
|
||||
ctx_dst.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
created_msgs.push(msg.id);
|
||||
}
|
||||
for msg_id in created_msgs {
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
ctx_dst.emit_msgs_changed(chat_id, msg_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5240,6 +5240,44 @@ async fn test_send_delete_request_no_encryption() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_msgs_2ctx() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let alice_sent = alice.send_text(alice_chat.id, "hi").await;
|
||||
let bob_alice_msg = bob.recv_msg(&alice_sent).await;
|
||||
let bob_chat_id = bob_alice_msg.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
let bob_text = "Hi, did you know we're using the same device so i have access to your profile?";
|
||||
let bob_sent = bob.send_text(bob_chat_id, bob_text).await;
|
||||
alice.recv_msg(&bob_sent).await;
|
||||
let alice_chat_len = alice_chat.id.get_msg_cnt(alice).await?;
|
||||
|
||||
forward_msgs_2ctx(
|
||||
bob,
|
||||
&[bob_alice_msg.id, bob_sent.sender_msg_id],
|
||||
alice,
|
||||
alice_chat.id,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, alice_chat_len + 2);
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.is_forwarded());
|
||||
assert_eq!(msg.text, bob_text);
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert!(msg.is_forwarded());
|
||||
assert_eq!(msg.text, bob_text);
|
||||
assert_eq!(msg.from_id, bob_alice_msg.from_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that in multi-device setup
|
||||
/// second device learns the key of a contact
|
||||
/// via Autocrypt-Gossip in 1:1 chats.
|
||||
|
||||
+33
-31
@@ -20,7 +20,7 @@ use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::Provider;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport};
|
||||
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
@@ -818,37 +818,39 @@ impl Context {
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
|
||||
.await?;
|
||||
} else {
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
// Clean up SMTP and IMAP APPEND queue.
|
||||
//
|
||||
// The messages in the queue have a different
|
||||
// From address so we cannot send them over
|
||||
// the new SMTP transport.
|
||||
transaction.execute("DELETE FROM smtp", ())?;
|
||||
transaction.execute("DELETE FROM imap_send", ())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
// Clean up SMTP and IMAP APPEND queue.
|
||||
//
|
||||
// The messages in the queue have a different
|
||||
// From address so we cannot send them over
|
||||
// the new SMTP transport.
|
||||
transaction.execute("DELETE FROM smtp", ())?;
|
||||
transaction.execute("DELETE FROM imap_send", ())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
|
||||
+41
-4
@@ -45,6 +45,10 @@ use crate::transport::{
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
|
||||
/// Maximum number of relays
|
||||
/// see <https://github.com/chatmail/core/issues/7608>
|
||||
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
@@ -269,17 +273,50 @@ impl Context {
|
||||
.await?
|
||||
{
|
||||
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
|
||||
bail!("Cannot use multi-transport with mvbox_move enabled.");
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
|
||||
bail!("Cannot use multi-transport with only_fetch_mvbox enabled.");
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
|
||||
bail!("Cannot use multi-transport with disabled fetching of classic emails.");
|
||||
bail!(
|
||||
"To use additional relays, set the legacy option \"Settings / Advanced / Show Classic Emails\" to \"All\"."
|
||||
);
|
||||
}
|
||||
|
||||
if self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM transports", ())
|
||||
.await?
|
||||
>= MAX_TRANSPORT_RELAYS
|
||||
{
|
||||
bail!(
|
||||
"You have reached the maximum number of relays ({}).",
|
||||
MAX_TRANSPORT_RELAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let provider = configure(self, param).await?;
|
||||
let provider = match configure(self, param).await {
|
||||
Err(error) => {
|
||||
// Log entered and actual params
|
||||
let configured_param = get_configured_param(self, param).await;
|
||||
warn!(
|
||||
self,
|
||||
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
|
||||
param.to_string(),
|
||||
configured_param
|
||||
.map(|param| param.to_string())
|
||||
.unwrap_or("error".to_owned())
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(provider) => provider,
|
||||
};
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, provider).await?;
|
||||
|
||||
+9
-3
@@ -1144,7 +1144,7 @@ WHERE c.id>?
|
||||
AND c.origin>=?
|
||||
AND c.blocked=0
|
||||
AND (IFNULL(c.name_normalized,IIF(c.name='',c.authname,c.name)) LIKE ? OR c.addr LIKE ?)
|
||||
ORDER BY c.last_seen DESC, c.id DESC
|
||||
ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
|
||||
",
|
||||
(
|
||||
ContactId::LAST_SPECIAL,
|
||||
@@ -1152,6 +1152,7 @@ ORDER BY c.last_seen DESC, c.id DESC
|
||||
minimal_origin,
|
||||
&s3str_like_cmd,
|
||||
&s3str_like_cmd,
|
||||
Origin::CreateChat,
|
||||
),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
@@ -1201,8 +1202,13 @@ ORDER BY c.last_seen DESC, c.id DESC
|
||||
AND (fingerprint='')=?
|
||||
AND origin>=?
|
||||
AND blocked=0
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
(ContactId::LAST_SPECIAL, flag_address, minimal_origin),
|
||||
ORDER BY origin>=? DESC, last_seen DESC, id DESC",
|
||||
(
|
||||
ContactId::LAST_SPECIAL,
|
||||
flag_address,
|
||||
minimal_origin,
|
||||
Origin::CreateChat,
|
||||
),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
|
||||
+11
-9
@@ -23,7 +23,6 @@ use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::warn;
|
||||
use crate::logged_debug_assert;
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::net::tls::TlsSessionStore;
|
||||
use crate::peer_channels::Iroh;
|
||||
@@ -417,7 +416,6 @@ impl Context {
|
||||
///
|
||||
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
|
||||
/// errors.
|
||||
#[deprecated(since = "TBD")]
|
||||
pub async fn open(&self, passphrase: String) -> Result<bool> {
|
||||
if self.sql.check_passphrase(passphrase.clone()).await? {
|
||||
self.sql.open(self, passphrase).await?;
|
||||
@@ -816,12 +814,17 @@ impl Context {
|
||||
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let l = EnteredLoginParam::load(self).await?;
|
||||
let l2 = ConfiguredLoginParam::load(self).await?.map_or_else(
|
||||
|| "Not configured".to_string(),
|
||||
|(_transport_id, param)| param.to_string(),
|
||||
);
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(transport_id, param)| format!("{transport_id}: {param}"))
|
||||
.collect();
|
||||
let all_transports = if all_transports.is_empty() {
|
||||
"Not configured".to_string()
|
||||
} else {
|
||||
all_transports.join(",")
|
||||
};
|
||||
let chats = get_chat_cnt(self).await?;
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
|
||||
let request_msgs = message::get_request_msg_cnt(self).await;
|
||||
@@ -900,8 +903,7 @@ impl Context {
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("proxy_enabled", proxy_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2);
|
||||
res.insert("used_transport_settings", all_transports);
|
||||
|
||||
if let Some(server_id) = &*self.server_id.read().await {
|
||||
res.insert("imap_server_id", format!("{server_id:?}"));
|
||||
|
||||
+6
-2
@@ -153,11 +153,15 @@ pub(crate) async fn download_msg(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let transport_id = session.transport_id();
|
||||
let row = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
(&msg.rfc724_mid,),
|
||||
"SELECT uid, folder FROM imap
|
||||
WHERE rfc724_mid=?
|
||||
AND transport_id=?
|
||||
AND target!=''",
|
||||
(&msg.rfc724_mid, transport_id),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
|
||||
+2
-8
@@ -12,29 +12,23 @@ use crate::pgp::{self, SeipdVersion};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptHelper {
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
}
|
||||
|
||||
impl EncryptHelper {
|
||||
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
let prefer_encrypt = EncryptPreference::Mutual;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let public_key = load_self_public_key(context).await?;
|
||||
|
||||
Ok(EncryptHelper {
|
||||
prefer_encrypt,
|
||||
addr,
|
||||
public_key,
|
||||
})
|
||||
Ok(EncryptHelper { addr, public_key })
|
||||
}
|
||||
|
||||
pub fn get_aheader(&self) -> Aheader {
|
||||
Aheader {
|
||||
addr: self.addr.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
prefer_encrypt: self.prefer_encrypt,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,10 @@ pub(crate) async fn emit_chatlist_item_changed_for_contact_chat(
|
||||
match ChatId::lookup_by_contact(context, contact_id).await {
|
||||
Ok(Some(chat_id)) => self::emit_chatlist_item_changed(context, chat_id),
|
||||
Ok(None) => {}
|
||||
Err(error) => context.emit_event(EventType::Error(format!(
|
||||
Err(error) => error!(
|
||||
context,
|
||||
"failed to find chat id for contact for chatlist event: {error:?}"
|
||||
))),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ pub enum EventType {
|
||||
/// Progress.
|
||||
///
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
|
||||
/// Progress comment or error, something to display to the user.
|
||||
comment: Option<String>,
|
||||
@@ -253,7 +253,7 @@ pub enum EventType {
|
||||
///
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
ImexProgress(usize),
|
||||
ImexProgress(u16),
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
/// This event may be sent multiple times by a single call to imex().
|
||||
@@ -280,7 +280,7 @@ pub enum EventType {
|
||||
chat_type: Chattype,
|
||||
|
||||
/// Progress, always 1000.
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
@@ -295,7 +295,7 @@ pub enum EventType {
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
|
||||
+43
-37
@@ -123,7 +123,7 @@ struct OAuth2 {
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ServerMetadata {
|
||||
/// IMAP METADATA `/shared/comment` as defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1>.
|
||||
@@ -916,7 +916,7 @@ impl Session {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
|
||||
transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
|
||||
for (uid, (rfc724_mid, target)) in &msgs {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
@@ -1054,14 +1054,16 @@ impl Session {
|
||||
///
|
||||
/// This is the only place where messages are moved or deleted on the IMAP server.
|
||||
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
let transport_id = self.transport_id();
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id, uid, target FROM imap
|
||||
WHERE folder = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
(folder,),
|
||||
WHERE folder = ?
|
||||
AND transport_id = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
(folder, transport_id),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
@@ -1277,10 +1279,10 @@ impl Session {
|
||||
};
|
||||
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
|
||||
if is_seen
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to update seen status for msg {folder}/{uid}")
|
||||
format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
|
||||
})?
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
@@ -1546,17 +1548,17 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves server metadata if it is supported.
|
||||
/// Retrieves server metadata if it is supported, otherwise uses fallback one.
|
||||
///
|
||||
/// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
|
||||
/// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
|
||||
/// metadata.
|
||||
pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
|
||||
if !self.can_metadata() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
|
||||
let mut lock = context.metadata.write().await;
|
||||
|
||||
if !self.can_metadata() {
|
||||
*lock = Some(Default::default());
|
||||
}
|
||||
if let Some(ref mut old_metadata) = *lock {
|
||||
let now = time();
|
||||
|
||||
@@ -1565,31 +1567,33 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(context, "ICE servers expired, requesting new credentials.");
|
||||
let mailbox = "";
|
||||
let options = "";
|
||||
let metadata = self
|
||||
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
|
||||
.await?;
|
||||
let mut got_turn_server = false;
|
||||
for m in metadata {
|
||||
if m.entry == "/shared/vendor/deltachat/turn"
|
||||
&& let Some(value) = m.value
|
||||
{
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
got_turn_server = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
if self.can_metadata() {
|
||||
info!(context, "ICE servers expired, requesting new credentials.");
|
||||
let mailbox = "";
|
||||
let options = "";
|
||||
let metadata = self
|
||||
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
|
||||
.await?;
|
||||
for m in metadata {
|
||||
if m.entry == "/shared/vendor/deltachat/turn"
|
||||
&& let Some(value) = m.value
|
||||
{
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
got_turn_server = true;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !got_turn_server {
|
||||
info!(context, "Will use fallback ICE servers.");
|
||||
// Set expiration timestamp 7 days in the future so we don't request it again.
|
||||
old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
|
||||
old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
|
||||
@@ -2357,6 +2361,7 @@ pub(crate) async fn prefetch_should_download(
|
||||
/// Returns updated chat ID if any message was marked as seen.
|
||||
async fn mark_seen_by_uid(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
folder: &str,
|
||||
uid_validity: u32,
|
||||
uid: u32,
|
||||
@@ -2367,12 +2372,13 @@ async fn mark_seen_by_uid(
|
||||
"SELECT id, chat_id FROM msgs
|
||||
WHERE id > 9 AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM imap
|
||||
WHERE folder=?1
|
||||
AND uidvalidity=?2
|
||||
AND uid=?3
|
||||
WHERE transport_id=?
|
||||
AND folder=?
|
||||
AND uidvalidity=?
|
||||
AND uid=?
|
||||
LIMIT 1
|
||||
)",
|
||||
(&folder, uid_validity, uid),
|
||||
(transport_id, &folder, uid_validity, uid),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let chat_id: ChatId = row.get(1)?;
|
||||
|
||||
+16
-13
@@ -25,7 +25,8 @@ use crate::pgp;
|
||||
use crate::qr::DCBACKUP_VERSION;
|
||||
use crate::sql;
|
||||
use crate::tools::{
|
||||
TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file,
|
||||
TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, usize_to_u64,
|
||||
write_file,
|
||||
};
|
||||
|
||||
mod key_transfer;
|
||||
@@ -263,14 +264,14 @@ struct ProgressReader<R> {
|
||||
inner: R,
|
||||
|
||||
/// Number of bytes successfully read from the internal reader.
|
||||
read: usize,
|
||||
read: u64,
|
||||
|
||||
/// Total size of the backup .tar file expected to be read from the reader.
|
||||
/// Used to calculate the progress.
|
||||
file_size: usize,
|
||||
file_size: u64,
|
||||
|
||||
/// Last progress emitted to avoid emitting the same progress value twice.
|
||||
last_progress: usize,
|
||||
last_progress: u16,
|
||||
|
||||
/// Context for emitting progress events.
|
||||
context: Context,
|
||||
@@ -281,7 +282,7 @@ impl<R> ProgressReader<R> {
|
||||
Self {
|
||||
inner: r,
|
||||
read: 0,
|
||||
file_size: file_size as usize,
|
||||
file_size,
|
||||
last_progress: 1,
|
||||
context,
|
||||
}
|
||||
@@ -301,9 +302,11 @@ where
|
||||
let before = buf.filled().len();
|
||||
let res = this.inner.poll_read(cx, buf);
|
||||
if let std::task::Poll::Ready(Ok(())) = res {
|
||||
*this.read = this.read.saturating_add(buf.filled().len() - before);
|
||||
*this.read = this
|
||||
.read
|
||||
.saturating_add(usize_to_u64(buf.filled().len() - before));
|
||||
|
||||
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999);
|
||||
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999) as u16;
|
||||
if progress > *this.last_progress {
|
||||
this.context.emit_event(EventType::ImexProgress(progress));
|
||||
*this.last_progress = progress;
|
||||
@@ -490,14 +493,14 @@ struct ProgressWriter<W> {
|
||||
inner: W,
|
||||
|
||||
/// Number of bytes successfully written into the internal writer.
|
||||
written: usize,
|
||||
written: u64,
|
||||
|
||||
/// Total size of the backup .tar file expected to be written into the writer.
|
||||
/// Used to calculate the progress.
|
||||
file_size: usize,
|
||||
file_size: u64,
|
||||
|
||||
/// Last progress emitted to avoid emitting the same progress value twice.
|
||||
last_progress: usize,
|
||||
last_progress: u16,
|
||||
|
||||
/// Context for emitting progress events.
|
||||
context: Context,
|
||||
@@ -508,7 +511,7 @@ impl<W> ProgressWriter<W> {
|
||||
Self {
|
||||
inner: w,
|
||||
written: 0,
|
||||
file_size: file_size as usize,
|
||||
file_size,
|
||||
last_progress: 1,
|
||||
context,
|
||||
}
|
||||
@@ -527,9 +530,9 @@ where
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write(cx, buf);
|
||||
if let std::task::Poll::Ready(Ok(written)) = res {
|
||||
*this.written = this.written.saturating_add(written);
|
||||
*this.written = this.written.saturating_add(usize_to_u64(written));
|
||||
|
||||
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999);
|
||||
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999) as u16;
|
||||
if progress > *this.last_progress {
|
||||
this.context.emit_event(EventType::ImexProgress(progress));
|
||||
*this.last_progress = progress;
|
||||
|
||||
+11
-5
@@ -15,16 +15,17 @@ use pin_project::pin_project;
|
||||
|
||||
use crate::events::{Event, EventType, Events};
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::tools::usize_to_u64;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
/// Total number of bytes read.
|
||||
pub total_read: usize,
|
||||
pub total_read: u64,
|
||||
|
||||
/// Total number of bytes written.
|
||||
pub total_written: usize,
|
||||
pub total_written: u64,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
@@ -91,6 +92,11 @@ impl<S: SessionStream> AsyncRead for LoggingStream<S> {
|
||||
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
|
||||
this.metrics.total_read, this.metrics.total_written
|
||||
);
|
||||
tracing::event!(
|
||||
::tracing::Level::WARN,
|
||||
account_id = *this.account_id,
|
||||
log_message
|
||||
);
|
||||
this.events.emit(Event {
|
||||
id: *this.account_id,
|
||||
typ: EventType::Warning(log_message),
|
||||
@@ -98,7 +104,7 @@ impl<S: SessionStream> AsyncRead for LoggingStream<S> {
|
||||
}
|
||||
|
||||
let n = old_remaining - buf.remaining();
|
||||
this.metrics.total_read = this.metrics.total_read.saturating_add(n);
|
||||
this.metrics.total_read = this.metrics.total_read.saturating_add(usize_to_u64(n));
|
||||
|
||||
res
|
||||
}
|
||||
@@ -113,7 +119,7 @@ impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write(cx, buf);
|
||||
if let Poll::Ready(Ok(n)) = res {
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(usize_to_u64(n));
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -140,7 +146,7 @@ impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write_vectored(cx, bufs);
|
||||
if let Poll::Ready(Ok(n)) = res {
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(usize_to_u64(n));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
+9
-4
@@ -171,12 +171,17 @@ impl MsgId {
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
|
||||
"SELECT transports.addr, imap.folder, imap.uid
|
||||
FROM imap
|
||||
LEFT JOIN transports
|
||||
ON transports.id = imap.transport_id
|
||||
WHERE imap.rfc724_mid=?",
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let folder: String = row.get("folder")?;
|
||||
let uid: u32 = row.get("uid")?;
|
||||
Ok(format!("</{folder}/;UID={uid}>"))
|
||||
let addr: String = row.get(0)?;
|
||||
let folder: String = row.get(1)?;
|
||||
let uid: u32 = row.get(2)?;
|
||||
Ok(format!("<{addr}/{folder}/;UID={uid}>"))
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
+76
-9
@@ -40,7 +40,7 @@
|
||||
//! used for successful connection timestamp of
|
||||
//! retrieving them from in-memory cache is used.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
@@ -506,10 +506,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"mail.nubo.coop",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
|
||||
),
|
||||
(
|
||||
"mehl.cloud",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
|
||||
),
|
||||
(
|
||||
"mx.freenet.de",
|
||||
vec![
|
||||
@@ -680,6 +676,72 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 164)),
|
||||
],
|
||||
),
|
||||
// Known public chatmail relays from https://chatmail.at/relays
|
||||
(
|
||||
"mehl.cloud",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
|
||||
),
|
||||
(
|
||||
"mailchat.pl",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 144, 137))],
|
||||
),
|
||||
(
|
||||
"chatmail.woodpeckersnest.space",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(85, 215, 162, 146))],
|
||||
),
|
||||
(
|
||||
"chatmail.culturanerd.it",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 94, 165))],
|
||||
),
|
||||
(
|
||||
"chatmail.hackea.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 11, 85))],
|
||||
),
|
||||
(
|
||||
"chika.aangat.lahat.computer",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(71, 19, 150, 113))],
|
||||
),
|
||||
(
|
||||
"tarpit.fun",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(152, 53, 86, 246))],
|
||||
),
|
||||
(
|
||||
"d.gaufr.es",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(51, 77, 140, 91))],
|
||||
),
|
||||
(
|
||||
"chtml.ca",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(51, 222, 156, 177))],
|
||||
),
|
||||
(
|
||||
"chatmail.au",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(45, 124, 54, 79))],
|
||||
),
|
||||
(
|
||||
"sombras.chat",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(82, 25, 70, 154))],
|
||||
),
|
||||
(
|
||||
"e2ee.wang",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(139, 84, 233, 161))],
|
||||
),
|
||||
(
|
||||
"chat.privittytech.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(35, 154, 144, 0))],
|
||||
),
|
||||
("e2ee.im", vec![IpAddr::V4(Ipv4Addr::new(45, 137, 99, 57))]),
|
||||
(
|
||||
"chatmail.email",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(57, 128, 220, 120))],
|
||||
),
|
||||
(
|
||||
"danneskjold.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 216, 132))],
|
||||
),
|
||||
(
|
||||
"darkrun.dev",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(72, 11, 149, 146))],
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
@@ -788,7 +850,7 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
}
|
||||
};
|
||||
|
||||
if load_cache {
|
||||
let addrs = if load_cache {
|
||||
let mut cache = lookup_cache(context, hostname, port, alpn, now).await?;
|
||||
if let Some(ips) = DNS_PRELOAD.get(hostname) {
|
||||
for ip in ips {
|
||||
@@ -799,10 +861,15 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(merge_with_cache(resolved_addrs, cache))
|
||||
merge_with_cache(resolved_addrs, cache)
|
||||
} else {
|
||||
Ok(resolved_addrs)
|
||||
}
|
||||
resolved_addrs
|
||||
};
|
||||
ensure!(
|
||||
!addrs.is_empty(),
|
||||
"Could not find DNS resolutions for {hostname}:{port}. Check server hostname and your network"
|
||||
);
|
||||
Ok(addrs)
|
||||
}
|
||||
|
||||
/// Merges results received from DNS with cached results.
|
||||
|
||||
@@ -433,6 +433,14 @@ impl Params {
|
||||
self.set(key, format!("{value}"));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn steal(&mut self, src: &mut Self, key: Param) -> &mut Self {
|
||||
let val = src.inner.remove(&key);
|
||||
if let Some(val) = val {
|
||||
self.inner.insert(key, val);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -16,7 +16,6 @@ use serde::Deserialize;
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::Fingerprint;
|
||||
use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam};
|
||||
use crate::net::http::post_empty;
|
||||
@@ -824,9 +823,10 @@ pub(crate) async fn login_param_from_account_qr(
|
||||
match serde_json::from_str::<CreateAccountErrorResponse>(&response_text) {
|
||||
Ok(error) => Err(anyhow!(error.reason)),
|
||||
Err(parse_error) => {
|
||||
context.emit_event(EventType::Error(format!(
|
||||
error!(
|
||||
context,
|
||||
"Cannot create account, server response could not be parsed:\n{parse_error:#}\nraw response:\n{response_text}"
|
||||
)));
|
||||
);
|
||||
bail!("Cannot create account, unexpected server response:\n{response_text:?}")
|
||||
}
|
||||
}
|
||||
|
||||
+12
-2
@@ -677,12 +677,22 @@ pub(crate) async fn receive_imf_inner(
|
||||
let res = if mime_parser.incoming {
|
||||
handle_securejoin_handshake(context, &mut mime_parser, from_id)
|
||||
.await
|
||||
.context("error in Secure-Join message handling")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Error in Secure-Join '{}' message handling",
|
||||
mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
|
||||
)
|
||||
})?
|
||||
} else if let Some(to_id) = to_ids.first().copied().flatten() {
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
observe_securejoin_on_other_device(context, &mime_parser, to_id)
|
||||
.await
|
||||
.context("error in Secure-Join watching")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Error in Secure-Join '{}' watching",
|
||||
mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
securejoin::HandshakeMessage::Propagate
|
||||
};
|
||||
|
||||
@@ -3852,6 +3852,38 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_contacts_goto_bottom() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let alice_chat_id = create_group(alice, "Testing contact list").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
|
||||
send_text_msg(alice, alice_chat_id, "hello".to_string()).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
|
||||
assert_eq!(Contact::get_all(bob, 0, None).await?.len(), 0);
|
||||
bob_chat_id.accept(bob).await?;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
assert_eq!(contacts[1], bob_fiona_id);
|
||||
|
||||
ChatId::create_for_contact(bob, bob_fiona_id).await?;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert_eq!(contacts[0], bob_fiona_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test for the bug when remote group membership changes from outdated messages overrode local
|
||||
/// ones. Especially that was a problem when a message is sent offline so that it doesn't
|
||||
/// incorporate recent group membership changes.
|
||||
|
||||
+12
-2
@@ -325,6 +325,8 @@ impl Drop for IoPausedGuard {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SchedBox {
|
||||
/// Hostname of used chatmail/email relay
|
||||
host: String,
|
||||
meaning: FolderMeaning,
|
||||
conn_state: ImapConnectionState,
|
||||
|
||||
@@ -536,9 +538,9 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
.await
|
||||
.context("Failed to download messages")?;
|
||||
session
|
||||
.fetch_metadata(ctx)
|
||||
.update_metadata(ctx)
|
||||
.await
|
||||
.context("Failed to fetch metadata")?;
|
||||
.context("update_metadata")?;
|
||||
session
|
||||
.register_token(ctx)
|
||||
.await
|
||||
@@ -881,7 +883,14 @@ impl Scheduler {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
|
||||
};
|
||||
let host = configured_login_param
|
||||
.addr
|
||||
.split("@")
|
||||
.last()
|
||||
.context("address has no host")?
|
||||
.to_owned();
|
||||
let inbox = SchedBox {
|
||||
host: host.clone(),
|
||||
meaning: FolderMeaning::Inbox,
|
||||
conn_state,
|
||||
handle,
|
||||
@@ -897,6 +906,7 @@ impl Scheduler {
|
||||
let meaning = FolderMeaning::Mvbox;
|
||||
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
|
||||
oboxes.push(SchedBox {
|
||||
host,
|
||||
meaning,
|
||||
conn_state,
|
||||
handle,
|
||||
|
||||
@@ -373,7 +373,13 @@ impl Context {
|
||||
InnerSchedulerState::Started(ref sched) => (
|
||||
sched
|
||||
.boxes()
|
||||
.map(|b| (b.meaning, b.conn_state.state.connectivity.clone()))
|
||||
.map(|b| {
|
||||
(
|
||||
b.host.clone(),
|
||||
b.meaning,
|
||||
b.conn_state.state.connectivity.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
sched.smtp.state.connectivity.clone(),
|
||||
),
|
||||
@@ -396,7 +402,7 @@ impl Context {
|
||||
let watched_folders = get_watched_folder_configs(self).await?;
|
||||
let incoming_messages = stock_str::incoming_messages(self).await;
|
||||
ret += &format!("<h3>{incoming_messages}</h3><ul>");
|
||||
for (folder, state) in &folders_states {
|
||||
for (host, folder, state) in &folders_states {
|
||||
let mut folder_added = false;
|
||||
|
||||
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
|
||||
@@ -407,7 +413,11 @@ impl Context {
|
||||
ret += "<li>";
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " <b>";
|
||||
ret += &*escaper::encode_minimal(&foldername);
|
||||
if folder == &FolderMeaning::Inbox {
|
||||
ret += &*escaper::encode_minimal(host);
|
||||
} else {
|
||||
ret += &*escaper::encode_minimal(&foldername);
|
||||
}
|
||||
ret += ":</b> ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
|
||||
ret += "</li>";
|
||||
|
||||
+2
-2
@@ -567,7 +567,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"vc-contact-confirm" => {
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress: JoinerProgress::Succeeded.to_usize(),
|
||||
progress: JoinerProgress::Succeeded.into_u16(),
|
||||
});
|
||||
Ok(HandshakeMessage::Ignore)
|
||||
}
|
||||
@@ -590,7 +590,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress: JoinerProgress::Succeeded.to_usize(),
|
||||
progress: JoinerProgress::Succeeded.into_u16(),
|
||||
});
|
||||
Ok(HandshakeMessage::Propagate)
|
||||
}
|
||||
|
||||
+93
-44
@@ -5,14 +5,16 @@ use anyhow::{Context as _, Result};
|
||||
use super::HandshakeMessage;
|
||||
use super::qrinvite::QrInvite;
|
||||
use crate::chat::{self, ChatId, is_contact_in_chat};
|
||||
use crate::chatlist_events;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::Origin;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint};
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
@@ -43,31 +45,21 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
|
||||
// hidden, if a user starts sending messages in it it will be unhidden in
|
||||
// receive_imf.
|
||||
let hidden = match invite {
|
||||
QrInvite::Contact { .. } => Blocked::Not,
|
||||
QrInvite::Group { .. } => Blocked::Yes,
|
||||
QrInvite::Broadcast { .. } => Blocked::Yes,
|
||||
};
|
||||
|
||||
// The 1:1 chat with the inviter
|
||||
let private_chat_id =
|
||||
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
|
||||
let private_chat_id = private_chat_id(context, &invite).await?;
|
||||
|
||||
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
let has_key = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
|
||||
(invite.fingerprint().hex(),),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Now start the protocol and initialise the state.
|
||||
{
|
||||
let has_key = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
|
||||
(invite.fingerprint().hex(),),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// `joining_chat_id` is `Some` if group chat
|
||||
// already exists and we are in the chat.
|
||||
let joining_chat_id = match invite {
|
||||
@@ -94,7 +86,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// Even if Alice is not verified, we don't send anything.
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id: invite.contact_id(),
|
||||
progress: JoinerProgress::Succeeded.to_usize(),
|
||||
progress: JoinerProgress::Succeeded.into_u16(),
|
||||
});
|
||||
return Ok(joining_chat_id);
|
||||
} else if has_key
|
||||
@@ -113,7 +105,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id: invite.contact_id(),
|
||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||
progress: JoinerProgress::RequestWithAuthSent.into_u16(),
|
||||
});
|
||||
} else {
|
||||
send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
|
||||
@@ -152,20 +144,22 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
Ok(joining_chat_id)
|
||||
}
|
||||
QrInvite::Contact { .. } => {
|
||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
||||
// uses it to send the handshake messages.
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
private_chat_id,
|
||||
&stock_str::securejoin_wait(context).await,
|
||||
SystemMessage::SecurejoinWait,
|
||||
None,
|
||||
time(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it is
|
||||
// used to send the handshake messages.
|
||||
if !has_key {
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
private_chat_id,
|
||||
&stock_str::securejoin_wait(context).await,
|
||||
SystemMessage::SecurejoinWait,
|
||||
None,
|
||||
time(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(private_chat_id)
|
||||
}
|
||||
}
|
||||
@@ -175,6 +169,9 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
///
|
||||
/// Returns the ID of the newly inserted entry.
|
||||
async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
|
||||
// The `chat_id` isn't actually needed anymore,
|
||||
// but we still save it;
|
||||
// can be removed as a future improvement.
|
||||
context
|
||||
.sql
|
||||
.insert(
|
||||
@@ -184,6 +181,38 @@ async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatI
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
if let Some((msg_id, param)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"
|
||||
SELECT id, param FROM msgs
|
||||
WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0)
|
||||
AND chat_id=? AND hidden=0
|
||||
LIMIT 1
|
||||
",
|
||||
(chat_id, chat_id),
|
||||
|row| {
|
||||
let id: MsgId = row.get(0)?;
|
||||
let param: String = row.get(1)?;
|
||||
let param: Params = param.parse().unwrap_or_default();
|
||||
Ok((id, param))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
&& param.get_cmd() == SystemMessage::SecurejoinWait
|
||||
{
|
||||
let on_server = false;
|
||||
msg_id.trash(context, on_server).await?;
|
||||
context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
|
||||
context.emit_msgs_changed_without_msg_id(chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles `vc-auth-required` and `vg-auth-required` handshake messages.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
@@ -195,11 +224,10 @@ pub(super) async fn handle_auth_required(
|
||||
// Load all Bob states that expect `vc-auth-required` or `vg-auth-required`.
|
||||
let bob_states = context
|
||||
.sql
|
||||
.query_map_vec("SELECT id, invite, chat_id FROM bobstate", (), |row| {
|
||||
.query_map_vec("SELECT id, invite FROM bobstate", (), |row| {
|
||||
let row_id: i64 = row.get(0)?;
|
||||
let invite: QrInvite = row.get(1)?;
|
||||
let chat_id: ChatId = row.get(2)?;
|
||||
Ok((row_id, invite, chat_id))
|
||||
Ok((row_id, invite))
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -209,7 +237,7 @@ pub(super) async fn handle_auth_required(
|
||||
);
|
||||
|
||||
let mut auth_sent = false;
|
||||
for (bobstate_row_id, invite, chat_id) in bob_states {
|
||||
for (bobstate_row_id, invite) in bob_states {
|
||||
if !encrypted_and_signed(context, message, invite.fingerprint()) {
|
||||
continue;
|
||||
}
|
||||
@@ -220,6 +248,12 @@ pub(super) async fn handle_auth_required(
|
||||
}
|
||||
|
||||
info!(context, "Fingerprint verified.",);
|
||||
let chat_id = private_chat_id(context, &invite).await?;
|
||||
delete_securejoin_wait_msg(context, chat_id)
|
||||
.await
|
||||
.context("delete_securejoin_wait_msg")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
|
||||
context
|
||||
.sql
|
||||
@@ -240,7 +274,7 @@ pub(super) async fn handle_auth_required(
|
||||
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id: invite.contact_id(),
|
||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||
progress: JoinerProgress::RequestWithAuthSent.into_u16(),
|
||||
});
|
||||
|
||||
auth_sent = true;
|
||||
@@ -348,6 +382,22 @@ impl BobHandshakeMsg {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the 1:1 chat with the inviter.
|
||||
///
|
||||
/// This is the chat in which securejoin messages are sent.
|
||||
/// The 1:1 chat will be created if it does not yet exist.
|
||||
async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
|
||||
let hidden = match invite {
|
||||
QrInvite::Contact { .. } => Blocked::Not,
|
||||
QrInvite::Group { .. } => Blocked::Yes,
|
||||
QrInvite::Broadcast { .. } => Blocked::Yes,
|
||||
};
|
||||
|
||||
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] of the chat being joined.
|
||||
///
|
||||
/// This is the chat in which you want to notify the user as well.
|
||||
@@ -406,8 +456,7 @@ pub(crate) enum JoinerProgress {
|
||||
}
|
||||
|
||||
impl JoinerProgress {
|
||||
#[expect(clippy::wrong_self_convention)]
|
||||
pub(crate) fn to_usize(self) -> usize {
|
||||
pub(crate) fn into_u16(self) -> u16 {
|
||||
match self {
|
||||
JoinerProgress::RequestWithAuthSent => 400,
|
||||
JoinerProgress::Succeeded => 1000,
|
||||
|
||||
@@ -100,6 +100,17 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||
Some(CantSendReason::MissingKey)
|
||||
);
|
||||
|
||||
// Check Bob's info messages.
|
||||
let msg_cnt = 2;
|
||||
let mut i = 0..msg_cnt;
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
|
||||
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
@@ -243,7 +254,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.unwrap();
|
||||
match case {
|
||||
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
||||
_ => assert_eq!(contact_alice.get_authname(), "Alice Exampleorg"),
|
||||
_ => assert_eq!(contact_alice.get_authname(), ""),
|
||||
};
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
@@ -272,15 +283,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert!(contact_alice.get_name().is_empty());
|
||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||
|
||||
// Check Bob got expected info messages in his 1:1 chat.
|
||||
let msg_cnt = 2;
|
||||
let mut i = 0..msg_cnt;
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
// The `SecurejoinWait` info message has been removed, but the e2ee notice remains.
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1217,3 +1223,33 @@ async fn test_qr_no_implicit_inviter_addition() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_user_deletes_chat_before_securejoin_completes() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
let bob_chat_id = join_securejoin(bob, &qr).await?;
|
||||
|
||||
let bob_alice_chat = bob.get_chat(alice).await;
|
||||
// It's not possible yet to send to the chat, because Bob doesn't have Alice's key:
|
||||
assert_eq!(bob_alice_chat.can_send(bob).await?, false);
|
||||
assert_eq!(bob_alice_chat.id, bob_chat_id);
|
||||
|
||||
let request = bob.pop_sent_msg().await;
|
||||
|
||||
bob_chat_id.delete(bob).await?;
|
||||
|
||||
alice.recv_msg_trash(&request).await;
|
||||
let auth_required = alice.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_trash(&auth_required).await;
|
||||
|
||||
// The chat with Alice should be recreated,
|
||||
// and it should be sendable now:
|
||||
assert!(bob.get_chat(alice).await.can_send(bob).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+6
-44
@@ -19,7 +19,7 @@ use crate::log::warn;
|
||||
use crate::message::MsgId;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::{Time, inc_and_check, normalize_text, time_elapsed};
|
||||
use crate::tools::{Time, inc_and_check, time_elapsed};
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
|
||||
const DBVERSION: i32 = 68;
|
||||
@@ -1456,52 +1456,14 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
|
||||
|
||||
inc_and_check(&mut migration_version, 143)?;
|
||||
if dbversion < migration_version {
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
t.execute_batch(
|
||||
"
|
||||
sql.execute_migration(
|
||||
"
|
||||
ALTER TABLE chats ADD COLUMN name_normalized TEXT;
|
||||
ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
|
||||
",
|
||||
)?;
|
||||
|
||||
let mut stmt = t.prepare("UPDATE chats SET name_normalized=? WHERE id=?")?;
|
||||
for res in t
|
||||
.prepare("SELECT id, name FROM chats LIMIT 10000")?
|
||||
.query_map((), |row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
Ok((id, name))
|
||||
})?
|
||||
{
|
||||
let (id, name) = res?;
|
||||
if let Some(name_normalized) = normalize_text(&name) {
|
||||
stmt.execute((name_normalized, id))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut stmt = t.prepare("UPDATE contacts SET name_normalized=? WHERE id=?")?;
|
||||
for res in t
|
||||
.prepare(
|
||||
"
|
||||
SELECT id, IIF(name='', authname, name) FROM contacts
|
||||
ORDER BY last_seen DESC LIMIT 10000
|
||||
",
|
||||
)?
|
||||
.query_map((), |row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
Ok((id, name))
|
||||
})?
|
||||
{
|
||||
let (id, name) = res?;
|
||||
if let Some(name_normalized) = normalize_text(&name) {
|
||||
stmt.execute((name_normalized, id))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
sql.execute_migration_transaction(trans_fn, migration_version)
|
||||
.await?;
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
|
||||
+32
-9
@@ -2,23 +2,28 @@
|
||||
use crate::{context::Context, message::MsgId};
|
||||
use anyhow::Result;
|
||||
use humansize::{BINARY, format_size};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Storage Usage Report
|
||||
/// Useful for debugging space usage problems in the deltachat database.
|
||||
#[derive(Debug)]
|
||||
pub struct StorageUsage {
|
||||
/// Total database size, subtract this from the backup size to estimate size of all blobs
|
||||
pub db_size: usize,
|
||||
pub db_size: u64,
|
||||
/// size and row count of the 10 biggest tables
|
||||
pub largest_tables: Vec<(String, usize, Option<usize>)>,
|
||||
pub largest_tables: Vec<(String, u64, Option<u64>)>,
|
||||
/// count and total size of status updates
|
||||
/// for the 10 webxdc apps with the most size usage in status updates
|
||||
pub largest_webxdc_data: Vec<(MsgId, usize, usize)>,
|
||||
pub largest_webxdc_data: Vec<(MsgId, u64, u64)>,
|
||||
/// Total size of all files in the blobdir
|
||||
pub blobdir_size: u64,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StorageUsage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Storage Usage:")?;
|
||||
let blobdir_size = format_size(self.blobdir_size, BINARY);
|
||||
writeln!(f, "[Blob Directory Size]: {blobdir_size}")?;
|
||||
let human_db_size = format_size(self.db_size, BINARY);
|
||||
writeln!(f, "[Database Size]: {human_db_size}")?;
|
||||
writeln!(f, "[Largest Tables]:")?;
|
||||
@@ -46,12 +51,16 @@ impl std::fmt::Display for StorageUsage {
|
||||
|
||||
/// Get storage usage information for the Context's database
|
||||
pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
let page_size: usize = ctx
|
||||
let context_clone = ctx.clone();
|
||||
let blobdir_size =
|
||||
tokio::task::spawn_blocking(move || get_blobdir_storage_usage(&context_clone));
|
||||
|
||||
let page_size: u64 = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_size", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let page_count: usize = ctx
|
||||
let page_count: u64 = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_count", ())
|
||||
.await?
|
||||
@@ -68,7 +77,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
(),
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let size: usize = row.get(1)?;
|
||||
let size: u64 = row.get(1)?;
|
||||
Ok((name, size, None))
|
||||
},
|
||||
)
|
||||
@@ -76,7 +85,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
|
||||
for row in &mut largest_tables {
|
||||
let name = &row.0;
|
||||
let row_count: Result<Option<usize>> = ctx
|
||||
let row_count: Result<Option<u64>> = ctx
|
||||
.sql
|
||||
// SECURITY: the table name comes from the db, not from the user
|
||||
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
|
||||
@@ -93,17 +102,31 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let size: usize = row.get(1)?;
|
||||
let count: usize = row.get(2)?;
|
||||
let size: u64 = row.get(1)?;
|
||||
let count: u64 = row.get(2)?;
|
||||
|
||||
Ok((msg_id, size, count))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let blobdir_size = blobdir_size.await?;
|
||||
|
||||
Ok(StorageUsage {
|
||||
db_size: page_size * page_count,
|
||||
largest_tables,
|
||||
largest_webxdc_data,
|
||||
blobdir_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns storage usage of the blob directory
|
||||
pub fn get_blobdir_storage_usage(ctx: &Context) -> u64 {
|
||||
WalkDir::new(ctx.get_blobdir())
|
||||
.max_depth(2)
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.metadata().ok())
|
||||
.filter(|metadata| metadata.is_file())
|
||||
.fold(0, |acc, m| acc + m.len())
|
||||
}
|
||||
|
||||
+11
-2
@@ -896,6 +896,15 @@ impl TestContext {
|
||||
/// If the contact does not exist yet, a new contact will be created
|
||||
/// with the correct fingerprint, but without the public key.
|
||||
pub async fn add_or_lookup_contact_no_key(&self, other: &TestContext) -> Contact {
|
||||
let contact_id = self.add_or_lookup_contact_id_no_key(other).await;
|
||||
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
|
||||
}
|
||||
|
||||
/// Returns the [`ContactId`] for the other [`TestContext`], creating it if necessary.
|
||||
///
|
||||
/// If the contact does not exist yet, a new contact will be created
|
||||
/// with the correct fingerprint, but without the public key.
|
||||
async fn add_or_lookup_contact_id_no_key(&self, other: &TestContext) -> ContactId {
|
||||
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let addr = ContactAddress::new(&primary_self_addr).unwrap();
|
||||
let fingerprint = self_fingerprint(other).await.unwrap();
|
||||
@@ -904,7 +913,7 @@ impl TestContext {
|
||||
Contact::add_or_lookup_ex(self, "", &addr, fingerprint, Origin::MailinglistAddress)
|
||||
.await
|
||||
.expect("add_or_lookup");
|
||||
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
|
||||
contact_id
|
||||
}
|
||||
|
||||
/// Returns 1:1 [`Chat`] with another account address-contact.
|
||||
@@ -935,7 +944,7 @@ impl TestContext {
|
||||
/// so may create a key-contact with a fingerprint
|
||||
/// but without the key.
|
||||
pub async fn get_chat(&self, other: &TestContext) -> Chat {
|
||||
let contact = self.add_or_lookup_contact_id(other).await;
|
||||
let contact = self.add_or_lookup_contact_id_no_key(other).await;
|
||||
|
||||
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact)
|
||||
.await
|
||||
|
||||
@@ -798,6 +798,26 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts usize to u64 without using `as`.
|
||||
///
|
||||
/// This is needed for example to convert in-memory buffer sizes
|
||||
/// to u64 type used for counting all the bytes written.
|
||||
///
|
||||
/// On 32-bit systems it is possible to have files
|
||||
/// larger than 4 GiB or write more than 4 GiB to network connection,
|
||||
/// in which case we need a 64-bit total counter,
|
||||
/// but use 32-bit usize for buffer sizes.
|
||||
///
|
||||
/// This can only break if usize has more than 64 bits
|
||||
/// and this is not the case as of 2025 and is
|
||||
/// unlikely to change for general purpose computers.
|
||||
/// See <https://github.com/rust-lang/rust/issues/30495>
|
||||
/// and <https://users.rust-lang.org/t/cant-convert-usize-to-u64/6243>
|
||||
/// and <https://github.com/rust-lang/rust/issues/106050>.
|
||||
pub(crate) fn usize_to_u64(v: usize) -> u64 {
|
||||
u64::try_from(v).unwrap_or(u64::MAX)
|
||||
}
|
||||
|
||||
/// Returns early with an error if a condition is not satisfied.
|
||||
/// In non-optimized builds, panics instead if so.
|
||||
#[macro_export]
|
||||
|
||||
Reference in New Issue
Block a user