mirror of
https://github.com/chatmail/core.git
synced 2026-10-08 06:00:17 +03:00
Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06955cf5a9 | ||
|
|
1b2e537a89 | ||
|
|
b899989c8c | ||
|
|
f2e600dc55 | ||
|
|
61fd0d400f | ||
|
|
7424d06416 | ||
|
|
aa71fbe04c | ||
|
|
c5cadd9991 | ||
|
|
c92554dc1f | ||
|
|
94c6d1dea4 | ||
|
|
d27d0ef476 | ||
|
|
d3f75360fa | ||
|
|
06a6cc48d2 | ||
|
|
b13f2709be | ||
|
|
1b824705fd | ||
|
|
6f22ce2722 | ||
|
|
5e58bf7575 | ||
|
|
85d7c1f942 | ||
|
|
df4fd82140 | ||
|
|
65b970a191 | ||
|
|
5e13b4c736 | ||
|
|
864833d232 | ||
|
|
3d07db6e62 | ||
|
|
9e88764a8a | ||
|
|
e70b879182 | ||
|
|
00d296e1ff | ||
|
|
e07f1a8b9c | ||
|
|
02b9085147 | ||
|
|
07fa9c35ee | ||
|
|
7db7c0aab1 | ||
|
|
30b23df816 | ||
|
|
4efd0d1ef7 | ||
|
|
f14880146a | ||
|
|
3a72188548 | ||
|
|
351f28361d | ||
|
|
c5b78741d6 | ||
|
|
57871bbaf8 | ||
|
|
287256693c | ||
|
|
d660f55a99 | ||
|
|
f1ca689f99 | ||
|
|
796b0d7752 | ||
|
|
2ea5c86a5a | ||
|
|
50b250cf78 | ||
|
|
3c03370589 | ||
|
|
8f41aed917 | ||
|
|
19be12a25d | ||
|
|
6a121b87eb | ||
|
|
420c0ed9b0 | ||
|
|
e05bb03db6 | ||
|
|
73fcb97eef | ||
|
|
8acf391ffe | ||
|
|
aacea2de25 | ||
|
|
b713e8cd94 | ||
|
|
b7be0b7bf6 | ||
|
|
2cb8b53256 | ||
|
|
a592a470cf | ||
|
|
c4d07ab99e | ||
|
|
eddd5a0d25 | ||
|
|
0f43d5d8f4 | ||
|
|
2e6d3aebae | ||
|
|
650995dc41 | ||
|
|
283a1f1653 | ||
|
|
d33909a054 | ||
|
|
129be3aa27 | ||
|
|
8a88479d8f | ||
|
|
5711f2fe3a | ||
|
|
46922d4d9d | ||
|
|
75fe4e106a | ||
|
|
7c60ac863e |
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.81.0
|
||||
RUSTUP_TOOLCHAIN: 1.82.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -95,11 +95,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.82.0
|
||||
- os: windows-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.82.0
|
||||
- os: macos-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.82.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
@@ -211,9 +211,9 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -263,11 +263,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: windows-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## [1.147.1] - 2024-10-13
|
||||
|
||||
### Build system
|
||||
|
||||
- Build Python 3.13 wheels.
|
||||
- deltachat-rpc-client: Add classifiers for all supported Python versions.
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Python 3.13.
|
||||
|
||||
### Documentation
|
||||
|
||||
- CONTRIBUTING.md: Add a note on deleting/changing db columns.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Reset quota on configured address change ([#5908](https://github.com/deltachat/deltachat-core-rust/pull/5908)).
|
||||
- Do not emit progress 1000 when configuration is cancelled.
|
||||
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/deltachat/deltachat-core-rust/pull/5338)).
|
||||
- Readd tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump futures-* from 0.3.30 to 0.3.31.
|
||||
- cargo: Upgrade async_zip to 0.0.17 ([#6035](https://github.com/deltachat/deltachat-core-rust/pull/6035)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- MsgId::update_download_state: Don't fail if the message doesn't exist anymore.
|
||||
|
||||
## [1.147.0] - 2024-10-05
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove deprecated get_next_media() APIs.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Reuse existing connections in background_fetch() if I/O is started.
|
||||
- MsgId::get_info(): Report original filename as well.
|
||||
- More context for the "Cannot establish guaranteed..." info message ([#6022](https://github.com/deltachat/deltachat-core-rust/pull/6022)).
|
||||
- deltachat-repl: Add `fetch` command to test `background_fetch()`.
|
||||
- deltachat-repl: Print send-backup QR code to the terminal.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not attempt to reference info messages.
|
||||
- query_row_optional: Do not treat rows with NULL as missing rows.
|
||||
- Skip unconfigured folders in `background_fetch()`.
|
||||
- Break out of accept() loop if there is an error transferring backup.
|
||||
- Make it possible to cancel ongoing backup transfer.
|
||||
- Make backup reception cancellable by stopping ongoing process.
|
||||
- Smooth progress bar for backup transfer.
|
||||
- Emit progress 0 if get_backup() fails.
|
||||
|
||||
### Documentation
|
||||
|
||||
- CONTRIBUTING.md: Add more SQL advices.
|
||||
|
||||
## [1.146.0] - 2024-10-03
|
||||
|
||||
### Fixes
|
||||
@@ -4946,3 +5006,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.144.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.143.0..v1.144.0
|
||||
[1.145.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.144.0..v1.145.0
|
||||
[1.146.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.145.0..v1.146.0
|
||||
[1.147.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.146.0..v1.147.0
|
||||
[1.147.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.0..v1.147.1
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ add_custom_command(
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ on the contributing page: <https://github.com/deltachat/deltachat-core-rust/cont
|
||||
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||
|
||||
### SQL
|
||||
|
||||
Multi-line SQL statements should be formatted using string literals,
|
||||
for example
|
||||
```
|
||||
@@ -65,6 +67,29 @@ is prone to errors like this if space before backslash is missing:
|
||||
Literal above results in `SELECT fooFROM bar` string.
|
||||
This style also does not allow using `--` comments.
|
||||
|
||||
---
|
||||
|
||||
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
|
||||
to make SQLite check column types.
|
||||
|
||||
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
|
||||
This avoids reuse of the row IDs and can avoid dangerous bugs
|
||||
like forwarding wrong message because the message was deleted
|
||||
and another message took its row ID.
|
||||
|
||||
Declare all new columns as `NOT NULL`
|
||||
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
|
||||
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
|
||||
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
|
||||
Use `HAVING COUNT(*) > 0` clause
|
||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||
|
||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||
an older version. Also don't change the column type, consider adding a new column with another name
|
||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||
keyword doesn't help here.
|
||||
|
||||
### Commit messages
|
||||
|
||||
Commit messages follow the [Conventional Commits] notation.
|
||||
|
||||
Generated
+71
-26
@@ -273,12 +273,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.3.15"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
|
||||
checksum = "e26a9844c659a2a293d239c7910b752f8487fe122c6c8bd1659bf85a6507c302"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -286,11 +287,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2162818f7b394e342a6591864bfc960824ed13e3f6609fee0d19e770ebcd99e1"
|
||||
checksum = "5488cd022c3c7bc41a9b34a540d9ac0d9c5cd42fdb106a67616521b7592d5b4e"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-compression",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -299,6 +301,7 @@ dependencies = [
|
||||
"log",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"pin-project",
|
||||
"pin-utils",
|
||||
"self_cell",
|
||||
"stop-token",
|
||||
@@ -368,16 +371,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async_zip"
|
||||
version = "0.0.12"
|
||||
version = "0.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2105142db9c6203b9dadc83b0553394589a6cb31b1449a3b46b42f47c3434d0"
|
||||
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"futures-lite 2.3.0",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1105,6 +1109,28 @@ version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"crossterm_winapi",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
@@ -1267,7 +1293,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1315,6 +1341,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"pgp",
|
||||
"pin-project",
|
||||
"pretty_assertions",
|
||||
"proptest",
|
||||
"qrcodegen",
|
||||
@@ -1366,7 +1393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1391,13 +1418,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
"dirs",
|
||||
"log",
|
||||
"nu-ansi-term",
|
||||
"qr2term",
|
||||
"rusqlite",
|
||||
"rustyline",
|
||||
"tokio",
|
||||
@@ -1406,7 +1434,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1435,7 +1463,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -2195,9 +2223,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -2220,9 +2248,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
@@ -2237,9 +2265,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -2271,9 +2299,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2282,15 +2310,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
@@ -2300,9 +2328,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -4452,6 +4480,22 @@ dependencies = [
|
||||
"unarray",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qr2term"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6867c60b38e9747a079a19614dbb5981a53f21b9a56c265f3bfdf6011a50a957"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"qrcode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
|
||||
[[package]]
|
||||
name = "qrcodegen"
|
||||
version = "1.8.0"
|
||||
@@ -6073,6 +6117,7 @@ checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hashbrown",
|
||||
|
||||
+6
-5
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -41,10 +41,10 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.1"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.1", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
base64 = { workspace = true }
|
||||
brotli = { version = "6", default-features=false, features = ["std"] }
|
||||
bytes = "1"
|
||||
@@ -77,6 +77,7 @@ once_cell = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.13.2", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.36"
|
||||
quoted_printable = "0.5"
|
||||
@@ -168,8 +169,8 @@ async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.38", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc" }
|
||||
deltachat = { path = "." }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.30"
|
||||
futures-lite = "2.3.0"
|
||||
libc = "0.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
+46
-26
@@ -522,6 +522,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
* fresh message and badge counters are not changed by this setting,
|
||||
* but should be tuned down where appropriate.
|
||||
* - `private_tag` = Optional tag as "Work", "Family".
|
||||
* Meant to help profile owner to differ between profiles with similar names.
|
||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
||||
* The prefix should be followed by the system and maybe subsystem,
|
||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||
@@ -1547,30 +1549,6 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
|
||||
dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t chat_id, int msg_type, int msg_type2, int msg_type3);
|
||||
|
||||
|
||||
/**
|
||||
* Search next/previous message based on a given message and a list of types.
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
||||
* @param dir 1=get the next message, -1=get the previous one.
|
||||
* @param msg_type The message type to search for.
|
||||
* If 0, the message type from curr_msg_id is used.
|
||||
* @param msg_type2 Alternative message type to search for. 0 to skip.
|
||||
* @param msg_type3 Alternative message type to search for. 0 to skip.
|
||||
* @return Returns the message ID that should be played next.
|
||||
* The returned message is in the same chat as the given one
|
||||
* and has one of the given types.
|
||||
* Typically, this result is passed again to dc_get_next_media()
|
||||
* later on the next swipe.
|
||||
* If there is not next/previous message, the function returns 0.
|
||||
*/
|
||||
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
|
||||
|
||||
|
||||
/**
|
||||
* Set chat visibility to pinned, archived or normal.
|
||||
*
|
||||
@@ -2633,6 +2611,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
|
||||
* See dc_get_securejoin_qr() for details about the contained QR code.
|
||||
*
|
||||
* @deprecated 2024-10 use dc_create_qr_svg(dc_get_securejoin_qr()) instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
|
||||
@@ -2813,6 +2792,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
|
||||
void dc_delete_all_locations (dc_context_t* context);
|
||||
|
||||
|
||||
// misc
|
||||
|
||||
/**
|
||||
* Create a QR code from any input data.
|
||||
*
|
||||
* The QR code is returned as a square SVG image.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param payload The content for the QR code.
|
||||
* @return SVG image with the QR code.
|
||||
* On errors, an empty string is returned.
|
||||
* The returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_create_qr_svg (const char* payload);
|
||||
|
||||
|
||||
/**
|
||||
* Get last error string.
|
||||
*
|
||||
@@ -2901,6 +2896,7 @@ char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
|
||||
* This works like dc_backup_provider_qr() but returns the text of a rendered
|
||||
* SVG image containing the QR code.
|
||||
*
|
||||
* @deprecated 2024-10 use dc_create_qr_svg(dc_backup_provider_get_qr()) instead.
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param backup_provider The backup provider object as created by
|
||||
* dc_backup_provider_new().
|
||||
@@ -2940,7 +2936,7 @@ void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
|
||||
* Gets a backup offered by a dc_backup_provider_t object on another device.
|
||||
*
|
||||
* This function is called on a device that scanned the QR code offered by
|
||||
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
|
||||
* dc_backup_provider_get_qr(). Typically this is a
|
||||
* different device than that which provides the backup.
|
||||
*
|
||||
* This call will block while the backup is being transferred and only
|
||||
@@ -6055,6 +6051,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_REACTIONS_CHANGED 2001
|
||||
|
||||
|
||||
/**
|
||||
* A reaction to one's own sent message received.
|
||||
* Typically, the UI will show a notification for that.
|
||||
*
|
||||
* In addition to this event, DC_EVENT_REACTIONS_CHANGED is emitted.
|
||||
*
|
||||
* @param data1 (int) contact_id ID of the contact sending this reaction.
|
||||
* @param data2 (int) msg_id + (char*) reaction.
|
||||
* ID of the message for which a reaction was received in dc_event_get_data2_int(),
|
||||
* and the reaction as dc_event_get_data2_str().
|
||||
* string must be passed to dc_str_unref() afterwards.
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_REACTION 2002
|
||||
|
||||
|
||||
/**
|
||||
* There is a fresh message. Typically, the user will show an notification
|
||||
* when receiving this message.
|
||||
@@ -6272,7 +6283,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* webxdc status update received.
|
||||
* Webxdc status update received.
|
||||
* To get the received status update, use dc_get_webxdc_status_updates() with
|
||||
* `serial` set to the last known update
|
||||
* (in case of special bots, `status_update_serial` from `data2`
|
||||
@@ -6307,6 +6318,15 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
|
||||
|
||||
/**
|
||||
* Advertisement for ephemeral peer channel communication received.
|
||||
* This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 0
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
|
||||
+37
-43
@@ -30,7 +30,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
@@ -541,6 +541,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||
EventType::MsgsChanged { .. } => 2000,
|
||||
EventType::ReactionsChanged { .. } => 2001,
|
||||
EventType::IncomingReaction { .. } => 2002,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::IncomingMsgBunch { .. } => 2006,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
@@ -563,10 +564,14 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
EventType::WebxdcRealtimeData { .. } => 2150,
|
||||
EventType::WebxdcRealtimeAdvertisementReceived { .. } => 2151,
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +602,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -621,11 +627,15 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
}
|
||||
EventType::WebxdcRealtimeData { msg_id, .. }
|
||||
| EventType::WebxdcStatusUpdate { msg_id, .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
|
||||
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
EventType::EventChannelOverflow { n } => *n as libc::c_int,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,9 +676,11 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
@@ -682,6 +694,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +748,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
@@ -754,6 +770,14 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
EventType::IncomingReaction { reaction, .. } => reaction
|
||||
.as_str()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1443,48 +1467,6 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(deprecated)]
|
||||
pub unsafe extern "C" fn dc_get_next_media(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
dir: libc::c_int,
|
||||
msg_type: libc::c_int,
|
||||
or_msg_type2: libc::c_int,
|
||||
or_msg_type3: libc::c_int,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_next_media()");
|
||||
return 0;
|
||||
}
|
||||
let direction = if dir < 0 {
|
||||
chat::Direction::Backward
|
||||
} else {
|
||||
chat::Direction::Forward
|
||||
};
|
||||
|
||||
let ctx = &*context;
|
||||
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {msg_type}"));
|
||||
let or_msg_type2 =
|
||||
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {or_msg_type2}"));
|
||||
let or_msg_type3 =
|
||||
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {or_msg_type3}"));
|
||||
|
||||
block_on(async move {
|
||||
chat::get_next_media(
|
||||
ctx,
|
||||
MsgId::new(msg_id),
|
||||
direction,
|
||||
msg_type,
|
||||
or_msg_type2,
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2612,6 +2594,18 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
||||
if payload.is_null() {
|
||||
eprintln!("ignoring careless call to dc_create_qr_svg()");
|
||||
return "".strdup();
|
||||
}
|
||||
|
||||
create_qr_svg(&to_string_lossy(payload))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -1552,55 +1552,6 @@ impl CommandApi {
|
||||
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
|
||||
}
|
||||
|
||||
/// Search next/previous message based on a given message and a list of types.
|
||||
/// Typically used to implement the "next" and "previous" buttons
|
||||
/// in a gallery or in a media player.
|
||||
///
|
||||
/// one combined call for getting chat::get_next_media for both directions
|
||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
///
|
||||
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||
/// and navigate the returned array instead.
|
||||
#[allow(deprecated)]
|
||||
async fn get_neighboring_chat_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
message_type: MessageViewtype,
|
||||
or_message_type2: Option<MessageViewtype>,
|
||||
or_message_type3: Option<MessageViewtype>,
|
||||
) -> Result<(Option<u32>, Option<u32>)> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let msg_type: Viewtype = message_type.into();
|
||||
let msg_type2: Viewtype = or_message_type2.map(|v| v.into()).unwrap_or_default();
|
||||
let msg_type3: Viewtype = or_message_type3.map(|v| v.into()).unwrap_or_default();
|
||||
|
||||
let prev = chat::get_next_media(
|
||||
&ctx,
|
||||
MsgId::new(msg_id),
|
||||
chat::Direction::Backward,
|
||||
msg_type,
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
)
|
||||
.await?
|
||||
.map(|id| id.to_u32());
|
||||
|
||||
let next = chat::get_next_media(
|
||||
&ctx,
|
||||
MsgId::new(msg_id),
|
||||
chat::Direction::Forward,
|
||||
msg_type,
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
)
|
||||
.await?
|
||||
.map(|id| id.to_u32());
|
||||
|
||||
Ok((prev, next))
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// backup
|
||||
// ---------------------------------------------
|
||||
@@ -1995,9 +1946,13 @@ impl CommandApi {
|
||||
|
||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = data.create_message(&ctx).await?;
|
||||
let mut message = data
|
||||
.create_message(&ctx)
|
||||
.await
|
||||
.context("Failed to create message")?;
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.await
|
||||
.context("Failed to send created message")?
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,14 @@ pub enum EventType {
|
||||
contact_id: u32,
|
||||
},
|
||||
|
||||
/// Incoming reaction, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingReaction {
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
reaction: String,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -244,6 +252,11 @@ pub enum EventType {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
|
||||
|
||||
/// Advertisement received over an ephemeral peer channel.
|
||||
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcRealtimeAdvertisementReceived { msg_id: u32 },
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcInstanceDeleted { msg_id: u32 },
|
||||
@@ -297,6 +310,15 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingReaction {
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
} => IncomingReaction {
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
reaction: reaction.as_str().to_string(),
|
||||
},
|
||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
@@ -373,6 +395,11 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
data,
|
||||
},
|
||||
CoreEventType::WebxdcRealtimeAdvertisementReceived { msg_id } => {
|
||||
WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: msg_id.to_u32(),
|
||||
}
|
||||
}
|
||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
|
||||
@@ -605,16 +605,13 @@ impl MessageData {
|
||||
message.set_location(latitude, longitude);
|
||||
}
|
||||
if let Some(id) = self.quoted_message_id {
|
||||
let quoted_message = Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("Failed to load quoted message")?;
|
||||
message
|
||||
.set_quote(
|
||||
context,
|
||||
Some(
|
||||
&Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("message to quote could not be loaded")?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
.set_quote(context, Some("ed_message))
|
||||
.await
|
||||
.context("Failed to set quote")?;
|
||||
} else if let Some(text) = self.quoted_text {
|
||||
let protect = false;
|
||||
message.set_quote_text(Some((text, protect)));
|
||||
@@ -640,7 +637,7 @@ pub struct MessageInfo {
|
||||
error: Option<String>,
|
||||
rfc724_mid: String,
|
||||
server_urls: Vec<String>,
|
||||
hop_info: Option<String>,
|
||||
hop_info: String,
|
||||
}
|
||||
|
||||
impl MessageInfo {
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.146.0"
|
||||
"version": "1.147.1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
@@ -11,6 +11,7 @@ deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "14"
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -22,6 +22,7 @@ use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::qr_code_generator::create_qr_svg;
|
||||
use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
@@ -355,6 +356,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
fetch\n\
|
||||
connectivity\n\
|
||||
maybenetwork\n\
|
||||
housekeeping\n\
|
||||
@@ -424,6 +426,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
createqrsvg <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
@@ -486,8 +489,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"send-backup" => {
|
||||
let provider = BackupProvider::prepare(&context).await?;
|
||||
let qr = provider.qr();
|
||||
println!("QR code: {}", format_backup(&qr)?);
|
||||
let qr = format_backup(&provider.qr())?;
|
||||
println!("QR code: {}", qr);
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
provider.await?;
|
||||
}
|
||||
"receive-backup" => {
|
||||
@@ -1247,6 +1251,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
"createqrsvg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let svg = create_qr_svg(arg1)?;
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
fs::write(&file, svg).await?;
|
||||
println!("{file:#?} written.");
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
let proxy_enabled = context
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use deltachat::chat::ChatId;
|
||||
@@ -168,7 +166,7 @@ const IMEX_COMMANDS: [&str; 13] = [
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 10] = [
|
||||
const DB_COMMANDS: [&str; 11] = [
|
||||
"info",
|
||||
"set",
|
||||
"get",
|
||||
@@ -176,6 +174,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"configure",
|
||||
"connect",
|
||||
"disconnect",
|
||||
"fetch",
|
||||
"connectivity",
|
||||
"maybenetwork",
|
||||
"housekeeping",
|
||||
@@ -241,12 +240,13 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"createqrsvg",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
@@ -417,6 +417,9 @@ async fn handle_cmd(
|
||||
"disconnect" => {
|
||||
ctx.stop_io().await;
|
||||
}
|
||||
"fetch" => {
|
||||
ctx.background_fetch().await?;
|
||||
}
|
||||
"configure" => {
|
||||
ctx.configure().await?;
|
||||
}
|
||||
@@ -446,12 +449,7 @@ async fn handle_cmd(
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{qr}");
|
||||
let output = Command::new("qrencode")
|
||||
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -13,10 +13,13 @@ classifiers = [
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -44,13 +45,6 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
|
||||
# Test that email address is in SVG
|
||||
# when we have no display name.
|
||||
# Check only the domain name, because
|
||||
# long address may be split over multiple lines
|
||||
# and not matched.
|
||||
assert domain in svg
|
||||
|
||||
alice.set_config("displayname", "Alice")
|
||||
|
||||
# Test that display name is used
|
||||
@@ -332,7 +326,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
|
||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
@@ -342,6 +335,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
event = ac2.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
chat_id = event.chat_id
|
||||
@@ -465,7 +460,10 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
|
||||
def test_aeap_flow_verified(acfactory):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# ac1new is only used to get a new address.
|
||||
ac1new = acfactory.new_preconfigured_account()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
@@ -474,6 +472,7 @@ def test_aeap_flow_verified(acfactory):
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("sending first message")
|
||||
msg_out = chat.send_text("old address").get_snapshot()
|
||||
@@ -571,6 +570,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
@@ -585,17 +585,29 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
# ac1 resetups the account.
|
||||
ac1 = acfactory.resetup_account(ac1)
|
||||
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
# Loop sending message from ac1 to ac2
|
||||
# until ac2 accepts new ac1 key.
|
||||
#
|
||||
# This may not happen immediately because resetup of ac1
|
||||
# rewinds "smeared timestamp" so Date: header for messages
|
||||
# sent by new ac1 are in the past compared to the last Date:
|
||||
# header sent by old ac1.
|
||||
while True:
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
|
||||
# ac2 receives a message.
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
# ac2 receives a message.
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
logging.info("ac2 received Hello!")
|
||||
|
||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
|
||||
if not ac2_contact_ac1.get_snapshot().is_verified:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.146.0"
|
||||
"version": "1.147.1"
|
||||
}
|
||||
|
||||
Generated
+38
-71
@@ -3,15 +3,15 @@
|
||||
"android": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712088936,
|
||||
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
|
||||
"lastModified": 1729369131,
|
||||
"narHash": "sha256-PtfScp+nQd1PsT5rf0Qgjdbsh4Iag6R1ivYMWLizyIc=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
|
||||
"rev": "82bffbf3f06bdccf44fc62a9bd4f152ac80a55b0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -22,18 +22,17 @@
|
||||
},
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"android",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711099426,
|
||||
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
|
||||
"lastModified": 1728330715,
|
||||
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
|
||||
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -48,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1714112748,
|
||||
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
|
||||
"lastModified": 1729375822,
|
||||
"narHash": "sha256-bRo4xVwUhvJ4Gz+OhWMREFMdBOYSw4Yi1Apj01ebbug=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
|
||||
"rev": "2853e7d9b5c52a148a9fb824bfe4f9f433f557ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -66,11 +65,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -84,29 +83,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -120,11 +101,11 @@
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713520724,
|
||||
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
|
||||
"lastModified": 1721727458,
|
||||
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
|
||||
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -150,11 +131,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1711703276,
|
||||
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
|
||||
"lastModified": 1729256560,
|
||||
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
|
||||
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -166,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1713895582,
|
||||
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
|
||||
"lastModified": 1729070438,
|
||||
"narHash": "sha256-KOTTUfPkugH52avUvXGxvWy8ibKKj4genodIYUED+Kc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
|
||||
"rev": "5785b6bb5eaae44e627d541023034e1601455827",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -182,11 +163,12 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1711668574,
|
||||
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
|
||||
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
|
||||
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
|
||||
"type": "path"
|
||||
"lastModified": 1729265718,
|
||||
"narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ccc0c2126893dd20963580b6478d1a10a4512185",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
@@ -195,11 +177,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1714076141,
|
||||
"narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=",
|
||||
"lastModified": 1729256560,
|
||||
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
|
||||
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -213,7 +195,7 @@
|
||||
"inputs": {
|
||||
"android": "android",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"naersk": "naersk",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
@@ -222,11 +204,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1714031783,
|
||||
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
|
||||
"lastModified": 1729255720,
|
||||
"narHash": "sha256-yODOuZxBkS0UfqMa6nmbqNbVfIbsu0tYLbV5vZzmsqI=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
|
||||
"rev": "72b214fbfbe6f7b95a7877b962783bd42062cc0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -265,21 +247,6 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -50,6 +50,7 @@ module.exports = {
|
||||
DC_EVENT_IMEX_PROGRESS: 2051,
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
|
||||
DC_EVENT_INCOMING_REACTION: 2002,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
@@ -67,6 +68,7 @@ module.exports = {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT: 2151,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -38,6 +39,7 @@ module.exports = {
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
|
||||
@@ -50,6 +50,7 @@ export enum C {
|
||||
DC_EVENT_IMEX_PROGRESS = 2051,
|
||||
DC_EVENT_INCOMING_MSG = 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
|
||||
DC_EVENT_INCOMING_REACTION = 2002,
|
||||
DC_EVENT_INFO = 100,
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
@@ -67,6 +68,7 @@ export enum C {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT = 2151,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
@@ -321,6 +323,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -343,6 +346,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
|
||||
@@ -475,47 +475,6 @@ export class Context extends EventEmitter {
|
||||
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getNextMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
getPreviousMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
_getNextMedia(
|
||||
messageId: number,
|
||||
dir: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
): number {
|
||||
return binding.dcn_get_next_media(
|
||||
this.dcn_context,
|
||||
Number(messageId),
|
||||
dir,
|
||||
msgType1 || 0,
|
||||
msgType2 || 0,
|
||||
msgType3 || 0
|
||||
)
|
||||
}
|
||||
|
||||
getSecurejoinQrCode(chatId: number): string {
|
||||
debug(`getSecurejoinQrCode ${chatId}`)
|
||||
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))
|
||||
|
||||
@@ -1053,27 +1053,6 @@ NAPI_METHOD(dcn_get_msg_html) {
|
||||
NAPI_RETURN_AND_UNREF_STRING(msg_html);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_get_next_media) {
|
||||
NAPI_ARGV(6);
|
||||
NAPI_DCN_CONTEXT();
|
||||
NAPI_ARGV_UINT32(msg_id, 1);
|
||||
NAPI_ARGV_INT32(dir, 2);
|
||||
NAPI_ARGV_INT32(msg_type1, 3);
|
||||
NAPI_ARGV_INT32(msg_type2, 4);
|
||||
NAPI_ARGV_INT32(msg_type3, 5);
|
||||
|
||||
//TRACE("calling..");
|
||||
uint32_t next_id = dc_get_next_media(dcn_context->dc_context,
|
||||
msg_id,
|
||||
dir,
|
||||
msg_type1,
|
||||
msg_type2,
|
||||
msg_type3);
|
||||
//TRACE("result %d", next_id);
|
||||
|
||||
NAPI_RETURN_UINT32(next_id);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_set_chat_visibility) {
|
||||
NAPI_ARGV(3);
|
||||
NAPI_DCN_CONTEXT();
|
||||
@@ -3443,7 +3422,6 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_msg_cnt);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_msg_info);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_msg_html);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_next_media);
|
||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_visibility);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr_svg);
|
||||
|
||||
+1
-1
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.146.0"
|
||||
"version": "1.147.1"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.146.0"
|
||||
version = "1.147.1"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -308,7 +308,7 @@ class Chat:
|
||||
msg = as_dc_charpointer(text)
|
||||
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be send, does chat exist?")
|
||||
raise ValueError("The message could not be sent. Does the chat exist?")
|
||||
return Message.from_db(self.account, msg_id)
|
||||
|
||||
def send_file(self, path, mime_type="application/octet-stream"):
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2024-10-03
|
||||
2024-10-13
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.81.0
|
||||
RUST_VERSION=1.82.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
+1
-1
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,py313,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
+42
-26
@@ -253,16 +253,16 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// The extension part will always be lowercased.
|
||||
fn sanitise_name(name: &str) -> (String, String) {
|
||||
let mut name = name.to_string();
|
||||
let mut name = name;
|
||||
for part in name.rsplit('/') {
|
||||
if !part.is_empty() {
|
||||
name = part.to_string();
|
||||
name = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for part in name.rsplit('\\') {
|
||||
if !part.is_empty() {
|
||||
name = part.to_string();
|
||||
name = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -272,32 +272,39 @@ impl<'a> BlobObject<'a> {
|
||||
replacement: "",
|
||||
};
|
||||
|
||||
let clean = sanitize_filename::sanitize_with_options(name, opts);
|
||||
// Let's take the tricky filename
|
||||
let name = sanitize_filename::sanitize_with_options(name, opts);
|
||||
// Let's take a tricky filename,
|
||||
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
|
||||
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
|
||||
let mut iter = clean.splitn(2, '.');
|
||||
|
||||
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
|
||||
// stem == "file"
|
||||
|
||||
let ext_chars = iter.next().unwrap_or_default().chars();
|
||||
let ext: String = ext_chars
|
||||
// Assume that the extension is 32 chars maximum.
|
||||
let ext: String = name
|
||||
.chars()
|
||||
.rev()
|
||||
.take(32)
|
||||
.take_while(|c| !c.is_whitespace())
|
||||
.take(33)
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.rev()
|
||||
.collect();
|
||||
// ext == "d_point_and_double_ending.tar.gz"
|
||||
// ext == "nd_point_and_double_ending.tar.gz"
|
||||
|
||||
if ext.is_empty() {
|
||||
(stem, "".to_string())
|
||||
// Split it into "nd_point_and_double_ending" and "tar.gz":
|
||||
let mut iter = ext.splitn(2, '.');
|
||||
iter.next();
|
||||
|
||||
let ext = iter.next().unwrap_or_default();
|
||||
let ext = if ext.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
(stem, format!(".{ext}").to_lowercase())
|
||||
// Return ("file", ".d_point_and_double_ending.tar.gz")
|
||||
// which is not perfect but acceptable.
|
||||
}
|
||||
format!(".{ext}")
|
||||
// ".tar.gz"
|
||||
};
|
||||
let stem = name
|
||||
.strip_suffix(&ext)
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.take(64)
|
||||
.collect();
|
||||
(stem, ext.to_lowercase())
|
||||
}
|
||||
|
||||
/// Checks whether a name is a valid blob name.
|
||||
@@ -615,7 +622,7 @@ fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for BlobObject<'a> {
|
||||
impl fmt::Display for BlobObject<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "$BLOBDIR/{}", self.name)
|
||||
}
|
||||
@@ -666,10 +673,6 @@ impl<'a> BlobDirContents<'a> {
|
||||
pub(crate) fn iter(&self) -> BlobDirIter<'_> {
|
||||
BlobDirIter::new(self.context, self.inner.iter())
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// A iterator over all the [`BlobObject`]s in the blobdir.
|
||||
@@ -967,6 +970,19 @@ mod tests {
|
||||
assert!(!stem.contains(':'));
|
||||
assert!(!stem.contains('*'));
|
||||
assert!(!stem.contains('?'));
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name(
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
|
||||
);
|
||||
assert_eq!(
|
||||
stem,
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending"
|
||||
);
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
|
||||
assert_eq!(stem, "a. tar");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+157
-115
@@ -578,7 +578,7 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and sends or adds a message.
|
||||
/// Sets protection and adds a message.
|
||||
///
|
||||
/// `timestamp_sort` is used as the timestamp of the added message
|
||||
/// and should be the timestamp of the change happening.
|
||||
@@ -589,20 +589,16 @@ impl ChatId {
|
||||
timestamp_sort: i64,
|
||||
contact_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
match self.inner_set_protection(context, protect).await {
|
||||
Ok(protection_status_modified) => {
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
|
||||
Err(e)
|
||||
}
|
||||
let protection_status_modified = self
|
||||
.inner_set_protection(context, protect)
|
||||
.await
|
||||
.with_context(|| format!("Cannot set protection for {self}"))?;
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and sends or adds a message.
|
||||
@@ -616,8 +612,9 @@ impl ChatId {
|
||||
contact_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
let ts = self
|
||||
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, false)
|
||||
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, received, incoming)
|
||||
.await?
|
||||
// Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones
|
||||
// in case of race conditions.
|
||||
@@ -869,13 +866,14 @@ impl ChatId {
|
||||
///
|
||||
/// Returns `true`, if message was deleted, `false` otherwise.
|
||||
async fn maybe_delete_draft(self, context: &Context) -> Result<bool> {
|
||||
match self.get_draft_msg_id(context).await? {
|
||||
Some(msg_id) => {
|
||||
msg_id.delete_from_db(context).await?;
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
Ok(context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs WHERE chat_id=? AND state=?",
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?
|
||||
> 0)
|
||||
}
|
||||
|
||||
/// Set provided message as draft message for specified chat.
|
||||
@@ -947,12 +945,18 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
|
||||
// insert new draft
|
||||
self.maybe_delete_draft(context).await?;
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs (
|
||||
.transaction(|transaction| {
|
||||
// Delete existing draft if it exists.
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs WHERE chat_id=? AND state=?",
|
||||
(self, MessageState::OutDraft),
|
||||
)?;
|
||||
|
||||
// Insert new draft.
|
||||
transaction.execute(
|
||||
"INSERT INTO msgs (
|
||||
chat_id,
|
||||
from_id,
|
||||
timestamp,
|
||||
@@ -964,19 +968,22 @@ impl ChatId {
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(transaction.last_insert_rowid())
|
||||
})
|
||||
.await?;
|
||||
msg.id = MsgId::new(row_id.try_into()?);
|
||||
Ok(true)
|
||||
@@ -1042,7 +1049,13 @@ impl ChatId {
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
let timestamp = context
|
||||
.sql
|
||||
.query_get_value("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", (self,))
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=?
|
||||
HAVING COUNT(*) > 0",
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
Ok(timestamp)
|
||||
}
|
||||
@@ -1228,6 +1241,7 @@ impl ChatId {
|
||||
AND ((state BETWEEN {} AND {}) OR (state >= {})) \
|
||||
AND NOT hidden \
|
||||
AND download_state={} \
|
||||
AND from_id != {} \
|
||||
ORDER BY timestamp DESC, id DESC \
|
||||
LIMIT 1;",
|
||||
MessageState::InFresh as u32,
|
||||
@@ -1236,6 +1250,9 @@ impl ChatId {
|
||||
// Do not reply to not fully downloaded messages. Such a message could be a group chat
|
||||
// message that we assigned to 1:1 chat.
|
||||
DownloadState::Done as u32,
|
||||
// Do not reference info messages, they are not actually sent out
|
||||
// and have Message-IDs unknown to other chat members.
|
||||
ContactId::INFO.to_u32(),
|
||||
);
|
||||
sql.query_row_optional(&query, (self,), f).await
|
||||
}
|
||||
@@ -1247,7 +1264,7 @@ impl ChatId {
|
||||
) -> Result<Option<(String, String, String)>> {
|
||||
self.parent_query(
|
||||
context,
|
||||
"rfc724_mid, mime_in_reply_to, mime_references",
|
||||
"rfc724_mid, mime_in_reply_to, IFNULL(mime_references, '')",
|
||||
state_out_min,
|
||||
|row: &rusqlite::Row| {
|
||||
let rfc724_mid: String = row.get(0)?;
|
||||
@@ -1382,12 +1399,14 @@ impl ChatId {
|
||||
/// corresponding event in case of a system message (usually the current system time).
|
||||
/// `always_sort_to_bottom` makes this ajust 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));
|
||||
@@ -1401,26 +1420,45 @@ impl ChatId {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state!=?",
|
||||
"SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=? AND state!=?
|
||||
HAVING COUNT(*) > 0",
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?
|
||||
} else if incoming {
|
||||
// get newest non fresh message for this chat.
|
||||
|
||||
// If a user hasn't been online for some time, the Inbox is fetched first and then the
|
||||
// Sentbox. In order for Inbox and Sent messages to be allowed to mingle, outgoing
|
||||
// messages are purely sorted by their sent timestamp. NB: The Inbox must be fetched
|
||||
// first otherwise Inbox messages would be always below old Sentbox messages. We could
|
||||
// take in the query below only incoming messages, but then new incoming messages would
|
||||
// mingle with just sent outgoing ones and apear somewhere in the middle of the chat.
|
||||
} 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_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0 AND state>?",
|
||||
(self, MessageState::InFresh),
|
||||
.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
|
||||
};
|
||||
@@ -2903,7 +2941,10 @@ async fn prepare_send_msg(
|
||||
);
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
}
|
||||
create_send_msg_jobs(context, msg).await
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
.await
|
||||
.context("Failed to create send jobs")?;
|
||||
Ok(row_ids)
|
||||
}
|
||||
|
||||
/// Constructs jobs for sending a message and inserts them into the appropriate table.
|
||||
@@ -3416,65 +3457,6 @@ pub async fn get_chat_media(
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Indicates the direction over which to iterate.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum Direction {
|
||||
/// Search forward.
|
||||
Forward = 1,
|
||||
|
||||
/// Search backward.
|
||||
Backward = -1,
|
||||
}
|
||||
|
||||
/// Searches next/previous message based on the given message and list of types.
|
||||
///
|
||||
/// Deprecated since 2023-10-03.
|
||||
#[deprecated(note = "use `get_chat_media` instead")]
|
||||
pub async fn get_next_media(
|
||||
context: &Context,
|
||||
curr_msg_id: MsgId,
|
||||
direction: Direction,
|
||||
msg_type: Viewtype,
|
||||
msg_type2: Viewtype,
|
||||
msg_type3: Viewtype,
|
||||
) -> Result<Option<MsgId>> {
|
||||
let mut ret: Option<MsgId> = None;
|
||||
|
||||
if let Ok(msg) = Message::load_from_db(context, curr_msg_id).await {
|
||||
let list: Vec<MsgId> = get_chat_media(
|
||||
context,
|
||||
Some(msg.chat_id),
|
||||
if msg_type != Viewtype::Unknown {
|
||||
msg_type
|
||||
} else {
|
||||
msg.viewtype
|
||||
},
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
)
|
||||
.await?;
|
||||
for (i, msg_id) in list.iter().enumerate() {
|
||||
if curr_msg_id == *msg_id {
|
||||
match direction {
|
||||
Direction::Forward => {
|
||||
if i + 1 < list.len() {
|
||||
ret = list.get(i + 1).copied();
|
||||
}
|
||||
}
|
||||
Direction::Backward => {
|
||||
if i >= 1 {
|
||||
ret = list.get(i - 1).copied();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Returns a vector of contact IDs for given chat ID.
|
||||
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
|
||||
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
|
||||
@@ -4400,7 +4382,10 @@ pub async fn add_device_msg_with_importance(
|
||||
if let Some(last_msg_time) = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
|
||||
"SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=?
|
||||
HAVING COUNT(*) > 0",
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
@@ -4726,6 +4711,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager};
|
||||
@@ -4881,6 +4867,37 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_only_one_draft_per_chat() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let msgs: Vec<message::Message> = (1..=1000)
|
||||
.map(|i| {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(i.to_string());
|
||||
msg
|
||||
})
|
||||
.collect();
|
||||
let mut tasks = Vec::new();
|
||||
for mut msg in msgs {
|
||||
let ctx = t.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let ctx = ctx;
|
||||
chat_id.set_draft(&ctx, Some(&mut msg)).await
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
futures::future::join_all(tasks.into_iter()).await;
|
||||
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
chat_id.set_draft(&t, None).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -7681,4 +7698,29 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that info message is ignored when constructing `In-Reply-To`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_info_not_referenced() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let bob_received_message = tcm.send_recv_accept(alice, bob, "Hi!").await;
|
||||
let bob_chat_id = bob_received_message.chat_id;
|
||||
add_info_msg(bob, bob_chat_id, "Some info", create_smeared_timestamp(bob)).await?;
|
||||
|
||||
// Bob sends a message.
|
||||
// This message should reference Alice's "Hi!" message and not the info message.
|
||||
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
|
||||
let mime_message = alice.parse_msg(&sent).await;
|
||||
|
||||
let in_reply_to = mime_message.get_header(HeaderDef::InReplyTo).unwrap();
|
||||
assert_eq!(
|
||||
in_reply_to,
|
||||
format!("<{}>", bob_received_message.rfc724_mid)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -321,6 +321,10 @@ pub enum Config {
|
||||
/// True if account is muted.
|
||||
IsMuted,
|
||||
|
||||
/// Optional tag as "Work", "Family".
|
||||
/// Meant to help profile owner to differ between profiles with similar names.
|
||||
PrivateTag,
|
||||
|
||||
/// All secondary self addresses separated by spaces
|
||||
/// (`addr1@example.org addr2@example.org addr3@example.org`)
|
||||
SecondaryAddrs,
|
||||
@@ -857,6 +861,8 @@ impl Context {
|
||||
///
|
||||
/// This should only be used by test code and during configure.
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
self.quota.write().await.take();
|
||||
|
||||
// add old primary address (if exists) to secondary addresses
|
||||
let mut secondary_addrs = self.get_all_self_addrs().await?;
|
||||
// never store a primary address also as a secondary
|
||||
@@ -869,7 +875,7 @@ impl Context {
|
||||
|
||||
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
|
||||
.await?;
|
||||
|
||||
self.emit_event(EventType::ConnectivityChanged);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -13,7 +13,7 @@ mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
pub(crate) mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
@@ -80,10 +80,7 @@ impl Context {
|
||||
|
||||
let res = self
|
||||
.inner_configure()
|
||||
.race(cancel_channel.recv().map(|_| {
|
||||
progress!(self, 0);
|
||||
Ok(())
|
||||
}))
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
|
||||
+3
-1
@@ -179,7 +179,9 @@ pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEX
|
||||
// and may be set together with the username, password etc.
|
||||
// via dc_set_config() using the key "server_flags".
|
||||
|
||||
/// Force OAuth2 authorization. This flag does not skip automatic configuration.
|
||||
/// Force OAuth2 authorization.
|
||||
///
|
||||
/// This flag does not skip automatic configuration.
|
||||
/// Before calling configure() with DC_LP_AUTH_OAUTH2 set,
|
||||
/// the user has to confirm access at the URL returned by dc_get_oauth2_url().
|
||||
pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
|
||||
|
||||
+59
-22
@@ -515,8 +515,11 @@ impl Context {
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Does a background fetch
|
||||
/// pauses the scheduler and does one imap fetch, then unpauses and returns
|
||||
/// Does a single round of fetching from IMAP and returns.
|
||||
///
|
||||
/// Can be used even if I/O is currently stopped.
|
||||
/// If I/O is currently stopped, starts a new IMAP connection
|
||||
/// and fetches from Inbox and DeltaChat folders.
|
||||
pub async fn background_fetch(&self) -> Result<()> {
|
||||
if !(self.is_configured().await?) {
|
||||
return Ok(());
|
||||
@@ -524,35 +527,63 @@ impl Context {
|
||||
|
||||
let address = self.get_primary_self_addr().await?;
|
||||
let time_start = tools::Time::now();
|
||||
info!(self, "background_fetch started fetching {address}");
|
||||
info!(self, "background_fetch started fetching {address}.");
|
||||
|
||||
let _pause_guard = self.scheduler.pause(self.clone()).await?;
|
||||
if self.scheduler.is_running().await {
|
||||
self.scheduler.maybe_network().await;
|
||||
|
||||
// connection
|
||||
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
|
||||
let mut session = connection.prepare(self).await?;
|
||||
// Wait until fetching is finished.
|
||||
// Ideally we could wait for connectivity change events,
|
||||
// but sleep loop is good enough.
|
||||
|
||||
// fetch imap folders
|
||||
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
|
||||
let (_, watch_folder) = convert_folder_meaning(self, folder_meaning).await?;
|
||||
connection
|
||||
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
|
||||
.await?;
|
||||
}
|
||||
// First 100 ms sleep in chunks of 10 ms.
|
||||
for _ in 0..10 {
|
||||
if self.all_work_done().await {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
// update quota (to send warning if full) - but only check it once in a while
|
||||
if self
|
||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
.await
|
||||
{
|
||||
if let Err(err) = self.update_recent_quota(&mut session).await {
|
||||
warn!(self, "Failed to update quota: {err:#}.");
|
||||
// If we are not finished in 100 ms, keep waking up every 100 ms.
|
||||
while !self.all_work_done().await {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
} else {
|
||||
// Pause the scheduler to ensure another connection does not start
|
||||
// while we are fetching on a dedicated connection.
|
||||
let _pause_guard = self.scheduler.pause(self.clone()).await?;
|
||||
|
||||
// Start a new dedicated connection.
|
||||
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
|
||||
let mut session = connection.prepare(self).await?;
|
||||
|
||||
// Fetch IMAP folders.
|
||||
// Inbox is fetched before Mvbox because fetching from Inbox
|
||||
// may result in moving some messages to Mvbox.
|
||||
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
|
||||
if let Some((_folder_config, watch_folder)) =
|
||||
convert_folder_meaning(self, folder_meaning).await?
|
||||
{
|
||||
connection
|
||||
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update quota (to send warning if full) - but only check it once in a while.
|
||||
if self
|
||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
.await
|
||||
{
|
||||
if let Err(err) = self.update_recent_quota(&mut session).await {
|
||||
warn!(self, "Failed to update quota: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
self,
|
||||
"background_fetch done for {address} took {:?}",
|
||||
"background_fetch done for {address} took {:?}.",
|
||||
time_elapsed(&time_start),
|
||||
);
|
||||
|
||||
@@ -826,6 +857,12 @@ impl Context {
|
||||
"is_muted",
|
||||
self.get_config_bool(Config::IsMuted).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"private_tag",
|
||||
self.get_config(Config::PrivateTag)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
|
||||
if let Some(metadata) = &*self.metadata.read().await {
|
||||
if let Some(comment) = &metadata.comment {
|
||||
|
||||
+1
-1
@@ -313,7 +313,7 @@ pub(crate) async fn get_autocrypt_peerstate(
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if addr_cmp(&peerstate.addr, from) {
|
||||
if allow_change {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.apply_header(context, header, message_time);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
} else {
|
||||
info!(
|
||||
|
||||
+18
-3
@@ -98,19 +98,26 @@ impl MsgId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the message download state. Returns `Ok` if the message doesn't exist anymore.
|
||||
pub(crate) async fn update_download_state(
|
||||
self,
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
) -> Result<()> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
context
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET download_state=? WHERE id=?;",
|
||||
(download_state, self),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
== 0
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let Some(msg) = Message::load_from_db_optional(context, self).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: self,
|
||||
@@ -322,11 +329,19 @@ mod tests {
|
||||
DownloadState::InProgress,
|
||||
DownloadState::Failure,
|
||||
DownloadState::Done,
|
||||
DownloadState::Done,
|
||||
] {
|
||||
msg_id.update_download_state(&t, *s).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), *s);
|
||||
}
|
||||
t.sql
|
||||
.execute("DELETE FROM msgs WHERE id=?", (msg_id,))
|
||||
.await?;
|
||||
// Nothing to do is ok.
|
||||
msg_id
|
||||
.update_download_state(&t, DownloadState::Done)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+22
-6
@@ -69,7 +69,7 @@ use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::timeout;
|
||||
@@ -176,9 +176,13 @@ impl ChatId {
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let timer = context
|
||||
.sql
|
||||
.query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
Ok(timer.unwrap_or_default())
|
||||
.query_get_value(
|
||||
"SELECT IFNULL(ephemeral_timer, 0) FROM chats WHERE id=?",
|
||||
(self,),
|
||||
)
|
||||
.await?
|
||||
.with_context(|| format!("Chat {self} not found"))?;
|
||||
Ok(timer)
|
||||
}
|
||||
|
||||
/// Set ephemeral timer value without sending a message.
|
||||
@@ -509,7 +513,8 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
|
||||
FROM msgs
|
||||
WHERE chat_id > ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?;
|
||||
AND chat_id != ?
|
||||
HAVING count(*) > 0
|
||||
"#,
|
||||
(DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
|
||||
)
|
||||
@@ -533,7 +538,8 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
|
||||
SELECT min(ephemeral_timestamp)
|
||||
FROM msgs
|
||||
WHERE ephemeral_timestamp != 0
|
||||
AND chat_id != ?;
|
||||
AND chat_id != ?
|
||||
HAVING count(*) > 0
|
||||
"#,
|
||||
(DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
|
||||
)
|
||||
@@ -1410,4 +1416,14 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
let chat_id = ChatId::new(12345);
|
||||
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::config::Config;
|
||||
use crate::contact::ContactId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
use crate::reaction::Reaction;
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
|
||||
/// Event payload.
|
||||
@@ -94,6 +95,18 @@ pub enum EventType {
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// Reactions for the message changed.
|
||||
IncomingReaction {
|
||||
/// ID of the contact whose reaction set is changed.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// ID of the message for which reactions were changed.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// The reaction.
|
||||
reaction: Reaction,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -288,6 +301,13 @@ pub enum EventType {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Advertisement received over an ephemeral peer channel.
|
||||
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
WebxdcRealtimeAdvertisementReceived {
|
||||
/// Message ID of the webxdc instance.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted.
|
||||
WebxdcInstanceDeleted {
|
||||
/// ID of the deleted message.
|
||||
|
||||
+19
-2
@@ -39,6 +39,7 @@ use crate::login_param::{
|
||||
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
|
||||
use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::oauth2::get_oauth2_access_token;
|
||||
use crate::receive_imf::{
|
||||
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
|
||||
@@ -55,7 +56,7 @@ pub mod scan_folders;
|
||||
pub mod select_folder;
|
||||
pub(crate) mod session;
|
||||
|
||||
use client::Client;
|
||||
use client::{determine_capabilities, Client};
|
||||
use mailparse::SingleInfo;
|
||||
use session::Session;
|
||||
|
||||
@@ -376,7 +377,23 @@ impl Imap {
|
||||
};
|
||||
|
||||
match login_res {
|
||||
Ok(session) => {
|
||||
Ok(mut session) => {
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
|
||||
let session = if capabilities.can_compress {
|
||||
info!(context, "Enabling IMAP compression.");
|
||||
let compressed_session = session
|
||||
.compress(|s| {
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(s);
|
||||
session_stream
|
||||
})
|
||||
.await
|
||||
.context("Failed to enable IMAP compression")?;
|
||||
Session::new(compressed_session, capabilities)
|
||||
} else {
|
||||
Session::new(session, capabilities)
|
||||
};
|
||||
|
||||
// Store server ID in the context to display in account info.
|
||||
let mut lock = context.server_id.write().await;
|
||||
lock.clone_from(&session.capabilities.server_id);
|
||||
|
||||
@@ -25,6 +25,10 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc5464>
|
||||
pub can_metadata: bool,
|
||||
|
||||
/// True if the server has COMPRESS=DEFLATE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc4978>
|
||||
pub can_compress: bool,
|
||||
|
||||
/// True if the server supports XDELTAPUSH capability.
|
||||
/// This capability means setting /private/devicetoken IMAP METADATA
|
||||
/// on the INBOX results in new mail notifications
|
||||
|
||||
+14
-10
@@ -7,7 +7,6 @@ use async_imap::Session as ImapSession;
|
||||
use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use super::session::Session;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
@@ -51,7 +50,7 @@ fn alpn(port: u16) -> &'static [&'static str] {
|
||||
/// Determine server capabilities.
|
||||
///
|
||||
/// If server supports ID capability, send our client ID.
|
||||
async fn determine_capabilities(
|
||||
pub(crate) async fn determine_capabilities(
|
||||
session: &mut ImapSession<Box<dyn SessionStream>>,
|
||||
) -> Result<Capabilities> {
|
||||
let caps = session
|
||||
@@ -69,6 +68,7 @@ async fn determine_capabilities(
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_condstore: caps.has_str("CONDSTORE"),
|
||||
can_metadata: caps.has_str("METADATA"),
|
||||
can_compress: caps.has_str("COMPRESS=DEFLATE"),
|
||||
can_push: caps.has_str("XDELTAPUSH"),
|
||||
is_chatmail: caps.has_str("XCHATMAIL"),
|
||||
server_id,
|
||||
@@ -83,28 +83,31 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
|
||||
pub(crate) async fn login(
|
||||
self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<ImapSession<Box<dyn SessionStream>>> {
|
||||
let Client { inner, .. } = self;
|
||||
let mut session = inner
|
||||
|
||||
let session = inner
|
||||
.login(username, password)
|
||||
.await
|
||||
.map_err(|(err, _client)| err)?;
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
Ok(Session::new(session, capabilities))
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub(crate) async fn authenticate(
|
||||
self,
|
||||
auth_type: &str,
|
||||
authenticator: impl async_imap::Authenticator,
|
||||
) -> Result<Session> {
|
||||
) -> Result<ImapSession<Box<dyn SessionStream>>> {
|
||||
let Client { inner, .. } = self;
|
||||
let mut session = inner
|
||||
let session = inner
|
||||
.authenticate(auth_type, authenticator)
|
||||
.await
|
||||
.map_err(|(err, _client)| err)?;
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
Ok(Session::new(session, capabilities))
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
async fn connection_attempt(
|
||||
@@ -177,6 +180,7 @@ impl Client {
|
||||
Client::connect_insecure_proxy(context, host, port, proxy_config).await?
|
||||
}
|
||||
};
|
||||
update_connection_history(context, "imap", host, port, host, time()).await?;
|
||||
Ok(client)
|
||||
} else {
|
||||
let load_cache = match security {
|
||||
|
||||
+146
-28
@@ -2,13 +2,16 @@
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
|
||||
use ::pgp::types::KeyTrait;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use futures::TryStreamExt;
|
||||
use futures_lite::FutureExt;
|
||||
use pin_project::pin_project;
|
||||
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio_tar::Archive;
|
||||
|
||||
use crate::blob::BlobDirContents;
|
||||
@@ -212,7 +215,7 @@ async fn imex_inner(
|
||||
path.display()
|
||||
);
|
||||
ensure!(context.sql.is_open().await, "Database not opened.");
|
||||
context.emit_event(EventType::ImexProgress(10));
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
|
||||
// before we export anything, make sure the private key exists
|
||||
@@ -294,12 +297,71 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
|
||||
.0
|
||||
}
|
||||
|
||||
/// Reader that emits progress events as bytes are read from it.
|
||||
#[pin_project]
|
||||
struct ProgressReader<R> {
|
||||
/// Wrapped reader.
|
||||
#[pin]
|
||||
inner: R,
|
||||
|
||||
/// Number of bytes successfully read from the internal reader.
|
||||
read: usize,
|
||||
|
||||
/// Total size of the backup .tar file expected to be read from the reader.
|
||||
/// Used to calculate the progress.
|
||||
file_size: usize,
|
||||
|
||||
/// Last progress emitted to avoid emitting the same progress value twice.
|
||||
last_progress: usize,
|
||||
|
||||
/// Context for emitting progress events.
|
||||
context: Context,
|
||||
}
|
||||
|
||||
impl<R> ProgressReader<R> {
|
||||
fn new(r: R, context: Context, file_size: u64) -> Self {
|
||||
Self {
|
||||
inner: r,
|
||||
read: 0,
|
||||
file_size: file_size as usize,
|
||||
last_progress: 1,
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> AsyncRead for ProgressReader<R>
|
||||
where
|
||||
R: AsyncRead,
|
||||
{
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let this = self.project();
|
||||
let before = buf.filled().len();
|
||||
let res = this.inner.poll_read(cx, buf);
|
||||
if let std::task::Poll::Ready(Ok(())) = res {
|
||||
*this.read = this.read.saturating_add(buf.filled().len() - before);
|
||||
|
||||
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999);
|
||||
if progress > *this.last_progress {
|
||||
this.context.emit_event(EventType::ImexProgress(progress));
|
||||
*this.last_progress = progress;
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
context: &Context,
|
||||
backup_file: R,
|
||||
file_size: u64,
|
||||
passphrase: String,
|
||||
) -> (Result<()>,) {
|
||||
let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
|
||||
let mut archive = Archive::new(backup_file);
|
||||
|
||||
let mut entries = match archive.entries() {
|
||||
@@ -307,29 +369,12 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
Err(e) => return (Err(e).context("Failed to get archive entries"),),
|
||||
};
|
||||
let mut blobs = Vec::new();
|
||||
// We already emitted ImexProgress(10) above
|
||||
let mut last_progress = 10;
|
||||
const PROGRESS_MIGRATIONS: u128 = 999;
|
||||
let mut total_size: u64 = 0;
|
||||
let mut res: Result<()> = loop {
|
||||
let mut f = match entries.try_next().await {
|
||||
Ok(Some(f)) => f,
|
||||
Ok(None) => break Ok(()),
|
||||
Err(e) => break Err(e).context("Failed to get next entry"),
|
||||
};
|
||||
total_size += match f.header().entry_size() {
|
||||
Ok(size) => size,
|
||||
Err(e) => break Err(e).context("Failed to get entry size"),
|
||||
};
|
||||
let max = PROGRESS_MIGRATIONS - 1;
|
||||
let progress = std::cmp::min(
|
||||
max * u128::from(total_size) / std::cmp::max(u128::from(file_size), 1),
|
||||
max,
|
||||
);
|
||||
if progress > last_progress {
|
||||
context.emit_event(EventType::ImexProgress(progress as usize));
|
||||
last_progress = progress;
|
||||
}
|
||||
|
||||
let path = match f.path() {
|
||||
Ok(path) => path.to_path_buf(),
|
||||
@@ -379,7 +424,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
.log_err(context)
|
||||
.ok();
|
||||
if res.is_ok() {
|
||||
context.emit_event(EventType::ImexProgress(PROGRESS_MIGRATIONS as usize));
|
||||
context.emit_event(EventType::ImexProgress(999));
|
||||
res = context.sql.run_migrations(context).await;
|
||||
}
|
||||
if res.is_ok() {
|
||||
@@ -452,7 +497,14 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
|
||||
|
||||
let file = File::create(&temp_path).await?;
|
||||
let blobdir = BlobDirContents::new(context).await?;
|
||||
export_backup_stream(context, &temp_db_path, blobdir, file)
|
||||
|
||||
let mut file_size = 0;
|
||||
file_size += temp_db_path.metadata()?.len();
|
||||
for blob in blobdir.iter() {
|
||||
file_size += blob.to_abs_path().metadata()?.len()
|
||||
}
|
||||
|
||||
export_backup_stream(context, &temp_db_path, blobdir, file, file_size)
|
||||
.await
|
||||
.context("Exporting backup to file failed")?;
|
||||
fs::rename(temp_path, &dest_path).await?;
|
||||
@@ -460,33 +512,99 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writer that emits progress events as bytes are written into it.
|
||||
#[pin_project]
|
||||
struct ProgressWriter<W> {
|
||||
/// Wrapped writer.
|
||||
#[pin]
|
||||
inner: W,
|
||||
|
||||
/// Number of bytes successfully written into the internal writer.
|
||||
written: usize,
|
||||
|
||||
/// Total size of the backup .tar file expected to be written into the writer.
|
||||
/// Used to calculate the progress.
|
||||
file_size: usize,
|
||||
|
||||
/// Last progress emitted to avoid emitting the same progress value twice.
|
||||
last_progress: usize,
|
||||
|
||||
/// Context for emitting progress events.
|
||||
context: Context,
|
||||
}
|
||||
|
||||
impl<W> ProgressWriter<W> {
|
||||
fn new(w: W, context: Context, file_size: u64) -> Self {
|
||||
Self {
|
||||
inner: w,
|
||||
written: 0,
|
||||
file_size: file_size as usize,
|
||||
last_progress: 1,
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> AsyncWrite for ProgressWriter<W>
|
||||
where
|
||||
W: AsyncWrite,
|
||||
{
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, std::io::Error>> {
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write(cx, buf);
|
||||
if let std::task::Poll::Ready(Ok(written)) = res {
|
||||
*this.written = this.written.saturating_add(written);
|
||||
|
||||
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999);
|
||||
if progress > *this.last_progress {
|
||||
this.context.emit_event(EventType::ImexProgress(progress));
|
||||
*this.last_progress = progress;
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
self.project().inner.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
self.project().inner.poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Exports the database and blobs into a stream.
|
||||
pub(crate) async fn export_backup_stream<'a, W>(
|
||||
context: &'a Context,
|
||||
temp_db_path: &Path,
|
||||
blobdir: BlobDirContents<'a>,
|
||||
writer: W,
|
||||
file_size: u64,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
|
||||
{
|
||||
let writer = ProgressWriter::new(writer, context.clone(), file_size);
|
||||
let mut builder = tokio_tar::Builder::new(writer);
|
||||
|
||||
builder
|
||||
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
|
||||
.await?;
|
||||
|
||||
let mut last_progress = 10;
|
||||
|
||||
for (i, blob) in blobdir.iter().enumerate() {
|
||||
for blob in blobdir.iter() {
|
||||
let mut file = File::open(blob.to_abs_path()).await?;
|
||||
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
|
||||
builder.append_file(path_in_archive, &mut file).await?;
|
||||
let progress = std::cmp::min(1000 * i / blobdir.len(), 999);
|
||||
if progress > last_progress {
|
||||
context.emit_event(EventType::ImexProgress(progress));
|
||||
last_progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
builder.finish().await?;
|
||||
|
||||
+34
-5
@@ -31,7 +31,8 @@ use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use futures_lite::FutureExt;
|
||||
use iroh_net::relay::RelayMode;
|
||||
use iroh_net::Endpoint;
|
||||
use tokio::fs;
|
||||
@@ -108,6 +109,7 @@ impl BackupProvider {
|
||||
.get_blobdir()
|
||||
.parent()
|
||||
.context("Context dir not found")?;
|
||||
|
||||
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
|
||||
if fs::metadata(&dbfile).await.is_ok() {
|
||||
fs::remove_file(&dbfile).await?;
|
||||
@@ -123,7 +125,6 @@ impl BackupProvider {
|
||||
export_database(context, &dbfile, passphrase, time())
|
||||
.await
|
||||
.context("Database export failed")?;
|
||||
context.emit_event(EventType::ImexProgress(300));
|
||||
|
||||
let drop_token = CancellationToken::new();
|
||||
let handle = {
|
||||
@@ -177,6 +178,7 @@ impl BackupProvider {
|
||||
}
|
||||
|
||||
info!(context, "Received valid backup authentication token.");
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
let blobdir = BlobDirContents::new(&context).await?;
|
||||
|
||||
@@ -188,7 +190,7 @@ impl BackupProvider {
|
||||
|
||||
send_stream.write_all(&file_size.to_be_bytes()).await?;
|
||||
|
||||
export_backup_stream(&context, &dbfile, blobdir, send_stream)
|
||||
export_backup_stream(&context, &dbfile, blobdir, send_stream, file_size)
|
||||
.await
|
||||
.context("Failed to write backup into QUIC stream")?;
|
||||
info!(context, "Finished writing backup into QUIC stream.");
|
||||
@@ -231,8 +233,20 @@ impl BackupProvider {
|
||||
let context = context.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
let dbfile = dbfile.clone();
|
||||
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).await {
|
||||
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).race(
|
||||
async {
|
||||
cancel_token.recv().await.ok();
|
||||
Err(format_err!("Backup transfer cancelled"))
|
||||
}
|
||||
).race(
|
||||
async {
|
||||
drop_token.cancelled().await;
|
||||
Err(format_err!("Backup provider dropped"))
|
||||
}
|
||||
).await {
|
||||
warn!(context, "Error while handling backup connection: {err:#}.");
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
} else {
|
||||
info!(context, "Backup transfer finished successfully.");
|
||||
break;
|
||||
@@ -242,10 +256,12 @@ impl BackupProvider {
|
||||
}
|
||||
},
|
||||
_ = cancel_token.recv() => {
|
||||
info!(context, "Backup transfer cancelled by the user, stopping accept loop.");
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
}
|
||||
_ = drop_token.cancelled() => {
|
||||
info!(context, "Backup transfer cancelled by dropping the provider, stopping accept loop.");
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
}
|
||||
@@ -329,7 +345,20 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
Qr::Backup2 {
|
||||
node_addr,
|
||||
auth_token,
|
||||
} => get_backup2(context, node_addr, auth_token).await?,
|
||||
} => {
|
||||
let cancel_token = context.alloc_ongoing().await?;
|
||||
let res = get_backup2(context, node_addr, auth_token)
|
||||
.race(async {
|
||||
cancel_token.recv().await.ok();
|
||||
Err(format_err!("Backup reception cancelled"))
|
||||
})
|
||||
.await;
|
||||
if res.is_err() {
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
}
|
||||
context.free_ongoing().await;
|
||||
res?;
|
||||
}
|
||||
_ => bail!("QR code for backup must be of type DCBACKUP2"),
|
||||
}
|
||||
Ok(())
|
||||
|
||||
+23
-24
@@ -148,21 +148,6 @@ impl MsgId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
|
||||
pub(crate) async fn delete_from_db(self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM smtp WHERE msg_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM msgs_mdns WHERE msg_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM msgs_status_updates WHERE msg_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM msgs WHERE id=?", (self,))?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
|
||||
update_msg_state(context, self, MessageState::OutDelivered).await?;
|
||||
let chat_id: ChatId = context
|
||||
@@ -219,11 +204,13 @@ impl MsgId {
|
||||
}
|
||||
|
||||
/// Returns information about hops of a message, used for message info
|
||||
pub async fn hop_info(self, context: &Context) -> Result<Option<String>> {
|
||||
context
|
||||
pub async fn hop_info(self, context: &Context) -> Result<String> {
|
||||
let hop_info = context
|
||||
.sql
|
||||
.query_get_value("SELECT hop_info FROM msgs WHERE id=?", (self,))
|
||||
.await
|
||||
.query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
.with_context(|| format!("Message {self} not found"))?;
|
||||
Ok(hop_info)
|
||||
}
|
||||
|
||||
/// Returns detailed message information in a multi-line text form.
|
||||
@@ -328,7 +315,12 @@ impl MsgId {
|
||||
|
||||
if let Some(path) = msg.get_file(context) {
|
||||
let bytes = get_filebytes(context, &path).await?;
|
||||
ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes);
|
||||
ret += &format!(
|
||||
"\nFile: {}, name: {}, {} bytes\n",
|
||||
path.display(),
|
||||
msg.get_filename().unwrap_or_default(),
|
||||
bytes
|
||||
);
|
||||
}
|
||||
|
||||
if msg.viewtype != Viewtype::Text {
|
||||
@@ -361,7 +353,11 @@ impl MsgId {
|
||||
let hop_info = self.hop_info(context).await?;
|
||||
|
||||
ret += "\n\n";
|
||||
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
|
||||
if hop_info.is_empty() {
|
||||
ret += "No Hop Info";
|
||||
} else {
|
||||
ret += &hop_info;
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
@@ -1815,8 +1811,8 @@ pub(crate) async fn update_msg_state(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!("UPDATE msgs SET state=?1 {error_subst} WHERE id=?2 AND (?1!=?3 OR state<?3)"),
|
||||
(state, msg_id, MessageState::OutDelivered),
|
||||
&format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
|
||||
(state, msg_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1904,6 +1900,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
|
||||
|
||||
/// Estimates the number of messages that will be deleted
|
||||
/// 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.
|
||||
///
|
||||
@@ -1993,7 +1990,9 @@ pub(crate) async fn rfc724_mid_exists_ex(
|
||||
.query_row_optional(
|
||||
&("SELECT id, timestamp_sent, MIN(".to_string()
|
||||
+ expr
|
||||
+ ") FROM msgs WHERE rfc724_mid=? ORDER BY timestamp_sent DESC"),
|
||||
+ ") FROM msgs WHERE rfc724_mid=?
|
||||
HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
|
||||
ORDER BY timestamp_sent DESC"),
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
|
||||
+13
-3
@@ -82,7 +82,10 @@ pub struct MimeFactory {
|
||||
/// as needed.
|
||||
references: String,
|
||||
|
||||
/// True if the message requests Message Disposition Notification
|
||||
/// using `Chat-Disposition-Notification-To` header.
|
||||
req_mdn: bool,
|
||||
|
||||
last_added_location_id: Option<u32>,
|
||||
|
||||
/// If the created mime-structure contains sync-items,
|
||||
@@ -201,7 +204,8 @@ impl MimeFactory {
|
||||
let (in_reply_to, references) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
|
||||
"SELECT mime_in_reply_to, IFNULL(mime_references, '')
|
||||
FROM msgs WHERE id=?",
|
||||
(msg.id,),
|
||||
|row| {
|
||||
let in_reply_to: String = row.get(0)?;
|
||||
@@ -351,7 +355,11 @@ impl MimeFactory {
|
||||
// beside key- and member-changes, force a periodic re-gossip.
|
||||
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
|
||||
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||
if time() >= gossiped_timestamp + gossip_period {
|
||||
// `gossip_period == 0` is a special case for testing,
|
||||
// enabling gossip in every message.
|
||||
// Othewise "smeared timestamps" may result in the condition
|
||||
// to fail even if the clock is monotonic.
|
||||
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
@@ -609,7 +617,9 @@ impl MimeFactory {
|
||||
// because replies to "Disposition-Notification-To" are weird in many cases
|
||||
// eg. are just freetext and/or do not follow any standard.
|
||||
headers.push(Header::new(
|
||||
"Chat-Disposition-Notification-To".into(),
|
||||
HeaderDef::ChatDispositionNotificationTo
|
||||
.get_headername()
|
||||
.to_string(),
|
||||
self.from_addr.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@ pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the timestamp of the last successfull connection
|
||||
/// to the given `host` and `port`
|
||||
/// with the given application protocol `alpn`.
|
||||
///
|
||||
/// `addr` is the string representation of IP address.
|
||||
/// If connection is made over a proxy which does
|
||||
/// its own DNS resolution,
|
||||
/// `addr` should be the same as `host`.
|
||||
pub(crate) async fn update_connection_history(
|
||||
context: &Context,
|
||||
alpn: &str,
|
||||
|
||||
+289
-26
@@ -1,4 +1,44 @@
|
||||
//! DNS resolution and cache.
|
||||
//!
|
||||
//! DNS cache in Delta Chat has two layers:
|
||||
//! in-memory cache and persistent `dns_cache` SQL table.
|
||||
//!
|
||||
//! In-memory cache is using a "stale-while-revalidate" strategy.
|
||||
//! If there is a cached value, it is returned immediately
|
||||
//! and revalidation task is started in the background
|
||||
//! to replace old cached IP addresses with new ones.
|
||||
//! If there is no cached value yet,
|
||||
//! lookup only finishes when `lookup_host` returns first results.
|
||||
//! In-memory cache is shared between all accounts
|
||||
//! and is never stored on the disk.
|
||||
//! It can be thought of as an extension
|
||||
//! of the system resolver.
|
||||
//!
|
||||
//! Persistent `dns_cache` SQL table is used to collect
|
||||
//! all IP addresses ever seen for the hostname
|
||||
//! together with the timestamp
|
||||
//! of the last time IP address has been seen.
|
||||
//! Note that this timestamp reflects the time
|
||||
//! IP address was returned by the in-memory cache
|
||||
//! rather than the underlying system resolver.
|
||||
//! Unused entries are removed after 30 days
|
||||
//! (`CACHE_TTL` constant) to avoid having
|
||||
//! old non-working IP addresses in the cache indefinitely.
|
||||
//!
|
||||
//! When Delta Chat needs an IP address for the host,
|
||||
//! it queries in-memory cache for the next result
|
||||
//! and merges the list of IP addresses
|
||||
//! with the list of IP addresses from persistent cache.
|
||||
//! Resulting list is constructed
|
||||
//! by taking the first two results from the resolver
|
||||
//! followed up by persistent cache results
|
||||
//! and terminated by the rest of resolver results.
|
||||
//!
|
||||
//! Persistent cache results are sorted
|
||||
//! by the time of the most recent successful connection
|
||||
//! using the result. For results that have never been
|
||||
//! used for successful connection timestamp of
|
||||
//! retrieving them from in-memory cache is used.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use std::collections::HashMap;
|
||||
@@ -42,33 +82,110 @@ pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Looks up the hostname and updates DNS cache
|
||||
/// on success.
|
||||
/// Map from hostname to IP addresses.
|
||||
///
|
||||
/// NOTE: sync RwLock is used, so it must not be held across `.await`
|
||||
/// to avoid deadlocks.
|
||||
/// See
|
||||
/// <https://docs.rs/tokio/1.40.0/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
|
||||
/// and
|
||||
/// <https://stackoverflow.com/questions/63712823/why-do-i-get-a-deadlock-when-using-tokio-with-a-stdsyncmutex>.
|
||||
static LOOKUP_HOST_CACHE: Lazy<parking_lot::RwLock<HashMap<String, Vec<IpAddr>>>> =
|
||||
Lazy::new(Default::default);
|
||||
|
||||
/// Wrapper for `lookup_host` that returns IP addresses.
|
||||
async fn lookup_ips(host: impl tokio::net::ToSocketAddrs) -> Result<impl Iterator<Item = IpAddr>> {
|
||||
Ok(lookup_host(host)
|
||||
.await
|
||||
.context("DNS lookup failure")?
|
||||
.map(|addr| addr.ip()))
|
||||
}
|
||||
|
||||
async fn lookup_host_with_memory_cache(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
) -> Result<Vec<IpAddr>> {
|
||||
let stale_result = {
|
||||
let rwlock_read_guard = LOOKUP_HOST_CACHE.read();
|
||||
rwlock_read_guard.get(hostname).cloned()
|
||||
};
|
||||
if let Some(stale_result) = stale_result {
|
||||
// Revalidate the cache in the background.
|
||||
{
|
||||
let context = context.clone();
|
||||
let hostname = hostname.to_string();
|
||||
tokio::spawn(async move {
|
||||
match lookup_ips((hostname.clone(), port)).await {
|
||||
Ok(res) => {
|
||||
LOOKUP_HOST_CACHE.write().insert(hostname, res.collect());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to revalidate results for {hostname:?}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Using memory-cached DNS resolution for {hostname}."
|
||||
);
|
||||
Ok(stale_result)
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"No memory-cached DNS resolution for {hostname} available, waiting for the resolver."
|
||||
);
|
||||
let res: Vec<IpAddr> = lookup_ips((hostname, port)).await?.collect();
|
||||
|
||||
// Insert initial result into the cache.
|
||||
//
|
||||
// There may already be a result from a parallel
|
||||
// task stored, overwriting it is not a problem.
|
||||
LOOKUP_HOST_CACHE
|
||||
.write()
|
||||
.insert(hostname.to_string(), res.clone());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up the hostname and updates
|
||||
/// persistent DNS cache on success.
|
||||
async fn lookup_host_and_update_cache(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
now: i64,
|
||||
) -> Result<Vec<SocketAddr>> {
|
||||
let res: Vec<SocketAddr> = timeout(super::TIMEOUT, lookup_host((hostname, port)))
|
||||
.await
|
||||
.context("DNS lookup timeout")?
|
||||
.context("DNS lookup failure")?
|
||||
.collect();
|
||||
let res: Vec<IpAddr> = timeout(
|
||||
super::TIMEOUT,
|
||||
lookup_host_with_memory_cache(context, hostname, port),
|
||||
)
|
||||
.await
|
||||
.context("DNS lookup timeout")?
|
||||
.context("DNS lookup with memory cache failure")?;
|
||||
|
||||
for addr in &res {
|
||||
let ip_string = addr.ip().to_string();
|
||||
for ip in &res {
|
||||
let ip_string = ip.to_string();
|
||||
if ip_string == hostname {
|
||||
// IP address resolved into itself, not interesting to cache.
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(context, "Resolved {hostname}:{port} into {addr}.");
|
||||
info!(context, "Resolved {hostname} into {ip}.");
|
||||
|
||||
// Update the cache.
|
||||
update_cache(context, hostname, &ip_string, now).await?;
|
||||
}
|
||||
|
||||
let res = res
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -597,9 +714,14 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
load_cache: bool,
|
||||
) -> Result<Vec<SocketAddr>> {
|
||||
let now = time();
|
||||
let mut resolved_addrs = match lookup_host_and_update_cache(context, hostname, port, now).await
|
||||
{
|
||||
Ok(res) => res,
|
||||
let resolved_addrs = match lookup_host_and_update_cache(context, hostname, port, now).await {
|
||||
Ok(res) => {
|
||||
if alpn.is_empty() {
|
||||
res
|
||||
} else {
|
||||
sort_by_connection_timestamp(context, res, alpn, hostname).await?
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
@@ -608,29 +730,43 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !alpn.is_empty() {
|
||||
resolved_addrs =
|
||||
sort_by_connection_timestamp(context, resolved_addrs, alpn, hostname).await?;
|
||||
}
|
||||
|
||||
if load_cache {
|
||||
for addr in lookup_cache(context, hostname, port, alpn, now).await? {
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = lookup_cache(context, hostname, port, alpn, now).await?;
|
||||
if let Some(ips) = DNS_PRELOAD.get(hostname) {
|
||||
for ip in ips {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
if !cache.contains(&addr) {
|
||||
cache.push(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(merge_with_cache(resolved_addrs, cache))
|
||||
} else {
|
||||
Ok(resolved_addrs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges results received from DNS with cached results.
|
||||
///
|
||||
/// At most 10 results are returned.
|
||||
fn merge_with_cache(
|
||||
mut resolved_addrs: Vec<SocketAddr>,
|
||||
cache: Vec<SocketAddr>,
|
||||
) -> Vec<SocketAddr> {
|
||||
let rest = resolved_addrs.split_off(std::cmp::min(resolved_addrs.len(), 2));
|
||||
|
||||
for addr in cache.into_iter().chain(rest.into_iter()) {
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
if resolved_addrs.len() >= 10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_addrs)
|
||||
resolved_addrs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -866,4 +1002,131 @@ mod tests {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_with_cache() {
|
||||
let first_addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
|
||||
let second_addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2));
|
||||
|
||||
// If there is no cache, just return resolved addresses.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
];
|
||||
let cache = vec![];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs.clone(), cache),
|
||||
resolved_addrs
|
||||
);
|
||||
}
|
||||
|
||||
// If cache contains address that is not in resolution results,
|
||||
// it is inserted in the merged result.
|
||||
{
|
||||
let resolved_addrs = vec![SocketAddr::new(first_addr, 993)];
|
||||
let cache = vec![SocketAddr::new(second_addr, 993)];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// If cache contains address that is already in resolution results,
|
||||
// it is not duplicated.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
];
|
||||
let cache = vec![SocketAddr::new(second_addr, 993)];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// If DNS resolvers returns a lot of results,
|
||||
// we should try cached results before going through all
|
||||
// the resolver results.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
];
|
||||
let cache = vec![SocketAddr::new(second_addr, 993)];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Even if cache already contains all the incorrect results
|
||||
// that resolver returns, this should not result in them being sorted to the top.
|
||||
// Cache has known to work result returned first,
|
||||
// so we should try it after the second result.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
];
|
||||
let cache = vec![
|
||||
SocketAddr::new(second_addr, 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ impl<T: SessionStream> SessionStream for shadowsocks::ProxyClientStream<T> {
|
||||
self.get_mut().set_read_timeout(timeout)
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for async_imap::DeflateStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// Session stream with a read buffer.
|
||||
pub(crate) trait SessionBufStream: SessionStream + AsyncBufRead {}
|
||||
|
||||
@@ -576,6 +576,13 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
|
||||
assert!(msg_id == alice_webxdc.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
|
||||
|
||||
// Bob adds alice to gossip peers.
|
||||
|
||||
+73
-22
@@ -343,7 +343,7 @@ impl Peerstate {
|
||||
}
|
||||
|
||||
/// Updates peerstate according to the given `Autocrypt` header.
|
||||
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
|
||||
pub fn apply_header(&mut self, context: &Context, header: &Aheader, message_time: i64) {
|
||||
if !addr_cmp(&self.addr, &header.addr) {
|
||||
return;
|
||||
}
|
||||
@@ -362,6 +362,13 @@ impl Peerstate {
|
||||
self.public_key = Some(header.public_key.clone());
|
||||
self.recalc_fingerprint();
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring outdated Autocrypt header because message_time={} < last_seen={}.",
|
||||
message_time,
|
||||
self.last_seen
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,23 +773,65 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
|
||||
// If the from addr is different from the peerstate address we know,
|
||||
// we may want to do an AEAP transition.
|
||||
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr)
|
||||
// Check if it's a chat message; we do this to avoid
|
||||
// some accidental transitions if someone writes from multiple
|
||||
// addresses with an MUA.
|
||||
&& mime_parser.has_chat_version()
|
||||
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
|
||||
// probably from a new contact sharing the same key.
|
||||
&& !mime_parser.signatures.is_empty()
|
||||
// Check if the From: address was also in the signed part of the email.
|
||||
// Without this check, an attacker could replay a message from Alice
|
||||
// to Bob. Then Bob's device would do an AEAP transition from Alice's
|
||||
// to the attacker's address, allowing for easier phishing.
|
||||
&& mime_parser.from_is_signed
|
||||
// DC avoids sending messages with the same timestamp, that's why `>` is here unlike in
|
||||
// `Peerstate::apply_header()`.
|
||||
&& info.message_time > peerstate.last_seen
|
||||
{
|
||||
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr) {
|
||||
// Check if it's a chat message; we do this to avoid
|
||||
// some accidental transitions if someone writes from multiple
|
||||
// addresses with an MUA.
|
||||
if !mime_parser.has_chat_version() {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because the message is not a chat message.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
|
||||
// probably from a new contact sharing the same key.
|
||||
if mime_parser.signatures.is_empty() {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because the message is not encrypted and signed.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if the From: address was also in the signed part of the email.
|
||||
// Without this check, an attacker could replay a message from Alice
|
||||
// to Bob. Then Bob's device would do an AEAP transition from Alice's
|
||||
// to the attacker's address, allowing for easier phishing.
|
||||
if !mime_parser.from_is_signed {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because From: is not signed.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// DC avoids sending messages with the same timestamp, that's why messages
|
||||
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
|
||||
if info.message_time <= peerstate.last_seen {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because {} < {}.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr,
|
||||
info.message_time,
|
||||
peerstate.last_seen
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
|
||||
);
|
||||
|
||||
let info = &mut mime_parser.decryption_info;
|
||||
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
|
||||
// Add info messages to chats with this (verified) contact
|
||||
@@ -800,7 +849,7 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
peerstate.apply_header(header, info.message_time);
|
||||
peerstate.apply_header(context, header, info.message_time);
|
||||
|
||||
peerstate
|
||||
.save_to_db_ex(&context.sql, Some(&old_addr))
|
||||
@@ -979,6 +1028,8 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_peerstate_degrade_reordering() {
|
||||
let ctx = crate::test_utils::TestContext::new().await;
|
||||
|
||||
let addr = "example@example.org";
|
||||
let pub_key = alice_keypair().public;
|
||||
let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
|
||||
@@ -1003,7 +1054,7 @@ mod tests {
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
peerstate.apply_header(&header, 100);
|
||||
peerstate.apply_header(&ctx, &header, 100);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
peerstate.degrade_encryption(300);
|
||||
@@ -1011,11 +1062,11 @@ mod tests {
|
||||
|
||||
// This has message time 200, while encryption was degraded at timestamp 300.
|
||||
// Because of reordering, header should not be applied.
|
||||
peerstate.apply_header(&header, 200);
|
||||
peerstate.apply_header(&ctx, &header, 200);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
// Same header will be applied in the future.
|
||||
peerstate.apply_header(&header, 300);
|
||||
peerstate.apply_header(&ctx, &header, 300);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ enum SignedPublicKeyOrSubkey<'a> {
|
||||
Subkey(&'a SignedPublicSubKey),
|
||||
}
|
||||
|
||||
impl<'a> KeyTrait for SignedPublicKeyOrSubkey<'a> {
|
||||
impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn fingerprint(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Key(k) => k.fingerprint(),
|
||||
@@ -64,7 +64,7 @@ impl<'a> KeyTrait for SignedPublicKeyOrSubkey<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
|
||||
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn verify_signature(
|
||||
&self,
|
||||
hash: HashAlgorithm,
|
||||
|
||||
@@ -14,6 +14,65 @@ use crate::qr::{self, Qr};
|
||||
use crate::securejoin;
|
||||
use crate::stock_str::{self, backup_transfer_qr};
|
||||
|
||||
/// Create a QR code from any input data.
|
||||
pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
|
||||
let all_size = 512.0;
|
||||
let qr_code_size = 416.0;
|
||||
|
||||
let qr = QrCode::encode_text(qrcode_content, QrCodeEcc::Medium)?;
|
||||
let mut svg = String::with_capacity(28000);
|
||||
let mut w = tagger::new(&mut svg);
|
||||
|
||||
w.elem("svg", |d| {
|
||||
d.attr("xmlns", "http://www.w3.org/2000/svg")?;
|
||||
d.attr("viewBox", format_args!("0 0 {all_size} {all_size}"))?;
|
||||
d.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")?; // required for enabling xlink:href on browsers
|
||||
Ok(())
|
||||
})?
|
||||
.build(|w| {
|
||||
// background
|
||||
w.single("rect", |d| {
|
||||
d.attr("x", 0)?;
|
||||
d.attr("y", 0)?;
|
||||
d.attr("width", all_size)?;
|
||||
d.attr("height", all_size)?;
|
||||
d.attr("style", "fill:#ffffff")?;
|
||||
Ok(())
|
||||
})?;
|
||||
// QR code
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(all_size - qr_code_size) / 2.0,
|
||||
((all_size - qr_code_size) / 2.0)
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| {
|
||||
w.single("path", |d| {
|
||||
let mut path_data = String::with_capacity(0);
|
||||
let scale = qr_code_size / qr.size() as f32;
|
||||
|
||||
for y in 0..qr.size() {
|
||||
for x in 0..qr.size() {
|
||||
if qr.get_module(x, y) {
|
||||
path_data += &format!("M{x},{y}h1v1h-1z");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.attr("style", "fill:#000000")?;
|
||||
d.attr("d", path_data)?;
|
||||
d.attr("transform", format!("scale({scale})"))
|
||||
})
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
/// Returns SVG of the QR code to join the group or verify contact.
|
||||
///
|
||||
/// If `chat_id` is `None`, returns verification QR code.
|
||||
@@ -304,6 +363,14 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_qr_svg() -> Result<()> {
|
||||
let svg = create_qr_svg("this is a test QR code \" < > &")?;
|
||||
assert!(svg.contains("<svg"));
|
||||
assert!(svg.contains("</svg>"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_svg_escaping() {
|
||||
let svg = inner_generate_secure_join_qr_code(
|
||||
|
||||
+12
-2
@@ -25,6 +25,7 @@ pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
|
||||
|
||||
/// if quota is below this value (again),
|
||||
/// QuotaExceeding is cleared.
|
||||
///
|
||||
/// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to
|
||||
/// avoid jittering and lots of warnings when quota is exactly at the warning threshold.
|
||||
///
|
||||
@@ -73,7 +74,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
|
||||
fn get_highest_usage<'t>(
|
||||
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
|
||||
) -> Result<(u64, &'t String, &QuotaResource)> {
|
||||
) -> Result<(u64, &'t String, &'t QuotaResource)> {
|
||||
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||
for (name, resources) in unique_quota_roots {
|
||||
for r in resources {
|
||||
@@ -197,7 +198,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_needs_update() {
|
||||
async fn test_quota_needs_update() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
const TIMEOUT: u64 = 60;
|
||||
@@ -214,5 +215,14 @@ mod tests {
|
||||
modified: tools::Time::now(),
|
||||
});
|
||||
assert!(!t.quota_needs_update(TIMEOUT).await);
|
||||
|
||||
t.evtracker.clear_events();
|
||||
t.set_primary_self_addr("new@addr").await?;
|
||||
assert!(t.quota.read().await.is_none());
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
|
||||
.await;
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+54
-5
@@ -19,6 +19,7 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{send_msg, Chat, ChatId};
|
||||
use crate::chatlist_events;
|
||||
@@ -31,7 +32,7 @@ use crate::param::Param;
|
||||
/// A single reaction consisting of multiple emoji sequences.
|
||||
///
|
||||
/// It is guaranteed to have all emojis sorted and deduplicated inside.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct Reaction {
|
||||
/// Canonical representation of reaction as a string of space-separated emojis.
|
||||
reaction: String,
|
||||
@@ -173,7 +174,7 @@ async fn set_msg_id_reaction(
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: Reaction,
|
||||
reaction: &Reaction,
|
||||
) -> Result<()> {
|
||||
if reaction.is_empty() {
|
||||
// Simply remove the record instead of setting it to empty string.
|
||||
@@ -244,7 +245,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
|
||||
msg.chat_id,
|
||||
ContactId::SELF,
|
||||
reaction_msg.timestamp_sort,
|
||||
reaction,
|
||||
&reaction,
|
||||
)
|
||||
.await?;
|
||||
Ok(reaction_msg_id)
|
||||
@@ -275,16 +276,28 @@ pub(crate) async fn set_msg_reaction(
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: Reaction,
|
||||
is_incoming_fresh: bool,
|
||||
) -> Result<()> {
|
||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
|
||||
|
||||
if is_incoming_fresh
|
||||
&& !reaction.is_empty()
|
||||
&& msg_id.get_state(context).await?.is_outgoing()
|
||||
{
|
||||
context.emit_event(EventType::IncomingReaction {
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get our own reaction for a given message.
|
||||
@@ -563,6 +576,38 @@ Here's my footer -- bob@example.net"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn expect_incoming_reactions_event(
|
||||
t: &TestContext,
|
||||
expected_msg_id: MsgId,
|
||||
expected_contact_id: ContactId,
|
||||
expected_reaction: &str,
|
||||
) -> Result<()> {
|
||||
let event = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingReaction { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::IncomingReaction {
|
||||
msg_id,
|
||||
contact_id,
|
||||
reaction,
|
||||
} => {
|
||||
assert_eq!(msg_id, expected_msg_id);
|
||||
assert_eq!(contact_id, expected_contact_id);
|
||||
assert_eq!(reaction, Reaction::from(expected_reaction));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn has_incoming_reactions_event(t: &TestContext) -> bool {
|
||||
t.evtracker
|
||||
.get_matching_opt(t, |evt| matches!(evt, EventType::IncomingReaction { .. }))
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_reaction() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -593,6 +638,7 @@ Here's my footer -- bob@example.net"
|
||||
|
||||
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
|
||||
assert!(!has_incoming_reactions_event(&bob).await);
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
@@ -610,6 +656,7 @@ Here's my footer -- bob@example.net"
|
||||
assert_eq!(bob_reaction.as_str(), "👍");
|
||||
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
|
||||
.await?;
|
||||
expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?;
|
||||
|
||||
// Alice reacts to own message.
|
||||
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
|
||||
@@ -650,6 +697,7 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&bob, bob_msg1.id, "👍").await?;
|
||||
let bob_send_reaction = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&bob_send_reaction).await;
|
||||
assert!(has_incoming_reactions_event(&alice).await);
|
||||
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
@@ -665,6 +713,7 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
|
||||
let alice_send_reaction = alice.pop_sent_msg().await;
|
||||
bob.recv_msg_opt(&alice_send_reaction).await;
|
||||
assert!(!has_incoming_reactions_event(&bob).await);
|
||||
|
||||
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
|
||||
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;
|
||||
|
||||
@@ -1255,11 +1255,13 @@ 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,
|
||||
)
|
||||
.await?;
|
||||
@@ -1444,6 +1446,9 @@ async fn add_parts(
|
||||
Ok(node_addr) => {
|
||||
info!(context, "Adding iroh peer with address {node_addr:?}.");
|
||||
let instance_id = parent.context("Failed to get parent message")?.id;
|
||||
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: instance_id,
|
||||
});
|
||||
if let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? {
|
||||
let node_id = node_addr.node_id;
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
@@ -1468,6 +1473,7 @@ async fn add_parts(
|
||||
for part in &mime_parser.parts {
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages;
|
||||
set_msg_reaction(
|
||||
context,
|
||||
mime_in_reply_to,
|
||||
@@ -1475,6 +1481,7 @@ async fn add_parts(
|
||||
from_id,
|
||||
sort_timestamp,
|
||||
Reaction::from(reaction_str.as_str()),
|
||||
is_incoming_fresh,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -3163,20 +3163,21 @@ Reply from different address
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_and_duplicated_filenames() -> Result<()> {
|
||||
async fn test_weird_and_duplicated_filenames() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
for filename_sent in &[
|
||||
"foo.bar very long file name test baz.tar.gz",
|
||||
"foobarabababababababbababababverylongfilenametestbaz.tar.gz",
|
||||
"foo.barabababababababbababababverylongfilenametestbaz.tar.gz",
|
||||
"fooo...tar.gz",
|
||||
"foo. .tar.gz",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz",
|
||||
"a.tar.gz",
|
||||
"a.tar.gz",
|
||||
"a.a..a.a.a.a.tar.gz",
|
||||
"a. tar.tar.gz",
|
||||
] {
|
||||
let attachment = alice.blobdir.join(filename_sent);
|
||||
let content = format!("File content of {filename_sent}");
|
||||
@@ -4810,6 +4811,36 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_older_message_from_2nd_device() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice
|
||||
.create_chat_with_contact("", "bob@example.net")
|
||||
.await
|
||||
.id;
|
||||
alice.send_text(chat_id, "We share this account").await;
|
||||
let received = receive_imf(
|
||||
alice,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Message-ID: <1234-2-4@example.org>\n\
|
||||
Date: Sat, 07 Dec 1970 19:00:26 +0000\n\
|
||||
\n\
|
||||
I'm Alice too\n",
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
alice
|
||||
.golden_test_chat(
|
||||
received.chat_id,
|
||||
"receive_imf_older_message_from_2nd_device",
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_create_adhoc_group_on_member_removal() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
+24
-30
@@ -433,36 +433,33 @@ async fn inbox_loop(
|
||||
}
|
||||
|
||||
/// Convert folder meaning
|
||||
/// used internally by [fetch_idle] and [Context::background_fetch]
|
||||
/// used internally by [fetch_idle] and [Context::background_fetch].
|
||||
///
|
||||
/// Returns folder configuration key and folder name
|
||||
/// if such folder is configured, `Ok(None)` otherwise.
|
||||
pub async fn convert_folder_meaning(
|
||||
ctx: &Context,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<(Config, String)> {
|
||||
) -> Result<Option<(Config, String)>> {
|
||||
let folder_config = match folder_meaning.to_config() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
bail!("Bad folder meaning: {}", folder_meaning);
|
||||
// Such folder cannot be configured,
|
||||
// e.g. a `FolderMeaning::Spam` folder.
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let folder = match ctx.get_config(folder_config).await {
|
||||
Ok(folder) => folder,
|
||||
Err(err) => {
|
||||
bail!(
|
||||
"Can not watch {} folder, failed to retrieve config: {:#}",
|
||||
folder_config,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
let folder = ctx
|
||||
.get_config(folder_config)
|
||||
.await
|
||||
.with_context(|| format!("Failed to retrieve {folder_config} folder"))?;
|
||||
|
||||
let watch_folder = if let Some(watch_folder) = folder {
|
||||
watch_folder
|
||||
if let Some(watch_folder) = folder {
|
||||
Ok(Some((folder_config, watch_folder)))
|
||||
} else {
|
||||
bail!("Can not watch {} folder, not set", folder_config);
|
||||
};
|
||||
|
||||
Ok((folder_config, watch_folder))
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
|
||||
@@ -554,17 +551,14 @@ async fn fetch_idle(
|
||||
mut session: Session,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<Session> {
|
||||
let (folder_config, watch_folder) = match convert_folder_meaning(ctx, folder_meaning).await {
|
||||
Ok(meaning) => meaning,
|
||||
Err(err) => {
|
||||
// Warning instead of error because the folder may not be configured.
|
||||
// For example, this happens if the server does not have Sent folder
|
||||
// but watching Sent folder is enabled.
|
||||
warn!(ctx, "Error converting IMAP Folder name: {err:#}.");
|
||||
connection.connectivity.set_not_configured(ctx).await;
|
||||
connection.idle_interrupt_receiver.recv().await.ok();
|
||||
return Err(err);
|
||||
}
|
||||
let Some((folder_config, watch_folder)) = convert_folder_meaning(ctx, folder_meaning).await?
|
||||
else {
|
||||
// The folder is not configured.
|
||||
// For example, this happens if the server does not have Sent folder
|
||||
// but watching Sent folder is enabled.
|
||||
connection.connectivity.set_not_configured(ctx).await;
|
||||
connection.idle_interrupt_receiver.recv().await.ok();
|
||||
bail!("Cannot fetch folder {folder_meaning} because it is not configured");
|
||||
};
|
||||
|
||||
if folder_config == Config::ConfiguredInboxFolder {
|
||||
|
||||
+21
-35
@@ -1,6 +1,6 @@
|
||||
//! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/).
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Error, Result};
|
||||
use anyhow::{ensure, Context as _, Error, Result};
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
@@ -89,12 +89,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
bail!("No fingerprint, cannot generate QR code.");
|
||||
}
|
||||
};
|
||||
let fingerprint = get_self_fingerprint(context).await?;
|
||||
|
||||
let self_addr_urlencoded =
|
||||
utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||
@@ -140,14 +135,11 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
Ok(qr)
|
||||
}
|
||||
|
||||
async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
match load_self_public_key(context).await {
|
||||
Ok(key) => Some(key.fingerprint()),
|
||||
Err(_) => {
|
||||
warn!(context, "get_self_fingerprint(): failed to load key");
|
||||
None
|
||||
}
|
||||
}
|
||||
async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
|
||||
let key = load_self_public_key(context)
|
||||
.await
|
||||
.context("Failed to load key")?;
|
||||
Ok(key.fingerprint())
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
|
||||
@@ -388,7 +380,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
let fingerprint: Fingerprint = fp.parse()?;
|
||||
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
|
||||
if !encrypted_and_signed(context, mime_message, &fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring {step} message because the message is not encrypted."
|
||||
@@ -577,11 +569,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
|
||||
if !encrypted_and_signed(
|
||||
context,
|
||||
mime_message,
|
||||
get_self_fingerprint(context).await.as_ref(),
|
||||
) {
|
||||
if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
@@ -696,7 +684,10 @@ async fn could_not_establish_secure_connection(
|
||||
details: &str,
|
||||
) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
let mut msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
msg += " (";
|
||||
msg += details;
|
||||
msg += ")";
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
warn!(
|
||||
context,
|
||||
@@ -737,24 +728,19 @@ async fn mark_peer_as_verified(
|
||||
fn encrypted_and_signed(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
expected_fingerprint: Option<&Fingerprint>,
|
||||
expected_fingerprint: &Fingerprint,
|
||||
) -> bool {
|
||||
if !mimeparser.was_encrypted() {
|
||||
warn!(context, "Message not encrypted.",);
|
||||
false
|
||||
} else if let Some(expected_fingerprint) = expected_fingerprint {
|
||||
if !mimeparser.signatures.contains(expected_fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.", expected_fingerprint,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Fingerprint for comparison missing.");
|
||||
} else if !mimeparser.signatures.contains(expected_fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.", expected_fingerprint,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
BobState::start_protocol(context, invite.clone(), chat_id).await?;
|
||||
for state in aborted_states {
|
||||
error!(context, "Aborting previously unfinished QR Join process.");
|
||||
state.notify_aborted(context, "new QR scanned").await?;
|
||||
state.notify_aborted(context, "New QR code scanned").await?;
|
||||
state.emit_progress(context, JoinerProgress::Error);
|
||||
}
|
||||
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
|
||||
@@ -73,8 +73,9 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// Calculate the sort timestamp before checking the chat protection status so that if we
|
||||
// race with its change, we don't add our message below the protection message.
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
let ts_sort = chat_id
|
||||
.calc_sort_timestamp(context, 0, sort_to_bottom, false)
|
||||
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
|
||||
.await?;
|
||||
if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
|
||||
let ts_start = time();
|
||||
@@ -194,7 +195,10 @@ impl BobState {
|
||||
/// This creates an info message in the chat being joined.
|
||||
async fn notify_aborted(&self, context: &Context, why: &str) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
|
||||
let msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
let mut msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
msg += " (";
|
||||
msg += why;
|
||||
msg += ")";
|
||||
let chat_id = self.joining_chat_id(context).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
warn!(
|
||||
|
||||
@@ -270,7 +270,7 @@ impl BobState {
|
||||
context,
|
||||
"Bob Step 4 - handling {{vc,vg}}-auth-required message."
|
||||
);
|
||||
if !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) {
|
||||
if !encrypted_and_signed(context, mime_message, self.invite.fingerprint()) {
|
||||
let reason = if mime_message.was_encrypted() {
|
||||
"Valid signature missing"
|
||||
} else {
|
||||
|
||||
@@ -168,6 +168,7 @@ async fn connect_stream(
|
||||
connect_insecure_proxy(context, host, port, proxy_config.clone()).await?
|
||||
}
|
||||
};
|
||||
update_connection_history(context, "smtp", host, port, host, time()).await?;
|
||||
Ok(stream)
|
||||
} else {
|
||||
let load_cache = match security {
|
||||
|
||||
+91
-69
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row};
|
||||
use tokio::sync::{Mutex, MutexGuard, RwLock};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, add_device_msg, update_device_icon, update_saved_messages_icon};
|
||||
@@ -60,11 +60,6 @@ pub struct Sql {
|
||||
/// Database file path
|
||||
pub(crate) dbfile: PathBuf,
|
||||
|
||||
/// Write transactions mutex.
|
||||
///
|
||||
/// See [`Self::write_lock`].
|
||||
write_mtx: Mutex<()>,
|
||||
|
||||
/// SQL connection pool.
|
||||
pool: RwLock<Option<Pool>>,
|
||||
|
||||
@@ -81,7 +76,6 @@ impl Sql {
|
||||
pub fn new(dbfile: PathBuf) -> Sql {
|
||||
Self {
|
||||
dbfile,
|
||||
write_mtx: Mutex::new(()),
|
||||
pool: Default::default(),
|
||||
is_encrypted: Default::default(),
|
||||
config_cache: Default::default(),
|
||||
@@ -147,7 +141,8 @@ impl Sql {
|
||||
let mut config_cache = self.config_cache.write().await;
|
||||
config_cache.clear();
|
||||
|
||||
self.call_write(move |conn| {
|
||||
let query_only = false;
|
||||
self.call(query_only, move |conn| {
|
||||
// Check that backup passphrase is correct before resetting our database.
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
@@ -325,7 +320,8 @@ impl Sql {
|
||||
let mut lock = self.pool.write().await;
|
||||
|
||||
let pool = lock.take().context("SQL connection pool is not open")?;
|
||||
let conn = pool.get().await?;
|
||||
let query_only = false;
|
||||
let conn = pool.get(query_only).await?;
|
||||
if !passphrase.is_empty() {
|
||||
conn.pragma_update(None, "rekey", passphrase.clone())
|
||||
.context("Failed to set PRAGMA rekey")?;
|
||||
@@ -337,59 +333,20 @@ impl Sql {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locks the write transactions mutex in order to make sure that there never are
|
||||
/// multiple write transactions at once.
|
||||
/// Allocates a connection and calls `function` with the connection.
|
||||
///
|
||||
/// Doing the locking ourselves instead of relying on SQLite has these reasons:
|
||||
///
|
||||
/// - SQLite's locking mechanism is non-async, blocking a thread
|
||||
/// - SQLite's locking mechanism just sleeps in a loop, which is really inefficient
|
||||
///
|
||||
/// ---
|
||||
///
|
||||
/// More considerations on alternatives to the current approach:
|
||||
///
|
||||
/// We use [DEFERRED](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions) transactions.
|
||||
///
|
||||
/// In order to never get concurrency issues, we could make all transactions IMMEDIATE,
|
||||
/// but this would mean that there can never be two simultaneous transactions.
|
||||
///
|
||||
/// Read transactions can simply be made DEFERRED to run in parallel w/o any drawbacks.
|
||||
///
|
||||
/// DEFERRED write transactions without doing the locking ourselves would have these drawbacks:
|
||||
///
|
||||
/// 1. As mentioned above, SQLite's locking mechanism is non-async and sleeps in a loop.
|
||||
/// 2. If there are other write transactions, we block the db connection until
|
||||
/// upgraded. If some reader comes then, it has to get the next, less used connection with a
|
||||
/// worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
|
||||
/// 3. If a transaction is blocked for more than `busy_timeout`, it fails with SQLITE_BUSY.
|
||||
/// 4. If upon a successful upgrade to a write transaction the db has been modified,
|
||||
/// the transaction has to be rolled back and retried, which means extra work in terms of
|
||||
/// CPU/battery.
|
||||
///
|
||||
/// The only pro of making write transactions DEFERRED w/o the external locking would be some
|
||||
/// parallelism between them.
|
||||
///
|
||||
/// Another option would be to make write transactions IMMEDIATE, also
|
||||
/// w/o the external locking. But then cons 1. - 3. above would still be valid.
|
||||
pub async fn write_lock(&self) -> MutexGuard<'_, ()> {
|
||||
self.write_mtx.lock().await
|
||||
}
|
||||
|
||||
/// Allocates a connection and calls `function` with the connection. If `function` does write
|
||||
/// queries,
|
||||
/// - either first take a lock using `write_lock()`
|
||||
/// - or use `call_write()` instead.
|
||||
/// If `query_only` is true, allocates read-only connection,
|
||||
/// otherwise allocates write connection.
|
||||
///
|
||||
/// Returns the result of the function.
|
||||
async fn call<'a, F, R>(&'a self, function: F) -> Result<R>
|
||||
async fn call<'a, F, R>(&'a self, query_only: bool, function: F) -> Result<R>
|
||||
where
|
||||
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let lock = self.pool.read().await;
|
||||
let pool = lock.as_ref().context("no SQL connection")?;
|
||||
let mut conn = pool.get().await?;
|
||||
let mut conn = pool.get(query_only).await?;
|
||||
let res = tokio::task::block_in_place(move || function(&mut conn))?;
|
||||
Ok(res)
|
||||
}
|
||||
@@ -403,8 +360,8 @@ impl Sql {
|
||||
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let _lock = self.write_lock().await;
|
||||
self.call(function).await
|
||||
let query_only = false;
|
||||
self.call(query_only, function).await
|
||||
}
|
||||
|
||||
/// Execute `query` assuming it is a write query, returning the number of affected rows.
|
||||
@@ -444,7 +401,8 @@ impl Sql {
|
||||
G: Send + FnMut(rusqlite::MappedRows<F>) -> Result<H>,
|
||||
H: Send + 'static,
|
||||
{
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let res = stmt.query_map(params, f)?;
|
||||
g(res)
|
||||
@@ -476,7 +434,8 @@ impl Sql {
|
||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T> + Send,
|
||||
T: Send + 'static,
|
||||
{
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let res = conn.query_row(query, params, f)?;
|
||||
Ok(res)
|
||||
})
|
||||
@@ -512,7 +471,8 @@ impl Sql {
|
||||
|
||||
/// Query the database if the requested table already exists.
|
||||
pub async fn table_exists(&self, name: &str) -> Result<bool> {
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let mut exists = false;
|
||||
conn.pragma(None, "table_info", name.to_string(), |_row| {
|
||||
// will only be executed if the info was found
|
||||
@@ -527,7 +487,8 @@ impl Sql {
|
||||
|
||||
/// Check if a column exists in a given table.
|
||||
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> Result<bool> {
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let mut exists = false;
|
||||
// `PRAGMA table_info` returns one row per column,
|
||||
// each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value
|
||||
@@ -555,18 +516,19 @@ impl Sql {
|
||||
F: Send + FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
T: Send + 'static,
|
||||
{
|
||||
self.call(move |conn| match conn.query_row(sql.as_ref(), params, f) {
|
||||
Ok(res) => Ok(Some(res)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(rusqlite::Error::InvalidColumnType(_, _, rusqlite::types::Type::Null)) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
match conn.query_row(sql.as_ref(), params, f) {
|
||||
Ok(res) => Ok(Some(res)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Executes a query which is expected to return one row and one
|
||||
/// column. If the query does not return a value or returns SQL
|
||||
/// `NULL`, returns `Ok(None)`.
|
||||
/// column. If the query does not return any rows, returns `Ok(None)`.
|
||||
pub async fn query_get_value<T>(
|
||||
&self,
|
||||
query: &str,
|
||||
@@ -1094,9 +1056,10 @@ mod tests {
|
||||
async fn test_auto_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let query_only = true;
|
||||
let auto_vacuum = t
|
||||
.sql
|
||||
.call(|conn| {
|
||||
.call(query_only, |conn| {
|
||||
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
||||
let auto_vacuum: i32 = row.get(0)?;
|
||||
Ok(auto_vacuum)
|
||||
@@ -1322,8 +1285,9 @@ mod tests {
|
||||
{
|
||||
let lock = sql.pool.read().await;
|
||||
let pool = lock.as_ref().unwrap();
|
||||
let conn1 = pool.get().await?;
|
||||
let conn2 = pool.get().await?;
|
||||
let query_only = true;
|
||||
let conn1 = pool.get(query_only).await?;
|
||||
let conn2 = pool.get(query_only).await?;
|
||||
conn1
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
@@ -1348,4 +1312,62 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_query_only() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// `query_row` does not acquire write lock
|
||||
// and operates on read-only connection.
|
||||
// Using it to `INSERT` should fail.
|
||||
let res = t
|
||||
.sql
|
||||
.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1",
|
||||
("xyz", "ijk"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
// If you want to `INSERT` and get value via `RETURNING`,
|
||||
// use `call_write` or `transaction`.
|
||||
|
||||
let res: Result<u32> = t
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
let val = conn.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2",
|
||||
("foo", "bar"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 2);
|
||||
|
||||
let res = t
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
let val = t.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3",
|
||||
("abc", "def"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1037,6 +1037,39 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 122)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE tokens ADD COLUMN foreign_id INTEGER NOT NULL DEFAULT 0",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 123)?;
|
||||
if dbversion < migration_version {
|
||||
// Add FOREIGN KEY(msg_id).
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE new_msgs_status_updates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
msg_id INTEGER,
|
||||
update_item TEXT DEFAULT '',
|
||||
uid TEXT UNIQUE,
|
||||
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT OR IGNORE INTO new_msgs_status_updates SELECT
|
||||
id, msg_id, update_item, uid
|
||||
FROM msgs_status_updates;
|
||||
DROP TABLE msgs_status_updates;
|
||||
ALTER TABLE new_msgs_status_updates RENAME TO msgs_status_updates;
|
||||
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
|
||||
CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
+106
-17
@@ -7,23 +7,67 @@
|
||||
//! Each SQLite connection has its own page cache, so allocating recently used connections
|
||||
//! improves the performance compared to, for example, organizing the pool as a queue
|
||||
//! and returning the least recently used connection each time.
|
||||
//!
|
||||
//! Pool returns at most one write connection (with `PRAGMA query_only=0`).
|
||||
//! This ensures that there never are multiple write transactions at once.
|
||||
//!
|
||||
//! Doing the locking ourselves instead of relying on SQLite has these reasons:
|
||||
//!
|
||||
//! - SQLite's locking mechanism is non-async, blocking a thread
|
||||
//! - SQLite's locking mechanism just sleeps in a loop, which is really inefficient
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! More considerations on alternatives to the current approach:
|
||||
//!
|
||||
//! We use [DEFERRED](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions) transactions.
|
||||
//!
|
||||
//! In order to never get concurrency issues, we could make all transactions IMMEDIATE,
|
||||
//! but this would mean that there can never be two simultaneous transactions.
|
||||
//!
|
||||
//! Read transactions can simply be made DEFERRED to run in parallel w/o any drawbacks.
|
||||
//!
|
||||
//! DEFERRED write transactions without doing the locking ourselves would have these drawbacks:
|
||||
//!
|
||||
//! 1. As mentioned above, SQLite's locking mechanism is non-async and sleeps in a loop.
|
||||
//! 2. If there are other write transactions, we block the db connection until
|
||||
//! upgraded. If some reader comes then, it has to get the next, less used connection with a
|
||||
//! worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
|
||||
//! 3. If a transaction is blocked for more than `busy_timeout`, it fails with SQLITE_BUSY.
|
||||
//! 4. If upon a successful upgrade to a write transaction the db has been modified,
|
||||
//! the transaction has to be rolled back and retried, which means extra work in terms of
|
||||
//! CPU/battery.
|
||||
//!
|
||||
//! The only pro of making write transactions DEFERRED w/o the external locking would be some
|
||||
//! parallelism between them.
|
||||
//!
|
||||
//! Another option would be to make write transactions IMMEDIATE, also
|
||||
//! w/o the external locking. But then cons 1. - 3. above would still be valid.
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard, OwnedSemaphorePermit, Semaphore};
|
||||
|
||||
/// Inner connection pool.
|
||||
#[derive(Debug)]
|
||||
struct InnerPool {
|
||||
/// Available connections.
|
||||
connections: Mutex<Vec<Connection>>,
|
||||
connections: parking_lot::Mutex<Vec<Connection>>,
|
||||
|
||||
/// Counts the number of available connections.
|
||||
semaphore: Arc<Semaphore>,
|
||||
|
||||
/// Write mutex.
|
||||
///
|
||||
/// This mutex ensures there is at most
|
||||
/// one write connection with `query_only=0`.
|
||||
///
|
||||
/// This mutex is locked when write connection
|
||||
/// is outside the pool.
|
||||
write_mutex: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl InnerPool {
|
||||
@@ -35,6 +79,56 @@ impl InnerPool {
|
||||
connections.push(connection);
|
||||
drop(connections);
|
||||
}
|
||||
|
||||
/// Retrieves a connection from the pool.
|
||||
///
|
||||
/// Sets `query_only` pragma to the provided value
|
||||
/// to prevent accidentaly misuse of connection
|
||||
/// for writing when reading is intended.
|
||||
/// Only pass `query_only=false` if you want
|
||||
/// to use the connection for writing.
|
||||
pub async fn get(self: Arc<Self>, query_only: bool) -> Result<PooledConnection> {
|
||||
if query_only {
|
||||
let permit = self.semaphore.clone().acquire_owned().await?;
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock();
|
||||
connections
|
||||
.pop()
|
||||
.context("Got a permit when there are no connections in the pool")?
|
||||
};
|
||||
conn.pragma_update(None, "query_only", "1")?;
|
||||
let conn = PooledConnection {
|
||||
pool: Arc::downgrade(&self),
|
||||
conn: Some(conn),
|
||||
_permit: permit,
|
||||
_write_mutex_guard: None,
|
||||
};
|
||||
Ok(conn)
|
||||
} else {
|
||||
// We get write guard first to avoid taking a permit
|
||||
// and not using it, blocking a reader from getting a connection
|
||||
// while being ourselves blocked by another wrtier.
|
||||
let write_mutex_guard = Arc::clone(&self.write_mutex).lock_owned().await;
|
||||
|
||||
// We may still have to wait for a connection
|
||||
// to be returned by some reader.
|
||||
let permit = self.semaphore.clone().acquire_owned().await?;
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock();
|
||||
connections.pop().context(
|
||||
"Got a permit and write lock when there are no connections in the pool",
|
||||
)?
|
||||
};
|
||||
conn.pragma_update(None, "query_only", "0")?;
|
||||
let conn = PooledConnection {
|
||||
pool: Arc::downgrade(&self),
|
||||
conn: Some(conn),
|
||||
_permit: permit,
|
||||
_write_mutex_guard: Some(write_mutex_guard),
|
||||
};
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pooled connection.
|
||||
@@ -47,6 +141,11 @@ pub struct PooledConnection {
|
||||
|
||||
/// Semaphore permit, dropped after returning the connection to the pool.
|
||||
_permit: OwnedSemaphorePermit,
|
||||
|
||||
/// Write mutex guard.
|
||||
///
|
||||
/// `None` for read-only connections with `PRAGMA query_only=1`.
|
||||
_write_mutex_guard: Option<OwnedMutexGuard<()>>,
|
||||
}
|
||||
|
||||
impl Drop for PooledConnection {
|
||||
@@ -86,24 +185,14 @@ impl Pool {
|
||||
pub fn new(connections: Vec<Connection>) -> Self {
|
||||
let semaphore = Arc::new(Semaphore::new(connections.len()));
|
||||
let inner = Arc::new(InnerPool {
|
||||
connections: Mutex::new(connections),
|
||||
connections: parking_lot::Mutex::new(connections),
|
||||
semaphore,
|
||||
write_mutex: Default::default(),
|
||||
});
|
||||
Pool { inner }
|
||||
}
|
||||
|
||||
/// Retrieves a connection from the pool.
|
||||
pub async fn get(&self) -> Result<PooledConnection> {
|
||||
let permit = self.inner.semaphore.clone().acquire_owned().await?;
|
||||
let mut connections = self.inner.connections.lock();
|
||||
let conn = connections
|
||||
.pop()
|
||||
.context("got a permit when there are no connections in the pool")?;
|
||||
let conn = PooledConnection {
|
||||
pool: Arc::downgrade(&self.inner),
|
||||
conn: Some(conn),
|
||||
_permit: permit,
|
||||
};
|
||||
Ok(conn)
|
||||
pub async fn get(&self, query_only: bool) -> Result<PooledConnection> {
|
||||
Arc::clone(&self.inner).get(query_only).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1106,6 +1106,24 @@ impl EventTracker {
|
||||
.expect("timeout waiting for event match")
|
||||
}
|
||||
|
||||
/// Consumes emitted events returning the first matching one if any.
|
||||
pub async fn get_matching_opt<F: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
event_matcher: F,
|
||||
) -> Option<EventType> {
|
||||
ctx.emit_event(EventType::Test);
|
||||
loop {
|
||||
let event = self.recv().await.unwrap();
|
||||
if event_matcher(&event.typ) {
|
||||
return Some(event.typ);
|
||||
}
|
||||
if let EventType::Test = event.typ {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes events looking for an [`EventType::Info`] with substring matching.
|
||||
pub async fn get_info_contains(&self, s: &str) -> EventType {
|
||||
self.get_matching(|evt| match evt {
|
||||
|
||||
@@ -730,6 +730,46 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob_old = &tcm.unconfigured().await;
|
||||
enable_verified_oneonone_chats(&[alice, bob_old]).await;
|
||||
mark_as_verified(bob_old, alice).await;
|
||||
bob_old.configure_addr("bob@example.net").await;
|
||||
let chat = bob_old.create_chat(alice).await;
|
||||
let sent_old = bob_old
|
||||
.send_text(chat.id, "Soon i'll have a new device")
|
||||
.await;
|
||||
SystemTime::shift(std::time::Duration::from_secs(3600));
|
||||
|
||||
tcm.section("Bob reinstalls DC");
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[bob]).await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
|
||||
tcm.send_recv(bob, alice, "Now i have it!").await;
|
||||
assert_verified(alice, bob, ProtectionStatus::Protected).await;
|
||||
|
||||
let msg = alice.recv_msg(&sent_old).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
let contact = alice.add_or_lookup_contact(bob).await;
|
||||
// The outdated Bob's Autocrypt header isn't applied, so the verification preserves.
|
||||
assert!(contact.is_verified(alice).await.unwrap());
|
||||
let chat = alice.get_chat(bob).await;
|
||||
// But the chat protection is broken because the old message is sorted to the bottom as it
|
||||
// mustn't be sorted over the protection info message (which is `InNoticed` moreover).
|
||||
assert_eq!(chat.is_protected(), false);
|
||||
assert_eq!(chat.is_protection_broken(), true);
|
||||
alice
|
||||
.golden_test_chat(msg.chat_id, "verified_chats_message_from_old_dc_setup")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the following bug:
|
||||
///
|
||||
/// - Scan your chat partner's QR Code
|
||||
|
||||
+5
-5
@@ -17,7 +17,7 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
|
||||
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
|
||||
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 30;
|
||||
|
||||
/// Smeared timestamp generator.
|
||||
#[derive(Debug)]
|
||||
@@ -157,11 +157,11 @@ mod tests {
|
||||
// because there were not enough timestamps in the past.
|
||||
assert_eq!(smeared_timestamp.current(), now + 4);
|
||||
|
||||
// Forward another 7 messages.
|
||||
// We cannot use more than 5 timestamps from the future,
|
||||
// so we use 5 timestamps from the future,
|
||||
// Forward another 32 messages.
|
||||
// We cannot use more than 30 timestamps from the future,
|
||||
// so we use 30 timestamps from the future,
|
||||
// the current timestamp and one timestamp from the past.
|
||||
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 1);
|
||||
assert_eq!(smeared_timestamp.create_n(now, 32), now - 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+9
-12
@@ -23,13 +23,13 @@ use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
|
||||
|
||||
use async_zip::tokio::read::fs::ZipFileReader as FsZipFileReader;
|
||||
use deltachat_contact_tools::sanitize_bidi_characters;
|
||||
use deltachat_derive::FromSql;
|
||||
use lettre_email::PartBuilder;
|
||||
use rusqlite::OptionalExtension;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::constants::Chattype;
|
||||
@@ -195,7 +195,7 @@ fn find_zip_entry<'a>(
|
||||
name: &str,
|
||||
) -> Option<(usize, &'a async_zip::StoredZipEntry)> {
|
||||
for (i, ent) in file.entries().iter().enumerate() {
|
||||
if ent.entry().filename() == name {
|
||||
if ent.filename().as_bytes() == name.as_bytes() {
|
||||
return Some((i, ent));
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ impl Context {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let archive = match async_zip::read::mem::ZipFileReader::new(file.to_vec()).await {
|
||||
let archive = match async_zip::base::read::mem::ZipFileReader::new(file.to_vec()).await {
|
||||
Ok(archive) => archive,
|
||||
Err(_) => {
|
||||
info!(self, "{} cannot be opened as zip-file", &filename);
|
||||
@@ -235,7 +235,7 @@ impl Context {
|
||||
bail!("{} is not a valid webxdc file", filename);
|
||||
}
|
||||
|
||||
let valid = match async_zip::read::fs::ZipFileReader::new(path).await {
|
||||
let valid = match FsZipFileReader::new(path).await {
|
||||
Ok(archive) => {
|
||||
if find_zip_entry(archive.file(), "index.html").is_none() {
|
||||
warn!(self, "{} misses index.html", filename);
|
||||
@@ -791,27 +791,24 @@ fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
async fn get_blob(archive: &async_zip::read::fs::ZipFileReader, name: &str) -> Result<Vec<u8>> {
|
||||
async fn get_blob(archive: &FsZipFileReader, name: &str) -> Result<Vec<u8>> {
|
||||
let (i, _) = find_zip_entry(archive.file(), name)
|
||||
.ok_or_else(|| anyhow!("no entry found for {}", name))?;
|
||||
let mut reader = archive.entry(i).await?;
|
||||
let mut reader = archive.reader_with_entry(i).await?;
|
||||
let mut buf = Vec::new();
|
||||
reader.read_to_end(&mut buf).await?;
|
||||
reader.read_to_end_checked(&mut buf).await?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Get handle to a webxdc ZIP-archive.
|
||||
/// To check for file existence use archive.by_name(), to read a file, use get_blob(archive).
|
||||
async fn get_webxdc_archive(
|
||||
&self,
|
||||
context: &Context,
|
||||
) -> Result<async_zip::read::fs::ZipFileReader> {
|
||||
async fn get_webxdc_archive(&self, context: &Context) -> Result<FsZipFileReader> {
|
||||
let path = self
|
||||
.get_file(context)
|
||||
.ok_or_else(|| format_err!("No webxdc instance file."))?;
|
||||
let path_abs = get_abs_path(context, &path);
|
||||
let archive = async_zip::read::fs::ZipFileReader::new(path_abs).await?;
|
||||
let archive = FsZipFileReader::new(path_abs).await?;
|
||||
Ok(archive)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Single#Chat#10: bob@example.net [bob@example.net]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: Me (Contact#Contact#Self): We share this account √
|
||||
Msg#11: Me (Contact#Contact#Self): I'm Alice too √
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,7 @@
|
||||
Single#Chat#10: bob@example.net [bob@example.net]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Now i have it! [FRESH]
|
||||
Msg#12: info (Contact#Contact#Info): bob@example.net sent a message from another device. [NOTICED][INFO 🛡️❌]
|
||||
Msg#13: (Contact#Contact#10): Soon i'll have a new device [FRESH]
|
||||
--------------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user