mirror of
https://github.com/chatmail/core.git
synced 2026-04-07 08:02:11 +03:00
Compare commits
35 Commits
wofwca/don
...
link2xt/le
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4183de4d4b | ||
|
|
3e52aa1eaa | ||
|
|
d2097d3523 | ||
|
|
1219cbe1a3 | ||
|
|
bc48b17e93 | ||
|
|
7233b4b811 | ||
|
|
d1e0088201 | ||
|
|
a5e41b0b49 | ||
|
|
2f76fd98dd | ||
|
|
6235f2a01a | ||
|
|
ec5117a6c2 | ||
|
|
d6e3a8829b | ||
|
|
2340818488 | ||
|
|
f175d2fed9 | ||
|
|
d318bbb0f4 | ||
|
|
a0f14a5978 | ||
|
|
7e49033f92 | ||
|
|
626ac8161a | ||
|
|
28cce5e31d | ||
|
|
3b87e27f34 | ||
|
|
24b21c0588 | ||
|
|
eb666d4cc3 | ||
|
|
ef265689dd | ||
|
|
49223792f9 | ||
|
|
920da083d1 | ||
|
|
8f1bf963b4 | ||
|
|
e33d50b4e0 | ||
|
|
f1dc03a4ee | ||
|
|
5d90cc7a2a | ||
|
|
68e630eb82 | ||
|
|
ef718bb869 | ||
|
|
f1860f90d4 | ||
|
|
a947f4296f | ||
|
|
8c3139f7a2 | ||
|
|
3dd7defaa1 |
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## [2.48.0] - 2026-03-30
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix reordering problems in multi-relay setups by not sorting received messages below the last seen one.
|
||||
- Always sort "Messages are end-to-end encrypted" notice to the beginning.
|
||||
- Make Message-ID of pre-messages stable across resends ([#8007](https://github.com/chatmail/core/pull/8007)).
|
||||
- Delete `imap_markseen` entries not corresponding to any `imap` rows.
|
||||
- Cleanup `imap` and `imap_sync` records without transport in housekeeping.
|
||||
- When receiving MDN, mark all preceding messages as noticed, even having same timestamp ([#7928](https://github.com/chatmail/core/pull/7928)).
|
||||
- Remove migration 108 preventing upgrades from core 1.86.0 to the latest version.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Improve IMAP loop logs.
|
||||
- Add decryption error to the device message about outgoing message decryption failure.
|
||||
- Log received message sort timestamp.
|
||||
|
||||
### Performance
|
||||
|
||||
- Move sorting outside of SQL query in `store_seen_flags_on_imap`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC API `markfresh_chat()`.
|
||||
- ffi: Correctly declare `dc_event_channel_new()` as having no params ([#7831](https://github.com/chatmail/core/pull/7831)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `wal_checkpoint_mutex`, lock `write_mutex` before getting sql connection instead.
|
||||
- Replace async `RwLock` with sync `RwLock` for stock strings.
|
||||
- Cleanup remaining Autocrypt Setup Message processing in `mimeparser`.
|
||||
- SecureJoin: do not check for self address in forwarding protection.
|
||||
- Fix clippy warnings.
|
||||
|
||||
### CI
|
||||
|
||||
- Update {c,py}.delta.chat website deployments.
|
||||
- Use environments for {rs,cffi,js.jsonrpc}.delta.chat deployments.
|
||||
- Fix https://docs.zizmor.sh/audits/#bot-conditions.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add SQL performance tips to STYLE.md.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove `test_old_message_5`.
|
||||
- Do not rely on loading newest chat in `load_imf_email()`.
|
||||
- Use `load_imf_email()` more.
|
||||
- The message is sorted correctly in the chat even if it arrives late.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: update rustls-webpki to 0.103.10.
|
||||
|
||||
## [2.47.0] - 2026-03-24
|
||||
|
||||
### Fixes
|
||||
@@ -7984,3 +8040,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.45.0]: https://github.com/chatmail/core/compare/v2.44.0..v2.45.0
|
||||
[2.46.0]: https://github.com/chatmail/core/compare/v2.45.0..v2.46.0
|
||||
[2.47.0]: https://github.com/chatmail/core/compare/v2.46.0..v2.47.0
|
||||
[2.48.0]: https://github.com/chatmail/core/compare/v2.47.0..v2.48.0
|
||||
|
||||
54
Cargo.lock
generated
54
Cargo.lock
generated
@@ -827,9 +827,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
@@ -1307,7 +1307,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1416,7 +1416,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1437,7 +1437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1453,7 +1453,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1482,7 +1482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -2860,9 +2860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.9"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
@@ -3260,9 +3260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -3483,9 +3483,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.7.5"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
@@ -4235,18 +4235,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
|
||||
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4615,9 +4615,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
|
||||
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"num-traits",
|
||||
@@ -4729,9 +4729,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -5988,9 +5988,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.26.0"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
@@ -6144,9 +6144,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -6403,9 +6403,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
@@ -181,7 +181,7 @@ harness = false
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.43", default-features = false }
|
||||
chrono = { version = "0.4.44", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
@@ -198,7 +198,7 @@ rusqlite = "0.37"
|
||||
sanitize-filename = "0.6"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.25.0"
|
||||
tempfile = "3.27.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.18"
|
||||
|
||||
6
STYLE.md
6
STYLE.md
@@ -68,6 +68,12 @@ keyword doesn't help here.
|
||||
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
|
||||
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
|
||||
|
||||
When changing complex SQL queries, test them on a new database with `EXPLAIN QUERY PLAN`
|
||||
to make sure that indexes are used and large tables are not going to be scanned.
|
||||
Never run `ANALYZE` on the databases,
|
||||
this makes query planner unpredictable
|
||||
and may make performance significantly worse: <https://github.com/chatmail/core/issues/6585>
|
||||
|
||||
## Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -364,18 +364,14 @@ uint32_t dc_get_id (dc_context_t* context);
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
*
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per context.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* may or may not be available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
||||
|
||||
@@ -3323,18 +3319,14 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
|
||||
@@ -4981,17 +4973,6 @@ uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
|
||||
*/
|
||||
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Force the message to be sent in plain text.
|
||||
*
|
||||
* This API is for bots, there is no need to expose it in the UI.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
*/
|
||||
void dc_msg_force_plaintext (dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
*
|
||||
@@ -5979,21 +5960,14 @@ void dc_event_channel_unref(dc_event_channel_t* event_channel);
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
*
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_event_channel_t
|
||||
* @param The event channel.
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager / event channel.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);
|
||||
|
||||
|
||||
@@ -4054,16 +4054,6 @@ pub unsafe extern "C" fn dc_msg_get_saved_msg_id(msg: *const dc_msg_t) -> u32 {
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_force_plaintext()");
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_msg.message.force_plaintext();
|
||||
}
|
||||
|
||||
// dc_contact_t
|
||||
|
||||
/// FFI struct for [dc_contact_t]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.48.0-dev"
|
||||
"version": "2.48.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -1047,6 +1047,7 @@ def test_no_old_msg_is_fresh(acfactory):
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
ac1_clone.wait_for_incoming_msg_event()
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
|
||||
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.48.0-dev"
|
||||
"version": "2.48.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.48.0-dev"
|
||||
version = "2.48.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -254,10 +254,6 @@ class Message:
|
||||
"""Quote setter."""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message."""
|
||||
|
||||
@@ -288,6 +288,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
chat2.accept()
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
lp.sec("ac1: receiving message")
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-03-24
|
||||
2026-03-30
|
||||
36
src/blob.rs
36
src/blob.rs
@@ -10,8 +10,8 @@ use anyhow::{Context as _, Result, ensure, format_err};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::ImageReader;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::{fs, task};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
@@ -362,7 +362,10 @@ impl<'a> BlobObject<'a> {
|
||||
return Ok(name);
|
||||
}
|
||||
let mut img = imgreader.decode().context("image decode failure")?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let orientation = exif
|
||||
.as_ref()
|
||||
.map(|exif| exif_orientation(exif, context))
|
||||
.unwrap_or(Orientation::NoTransforms);
|
||||
let mut encoded = Vec::new();
|
||||
|
||||
if *vt == Viewtype::Sticker {
|
||||
@@ -381,13 +384,7 @@ impl<'a> BlobObject<'a> {
|
||||
return Ok(name);
|
||||
}
|
||||
}
|
||||
|
||||
img = match orientation {
|
||||
Some(90) => img.rotate90(),
|
||||
Some(180) => img.rotate180(),
|
||||
Some(270) => img.rotate270(),
|
||||
_ => img,
|
||||
};
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
||||
// target_wh target-resolution for resizing the image.
|
||||
@@ -551,18 +548,17 @@ fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
|
||||
Ok((len, exif))
|
||||
}
|
||||
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
||||
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
// we only use rotation, in practise, flipping is not used.
|
||||
match orientation.value.get_uint(0) {
|
||||
Some(3) => return 180,
|
||||
Some(6) => return 90,
|
||||
Some(8) => return 270,
|
||||
other => warn!(context, "Exif orientation value ignored: {other:?}."),
|
||||
}
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation {
|
||||
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)
|
||||
&& let Some(val) = orientation.value.get_uint(0)
|
||||
&& let Ok(val) = TryInto::<u8>::try_into(val)
|
||||
{
|
||||
return Orientation::from_exif(val).unwrap_or({
|
||||
warn!(context, "Exif orientation value ignored: {val:?}.");
|
||||
Orientation::NoTransforms
|
||||
});
|
||||
}
|
||||
0
|
||||
Orientation::NoTransforms
|
||||
}
|
||||
|
||||
/// All files in the blobdir.
|
||||
|
||||
@@ -305,7 +305,7 @@ async fn test_recode_image_2() {
|
||||
has_exif: true,
|
||||
original_width: 2000,
|
||||
original_height: 1800,
|
||||
orientation: 270,
|
||||
orientation: Some(Orientation::Rotate270),
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
@@ -336,6 +336,28 @@ async fn test_recode_image_2() {
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_vflipped() {
|
||||
let bytes = include_bytes!("../../test-data/image/rectangle200x180-vflipped.jpg");
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 200,
|
||||
original_height: 180,
|
||||
orientation: Some(Orientation::FlipVertical),
|
||||
compressed_width: 200,
|
||||
compressed_height: 180,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_bad_exif() {
|
||||
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
|
||||
@@ -530,7 +552,7 @@ struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) has_exif: bool,
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) orientation: Option<Orientation>,
|
||||
pub(crate) res_viewtype: Option<Viewtype>,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
@@ -546,7 +568,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
let has_exif = self.has_exif;
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation;
|
||||
let orientation = self.orientation.unwrap_or(Orientation::NoTransforms);
|
||||
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
|
||||
76
src/chat.rs
76
src/chat.rs
@@ -477,12 +477,17 @@ impl ChatId {
|
||||
/// Adds message "Messages are end-to-end encrypted".
|
||||
pub(crate) async fn add_e2ee_notice(self, context: &Context, timestamp: i64) -> Result<()> {
|
||||
let text = stock_str::messages_e2ee_info_msg(context);
|
||||
|
||||
// Sort this notice to the very beginning of the chat.
|
||||
// We don't want any message to appear before this notice
|
||||
// which is normally added when encrypted chat is created.
|
||||
let sort_timestamp = 0;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
SystemMessage::ChatE2ee,
|
||||
Some(timestamp),
|
||||
Some(sort_timestamp),
|
||||
timestamp,
|
||||
None,
|
||||
None,
|
||||
@@ -925,6 +930,17 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Returns timestamp of us joining the chat if we are the member of the chat.
|
||||
pub(crate) async fn join_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT add_timestamp FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
(self, ContactId::SELF),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat,
|
||||
/// including hidden messages or a draft if there is one.
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
@@ -1212,15 +1228,11 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
/// corresponding event in case of a system message (usually the current system time).
|
||||
/// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
|
||||
/// to the chat bottom.
|
||||
/// `received` -- whether the message is received. Otherwise being sent.
|
||||
/// `incoming` -- whether the message is incoming.
|
||||
pub(crate) async fn calc_sort_timestamp(
|
||||
self,
|
||||
context: &Context,
|
||||
message_timestamp: i64,
|
||||
always_sort_to_bottom: bool,
|
||||
received: bool,
|
||||
incoming: bool,
|
||||
) -> Result<i64> {
|
||||
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
|
||||
|
||||
@@ -1240,38 +1252,6 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?
|
||||
} else if received {
|
||||
// Received messages shouldn't mingle with just sent ones and appear somewhere in the
|
||||
// middle of the chat, so we go after the newest non fresh message.
|
||||
//
|
||||
// But if a received outgoing message is older than some seen message, better sort the
|
||||
// received message purely by timestamp. We could place it just before that seen
|
||||
// message, but anyway the user may not notice it.
|
||||
//
|
||||
// NB: Received outgoing messages may break sorting of fresh incoming ones, but this
|
||||
// shouldn't happen frequently. Seen incoming messages don't really break sorting of
|
||||
// fresh ones, they rather mean that older incoming messages are actually seen as well.
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
|
||||
FROM msgs
|
||||
WHERE chat_id=? AND hidden=0 AND state>?
|
||||
HAVING COUNT(*) > 0",
|
||||
(MessageState::InSeen, self, MessageState::InFresh),
|
||||
|row| {
|
||||
let ts: i64 = row.get(0)?;
|
||||
let ts_sent_seen: i64 = row.get(1)?;
|
||||
Ok((ts, ts_sent_seen))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.and_then(|(ts, ts_sent_seen)| {
|
||||
match incoming || ts_sent_seen <= message_timestamp {
|
||||
true => Some(ts),
|
||||
false => None,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1282,7 +1262,16 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
sort_timestamp = last_msg_time;
|
||||
}
|
||||
|
||||
Ok(sort_timestamp)
|
||||
if let Some(join_timestamp) = self.join_timestamp(context).await? {
|
||||
// If we are the member of the chat, don't add messages
|
||||
// before the timestamp of us joining it.
|
||||
// This is needed to avoid sorting "Member added"
|
||||
// or automatically sent bot welcome messages
|
||||
// above SecureJoin system messages.
|
||||
Ok(std::cmp::max(sort_timestamp, join_timestamp))
|
||||
} else {
|
||||
Ok(sort_timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4524,6 +4513,8 @@ pub async fn forward_msgs_2ctx(
|
||||
msg.param.steal(param, Param::Height);
|
||||
msg.param.steal(param, Param::Duration);
|
||||
msg.param.steal(param, Param::MimeType);
|
||||
msg.param.steal(param, Param::ProtectQuote);
|
||||
msg.param.steal(param, Param::Quote);
|
||||
msg.param.steal(param, Param::Summary1);
|
||||
if msg.has_html() {
|
||||
msg.set_html(src_msg_id.get_html(ctx_src).await?);
|
||||
@@ -4936,15 +4927,8 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
ts
|
||||
} else {
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
chat_id
|
||||
.calc_sort_timestamp(
|
||||
context,
|
||||
smeared_time(context),
|
||||
sort_to_bottom,
|
||||
received,
|
||||
incoming,
|
||||
)
|
||||
.calc_sort_timestamp(context, smeared_time(context), sort_to_bottom)
|
||||
.await?
|
||||
};
|
||||
|
||||
|
||||
@@ -2300,11 +2300,14 @@ async fn test_forward_quote() -> Result<()> {
|
||||
let forwarded_msg = alice.pop_sent_msg().await;
|
||||
let alice_forwarded_msg = bob.recv_msg(&forwarded_msg).await;
|
||||
assert!(alice_forwarded_msg.quoted_message(&alice).await?.is_none());
|
||||
assert!(alice_forwarded_msg.quoted_text().is_none());
|
||||
assert_eq!(
|
||||
alice_forwarded_msg.quoted_text(),
|
||||
Some("Hi Bob".to_string())
|
||||
);
|
||||
|
||||
let bob_forwarded_msg = bob.get_last_msg().await;
|
||||
assert!(bob_forwarded_msg.quoted_message(&bob).await?.is_none());
|
||||
assert!(bob_forwarded_msg.quoted_text().is_none());
|
||||
assert_eq!(bob_forwarded_msg.quoted_text(), Some("Hi Bob".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2789,7 +2792,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&vc_pubkey).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
assert!(parsed_by_bob.decryption_error.is_some());
|
||||
|
||||
charlie.recv_msg_trash(&vc_pubkey).await;
|
||||
}
|
||||
@@ -2818,7 +2821,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&member_added).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
assert!(parsed_by_bob.decryption_error.is_some());
|
||||
|
||||
let rcvd = charlie.recv_msg(&member_added).await;
|
||||
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberAddedToGroup);
|
||||
@@ -2833,7 +2836,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&hi_msg).await;
|
||||
assert_eq!(parsed_by_bob.decrypting_failed, false);
|
||||
assert!(parsed_by_bob.decryption_error.is_none());
|
||||
}
|
||||
|
||||
tcm.section("Alice removes Charlie. Bob must not see it.");
|
||||
@@ -2850,7 +2853,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&member_removed).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
assert!(parsed_by_bob.decryption_error.is_some());
|
||||
|
||||
let rcvd = charlie.recv_msg(&member_removed).await;
|
||||
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberRemovedFromGroup);
|
||||
@@ -3765,14 +3768,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
// The contact should be marked as verified.
|
||||
check_direct_chat_is_hidden_and_contact_is_verified(alice, bob0).await;
|
||||
check_direct_chat_is_hidden_and_contact_is_verified(bob0, alice).await;
|
||||
|
||||
// TODO: There is a known bug in `observe_securejoin_on_other_device()`:
|
||||
// When Bob joins a group or broadcast with his first device,
|
||||
// then a chat with Alice will pop up on his second device.
|
||||
// When it's fixed, the 2 following lines can be replaced with
|
||||
// `check_direct_chat_is_hidden_and_contact_is_verified(bob1, alice).await;`
|
||||
let bob1_alice_contact = bob1.add_or_lookup_contact_no_key(alice).await;
|
||||
assert!(bob1_alice_contact.is_verified(bob1).await.unwrap());
|
||||
check_direct_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
@@ -6109,3 +6105,54 @@ async fn test_leftgrps() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if the message arrives late,
|
||||
/// it can still be sorted above the last seen message.
|
||||
///
|
||||
/// Versions 2.47 and below always sorted incoming messages
|
||||
/// after the last seen message, but with
|
||||
/// the introduction of multi-relay it is possible
|
||||
/// that some messages arrive only to some relays
|
||||
/// and are fetched after the already arrived seen message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_late_message_above_seen() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members("Group", &[bob, charlie])
|
||||
.await;
|
||||
let alice_sent = alice.send_text(alice_chat_id, "Hello everyone!").await;
|
||||
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
let charlie_chat_id = charlie.recv_msg(&alice_sent).await.chat_id;
|
||||
charlie_chat_id.accept(charlie).await?;
|
||||
|
||||
// Bob sends a message, but the message is delayed.
|
||||
let bob_sent = bob.send_text(bob_chat_id, "Hello from Bob!").await;
|
||||
SystemTime::shift(Duration::from_secs(1000));
|
||||
|
||||
let charlie_sent = charlie
|
||||
.send_text(charlie_chat_id, "Hello from Charlie!")
|
||||
.await;
|
||||
|
||||
// Alice immediately receives a message from Charlie and reads it.
|
||||
let alice_received_from_charlie = alice.recv_msg(&charlie_sent).await;
|
||||
assert_eq!(
|
||||
alice_received_from_charlie.get_text(),
|
||||
"Hello from Charlie!"
|
||||
);
|
||||
message::markseen_msgs(alice, vec![alice_received_from_charlie.id]).await?;
|
||||
|
||||
// Bob message arrives later, it should be above the message from Charlie.
|
||||
let alice_received_from_bob = alice.recv_msg(&bob_sent).await;
|
||||
assert_eq!(alice_received_from_bob.get_text(), "Hello from Bob!");
|
||||
|
||||
// The last message in the chat is still from Charlie.
|
||||
let last_msg = alice.get_last_msg_in(alice_chat_id).await;
|
||||
assert_eq!(last_msg.get_text(), "Hello from Charlie!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ impl Chatlist {
|
||||
AND c.blocked!=1
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
@@ -168,7 +168,7 @@ impl Chatlist {
|
||||
AND c.blocked!=1
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft,),
|
||||
process_row,
|
||||
)
|
||||
@@ -204,7 +204,7 @@ impl Chatlist {
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?3
|
||||
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
process_row,
|
||||
)
|
||||
@@ -253,7 +253,7 @@ impl Chatlist {
|
||||
AND NOT c.archived=?
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(
|
||||
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
|
||||
Chattype::Group, ContactId::SELF,
|
||||
@@ -279,7 +279,7 @@ impl Chatlist {
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND NOT c.archived=?
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
|
||||
12
src/imex.rs
12
src/imex.rs
@@ -1137,4 +1137,16 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests importing a backup from Delta Chat 1.30.3 for Android (core v1.86.0).
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_import_ancient_backup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = &tcm.unconfigured().await;
|
||||
|
||||
let backup_path = Path::new("test-data/core-1.86.0-backup.tar");
|
||||
imex(context, ImexMode::ImportBackup, backup_path, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,7 +815,11 @@ impl Message {
|
||||
|
||||
/// Returns the timestamp of the message for sorting.
|
||||
pub fn get_sort_timestamp(&self) -> i64 {
|
||||
self.timestamp_sort
|
||||
if self.timestamp_sort != 0 {
|
||||
self.timestamp_sort
|
||||
} else {
|
||||
self.timestamp_sent
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the text of the message.
|
||||
@@ -1315,7 +1319,7 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Force the message to be sent in plain text.
|
||||
pub fn force_plaintext(&mut self) {
|
||||
pub(crate) fn force_plaintext(&mut self) {
|
||||
self.param.set_int(Param::ForcePlaintext, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -232,11 +232,7 @@ impl MimeFactory {
|
||||
if chat.is_self_talk() {
|
||||
to.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
|
||||
encryption_pubkeys = if msg.param.get_bool(Param::ForcePlaintext).unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(Vec::new())
|
||||
};
|
||||
encryption_pubkeys = Some(Vec::new());
|
||||
} else if chat.is_mailing_list() {
|
||||
let list_post = chat
|
||||
.param
|
||||
|
||||
@@ -86,7 +86,9 @@ pub(crate) struct MimeMessage {
|
||||
/// messages to this address to post them to the list.
|
||||
pub list_post: Option<String>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Decryption error if decryption of the message has failed.
|
||||
pub decryption_error: Option<String>,
|
||||
|
||||
/// Valid signature fingerprint if a message is an
|
||||
/// Autocrypt encrypted and signed message and corresponding intended recipient fingerprints
|
||||
@@ -371,7 +373,7 @@ impl MimeMessage {
|
||||
hop_info += "\n\n";
|
||||
hop_info += &dkim_results.to_string();
|
||||
|
||||
let incoming = !context.is_self_addr(&from.addr).await?;
|
||||
let from_is_not_self_addr = !context.is_self_addr(&from.addr).await?;
|
||||
|
||||
let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
|
||||
|
||||
@@ -436,7 +438,7 @@ impl MimeMessage {
|
||||
};
|
||||
|
||||
let mut autocrypt_header = None;
|
||||
if incoming {
|
||||
if from_is_not_self_addr {
|
||||
// See `get_all_addresses_from_header()` for why we take the last valid header.
|
||||
for val in aheader_values.iter().rev() {
|
||||
autocrypt_header = match Aheader::from_str(val) {
|
||||
@@ -467,7 +469,7 @@ impl MimeMessage {
|
||||
None
|
||||
};
|
||||
|
||||
let mut public_keyring = if incoming {
|
||||
let mut public_keyring = if from_is_not_self_addr {
|
||||
if let Some(autocrypt_header) = autocrypt_header {
|
||||
vec![autocrypt_header.public_key]
|
||||
} else {
|
||||
@@ -652,6 +654,15 @@ impl MimeMessage {
|
||||
.into_iter()
|
||||
.last()
|
||||
.map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
|
||||
|
||||
let incoming = if let Some((ref sig_fp, _)) = signature {
|
||||
sig_fp.hex() != key::self_fingerprint(context).await?
|
||||
} else {
|
||||
// rare case of getting a cleartext message
|
||||
// so we determine 'incoming' flag by From-address
|
||||
from_is_not_self_addr
|
||||
};
|
||||
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
headers,
|
||||
@@ -664,7 +675,7 @@ impl MimeMessage {
|
||||
from,
|
||||
incoming,
|
||||
chat_disposition_notification_to,
|
||||
decrypting_failed: mail.is_err(),
|
||||
decryption_error: mail.err().map(|err| format!("{err:#}")),
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signature,
|
||||
@@ -905,7 +916,7 @@ impl MimeMessage {
|
||||
&& let Some(ref subject) = self.get_subject()
|
||||
{
|
||||
let mut prepend_subject = true;
|
||||
if !self.decrypting_failed {
|
||||
if self.decryption_error.is_none() {
|
||||
let colon = subject.find(':');
|
||||
if colon == Some(2)
|
||||
|| colon == Some(3)
|
||||
@@ -946,7 +957,7 @@ impl MimeMessage {
|
||||
self.parse_attachments();
|
||||
|
||||
// See if an MDN is requested from the other side
|
||||
if !self.decrypting_failed
|
||||
if self.decryption_error.is_none()
|
||||
&& !self.parts.is_empty()
|
||||
&& let Some(ref dn_to) = self.chat_disposition_notification_to
|
||||
{
|
||||
@@ -1078,7 +1089,7 @@ impl MimeMessage {
|
||||
#[cfg(test)]
|
||||
/// Returns whether the decrypted data contains the given `&str`.
|
||||
pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
|
||||
assert!(!self.decrypting_failed);
|
||||
assert!(self.decryption_error.is_none());
|
||||
let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
|
||||
decoded_str.contains(s)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ use tokio_io_timeout::TimeoutStream;
|
||||
use url::Url;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::net::session::SessionStream;
|
||||
@@ -93,13 +92,12 @@ impl HttpConfig {
|
||||
}
|
||||
|
||||
fn to_url(&self, scheme: &str) -> String {
|
||||
let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT);
|
||||
if let Some((user, password)) = &self.user_password {
|
||||
let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
|
||||
let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
|
||||
format!("{scheme}://{user}:{password}@{host}:{}", self.port)
|
||||
format!("{scheme}://{user}:{password}@{}:{}", self.host, self.port)
|
||||
} else {
|
||||
format!("{scheme}://{host}:{}", self.port)
|
||||
format!("{scheme}://{}:{}", self.host, self.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,13 +141,12 @@ impl Socks5Config {
|
||||
}
|
||||
|
||||
fn to_url(&self) -> String {
|
||||
let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT);
|
||||
if let Some((user, password)) = &self.user_password {
|
||||
let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
|
||||
let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
|
||||
format!("socks5://{user}:{password}@{host}:{}", self.port)
|
||||
format!("socks5://{user}:{password}@{}:{}", self.host, self.port)
|
||||
} else {
|
||||
format!("socks5://{host}:{}", self.port)
|
||||
format!("socks5://{}:{}", self.host, self.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,6 +562,20 @@ mod tests {
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("socks5://my-proxy.example.org").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Socks5(Socks5Config {
|
||||
host: "my-proxy.example.org".to_string(),
|
||||
port: 1080,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
proxy_config.to_url(),
|
||||
"socks5://my-proxy.example.org:1080".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -598,6 +609,20 @@ mod tests {
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("http://my-proxy.example.org").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Http(HttpConfig {
|
||||
host: "my-proxy.example.org".to_string(),
|
||||
port: 80,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
proxy_config.to_url(),
|
||||
"http://my-proxy.example.org:80".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -631,6 +656,20 @@ mod tests {
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("https://my-proxy.example.org").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Https(HttpConfig {
|
||||
host: "my-proxy.example.org".to_string(),
|
||||
port: 443,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
proxy_config.to_url(),
|
||||
"https://my-proxy.example.org:443".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -892,6 +892,32 @@ async fn test_set_proxy_config_from_qr() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_encode_hyphen_in_proxy_hostnames() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
|
||||
let qr_text = "socks5://my-proxy.example.org";
|
||||
|
||||
let qr = check_qr(t, qr_text).await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://my-proxy.example.org".to_string(),
|
||||
host: "my-proxy.example.org".to_string(),
|
||||
port: 1080,
|
||||
}
|
||||
);
|
||||
|
||||
set_config_from_qr(t, "socks5://my-proxy.example.org").await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some("socks5://my-proxy.example.org:1080".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_shadowsocks() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
@@ -45,6 +45,7 @@ use crate::securejoin::{
|
||||
self, get_secure_join_step, handle_securejoin_handshake, observe_securejoin_on_other_device,
|
||||
};
|
||||
use crate::simplify;
|
||||
use crate::smtp::msg_has_pending_smtp_job;
|
||||
use crate::stats::STATISTICS_BOT_EMAIL;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
@@ -582,14 +583,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
(rfc724_mid_orig, &self_addr),
|
||||
)
|
||||
.await?;
|
||||
if !context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
|
||||
(rfc724_mid_orig,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if !msg_has_pending_smtp_job(context, msg_id).await? {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
return Ok(None);
|
||||
@@ -727,7 +721,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default();
|
||||
|
||||
let allow_creation = if mime_parser.decrypting_failed {
|
||||
let allow_creation = if mime_parser.decryption_error.is_some() {
|
||||
false
|
||||
} else if is_dc_message == MessengerMessage::No
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
@@ -1010,11 +1004,11 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
&& msg.chat_visibility == ChatVisibility::Archived;
|
||||
updated_chats
|
||||
.entry(msg.chat_id)
|
||||
.and_modify(|ts| *ts = cmp::max(*ts, msg.timestamp_sort))
|
||||
.or_insert(msg.timestamp_sort);
|
||||
.and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
|
||||
.or_insert((msg.timestamp_sort, msg.id));
|
||||
}
|
||||
}
|
||||
for (chat_id, timestamp_sort) in updated_chats {
|
||||
for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -1023,12 +1017,13 @@ UPDATE msgs SET state=? WHERE
|
||||
state=? AND
|
||||
hidden=0 AND
|
||||
chat_id=? AND
|
||||
timestamp<?",
|
||||
(timestamp,id)<(?,?)",
|
||||
(
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
chat_id,
|
||||
timestamp_sort,
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -1211,14 +1206,18 @@ async fn decide_chat_assignment(
|
||||
{
|
||||
info!(context, "Call state changed (TRASH).");
|
||||
true
|
||||
} else if mime_parser.decrypting_failed && !mime_parser.incoming {
|
||||
} else if let Some(ref decryption_error) = mime_parser.decryption_error
|
||||
&& !mime_parser.incoming
|
||||
{
|
||||
// Outgoing undecryptable message.
|
||||
let last_time = context
|
||||
.get_config_i64(Config::LastCantDecryptOutgoingMsgs)
|
||||
.await?;
|
||||
let now = tools::time();
|
||||
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
|
||||
let txt = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions.";
|
||||
let txt = format!(
|
||||
"⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions. (Error: {decryption_error}, {rfc724_mid})."
|
||||
);
|
||||
let mut msg = Message::new_text(txt.to_string());
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
@@ -1835,6 +1834,16 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
// Sort message to the bottom if we are not in the chat
|
||||
// so if we are added via QR code scan
|
||||
// the message about our addition goes after all the info messages.
|
||||
// Info messages are sorted by local smeared_timestamp()
|
||||
// which advances quickly during SecureJoin,
|
||||
// while "member added" message may have older timestamp
|
||||
// corresponding to the sender clock.
|
||||
// In practice inviter clock may even be slightly in the past.
|
||||
let sort_to_bottom = !chat.is_self_in_chat(context).await?;
|
||||
|
||||
let is_location_kml = mime_parser.location_kml.is_some();
|
||||
let mut group_changes = match chat.typ {
|
||||
_ if chat.id.is_special() => GroupChangesInfo::default(),
|
||||
@@ -1885,16 +1894,8 @@ async fn add_parts(
|
||||
};
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
|
||||
let sort_to_bottom = false;
|
||||
let received = true;
|
||||
let sort_timestamp = chat_id
|
||||
.calc_sort_timestamp(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
sort_to_bottom,
|
||||
received,
|
||||
mime_parser.incoming,
|
||||
)
|
||||
.calc_sort_timestamp(context, mime_parser.timestamp_sent, sort_to_bottom)
|
||||
.await?;
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
@@ -2290,7 +2291,7 @@ RETURNING id
|
||||
if trash { 0 } else { ephemeral_timestamp },
|
||||
if trash {
|
||||
DownloadState::Done
|
||||
} else if mime_parser.decrypting_failed {
|
||||
} else if mime_parser.decryption_error.is_some() {
|
||||
DownloadState::Undecipherable
|
||||
} else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
|
||||
DownloadState::Available
|
||||
@@ -2354,7 +2355,7 @@ RETURNING id
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Message has {icnt} parts and is assigned to chat #{chat_id}."
|
||||
"Message has {icnt} parts and is assigned to chat #{chat_id}, timestamp={sort_timestamp}."
|
||||
);
|
||||
|
||||
if !chat_id.is_trash() && !hidden {
|
||||
@@ -2703,7 +2704,7 @@ async fn lookup_or_create_adhoc_group(
|
||||
allow_creation: bool,
|
||||
create_blocked: Blocked,
|
||||
) -> Result<Option<(ChatId, Blocked, bool)>> {
|
||||
if mime_parser.decrypting_failed {
|
||||
if mime_parser.decryption_error.is_some() {
|
||||
warn!(
|
||||
context,
|
||||
"Not creating ad-hoc group for message that cannot be decrypted."
|
||||
@@ -2925,7 +2926,7 @@ async fn create_group(
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
Ok(Some((chat_id, chat_id_blocked)))
|
||||
} else if mime_parser.decrypting_failed {
|
||||
} else if mime_parser.decryption_error.is_some() {
|
||||
// It is possible that the message was sent to a valid,
|
||||
// yet unknown group, which was rejected because
|
||||
// Chat-Group-Name, which is in the encrypted part, was
|
||||
@@ -3561,7 +3562,14 @@ async fn create_or_lookup_mailinglist_or_broadcast(
|
||||
chattype,
|
||||
&listid,
|
||||
name,
|
||||
create_blocked,
|
||||
if chattype == Chattype::InBroadcast {
|
||||
// If we joined the broadcast, we have scanned a QR code.
|
||||
// Even if 1:1 chat does not exist or is in a contact request,
|
||||
// create the channel as unblocked.
|
||||
Blocked::Not
|
||||
} else {
|
||||
create_blocked
|
||||
},
|
||||
param,
|
||||
mime_parser.timestamp_sent,
|
||||
)
|
||||
|
||||
@@ -13,9 +13,10 @@ use crate::constants::DC_GCL_FOR_FORWARDING;
|
||||
use crate::contact;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{ImexMode, imex};
|
||||
use crate::key;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
@@ -819,9 +820,12 @@ async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
receive_imf(context, imf_raw, false).await.unwrap();
|
||||
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
let msg_id = chats.get_msg_id(0).unwrap().unwrap();
|
||||
let received_msg = receive_imf(context, imf_raw, false)
|
||||
.await
|
||||
.expect("receive_imf failure")
|
||||
.expect("No message received");
|
||||
assert_eq!(received_msg.msg_ids.len(), 1);
|
||||
let msg_id = received_msg.msg_ids[0];
|
||||
Message::load_from_db(context, msg_id).await.unwrap()
|
||||
}
|
||||
|
||||
@@ -2872,9 +2876,8 @@ async fn test_rfc1847_encapsulation() -> Result<()> {
|
||||
|
||||
// Alice sends a message to Bob using Thunderbird.
|
||||
let raw = include_bytes!("../../test-data/message/rfc1847_encapsulation.eml");
|
||||
receive_imf(bob, raw, false).await?;
|
||||
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = load_imf_email(bob, raw).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
@@ -3082,8 +3085,8 @@ async fn test_auto_accept_for_bots() -> Result<()> {
|
||||
async fn test_auto_accept_group_for_bots() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
receive_imf(&t, GRP_MAIL, false).await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
let msg = load_imf_email(&t, GRP_MAIL).await;
|
||||
|
||||
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert!(!chat.is_contact_request());
|
||||
Ok(())
|
||||
@@ -3327,7 +3330,7 @@ async fn test_outgoing_undecryptable() -> Result<()> {
|
||||
assert!(
|
||||
dev_msg
|
||||
.text
|
||||
.contains("⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions.")
|
||||
.starts_with("⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions. (Error:")
|
||||
);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
|
||||
@@ -3556,9 +3559,9 @@ async fn test_messed_up_message_id() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/messed_up_message_id.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
let msg = load_imf_email(&t, raw).await;
|
||||
assert_eq!(
|
||||
t.get_last_msg().await.rfc724_mid,
|
||||
msg.rfc724_mid,
|
||||
"0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org"
|
||||
);
|
||||
|
||||
@@ -5360,6 +5363,46 @@ async fn test_outgoing_unencrypted_chat_assignment() {
|
||||
assert_eq!(received.chat_id, chat.id);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_incoming_reply_with_date_in_past() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let msg0 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <message@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:22:22 +0000\n\
|
||||
\n\
|
||||
This device has an atomic clock\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
let msg1 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <message1@example.net>\n\
|
||||
In-Reply-To: <message@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 11:11:11 +0000\n\
|
||||
\n\
|
||||
And this one has a wind-up clock\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(msg1.chat_id, msg0.chat_id);
|
||||
assert!(msg1.sort_timestamp >= msg0.sort_timestamp);
|
||||
assert_eq!(
|
||||
alice.get_last_msg_in(msg0.chat_id).await.id,
|
||||
*msg1.msg_ids.last().unwrap()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests Bob receiving a message from Alice
|
||||
/// in a new group she just created
|
||||
/// with only Alice and Bob.
|
||||
@@ -5559,3 +5602,90 @@ async fn test_calendar_alternative() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that outgoing encrypted messages are detected
|
||||
/// by verifying own signature, completely ignoring From address.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing_determined_by_signature() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// alice_dev2: same key, different address.
|
||||
let different_from = "very@different.from";
|
||||
assert!(!alice.is_self_addr(different_from).await?);
|
||||
let alice_dev2 = &tcm.unconfigured().await;
|
||||
alice_dev2.configure_addr(different_from).await;
|
||||
key::store_self_keypair(alice_dev2, &alice_keypair()).await?;
|
||||
assert_ne!(
|
||||
alice.get_config(Config::Addr).await?.unwrap(),
|
||||
different_from
|
||||
);
|
||||
|
||||
// Send message from alice_dev2 and check alice sees it as outgoing
|
||||
let chat_id = alice_dev2.create_chat_id(bob).await;
|
||||
let sent_msg = alice_dev2.send_text(chat_id, "hello from new device").await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mark_message_as_delivered_only_after_sent_out_fully() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
alice.set_config_bool(Config::BccSelf, true).await?;
|
||||
let alice_chat_id = alice.create_chat_id(bob).await;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(alice, "a.jpg", file_bytes, None)?;
|
||||
let msg_id = chat::send_msg(alice, alice_chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (pre_msg_id, pre_msg_payload) = first_row_in_smtp_queue(alice).await;
|
||||
assert_eq!(msg_id, pre_msg_id);
|
||||
assert!(pre_msg_payload.len() < file_bytes.len());
|
||||
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
// Alice receives her own pre-message because of bcc_self
|
||||
// This should not yet mark the message as delivered,
|
||||
// because not everything was sent,
|
||||
// but it does remove the pre-message from the SMTP queue
|
||||
receive_imf(alice, pre_msg_payload.as_bytes(), false).await?;
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
|
||||
let (post_msg_id, post_msg_payload) = first_row_in_smtp_queue(alice).await;
|
||||
assert_eq!(msg_id, post_msg_id);
|
||||
assert!(post_msg_payload.len() > file_bytes.len());
|
||||
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
// Alice receives her own post-message because of bcc_self
|
||||
// This should now mark the message as delivered,
|
||||
// because everything was sent by now.
|
||||
receive_imf(alice, post_msg_payload.as_bytes(), false).await?;
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the first sent message in the SMTP queue
|
||||
/// without removing it from the SMTP queue.
|
||||
/// This simulates the case that a message is successfully sent out,
|
||||
/// but the 'OK' answer from the server doesn't arrive,
|
||||
/// so that the SMTP row stays in the database.
|
||||
pub(crate) async fn first_row_in_smtp_queue(alice: &TestContext) -> (MsgId, String) {
|
||||
alice
|
||||
.sql
|
||||
.query_row_optional("SELECT msg_id, mime FROM smtp ORDER BY id", (), |row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let mime: String = row.get(1)?;
|
||||
Ok((msg_id, mime))
|
||||
})
|
||||
.await
|
||||
.expect("query_row_optional failed")
|
||||
.expect("No SMTP row found")
|
||||
}
|
||||
|
||||
@@ -450,10 +450,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
) {
|
||||
let mut self_found = false;
|
||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||
for (addr, key) in &mime_message.gossiped_keys {
|
||||
if key.public_key.dc_fingerprint() == self_fingerprint
|
||||
&& context.is_self_addr(addr).await?
|
||||
{
|
||||
for key in mime_message.gossiped_keys.values() {
|
||||
if key.public_key.dc_fingerprint() == self_fingerprint {
|
||||
self_found = true;
|
||||
break;
|
||||
}
|
||||
@@ -841,13 +839,6 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
inviter_progress(context, contact_id, chat_id, chat_type)?;
|
||||
}
|
||||
|
||||
if matches!(step, SecureJoinStep::RequestWithAuth) {
|
||||
// This actually reflects what happens on the first device (which does the secure
|
||||
// join) and causes a subsequent "vg-member-added" message to create an unblocked
|
||||
// verified group.
|
||||
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
|
||||
}
|
||||
|
||||
if matches!(step, SecureJoinStep::MemberAdded) {
|
||||
Ok(HandshakeMessage::Propagate)
|
||||
} else {
|
||||
|
||||
16
src/smtp.rs
16
src/smtp.rs
@@ -465,11 +465,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
match status {
|
||||
SendResult::Retry => Err(format_err!("Retry")),
|
||||
SendResult::Success => {
|
||||
if !context
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
|
||||
.await?
|
||||
{
|
||||
if !msg_has_pending_smtp_job(context, msg_id).await? {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -478,6 +474,16 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn msg_has_pending_smtp_job(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
) -> Result<bool, Error> {
|
||||
context
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Attempts to send queued MDNs.
|
||||
async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
|
||||
loop {
|
||||
|
||||
@@ -16,7 +16,6 @@ use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::key::DcKey;
|
||||
use crate::log::warn;
|
||||
use crate::message::MsgId;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::{Time, inc_and_check, time_elapsed};
|
||||
@@ -734,12 +733,6 @@ impl Sql {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_db_version_in_cache(&self, version: i32) -> Result<()> {
|
||||
let mut lock = self.config_cache.write().await;
|
||||
lock.insert(VERSION_CFG.to_string(), Some(format!("{version}")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
|
||||
self.execute_migration_transaction(
|
||||
|transaction| {
|
||||
@@ -1612,51 +1605,11 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 108 {
|
||||
let version = 108;
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?;
|
||||
sql.transaction(move |trans| {
|
||||
Sql::set_db_version_trans(trans, version)?;
|
||||
let id_max =
|
||||
trans.query_row("SELECT IFNULL((SELECT MAX(id) FROM smtp), 0)", (), |row| {
|
||||
let id_max: i64 = row.get(0)?;
|
||||
Ok(id_max)
|
||||
})?;
|
||||
while let Some((id, rfc724_mid, mime, msg_id, recipients, retries)) = trans
|
||||
.query_row(
|
||||
"SELECT id, rfc724_mid, mime, msg_id, recipients, retries FROM smtp \
|
||||
WHERE id<=? LIMIT 1",
|
||||
(id_max,),
|
||||
|row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
let mime: String = row.get(2)?;
|
||||
let msg_id: MsgId = row.get(3)?;
|
||||
let recipients: String = row.get(4)?;
|
||||
let retries: i64 = row.get(5)?;
|
||||
Ok((id, rfc724_mid, mime, msg_id, recipients, retries))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
{
|
||||
trans.execute("DELETE FROM smtp WHERE id=?", (id,))?;
|
||||
let recipients = recipients.split(' ').collect::<Vec<_>>();
|
||||
for recipients in recipients.chunks(chunk_size) {
|
||||
let recipients = recipients.join(" ");
|
||||
trans.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, mime, msg_id, recipients, retries) \
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
(&rfc724_mid, &mime, msg_id, recipients, retries),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("migration failed for version {version}"))?;
|
||||
|
||||
sql.set_db_version_in_cache(version).await?;
|
||||
}
|
||||
// Migration 108 is removed, it was using high level code
|
||||
// to split SMTP queue messages into chunks with smaller number of recipients
|
||||
// and started to fail later as high level code started
|
||||
// expecting `transports` table that is only added in future migrations.
|
||||
// Migration 108 was not changing the database schema.
|
||||
|
||||
if dbversion < 109 {
|
||||
sql.execute_migration(
|
||||
|
||||
@@ -40,6 +40,7 @@ use crate::message::{Message, MessageState, MsgId, update_msg_state};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::smtp::msg_has_pending_smtp_job;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::tools::time;
|
||||
|
||||
@@ -658,10 +659,7 @@ impl TestContext {
|
||||
.execute("DELETE FROM smtp WHERE id=?;", (rowid,))
|
||||
.await
|
||||
.expect("failed to remove job");
|
||||
if !self
|
||||
.ctx
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
|
||||
if !msg_has_pending_smtp_job(self, msg_id)
|
||||
.await
|
||||
.expect("Failed to check for more jobs")
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ async fn test_sending_pre_message() -> Result<()> {
|
||||
);
|
||||
|
||||
let decrypted_post_message = bob.parse_msg(post_message).await;
|
||||
assert_eq!(decrypted_post_message.decrypting_failed, false);
|
||||
assert!(decrypted_post_message.decryption_error.is_none());
|
||||
assert_eq!(
|
||||
decrypted_post_message.header_exists(HeaderDef::ChatPostMessageId),
|
||||
false
|
||||
|
||||
@@ -246,46 +246,6 @@ async fn test_old_message_4() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Alice is offline for some time.
|
||||
/// When they come online, first their mvbox is synced and then their inbox.
|
||||
/// This test tests that the messages are still in the right order.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_old_message_5() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let msg_sent = receive_imf(
|
||||
&alice,
|
||||
b"From: alice@example.org\n\
|
||||
To: Bob <bob@example.net>\n\
|
||||
Message-ID: <1234-2-4@example.org>\n\
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
|
||||
\n\
|
||||
Happy birthday, Bob!\n",
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
let msg_incoming = receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.net>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <1234-2-3@example.org>\n\
|
||||
Date: Sun, 07 Dec 2019 19:00:26 +0000\n\
|
||||
\n\
|
||||
Happy birthday to me, Alice!\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
assert!(msg_sent.sort_timestamp == msg_incoming.sort_timestamp);
|
||||
alice
|
||||
.golden_test_chat(msg_sent.chat_id, "test_old_message_5")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdn_doesnt_disable_verification() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -64,9 +64,11 @@ DKIM Results: Passed=true";
|
||||
|
||||
async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(&t, raw, false).await.unwrap();
|
||||
let msg = t.get_last_msg().await;
|
||||
let msg_info = msg.id.get_info(&t).await.unwrap();
|
||||
let received = receive_imf(&t, raw, false).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(received.msg_ids.len(), 1);
|
||||
let msg_id = received.msg_ids[0];
|
||||
let msg_info = msg_id.get_info(&t).await.unwrap();
|
||||
|
||||
// Ignore the first rows of the msg_info because they contain a
|
||||
// received time that depends on the test time which makes it impossible to
|
||||
|
||||
BIN
test-data/core-1.86.0-backup.tar
Normal file
BIN
test-data/core-1.86.0-backup.tar
Normal file
Binary file not shown.
@@ -2,9 +2,9 @@ Group#Chat#1001: Group [5 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
|
||||
Msg#1002🔒: Me (Contact#Contact#Self): populate √
|
||||
Msg#1007🔒: (Contact#Contact#1001): Member fiona@example.net removed by bob@example.net. [FRESH][INFO]
|
||||
Msg#1003: info (Contact#Contact#Info): Member dom@example.net added. [NOTICED][INFO]
|
||||
Msg#1004: info (Contact#Contact#Info): Member fiona@example.net removed. [NOTICED][INFO]
|
||||
Msg#1005🔒: (Contact#Contact#1001): Member elena@example.net added by bob@example.net. [FRESH][INFO]
|
||||
Msg#1006🔒: Me (Contact#Contact#Self): You added member fiona@example.net. [INFO] o
|
||||
Msg#1007🔒: (Contact#Contact#1001): Member fiona@example.net removed by bob@example.net. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Single#Chat#1001: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#1001: Me (Contact#Contact#Self): We share this account √
|
||||
Msg#1002: Me (Contact#Contact#Self): I'm Alice too √
|
||||
Msg#1001: Me (Contact#Contact#Self): We share this account √
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
Single#Chat#11001: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#11001: Me (Contact#Contact#Self): Happy birthday, Bob! √
|
||||
Msg#11002: (Contact#Contact#11001): Happy birthday to me, Alice! [FRESH]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1,7 +1,7 @@
|
||||
OutBroadcast#Chat#1001: Channel [0 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
|
||||
Msg#1006🔒: Me (Contact#Contact#Self): Member bob@example.net added. [INFO] √
|
||||
Msg#1008🔒: Me (Contact#Contact#Self): hi √
|
||||
Msg#1006🔒: Me (Contact#Contact#Self): Member bob@example.net added. [INFO] √
|
||||
Msg#1009🔒: Me (Contact#Contact#Self): You removed member bob@example.net. [INFO] √
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
BIN
test-data/image/rectangle200x180-vflipped.jpg
Normal file
BIN
test-data/image/rectangle200x180-vflipped.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user