mirror of
https://github.com/chatmail/core.git
synced 2026-05-14 04:16:30 +03:00
Compare commits
1 Commits
hoc/remove
...
link2xt/ol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd036bd33a |
88
Cargo.lock
generated
88
Cargo.lock
generated
@@ -2608,25 +2608,6 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
@@ -3276,16 +3257,6 @@ dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kem"
|
||||
version = "0.3.0-pre.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -3499,35 +3470,6 @@ dependencies = [
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-dsa"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"hybrid-array 0.3.1",
|
||||
"num-traits",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha3",
|
||||
"signature",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-kem"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f"
|
||||
dependencies = [
|
||||
"hybrid-array 0.2.3",
|
||||
"kem",
|
||||
"rand_core 0.6.4",
|
||||
"sha3",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.10"
|
||||
@@ -4263,8 +4205,6 @@ dependencies = [
|
||||
"k256",
|
||||
"log",
|
||||
"md-5",
|
||||
"ml-dsa",
|
||||
"ml-kem",
|
||||
"nom 8.0.0",
|
||||
"num-bigint-dig",
|
||||
"num-traits",
|
||||
@@ -4283,7 +4223,6 @@ dependencies = [
|
||||
"sha2",
|
||||
"sha3",
|
||||
"signature",
|
||||
"slh-dsa",
|
||||
"smallvec",
|
||||
"snafu",
|
||||
"twofish",
|
||||
@@ -5748,25 +5687,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slh-dsa"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
"hmac",
|
||||
"hybrid-array 0.3.1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha2",
|
||||
"sha3",
|
||||
"signature",
|
||||
"typenum",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -7505,9 +7425,9 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
@@ -7515,9 +7435,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -78,7 +78,7 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
|
||||
pgp = { version = "0.19.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||
|
||||
@@ -413,6 +413,11 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
||||
* 1=delete messages directly after receiving from server, mvbox is skipped.
|
||||
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
|
||||
* "Saved messages" are deleted from the server as well as emails, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||
* DC_MEDIA_QUALITY_WORSE (1)
|
||||
@@ -1456,16 +1461,16 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
* by the dc_set_config()-option `delete_device_after`.
|
||||
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
* This is typically used to show the estimated impact to the user
|
||||
* before actually enabling deletion of old messages.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param from_server Deprecated, pass 0 here
|
||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||
* @param seconds Count messages older than the given number of seconds.
|
||||
* @return Number of messages that are older than the given number of seconds.
|
||||
* Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
*/
|
||||
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
||||
|
||||
|
||||
@@ -275,7 +275,7 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.strdup()
|
||||
} else {
|
||||
match config::Config::from_str(&key)
|
||||
.with_context(|| format!("Invalid key {key:?}"))
|
||||
.with_context(|| format!("Invalid key {:?}", &key))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(key) => ctx
|
||||
|
||||
@@ -735,19 +735,10 @@ impl CommandApi {
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// Estimates the number of messages that will be deleted
|
||||
/// by the `set_config()`-option `delete_device_after`.
|
||||
///
|
||||
/// Estimate the number of messages that will be deleted
|
||||
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
/// This is typically used to show the estimated impact to the user
|
||||
/// before actually enabling deletion of old messages.
|
||||
///
|
||||
/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `from_server`: Deprecated, pass `false` here
|
||||
/// - `seconds`: Count messages older than the given number of seconds.
|
||||
///
|
||||
/// Returns the number of messages that are older than the given number of seconds.
|
||||
async fn estimate_auto_deletion_count(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -122,7 +122,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.ends_with(".eml") {
|
||||
let path_plus_name = format!("{real_spec}/{name}");
|
||||
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||
println!("Import: {path_plus_name}");
|
||||
if poke_eml_file(context, Path::new(&path_plus_name))
|
||||
.await
|
||||
@@ -133,11 +133,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Import: Cannot open directory {real_spec:?}.");
|
||||
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("Import: {read_cnt} items read from {real_spec:?}.");
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
if read_cnt > 0 {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
@@ -179,7 +179,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
if msg.has_location() { "📍" } else { "" },
|
||||
contact_name,
|
||||
&contact_name,
|
||||
contact_id,
|
||||
msgtext,
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
@@ -221,7 +221,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
},
|
||||
statestr,
|
||||
downloadstate,
|
||||
temp2,
|
||||
&temp2,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -561,7 +561,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.map_or_else(String::new, |prefix| format!("{prefix}: ")),
|
||||
summary.text,
|
||||
statestr,
|
||||
timestr,
|
||||
×tr,
|
||||
if chat.is_sending_locations() {
|
||||
"📍"
|
||||
} else {
|
||||
|
||||
@@ -432,7 +432,7 @@ async fn handle_cmd(
|
||||
{
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||
} else {
|
||||
println!("OAuth2 not available for {addr}.");
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
}
|
||||
} else {
|
||||
println!("oauth2: set addr first.");
|
||||
|
||||
@@ -29,7 +29,7 @@ $ pip install .
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
2. Install tox `pip install -U tox`
|
||||
3. Run `CHATMAIL_DOMAIN=ci-chatmail.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
@@ -14,13 +14,10 @@ def test_moved_markseen(acfactory, direct_imap, log):
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2.bring_online()
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
log.section("ac2: creating DeltaChat folder")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
||||
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
|
||||
@@ -60,9 +57,11 @@ def test_moved_markseen(acfactory, direct_imap, log):
|
||||
def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2.set_config("bcc_self", "1")
|
||||
for ac in ac1, ac2:
|
||||
ac.set_config("delete_server_after", "0")
|
||||
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2.wait_for_incoming_msg()
|
||||
@@ -82,18 +81,17 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
ac1_direct_imap.select_folder("INBOX")
|
||||
ac2_direct_imap.select_folder("INBOX")
|
||||
|
||||
# Check that the mdn and original message is marked as seen
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||
# Check that the mdn is marked as seen
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
# Check original message is marked as seen
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
|
||||
|
||||
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0")
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
@@ -4,29 +4,39 @@ from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import MessageState
|
||||
|
||||
|
||||
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
|
||||
def test_bcc_self_delete_server_after_defaults(acfactory):
|
||||
"""Test default values for bcc_self and delete_server_after."""
|
||||
ac = acfactory.get_online_account()
|
||||
|
||||
# Initially after getting online
|
||||
# the setting bcc_self is set to 0 because there is only one device
|
||||
# and delete_server_after is "1", meaning immediate deletion.
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Setup a second device.
|
||||
ac_clone = ac.clone()
|
||||
ac_clone.bring_online()
|
||||
|
||||
# Second device setup enables bcc_self.
|
||||
# Second device setup
|
||||
# enables bcc_self and changes default delete_server_after.
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac_clone.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
# Test manually disabling bcc_self
|
||||
assert ac_clone.get_config("bcc_self") == "1"
|
||||
assert ac_clone.get_config("delete_server_after") == "0"
|
||||
|
||||
# Manually disabling bcc_self
|
||||
# also restores the default for delete_server_after.
|
||||
ac.set_config("bcc_self", "0")
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Cloning the account again enables bcc_self again
|
||||
# Cloning the account again enables bcc_self
|
||||
# even though it was manually disabled.
|
||||
ac_clone = ac.clone()
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
|
||||
@@ -1232,12 +1232,11 @@ def test_leave_and_delete_group(acfactory, log):
|
||||
|
||||
|
||||
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
"""
|
||||
`bcc_self` is off by default,
|
||||
so that messages are supposed to be immediately autodeleted
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
log.section("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
@@ -43,12 +43,7 @@ ignore = [
|
||||
# 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>
|
||||
"RUSTSEC-2025-0144",
|
||||
"RUSTSEC-2026-0119"
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -67,7 +62,6 @@ skip = [
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hybrid-array", version = "0.2.3" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "lru", version = "0.12.5" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
|
||||
@@ -521,6 +521,7 @@ class ACFactory:
|
||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
configdict.setdefault("delete_server_after", 0)
|
||||
ac.update_config(configdict)
|
||||
self._acsetup._account2config[ac] = configdict
|
||||
self._preconfigure_key(ac)
|
||||
|
||||
@@ -298,6 +298,73 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert msg_in.text == msg_out.text
|
||||
|
||||
|
||||
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
"""Test for the issue #4346:
|
||||
- User is added to a verified group.
|
||||
- First device of the user downloads "member added" from the group.
|
||||
- First device removes "member added" from the server.
|
||||
- Some new messages are sent to the group.
|
||||
- Second device comes online, receives these new messages.
|
||||
The result is an unverified group with unverified members.
|
||||
- First device re-gossips Autocrypt keys to the group.
|
||||
- Now the second device has all members and group verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
ac2.set_config("gossip_period", "0") # Re-gossip in every message
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac2.export_self_keys(str(dir))
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
# Wait for "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.is_system_message()
|
||||
|
||||
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
|
||||
lp.sec("ac1: sending 'hi' to the group")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
chat1.send_text("hi")
|
||||
|
||||
lp.sec("ac2_offl: going online, checking the 'hi' message")
|
||||
ac2_offl.start_io()
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi"
|
||||
ac2_offl_ac1_contact = msg_in.get_sender_contact()
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
def test_deleted_msgs_dont_reappear(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
@@ -15,9 +15,6 @@ def test_basic_imap_api(acfactory, tmp_path):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
|
||||
with imap2.idle() as idle2:
|
||||
@@ -165,9 +162,6 @@ def test_webxdc_message(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_webxdc()
|
||||
@@ -368,10 +362,6 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
# make DC's life harder wrt to encodings
|
||||
ac1.set_config("displayname", "ä name")
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
# clear any fresh device messages
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
ac2.get_device_chat().mark_noticed()
|
||||
@@ -516,15 +506,9 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
ac1.set_config("mdns_enabled", "1")
|
||||
ac2.set_config("mdns_enabled", "1")
|
||||
|
||||
# Make sure that the mdn is not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("disable ac1 MDNs")
|
||||
@@ -541,7 +525,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("ac1: waiting for incoming activity")
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
# Wait for the mdn to be marked as seen on IMAP.
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
@@ -1089,8 +1073,6 @@ def test_send_receive_locations(acfactory, lp):
|
||||
|
||||
def test_delete_multiple_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending seven messages")
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c
|
||||
REV=ad097ee40579c884e7757de2d3bb0a51f481a32a
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
@@ -794,7 +794,7 @@ impl Config {
|
||||
.with_push_subscriber(push_subscriber.clone())
|
||||
.build()
|
||||
.await
|
||||
.with_context(|| format!("failed to create context from file {dbfile:?}"))?;
|
||||
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
@@ -2529,7 +2529,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
// running numbers, etc.
|
||||
let filename: String = match viewtype_orig {
|
||||
Viewtype::Voice => format!(
|
||||
"voice-messsage_{}.{suffix}",
|
||||
"voice-messsage_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
@@ -2537,9 +2537,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
&suffix
|
||||
),
|
||||
Viewtype::Image | Viewtype::Gif => format!(
|
||||
"image_{}.{suffix}",
|
||||
"image_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
@@ -2547,9 +2548,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
|
||||
),
|
||||
&suffix,
|
||||
),
|
||||
Viewtype::Video => format!(
|
||||
"video_{}.{suffix}",
|
||||
"video_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
@@ -2557,6 +2559,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
&suffix
|
||||
),
|
||||
_ => filename,
|
||||
};
|
||||
|
||||
@@ -194,6 +194,17 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
/// 0 means messages are never deleted by Delta Chat.
|
||||
///
|
||||
/// Value 1 is treated as "delete at once": messages are deleted
|
||||
/// immediately, without moving to DeltaChat folder.
|
||||
///
|
||||
/// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise.
|
||||
DeleteServerAfter,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// device.
|
||||
///
|
||||
@@ -543,6 +554,14 @@ impl Context {
|
||||
// Default values
|
||||
let val = match key {
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
||||
Config::DeleteServerAfter => {
|
||||
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
|
||||
&& Box::pin(self.is_chatmail()).await?
|
||||
{
|
||||
true => Some("1".to_string()),
|
||||
false => Some("0".to_string()),
|
||||
}
|
||||
}
|
||||
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
|
||||
_ => key.get_str("default").map(|s| s.to_string()),
|
||||
};
|
||||
@@ -623,6 +642,23 @@ impl Context {
|
||||
self.get_config_bool(Config::MdnsEnabled).await
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
|
||||
let val = match self
|
||||
.get_config_parsed::<i64>(Config::DeleteServerAfter)
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
{
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x),
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Gets the configured provider.
|
||||
///
|
||||
/// The provider is determined by the current primary transport.
|
||||
|
||||
@@ -142,6 +142,28 @@ async fn test_mdns_default_behaviour() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_server_after_default() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
|
||||
// Check that the settings are displayed correctly.
|
||||
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
|
||||
assert_eq!(
|
||||
t.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
|
||||
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
|
||||
// does).
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -707,7 +707,8 @@ async fn get_autoconfig(
|
||||
ctx,
|
||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
|
||||
&format!(
|
||||
"https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
|
||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||
¶m_domain, ¶m_addr_urlencoded
|
||||
),
|
||||
¶m.addr,
|
||||
)
|
||||
@@ -720,7 +721,7 @@ async fn get_autoconfig(
|
||||
// Outlook uses always SSL but different domains (this comment describes the next two steps)
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
format!("https://{param_domain}/autodiscover/autodiscover.xml"),
|
||||
format!("https://{}/autodiscover/autodiscover.xml", ¶m_domain),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -730,7 +731,10 @@ async fn get_autoconfig(
|
||||
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",),
|
||||
format!(
|
||||
"https://autodiscover.{}/autodiscover/autodiscover.xml",
|
||||
¶m_domain
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -741,7 +745,7 @@ async fn get_autoconfig(
|
||||
// always SSL for Thunderbird's database
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
&format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"),
|
||||
&format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
|
||||
¶m.addr,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -973,6 +973,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"delete_server_after",
|
||||
self.get_config_int(Config::DeleteServerAfter)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_housekeeping",
|
||||
self.get_config_int(Config::LastHousekeeping)
|
||||
|
||||
@@ -15,6 +15,12 @@ use crate::{EventType, chatlist_events};
|
||||
pub(crate) mod post_msg_metadata;
|
||||
pub(crate) use post_msg_metadata::PostMsgMetadata;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
/// the user might have no chance to actually download that message.
|
||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||
|
||||
/// From this point onward outgoing messages are considered large
|
||||
/// and get a Pre-Message, which announces the Post-Message.
|
||||
/// This is only about sending so we can modify it any time.
|
||||
|
||||
153
src/ephemeral.rs
153
src/ephemeral.rs
@@ -23,15 +23,16 @@
|
||||
//! ## Device settings
|
||||
//!
|
||||
//! In addition to per-chat ephemeral message setting, each device has
|
||||
//! a global user-configured setting that complements per-chat
|
||||
//! settings, `delete_device_after`.
|
||||
//! This setting is not synchronized among devices and applies to all
|
||||
//! two global user-configured settings that complement per-chat
|
||||
//! settings: `delete_device_after` and `delete_server_after`. These
|
||||
//! settings are not synchronized among devices and apply to all
|
||||
//! messages known to the device, including messages sent or received
|
||||
//! before configuring the setting.
|
||||
//!
|
||||
//! `delete_device_after` configures the maximum time device is
|
||||
//! storing the messages locally,
|
||||
//! but does not delete messages from the server.
|
||||
//! storing the messages locally. `delete_server_after` configures the
|
||||
//! time after which device will delete the messages it knows about
|
||||
//! from the server.
|
||||
//!
|
||||
//! ## How messages are deleted
|
||||
//!
|
||||
@@ -59,8 +60,9 @@
|
||||
//!
|
||||
//! Server deletion happens by updating the `imap` table based on
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers.
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
@@ -73,11 +75,10 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::chat::{ChatId, ChatIdBlocked, send_msg};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::download::DownloadState;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
@@ -650,115 +651,37 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
}
|
||||
|
||||
/// Schedules expired IMAP messages for deletion.
|
||||
pub(crate) async fn delete_expired_imap_messages(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
is_chatmail: bool,
|
||||
) -> Result<()> {
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
if !context.get_config_bool(Config::BccSelf).await? && is_chatmail {
|
||||
info!(
|
||||
context,
|
||||
"dbg marking all as deleted 1 - rfc724_mids: {:?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?)
|
||||
OR download_state=?",
|
||||
(now, DownloadState::Done),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
info!(
|
||||
context,
|
||||
"dbg marking all as deleted 1 - pre_rfc724_mids: {:?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''",
|
||||
(),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
// This the only device using this relay.
|
||||
// Mark all downloaded messages for deletion, because they are not needed anymore.
|
||||
//
|
||||
// For pre- and post-messages, `rfc724_mid` contains the post-message's Message-Id.
|
||||
// The pre-message's Message-Id is in pre_rfc724_mid, if it exists.
|
||||
//
|
||||
// Pre-messages can be deleted even if the message wasn't fully downloaded yet,
|
||||
// because it's only the post-message that hasn't been downloaded.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||
OR download_state=?3
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
)",
|
||||
(transport_id, now, DownloadState::Done),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"dbg marking ephemeral as deleted 1 - rfc724_mids: {:?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)",
|
||||
(transport_id, now),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
info!(
|
||||
context,
|
||||
"dbg marking ephemeral as deleted 1 - pre_rfc724_mids: {:?}",
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?)",
|
||||
(now,),
|
||||
|row| Ok(row.get::<_, String>(0)?)
|
||||
)
|
||||
.await
|
||||
);
|
||||
// There may be other devices using this relay,
|
||||
// either because there is multi-relay or because this is a classical email server.
|
||||
// Only delete expired ephemeral messages.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND (ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2)
|
||||
)",
|
||||
(transport_id, now),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let (threshold_timestamp, threshold_timestamp_extended) =
|
||||
match context.get_config_delete_server_after().await? {
|
||||
None => (0, 0),
|
||||
Some(delete_server_after) => (
|
||||
match delete_server_after {
|
||||
// Guarantee immediate deletion.
|
||||
0 => i64::MAX,
|
||||
_ => now - delete_server_after,
|
||||
},
|
||||
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
),
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ((download_state = 0 AND timestamp < ?) OR
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
(threshold_timestamp, threshold_timestamp_extended, now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -451,178 +451,105 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
let transport_id: u32 = 1;
|
||||
let other_transport_id: u32 = 2;
|
||||
let uidvalidity = 12345u32;
|
||||
|
||||
async fn is_deleted(context: &Context, mid: &str) -> Result<bool> {
|
||||
Ok(context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(mid,),
|
||||
let transport_id = 1;
|
||||
let uidvalidity = 12345;
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
(3000, now + HOUR, 0),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (?, ?,'INBOX',?, 'INBOX', ?);",
|
||||
(transport_id, &message_id, id, uidvalidity),
|
||||
)
|
||||
.await?
|
||||
== 1)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn reset_targets(context: &Context) {
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE imap SET target='INBOX'", ())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// ── Test messages ────────────────────────────────────────────────────────
|
||||
//
|
||||
// (id, rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid)
|
||||
//
|
||||
// "expired" – expired ephemeral, no pre-msg
|
||||
// "no_expire" – ephemeral_timestamp=0, not Done → never deleted
|
||||
// "future" – future ephemeral, not Done → never deleted
|
||||
// "done" – Done, no ephemeral → branch 1 only
|
||||
// "pre_no_expire_*" – has pre-msg, but no expiry/Done
|
||||
// "pre_expired_*" – has pre-msg, expired ephemeral
|
||||
// "pre_future_*" – has pre-msg, future ephemeral
|
||||
// "wrong_tid" – expired+Done, but wrong transport_id in imap
|
||||
let msgs: &[(&str, i64, DownloadState, &str)] = &[
|
||||
("expired", now - HOUR, DownloadState::Available, ""),
|
||||
("no_expire", 0, DownloadState::Available, ""),
|
||||
("future", now + HOUR, DownloadState::Available, ""),
|
||||
("done", 0, DownloadState::Done, ""),
|
||||
(
|
||||
"pre_no_expire_post",
|
||||
0,
|
||||
DownloadState::Available,
|
||||
"pre_no_expire_pre",
|
||||
),
|
||||
(
|
||||
"pre_expired_post",
|
||||
now - HOUR,
|
||||
DownloadState::Available,
|
||||
"pre_expired_pre",
|
||||
),
|
||||
(
|
||||
"pre_future_post",
|
||||
now + HOUR,
|
||||
DownloadState::Available,
|
||||
"pre_future_pre",
|
||||
),
|
||||
("wrong_tid", now - HOUR, DownloadState::Done, ""),
|
||||
];
|
||||
for (mid, eph_ts, dl_state, pre_mid) in msgs {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs \
|
||||
(rfc724_mid, timestamp, ephemeral_timestamp, download_state, pre_rfc724_mid) \
|
||||
VALUES (?,?,0,?,?,?)",
|
||||
(*mid, *eph_ts, *dl_state, *pre_mid),
|
||||
)
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// One imap row per mid (including separate rows for pre-messages),
|
||||
// plus "wrong_tid" on a different transport_id.
|
||||
let imap_rows: &[(&str, u32)] = &[
|
||||
("expired", transport_id),
|
||||
("no_expire", transport_id),
|
||||
("future", transport_id),
|
||||
("done", transport_id),
|
||||
("pre_no_expire_post", transport_id),
|
||||
("pre_no_expire_pre", transport_id), // the pre-message's own imap row
|
||||
("pre_expired_post", transport_id),
|
||||
("pre_expired_pre", transport_id),
|
||||
("pre_future_post", transport_id),
|
||||
("pre_future_pre", transport_id),
|
||||
("wrong_tid", other_transport_id), // transport_id filter test
|
||||
];
|
||||
for (i, (mid, tid)) in imap_rows.iter().enumerate() {
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap \
|
||||
(transport_id, rfc724_mid, folder, uid, target, uidvalidity) \
|
||||
VALUES (?,?,'INBOX',?,'INBOX',?)",
|
||||
(*tid, *mid, (i + 1) as u32, uidvalidity),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
// ── Branch 1: is_chatmail=true, BccSelf=false (default) ─────────────────
|
||||
//
|
||||
// SQL deletes: (ephemeral_timestamp!=0 AND <=now) OR download_state=Done
|
||||
// Pre-messages: ALL with pre_rfc724_mid!='' unconditionally.
|
||||
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
// Tests (ephemeral_timestamp!=0 AND ephemeral_timestamp<=now) path.
|
||||
assert!(is_deleted(&t, "expired").await?);
|
||||
// Tests the ephemeral_timestamp!=0 guard: timestamp=0 satisfies <=now but must not match.
|
||||
assert!(!is_deleted(&t, "no_expire").await?);
|
||||
// Tests the ephemeral_timestamp<=now guard.
|
||||
assert!(!is_deleted(&t, "future").await?);
|
||||
// Tests the OR download_state=Done clause.
|
||||
assert!(is_deleted(&t, "done").await?);
|
||||
// Post-message: no expiry, not Done → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_no_expire_post").await?);
|
||||
// Pre-message: deleted unconditionally (tests UNION SELECT pre_rfc724_mid ... WHERE pre_rfc724_mid!='').
|
||||
assert!(is_deleted(&t, "pre_no_expire_pre").await?);
|
||||
// Post-message with expired ephemeral → deleted.
|
||||
assert!(is_deleted(&t, "pre_expired_post").await?);
|
||||
// Pre-message of expired post → deleted (unconditional pre path).
|
||||
assert!(is_deleted(&t, "pre_expired_pre").await?);
|
||||
// Post-message with future ephemeral → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_future_post").await?);
|
||||
// Pre-message of future post → still deleted (branch 1 pre path has NO ephemeral condition).
|
||||
// If the pre UNION clause gains an ephemeral condition, this would wrongly not be deleted.
|
||||
assert!(is_deleted(&t, "pre_future_pre").await?);
|
||||
// Tests transport_id=?1: expired+Done but on wrong transport_id → not deleted.
|
||||
assert!(!is_deleted(&t, "wrong_tid").await?);
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
reset_targets(&t).await;
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
||||
.await?;
|
||||
|
||||
// ── Branch 2: is_chatmail=false ──────────────────────────────────────────
|
||||
//
|
||||
// SQL deletes: ephemeral_timestamp!=0 AND <=now only (no Done).
|
||||
// Pre-messages: only when the post also satisfies the ephemeral condition.
|
||||
delete_expired_imap_messages(&t, transport_id, false).await?;
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
// Expired ephemeral → deleted.
|
||||
assert!(is_deleted(&t, "expired").await?);
|
||||
// ephemeral_timestamp=0 → not deleted (tests !=0 guard in branch 2).
|
||||
assert!(!is_deleted(&t, "no_expire").await?);
|
||||
// Future ephemeral → not deleted (tests <=now guard in branch 2).
|
||||
assert!(!is_deleted(&t, "future").await?);
|
||||
// Done without expired ephemeral → NOT deleted (key branch 1 vs 2 difference).
|
||||
// If download_state=Done were added to branch 2, this would wrongly be deleted.
|
||||
assert!(!is_deleted(&t, "done").await?);
|
||||
// Post-message: no expiry → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_no_expire_post").await?);
|
||||
// Pre-message of non-expiring post → NOT deleted
|
||||
// (tests ephemeral_timestamp!=0 in branch 2's pre subquery).
|
||||
assert!(!is_deleted(&t, "pre_no_expire_pre").await?);
|
||||
// Post-message with expired ephemeral → deleted.
|
||||
assert!(is_deleted(&t, "pre_expired_post").await?);
|
||||
// Pre-message of expired post → deleted (tests full ephemeral condition in pre subquery).
|
||||
assert!(is_deleted(&t, "pre_expired_pre").await?);
|
||||
// Post-message with future ephemeral → not deleted.
|
||||
assert!(!is_deleted(&t, "pre_future_post").await?);
|
||||
// Pre-message of future post → NOT deleted
|
||||
// (tests ephemeral_timestamp<=now in branch 2's pre subquery).
|
||||
// If the <=now guard were removed there, this would wrongly be deleted.
|
||||
assert!(!is_deleted(&t, "pre_future_pre").await?);
|
||||
// Wrong transport_id → not deleted.
|
||||
assert!(!is_deleted(&t, "wrong_tid").await?);
|
||||
|
||||
reset_targets(&t).await;
|
||||
|
||||
// ── BccSelf=true forces branch 2 even when is_chatmail=true ─────────────
|
||||
//
|
||||
// Tests the `!BccSelf` part of the Rust condition.
|
||||
// If `!BccSelf` were dropped, Done would be deleted here (branch 1 behaviour).
|
||||
t.set_config(Config::BccSelf, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||
assert!(!is_deleted(&t, "done").await?); // must stay on branch 2
|
||||
assert!(is_deleted(&t, "expired").await?); // branch 2 still runs normally
|
||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 3000).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
42
src/imap.rs
42
src/imap.rs
@@ -21,6 +21,9 @@ use futures_lite::FutureExt;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::calls::{
|
||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
||||
};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
@@ -46,10 +49,6 @@ use crate::tools::{self, create_id, duration_to_str, time};
|
||||
use crate::transport::{
|
||||
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
||||
};
|
||||
use crate::{
|
||||
calls::{UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata},
|
||||
ephemeral::delete_expired_imap_messages,
|
||||
};
|
||||
|
||||
pub(crate) mod capabilities;
|
||||
mod client;
|
||||
@@ -526,12 +525,6 @@ impl Imap {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
|
||||
// Mark expired messages for deletion. Note that `delete_expired_imap_messages` is not
|
||||
// not well optimized and should not be called before fetching.
|
||||
delete_expired_imap_messages(context, session.transport_id(), session.is_chatmail())
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
session
|
||||
.move_delete_messages(context, watch_folder)
|
||||
.await
|
||||
@@ -1052,12 +1045,15 @@ impl Session {
|
||||
if target.is_empty() {
|
||||
self.delete_message_batch(context, &uid_set, rowid_set)
|
||||
.await
|
||||
.with_context(|| format!("cannot delete batch of messages {uid_set:?}"))?;
|
||||
.with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
|
||||
} else {
|
||||
self.move_message_batch(context, &uid_set, rowid_set, &target)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("cannot move batch of messages {uid_set:?} to folder {target:?}",)
|
||||
format!(
|
||||
"cannot move batch of messages {:?} to folder {:?}",
|
||||
&uid_set, target
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -1291,10 +1287,9 @@ impl Session {
|
||||
|
||||
for (request_uids, set) in build_sequence_sets(&request_uids)? {
|
||||
info!(context, "Starting UID FETCH of message set \"{}\".", set);
|
||||
let mut fetch_responses = self
|
||||
.uid_fetch(&set, BODY_FULL)
|
||||
.await
|
||||
.with_context(|| format!("fetching messages {set} from folder {folder:?}"))?;
|
||||
let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
|
||||
format!("fetching messages {} from folder \"{}\"", &set, folder)
|
||||
})?;
|
||||
|
||||
// Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
|
||||
// when we want to process other messages first.
|
||||
@@ -1386,21 +1381,6 @@ impl Session {
|
||||
"Passing message UID {} to receive_imf().", request_uid
|
||||
);
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
||||
|
||||
// TODO I don't think this code is needed anymore:
|
||||
// // If the message is not needed anymore on the server, mark it for deletion:
|
||||
// if !context.get_config_bool(Config::BccSelf).await? && is_chatmail {
|
||||
// context
|
||||
// .sql
|
||||
// .execute(
|
||||
// "UPDATE imap SET target='' WHERE rfc724_mid=?",
|
||||
// (rfc724_mid,),
|
||||
// )
|
||||
// .await?;
|
||||
// context.scheduler.interrupt_inbox().await;
|
||||
// }
|
||||
|
||||
// If there was an error receiving the message, show a device message:
|
||||
let received_msg = match res {
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf error: {err:#}.");
|
||||
|
||||
27
src/imex.rs
27
src/imex.rs
@@ -980,15 +980,20 @@ mod tests {
|
||||
|
||||
// Check that the settings are displayed correctly.
|
||||
assert_eq!(
|
||||
context1.get_config(Config::BccSelf).await?,
|
||||
context1.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
context1.set_config_bool(Config::IsChatmail, true).await?;
|
||||
assert_eq!(
|
||||
context1.get_config(Config::BccSelf).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
context1.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("1".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, false);
|
||||
context1.set_config_bool(Config::IsMuted, true).await?;
|
||||
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, true);
|
||||
|
||||
assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
|
||||
imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
|
||||
let _event = context1
|
||||
.evtracker
|
||||
@@ -1005,9 +1010,15 @@ mod tests {
|
||||
assert!(context2.is_configured().await?);
|
||||
assert!(context2.is_chatmail().await?);
|
||||
for ctx in [context1, context2] {
|
||||
// BccSelf should be enabled automatically when exporting a backup
|
||||
assert_eq!(ctx.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(ctx.get_config_bool(Config::IsMuted).await?, true);
|
||||
assert_eq!(
|
||||
ctx.get_config(Config::BccSelf).await?,
|
||||
Some("1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
assert_eq!(ctx.get_config_delete_server_after().await?, None);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
11
src/key.rs
11
src/key.rs
@@ -570,11 +570,9 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
/// Creates new fingerprint.
|
||||
///
|
||||
/// It is 160-bit (20 bytes) for v4 keys and 32 bytes for v6 keys.
|
||||
/// Creates new 160-bit (20 bytes) fingerprint.
|
||||
pub fn new(v: Vec<u8>) -> Fingerprint {
|
||||
debug_assert!(v.len() == 20 || v.len() == 32);
|
||||
debug_assert_eq!(v.len(), 20);
|
||||
Fingerprint(v)
|
||||
}
|
||||
|
||||
@@ -627,10 +625,7 @@ impl std::str::FromStr for Fingerprint {
|
||||
.filter(|&c| c.is_ascii_hexdigit())
|
||||
.collect();
|
||||
let v: Vec<u8> = hex::decode(&hex_repr)?;
|
||||
ensure!(
|
||||
v.len() == 20 || v.len() == 32,
|
||||
"wrong fingerprint length: {hex_repr}"
|
||||
);
|
||||
ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}");
|
||||
let fp = Fingerprint::new(v);
|
||||
Ok(fp)
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
|
||||
} else {
|
||||
msg.timestamp_sort
|
||||
});
|
||||
ret += &format!("Received: {s}");
|
||||
ret += &format!("Received: {}", &s);
|
||||
ret += "\n";
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
|
||||
ret += "Type: ";
|
||||
ret += &format!("{}", msg.viewtype);
|
||||
ret += "\n";
|
||||
ret += &format!("Mimetype: {}\n", msg.get_filemime().unwrap_or_default());
|
||||
ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
|
||||
}
|
||||
let w = msg.param.get_int(Param::Width).unwrap_or_default();
|
||||
let h = msg.param.get_int(Param::Height).unwrap_or_default();
|
||||
@@ -2099,52 +2099,63 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
|
||||
}
|
||||
|
||||
/// Estimates the number of messages that will be deleted
|
||||
/// by the `set_config()`-option `delete_device_after`.
|
||||
/// by the options `delete_device_after` or `delete_server_after`.
|
||||
///
|
||||
/// This is typically used to show the estimated impact to the user
|
||||
/// before actually enabling deletion of old messages.
|
||||
///
|
||||
/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||
/// If `from_server` is true,
|
||||
/// estimate deletion count for server,
|
||||
/// otherwise estimate deletion count for device.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `from_server`: Deprecated, pass `false` here
|
||||
/// - `seconds`: Count messages older than the given number of seconds.
|
||||
/// Count messages older than the given number of `seconds`.
|
||||
///
|
||||
/// Returns the number of messages that are older than the given number of seconds.
|
||||
/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn estimate_deletion_cnt(
|
||||
context: &Context,
|
||||
from_server: bool,
|
||||
seconds: i64,
|
||||
) -> Result<usize> {
|
||||
ensure!(
|
||||
!from_server,
|
||||
"The `delete_server_after` config option was removed. You need to pass `false` for `from_server`."
|
||||
);
|
||||
|
||||
let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
|
||||
.await?
|
||||
.map(|c| c.id)
|
||||
.unwrap_or_default();
|
||||
let threshold_timestamp = time() - seconds;
|
||||
|
||||
let cnt = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
let cnt = if from_server {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs m
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
|
||||
(DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs m
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ? AND hidden = 0;",
|
||||
(
|
||||
DC_MSG_ID_LAST_SPECIAL,
|
||||
threshold_timestamp,
|
||||
self_chat_id,
|
||||
DC_CHAT_ID_TRASH,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
(
|
||||
DC_MSG_ID_LAST_SPECIAL,
|
||||
threshold_timestamp,
|
||||
self_chat_id,
|
||||
DC_CHAT_ID_TRASH,
|
||||
),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(cnt)
|
||||
}
|
||||
|
||||
|
||||
@@ -1939,18 +1939,27 @@ pub(crate) fn render_outer_message(
|
||||
/// Takes the encrypted part, wraps it in a MimePart,
|
||||
/// and sets the appropriate Content-Type for the outer message
|
||||
pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> {
|
||||
// XXX: additional newline is needed
|
||||
// to pass filtermail at
|
||||
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>:
|
||||
let encrypted = encrypted + "\n";
|
||||
|
||||
MimePart::new(
|
||||
"multipart/encrypted; protocol=\"application/pgp-encrypted\"",
|
||||
vec![
|
||||
// Autocrypt part 1
|
||||
MimePart::new("application/pgp-encrypted", "Version: 1\r\n"),
|
||||
MimePart::new("application/pgp-encrypted", "Version: 1\r\n").header(
|
||||
"Content-Description",
|
||||
mail_builder::headers::raw::Raw::new("PGP/MIME version identification"),
|
||||
),
|
||||
// Autocrypt part 2
|
||||
MimePart::new("application/octet-stream", encrypted),
|
||||
MimePart::new(
|
||||
"application/octet-stream; name=\"encrypted.asc\"",
|
||||
encrypted,
|
||||
)
|
||||
.header(
|
||||
"Content-Description",
|
||||
mail_builder::headers::raw::Raw::new("OpenPGP encrypted message"),
|
||||
)
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
mail_builder::headers::raw::Raw::new("inline; filename=\"encrypted.asc\";"),
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ impl MimeMessage {
|
||||
let decrypted_msg; // Decrypted signed OpenPGP message.
|
||||
let expected_sender_fingerprint: Option<String>;
|
||||
|
||||
let (mail, is_encrypted) = match Box::pin(decrypt::decrypt(context, &mail)).await {
|
||||
let (mail, is_encrypted) = match decrypt::decrypt(context, &mail).await {
|
||||
Ok(Some((mut msg, expected_sender_fp))) => {
|
||||
mail_raw = msg.as_data_vec().unwrap_or_default();
|
||||
|
||||
|
||||
16
src/net.rs
16
src/net.rs
@@ -109,8 +109,8 @@ pub(crate) async fn connect_tcp_inner(
|
||||
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
|
||||
let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr))
|
||||
.await
|
||||
.with_context(|| format!("Connection to {addr} timed out"))?
|
||||
.with_context(|| format!("Connection to {addr} failed"))?;
|
||||
.context("Connection timeout")?
|
||||
.context("Connection failure")?;
|
||||
|
||||
// Disable Nagle's algorithm.
|
||||
tcp_stream.set_nodelay(true)?;
|
||||
@@ -180,7 +180,7 @@ where
|
||||
delay_set.spawn(tokio::time::sleep(delay));
|
||||
}
|
||||
|
||||
let mut all_errors = Vec::new();
|
||||
let mut first_error = None;
|
||||
|
||||
let res = loop {
|
||||
if let Some(fut) = futures.next() {
|
||||
@@ -200,7 +200,7 @@ where
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
// Some connection attempt failed.
|
||||
all_errors.push(err);
|
||||
first_error.get_or_insert(err);
|
||||
}
|
||||
Err(err) => {
|
||||
break Err(err);
|
||||
@@ -211,11 +211,9 @@ where
|
||||
// Out of connection attempts.
|
||||
//
|
||||
// Break out of the loop and return error.
|
||||
break if all_errors.is_empty() {
|
||||
Err(format_err!("No connection attempts were made"))
|
||||
} else {
|
||||
Err(format_err!("All connection attempts failed: {}", all_errors.into_iter().map(|err| format!("{err:#}")).collect::<Vec<String>>().join("; ")))
|
||||
};
|
||||
break Err(
|
||||
first_error.unwrap_or_else(|| format_err!("No connection attempts were made"))
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
37
src/pgp.rs
37
src/pgp.rs
@@ -847,41 +847,4 @@ mod tests {
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pqc() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let pqc = &tcm.pqc().await;
|
||||
|
||||
let pqc_received_message = tcm.send_recv_accept(alice, pqc, "Hi!").await;
|
||||
let pqc_chat_id = pqc_received_message.chat_id;
|
||||
let pqc_sent = pqc.send_text(pqc_chat_id, "Hello back!").await;
|
||||
|
||||
let alice_rcvd = alice.recv_msg(&pqc_sent).await;
|
||||
assert_eq!(alice_rcvd.text, "Hello back!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests securejoin with inviter using PQC key.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_securejoin_pqc_inviter() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let pqc = &tcm.pqc().await;
|
||||
|
||||
tcm.execute_securejoin(pqc, alice).await;
|
||||
}
|
||||
|
||||
/// Tests securejoin with joiner using PQC key.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_securejoin_pqc_joiner() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let pqc = &tcm.pqc().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.execute_securejoin(bob, pqc).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,10 +890,16 @@ static P_NAUTA_CU: Provider = Provider {
|
||||
strict_tls: false,
|
||||
..ProviderOptions::new()
|
||||
},
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MediaQuality,
|
||||
value: "1",
|
||||
}]),
|
||||
config_defaults: Some(&[
|
||||
ConfigDefault {
|
||||
key: Config::DeleteServerAfter,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MediaQuality,
|
||||
value: "1",
|
||||
},
|
||||
]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
@@ -2376,4 +2382,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 5, 6).unwrap());
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 4, 21).unwrap());
|
||||
|
||||
@@ -332,7 +332,7 @@ fn inner_generate_secure_join_qr_code(
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
|
||||
d.attr("r", HALF_LOGO_SIZE)?;
|
||||
d.attr("style", format!("fill:{color}"))
|
||||
d.attr("style", format!("fill:{}", &color))
|
||||
})?;
|
||||
|
||||
let avatar_font_size = LOGO_SIZE * 0.65;
|
||||
|
||||
@@ -902,8 +902,10 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
}
|
||||
|
||||
// Get user-configured server deletion
|
||||
let delete_server_after = context.get_config_delete_server_after().await?;
|
||||
|
||||
if !received_msg.msg_ids.is_empty() {
|
||||
let target = if received_msg.needs_delete_job {
|
||||
let target = if received_msg.needs_delete_job || delete_server_after == Some(0) {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
None
|
||||
@@ -3558,7 +3560,12 @@ async fn create_or_lookup_mailinglist_or_broadcast(
|
||||
mime_parser.timestamp_sent,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to create mailinglist '{name}' for grpid={listid}",))?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create mailinglist '{}' for grpid={}",
|
||||
&name, &listid
|
||||
)
|
||||
})?;
|
||||
|
||||
if chattype == Chattype::InBroadcast {
|
||||
chat::add_to_chat_contacts_table(
|
||||
|
||||
@@ -1976,10 +1976,15 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> {
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
|
||||
bob.set_config_bool(Config::BccSelf, true).await?;
|
||||
bob.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
|
||||
bob.set_config(Config::DeleteServerAfter, None).await?;
|
||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::config::Config;
|
||||
use crate::contact::{ContactId, RecentlySeenLoop};
|
||||
use crate::context::Context;
|
||||
use crate::download::{download_known_post_messages_without_pre_message, download_msgs};
|
||||
use crate::ephemeral;
|
||||
use crate::ephemeral::{self, delete_expired_imap_messages};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{Imap, session::Session};
|
||||
use crate::location;
|
||||
@@ -484,6 +484,14 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session)
|
||||
.await
|
||||
.context("fetch_move_delete")?;
|
||||
|
||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
||||
// otherwise slow down message fetching.
|
||||
delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
|
||||
download_msgs(ctx, &mut session)
|
||||
.await
|
||||
|
||||
@@ -218,7 +218,7 @@ pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStor
|
||||
impl fmt::Debug for ConnectivityStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(guard) = self.0.try_lock() {
|
||||
write!(f, "ConnectivityStore {:?}", *guard)
|
||||
write!(f, "ConnectivityStore {:?}", &*guard)
|
||||
} else {
|
||||
write!(f, "ConnectivityStore [LOCKED]")
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ fn shorten_name(name: &str, length: usize) -> String {
|
||||
// We use _ rather than ... to avoid dots at the end of the URL, which would confuse linkifiers
|
||||
format!(
|
||||
"{}_",
|
||||
name.chars()
|
||||
&name
|
||||
.chars()
|
||||
.take(length.saturating_sub(1))
|
||||
.collect::<String>()
|
||||
)
|
||||
|
||||
@@ -992,7 +992,7 @@ async fn test_wrong_auth_token() -> Result<()> {
|
||||
tcm.send_recv(alice, bob, "hi").await;
|
||||
|
||||
let alice_qr = get_securejoin_qr(alice, None).await?;
|
||||
println!("{alice_qr}");
|
||||
println!("{}", &alice_qr);
|
||||
let invalid_alice_qr = alice_qr.replace("&s=", "&s=INVALIDAUTHTOKEN&someotherkey=");
|
||||
|
||||
join_securejoin(bob, &invalid_alice_qr).await?;
|
||||
|
||||
34
src/smtp.rs
34
src/smtp.rs
@@ -699,22 +699,26 @@ pub(crate) async fn add_self_recipients(
|
||||
recipients: &mut Vec<String>,
|
||||
encrypted: bool,
|
||||
) -> Result<()> {
|
||||
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||
// messages.
|
||||
if encrypted {
|
||||
for addr in context.get_published_secondary_self_addrs().await? {
|
||||
recipients.push(addr);
|
||||
// Previous versions of Delta Chat did not send BCC self
|
||||
// if DeleteServerAfter was set to immediately delete messages
|
||||
// from the server. This is not the case anymore
|
||||
// because BCC-self messages are also used to detect
|
||||
// that message was sent if SMTP server is slow to respond
|
||||
// and connection is frequently lost
|
||||
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
||||
// disabled by default is fine.
|
||||
if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() {
|
||||
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||
// messages.
|
||||
if encrypted {
|
||||
for addr in context.get_published_secondary_self_addrs().await? {
|
||||
recipients.push(addr);
|
||||
}
|
||||
}
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
recipients.push(from);
|
||||
}
|
||||
// `from` must be the last addr
|
||||
// because `receive_imf_inner()` marks the message as 'delivered'
|
||||
// if it arrives to the self-server via `bcc_self`.
|
||||
// This helps with marking messages as delivered
|
||||
// if the server is slow and we never get an `OK` response
|
||||
// before the connection times out.
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
recipients.push(from);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -137,17 +137,6 @@ impl TestContextManager {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a new "device" with a preconfigured v6 PQC key.
|
||||
pub async fn pqc(&mut self) -> TestContext {
|
||||
TestContext::builder()
|
||||
.with_key_pair(pqc_keypair())
|
||||
.with_address("pqc@example.org".to_string())
|
||||
.with_id_offset(7000)
|
||||
.with_log_sink(self.log_sink.clone())
|
||||
.build(Some(&mut self.used_names))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates a new unconfigured test account.
|
||||
pub async fn unconfigured(&mut self) -> TestContext {
|
||||
TestContext::builder()
|
||||
@@ -315,9 +304,6 @@ impl TestContextManager {
|
||||
pub struct TestContextBuilder {
|
||||
key_pair: Option<SignedSecretKey>,
|
||||
|
||||
/// Email address.
|
||||
address: Option<String>,
|
||||
|
||||
/// Log sink if set.
|
||||
///
|
||||
/// If log sink is not set,
|
||||
@@ -342,7 +328,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(alice_keypair())`.
|
||||
pub fn configure_alice(self) -> Self {
|
||||
self.with_key_pair(alice_keypair())
|
||||
.with_address("alice@example.org".to_string())
|
||||
}
|
||||
|
||||
/// Configures as bob@example.net with fixed secret key.
|
||||
@@ -350,7 +335,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(bob_keypair())`.
|
||||
pub fn configure_bob(self) -> Self {
|
||||
self.with_key_pair(bob_keypair())
|
||||
.with_address("bob@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as charlie@example.net with fixed secret key.
|
||||
@@ -358,7 +342,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(charlie_keypair())`.
|
||||
pub fn configure_charlie(self) -> Self {
|
||||
self.with_key_pair(charlie_keypair())
|
||||
.with_address("charlie@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as dom@example.net with fixed secret key.
|
||||
@@ -366,7 +349,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(dom_keypair())`.
|
||||
pub fn configure_dom(self) -> Self {
|
||||
self.with_key_pair(dom_keypair())
|
||||
.with_address("dom@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as elena@example.net with fixed secret key.
|
||||
@@ -374,7 +356,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(elena_keypair())`.
|
||||
pub fn configure_elena(self) -> Self {
|
||||
self.with_key_pair(elena_keypair())
|
||||
.with_address("elena@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures as fiona@example.net with fixed secret key.
|
||||
@@ -382,7 +363,6 @@ impl TestContextBuilder {
|
||||
/// This is a shortcut for `.with_key_pair(fiona_keypair())`.
|
||||
pub fn configure_fiona(self) -> Self {
|
||||
self.with_key_pair(fiona_keypair())
|
||||
.with_address("fiona@example.net".to_string())
|
||||
}
|
||||
|
||||
/// Configures the new [`TestContext`] with the provided [`SignedSecretKey`].
|
||||
@@ -394,12 +374,6 @@ impl TestContextBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets email address.
|
||||
pub fn with_address(mut self, address: String) -> Self {
|
||||
self.address = Some(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attaches a [`LogSink`] to this [`TestContext`].
|
||||
///
|
||||
/// This is useful when using multiple [`TestContext`] instances in one test: it allows
|
||||
@@ -422,7 +396,16 @@ impl TestContextBuilder {
|
||||
/// Builds the [`TestContext`].
|
||||
pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext {
|
||||
if let Some(key_pair) = self.key_pair {
|
||||
let addr = self.address.expect("Address is not set").clone();
|
||||
let userid = {
|
||||
let public_key = key_pair.to_public_key();
|
||||
let id_bstr = public_key.details.users.first().unwrap().id.id();
|
||||
String::from_utf8(id_bstr.to_vec()).unwrap()
|
||||
};
|
||||
let addr = mailparse::addrparse(&userid)
|
||||
.unwrap()
|
||||
.extract_single_info()
|
||||
.unwrap()
|
||||
.addr;
|
||||
let name = EmailAddress::new(&addr).unwrap().local;
|
||||
|
||||
let mut unused_name = name.clone();
|
||||
@@ -1437,13 +1420,6 @@ pub fn fiona_keypair() -> SignedSecretKey {
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap()
|
||||
}
|
||||
|
||||
/// Loads a pre-generated v6 PQC keypair from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn pqc_keypair() -> SignedSecretKey {
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/pqc-secret.asc")).unwrap()
|
||||
}
|
||||
|
||||
/// Utility to help wait for and retrieve events.
|
||||
///
|
||||
/// This buffers the events in order they are emitted. This allows consuming events in
|
||||
@@ -1707,7 +1683,7 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
if msg.has_location() { "📍" } else { "" },
|
||||
contact_name,
|
||||
&contact_name,
|
||||
contact_id,
|
||||
msgtext,
|
||||
if msg.get_from_id() == ContactId::SELF {
|
||||
|
||||
@@ -161,7 +161,7 @@ async fn check_that_transition_worked(
|
||||
2,
|
||||
"Group {} has members {:?}, but should have members {:?} and {:?}",
|
||||
group,
|
||||
members,
|
||||
&members,
|
||||
alice_contact_id,
|
||||
ContactId::SELF
|
||||
);
|
||||
|
||||
@@ -62,13 +62,13 @@ pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<'_, str> {
|
||||
if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) {
|
||||
Cow::Owned(format!(
|
||||
"{}{}",
|
||||
buf.get(..=index).unwrap_or_default(),
|
||||
&buf.get(..=index).unwrap_or_default(),
|
||||
DC_ELLIPSIS
|
||||
))
|
||||
} else {
|
||||
Cow::Owned(format!(
|
||||
"{}{}",
|
||||
buf.get(..end_pos).unwrap_or_default(),
|
||||
&buf.get(..end_pos).unwrap_or_default(),
|
||||
DC_ELLIPSIS
|
||||
))
|
||||
}
|
||||
|
||||
@@ -247,12 +247,12 @@ proptest! {
|
||||
assert!(
|
||||
l <= approx_chars + el_len,
|
||||
"buf: '{}' - res: '{}' - len {}, approx {}",
|
||||
buf, res, res.len(), approx_chars
|
||||
&buf, &res, res.len(), approx_chars
|
||||
);
|
||||
|
||||
if buf.chars().count() > approx_chars + el_len {
|
||||
let l = res.len();
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {res}");
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ pub(crate) struct ConnectionCandidate {
|
||||
|
||||
impl fmt::Display for ConnectionCandidate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.host, self.port, self.security)?;
|
||||
write!(f, "{}:{}:{}", &self.host, self.port, self.security)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ pub(crate) struct ConfiguredServerLoginParam {
|
||||
|
||||
impl fmt::Display for ConfiguredServerLoginParam {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}:{}", self.connection, self.user)?;
|
||||
write!(f, "{}:{}", self.connection, &self.user)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
xUsGaf8NSRsAAAAgYy+GaofURMeV0+bcZZGY2ZdAamU+LG69ONjd3haVU3cAhm6G
|
||||
IT/UEgFgVdPEhiXER9cfPLiCgkiw/L5mrAZfuLfCqgYfGwgAAABLBQJp/w1JIiEG
|
||||
hys0q6D+DFWPnwQoWtuX0mL6ovH2kCjWmDufAFmB0+QCGwMCHgkECwkIBwYVCg4J
|
||||
CAwBFg0nCQIIAgcCCQEIAQcBAAAAAEGQEO9Py9Q7njj1WXhtn1wMJSLBdHBE+qQu
|
||||
RaCaiWkY5l4EWLlVRPAjX2bBSGq6n3+M+H6oFpOHETAX8IcFSxc260UD+PM0jQpV
|
||||
H6ReNy7PBCQKx8RrBmn/DUkjAAAEwPmkVcPy1ye0/7D9nDQCkENUGry97iLkpcw/
|
||||
tLJfzL5gJAdzrPkDkyukHxrO7kiUx+mzpiGZRZeyRgBd5YQ+mTgGrptxXLFHcKFR
|
||||
79Fjg1UjgHEFjxCkCHUfnNcGZVM3p5skESnNgzsgFGiODfKhM4ew3AFgkUc5LNZj
|
||||
Zgpgt4ETIhylbLUY89ccfNpKnQeJl3cv8lvA/yqhoUutJXwZQ/qYKFnEIGEBTFto
|
||||
hLZn0KauF9KYOYvOV4yjeZQBlxSPNAWj9SqSNcalpTUFzwoQVSsqWwiys1PEzGAu
|
||||
twQVKsZ3e/hlZAyR4eGMiYEmCEy7qjuaOJsqHQuW7hdOHWdVRUpRHOtfj3QAzdc0
|
||||
CehVbyCRJVwnTSKiT3AYsdACH8U7mhI5/VxeSHNRIDN1Y6g6N5sx6Wur/HuKGFwx
|
||||
L4urdPdpJJgyLXR8GUkL/yeqUhogu4mbVAmULbq2BCIKFNpMyGdhnDugN6Sp5MWc
|
||||
GOxCW7CASuBYPHW/rto0C4M/3gCtN2sPtRAhOsXNBBhMqLlzzCgawulCiGtNjHUK
|
||||
HsVhghgYwKRBT7vLSKDNsCVizzoZxNQq8yUEXpFIRsTGt3wYoigZn4wOSpmQbxGe
|
||||
P3Uc2GWuuukCBNEP5oW4+TCFaNw5mvZgZwl5n4K34poxVgpqBIM2m2fEu8oyLPJZ
|
||||
bBxnbty3MUAdLpxv+0otGSHJF4xa3lsEyUdr6+JZZXohNXKoyjeJMGo6qPkvCADI
|
||||
upMnDSYZeLU5bVstHWS6otuRMEcjdLBkYfqfzBhkzbptscaUXzsaK4cd/iQzAA1r
|
||||
A0ygvcA78Vo363cElNAJh3lntrZZGpBYnzcU/zLACKAVJCYPy3Cj8Al8x+gHP0Yr
|
||||
ZSOYdZA1q9s2Kuqk7upCpcYDZ+uXGZs3ubA0TYCcO3FKhAwLhzJad5WApBFETYt2
|
||||
3KJEwgEjQaCs7sNNiwaKxhLC2VJhUckgluGs5iUu9ck5jdU+N9MqTmloF/u2Gok8
|
||||
QEqF9+DBhPg/fJoI9sN8sIyLrksEUQsm59mvJbVWOpxtbwpWZ+J4cat4azHE0khy
|
||||
rolL6lZqDJYW4xVeoAVl5iccicjE6mJLemoxf6iJdohi5cN5JXyZtgtdsbIesJib
|
||||
BLPJVahmv5W1Q4RmrwEp5Ua4xra5Mcac4PeINTOkGMErIhdvnuxEH/Cxd8VKhNlU
|
||||
vdty4MOyUOkRRPhOMNKUyTkwS9yjprK3QbhEJgrJygHCGpQ0jwp9PrtKqNnOONSX
|
||||
t4ZORuiAYHDFz3DPlJhLLzNoAJse0RAyolkPThoMl2JlY5ci8pVHb+Ed/kaeFxnE
|
||||
UJJIOZvDFfNCFCM5CCXG/2pi/icA7nHPDFVBeYPMz1B5vrgmdDNMMFVQNMBtrroT
|
||||
4pi1N5U4+EAv1vah40akQ/iFcfZjt4sE/jG0M0NCWqHBDPO7e0ae+2IqnIJmsHjL
|
||||
N1ak3egY00CnRHdrPCkOkhooFIHA1hYxIwyP07qfkhUBNwSqZ4AfF8UW/nuLjeaj
|
||||
ajEWz3zGLvQpfHSobEGPQKk+eIA1fOVeAuJAUAmJz5YO1Dk4OfczeQqQiOhtv+qe
|
||||
PYaZQfBFJVamGocDHomPQkP/IvAJhuO9xWPapqbdRwGfVRJZgGsAy89mT1w0PU1C
|
||||
u6VpIoyZB2J9LZkw9qb9sRRJAr2gpWGBD4CCmPZ8d17ZGDcIr8o+eI+bo5eKf+1j
|
||||
6NhsjM7AmIccStNxZYWE4ZucvYYbPvT3ns/TNa7BH2DBqfGK84PawosGGBsIAAAA
|
||||
LAUCaf8NSQIbDCIhBocrNKug/gxVj58EKFrbl9Ji+qLx9pAo1pg7nwBZgdPkAAAA
|
||||
ADrcEIqnwTwJoiZAxzK+w7uQFHzsYMWIj8x+DKsn7D1silKINHDnFSrlSKRtbAW6
|
||||
x9+HrN/nvR7bOnXZvZhz7lQ3Lp3YUdzEcqRMj8BWW8IXdm0C
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
Reference in New Issue
Block a user