mirror of
https://github.com/chatmail/core.git
synced 2026-10-05 20:50:16 +03:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38aaf52d9a | ||
|
|
4e81309315 | ||
|
|
736979b451 | ||
|
|
5a882b0aca | ||
|
|
1e36fb74be | ||
|
|
ebf1c87483 | ||
|
|
172ebc08f9 | ||
|
|
eeb9337a56 | ||
|
|
a1eb593d81 | ||
|
|
7e070efc28 | ||
|
|
5d3145d165 | ||
|
|
1605b971ba | ||
|
|
22578ea4b6 | ||
|
|
076f83f320 | ||
|
|
8ab98019a7 |
Generated
+25
-19
@@ -271,19 +271,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.11.3"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a6728e0f7931b36d725ac234fcb02539e9f7888dbeaaa8a18d9ea5792181570"
|
||||
checksum = "f97b87216c9f0ccc63c516263169085fa34bba25633f6d96c6a9b9ca1fd5f70a"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-compression",
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"imap-proto",
|
||||
"log",
|
||||
"nom 7.1.3",
|
||||
"nom 8.0.0",
|
||||
"pin-project",
|
||||
"pin-utils",
|
||||
"self_cell",
|
||||
@@ -322,7 +322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55219982f938e74491ba85dc4e49cefe8096b1e8f49348c67180a7d244988dca"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"futures",
|
||||
"log",
|
||||
"nom 8.0.0",
|
||||
@@ -435,6 +435,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
@@ -832,7 +838,7 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
@@ -1337,7 +1343,7 @@ dependencies = [
|
||||
"async-native-tls",
|
||||
"async-smtp",
|
||||
"async_zip",
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"blake3",
|
||||
"brotli",
|
||||
"bytes",
|
||||
@@ -1439,7 +1445,7 @@ version = "2.63.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"deltachat",
|
||||
"deltachat-contact-tools",
|
||||
"futures",
|
||||
@@ -2940,11 +2946,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imap-proto"
|
||||
version = "0.16.5"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de555d9526462b6f9ece826a26fb7c67eca9a0245bd9ff84fa91972a5d5d8856"
|
||||
checksum = "5ccf963d57074747b455398a1763d174da80bcaba6f51e3671a82252b531a68b"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
"nom 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4180,7 +4186,7 @@ version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4255,7 +4261,7 @@ dependencies = [
|
||||
"aes-gcm",
|
||||
"aes-kw",
|
||||
"argon2",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitfields",
|
||||
"block-padding",
|
||||
"blowfish",
|
||||
@@ -4534,7 +4540,7 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"derive_more 1.0.0",
|
||||
"futures-lite",
|
||||
@@ -5090,7 +5096,7 @@ version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -5696,7 +5702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78db9c9912c90ea7487f49bc149b329b535806bfa12b740fbade73f573a3d9f"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"blake3",
|
||||
"byte_string",
|
||||
"bytes",
|
||||
@@ -5996,7 +6002,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bounded-integer",
|
||||
"byteorder",
|
||||
"crc",
|
||||
@@ -6420,7 +6426,7 @@ version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.12.0", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -180,7 +180,7 @@ harness = false
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
base64 = "0.23"
|
||||
chrono = { version = "0.4.44", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -63,6 +63,7 @@ ignore = [
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base64", version = "0.22.1" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "constant_time_eq", version = "0.3.1" },
|
||||
{ name = "cpufeatures", version = "0.2.17" },
|
||||
|
||||
+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",
|
||||
|
||||
+14
-24
@@ -1454,10 +1454,9 @@ 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;
|
||||
|
||||
let folder = self.selected_folder.as_deref().unwrap_or_default();
|
||||
let mut should_refetch = false;
|
||||
@@ -1472,34 +1471,25 @@ impl Session {
|
||||
}
|
||||
|
||||
Expunge(_) | Recent(_) => {}
|
||||
Other(ref response_data) => {
|
||||
match response_data.parsed() {
|
||||
Response::Fetch { .. } => {
|
||||
info!(
|
||||
context,
|
||||
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
|
||||
);
|
||||
should_refetch = true;
|
||||
}
|
||||
|
||||
// We are not interested in the following responses and they are are
|
||||
// sent quite frequently, so, we ignore them without logging them.
|
||||
Response::Done {
|
||||
code: Some(ResponseCode::CopyUid(_, _, _)),
|
||||
..
|
||||
} => {}
|
||||
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
Other(ref response_data) => match response_data.parsed() {
|
||||
Response::Fetch { .. } => {
|
||||
info!(
|
||||
context,
|
||||
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
|
||||
);
|
||||
should_refetch = true;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(should_refetch)
|
||||
should_refetch
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl Session {
|
||||
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
if self.drain_unsolicited_responses(context) {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::tools::{
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub(crate) use transfer::maybe_readd_backup_transfer_msg;
|
||||
pub use transfer::{BackupProvider, get_backup};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
|
||||
+97
-7
@@ -40,21 +40,68 @@ use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::EventType;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::key;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{TempPathGuard, create_id};
|
||||
use crate::tools::{TempPathGuard, create_id, time};
|
||||
|
||||
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
|
||||
|
||||
/// ALPN protocol identifier for the backup transfer protocol.
|
||||
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
|
||||
/// Minimum time after a backup transfer before we check
|
||||
/// whether the "second device added" device message still exists.
|
||||
///
|
||||
/// The check is done in the first housekeeping after this delay;
|
||||
/// if the message was deleted until then, it is re-added with the original timestamp.
|
||||
/// After the check, the user can delete the message as usual without it being re-added.
|
||||
///
|
||||
/// This makes it easier to spot unwanted "add second device" actions.
|
||||
/// It is clear that this does not catch all eventualities, it is best-effort.
|
||||
/// First line of defense is device locking and asking for secret explicitly before adding a second device.
|
||||
const READD_BACKUP_TRANSFER_MSG_DELAY: i64 = 60 * 60;
|
||||
|
||||
/// Checks `Config::BackupTransferMsgId`
|
||||
/// and re-adds the backup transfer device message if it does not exist and some time passed.
|
||||
pub(crate) async fn maybe_readd_backup_transfer_msg(context: &Context) -> Result<()> {
|
||||
let Some(msg_id) = context
|
||||
.get_config_parsed::<u32>(Config::BackupTransferMsgId)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let timestamp = context
|
||||
.get_config_i64(Config::BackupTransferTimestamp)
|
||||
.await?;
|
||||
if time() < timestamp.saturating_add(READD_BACKUP_TRANSFER_MSG_DELAY) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
if Message::load_from_db_optional(context, MsgId::new(msg_id))
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(context));
|
||||
add_device_msg_with_importance(context, None, Some(&mut msg), false, timestamp).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provide or send a backup of this device.
|
||||
///
|
||||
/// This creates a backup of the current device and starts a service which offers another
|
||||
@@ -212,8 +259,23 @@ impl BackupProvider {
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let timestamp = time();
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
let msg_id =
|
||||
add_device_msg_with_importance(&context, None, Some(&mut msg), false, timestamp)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferTimestamp,
|
||||
Some(×tamp.to_string()),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferMsgId,
|
||||
Some(&msg_id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -393,14 +455,17 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::message::Viewtype;
|
||||
use crate::chat::{ChatId, ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{Viewtype, delete_msgs};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_receive() {
|
||||
async fn test_send_receive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Create first device.
|
||||
@@ -472,6 +537,31 @@ mod tests {
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
|
||||
// When deleting the backup transfer message on the sending device,
|
||||
// it is re-added once on housekeeping,
|
||||
// but not within the first hour after the transfer.
|
||||
let device_chat_id = ChatId::get_for_contact(&ctx0, ContactId::DEVICE).await?;
|
||||
let original_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_eq!(original_msg.text, backup_transfer_msg_body(&ctx0));
|
||||
delete_msgs(&ctx0, &[original_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
assert!(get_chat_msgs(&ctx0, device_chat_id).await?.is_empty()); // re-adding not done in first hour
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60 * 60));
|
||||
housekeeping(&ctx0).await?;
|
||||
let readded_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_ne!(readded_msg.id, original_msg.id);
|
||||
assert_eq!(readded_msg.get_text(), original_msg.get_text());
|
||||
assert_eq!(readded_msg.get_timestamp(), original_msg.get_timestamp());
|
||||
assert!(readded_msg.get_sort_timestamp() > original_msg.get_sort_timestamp());
|
||||
|
||||
delete_msgs(&ctx0, &[readded_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
let device_msgs = get_chat_msgs(&ctx0, device_chat_id).await?;
|
||||
assert!(device_msgs.is_empty()); // re-adding is done only once
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that trying to accidentally overwrite a profile
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let mut buf = Vec::new();
|
||||
self.to_writer(&mut buf).unwrap();
|
||||
self.to_writer(&mut buf)
|
||||
.expect("Writing to Vec<u8> cannot fail");
|
||||
buf
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied,
|
||||
clippy::manual_is_variant_and
|
||||
clippy::manual_is_variant_and,
|
||||
clippy::unnecessary_wraps
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
|
||||
+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(())
|
||||
}
|
||||
|
||||
+6
-5
@@ -511,11 +511,6 @@ impl MimeFactory {
|
||||
let mut member_fingerprints = Vec::new();
|
||||
let mut member_timestamps = Vec::new();
|
||||
let mut recipient_ids = HashSet::new();
|
||||
let req_mdn = !chat.is_self_talk()
|
||||
&& !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
&& context.should_request_mdns().await?;
|
||||
|
||||
let self_fingerprint = self_fingerprint(context).await?;
|
||||
|
||||
let encryption = if chat.is_self_talk() {
|
||||
@@ -796,6 +791,12 @@ impl MimeFactory {
|
||||
}
|
||||
};
|
||||
|
||||
let req_mdn = encryption.is_encrypted()
|
||||
&& !chat.is_self_talk()
|
||||
&& !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
&& context.should_request_mdns().await?;
|
||||
|
||||
let (in_reply_to, references) = context
|
||||
.sql
|
||||
.query_row(
|
||||
|
||||
@@ -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"));
|
||||
@@ -1100,7 +1100,6 @@ To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello!"#
|
||||
@@ -1127,7 +1126,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;
|
||||
|
||||
@@ -1152,7 +1151,6 @@ To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
|
||||
|
||||
--BOUNDARY
|
||||
|
||||
+7
-10
@@ -294,11 +294,7 @@ impl MimeMessage {
|
||||
&mut wants_mdn,
|
||||
&mail,
|
||||
);
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| is_hidden(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
);
|
||||
headers_removed.extend(headers.extract_if(|k, _v| is_hidden(k)).map(|(k, _v)| k));
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -935,9 +931,10 @@ impl MimeMessage {
|
||||
// See if an MDN is requested from the other side
|
||||
if self.decryption_error.is_none()
|
||||
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
|
||||
&& self.wants_mdn
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
&& self.wants_mdn
|
||||
&& self.was_encrypted() // Do not send MDNs for unencrypted messages.
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
{
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
@@ -951,7 +948,7 @@ impl MimeMessage {
|
||||
typ: Viewtype::Text,
|
||||
..Default::default()
|
||||
};
|
||||
if self.wants_mdn && self.incoming {
|
||||
if self.wants_mdn && self.was_encrypted() && self.incoming {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
if let Some(ref subject) = self.get_subject()
|
||||
@@ -1751,7 +1748,7 @@ impl MimeMessage {
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| has_header_protection || is_protected(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
.map(|(k, _v)| k),
|
||||
);
|
||||
|
||||
if has_header_protection {
|
||||
|
||||
@@ -46,7 +46,7 @@ use crate::mimeparser::SystemMessage;
|
||||
|
||||
/// The length of an ed25519 `PublicKey`, in bytes.
|
||||
const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
const PUBLIC_KEY_STUB: &[u8] = b"static_string";
|
||||
|
||||
/// Store Iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -86,14 +86,14 @@ async fn test_can_communicate() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -107,14 +107,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -149,14 +149,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice 2".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice 2".as_bytes() {
|
||||
if data == b"bob -> alice 2" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -314,14 +314,14 @@ async fn test_can_reconnect() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -373,14 +373,14 @@ async fn test_can_reconnect() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
|
||||
+1
-1
@@ -401,7 +401,7 @@ pub fn merge_openpgp_certificates(
|
||||
// such as Alice's key in `test-data/key/alice-secret.asc`.
|
||||
let best_user: Option<SignedUser> = old_users
|
||||
.into_iter()
|
||||
.chain(new_users.clone())
|
||||
.chain(new_users)
|
||||
.filter_map(|SignedUser { id, signatures }| {
|
||||
// Select the best signature for each User ID.
|
||||
// If User ID has no valid signatures, it is filtered out.
|
||||
|
||||
@@ -384,7 +384,7 @@ fn test_merge_openpgp_certificates() {
|
||||
|
||||
// Cannot merge certificates with different primary key.
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob, alice).is_err());
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
|
||||
+5
-9
@@ -1,6 +1,6 @@
|
||||
//! Handle plain text together with some attributes.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use regex::regex;
|
||||
|
||||
use crate::simplify::remove_message_footer;
|
||||
|
||||
@@ -25,12 +25,8 @@ impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
let linkify_mail_re = regex!(r"\b([\w.\-+]+@[\w.\-]+)\b");
|
||||
let linkify_url_re = regex!(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)");
|
||||
|
||||
let lines: Vec<&str> = self.text.lines().collect();
|
||||
let (lines, _footer) = remove_message_footer(&lines);
|
||||
@@ -52,12 +48,12 @@ impl PlainText {
|
||||
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
|
||||
let line = line.to_string().replace('\r', "");
|
||||
|
||||
let mut line = LINKIFY_MAIL_RE
|
||||
let mut line = linkify_mail_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
line = LINKIFY_URL_RE
|
||||
line = linkify_url_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
mod dclogin_scheme;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
pub(crate) use dclogin_scheme::login_param_from_login_qr;
|
||||
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
|
||||
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
||||
use regex::regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::autorelay::login_param_from_host;
|
||||
@@ -764,7 +764,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
|
||||
let addr = server_config.addr();
|
||||
let host = addr.host().to_string();
|
||||
let host = addr.host();
|
||||
let port = addr.port();
|
||||
Ok(Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
@@ -1071,16 +1071,11 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
}
|
||||
|
||||
static VCARD_NAME_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
|
||||
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
|
||||
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let name = VCARD_NAME_RE
|
||||
let name = regex!(r"(?m)^N:([^;]*);([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
@@ -1090,7 +1085,10 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
|
||||
let addr = if let Some(cap) = regex!(r"(?m)^EMAIL([^:\n]*):([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| caps.get(2))
|
||||
{
|
||||
normalize_address(cap.as_str().trim())?
|
||||
} else {
|
||||
bail!("Bad e-mail address");
|
||||
@@ -1124,7 +1122,7 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
|
||||
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
|
||||
|
||||
Ok(new_addr.to_string())
|
||||
Ok(new_addr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -120,7 +120,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
};
|
||||
|
||||
Ok(Qr::Login {
|
||||
address: addr.to_owned(),
|
||||
address: addr,
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ async fn set_msg_id_reaction(
|
||||
.await?;
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)
|
||||
{
|
||||
chat.param
|
||||
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
|
||||
|
||||
+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 {
|
||||
|
||||
@@ -416,7 +416,9 @@ async fn test_escaped_from() {
|
||||
);
|
||||
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.text, "hello");
|
||||
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||
|
||||
// MDN request from unencrypted message is ignored.
|
||||
assert!(msg.param.get_int(Param::WantsMdn).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+14
-15
@@ -33,14 +33,14 @@ pub(crate) use qrinvite::QrInvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_').remove(b'@');
|
||||
|
||||
fn inviter_progress(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
// No other values are used.
|
||||
let progress = 1000;
|
||||
context.emit_event(EventType::SecurejoinInviterProgress {
|
||||
@@ -49,8 +49,6 @@ fn inviter_progress(
|
||||
chat_type,
|
||||
progress,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shorten name to max. `length` characters.
|
||||
@@ -124,18 +122,19 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
|
||||
|
||||
let r_param = context
|
||||
let encoded_extra_relays: Vec<String> = context
|
||||
.get_self_addrs()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|addr| *addr != self_addr)
|
||||
.reduce(|acc, addr| {
|
||||
format!(
|
||||
"{acc},{}",
|
||||
utf8_percent_encode(&addr, DISALLOWED_CHARACTERS)
|
||||
)
|
||||
})
|
||||
.map_or(String::default(), |addrs| format!("&r={addrs}"));
|
||||
.map(|addr| utf8_percent_encode(&addr, DISALLOWED_CHARACTERS).to_string())
|
||||
.collect();
|
||||
|
||||
let r_param = if encoded_extra_relays.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("&r={}", encoded_extra_relays.join(","))
|
||||
};
|
||||
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
@@ -659,7 +658,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ);
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
@@ -670,7 +669,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single);
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
}
|
||||
@@ -817,7 +816,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
// and tests which don't care about the chat ID,
|
||||
// so we pass invalid chat ID here.
|
||||
let chat_id = ChatId::new(0);
|
||||
inviter_progress(context, contact_id, chat_id, chat_type)?;
|
||||
inviter_progress(context, contact_id, chat_id, chat_type);
|
||||
}
|
||||
|
||||
if matches!(step, SecureJoinStep::MemberAdded) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::message::rfc724_mid_exists_ext;
|
||||
use crate::qr::Qr;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, messages_e2ee_info_msg};
|
||||
@@ -13,6 +14,7 @@ use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, get_chat_msg, sync,
|
||||
};
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SetupContactCase {
|
||||
@@ -1142,6 +1144,43 @@ async fn test_get_securejoin_qr_name_is_last() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that addresses in QR codes are percent-encoded.
|
||||
/// `@` should not be encoded unnecessarily,
|
||||
/// since this would just make the QR code longer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_securejoin_qr_encoding() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// `@` in email addresses must not be percent-encoded:
|
||||
add_pseudo_transport(alice, "asdf@example.org").await?;
|
||||
// But `%` does need percent-encoding:
|
||||
add_pseudo_transport(alice, "jk%l@example.net").await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
assert!(
|
||||
qr.contains("a=alice@example.org"),
|
||||
"{qr} doesn't contain 'a=alice@example.org'"
|
||||
);
|
||||
assert!(
|
||||
qr.contains("r=jk%25l@example.net,asdf@example.org"),
|
||||
"{qr} doesn't contain 'r=jk%25l@example.net,asdf@example.org'"
|
||||
);
|
||||
|
||||
let qr = check_qr(bob, &qr).await?;
|
||||
let Qr::AskVerifyContact { mut addrs, .. } = qr else {
|
||||
unreachable!()
|
||||
};
|
||||
addrs.sort();
|
||||
assert_eq!(
|
||||
addrs,
|
||||
vec!["alice@example.org", "asdf@example.org", "jk%l@example.net",]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// QR codes should not get arbitrary big because of long names.
|
||||
/// The truncation, however, should not let the url end with a `.`, which is a call for trouble in linkfiers.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1525,3 +1564,29 @@ async fn test_deduplicate_member_added() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a handled join request is also marked as "deleted"
|
||||
/// in the database, so that if a copy of the request arrives via
|
||||
/// other relays in the future, then this copy will also be deleted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_join_request_deleted_on_all_relays() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = chat::create_group(alice, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
bob.add_or_lookup_contact_id(alice).await;
|
||||
join_securejoin(bob, &qr).await?;
|
||||
let request = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&request).await;
|
||||
|
||||
let rfc724_mid = Message::load_from_db(bob, request.sender_msg_id)
|
||||
.await?
|
||||
.rfc724_mid;
|
||||
let (_, deleted) = rfc724_mid_exists_ext(alice, &rfc724_mid, "deleted=1")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(deleted);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
-2
@@ -356,7 +356,7 @@ pub(crate) async fn insert_into_smtp(
|
||||
queued_msg: &QueuedMail,
|
||||
) -> Result<()> {
|
||||
let now = tools::time();
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid).await?;
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid, false).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
|
||||
@@ -720,7 +720,7 @@ async fn send_mdn_rfc724_mid(
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid).await?;
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid, false).await?;
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
|
||||
+7
-1
@@ -14,7 +14,7 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::imex::{self, BLOBS_BACKUP_NAME};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
@@ -827,6 +827,12 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
imex::maybe_readd_backup_transfer_msg(context)
|
||||
.await
|
||||
.context("Failed to re-add backup transfer message")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
if let Err(err) = incremental_vacuum(context).await {
|
||||
warn!(context, "Failed to run incremental vacuum: {err:#}.");
|
||||
}
|
||||
|
||||
+4
-12
@@ -32,8 +32,6 @@ fn migrate_key_contacts(
|
||||
context: &Context,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
) -> std::result::Result<(), anyhow::Error> {
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 1: ===============================
|
||||
// Alter tables
|
||||
transaction.execute_batch(
|
||||
@@ -79,13 +77,12 @@ fn migrate_key_contacts(
|
||||
.optional()
|
||||
.context("Step 0")?
|
||||
else {
|
||||
info!(
|
||||
context,
|
||||
"Not yet configured, no need to migrate key-contacts"
|
||||
);
|
||||
// Not yet configured, no need to migrate key-contacts.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 2: ===============================
|
||||
// Create up to 3 new contacts for every contact that has a peerstate:
|
||||
// one from the Autocrypt key fingerprint, one from the verified key fingerprint,
|
||||
@@ -1936,14 +1933,9 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
|
||||
inc_and_check(&mut migration_version, 132)?;
|
||||
if dbversion < migration_version {
|
||||
let start = Time::now();
|
||||
sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"key-contacts migration took {:?} in total.",
|
||||
time_elapsed(&start),
|
||||
);
|
||||
|
||||
// Schedule `msgs_to_key_contacts()`.
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
|
||||
@@ -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