mirror of
https://github.com/chatmail/core.git
synced 2026-10-06 05:00:17 +03:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02f2990423 | ||
|
|
5597a8bffd | ||
|
|
86a40df407 | ||
|
|
232b178220 | ||
|
|
039cc9ee6b | ||
|
|
5a882b0aca | ||
|
|
1e36fb74be | ||
|
|
ebf1c87483 | ||
|
|
172ebc08f9 | ||
|
|
eeb9337a56 | ||
|
|
a1eb593d81 | ||
|
|
7e070efc28 | ||
|
|
5d3145d165 | ||
|
|
1605b971ba | ||
|
|
22578ea4b6 | ||
|
|
076f83f320 | ||
|
|
8ab98019a7 |
@@ -23,7 +23,7 @@ env:
|
||||
RUST_VERSION: 1.98.1
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
MSRV: 1.91.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
|
||||
Generated
+1301
-1384
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -3,7 +3,7 @@ name = "deltachat"
|
||||
version = "2.63.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
rust-version = "1.91"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -66,8 +66,8 @@ humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.16"
|
||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
iroh-gossip = { version = "0.101.0", default-features = false, features = ["net"] }
|
||||
iroh = { version = "1.3.0", default-features = false, features = ["tls-ring"] }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.5.0", default-features = false }
|
||||
@@ -100,7 +100,7 @@ tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["ring", "tls12", "brotli"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.3", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
|
||||
@@ -21,9 +21,9 @@ fn main() {
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
||||
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
||||
prefix = option_env!("PREFIX").unwrap_or_else(|| "/usr/local"),
|
||||
libdir = option_env!("LIBDIR").unwrap_or_else(|| "/usr/local/lib"),
|
||||
includedir = option_env!("INCLUDEDIR").unwrap_or_else(|| "/usr/local/include"),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
|
||||
@@ -27,55 +27,6 @@ use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
@@ -304,7 +255,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
============================================="
|
||||
),
|
||||
@@ -444,15 +394,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
const IMEX_COMMANDS: [&str; 9] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -156,7 +156,6 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -90,6 +91,30 @@ def test_qr_securejoin(acf):
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_qr_securejoin_request_deleted_on_all_transports(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice_chat = alice.create_group("Group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
alice2 = alice.clone()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
alice_chat.remove_contact(bob)
|
||||
# Stop and start io in order to wait until the remove message is sent out.
|
||||
alice.stop_io()
|
||||
alice.bring_online()
|
||||
|
||||
# Membership timestamps have a resolution of one second.
|
||||
time.sleep(1)
|
||||
# By now, the securejoin message sent by Bob must be deleted on Alice's relays.
|
||||
# Otherwise, alice2 would execute securejoin again and re-add Bob.
|
||||
alice2.bring_online()
|
||||
alice2_chat = alice2.get_chat_by_id(alice_chat.id)
|
||||
assert alice2.create_contact(bob) not in alice2_chat.get_contacts()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
|
||||
@@ -17,7 +17,7 @@ futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["io-std"] }
|
||||
tokio = { workspace = true, features = ["io-std", "signal"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
@@ -7,49 +7,14 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
|
||||
"RUSTSEC-2023-0071",
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# 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",
|
||||
|
||||
# Unmaintained proc-macro-error2
|
||||
# Transitive dependency of typescript-type-def 0.5.13.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0173>
|
||||
"RUSTSEC-2026-0173",
|
||||
|
||||
# rustls-webpki v0.102.8
|
||||
# We cannot upgrade to >=0.103.10 because
|
||||
# it is a transitive dependency of iroh 0.35.0
|
||||
# which depends on ^0.102.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0098>
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
|
||||
"RUSTSEC-2026-0049",
|
||||
"RUSTSEC-2026-0098",
|
||||
"RUSTSEC-2026-0099",
|
||||
|
||||
# Panic in CRL signature checks.
|
||||
# We do not check CRL and cannot update rustls-webpki 0.102.8
|
||||
# which is a dependency of iroh 0.35.0.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0104>
|
||||
"RUSTSEC-2026-0104",
|
||||
|
||||
# hickory-proto 0.25.2 unbounded loop in DNSSEC code.
|
||||
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0118>
|
||||
"RUSTSEC-2026-0118",
|
||||
|
||||
# hickory-proto 0.25.2 quadratic complexity issue.
|
||||
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0119>
|
||||
"RUSTSEC-2026-0119",
|
||||
|
||||
# Timing side channel in ml-dsa dependency of rPGP.
|
||||
# We enable PQC for encryption rather than signatures.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2025-0144>
|
||||
@@ -63,40 +28,62 @@ ignore = [
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "constant_time_eq", version = "0.3.1" },
|
||||
{ name = "base16ct", version = "0.2.0" },
|
||||
{ name = "base64", version = "0.22.1" },
|
||||
{ name = "block-buffer", version = "0.10.4" },
|
||||
{ name = "chacha20", version = "0.9.1" },
|
||||
{ name = "const-oid", version = "0.9.6" },
|
||||
{ name = "convert_case", version = "0.5.0" },
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ name = "cpufeatures", version = "0.2.17" },
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "crypto-common", version = "0.1.6" },
|
||||
{ name = "curve25519-dalek", version = "4.1.3" },
|
||||
{ name = "der", version = "0.7.9" },
|
||||
{ name = "digest", version = "0.10.7" },
|
||||
{ name = "ed25519-dalek", version = "2.1.1" },
|
||||
{ name = "ed25519", version = "2.2.3" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "fiat-crypto", version = "0.2.9" },
|
||||
{ name = "foldhash", version = "0.1.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "getrandom", version = "0.3.3" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hashbrown", version = "0.15.4" },
|
||||
{ name = "hybrid-array", version = "0.2.3" },
|
||||
{ name = "hybrid-array", version = "0.3.1" },
|
||||
{ name = "jni-sys", version = "0.3.1" },
|
||||
{ name = "jni", version = "0.21.1" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "lru", version = "0.12.5" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "netdev", version = "0.45.1" },
|
||||
{ name = "netlink-packet-core", version = "0.8.2" },
|
||||
{ name = "netlink-packet-route", version = "0.31.0" },
|
||||
{ name = "netlink-sys", version = "0.8.8" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "openssl-probe", version = "0.1.6" },
|
||||
{ name = "pem-rfc7468", version = "0.7.0" },
|
||||
{ name = "pkcs8", version = "0.10.2" },
|
||||
{ name = "quick-xml", version = "0.41.0" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand_core", version = "0.9.3" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "rand", version = "0.9.4" },
|
||||
{ name = "r-efi", version = "5.2.0" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "rustls-webpki", version = "0.102.8" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "snafu-derive", version = "0.8.5" },
|
||||
{ name = "snafu", version = "0.8.5" },
|
||||
{ name = "serdect", version = "0.3.0" },
|
||||
{ name = "sha2", version = "0.10.9"},
|
||||
{ name = "signature", version = "2.2.0"},
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "spki", version = "0.7.3"},
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "syn", version = "2.0.118" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_datetime", version = "0.6.11" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "webpki-roots", version = "0.26.8" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
{ name = "windows_aarch64_msvc" },
|
||||
@@ -113,6 +100,8 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "zerocopy-derive", version = "0.7.35" },
|
||||
{ name = "zerocopy", version = "0.7.35" },
|
||||
]
|
||||
|
||||
|
||||
@@ -124,6 +113,7 @@ allow = [
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0", # Boost Software License 1.0
|
||||
"CC0-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
|
||||
+3
-1
@@ -34,6 +34,8 @@ const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||
"chat.nuvon.app",
|
||||
"chat.tinydispatch.org",
|
||||
"chat.vim.wtf",
|
||||
"chatmail.au",
|
||||
"chatmail.cc",
|
||||
"chatmail.uk",
|
||||
"chtml.ca",
|
||||
"deltachat.me",
|
||||
@@ -163,7 +165,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
for _ in 0..NUM_TRANSPORTS_TARGET {
|
||||
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
|
||||
context
|
||||
.set_config_internal(Config::AutorelayFinished, config::from_bool(true))
|
||||
.set_config_internal(Config::AutorelayFinished, Some(config::from_bool(true)))
|
||||
.await?;
|
||||
|
||||
return Ok(relay_added);
|
||||
|
||||
+3
-3
@@ -164,9 +164,9 @@ impl<'a> BlobObject<'a> {
|
||||
/// you want to create a [BlobObject] for a filename read from the
|
||||
/// database.
|
||||
pub fn from_name(context: &'a Context, name: &str) -> Result<BlobObject<'a>> {
|
||||
let name = match name.starts_with("$BLOBDIR/") {
|
||||
true => name.splitn(2, '/').last().unwrap(),
|
||||
false => name,
|
||||
let name = match name.strip_prefix("$BLOBDIR/") {
|
||||
Some(name) => name,
|
||||
None => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {name}"));
|
||||
|
||||
+11
-4
@@ -3639,7 +3639,7 @@ pub(crate) async fn create_out_broadcast_ext(
|
||||
)?;
|
||||
ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}");
|
||||
let mut params: Params = Params::new();
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time())?;
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time());
|
||||
|
||||
t.execute(
|
||||
"INSERT INTO chats
|
||||
@@ -4795,12 +4795,14 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
///
|
||||
/// Optional `label` can be provided to ensure that message is added only once.
|
||||
/// If `important` is true, a notification will be sent.
|
||||
/// `timestamp_sent` is the time shown on the message; it does not affect ordering.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn add_device_msg_with_importance(
|
||||
context: &Context,
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
important: bool,
|
||||
timestamp_sent: i64,
|
||||
) -> Result<MsgId> {
|
||||
ensure!(
|
||||
label.is_some() || msg.is_some(),
|
||||
@@ -4820,11 +4822,10 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let timestamp_sent = time();
|
||||
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
msg.timestamp_sort = timestamp_sent;
|
||||
msg.timestamp_sort = time();
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
|
||||
&& msg.timestamp_sort <= last_msg_time
|
||||
{
|
||||
@@ -4892,7 +4893,7 @@ pub async fn add_device_msg(
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
) -> Result<MsgId> {
|
||||
add_device_msg_with_importance(context, label, msg, false).await
|
||||
add_device_msg_with_importance(context, label, msg, false, time()).await
|
||||
}
|
||||
|
||||
/// Returns true if device message with a given label was ever added to the device chat.
|
||||
@@ -4920,6 +4921,12 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
|
||||
.await?;
|
||||
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
|
||||
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
|
||||
context
|
||||
|
||||
+14
-3
@@ -336,6 +336,17 @@ pub enum Config {
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// ID of the device message added after backup transfer.
|
||||
///
|
||||
/// If the message does not exist on the first housekeeping
|
||||
/// after `READD_BACKUP_TRANSFER_MSG_DELAY`, it is re-added.
|
||||
BackupTransferMsgId,
|
||||
|
||||
/// Timestamp of the device message added after backup transfer.
|
||||
///
|
||||
/// Needed to check against `READD_BACKUP_TRANSFER_MSG_DELAY`.
|
||||
BackupTransferTimestamp,
|
||||
|
||||
/// Timestamp of the last time accumulated broadcast channel reactions were sent
|
||||
LastReactionsBroadcast,
|
||||
|
||||
@@ -809,7 +820,7 @@ impl Context {
|
||||
|
||||
/// Set the given config to a boolean value.
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, from_bool(value)).await?;
|
||||
self.set_config(key, Some(from_bool(value))).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -830,8 +841,8 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
|
||||
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
|
||||
Some(if val { "1" } else { "0" })
|
||||
pub(crate) fn from_bool(val: bool) -> &'static str {
|
||||
if val { "1" } else { "0" }
|
||||
}
|
||||
|
||||
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
|
||||
|
||||
@@ -29,7 +29,7 @@ impl ServerParams {
|
||||
if self.username.is_empty() {
|
||||
vec![Self {
|
||||
username: addr.to_string(),
|
||||
..self.clone()
|
||||
..self
|
||||
}]
|
||||
} else {
|
||||
vec![self]
|
||||
|
||||
@@ -294,6 +294,8 @@ async fn test_get_info_completeness() {
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"backup_transfer_msg_id",
|
||||
"backup_transfer_timestamp",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
|
||||
+2
-2
@@ -1454,7 +1454,7 @@ impl Session {
|
||||
/// or flags have been changed.
|
||||
/// In this case we may want to skip next IDLE and do a round
|
||||
/// of fetching new messages and synchronizing seen flags.
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> bool {
|
||||
use UnsolicitedResponse::*;
|
||||
use async_imap::imap_proto::Response;
|
||||
use async_imap::imap_proto::ResponseCode;
|
||||
@@ -1499,7 +1499,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(should_refetch)
|
||||
should_refetch
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl Session {
|
||||
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
if self.drain_unsolicited_responses(context) {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::tools::{
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub(crate) use transfer::maybe_readd_backup_transfer_msg;
|
||||
pub use transfer::{BackupProvider, get_backup};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
|
||||
+104
-20
@@ -30,7 +30,6 @@ use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, format_err};
|
||||
use futures_lite::FutureExt;
|
||||
@@ -40,21 +39,68 @@ use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::EventType;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::key;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{TempPathGuard, create_id};
|
||||
use crate::tools::{TempPathGuard, create_id, time};
|
||||
|
||||
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
|
||||
|
||||
/// ALPN protocol identifier for the backup transfer protocol.
|
||||
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
|
||||
/// Minimum time after a backup transfer before we check
|
||||
/// whether the "second device added" device message still exists.
|
||||
///
|
||||
/// The check is done in the first housekeeping after this delay;
|
||||
/// if the message was deleted until then, it is re-added with the original timestamp.
|
||||
/// After the check, the user can delete the message as usual without it being re-added.
|
||||
///
|
||||
/// This makes it easier to spot unwanted "add second device" actions.
|
||||
/// It is clear that this does not catch all eventualities, it is best-effort.
|
||||
/// First line of defense is device locking and asking for secret explicitly before adding a second device.
|
||||
const READD_BACKUP_TRANSFER_MSG_DELAY: i64 = 60 * 60;
|
||||
|
||||
/// Checks `Config::BackupTransferMsgId`
|
||||
/// and re-adds the backup transfer device message if it does not exist and some time passed.
|
||||
pub(crate) async fn maybe_readd_backup_transfer_msg(context: &Context) -> Result<()> {
|
||||
let Some(msg_id) = context
|
||||
.get_config_parsed::<u32>(Config::BackupTransferMsgId)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let timestamp = context
|
||||
.get_config_i64(Config::BackupTransferTimestamp)
|
||||
.await?;
|
||||
if time() < timestamp.saturating_add(READD_BACKUP_TRANSFER_MSG_DELAY) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
if Message::load_from_db_optional(context, MsgId::new(msg_id))
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(context));
|
||||
add_device_msg_with_importance(context, None, Some(&mut msg), false, timestamp).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provide or send a backup of this device.
|
||||
///
|
||||
/// This creates a backup of the current device and starts a service which offers another
|
||||
@@ -71,7 +117,7 @@ pub struct BackupProvider {
|
||||
_endpoint: Endpoint,
|
||||
|
||||
/// iroh address.
|
||||
node_addr: iroh::NodeAddr,
|
||||
node_addr: iroh::EndpointAddr,
|
||||
|
||||
/// Authentication token that should be submitted
|
||||
/// to retrieve the backup.
|
||||
@@ -98,14 +144,13 @@ impl BackupProvider {
|
||||
pub async fn prepare(context: &Context) -> Result<Self> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
let endpoint = Box::pin(
|
||||
Endpoint::builder()
|
||||
.tls_x509() // For compatibility with iroh <0.34.0
|
||||
Endpoint::builder(iroh::endpoint::presets::Minimal)
|
||||
.alpns(vec![BACKUP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
.bind(),
|
||||
)
|
||||
.await?;
|
||||
let node_addr = endpoint.node_addr().await?;
|
||||
let node_addr = endpoint.addr();
|
||||
|
||||
// Acquire global "ongoing" mutex.
|
||||
let cancel_token = context.alloc_ongoing().await?;
|
||||
@@ -172,7 +217,7 @@ impl BackupProvider {
|
||||
|
||||
async fn handle_connection(
|
||||
context: Context,
|
||||
conn: iroh::endpoint::Connecting,
|
||||
conn: iroh::endpoint::Accepting,
|
||||
auth_token: String,
|
||||
dbfile: Arc<TempPathGuard>,
|
||||
) -> Result<()> {
|
||||
@@ -212,8 +257,23 @@ impl BackupProvider {
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let timestamp = time();
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
let msg_id =
|
||||
add_device_msg_with_importance(&context, None, Some(&mut msg), false, timestamp)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferTimestamp,
|
||||
Some(×tamp.to_string()),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferMsgId,
|
||||
Some(&msg_id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -303,18 +363,14 @@ impl Future for BackupProvider {
|
||||
|
||||
pub async fn get_backup2(
|
||||
context: &Context,
|
||||
node_addr: iroh::NodeAddr,
|
||||
node_addr: iroh::EndpointAddr,
|
||||
auth_token: String,
|
||||
) -> Result<()> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
|
||||
let mut transport_config = iroh::endpoint::TransportConfig::default();
|
||||
transport_config.max_idle_timeout(Some(Duration::from_secs(60).try_into()?));
|
||||
let endpoint = Box::pin(
|
||||
Endpoint::builder()
|
||||
.tls_x509() // For compatibility with iroh <0.34.0
|
||||
Endpoint::builder(iroh::endpoint::presets::Minimal)
|
||||
.relay_mode(relay_mode)
|
||||
.transport_config(transport_config)
|
||||
.bind(),
|
||||
)
|
||||
.await?;
|
||||
@@ -362,7 +418,7 @@ pub async fn get_backup2(
|
||||
/// This is a long running operation which will return only when completed.
|
||||
///
|
||||
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
|
||||
/// does avoid having [`iroh::NodeAddr`] in the primary API however, without
|
||||
/// does avoid having [`iroh::EndpointAddr`] in the primary API however, without
|
||||
/// having to revert to untyped bytes.
|
||||
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
match qr {
|
||||
@@ -393,14 +449,17 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::message::Viewtype;
|
||||
use crate::chat::{ChatId, ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{Viewtype, delete_msgs};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_receive() {
|
||||
async fn test_send_receive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Create first device.
|
||||
@@ -472,6 +531,31 @@ mod tests {
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
|
||||
// When deleting the backup transfer message on the sending device,
|
||||
// it is re-added once on housekeeping,
|
||||
// but not within the first hour after the transfer.
|
||||
let device_chat_id = ChatId::get_for_contact(&ctx0, ContactId::DEVICE).await?;
|
||||
let original_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_eq!(original_msg.text, backup_transfer_msg_body(&ctx0));
|
||||
delete_msgs(&ctx0, &[original_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
assert!(get_chat_msgs(&ctx0, device_chat_id).await?.is_empty()); // re-adding not done in first hour
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60 * 60));
|
||||
housekeeping(&ctx0).await?;
|
||||
let readded_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_ne!(readded_msg.id, original_msg.id);
|
||||
assert_eq!(readded_msg.get_text(), original_msg.get_text());
|
||||
assert_eq!(readded_msg.get_timestamp(), original_msg.get_timestamp());
|
||||
assert!(readded_msg.get_sort_timestamp() > original_msg.get_sort_timestamp());
|
||||
|
||||
delete_msgs(&ctx0, &[readded_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
let device_msgs = get_chat_msgs(&ctx0, device_chat_id).await?;
|
||||
assert!(device_msgs.is_empty()); // re-adding is done only once
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that trying to accidentally overwrite a profile
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let mut buf = Vec::new();
|
||||
self.to_writer(&mut buf).unwrap();
|
||||
self.to_writer(&mut buf)
|
||||
.expect("Writing to Vec<u8> cannot fail");
|
||||
buf
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied,
|
||||
clippy::manual_is_variant_and
|
||||
clippy::manual_is_variant_and,
|
||||
clippy::unnecessary_wraps
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
|
||||
+13
-8
@@ -580,7 +580,7 @@ impl Message {
|
||||
|
||||
if let Some(msg) = &mut msg {
|
||||
msg.additional_text =
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param)?;
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param);
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
@@ -618,7 +618,7 @@ impl Message {
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
param: &Params,
|
||||
) -> Result<String> {
|
||||
) -> String {
|
||||
if download_state != DownloadState::Done {
|
||||
let file_size = param
|
||||
.get(Param::PostMessageFileBytes)
|
||||
@@ -635,14 +635,14 @@ impl Message {
|
||||
.unwrap_or("?".to_owned());
|
||||
|
||||
return match viewtype {
|
||||
Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
|
||||
Viewtype::File => format!(" [{file_name} – {file_size}]"),
|
||||
_ => {
|
||||
let translated_viewtype = viewtype.to_locale_string(context);
|
||||
Ok(format!(" [{translated_viewtype} – {file_size}]"))
|
||||
format!(" [{translated_viewtype} – {file_size}]")
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(String::new())
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Returns the MIME type of an attached file if it exists.
|
||||
@@ -2017,14 +2017,19 @@ pub(crate) async fn set_msg_failed(
|
||||
|
||||
/// Inserts a tombstone into `msgs` table
|
||||
/// to prevent downloading the same message in the future.
|
||||
/// With `on_server`, copies seen on IMAP later are deleted on the server.
|
||||
///
|
||||
/// Returns tombstone database row ID.
|
||||
pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
|
||||
pub(crate) async fn insert_tombstone(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
on_server: bool,
|
||||
) -> Result<MsgId> {
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, ChatId::TRASH),
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
|
||||
(rfc724_mid, ChatId::TRASH, on_server),
|
||||
)
|
||||
.await?;
|
||||
let msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
|
||||
@@ -784,7 +784,7 @@ async fn test_get_existing_msg_ids() -> Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_fail() -> Result<()> {
|
||||
fn test_can_fail() {
|
||||
use MessageState::*;
|
||||
|
||||
// states that are not allowed to transition to OutFailed
|
||||
@@ -799,6 +799,4 @@ fn test_can_fail() -> Result<()> {
|
||||
assert!(OutPending.can_fail());
|
||||
assert!(OutDelivered.can_fail());
|
||||
assert!(OutFailed.can_fail());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-1
@@ -1856,7 +1856,7 @@ impl MimeFactory {
|
||||
|
||||
// We should not send `null` as relay URL
|
||||
// as this is the only way to reach the node.
|
||||
debug_assert!(node_addr.relay_url().is_some());
|
||||
debug_assert_eq!(node_addr.relay_urls().count(), 1);
|
||||
headers.push((
|
||||
HeaderDef::IrohNodeAddr.into(),
|
||||
mail_builder::headers::text::Text::new(serde_json::to_string(&node_addr)?)
|
||||
|
||||
@@ -795,7 +795,7 @@ async fn test_protected_headers_directive() -> Result<()> {
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presence of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", b"content", None)?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
|
||||
@@ -1127,7 +1127,7 @@ async fn test_render_unencrypted_msg_with_attachment() -> Result<()> {
|
||||
.await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text("Hello!".to_string());
|
||||
msg.set_file_from_bytes(alice, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(alice, "foo.bar", b"content", None)?;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let unencrypted = normalized_payload(sent).await;
|
||||
|
||||
|
||||
+2
-6
@@ -294,11 +294,7 @@ impl MimeMessage {
|
||||
&mut wants_mdn,
|
||||
&mail,
|
||||
);
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| is_hidden(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
);
|
||||
headers_removed.extend(headers.extract_if(|k, _v| is_hidden(k)).map(|(k, _v)| k));
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -1751,7 +1747,7 @@ impl MimeMessage {
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| has_header_protection || is_protected(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
.map(|(k, _v)| k),
|
||||
);
|
||||
|
||||
if has_header_protection {
|
||||
|
||||
+80
-69
@@ -19,18 +19,22 @@
|
||||
//! This message contains the users relay-server and public key.
|
||||
//! Direct IP address is not included as this information can be persisted by email providers.
|
||||
//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
|
||||
//! 5. Upon receiving an announcement message, other peers store the sender's [NodeAddr] in the database
|
||||
//! 5. Upon receiving an announcement message, other peers store the sender's [EndpointAddr] in the database
|
||||
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
|
||||
//! and `joinRealtimeChannel().send()` just like the other peers.
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
|
||||
use iroh_gossip::net::{Event, GOSSIP_ALPN, Gossip, GossipEvent, JoinOptions};
|
||||
use iroh::address_lookup::MemoryLookup;
|
||||
use iroh::{
|
||||
Endpoint, EndpointAddr, EndpointId, PublicKey, RelayMode, RelayUrl, SecretKey, TransportAddr,
|
||||
};
|
||||
use iroh_gossip::api::{Event as GossipEvent, GossipReceiver, GossipSender, JoinOptions};
|
||||
use iroh_gossip::net::{GOSSIP_ALPN, Gossip};
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -46,7 +50,7 @@ use crate::mimeparser::SystemMessage;
|
||||
|
||||
/// The length of an ed25519 `PublicKey`, in bytes.
|
||||
const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
const PUBLIC_KEY_STUB: &[u8] = b"static_string";
|
||||
|
||||
/// Store Iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
@@ -54,6 +58,9 @@ pub struct Iroh {
|
||||
/// Iroh router needed for Iroh peer channels.
|
||||
pub(crate) router: iroh::protocol::Router,
|
||||
|
||||
/// Address lookup, called "Discovery service" before Iroh 0.96.0.
|
||||
pub(crate) address_lookup: MemoryLookup,
|
||||
|
||||
/// [Gossip] needed for Iroh peer channels.
|
||||
pub(crate) gossip: Gossip,
|
||||
|
||||
@@ -105,7 +112,7 @@ impl Iroh {
|
||||
}
|
||||
|
||||
let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
|
||||
let node_ids = peers.iter().map(|p| p.node_id).collect::<Vec<_>>();
|
||||
let node_ids = peers.iter().map(|p| p.id).collect::<Vec<_>>();
|
||||
|
||||
info!(
|
||||
ctx,
|
||||
@@ -115,7 +122,7 @@ impl Iroh {
|
||||
// Inform iroh of potentially new node addresses
|
||||
for node_addr in &peers {
|
||||
if !node_addr.is_empty() {
|
||||
self.router.endpoint().add_node_addr(node_addr.clone())?;
|
||||
self.address_lookup.add_endpoint_info(node_addr.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +131,7 @@ impl Iroh {
|
||||
let (gossip_sender, gossip_receiver) = self
|
||||
.gossip
|
||||
.subscribe_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
|
||||
.await?
|
||||
.split();
|
||||
|
||||
let ctx = ctx.clone();
|
||||
@@ -139,10 +147,10 @@ impl Iroh {
|
||||
}
|
||||
|
||||
/// Add gossip peer to realtime channel if it is already active.
|
||||
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
|
||||
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: EndpointAddr) -> Result<()> {
|
||||
if self.iroh_channels.read().await.get(&topic).is_some() {
|
||||
self.router.endpoint().add_node_addr(peer.clone())?;
|
||||
self.gossip.subscribe(topic, vec![peer.node_id])?;
|
||||
self.address_lookup.add_endpoint_info(peer.clone());
|
||||
self.gossip.subscribe(topic, vec![peer.id]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -184,16 +192,20 @@ impl Iroh {
|
||||
*entry
|
||||
}
|
||||
|
||||
/// Get the iroh [NodeAddr] without direct IP addresses.
|
||||
/// Get the iroh [EndpointAddr] without direct IP addresses.
|
||||
///
|
||||
/// The address is guaranteed to have home relay URL set
|
||||
/// as it is the only way to reach the node
|
||||
/// without global discovery mechanisms.
|
||||
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
|
||||
let mut addr = self.router.endpoint().node_addr().await?;
|
||||
addr.direct_addresses = BTreeSet::new();
|
||||
debug_assert!(addr.relay_url().is_some());
|
||||
Ok(addr)
|
||||
pub(crate) async fn get_node_addr(&self) -> Result<EndpointAddr> {
|
||||
// Wait until home relay connection is established.
|
||||
self.router.endpoint().online().await;
|
||||
let mut endpoint_addr = self.router.endpoint().addr();
|
||||
endpoint_addr
|
||||
.addrs
|
||||
.retain(|addr| matches!(addr, TransportAddr::Relay(_)));
|
||||
debug_assert_eq!(endpoint_addr.addrs.len(), 1);
|
||||
Ok(endpoint_addr)
|
||||
}
|
||||
|
||||
/// Leave the realtime channel for a given topic.
|
||||
@@ -219,11 +231,11 @@ pub(crate) struct ChannelState {
|
||||
/// The subscribe loop handle.
|
||||
subscribe_loop: JoinHandle<()>,
|
||||
|
||||
sender: iroh_gossip::net::GossipSender,
|
||||
sender: GossipSender,
|
||||
}
|
||||
|
||||
impl ChannelState {
|
||||
fn new(subscribe_loop: JoinHandle<()>, sender: iroh_gossip::net::GossipSender) -> Self {
|
||||
fn new(subscribe_loop: JoinHandle<()>, sender: GossipSender) -> Self {
|
||||
Self {
|
||||
subscribe_loop,
|
||||
sender,
|
||||
@@ -235,7 +247,7 @@ impl Context {
|
||||
/// Create iroh endpoint and gossip.
|
||||
async fn init_peer_channels(&self) -> Result<Iroh> {
|
||||
info!(self, "Initializing peer channels.");
|
||||
let secret_key = SecretKey::generate(rand_old::rngs::OsRng);
|
||||
let secret_key = SecretKey::generate();
|
||||
let public_key = secret_key.public();
|
||||
|
||||
let relay_mode = if let Some(relay_url) = self
|
||||
@@ -253,9 +265,10 @@ impl Context {
|
||||
RelayMode::Default
|
||||
};
|
||||
|
||||
let address_lookup = MemoryLookup::new();
|
||||
let endpoint = Box::pin(
|
||||
Endpoint::builder()
|
||||
.tls_x509() // For compatibility with iroh <0.34.0
|
||||
Endpoint::builder(iroh::endpoint::presets::Minimal)
|
||||
.address_lookup(address_lookup.clone())
|
||||
.secret_key(secret_key)
|
||||
.alpns(vec![GOSSIP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
@@ -270,8 +283,7 @@ impl Context {
|
||||
|
||||
let gossip = Gossip::builder()
|
||||
.max_message_size(128 * 1024)
|
||||
.spawn(endpoint.clone())
|
||||
.await?;
|
||||
.spawn(endpoint.clone());
|
||||
|
||||
let router = iroh::protocol::Router::builder(endpoint)
|
||||
.accept(GOSSIP_ALPN, gossip.clone())
|
||||
@@ -279,6 +291,7 @@ impl Context {
|
||||
|
||||
Ok(Iroh {
|
||||
router,
|
||||
address_lookup,
|
||||
gossip,
|
||||
sequence_numbers: Mutex::new(HashMap::new()),
|
||||
iroh_channels: RwLock::new(HashMap::new()),
|
||||
@@ -325,11 +338,15 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
|
||||
pub(crate) async fn maybe_add_gossip_peer(
|
||||
&self,
|
||||
topic: TopicId,
|
||||
peer: EndpointAddr,
|
||||
) -> Result<()> {
|
||||
if let Some(iroh) = &*self.iroh.read().await {
|
||||
info!(
|
||||
self,
|
||||
"Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
|
||||
"Adding (maybe existing) peer with id {} to {topic}.", peer.id
|
||||
);
|
||||
iroh.maybe_add_gossip_peer(topic, peer).await?;
|
||||
}
|
||||
@@ -337,12 +354,12 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache a peers [NodeId] for one topic.
|
||||
/// Cache a peers [EndpointId] for one topic.
|
||||
pub(crate) async fn iroh_add_peer_for_topic(
|
||||
ctx: &Context,
|
||||
msg_id: MsgId,
|
||||
topic: TopicId,
|
||||
peer: NodeId,
|
||||
peer: EndpointId,
|
||||
relay_server: Option<&str>,
|
||||
) -> Result<()> {
|
||||
ctx.sql
|
||||
@@ -368,11 +385,11 @@ pub async fn add_gossip_peer_from_header(
|
||||
}
|
||||
|
||||
let node_addr =
|
||||
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
|
||||
serde_json::from_str::<EndpointAddr>(node_addr).context("Failed to parse node address")?;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
|
||||
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.id
|
||||
);
|
||||
|
||||
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
|
||||
@@ -387,8 +404,8 @@ pub async fn add_gossip_peer_from_header(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let node_id = node_addr.node_id;
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
let node_id = node_addr.id;
|
||||
let relay_server = node_addr.relay_urls().map(|relay| relay.as_str()).next();
|
||||
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
|
||||
|
||||
context.maybe_add_gossip_peer(topic, node_addr).await?;
|
||||
@@ -406,8 +423,8 @@ pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: Topic
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a list of [NodeAddr]s for one webxdc.
|
||||
async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeAddr>> {
|
||||
/// Get a list of [EndpointAddr]s for one webxdc.
|
||||
async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<EndpointAddr>> {
|
||||
ctx.sql
|
||||
.query_map(
|
||||
"SELECT public_key, relay_server FROM iroh_gossip_peers WHERE msg_id = ? AND public_key != ?",
|
||||
@@ -420,11 +437,11 @@ async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeA
|
||||
|g| {
|
||||
g.map(|data| {
|
||||
let (key, server) = data?;
|
||||
let server = server.map(|data| Ok::<_, url::ParseError>(RelayUrl::from(Url::parse(&data)?))).transpose()?;
|
||||
let id = NodeId::from_bytes(&key.try_into()
|
||||
let server: Option<TransportAddr> = server.map(|data| Ok::<_, url::ParseError>(TransportAddr::Relay(RelayUrl::from(Url::parse(&data)?)))).transpose()?;
|
||||
let id = EndpointId::from_bytes(&key.try_into()
|
||||
.map_err(|_| anyhow!("Can't convert sql data to [u8; 32]"))?)?;
|
||||
Ok::<_, anyhow::Error>(NodeAddr::from_parts(
|
||||
id, server, vec![]
|
||||
Ok::<_, anyhow::Error>(EndpointAddr::from_parts(
|
||||
id, server
|
||||
))
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
@@ -539,45 +556,39 @@ pub(crate) fn iroh_topic_from_str(topic: &str) -> Result<TopicId> {
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn subscribe_loop(
|
||||
context: &Context,
|
||||
mut stream: iroh_gossip::net::GossipReceiver,
|
||||
mut stream: GossipReceiver,
|
||||
topic: TopicId,
|
||||
msg_id: MsgId,
|
||||
join_tx: oneshot::Sender<()>,
|
||||
) -> Result<()> {
|
||||
let mut join_tx = Some(join_tx);
|
||||
stream.joined().await?;
|
||||
// Try to notify that at least one peer joined,
|
||||
// but ignore the error if receiver is dropped and nobody listens.
|
||||
join_tx.send(()).ok();
|
||||
|
||||
for node in stream.neighbors() {
|
||||
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
|
||||
}
|
||||
|
||||
while let Some(event) = stream.try_next().await? {
|
||||
match event {
|
||||
Event::Gossip(event) => match event {
|
||||
GossipEvent::Joined(nodes) => {
|
||||
if let Some(join_tx) = join_tx.take() {
|
||||
// Try to notify that at least one peer joined,
|
||||
// but ignore the error if receiver is dropped and nobody listens.
|
||||
join_tx.send(()).ok();
|
||||
}
|
||||
|
||||
for node in nodes {
|
||||
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
|
||||
}
|
||||
}
|
||||
GossipEvent::NeighborUp(node) => {
|
||||
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
|
||||
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
|
||||
}
|
||||
GossipEvent::NeighborDown(_node) => {}
|
||||
GossipEvent::Received(message) => {
|
||||
info!(context, "IROH_REALTIME: Received realtime data");
|
||||
context.emit_event(EventType::WebxdcRealtimeData {
|
||||
msg_id,
|
||||
data: message
|
||||
.content
|
||||
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
|
||||
.context("too few bytes in iroh message")?
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
},
|
||||
Event::Lagged => {
|
||||
GossipEvent::NeighborUp(node) => {
|
||||
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
|
||||
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
|
||||
}
|
||||
GossipEvent::NeighborDown(_node) => {}
|
||||
GossipEvent::Received(message) => {
|
||||
info!(context, "IROH_REALTIME: Received realtime data");
|
||||
context.emit_event(EventType::WebxdcRealtimeData {
|
||||
msg_id,
|
||||
data: message
|
||||
.content
|
||||
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
|
||||
.context("too few bytes in iroh message")?
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
GossipEvent::Lagged => {
|
||||
warn!(context, "Gossip lost some messages");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ async fn test_can_communicate() {
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|addr| addr.node_id)
|
||||
.map(|addr| addr.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
@@ -67,7 +67,7 @@ async fn test_can_communicate() {
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
.id
|
||||
]
|
||||
);
|
||||
|
||||
@@ -86,14 +86,14 @@ async fn test_can_communicate() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -107,14 +107,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -130,7 +130,7 @@ async fn test_can_communicate() {
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|addr| addr.node_id)
|
||||
.map(|addr| addr.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
@@ -142,21 +142,21 @@ async fn test_can_communicate() {
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
.id
|
||||
]
|
||||
);
|
||||
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice 2".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice 2".as_bytes() {
|
||||
if data == b"bob -> alice 2" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -217,7 +217,7 @@ async fn test_duplicated_out_of_order_advertisement() -> Result<()> {
|
||||
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| addr.node_id)
|
||||
.map(|addr| addr.id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
members,
|
||||
@@ -229,7 +229,7 @@ async fn test_duplicated_out_of_order_advertisement() -> Result<()> {
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
.id
|
||||
]
|
||||
);
|
||||
bob.assert_warn("Cannot add iroh peer").await;
|
||||
@@ -282,7 +282,7 @@ async fn test_can_reconnect() {
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|addr| addr.node_id)
|
||||
.map(|addr| addr.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
@@ -295,7 +295,7 @@ async fn test_can_reconnect() {
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
.id
|
||||
]
|
||||
);
|
||||
|
||||
@@ -314,14 +314,14 @@ async fn test_can_reconnect() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -373,14 +373,14 @@ async fn test_can_reconnect() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
|
||||
+1
-1
@@ -401,7 +401,7 @@ pub fn merge_openpgp_certificates(
|
||||
// such as Alice's key in `test-data/key/alice-secret.asc`.
|
||||
let best_user: Option<SignedUser> = old_users
|
||||
.into_iter()
|
||||
.chain(new_users.clone())
|
||||
.chain(new_users)
|
||||
.filter_map(|SignedUser { id, signatures }| {
|
||||
// Select the best signature for each User ID.
|
||||
// If User ID has no valid signatures, it is filtered out.
|
||||
|
||||
@@ -384,7 +384,7 @@ fn test_merge_openpgp_certificates() {
|
||||
|
||||
// Cannot merge certificates with different primary key.
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob, alice).is_err());
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
|
||||
+5
-9
@@ -1,6 +1,6 @@
|
||||
//! Handle plain text together with some attributes.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use regex::regex;
|
||||
|
||||
use crate::simplify::remove_message_footer;
|
||||
|
||||
@@ -25,12 +25,8 @@ impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
let linkify_mail_re = regex!(r"\b([\w.\-+]+@[\w.\-]+)\b");
|
||||
let linkify_url_re = regex!(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)");
|
||||
|
||||
let lines: Vec<&str> = self.text.lines().collect();
|
||||
let (lines, _footer) = remove_message_footer(&lines);
|
||||
@@ -52,12 +48,12 @@ impl PlainText {
|
||||
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
|
||||
let line = line.to_string().replace('\r', "");
|
||||
|
||||
let mut line = LINKIFY_MAIL_RE
|
||||
let mut line = linkify_mail_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
line = LINKIFY_URL_RE
|
||||
line = linkify_url_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
mod dclogin_scheme;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
pub(crate) use dclogin_scheme::login_param_from_login_qr;
|
||||
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
|
||||
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
||||
use regex::regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::autorelay::login_param_from_host;
|
||||
@@ -143,7 +143,7 @@ pub enum Qr {
|
||||
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
|
||||
Backup2 {
|
||||
/// Iroh node address.
|
||||
node_addr: iroh::NodeAddr,
|
||||
node_addr: iroh::EndpointAddr,
|
||||
|
||||
/// Authentication token.
|
||||
auth_token: String,
|
||||
@@ -764,7 +764,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
|
||||
let addr = server_config.addr();
|
||||
let host = addr.host().to_string();
|
||||
let host = addr.host();
|
||||
let port = addr.port();
|
||||
Ok(Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
@@ -790,7 +790,7 @@ fn decode_backup2(qr: &str) -> Result<Qr> {
|
||||
.split_once('&')
|
||||
.context("Backup QR code has no separator")?;
|
||||
let auth_token = auth_token.to_string();
|
||||
let node_addr = serde_json::from_str::<iroh::NodeAddr>(node_addr)
|
||||
let node_addr = serde_json::from_str::<iroh::EndpointAddr>(node_addr)
|
||||
.context("Invalid node addr in backup QR code")?;
|
||||
|
||||
Ok(Qr::Backup2 {
|
||||
@@ -1071,16 +1071,11 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
}
|
||||
|
||||
static VCARD_NAME_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
|
||||
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
|
||||
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let name = VCARD_NAME_RE
|
||||
let name = regex!(r"(?m)^N:([^;]*);([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
@@ -1090,7 +1085,10 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
|
||||
let addr = if let Some(cap) = regex!(r"(?m)^EMAIL([^:\n]*):([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| caps.get(2))
|
||||
{
|
||||
normalize_address(cap.as_str().trim())?
|
||||
} else {
|
||||
bail!("Bad e-mail address");
|
||||
@@ -1124,7 +1122,7 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
|
||||
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
|
||||
|
||||
Ok(new_addr.to_string())
|
||||
Ok(new_addr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -120,7 +120,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
};
|
||||
|
||||
Ok(Qr::Login {
|
||||
address: addr.to_owned(),
|
||||
address: addr,
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -973,25 +973,3 @@ async fn test_decode_socks5() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that `DCBACKUP2` QR code does not fail to deserialize
|
||||
/// because iroh changes the format of `NodeAddr`
|
||||
/// as happened between iroh 0.29 and iroh 0.30 before.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_backup() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP2:TWSv6ZjDPa5eoxkocj7xMi8r&{"node_id":"9afc1ea5b4f543e5cdd7b7a21cd26aee7c0b1e1c2af26790896fbd8932a06e1e","relay_url":null,"direct_addresses":["192.168.1.10:12345"]}"#).await?;
|
||||
assert!(matches!(qr, Qr::Backup2 { .. }));
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP2:AIvFjRFBt_aMiisSZ8P33JqY&{"node_id":"buzkyd4x76w66qtanjk5fm6ikeuo4quletajowsl3a3p7l6j23pa","info":{"relay_url":null,"direct_addresses":["192.168.1.5:12345"]}}"#).await?;
|
||||
assert!(matches!(qr, Qr::Backup2 { .. }));
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP9:from-the-future"#).await?;
|
||||
assert!(matches!(qr, Qr::BackupTooNew { .. }));
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP99:far-from-the-future"#).await?;
|
||||
assert!(matches!(qr, Qr::BackupTooNew { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ async fn set_msg_id_reaction(
|
||||
.await?;
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)
|
||||
{
|
||||
chat.param
|
||||
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
|
||||
|
||||
+7
-6
@@ -483,7 +483,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
let trash = || async {
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid, false).await?];
|
||||
Ok(Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::Undefined,
|
||||
@@ -668,14 +668,15 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
match res {
|
||||
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
|
||||
let msg_id = insert_tombstone(context, rfc724_mid).await?;
|
||||
let needs_delete_job = res == securejoin::HandshakeMessage::Done;
|
||||
let msg_id = insert_tombstone(context, rfc724_mid, needs_delete_job).await?;
|
||||
received_msg = Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::InSeen,
|
||||
hidden: false,
|
||||
sort_timestamp: mime_parser.timestamp_sent,
|
||||
msg_ids: vec![msg_id],
|
||||
needs_delete_job: res == securejoin::HandshakeMessage::Done,
|
||||
needs_delete_job,
|
||||
});
|
||||
}
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
@@ -2311,7 +2312,7 @@ INSERT INTO msgs
|
||||
// This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)
|
||||
{
|
||||
// write the last subject even if empty -
|
||||
// otherwise a reply may get an outdated subject.
|
||||
@@ -2401,7 +2402,7 @@ async fn handle_edit_delete(
|
||||
let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
|
||||
insert_tombstone(context, rfc724_mid).await?;
|
||||
insert_tombstone(context, rfc724_mid, false).await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -3414,7 +3415,7 @@ async fn apply_chat_name_avatar_and_description_changes(
|
||||
&& is_from_in_chat
|
||||
&& chat
|
||||
.param
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)
|
||||
{
|
||||
info!(context, "Group-avatar change for {}.", chat.id);
|
||||
match avatar_action {
|
||||
|
||||
+14
-15
@@ -33,14 +33,14 @@ pub(crate) use qrinvite::QrInvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_').remove(b'@');
|
||||
|
||||
fn inviter_progress(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
// No other values are used.
|
||||
let progress = 1000;
|
||||
context.emit_event(EventType::SecurejoinInviterProgress {
|
||||
@@ -49,8 +49,6 @@ fn inviter_progress(
|
||||
chat_type,
|
||||
progress,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shorten name to max. `length` characters.
|
||||
@@ -124,18 +122,19 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
|
||||
|
||||
let r_param = context
|
||||
let encoded_extra_relays: Vec<String> = context
|
||||
.get_self_addrs()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|addr| *addr != self_addr)
|
||||
.reduce(|acc, addr| {
|
||||
format!(
|
||||
"{acc},{}",
|
||||
utf8_percent_encode(&addr, DISALLOWED_CHARACTERS)
|
||||
)
|
||||
})
|
||||
.map_or(String::default(), |addrs| format!("&r={addrs}"));
|
||||
.map(|addr| utf8_percent_encode(&addr, DISALLOWED_CHARACTERS).to_string())
|
||||
.collect();
|
||||
|
||||
let r_param = if encoded_extra_relays.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("&r={}", encoded_extra_relays.join(","))
|
||||
};
|
||||
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
@@ -659,7 +658,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ);
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
@@ -670,7 +669,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single);
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
}
|
||||
@@ -817,7 +816,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
// and tests which don't care about the chat ID,
|
||||
// so we pass invalid chat ID here.
|
||||
let chat_id = ChatId::new(0);
|
||||
inviter_progress(context, contact_id, chat_id, chat_type)?;
|
||||
inviter_progress(context, contact_id, chat_id, chat_type);
|
||||
}
|
||||
|
||||
if matches!(step, SecureJoinStep::MemberAdded) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::message::rfc724_mid_exists_ext;
|
||||
use crate::qr::Qr;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, messages_e2ee_info_msg};
|
||||
@@ -13,6 +14,7 @@ use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, get_chat_msg, sync,
|
||||
};
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SetupContactCase {
|
||||
@@ -1142,6 +1144,43 @@ async fn test_get_securejoin_qr_name_is_last() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that addresses in QR codes are percent-encoded.
|
||||
/// `@` should not be encoded unnecessarily,
|
||||
/// since this would just make the QR code longer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_securejoin_qr_encoding() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// `@` in email addresses must not be percent-encoded:
|
||||
add_pseudo_transport(alice, "asdf@example.org").await?;
|
||||
// But `%` does need percent-encoding:
|
||||
add_pseudo_transport(alice, "jk%l@example.net").await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
assert!(
|
||||
qr.contains("a=alice@example.org"),
|
||||
"{qr} doesn't contain 'a=alice@example.org'"
|
||||
);
|
||||
assert!(
|
||||
qr.contains("r=jk%25l@example.net,asdf@example.org"),
|
||||
"{qr} doesn't contain 'r=jk%25l@example.net,asdf@example.org'"
|
||||
);
|
||||
|
||||
let qr = check_qr(bob, &qr).await?;
|
||||
let Qr::AskVerifyContact { mut addrs, .. } = qr else {
|
||||
unreachable!()
|
||||
};
|
||||
addrs.sort();
|
||||
assert_eq!(
|
||||
addrs,
|
||||
vec!["alice@example.org", "asdf@example.org", "jk%l@example.net",]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// QR codes should not get arbitrary big because of long names.
|
||||
/// The truncation, however, should not let the url end with a `.`, which is a call for trouble in linkfiers.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1525,3 +1564,29 @@ async fn test_deduplicate_member_added() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a handled join request is also marked as "deleted"
|
||||
/// in the database, so that if a copy of the request arrives via
|
||||
/// other relays in the future, then this copy will also be deleted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_join_request_deleted_on_all_relays() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = chat::create_group(alice, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
bob.add_or_lookup_contact_id(alice).await;
|
||||
join_securejoin(bob, &qr).await?;
|
||||
let request = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&request).await;
|
||||
|
||||
let rfc724_mid = Message::load_from_db(bob, request.sender_msg_id)
|
||||
.await?
|
||||
.rfc724_mid;
|
||||
let (_, deleted) = rfc724_mid_exists_ext(alice, &rfc724_mid, "deleted=1")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(deleted);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
-2
@@ -356,7 +356,7 @@ pub(crate) async fn insert_into_smtp(
|
||||
queued_msg: &QueuedMail,
|
||||
) -> Result<()> {
|
||||
let now = tools::time();
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid).await?;
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid, false).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
|
||||
@@ -720,7 +720,7 @@ async fn send_mdn_rfc724_mid(
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid).await?;
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid, false).await?;
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
|
||||
+7
-1
@@ -14,7 +14,7 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::imex::{self, BLOBS_BACKUP_NAME};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
@@ -827,6 +827,12 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
imex::maybe_readd_backup_transfer_msg(context)
|
||||
.await
|
||||
.context("Failed to re-add backup transfer message")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
if let Err(err) = incremental_vacuum(context).await {
|
||||
warn!(context, "Failed to run incremental vacuum: {err:#}.");
|
||||
}
|
||||
|
||||
+4
-12
@@ -32,8 +32,6 @@ fn migrate_key_contacts(
|
||||
context: &Context,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
) -> std::result::Result<(), anyhow::Error> {
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 1: ===============================
|
||||
// Alter tables
|
||||
transaction.execute_batch(
|
||||
@@ -79,13 +77,12 @@ fn migrate_key_contacts(
|
||||
.optional()
|
||||
.context("Step 0")?
|
||||
else {
|
||||
info!(
|
||||
context,
|
||||
"Not yet configured, no need to migrate key-contacts"
|
||||
);
|
||||
// Not yet configured, no need to migrate key-contacts.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 2: ===============================
|
||||
// Create up to 3 new contacts for every contact that has a peerstate:
|
||||
// one from the Autocrypt key fingerprint, one from the verified key fingerprint,
|
||||
@@ -1936,14 +1933,9 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
|
||||
inc_and_check(&mut migration_version, 132)?;
|
||||
if dbversion < migration_version {
|
||||
let start = Time::now();
|
||||
sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"key-contacts migration took {:?} in total.",
|
||||
time_elapsed(&start),
|
||||
);
|
||||
|
||||
// Schedule `msgs_to_key_contacts()`.
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
|
||||
@@ -220,6 +220,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
|
||||
),
|
||||
Some(&mut msg),
|
||||
true,
|
||||
time(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
+13
-13
@@ -27,7 +27,7 @@ impl Context {
|
||||
Ok(param.parse().unwrap_or_default())
|
||||
},
|
||||
)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp);
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
@@ -57,7 +57,7 @@ impl ChatId {
|
||||
let param: String = row.get(0)?;
|
||||
Ok(param.parse().unwrap_or_default())
|
||||
})?;
|
||||
let update = param.update_timestamp(scope, new_timestamp)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp);
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE chats SET param=? WHERE id=?",
|
||||
@@ -73,13 +73,13 @@ impl ChatId {
|
||||
impl Params {
|
||||
/// Updates a param's timestamp in memory, if reasonable.
|
||||
/// Returns true if the caller shall update the settings belonging to the scope.
|
||||
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> Result<bool> {
|
||||
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> bool {
|
||||
let old_timestamp = self.get_i64(scope).unwrap_or_default();
|
||||
if new_timestamp >= old_timestamp {
|
||||
self.set_i64(scope, new_timestamp);
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
Ok(false)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,18 +96,18 @@ mod tests {
|
||||
let mut params = Params::new();
|
||||
let ts = time();
|
||||
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?);
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?); // same timestamp -> update
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts + 10)?);
|
||||
assert!(!params.update_timestamp(Param::LastSubject, ts)?); // `ts` is now too old
|
||||
assert!(!params.update_timestamp(Param::LastSubject, 0)?);
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts));
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)); // same timestamp -> update
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts + 10));
|
||||
assert!(!params.update_timestamp(Param::LastSubject, ts)); // `ts` is now too old
|
||||
assert!(!params.update_timestamp(Param::LastSubject, 0));
|
||||
assert_eq!(params.get_i64(Param::LastSubject).unwrap(), ts + 10);
|
||||
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // stay unset -> update ...
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // ... also on multiple calls
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // stay unset -> update ...
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // ... also on multiple calls
|
||||
assert_eq!(params.get_i64(Param::GroupNameTimestamp).unwrap(), 0);
|
||||
|
||||
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1)?);
|
||||
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1));
|
||||
assert_eq!(params.get_i64(Param::AvatarTimestamp), None);
|
||||
|
||||
Ok(())
|
||||
|
||||
+3
-3
@@ -350,7 +350,7 @@ impl Context {
|
||||
if let Some(ref document) = status_update_item.document
|
||||
&& instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
|
||||
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)
|
||||
{
|
||||
instance.param.set(Param::WebxdcDocument, document);
|
||||
param_changed = true;
|
||||
@@ -359,10 +359,10 @@ impl Context {
|
||||
if let Some(ref summary) = status_update_item.summary
|
||||
&& instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)
|
||||
{
|
||||
let summary = sanitize_bidi_characters(summary);
|
||||
instance.param.set(Param::WebxdcSummary, summary.clone());
|
||||
instance.param.set(Param::WebxdcSummary, summary);
|
||||
param_changed = true;
|
||||
}
|
||||
|
||||
|
||||
+12
-14
@@ -1201,30 +1201,28 @@ async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"key = syntax error"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"no_name = "no name, no icon""#)?;
|
||||
assert_eq!(manifest.name, None);
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"name = "name, no icon""#)?;
|
||||
assert_eq!(manifest.name, Some("name, no icon".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
r#"name = "foo"
|
||||
icon = "bar""#
|
||||
.as_bytes(),
|
||||
br#"name = "foo"
|
||||
icon = "bar""#,
|
||||
)?;
|
||||
assert_eq!(manifest.name, Some("foo".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
r#"name = "foz"
|
||||
br#"name = "foz"
|
||||
icon = "baz"
|
||||
add_item = "that should be just ignored"
|
||||
|
||||
[section]
|
||||
sth_for_the = "future""#
|
||||
.as_bytes(),
|
||||
sth_for_the = "future""#,
|
||||
)?;
|
||||
assert_eq!(manifest.name, Some("foz".to_string()));
|
||||
Ok(())
|
||||
@@ -1232,13 +1230,13 @@ sth_for_the = "future""#
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
||||
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"min_api = 3"#)?;
|
||||
assert_eq!(manifest.min_api, Some(3));
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"min_api = "1""#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"min_api = 1.2"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -1246,10 +1244,10 @@ async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"source_code_url = 3"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"source_code_url = "https://foo.bar""#)?;
|
||||
assert_eq!(
|
||||
manifest.source_code_url,
|
||||
Some("https://foo.bar".to_string())
|
||||
|
||||
Reference in New Issue
Block a user