Compare commits

..

19 Commits

Author SHA1 Message Date
adbenitez
b26dccd174 deltachat-rpc-client: fix bug in client.py and add missing EventType.MSG_DELETED 2023-07-23 20:07:10 +02:00
Simon Laux
3a63628f1f update node constants
looks like this was fogotten when changing the chat protection stock strings
2023-07-23 09:26:01 +00:00
link2xt
3705616cd9 Merge branch 'stable' 2023-07-23 09:17:13 +00:00
Simon Laux
b8fcb660ad cargo fmt 2023-07-23 02:29:42 +02:00
Simon Laux
5673294623 api(jsonrpc): add resend_messages 2023-07-23 02:29:42 +02:00
B. Petersen
7062bb0502 clarify transitive behaviour of dc_contact_is_verfified() 2023-07-22 20:58:05 +02:00
link2xt
659cffe0cc ci: remove comment about python from rust tests 2023-07-19 13:43:20 +00:00
link2xt
a1663a98e0 build: use Rust 1.71.0 and increase MSRV to 1.66.0
Rust 1.66 is required by constant_time_eq 0.3.0.
2023-07-19 13:41:31 +00:00
link2xt
3de1dbc9e4 chore(deps): update dependencies 2023-07-19 13:26:47 +00:00
link2xt
6d37e8601e Merge branch 'stable' 2023-07-17 17:11:38 +00:00
Hocuri
d762753103 fix: Allow to save a draft if the verification is broken (#4542)
If the verification is broken, `can_send()` is false.

But if the user was typing a message right when a verification-breaking message came in, the UI still needs to be able to save it as a draft.

Steps to reproduce the bug:
  - Set a draft
  - Your chat partner breaks verification
  - Go back to the chats list
  - Go to the chat again
  - Accept the breakage
  - Expected: The draft is still there
  - Bug behavior: The draft is gone
2023-07-16 12:04:43 +02:00
link2xt
a020d5ccce Merge branch 'stable' 2023-07-14 11:23:43 +00:00
Hocuri
1e28ea9bb0 fix: Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt (#4538) 2023-07-11 17:39:59 +00:00
Hocuri
17f2d33731 test: Remove unnecessary inner_set_protection() call (#4539)
1:1 chats are automatically created as protected if the contact is
verified, there is no need to explicitly do this.

Plus, by removing this call, the test also tests that automatically
creating 1:1 chats as protected works.
2023-07-11 19:15:23 +02:00
link2xt
976797d4cf build: remove examples/simple.rs
When `cargo test` is executed,
all examples are built by default
to ensure that they can be compiled.

This is a documented and expected behaviour,
even though it was previously reported as a bug:
<https://github.com/rust-lang/cargo/issues/6675>

In particular, `examples/simple.rs` is built into
a 67M binary `target/debug/examples/simple`.
This is unnecessary to do so every time
you change a line in the `deltachat` crate
and want to rerun the tests.

Workaround is to run `cargo test --tests`,
but it is easy to forget and is not discoverable
unless you read the "Target Selection" section of `cargo help test`.

We have a maintained example at https://github.com/deltachat-bot/echo,
so there is no need for an example in the core repository.
2023-07-10 21:49:31 +00:00
link2xt
31e3169433 chore: nightly clippy fixes 2023-07-10 11:38:46 +02:00
link2xt
d2b15cb629 docs: document how logs and error messages should be formatted 2023-07-09 16:18:18 +00:00
Hocuri
9cd000c4f2 feat: Verified 1:1 chats (#4315)
Implement #4188

BREAKING CHANGE: Remove unused DC_STR_PROTECTION_(EN)ABLED* strings
BREAKING CHANGE: Remove unused dc_set_chat_protection()
2023-07-09 14:06:45 +02:00
link2xt
243c035b03 chore: spellcheck 2023-07-07 21:56:59 +00:00
73 changed files with 1499 additions and 1384 deletions

View File

@@ -15,7 +15,6 @@ on:
push:
branches:
- master
- stable
env:
RUSTFLAGS: -Dwarnings
@@ -25,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.72.0
RUSTUP_TOOLCHAIN: 1.71.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -81,19 +80,15 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.68.2
rust: 1.71.0
- os: windows-latest
rust: 1.68.2
rust: 1.71.0
- os: macos-latest
rust: 1.68.2
rust: 1.71.0
# Minimum Supported Rust Version = 1.65.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
# Minimum Supported Rust Version = 1.66.0
- os: ubuntu-latest
rust: 1.65.0
rust: 1.66.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3

View File

@@ -1,93 +1,10 @@
# Changelog
## [1.120.0] - 2023-08-28
### API-Changes
- jsonrpc: Add `resend_messages`.
## Unreleased
### Fixes
- Update async-imap to 0.9.1 to fix memory leak.
- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
### Refactor
- Hide accounts.rs constants from public API.
- Hide pgp module from public API.
### Build system
- Update to Zig 0.11.0.
- Update to Rust 1.72.0.
### CI
- Run on push to stable branch.
### Miscellaneous Tasks
- python: Fix lint errors.
- python: Fix `ruff` 0.0.286 warnings.
- Fix beta clippy warnings.
## [1.119.1] - 2023-08-06
Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
### Features / Changes
- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
### Fixes
- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
### Tests
- webxdc: Ensure unknown WebXDC update properties do not result in an error.
## [1.119.0] - 2023-08-03
### Fixes
- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
- imap: Do not resync IMAP after initial configuration.
- webxdc: Accept WebXDC updates in mailing lists.
- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
- webxdc: Delete old webxdc status updates during housekeeping.
- Return valid MsgId from `receive_imf()` when the message is replaced.
- Emit MsgsChanged event with correct chat id for replaced messages.
- deltachat-rpc-server: Update tokio-tar to fix backup import.
### Features / Changes
- deltachat-rpc-client: Add `MSG_DELETED` constant.
- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
### API-Changes
- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
### Documentation
- Update default value for `show_emails` in `dc_set_config()` documentation.
### Refactor
- Improve IMAP logs.
### Tests
- Add basic import/export test for async python.
- Add `test_webxdc_download_on_demand`.
- Add tests for deletion of webxdc status-updates.
- deltachat-rpc-client: fix bug in client.py and add missing `EventType.MSG_DELETED`
## [1.118.0] - 2023-07-07
@@ -2761,6 +2678,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0

View File

@@ -76,6 +76,29 @@ If you have multiple changes in one PR, create multiple conventional commits, an
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
### Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
For example:
```
.with_context(|| format!("Unable to trash message {msg_id}"))
```
### Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
### Reviewing
Once a PR has an approval and passes CI, it can be merged.

333
Cargo.lock generated
View File

@@ -89,9 +89,9 @@ dependencies = [
[[package]]
name = "allocator-api2"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "android-tzdata"
@@ -131,9 +131,9 @@ checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
[[package]]
name = "anyhow"
version = "1.0.71"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
dependencies = [
"backtrace",
]
@@ -169,7 +169,7 @@ dependencies = [
"num-traits",
"rusticata-macros",
"thiserror",
"time 0.3.22",
"time 0.3.23",
]
[[package]]
@@ -197,9 +197,9 @@ dependencies = [
[[package]]
name = "async-channel"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
@@ -221,13 +221,13 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
dependencies = [
"async-channel",
"base64 0.21.2",
"bytes",
"byte-pool",
"chrono",
"futures",
"imap-proto",
@@ -281,13 +281,13 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.70"
version = "0.1.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79fa67157abdfd688a259b6648808757db9347af834624f27ec646da976aee5d"
checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -312,9 +312,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.18"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
checksum = "a6a1de45611fdb535bfde7b7de4fd54f4fd2b17b1737c0a59b69bf9b92074b8c"
dependencies = [
"async-trait",
"axum-core",
@@ -448,9 +448,9 @@ checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "blake3"
version = "1.4.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888"
checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5"
dependencies = [
"arrayref",
"arrayvec",
@@ -520,9 +520,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
@@ -544,6 +544,16 @@ version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "byte-pool"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
dependencies = [
"crossbeam-queue",
"stable_deref_trait",
]
[[package]]
name = "bytemuck"
version = "1.13.1"
@@ -574,18 +584,18 @@ dependencies = [
[[package]]
name = "camino"
version = "1.1.4"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2"
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27"
checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479"
dependencies = [
"serde",
]
@@ -703,18 +713,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.3.10"
version = "4.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a"
checksum = "74bb1b4028935821b2d6b439bba2e970bdcf740832732437ead910c632e30d7d"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.3.10"
version = "4.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d"
checksum = "5ae467cbb0111869b765e13882a1dbbd6cb52f58203d8b80c44f667d4dd19843"
dependencies = [
"anstyle",
"clap_lex",
@@ -760,9 +770,9 @@ dependencies = [
[[package]]
name = "const-oid"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc"
checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747"
[[package]]
name = "const_format"
@@ -786,9 +796,9 @@ dependencies = [
[[package]]
name = "constant_time_eq"
version = "0.2.6"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "convert_case"
@@ -820,9 +830,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
@@ -914,6 +924,16 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
@@ -957,16 +977,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctor"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "curve25519-dalek"
version = "3.2.0"
@@ -1005,7 +1015,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1103,7 +1113,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.120.0"
version = "1.118.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1179,7 +1189,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.120.0"
version = "1.118.0"
dependencies = [
"anyhow",
"async-channel",
@@ -1203,7 +1213,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.120.0"
version = "1.118.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1218,7 +1228,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.120.0"
version = "1.118.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1238,12 +1248,12 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
name = "deltachat_ffi"
version = "1.120.0"
version = "1.118.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1438,7 +1448,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1466,9 +1476,9 @@ dependencies = [
[[package]]
name = "dyn-clone"
version = "1.0.11"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
[[package]]
name = "ecdsa"
@@ -1597,7 +1607,7 @@ dependencies = [
"pem-rfc7468 0.7.0",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"sec1 0.7.2",
"sec1 0.7.3",
"subtle",
"zeroize",
]
@@ -1738,7 +1748,7 @@ dependencies = [
"num-traits",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1756,9 +1766,9 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
@@ -1856,7 +1866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
"rustix 0.38.2",
"rustix 0.38.4",
"windows-sys 0.48.0",
]
@@ -2035,7 +2045,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -2547,12 +2557,12 @@ dependencies = [
[[package]]
name = "is-terminal"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix 0.38.2",
"rustix 0.38.4",
"windows-sys 0.48.0",
]
@@ -2567,9 +2577,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jpeg-decoder"
@@ -2985,7 +2995,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -3099,7 +3109,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -3147,15 +3157,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "output_vt100"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
dependencies = [
"winapi",
]
[[package]]
name = "overload"
version = "0.1.1"
@@ -3239,9 +3240,9 @@ dependencies = [
[[package]]
name = "paste"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pem"
@@ -3344,7 +3345,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -3457,15 +3458,15 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.3.3"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794"
checksum = "edc55135a600d700580e406b4de0d59cb9ad25e344a3a091a97ded2622ec4ec6"
[[package]]
name = "postcard"
version = "1.0.4"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00"
checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb"
dependencies = [
"cobs",
"const_format",
@@ -3492,13 +3493,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "pretty_assertions"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"ctor",
"diff",
"output_vt100",
"yansi",
]
@@ -3547,9 +3546,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.63"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
@@ -3660,9 +3659,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.29"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
dependencies = [
"proc-macro2",
]
@@ -3797,7 +3796,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b"
dependencies = [
"pem",
"ring",
"time 0.3.22",
"time 0.3.23",
"yasna",
]
@@ -3838,7 +3837,7 @@ checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.2",
"regex-syntax 0.7.4",
]
[[package]]
@@ -3858,9 +3857,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "reqwest"
@@ -4048,9 +4047,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.37.22"
version = "0.37.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8818fa822adcc98b18fedbb3632a6a33213c070556b5aa7c4c8cc21cff565c4c"
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
dependencies = [
"bitflags 1.3.2",
"errno",
@@ -4062,9 +4061,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.2"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4"
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
dependencies = [
"bitflags 2.3.3",
"errno",
@@ -4107,9 +4106,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline"
@@ -4136,9 +4135,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "safemem"
@@ -4200,9 +4199,9 @@ dependencies = [
[[package]]
name = "scopeguard"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
@@ -4230,9 +4229,9 @@ dependencies = [
[[package]]
name = "sec1"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct 0.2.0",
"der 0.7.7",
@@ -4273,18 +4272,18 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
[[package]]
name = "semver"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.166"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
dependencies = [
"serde_derive",
]
@@ -4300,22 +4299,22 @@ dependencies = [
[[package]]
name = "serde_bytes"
version = "0.11.10"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c5113243e4a3a1c96587342d067f3e6b0f50790b6cf40d2868eb647a3eef0e"
checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.166"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -4331,9 +4330,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.100"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
dependencies = [
"itoa",
"ryu",
@@ -4342,9 +4341,9 @@ dependencies = [
[[package]]
name = "serde_path_to_error"
version = "0.1.12"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1b6471d7496b051e03f1958802a73f88b947866f5146f329e47e36554f4e55"
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
dependencies = [
"itoa",
"serde",
@@ -4482,9 +4481,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "smawk"
@@ -4576,6 +4575,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -4622,7 +4627,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -4644,9 +4649,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.23"
version = "2.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
dependencies = [
"proc-macro2",
"quote",
@@ -4708,9 +4713,9 @@ dependencies = [
[[package]]
name = "tagger"
version = "4.3.4"
version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63"
checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5"
[[package]]
name = "tempfile"
@@ -4722,7 +4727,7 @@ dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"rustix 0.37.22",
"rustix 0.37.23",
"windows-sys 0.48.0",
]
@@ -4762,22 +4767,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.41"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.41"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -4803,9 +4808,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
dependencies = [
"itoa",
"serde",
@@ -4821,9 +4826,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.9"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
dependencies = [
"time-core",
]
@@ -4891,7 +4896,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -4947,9 +4952,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c"
dependencies = [
"futures-util",
"log",
@@ -4973,9 +4978,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.5"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
dependencies = [
"serde",
"serde_spanned",
@@ -4994,9 +4999,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.11"
version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap 2.0.0",
"serde",
@@ -5054,7 +5059,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -5159,13 +5164,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67"
dependencies = [
"base64 0.13.1",
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
@@ -5193,9 +5198,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typescript-type-def"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e6b74ffbd5684d318252bb7182051df8c4ecc098b542f63fddf792e7f42aa02"
checksum = "3c4be05eb0171e18da471e545cfff85d80b8118e5aa2d50c2d152265d7435997"
dependencies = [
"serde_json",
"typescript-type-def-derive",
@@ -5203,9 +5208,9 @@ dependencies = [
[[package]]
name = "typescript-type-def-derive"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b10a4f5dd87c279f90beef31edb7055bfd1ceb66e73148de107a5c9005e9f864"
checksum = "71aeb2295da6c2bfe665493332b4e4281d113fab3a0786e9651dcc85708095a4"
dependencies = [
"darling 0.13.4",
"ident_case",
@@ -5229,9 +5234,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unicode-linebreak"
@@ -5301,9 +5306,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom 0.2.10",
"serde",
@@ -5391,7 +5396,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
"wasm-bindgen-shared",
]
@@ -5425,7 +5430,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -5695,9 +5700,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.7"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
dependencies = [
"memchr",
]
@@ -5747,14 +5752,14 @@ dependencies = [
"oid-registry",
"rusticata-macros",
"thiserror",
"time 0.3.22",
"time 0.3.23",
]
[[package]]
name = "xattr"
version = "1.0.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985"
checksum = "ea263437ca03c1522846a4ddafbca2542d0ad5ed9b784909d4b27b76f62bc34a"
dependencies = [
"libc",
]
@@ -5771,7 +5776,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time 0.3.22",
"time 0.3.23",
]
[[package]]
@@ -5827,5 +5832,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.120.0"
version = "1.118.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
rust-version = "1.66"
[profile.dev]
debug = 0
@@ -36,7 +36,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "1.8.0"
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.9.0", default-features = false, features = ["runtime-tokio"] }
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"] }
@@ -118,11 +118,6 @@ members = [
"format-flowed",
]
[[example]]
name = "simple"
path = "examples/simple.rs"
[[bench]]
name = "create_account"
harness = false

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.120.0"
version = "1.118.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -485,6 +485,13 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
* and when the key changes, an info message is posted into the chat.
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `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`.
@@ -1470,24 +1477,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
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);
/**
* Enable or disable protection against active attacks.
* To enable protection, it is needed that all members are verified;
* if this condition is met, end-to-end-encryption is always enabled
* and only the verified keys are used.
*
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to change the protection for.
* @param protect 1=protect chat, 0=unprotect chat
* @return 1=success, 0=error, e.g. some members may be unverified
*/
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -3712,7 +3701,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
* Check if a chat is protected.
* Protected chats contain only verified members and encryption is always enabled.
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
* The status can be changed using dc_set_chat_protection().
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3721,6 +3709,26 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
* otherwise it will return false for all chats.
*
* 1:1 chats are automatically set as protected when a contact is verified.
* When a message comes in that is not encrypted / signed correctly,
* the chat is automatically set as unprotected again.
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
*/
int dc_chat_is_protection_broken (const dc_chat_t* chat);
/**
* Check if locations are sent to the chat
* at the time the object was created using dc_get_chat().
@@ -3978,17 +3986,16 @@ char* dc_msg_get_text (const dc_msg_t* msg);
*/
char* dc_msg_get_subject (const dc_msg_t* msg);
/**
* Find out full path of the file associated with a message.
* Find out full path, file name and extension of the file associated with a
* message.
*
* Typically files are associated with images, videos, audios, documents.
* Plain text messages do not have a file.
* File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The full path (with file name and extension) of the file associated with the message.
* @return The full path, the file name, and the extension of the file associated with the message.
* If there is no file associated with the message, an empty string is returned.
* NULL is never returned and the returned value must be released using dc_str_unref().
*/
@@ -3996,13 +4003,14 @@ char* dc_msg_get_file (const dc_msg_t* msg);
/**
* Get an original attachment filename, with extension but without the path. To get the full path,
* use dc_msg_get_file().
* Get a base file name without the path. The base file name includes the extension; the path
* is not returned. To get the full path, use dc_msg_get_file().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The attachment filename. If there is no file associated with the message, an empty string
* is returned. The returned value must be released using dc_str_unref().
* @return The base file name plus the extension without part. If there is no file
* associated with the message, an empty string is returned. The returned
* value must be released using dc_str_unref().
*/
char* dc_msg_get_filename (const dc_msg_t* msg);
@@ -4315,7 +4323,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
* created due to other actions,
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -5009,7 +5017,12 @@ int dc_contact_is_verified (dc_contact_t* contact);
/**
* Return the address that verified a contact
*
* The UI may use this in addition to a checkmark showing the verification status
* The UI may use this in addition to a checkmark showing the verification status.
* In case of verification chains,
* the last contact in the chain is shown.
* This is because of privacy reasons, but also as it would not help the user
* to see a unknown name here - where one can mostly always ask the shown name
* as it is directly known.
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -6749,15 +6762,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in error strings.
#define DC_STR_ERROR_NO_NETWORK 87
/// "Chat protection enabled."
///
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
#define DC_STR_PROTECTION_ENABLED 88
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
#define DC_STR_PROTECTION_DISABLED 89
/// "Reply"
///
/// Used in summaries.
@@ -7202,26 +7206,6 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You enabled chat protection."
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
/// "Chat protection enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
/// "You disabled chat protection."
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
/// "Chat protection disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
@@ -7232,6 +7216,16 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/// "Messages are guaranteed to be end-to-end encrypted from now on."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
/// "%1$s sent a message from another device."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/**
* @}
*/

View File

@@ -1429,32 +1429,6 @@ pub unsafe extern "C" fn dc_get_next_media(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_protection(
context: *mut dc_context_t,
chat_id: u32,
protect: libc::c_int,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_protection()");
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
return 0;
};
block_on(async move {
match ChatId::new(chat_id).set_protection(ctx, protect).await {
Ok(()) => 1,
Err(_) => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -3084,6 +3058,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
ffi_chat.chat.is_protected() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protection_broken() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.120.0"
version = "1.118.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -34,7 +34,7 @@ pub struct MessageObject {
quote: Option<MessageQuote>,
parent_id: Option<u32>,
text: String,
text: Option<String>,
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
@@ -180,7 +180,7 @@ impl MessageObject {
from_id: message.get_from_id().to_u32(),
quote,
parent_id,
text: message.get_text(),
text: Some(message.get_text()).filter(|s| !s.is_empty()),
has_location: message.has_location(),
has_html: message.has_html(),
view_type: message.get_viewtype().into(),

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.120.0"
"version": "1.118.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.120.0"
version = "1.118.0"
license = "MPL-2.0"
edition = "2021"

View File

@@ -18,6 +18,7 @@ use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
@@ -395,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
@@ -1056,20 +1065,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
};
chat::set_muted(&context, chat_id, duration).await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);

View File

@@ -155,7 +155,7 @@ class Client:
async def _on_new_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(command="", payload="", message_snapshot=snapshot)
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
if not snapshot.is_info and (snapshot.text or "").startswith(COMMAND_PREFIX):
await self._parse_command(event)
await self._on_event(event, NewMessage)

View File

@@ -101,16 +101,6 @@ async def test_account(acfactory) -> None:
assert await alice.get_fresh_messages()
assert await alice.get_next_messages()
# Test sending empty message.
assert len(await bob.wait_next_messages()) == 0
await alice_chat_bob.send_text("")
messages = await bob.wait_next_messages()
assert len(messages) == 1
message = messages[0]
snapshot = await message.get_snapshot()
assert snapshot.text == ""
await bob.mark_seen_messages([message])
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
group_msg = await group.send_message(text="hello")

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.120.0"
version = "1.118.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -2,12 +2,6 @@
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Exponential CPU time usage for TLS certificate processing in webpki.
# It is only used for backup transfer, so does not affect IMAP and SMTP connections.
# Waiting for `iroh` to update dependencies.
"RUSTSEC-2023-0052",
]
[bans]

View File

@@ -1,100 +0,0 @@
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::message::Message;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use tempfile::tempdir;
fn cb(event: EventType) {
match event {
EventType::ConfigureProgress { progress, .. } => {
log::info!("progress: {}", progress);
}
EventType::Info(msg) => {
log::info!("{}", msg);
}
EventType::Warning(msg) => {
log::warn!("{}", msg);
}
EventType::Error(msg) => {
log::error!("{}", msg);
}
event => {
log::info!("{:?}", event);
}
}
}
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
#[tokio::main]
async fn main() {
pretty_env_logger::try_init_timed().ok();
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
log::info!("creating database {:?}", dbfile);
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
log::info!("info: {:#?}", info);
let events = ctx.get_event_emitter();
let events_spawn = tokio::task::spawn(async move {
while let Some(event) = events.recv().await {
cb(event.typ);
}
});
log::info!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 3, "requires email password");
let email = args[1].clone();
let pw = args[2].clone();
ctx.set_config(config::Config::Addr, Some(&email))
.await
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw))
.await
.unwrap();
ctx.configure().await.unwrap();
log::info!("------ RUN ------");
ctx.start_io().await;
log::info!("--- SENDING A MESSAGE ---");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
.await
.unwrap();
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
for i in 0..1 {
log::info!("sending message {}", i);
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
.await
.unwrap();
}
// wait for the message to be sent out
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
log::info!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
.await
.unwrap();
log::info!("[{}] msg: {:?}", i, msg);
}
log::info!("stopping");
ctx.stop_io().await;
log::info!("closing");
drop(ctx);
events_spawn.await.unwrap();
}

View File

@@ -158,6 +158,8 @@ module.exports = {
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,
DC_STR_CANTDECRYPT_MSG_BODY: 29,
DC_STR_CHAT_PROTECTION_DISABLED: 171,
DC_STR_CHAT_PROTECTION_ENABLED: 170,
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
@@ -243,12 +245,6 @@ module.exports = {
DC_STR_OUTGOING_MESSAGES: 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_PROTECTION_DISABLED: 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
DC_STR_PROTECTION_ENABLED: 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,

View File

@@ -158,6 +158,8 @@ export enum C {
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CHAT_PROTECTION_DISABLED = 171,
DC_STR_CHAT_PROTECTION_ENABLED = 170,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
@@ -243,12 +245,6 @@ export enum C {
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,

View File

@@ -699,23 +699,6 @@ export class Context extends EventEmitter {
)
}
/**
*
* @param chatId
* @param protect
* @returns success boolean
*/
setChatProtection(chatId: number, protect: boolean) {
debug(`setChatProtection ${chatId} ${protect}`)
return Boolean(
binding.dcn_set_chat_protection(
this.dcn_context,
Number(chatId),
protect ? 1 : 0
)
)
}
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(

View File

@@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) {
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_set_chat_protection) {
NAPI_ARGV(3);
NAPI_DCN_CONTEXT();
NAPI_ARGV_UINT32(chat_id, 1);
NAPI_ARGV_INT32(protect, 1);
int result = dc_set_chat_protection(dcn_context->dc_context,
chat_id,
protect);
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
NAPI_ARGV(2);
NAPI_DCN_CONTEXT();
@@ -3491,7 +3479,6 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_msg);
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);

View File

@@ -446,8 +446,7 @@ describe('Offline Tests with unconfigured account', function () {
context.setChatProfileImage(chatId, imagePath)
const blobPath = context.getChat(chatId).getProfileImage()
expect(blobPath.startsWith(blobs)).to.be.true
expect(blobPath.includes('image')).to.be.true
expect(blobPath.endsWith('.jpeg')).to.be.true
expect(blobPath.endsWith(image)).to.be.true
context.setChatProfileImage(chatId, null)
expect(context.getChat(chatId).getProfileImage()).to.be.equal(

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.120.0"
"version": "1.118.0"
}

View File

@@ -427,7 +427,7 @@ class Account:
assert dc_chatlist != ffi.NULL
chatlist = []
for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
chatlist.append(Chat(self, chat_id))
return chatlist

View File

@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
for i in range(lib.dc_array_get_cnt(dc_array_t)):
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
yield constructor(lib.dc_array_get_id(dc_array_t, i))

View File

@@ -510,7 +510,8 @@ def get_viewtype_code_from_name(view_type_name):
if code is not None:
return code
raise ValueError(
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
"message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
)

View File

@@ -15,6 +15,6 @@ class TestEmpty:
def test_prepare_setup_measurings(self, acfactory):
acfactory.get_online_accounts(BENCH_NUM)
@pytest.mark.parametrize("num", range(BENCH_NUM + 1))
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
def test_setup_online_accounts(self, acfactory, num):
acfactory.get_online_accounts(num)

View File

@@ -162,9 +162,8 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "somedäüta"
ext = ".html.zip"
p = tmp_path / (basename + ext)
basename = "somedäüta.html.zip"
p = tmp_path / basename
p.write_text("some data")
def send_and_receive_message():
@@ -182,14 +181,12 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
msg = send_and_receive_message()
assert msg.text == "withfile"
assert open(msg.filename).read() == "some data"
msg.filename.index(basename)
assert msg.filename.endswith(ext)
assert msg.filename.endswith(basename)
msg2 = send_and_receive_message()
assert msg2.text == "withfile"
assert open(msg2.filename).read() == "some data"
msg2.filename.index(basename)
assert msg2.filename.endswith(ext)
assert msg2.filename.endswith("html.zip")
assert msg.filename != msg2.filename
@@ -197,11 +194,10 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "test"
ext = ".html"
basename = "test.html"
content = "<html><body>text</body>data"
p = tmp_path / (basename + ext)
p = tmp_path / basename
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
p.write_text(content)
@@ -215,8 +211,7 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
msg = ac2.get_message_by_id(ev.data2)
assert open(msg.filename).read() == content
msg.filename.index(basename)
assert msg.filename.endswith(ext)
assert msg.filename.endswith(basename)
def test_html_message(acfactory, lp):
@@ -329,27 +324,6 @@ def test_webxdc_message(acfactory, data, lp):
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_webxdc_huge_update(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
msg1 = Message.new_empty(ac1, "webxdc")
msg1.set_text("message1")
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
msg1 = chat.send_msg(msg1)
assert msg1.is_webxdc()
assert msg1.filename
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.is_webxdc()
payload = "A" * 1000
assert msg1.send_status_update({"payload": payload}, "some test data")
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
update = msg2.get_status_updates()[0]
assert update["payload"] == payload
def test_webxdc_download_on_demand(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.introduce_each_other([ac1, ac2])
@@ -376,11 +350,6 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
assert msg2.get_status_updates()
# Get a event notifying that the message disappeared from the chat.
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert msgs_changed_event.data1 == msg2.chat.id
assert msgs_changed_event.data2 == 0
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")

View File

@@ -49,9 +49,10 @@ class TestOnlineInCreation:
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.write_text("hello there\n")
msg = chat.send_file(str(src))
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
assert msg.filename.endswith(".txt")
chat.send_file(str(src))
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)

View File

@@ -1 +1 @@
2023-08-28
2023-07-07

View File

@@ -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.72.0
RUST_VERSION=1.68.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -9,9 +9,9 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
export RUSTUP_TOOLCHAIN=1.71.0
ZIG_VERSION=0.11.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"

View File

@@ -8,9 +8,9 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
export RUSTUP_TOOLCHAIN=1.71.0
ZIG_VERSION=0.11.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"

View File

@@ -296,10 +296,10 @@ impl Accounts {
}
/// Configuration file name.
const CONFIG_NAME: &str = "accounts.toml";
pub const CONFIG_NAME: &str = "accounts.toml";
/// Database file name.
const DB_NAME: &str = "dc.db";
pub const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone, PartialEq)]

View File

@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use futures::StreamExt;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
@@ -323,35 +323,18 @@ impl<'a> BlobObject<'a> {
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
};
let maybe_sticker = &mut false;
let strict_limits = true;
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
if let Some(new_name) = self.recode_to_size(
context,
blob_abs,
maybe_sticker,
img_wh,
20_000,
strict_limits,
)? {
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
{
self.name = new_name;
}
Ok(())
}
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
/// height and file size specified by the config.
///
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
/// reset.
pub async fn recode_to_image_size(
&mut self,
context: &Context,
maybe_sticker: &mut bool,
) -> Result<()> {
pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> {
let blob_abs = self.to_abs_path();
let (img_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
@@ -364,14 +347,9 @@ impl<'a> BlobObject<'a> {
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
};
let strict_limits = false;
if let Some(new_name) = self.recode_to_size(
context,
blob_abs,
maybe_sticker,
img_wh,
max_bytes,
strict_limits,
)? {
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
{
self.name = new_name;
}
Ok(())
@@ -380,37 +358,20 @@ impl<'a> BlobObject<'a> {
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
/// proceed with the result.
fn recode_to_size(
&mut self,
&self,
context: &Context,
mut blob_abs: PathBuf,
maybe_sticker: &mut bool,
mut img_wh: u32,
max_bytes: usize,
strict_limits: bool,
) -> Result<Option<String>> {
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
let (nr_bytes, exif) = self.metadata()?;
*no_exif_ref = exif.is_none();
tokio::task::block_in_place(move || {
let mut img = image::open(&blob_abs).context("image decode failure")?;
let (nr_bytes, exif) = self.metadata()?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
let mut changed_name = None;
if *maybe_sticker {
let x_max = img.width().saturating_sub(1);
let y_max = img.height().saturating_sub(1);
*maybe_sticker = img.in_bounds(x_max, y_max)
&& (img.get_pixel(0, 0).0[3] == 0
|| img.get_pixel(x_max, 0).0[3] == 0
|| img.get_pixel(0, y_max).0[3] == 0
|| img.get_pixel(x_max, y_max).0[3] == 0);
}
if *maybe_sticker && exif.is_none() {
return Ok(None);
}
img = match orientation {
Some(90) => img.rotate90(),
Some(180) => img.rotate180(),
@@ -508,21 +469,7 @@ impl<'a> BlobObject<'a> {
}
Ok(changed_name)
});
match res {
Ok(_) => res,
Err(err) => {
if !strict_limits && no_exif {
warn!(
context,
"Cannot recode image, using original data: {err:#}.",
);
Ok(None)
} else {
Err(err)
}
}
}
})
}
/// Returns image file size and Exif.
@@ -913,18 +860,10 @@ mod tests {
file.metadata().await.unwrap().len()
}
let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
let maybe_sticker = &mut false;
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
let strict_limits = true;
blob.recode_to_size(
&t,
blob.to_abs_path(),
maybe_sticker,
1000,
3000,
strict_limits,
)
.unwrap();
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
tokio::task::block_in_place(move || {
@@ -984,7 +923,6 @@ mod tests {
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"jpg",
@@ -998,7 +936,6 @@ mod tests {
.await
.unwrap();
send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
bytes,
"jpg",
@@ -1018,7 +955,6 @@ mod tests {
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"jpg",
@@ -1038,7 +974,6 @@ mod tests {
let bytes = buf.into_inner();
let img_rotated = send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
&bytes,
"jpg",
@@ -1059,7 +994,6 @@ mod tests {
let bytes = include_bytes!("../test-data/image/screenshot.png");
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"png",
@@ -1074,7 +1008,6 @@ mod tests {
.unwrap();
send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
bytes,
"png",
@@ -1087,29 +1020,12 @@ mod tests {
)
.await
.unwrap();
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
send_image_check_mediaquality(
Viewtype::Sticker,
Some("0"),
bytes,
"png",
false, // no Exif
1920,
1080,
0,
1920,
1080,
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_huge_jpg() {
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"jpg",
@@ -1143,7 +1059,6 @@ mod tests {
#[allow(clippy::too_many_arguments)]
async fn send_image_check_mediaquality(
viewtype: Viewtype,
media_quality_config: Option<&str>,
bytes: &[u8],
extension: &str,
@@ -1175,7 +1090,7 @@ mod tests {
assert!(exif.is_none());
}
let mut msg = Message::new(viewtype);
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
@@ -1189,7 +1104,6 @@ mod tests {
);
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file = bob_msg.get_file(&bob).unwrap();

View File

@@ -86,6 +86,14 @@ pub enum ProtectionStatus {
///
/// All members of the chat must be verified.
Protected = 1,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
/// The user has to confirm that this is OK.
///
/// We only do this in 1:1 chats; in group chats, the chat just
/// stays protected.
ProtectionBroken = 3, // `2` was never used as a value.
}
/// The reason why messages cannot be sent to the chat.
@@ -102,6 +110,10 @@ pub(crate) enum CantSendReason {
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
ProtectionBroken,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
@@ -118,6 +130,10 @@ impl fmt::Display for CantSendReason {
f,
"contact request chat should be accepted before sending messages"
),
Self::ProtectionBroken => write!(
f,
"accept that the encryption isn't verified anymore before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
@@ -270,6 +286,7 @@ impl ChatId {
param: Option<String>,
) -> Result<Self> {
let grpname = strip_rtlo_characters(grpname);
let smeared_time = create_smeared_timestamp(context);
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
@@ -278,13 +295,20 @@ impl ChatId {
&grpname,
grpid,
create_blocked,
create_smeared_timestamp(context),
smeared_time,
create_protected,
param.unwrap_or_default(),
),
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
if create_protected == ProtectionStatus::Protected {
chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time)
.await?;
}
info!(
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
@@ -374,6 +398,13 @@ impl ChatId {
match chat.typ {
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
Chattype::Single if chat.protected == ProtectionStatus::ProtectionBroken => {
// The chat was in the 'Request' state because the protection was broken.
// The user clicked 'Accept', so, now we want to set the status to Unprotected again:
chat.id
.inner_set_protection(context, ProtectionStatus::Unprotected)
.await?;
}
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
// User has "created a chat" with all these contacts.
//
@@ -400,20 +431,19 @@ impl ChatId {
/// Sets protection without sending a message.
///
/// Used when a message arrives indicating that someone else has
/// changed the protection value for a chat.
/// Returns whether the protection status was actually modified.
pub(crate) async fn inner_set_protection(
self,
context: &Context,
protect: ProtectionStatus,
) -> Result<()> {
ensure!(!self.is_special(), "Invalid chat-id.");
) -> Result<bool> {
ensure!(!self.is_special(), "Invalid chat-id {self}.");
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!(context, "Protection status unchanged for {}.", self);
return Ok(());
return Ok(false);
}
match protect {
@@ -430,7 +460,7 @@ impl ChatId {
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
Chattype::Undefined => bail!("Undefined group type"),
},
ProtectionStatus::Unprotected => {}
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
};
context
@@ -443,68 +473,58 @@ impl ChatId {
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
Ok(())
Ok(true)
}
/// Send protected status message to the chat.
/// Adds an info message to the chat, telling the user that the protection status changed.
///
/// This sends the message with the protected status change to the chat,
/// notifying the user on this device as well as the other users in the chat.
/// Params:
///
/// If `promote` is false this means, the message must not be sent out
/// and only a local info message should be added to the chat.
/// This is used when protection is enabled implicitly or when a chat is not yet promoted.
/// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
/// * `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn add_protection_msg(
self,
context: &Context,
protect: ProtectionStatus,
promote: bool,
from_id: ContactId,
contact_id: Option<ContactId>,
timestamp_sort: i64,
) -> Result<()> {
let text = context.stock_protection_msg(protect, from_id).await;
let text = context.stock_protection_msg(protect, contact_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
if promote {
let mut msg = Message {
viewtype: Viewtype::Text,
text,
..Default::default()
};
msg.param.set_cmd(cmd);
send_msg(context, self, &mut msg).await?;
} else {
add_info_msg_with_cmd(
context,
self,
&text,
cmd,
create_smeared_timestamp(context),
None,
None,
None,
)
.await?;
}
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
Ok(())
}
/// Sets protection and sends or adds a message.
pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> {
ensure!(!self.is_special(), "set protection: invalid chat-id.");
let chat = Chat::load_from_db(context, self).await?;
if let Err(e) = self.inner_set_protection(context, protect).await {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
return Err(e);
///
/// `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn set_protection(
self,
context: &Context,
protect: ProtectionStatus,
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?;
}
Ok(())
}
Err(e) => {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
Err(e)
}
}
self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF)
.await
}
/// Archives or unarchives a chat.
@@ -741,14 +761,6 @@ impl ChatId {
}
}
let chat = Chat::load_from_db(context, self).await?;
if let Some(cant_send_reason) = chat.why_cant_send(context).await? {
bail!(
"Can't set a draft because chat is not writeable: {}",
cant_send_reason
);
}
// set back draft information to allow identifying the draft later on -
// no matter if message object is reused or reloaded from db
msg.state = MessageState::OutDraft;
@@ -1141,7 +1153,7 @@ pub struct Chat {
pub grpid: String,
/// Whether the chat is blocked, unblocked or a contact request.
pub(crate) blocked: Blocked,
pub blocked: Blocked,
/// Additional chat parameters stored in the database.
pub param: Params,
@@ -1153,7 +1165,7 @@ pub struct Chat {
pub mute_duration: MuteDuration,
/// If the chat is protected (verified).
protected: ProtectionStatus,
pub(crate) protected: ProtectionStatus,
}
impl Chat {
@@ -1247,6 +1259,8 @@ impl Chat {
Some(DeviceChat)
} else if self.is_contact_request() {
Some(ContactRequest)
} else if self.is_protection_broken() {
Some(ProtectionBroken)
} else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
@@ -1410,6 +1424,27 @@ impl Chat {
self.protected == ProtectionStatus::Protected
}
/// Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
/// otherwise it will return false for all chats.
///
/// 1:1 chats are automatically set as protected when a contact is verified.
/// When a message comes in that is not encrypted / signed correctly,
/// the chat is automatically set as unprotected again.
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
///
/// The UI should let the user confirm that this is OK with a message like
/// `Bob sent a message from another device. Tap to learn more`
/// and then call `chat_id.accept()`.
pub fn is_protection_broken(&self) -> bool {
match self.protected {
ProtectionStatus::Protected => false,
ProtectionStatus::Unprotected => false,
ProtectionStatus::ProtectionBroken => true,
}
}
/// Returns true if location streaming is enabled in the chat.
pub fn is_sending_locations(&self) -> bool {
self.is_sending_locations
@@ -1440,15 +1475,6 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if let Some(reason) = self.why_cant_send(context).await? {
if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot send message; self not in group.".into(),
));
}
bail!("Cannot send message to {}: {}", self.id, reason);
}
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
@@ -1964,19 +1990,30 @@ impl ChatIdBlocked {
_ => (),
}
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let protected = peerstate.map_or(false, |p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
});
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
.sql
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(type, name, param, blocked, created_timestamp, protected)
VALUES(?, ?, ?, ?, ?, ?)",
(
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
create_smeared_timestamp(context),
smeared_time,
if protected {
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
},
),
)?;
let chat_id = ChatId::new(
@@ -1997,6 +2034,17 @@ impl ChatIdBlocked {
})
.await?;
if protected {
chat_id
.add_protection_msg(
context,
ProtectionStatus::Protected,
Some(contact_id),
smeared_time,
)
.await?;
}
match contact_id {
ContactId::SELF => update_saved_messages_icon(context).await?,
ContactId::DEVICE => update_device_icon(context).await?,
@@ -2033,23 +2081,15 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.await?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
if msg.viewtype == Viewtype::Image || maybe_sticker {
blob.recode_to_image_size(context, &mut maybe_sticker)
.await?;
if !maybe_sticker {
msg.viewtype = Viewtype::Image;
if msg.viewtype == Viewtype::Image {
if let Err(err) = blob.recode_to_image_size(context).await {
warn!(
context,
"Cannot recode image, using original data: {err:#}."
);
}
}
msg.param.set(Param::File, blob.as_name());
if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
let stem = match filename.rsplit_once('.') {
Some((stem, _)) => stem,
None => filename,
};
msg.param
.set(Param::Filename, stem.to_string() + "." + blob_ext);
}
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
@@ -2108,7 +2148,13 @@ async fn prepare_msg_common(
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
if reason == CantSendReason::ProtectionBroken
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification
} else {
bail!("cannot send to {chat_id}: {reason}");
}
}
// check current MessageState for drafts (to keep msg_id) ...
@@ -2210,7 +2256,7 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// protect all system messages againts RTLO attacks
// protect all system messages against RTLO attacks
if msg.is_system_message() {
msg.text = strip_rtlo_characters(&msg.text);
}
@@ -2858,18 +2904,14 @@ pub async fn create_group_chat(
let grpid = create_id();
let timestamp = create_smeared_timestamp(context);
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context),
),
(Chattype::Group, chat_name, grpid, timestamp),
)
.await?;
@@ -2881,9 +2923,9 @@ pub async fn create_group_chat(
context.emit_msgs_changed_without_ids();
if protect == ProtectionStatus::Protected {
// this part is to stay compatible to verified groups,
// in some future, we will drop the "protect"-flag from create_group_chat()
chat_id.inner_set_protection(context, protect).await?;
chat_id
.set_protection(context, protect, timestamp, None)
.await?;
}
Ok(chat_id)
@@ -5139,72 +5181,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_protection() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::BccSelf, false).await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 1);
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// disable protection again, still unpromoted
chat_id
.set_protection(&t, ProtectionStatus::Unprotected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// send a message, this switches to promoted state
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(!chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 3);
// enable protection on promoted chat, the info-message is sent via send_msg() this time
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(!chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_by_contact_id() {
let ctx = TestContext::new_alice().await;
@@ -5510,13 +5486,7 @@ mod tests {
Ok(())
}
async fn test_sticker(
filename: &str,
bytes: &[u8],
res_viewtype: Viewtype,
w: i32,
h: i32,
) -> Result<()> {
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
@@ -5530,19 +5500,12 @@ mod tests {
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let mime = sent_msg.payload();
if res_viewtype == Viewtype::Sticker {
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
}
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, bob_chat.id);
assert_eq!(msg.get_viewtype(), res_viewtype);
let msg_filename = msg.get_filename().unwrap();
match res_viewtype {
Viewtype::Sticker => assert_eq!(msg_filename, filename),
Viewtype::Image => assert!(msg_filename.starts_with("image_")),
_ => panic!("Not implemented"),
}
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
assert_eq!(msg.get_filename(), Some(filename.to_string()));
assert_eq!(msg.get_width(), w);
assert_eq!(msg.get_height(), h);
assert!(msg.get_filebytes(&bob).await?.unwrap() > 250);
@@ -5554,10 +5517,9 @@ mod tests {
async fn test_sticker_png() -> Result<()> {
test_sticker(
"sticker.png",
include_bytes!("../test-data/image/logo.png"),
Viewtype::Sticker,
135,
135,
include_bytes!("../test-data/image/avatar64x64.png"),
64,
64,
)
.await
}
@@ -5567,7 +5529,6 @@ mod tests {
test_sticker(
"sticker.jpg",
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
Viewtype::Image,
1000,
1000,
)
@@ -5578,10 +5539,9 @@ mod tests {
async fn test_sticker_gif() -> Result<()> {
test_sticker(
"sticker.gif",
include_bytes!("../test-data/image/logo.gif"),
Viewtype::Sticker,
135,
135,
include_bytes!("../test-data/image/image100x50.gif"),
100,
50,
)
.await
}
@@ -5595,8 +5555,8 @@ mod tests {
let bob_chat = bob.create_chat(&alice).await;
// create sticker
let file_name = "sticker.png";
let bytes = include_bytes!("../test-data/image/logo.png");
let file_name = "sticker.jpg";
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
let file = alice.get_blobdir().join(file_name);
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(Viewtype::Sticker);
@@ -6133,7 +6093,7 @@ mod tests {
chat_id1,
Viewtype::Sticker,
"b.png",
include_bytes!("../test-data/image/logo.png"),
include_bytes!("../test-data/image/avatar64x64.png"),
)
.await?;
let second_image_msg_id = send_media(

View File

@@ -311,6 +311,16 @@ pub enum Config {
/// Last message processed by the bot.
LastMsgId,
/// Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.
/// 0=Nothing else happens when the key changes.
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
/// until `chat_id.accept()` is called.
#[strum(props(default = "0"))]
VerifiedOneOnOneChats,
}
impl Context {

View File

@@ -755,6 +755,12 @@ impl Context {
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats",
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));

View File

@@ -545,7 +545,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
ephemeral_timestamp
.into_iter()
.chain(delete_device_after_timestamp.into_iter())
.chain(delete_device_after_timestamp)
.min()
}
@@ -1062,14 +1062,14 @@ mod tests {
delete_expired_messages(t, not_deleted_at).await?;
let loaded = Message::load_from_db(t, msg_id).await?;
assert!(!loaded.text.is_empty());
assert_eq!(loaded.text, "Message text");
assert_eq!(loaded.chat_id, chat.id);
assert!(next_expiration < deleted_at);
delete_expired_messages(t, deleted_at).await?;
t.evtracker
.get_matching(|evt| {
if let EventType::MsgDeleted {
if let EventType::MsgsChanged {
msg_id: event_msg_id,
..
} = evt
@@ -1082,6 +1082,7 @@ mod tests {
.await;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text, "");
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
// Check that the msg was deleted locally.
@@ -1299,32 +1300,4 @@ mod tests {
Ok(())
}
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
// successful reconnection.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ephemeral_msg_offline() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
let duration = 60;
chat.id
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
let now = time();
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1)
.await?;
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
Ok(())
}
}

View File

@@ -611,7 +611,8 @@ impl Imap {
if uid_next < old_uid_next {
warn!(
context,
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
"The server illegally decreased the uid_next of folder {} from {} to {} without changing validity ({}), resyncing UIDs...",
folder, old_uid_next, uid_next, new_uid_validity,
);
set_uid_next(context, folder, uid_next).await?;
job::schedule_resync(context).await?;
@@ -627,7 +628,7 @@ impl Imap {
set_modseq(context, folder, 0).await?;
if mailbox.exists == 0 {
info!(context, "Folder {folder:?} is empty.");
info!(context, "Folder \"{}\" is empty.", folder);
// set uid_next=1 for empty folders.
// If we do not do this here, we'll miss the first message
@@ -645,7 +646,7 @@ impl Imap {
None => {
warn!(
context,
"IMAP folder {folder:?} has no uid_next, fall back to fetching."
"IMAP folder has no uid_next, fall back to fetching"
);
// note that we use fetch by sequence number
// and thus we only need to get exactly the
@@ -684,7 +685,7 @@ impl Imap {
}
info!(
context,
"uid/validity change folder {}: new {}/{} previous {}/{}.",
"uid/validity change folder {}: new {}/{} previous {}/{}",
folder,
new_uid_next,
new_uid_validity,
@@ -705,17 +706,17 @@ impl Imap {
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder, folder_meaning).await? {
info!(context, "Not fetching from {folder:?}.");
info!(context, "Not fetching from {}", folder);
return Ok(false);
}
let new_emails = self
.select_with_uidvalidity(context, folder)
.await
.with_context(|| format!("Failed to select folder {folder:?}"))?;
.with_context(|| format!("failed to select folder {folder}"))?;
if !new_emails && !fetch_existing_msgs {
info!(context, "No new emails in folder {folder:?}.");
info!(context, "No new emails in folder {}", folder);
return Ok(false);
}
@@ -741,7 +742,7 @@ impl Imap {
let headers = match get_fetch_headers(fetch_response) {
Ok(headers) => headers,
Err(err) => {
warn!(context, "Failed to parse FETCH headers: {err:#}.");
warn!(context, "Failed to parse FETCH headers: {}", err);
continue;
}
};
@@ -932,7 +933,7 @@ impl Imap {
if let Some(folder) = context.get_config(config).await? {
info!(
context,
"Fetching existing messages from folder {folder:?}."
"Fetching existing messages from folder \"{}\"", folder
);
self.fetch_new_messages(context, &folder, meaning, true)
.await

View File

@@ -650,9 +650,7 @@ mod tests {
_ => panic!("wrong chat item"),
};
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let path = msg.get_file(&ctx1).unwrap();
assert_eq!(path.with_file_name("hello.txt"), path);
let text = fs::read_to_string(&path).await.unwrap();
assert_eq!(text, "i am attachment");

View File

@@ -155,8 +155,8 @@ impl<'a> Connection<'a> {
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
info!(context, "Job {} started...", &job);
let try_res = match perform_job_action(context, &job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &job, &mut connection, 1).await,
let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await,
x => x,
};
@@ -205,7 +205,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
async fn perform_job_action(
context: &Context,
job: &Job,
job: &mut Job,
connection: &mut Connection<'_>,
tries: u32,
) -> Status {

View File

@@ -260,7 +260,7 @@ pub(crate) async fn load_keypair(
})
}
/// Use of a key pair for encryption or decryption.
/// Use of a [KeyPair] for encryption or decryption.
///
/// This is used by [store_self_keypair] to know what kind of key is
/// being saved.

View File

@@ -44,6 +44,10 @@ where
self.keys.push(key);
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}

View File

@@ -8,7 +8,6 @@
missing_debug_implementations,
missing_docs,
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless,
@@ -17,6 +16,7 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
@@ -79,7 +79,7 @@ pub mod mimeparser;
pub mod oauth2;
mod param;
pub mod peerstate;
mod pgp;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod qr_code_generator;

View File

@@ -735,7 +735,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
next_event = next_event
.into_iter()
.chain(u64::try_from(locations_send_until - now).into_iter())
.chain(u64::try_from(locations_send_until - now))
.min();
if has_locations {
@@ -759,7 +759,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
);
next_event = next_event
.into_iter()
.chain(u64::try_from(locations_last_sent + 61 - now).into_iter())
.chain(u64::try_from(locations_last_sent + 61 - now))
.min();
}
} else {

View File

@@ -688,13 +688,11 @@ impl Message {
&self.subject
}
/// Returns original filename (as shown in chat).
/// Returns base file name without the path.
/// The base file name includes the extension.
///
/// To get the full path, use [`Self::get_file()`].
pub fn get_filename(&self) -> Option<String> {
if let Some(name) = self.param.get(Param::Filename) {
return Some(name.to_string());
}
self.param
.get(Param::File)
.and_then(|file| Path::new(file).file_name())
@@ -974,11 +972,6 @@ impl Message {
/// the file will only be used when the message is prepared
/// for sending.
pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) {
if let Some(name) = Path::new(&file.to_string()).file_name() {
if let Some(name) = name.to_str() {
self.param.set(Param::Filename, name);
}
}
self.param.set(Param::File, file);
if let Some(filemime) = filemime {
self.param.set(Param::MimeType, filemime);
@@ -1429,7 +1422,6 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
/// and scheduling for deletion on IMAP.
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
let mut modified_chat_ids = BTreeSet::new();
let mut res = Ok(());
for &msg_id in msg_ids {
let msg = Message::load_from_db(context, msg_id).await?;
@@ -1453,19 +1445,13 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
modified_chat_ids.insert(msg.chat_id);
let target = context.get_delete_msgs_target().await?;
let update_db = |conn: &mut rusqlite::Connection| {
conn.execute(
context
.sql
.execute(
"UPDATE imap SET target=? WHERE rfc724_mid=?",
(target, msg.rfc724_mid),
)?;
conn.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
Ok(())
};
if let Err(e) = context.sql.call_write(update_db).await {
error!(context, "delete_msgs: failed to update db: {e:#}.");
res = Err(e);
continue;
}
)
.await?;
let logging_xdc_id = context
.debug_logging
@@ -1480,7 +1466,6 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
}
}
}
res?;
for modified_chat_id in modified_chat_ids {
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
@@ -1649,6 +1634,24 @@ pub(crate) async fn update_msg_state(
// Context functions to work with messages
/// Returns true if given message ID exists in the database and is not trashed.
pub(crate) async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
if msg_id.is_special() {
return Ok(false);
}
let chat_id: Option<ChatId> = context
.sql
.query_get_value("SELECT chat_id FROM msgs WHERE id=?;", (msg_id,))
.await?;
if let Some(chat_id) = chat_id {
Ok(!chat_id.is_trash())
} else {
Ok(false)
}
}
pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) {
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
if msg.state.can_fail() {
@@ -2420,23 +2423,4 @@ def hello():
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_msgs_offline() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
delete_msgs(&alice, &[msg.id]).await?;
assert!(!alice.sql.exists(stmt, (msg.id,)).await?);
Ok(())
}
}

View File

@@ -896,7 +896,17 @@ impl<'a> MimeFactory<'a> {
let mut placeholdertext = None;
let mut meta_part = None;
if chat.is_protected() {
let send_verified_headers = match chat.typ {
Chattype::Undefined => bail!("Undefined chat type"),
// In single chats, the protection status isn't necessarily the same for both sides,
// so we don't send the Chat-Verified header:
Chattype::Single => false,
Chattype::Group => true,
// Mailinglists and broadcast lists can actually never be verified:
Chattype::Mailinglist => false,
Chattype::Broadcast => false,
};
if chat.is_protected() && send_verified_headers {
headers
.protected
.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
@@ -1368,7 +1378,7 @@ impl<'a> MimeFactory<'a> {
///
/// This line length limit is an
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
fn wrapped_base64_encode(buf: &[u8]) -> String {
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
let mut chars = base64.chars();
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
@@ -1386,7 +1396,7 @@ async fn build_body_file(
.param
.get_blob(Param::File, context, true)
.await?
.context("msg has no file")?;
.context("msg has no filename")?;
let suffix = blob.suffix().unwrap_or("dat");
// Get file name to use for sending. For privacy purposes, we do
@@ -1431,11 +1441,7 @@ async fn build_body_file(
),
&suffix
),
_ => msg
.param
.get(Param::Filename)
.unwrap_or_else(|| blob.as_file_name())
.to_string(),
_ => blob.as_file_name().to_string(),
};
/* check mimetype */

View File

@@ -2,7 +2,6 @@
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::str;
@@ -862,7 +861,7 @@ impl MimeMessage {
is_related: bool,
) -> Result<bool> {
let mut any_part_added = false;
let mimetype = get_mime_type(mail, &get_attachment_filename(context, mail)?)?.0;
let mimetype = get_mime_type(mail)?.0;
match (mimetype.type_(), mimetype.subtype().as_str()) {
/* Most times, multipart/alternative contains true alternatives
as text/plain and text/html. If we find a multipart/mixed
@@ -870,9 +869,9 @@ impl MimeMessage {
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
(mime::MULTIPART, "alternative") => {
for cur_data in &mail.subparts {
let mime_type =
get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?.0;
if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
if get_mime_type(cur_data)?.0 == "multipart/mixed"
|| get_mime_type(cur_data)?.0 == "multipart/related"
{
any_part_added = self
.parse_mime_recursive(context, cur_data, is_related)
.await?;
@@ -882,11 +881,7 @@ impl MimeMessage {
if !any_part_added {
/* search for text/plain and add this */
for cur_data in &mail.subparts {
if get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?
.0
.type_()
== mime::TEXT
{
if get_mime_type(cur_data)?.0.type_() == mime::TEXT {
any_part_added = self
.parse_mime_recursive(context, cur_data, is_related)
.await?;
@@ -1012,10 +1007,11 @@ impl MimeMessage {
is_related: bool,
) -> Result<bool> {
// return true if a part was added
let filename = get_attachment_filename(context, mail)?;
let (mime_type, msg_type) = get_mime_type(mail, &filename)?;
let (mime_type, msg_type) = get_mime_type(mail)?;
let raw_mime = mail.ctype.mimetype.to_lowercase();
let filename = get_attachment_filename(context, mail)?;
let old_part_count = self.parts.len();
match filename {
@@ -1272,7 +1268,6 @@ impl MimeMessage {
part.mimetype = Some(mime_type);
part.bytes = decoded_data.len();
part.param.set(Param::File, blob.as_name());
part.param.set(Param::Filename, filename);
part.param.set(Param::MimeType, raw_mime);
part.is_related = is_related;
@@ -1869,10 +1864,7 @@ pub struct Part {
}
/// return mimetype and viewtype for a parsed mail
fn get_mime_type(
mail: &mailparse::ParsedMail<'_>,
filename: &Option<String>,
) -> Result<(Mime, Viewtype)> {
fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
let viewtype = match mimetype.type_() {
@@ -1900,7 +1892,7 @@ fn get_mime_type(
} else {
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
// be handled separatedly.
// be handled separately.
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
// which are unwanted at all).
// For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
@@ -1908,16 +1900,7 @@ fn get_mime_type(
Viewtype::Unknown
}
}
mime::APPLICATION => match mimetype.subtype() {
mime::OCTET_STREAM => match filename {
Some(filename) => match message::guess_msgtype_from_suffix(Path::new(&filename)) {
Some((viewtype, _)) => viewtype,
None => Viewtype::File,
},
None => Viewtype::File,
},
_ => Viewtype::File,
},
mime::APPLICATION => Viewtype::File,
_ => Viewtype::Unknown,
};
@@ -3772,22 +3755,4 @@ Content-Disposition: reaction\n\
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_jpeg_as_application_octet_stream() -> Result<()> {
let context = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/jpeg-as-application-octet-stream.eml");
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(msg.parts.len(), 1);
assert_eq!(msg.parts[0].typ, Viewtype::Image);
receive_imf(&context, &raw[..], false).await?;
let msg = context.get_last_msg().await;
assert_eq!(msg.get_viewtype(), Viewtype::Image);
Ok(())
}
}

View File

@@ -21,9 +21,6 @@ pub enum Param {
/// For messages and jobs
File = b'f',
/// For messages: original filename (as shown in chat)
Filename = b'v',
/// For messages: This name should be shown instead of contact.get_display_name()
/// (used if this is a mailinglist
/// or explicitly set using set_override_sender_name(), eg. by bots)
@@ -531,7 +528,7 @@ mod tests {
fs::write(fname, b"boo").await.unwrap();
let blob = p.get_blob(Param::File, &t, true).await.unwrap().unwrap();
assert!(blob.as_file_name().starts_with("foo"));
assert_eq!(blob, BlobObject::from_name(&t, "foo".to_string()).unwrap());
// Blob in blobdir, expect blob.
let bar_path = t.get_blobdir().join("bar");

View File

@@ -392,6 +392,31 @@ impl Peerstate {
}
}
/// Returns a reference to the contact's public key fingerprint.
///
/// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key.
fn peek_key_fingerprint(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Fingerprint> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key_fingerprint.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key_fingerprint
.as_ref()
.or(self.gossip_key_fingerprint.as_ref()),
}
}
/// Returns true if the key used for opportunistic encryption in the 1:1 chat
/// is the same as the verified key.
///
/// Note that verified groups always use the verified key no matter if the
/// opportunistic key matches or not.
pub(crate) fn is_using_verified_key(&self) -> bool {
let verified = self.peek_key_fingerprint(PeerstateVerifiedStatus::BidirectVerified);
verified.is_some()
&& verified == self.peek_key_fingerprint(PeerstateVerifiedStatus::Unverified)
}
/// Set this peerstate to verified
/// Make sure to call `self.save_to_db` to save these changes
/// Params:

View File

@@ -24,8 +24,7 @@ use crate::keyring::Keyring;
use crate::tools::EmailAddress;
#[allow(missing_docs)]
#[cfg(test)]
pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
#[allow(missing_docs)]
pub const HEADER_SETUPCODE: &str = "passphrase-begin";

View File

@@ -71,7 +71,7 @@ async fn get_unique_quota_roots_and_usage(
// messages could be received and so the usage could have been changed
*unique_quota_roots
.entry(quota_root_name.clone())
.or_default() = quota.resources;
.or_insert_with(Vec::new) = quota.resources;
}
}
}

View File

@@ -4,7 +4,7 @@ use std::cmp::min;
use std::collections::HashSet;
use std::convert::TryFrom;
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{Context as _, Result};
use mailparse::{parse_mail, SingleInfo};
use num_traits::FromPrimitive;
use once_cell::sync::Lazy;
@@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin, VerifiedStatus,
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -143,7 +143,7 @@ pub(crate) async fn receive_imf_inner(
// check, if the mail is already in our database.
// make sure, this check is done eg. before securejoin-processing.
let (replace_partial_download, replace_chat_id) =
let replace_partial_download =
if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
let msg = Message::load_from_db(context, old_msg_id).await?;
if msg.download_state() != DownloadState::Done && is_partial_download.is_none() {
@@ -152,14 +152,14 @@ pub(crate) async fn receive_imf_inner(
context,
"Message already partly in DB, replacing by full message."
);
(Some(old_msg_id), Some(msg.chat_id))
Some(old_msg_id)
} else {
// the message was probably moved around.
info!(context, "Message already in DB, doing nothing.");
return Ok(None);
}
} else {
(None, None)
None
};
let prevent_rename =
@@ -347,8 +347,8 @@ pub(crate) async fn receive_imf_inner(
}
}
if let Some(replace_chat_id) = replace_chat_id {
context.emit_msgs_changed(replace_chat_id, MsgId::new(0));
if replace_partial_download.is_some() {
context.emit_msgs_changed(chat_id, MsgId::new(0));
} else if !chat_id.is_trash() {
let fresh = received_msg.state == MessageState::InFresh;
for msg_id in &received_msg.msg_ids {
@@ -594,12 +594,17 @@ async fn add_parts(
// In lookup_chat_by_reply() and create_or_lookup_group(), it can happen that the message is put into a chat
// but the From-address is not a member of this chat.
if let Some(chat_id) = chat_id {
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
let chat = Chat::load_from_db(context, chat_id).await?;
if let Some(group_chat_id) = chat_id {
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
let chat = Chat::load_from_db(context, group_chat_id).await?;
if chat.is_protected() {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
if chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
}
} else {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
@@ -615,7 +620,7 @@ async fn add_parts(
context,
mime_parser,
sent_timestamp,
chat_id,
group_chat_id,
from_id,
to_ids,
)
@@ -718,6 +723,44 @@ async fn add_parts(
);
}
}
// The next block checks if the message was sent with verified encryption
// and sets the protection of the 1:1 chat accordingly.
if is_partial_download.is_none()
&& mime_parser.get_header(HeaderDef::SecureJoin).is_none()
&& !is_mdn
{
let mut new_protection = match has_verified_encryption(
context,
mime_parser,
from_id,
to_ids,
Chattype::Single,
)
.await?
{
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.protected != new_protection {
if new_protection == ProtectionStatus::Unprotected
&& context
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
{
new_protection = ProtectionStatus::ProtectionBroken;
}
let sort_timestamp =
calc_sort_timestamp(context, sent_timestamp, chat_id, true).await?;
chat_id
.set_protection(context, new_protection, sort_timestamp, Some(from_id))
.await?;
}
}
}
}
@@ -984,42 +1027,14 @@ async fn add_parts(
// if a chat is protected and the message is fully downloaded, check additional properties
if !chat_id.is_special() && is_partial_download.is_none() {
let chat = Chat::load_from_db(context, chat_id).await?;
let new_status = match mime_parser.is_system_message {
SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected),
SystemMessage::ChatProtectionDisabled => Some(ProtectionStatus::Unprotected),
_ => None,
};
if chat.is_protected() || new_status.is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
if chat.is_protected() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await?
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
} else {
// change chat protection only when verification check passes
if let Some(new_status) = new_status {
if chat_id
.update_timestamp(
context,
Param::ProtectionSettingsTimestamp,
sent_timestamp,
)
.await?
{
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
chat::add_info_msg(
context,
chat_id,
&format!("Cannot set protection: {e}"),
sort_timestamp,
)
.await?;
// do not return an error as this would result in retrying the message
}
}
better_msg = Some(context.stock_protection_msg(new_status, from_id).await);
}
}
}
}
@@ -1527,7 +1542,9 @@ async fn create_or_lookup_group(
}
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, Chattype::Group).await?
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
@@ -1775,20 +1792,6 @@ async fn apply_group_changes(
}
}
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
}
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
}
}
// Recreate the member list.
if recreate_member_list {
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
@@ -2125,49 +2128,53 @@ async fn create_adhoc_group(
Ok(Some(new_chat_id))
}
async fn check_verified_properties(
enum VerifiedEncryption {
Verified,
NotVerified(String), // The string contains the reason why it's not verified
}
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted, signed with a verified key,
/// and if it's a group, all the recipients are verified.
async fn has_verified_encryption(
context: &Context,
mimeparser: &MimeMessage,
from_id: ContactId,
to_ids: &[ContactId],
) -> Result<()> {
let contact = Contact::get_by_id(context, from_id).await?;
chat_type: Chattype,
) -> Result<VerifiedEncryption> {
use VerifiedEncryption::*;
ensure!(mimeparser.was_encrypted(), "This message is not encrypted");
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
// we do not fail here currently, this would exclude (a) non-deltas
// and (b) deltas with different protection views across multiple devices.
// for group creation or protection enabled/disabled, however, Chat-Verified is respected.
warn!(
context,
"{} did not mark message as protected.",
contact.get_addr()
);
if from_id == ContactId::SELF && chat_type == Chattype::Single {
// For outgoing emails in the 1:1 chat, we have an exception that
// they are allowed to be unencrypted:
// 1. They can't be an attack (they are outgoing, not incoming)
// 2. Probably the unencryptedness is just a temporary state, after all
// the user obviously still uses DC
// -> Showing info messages everytime would be a lot of noise
// 3. The info messages that are shown to the user ("Your chat partner
// likely reinstalled DC" or similar) would be wrong.
return Ok(Verified);
}
if !mimeparser.was_encrypted() {
return Ok(NotVerified("This message is not encrypted".to_string()));
};
// ensure, the contact is verified
// and the message is signed with a verified key of the sender.
// this check is skipped for SELF as there is no proper SELF-peerstate
// and results in group-splits otherwise.
if from_id != ContactId::SELF {
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
return Ok(NotVerified("No peerstate, the contact isn't verified".to_string()));
};
if peerstate.is_none()
|| contact.is_verified_ex(context, peerstate.as_ref()).await?
!= VerifiedStatus::BidirectVerified
{
bail!(
"Sender of this message is not verified: {}",
contact.get_addr()
);
}
if let Some(peerstate) = peerstate {
ensure!(
peerstate.has_verified_key(&mimeparser.signatures),
"The message was sent with non-verified encryption"
);
if !peerstate.has_verified_key(&mimeparser.signatures) {
return Ok(NotVerified(
"The message was sent with non-verified encryption".to_string(),
));
}
}
@@ -2179,7 +2186,7 @@ async fn check_verified_properties(
.collect::<Vec<ContactId>>();
if to_ids.is_empty() {
return Ok(());
return Ok(Verified);
}
let rows = context
@@ -2203,10 +2210,12 @@ async fn check_verified_properties(
)
.await?;
let contact = Contact::get_by_id(context, from_id).await?;
for (to_addr, mut is_verified) in rows {
info!(
context,
"check_verified_properties: {:?} self={:?}.",
"has_verified_encryption: {:?} self={:?}.",
to_addr,
context.is_self_addr(&to_addr).await
);
@@ -2240,13 +2249,13 @@ async fn check_verified_properties(
}
}
if !is_verified {
bail!(
return Ok(NotVerified(format!(
"{} is not a member of this protected chat",
to_addr.to_string()
);
to_addr
)));
}
}
Ok(())
Ok(Verified)
}
/// Returns the last message referenced from `References` header if it is in the database.

View File

@@ -1456,9 +1456,7 @@ async fn test_pdf_filename_simple() {
.await;
assert_eq!(msg.viewtype, Viewtype::File);
assert_eq!(msg.text, "mail body");
let file_path = msg.param.get(Param::File).unwrap();
assert!(file_path.starts_with("$BLOBDIR/simple"));
assert!(file_path.ends_with(".pdf"));
assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1472,9 +1470,10 @@ async fn test_pdf_filename_continuation() {
.await;
assert_eq!(msg.viewtype, Viewtype::File);
assert_eq!(msg.text, "mail body");
let file_path = msg.param.get(Param::File).unwrap();
assert!(file_path.starts_with("$BLOBDIR/test pdf äöüß"));
assert!(file_path.ends_with(".pdf"));
assert_eq!(
msg.param.get(Param::File).unwrap(),
"$BLOBDIR/test pdf äöüß.pdf"
);
}
/// HTML-images may come with many embedded images, eg. tiny icons, corners for formatting,
@@ -2798,7 +2797,7 @@ Reply from different address
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_and_duplicated_filenames() -> Result<()> {
async fn test_long_filenames() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -2810,7 +2809,6 @@ async fn test_long_and_duplicated_filenames() -> Result<()> {
"foo. .tar.gz",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz",
"a.tar.gz",
"a.tar.gz",
"a.a..a.a.a.a.tar.gz",
] {
let attachment = alice.blobdir.join(filename_sent);
@@ -2825,19 +2823,22 @@ async fn test_long_and_duplicated_filenames() -> Result<()> {
let msg_bob = bob.recv_msg(&sent).await;
async fn check_message(msg: &Message, t: &TestContext, filename: &str, content: &str) {
async fn check_message(msg: &Message, t: &TestContext, content: &str) {
assert_eq!(msg.get_viewtype(), Viewtype::File);
let resulting_filename = msg.get_filename().unwrap();
assert_eq!(resulting_filename, filename);
let path = msg.get_file(t).unwrap();
assert!(
resulting_filename.ends_with(".tar.gz"),
"{resulting_filename:?} doesn't end with .tar.gz, path: {path:?}"
);
assert!(
path.to_str().unwrap().ends_with(".tar.gz"),
"path {path:?} doesn't end with .tar.gz"
);
assert_eq!(fs::read_to_string(path).await.unwrap(), content);
}
check_message(&msg_alice, &alice, filename_sent, &content).await;
check_message(&msg_bob, &bob, filename_sent, &content).await;
check_message(&msg_alice, &alice, &content).await;
check_message(&msg_bob, &bob, &content).await;
}
Ok(())

View File

@@ -864,7 +864,7 @@ impl Scheduler {
// Actually shutdown tasks.
let timeout_duration = std::time::Duration::from_secs(30);
for b in once(self.inbox).chain(self.oboxes.into_iter()) {
for b in once(self.inbox).chain(self.oboxes) {
tokio::time::timeout(timeout_duration, b.handle)
.await
.log_err(context)

View File

@@ -6,7 +6,7 @@ use anyhow::{bail, Context as _, Error, Result};
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
@@ -701,6 +701,14 @@ async fn secure_connection_established(
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, &contact).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact_id),
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}
@@ -783,6 +791,8 @@ mod tests {
use crate::contact::VerifiedStatus;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str::chat_protection_enabled;
use crate::test_utils::get_chat_msg;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::EmailAddress;
@@ -921,7 +931,7 @@ mod tests {
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id())
let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
@@ -929,11 +939,17 @@ mod tests {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Alice's 1:1 chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
assert!(msg.get_text().contains("bob@example.net verified"));
.collect();
assert_eq!(msg_ids.len(), 2);
let msg0 = Message::load_from_db(&alice.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("bob@example.net verified"));
let msg1 = Message::load_from_db(&alice.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg1.get_text(), expected_text);
}
// Check Alice sent the right message to Bob.
@@ -969,7 +985,7 @@ mod tests {
// Check Bob got the verified message in his 1:1 chat.
{
let chat = bob.create_chat(&alice).await;
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id())
let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
@@ -977,11 +993,16 @@ mod tests {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Bob's 1:1 chat");
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
assert!(msg.get_text().contains("alice@example.org verified"));
.collect();
let msg0 = Message::load_from_db(&bob.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("alice@example.org verified"));
let msg1 = Message::load_from_db(&bob.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let expected_text = chat_protection_enabled(&bob).await;
assert_eq!(msg1.get_text(), expected_text);
}
// Check Bob sent the final message
@@ -1278,17 +1299,11 @@ mod tests {
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
);
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.min()
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
// There should be 3 messages in the chat:
// - The ChatProtectionEnabled message
// - bob@example.net verified
// - You added member bob@example.net
let msg = get_chat_msg(&alice, alice_chatid, 1, 3).await;
assert!(msg.is_info());
assert!(msg.get_text().contains("bob@example.net verified"));
}

View File

@@ -222,6 +222,14 @@ impl BobState {
let msg = stock_str::contact_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact.id),
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}

View File

@@ -565,6 +565,24 @@ pub(crate) async fn send_msg_to_smtp(
)
.collect::<Vec<_>>();
// If there is a msg-id and it does not exist in the db, cancel sending. this happens if
// delete_msgs() was called before the generated mime was sent out.
if !message::exists(context, msg_id)
.await
.with_context(|| format!("failed to check message {msg_id} existence"))?
{
info!(
context,
"Sending of message {msg_id} (entry {rowid}) was cancelled by the user."
);
context
.sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
.await
.context("failed to remove cancelled message from smtp table")?;
return Ok(());
}
let status = smtp_send(context, &recipients_list, body.as_str(), smtp, msg_id).await;
match status {

View File

@@ -393,18 +393,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
MsgEphemeralTimerWeeksBy = 157,
#[strum(props(fallback = "You enabled chat protection."))]
YouEnabledProtection = 158,
#[strum(props(fallback = "Chat protection enabled by %1$s."))]
ProtectionEnabledBy = 159,
#[strum(props(fallback = "You disabled chat protection."))]
YouDisabledProtection = 160,
#[strum(props(fallback = "Chat protection disabled by %1$s."))]
ProtectionDisabledBy = 161,
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
BackupTransferQr = 162,
@@ -419,6 +407,12 @@ pub enum StockMessage {
#[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166,
#[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
ChatProtectionEnabled = 170,
#[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171,
}
impl StockMessage {
@@ -515,13 +509,21 @@ trait StockStringMods: AsRef<str> + Sized {
}
impl ContactId {
/// Get contact name for stock string.
async fn get_stock_name(self, context: &Context) -> String {
/// Get contact name and address for stock string, e.g. `Bob (bob@example.net)`
async fn get_stock_name_n_addr(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_else(|_| self.to_string())
}
/// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set.
async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_display_name().to_string())
.unwrap_or_else(|_| self.to_string())
}
}
impl StockStringMods for String {}
@@ -583,7 +585,7 @@ pub(crate) async fn msg_grp_name(
.await
.replace1(from_group)
.replace2(to_group)
.replace3(&by_contact.get_stock_name(context).await)
.replace3(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -593,7 +595,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgChangedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -640,7 +642,7 @@ pub(crate) async fn msg_add_member_local(
translated(context, StockMessage::MsgAddMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -687,7 +689,7 @@ pub(crate) async fn msg_del_member_local(
translated(context, StockMessage::MsgDelMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -703,7 +705,7 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
} else {
translated(context, StockMessage::MsgGroupLeftBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -756,7 +758,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgDeletedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -782,13 +784,9 @@ pub(crate) async fn secure_join_started(
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(contact.get_display_name())
} else {
format!("secure_join_replies: unknown contact {contact_id}")
}
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
/// Stock string: `Scan to chat with %1$s`.
@@ -881,7 +879,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
} else {
translated(context, StockMessage::MsgLocationEnabledBy)
.await
.replace1(&contact.get_stock_name(context).await)
.replace1(&contact.get_stock_name_n_addr(context).await)
}
}
@@ -950,7 +948,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
} else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -968,7 +966,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await
.replace1(timer)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -979,7 +977,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
} else {
translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -990,7 +988,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1001,7 +999,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
} else {
translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1012,7 +1010,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1053,26 +1051,16 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
translated(context, StockMessage::ErrorNoNetwork).await
}
/// Stock string: `Chat protection enabled.`.
pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::YouEnabledProtection).await
} else {
translated(context, StockMessage::ProtectionEnabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.`
pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await
}
/// Stock string: `Chat protection disabled.`.
pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::YouDisabledProtection).await
} else {
translated(context, StockMessage::ProtectionDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
/// Stock string: `%1$s sent a message from another device.`
pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::ChatProtectionDisabled)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
/// Stock string: `Reply`.
@@ -1104,7 +1092,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await
.replace1(minutes)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1122,7 +1110,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await
.replace1(hours)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1140,7 +1128,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await
.replace1(days)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1158,7 +1146,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await
.replace1(weeks)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
@@ -1332,11 +1320,19 @@ impl Context {
pub(crate) async fn stock_protection_msg(
&self,
protect: ProtectionStatus,
from_id: ContactId,
contact_id: Option<ContactId>,
) -> String {
match protect {
ProtectionStatus::Unprotected => protection_enabled(self, from_id).await,
ProtectionStatus::Protected => protection_disabled(self, from_id).await,
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
if let Some(contact_id) = contact_id {
chat_protection_disabled(self, contact_id).await
} else {
// In a group chat, it's not possible to downgrade verification.
// In a 1:1 chat, the `contact_id` always has to be provided.
"[Error] No contact_id given".to_string()
}
}
ProtectionStatus::Protected => chat_protection_enabled(self).await,
}
}

View File

@@ -9,6 +9,7 @@ use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::stock_str;
use crate::tools::truncate;
@@ -132,8 +133,14 @@ impl Message {
append_text = false;
stock_str::ac_setup_msg_subject(context).await
} else {
let file_name = self
.get_filename()
let file_name: String = self
.param
.get_path(Param::File, context)
.unwrap_or(None)
.and_then(|path| {
path.file_name()
.map(|fname| fname.to_string_lossy().into_owned())
})
.unwrap_or_else(|| String::from("ErrFileName"));
let label = if self.viewtype == Viewtype::Audio {
stock_str::audio(context).await
@@ -193,7 +200,6 @@ impl Message {
#[cfg(test)]
mod tests {
use super::*;
use crate::param::Param;
use crate::test_utils as test;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -31,11 +31,14 @@ use crate::constants::Chattype;
use crate::constants::{DC_GCL_NO_SPECIALS, DC_MSG_ID_DAYMARKER};
use crate::contact::{Contact, ContactAddress, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::events::{Event, EventType, Events};
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::MimeMessage;
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str::StockStrings;
use crate::tools::EmailAddress;
@@ -108,9 +111,15 @@ impl TestContextManager {
/// - Let one TestContext send a message
/// - Let the other TestContext receive it and accept the chat
/// - Assert that the message arrived
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
pub async fn send_recv_accept(
&self,
from: &TestContext,
to: &TestContext,
msg: &str,
) -> Message {
let received_msg = self.send_recv(from, to, msg).await;
received_msg.chat_id.accept(to).await.unwrap();
received_msg
}
/// - Let one TestContext send a message
@@ -152,6 +161,27 @@ impl TestContextManager {
new_addr
);
}
pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) {
self.section(&format!(
"{} scans {}'s QR code",
scanner.name(),
scanned.name()
));
let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap();
join_securejoin(&scanner.ctx, &qr).await.unwrap();
loop {
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
scanned.recv_msg(&sent).await;
} else if let Some(sent) = scanned.pop_sent_msg_opt(Duration::ZERO).await {
scanner.recv_msg(&sent).await;
} else {
break;
}
}
}
}
#[derive(Debug, Clone, Default)]
@@ -636,7 +666,7 @@ impl TestContext {
// We're using `unwrap_or_default()` here so that if the file doesn't exist,
// it can be created using `write` below.
let expected = fs::read(&filename).await.unwrap_or_default();
let expected = String::from_utf8(expected).unwrap();
let expected = String::from_utf8(expected).unwrap().replace("\r\n", "\n");
if (std::env::var("UPDATE_GOLDEN_TESTS") == Ok("1".to_string())) && actual != expected {
fs::write(&filename, &actual)
.await
@@ -1008,6 +1038,26 @@ fn print_logevent(logevent: &LogEvent) {
}
}
/// Saves the other account's public key as verified.
pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let mut peerstate = Peerstate::from_header(
&EncryptHelper::new(other).await.unwrap().get_aheader(),
// We have to give 0 as the time, not the current time:
// The time is going to be saved in peerstate.last_seen.
// The code in `peerstate.rs` then compares `if message_time > self.last_seen`,
// and many similar checks in peerstate.rs, and doesn't allow changes otherwise.
// Giving the current time would mean that message_time == peerstate.last_seen,
// so changes would not be allowed.
// This might lead to flaky tests.
0,
);
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.save_to_db(&this.sql).await.unwrap();
}
/// Pretty-print an event to stdout
///
/// Done during tests this is captured by `cargo test` and associated with the test itself.
@@ -1114,7 +1164,17 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
} else {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",

View File

@@ -1 +1,2 @@
mod aeap;
mod verified_chats;

View File

@@ -8,10 +8,10 @@ use crate::contact;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::message::Message;
use crate::peerstate;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str;
use crate::test_utils::mark_as_verified;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
@@ -327,19 +327,6 @@ async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob:
}
}
async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let other_addr = other.get_primary_self_addr().await.unwrap();
let mut peerstate = peerstate::Peerstate::from_addr(this, &other_addr)
.await
.unwrap()
.unwrap();
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.save_to_db(&this.sql).await.unwrap();
}
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {
let msgs = chat::get_chat_msgs_ex(
&t.ctx,

547
src/tests/verified_chats.rs Normal file
View File

@@ -0,0 +1,547 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::{Chat, ProtectionStatus};
use crate::config::Config;
use crate::contact::VerifiedStatus;
use crate::contact::{Contact, Origin};
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::stock_str;
use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
use crate::{e2ee, message};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_classical() {
check_verified_oneonone_chat(true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_device_change() {
check_verified_oneonone_chat(false).await;
}
async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
tcm.execute_securejoin(&alice, &bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
if broken_by_classical_email {
tcm.section("Bob uses a classical MUA to send a message to Alice");
receive_imf(
&alice,
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\
Message-ID: <abcd@example.net>\r\n\
\r\n\
Heyho!\r\n",
false,
)
.await
.unwrap()
.unwrap();
} else {
tcm.section("Bob sets up another Delta Chat device");
let bob2 = TestContext::new().await;
enable_verified_oneonone_chats(&[&bob2]).await;
bob2.set_name("bob2");
bob2.configure_addr("bob@example.net").await;
tcm.send_recv(&bob2, &alice, "Using another device now")
.await;
}
// Bob's contact is still verified, but the chat isn't marked as protected anymore
assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await;
tcm.section("Bob sends another message from DC");
tcm.send_recv(&bob, &alice, "Using DC again").await;
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(
contact.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// Bob's chat is marked as verified again
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
enable_verified_oneonone_chats(&[&alice, &bob, &fiona]).await;
tcm.execute_securejoin(&alice, &bob).await;
tcm.execute_securejoin(&bob, &fiona).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
assert_verified(&bob, &fiona, ProtectionStatus::Protected).await;
assert_verified(&fiona, &bob, ProtectionStatus::Protected).await;
let group_id = bob
.create_group_with_members(
ProtectionStatus::Protected,
"Group with everyone",
&[&alice, &fiona],
)
.await;
assert_eq!(
get_chat_msg(&bob, group_id, 0, 1).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
{
let sent = bob.send_text(group_id, "Heyho").await;
alice.recv_msg(&sent).await;
let msg = fiona.recv_msg(&sent).await;
assert_eq!(
get_chat_msg(&fiona, msg.chat_id, 0, 2)
.await
.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
}
// Alice and Fiona should now be verified because of gossip
let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await;
assert_eq!(
alice_fiona_contact.is_verified(&alice).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// As soon as Alice creates a chat with Fiona, it should directly be protected
{
let chat = alice.create_chat(&fiona).await;
assert!(chat.is_protected());
let msg = alice.get_last_msg().await;
let expected_text = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg.text, expected_text);
}
// Fiona should also see the chat as protected
{
let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await;
let alice_fiona_id = rcvd.chat_id;
let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?;
assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await;
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
assert_eq!(msg0.text, expected_text);
}
tcm.section("Fiona reinstalls DC");
drop(fiona);
let fiona_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&fiona_new]).await;
fiona_new.configure_addr("fiona@example.net").await;
e2ee::ensure_secret_key_exists(&fiona_new).await?;
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
// The chat should be and stay unprotected
{
let chat = alice.get_chat(&fiona_new).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
// After recreating the chat, it should still be unprotected
chat.id.delete(&alice).await?;
let chat = alice.create_chat(&fiona_new).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_unverified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// A chat with an unknown contact should be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2@example.org>\n\
\n\
hello\n",
false,
)
.await?;
chat.id.delete(&alice).await.unwrap();
// Now Bob is a known contact, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
tcm.send_recv(&bob, &alice, "hi").await;
chat.id.delete(&alice).await.unwrap();
// Now we have a public key, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_degrade_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
let alice_chat = alice.create_chat(&bob).await;
assert!(alice_chat.is_protected());
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2@example.org>\n\
\n\
hello\n",
false,
)
.await?;
let contact_id = Contact::lookup_id_by_addr(&alice, "bob@example.net", Origin::Hidden)
.await?
.unwrap();
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 3).await;
let enabled = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg0.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
let msg1 = get_chat_msg(&alice, alice_chat.id, 1, 3).await;
let disabled = stock_str::chat_protection_disabled(&alice, contact_id).await;
assert_eq!(msg1.text, disabled);
assert_eq!(msg1.param.get_cmd(), SystemMessage::ChatProtectionDisabled);
let msg2 = get_chat_msg(&alice, alice_chat.id, 2, 3).await;
assert_eq!(msg2.text, "hello".to_string());
assert!(!msg2.is_system_message());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_enable_disable() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let chat = alice.create_chat(&bob).await;
assert!(chat.is_protected());
for alice_accepts_breakage in [true, false] {
// Bob uses Thunderbird to send a message
receive_imf(
&alice,
format!(
"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2{alice_accepts_breakage}@example.org>\n\
\n\
Message from Thunderbird\n"
)
.as_bytes(),
false,
)
.await?;
let chat = alice.get_chat(&bob).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
if alice_accepts_breakage {
tcm.section("Alice clicks 'Accept' on the input-bar-dialog");
chat.id.accept(&alice).await?;
let chat = alice.get_chat(&bob).await.unwrap();
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
// Bob sends a message from DC again
tcm.send_recv(&bob, &alice, "Hello from DC").await;
let chat = alice.get_chat(&bob).await.unwrap();
assert!(chat.is_protected());
assert!(!chat.is_protection_broken());
}
alice
.golden_test_chat(chat.id, "test_verified_oneonone_chat_enable_disable")
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_doesnt_disable_verification() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?;
let rendered_msg = mimefactory.render(&bob).await?;
let body = rendered_msg.message;
receive_imf(&alice, body.as_bytes(), false).await.unwrap();
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_mua_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
let sent = receive_imf(
&alice,
b"From: alice@example.org\n\
To: bob@example.net\n\
\n\
One classical MUA message",
false,
)
.await?
.unwrap();
tcm.send_recv_accept(&alice, &bob, "Sending with DC again")
.await;
alice
.golden_test_chat(sent.chat_id, "test_outgoing_mua_msg")
.await;
Ok(())
}
/// If Bob answers unencrypted from another address with a classical MUA,
/// the message is under some circumstances still assigned to the original
/// chat (see lookup_chat_by_reply()); this is meant to make aliases
/// work nicely.
/// However, if the original chat is verified, the unencrypted message
/// must NOT be assigned to it (it would be replaced by an error
/// message in the verified chat, so, this would just be a usability issue,
/// not a security issue).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reply() -> Result<()> {
for verified in [false, true] {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
if verified {
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
}
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
let encrypted_msg = tcm.send_recv_accept(&alice, &bob, "Heyho back").await;
let unencrypted_msg = receive_imf(
&alice,
format!(
"From: bob@someotherdomain.org\n\
To: some-alias-forwarding-to-alice@example.org\n\
In-Reply-To: {}\n\
\n\
Weird reply",
encrypted_msg.rfc724_mid
)
.as_bytes(),
false,
)
.await?
.unwrap();
let unencrypted_msg = Message::load_from_db(&alice, unencrypted_msg.msg_ids[0]).await?;
assert_eq!(unencrypted_msg.text, "Weird reply");
if verified {
assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
} else {
assert_eq!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
}
}
Ok(())
}
/// Regression test for the following bug:
///
/// - Scan your chat partner's QR Code
/// - They change devices
/// - They send you a message
/// - Without accepting the encryption downgrade, scan your chat partner's QR Code again
///
/// -> The re-verification fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_break_protection_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Cave: Bob can't write a message to Alice here.
// If he did, alice would increase his peerstate's last_seen timestamp.
// Then, after Bob reinstalls DC, alice's `if message_time > last_seen*`
// checks would return false (there are many checks of this form in peerstate.rs).
// Therefore, during the securejoin, Alice wouldn't accept the new key
// and reject the securejoin.
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&bob_new]).await;
bob_new.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.send_recv(&bob_new, &alice, "I have a new device").await;
assert_verified(&alice, &bob_new, ProtectionStatus::ProtectionBroken).await;
{
let alice_bob_chat = alice.get_chat(&bob_new).await.unwrap();
assert!(!alice_bob_chat.can_send(&alice).await?);
// Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message:
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Draftttt".to_string());
alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?;
assert_eq!(
alice_bob_chat.id.get_draft(&alice).await?.unwrap().text,
"Draftttt"
);
}
tcm.execute_securejoin(&alice, &bob_new).await;
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
Ok(())
}
/// Regression test:
/// - Verify a contact
/// - The contact stops using DC and sends a message from a classical MUA instead
/// - Delete the 1:1 chat
/// - Create a 1:1 chat
/// - Check that the created chat is not marked as protected
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_oneonone_chat_with_former_verified_contact() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice]).await;
mark_as_verified(&alice, &bob).await;
receive_imf(
&alice,
b"Subject: Message from bob\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\
Message-ID: <abcd@example.net>\r\n\
\r\n\
Heyho!\r\n",
false,
)
.await
.unwrap()
.unwrap();
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Unprotected).await;
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(
contact.is_verified(this).await.unwrap(),
VerifiedStatus::BidirectVerified
);
let chat = this.get_chat(other).await.unwrap();
let (expect_protected, expect_broken) = match protected {
ProtectionStatus::Unprotected => (false, false),
ProtectionStatus::Protected => (true, false),
ProtectionStatus::ProtectionBroken => (false, true),
};
assert_eq!(chat.is_protected(), expect_protected);
assert_eq!(chat.is_protection_broken(), expect_broken);
}
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {
for t in test_contexts {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap()
}
}

View File

@@ -702,7 +702,7 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result<Vec<u8>> {
}
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
/// This method strips all occurances of the RTLO Unicode character.
/// This method strips all occurrences of the RTLO Unicode character.
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
pub(crate) fn strip_rtlo_characters(input_str: &str) -> String {
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")

View File

@@ -18,7 +18,6 @@ use crate::contact::ContactId;
use crate::context::Context;
use crate::download::DownloadState;
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::wrapped_base64_encode;
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::param::Params;
@@ -536,16 +535,13 @@ impl Context {
}
pub(crate) fn build_status_update_part(&self, json: &str) -> PartBuilder {
let encoded_body = wrapped_base64_encode(json.as_bytes());
PartBuilder::new()
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
.header((
"Content-Disposition",
"attachment; filename=\"status-update.json\"",
))
.header(("Content-Transfer-Encoding", "base64"))
.body(encoded_body)
.body(json)
}
/// Receives status updates from receive_imf to the database
@@ -565,6 +561,7 @@ impl Context {
json: &str,
) -> Result<()> {
let msg = Message::load_from_db(self, msg_id).await?;
let chat_id = msg.chat_id;
let (timestamp, mut instance, can_info_msg) = if msg.viewtype == Viewtype::Webxdc {
(msg.timestamp_sort, msg, false)
} else if let Some(parent) = msg.parent(self).await? {
@@ -578,16 +575,17 @@ impl Context {
} else {
bail!("receive_status_update: status message has no parent.")
};
let chat_id = instance.chat_id;
if from_id != ContactId::SELF && !chat::is_contact_in_chat(self, chat_id, from_id).await? {
if from_id != ContactId::SELF
&& !chat::is_contact_in_chat(self, instance.chat_id, from_id).await?
{
let chat_type: Chattype = self
.sql
.query_get_value("SELECT type FROM chats WHERE id=?", (chat_id,))
.await?
.with_context(|| format!("Chat type for chat {chat_id} not found"))?;
if chat_type != Chattype::Mailinglist {
bail!("receive_status_update: status sender {from_id} is not a member of chat {chat_id}")
bail!("receive_status_update: status sender not chat member.")
}
}
@@ -707,7 +705,7 @@ 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: &mut async_zip::read::fs::ZipFileReader, 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?;
@@ -750,10 +748,10 @@ impl Message {
name
};
let archive = self.get_webxdc_archive(context).await?;
let mut archive = self.get_webxdc_archive(context).await?;
if name == "index.html" {
if let Ok(bytes) = get_blob(&archive, "manifest.toml").await {
if let Ok(bytes) = get_blob(&mut archive, "manifest.toml").await {
if let Ok(manifest) = parse_webxdc_manifest(&bytes) {
if let Some(min_api) = manifest.min_api {
if min_api > WEBXDC_API_VERSION {
@@ -766,15 +764,15 @@ impl Message {
}
}
get_blob(&archive, name).await
get_blob(&mut archive, name).await
}
/// Return info from manifest.toml or from fallbacks.
pub async fn get_webxdc_info(&self, context: &Context) -> Result<WebxdcInfo> {
ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
let archive = self.get_webxdc_archive(context).await?;
let mut archive = self.get_webxdc_archive(context).await?;
let mut manifest = get_blob(&archive, "manifest.toml")
let mut manifest = get_blob(&mut archive, "manifest.toml")
.await
.map(|bytes| parse_webxdc_manifest(&bytes).unwrap_or_default())
.unwrap_or_default();
@@ -1088,7 +1086,7 @@ mod tests {
.await?;
let instance = t.get_last_msg().await;
assert_eq!(instance.viewtype, Viewtype::Webxdc);
assert_eq!(instance.get_filename().unwrap(), "minimal.xdc");
assert_eq!(instance.get_filename(), Some("minimal.xdc".to_string()));
receive_imf(
&t,
@@ -1098,7 +1096,7 @@ mod tests {
.await?;
let instance = t.get_last_msg().await;
assert_eq!(instance.viewtype, Viewtype::File); // we require the correct extension, only a mime type is not sufficient
assert_eq!(instance.get_filename().unwrap(), "index.html");
assert_eq!(instance.get_filename(), Some("index.html".to_string()));
Ok(())
}
@@ -1786,9 +1784,10 @@ mod tests {
// bob receives the instance together with the initial updates in a single message
let bob_instance = bob.recv_msg(&sent1).await;
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
assert_eq!(bob_instance.get_filename().unwrap(), "minimal.xdc");
assert_eq!(bob_instance.get_filename(), Some("minimal.xdc".to_string()));
assert!(sent1.payload().contains("Content-Type: application/json"));
assert!(sent1.payload().contains("status-update.json"));
assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#));
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
@@ -2568,42 +2567,4 @@ sth_for_the = "future""#
);
Ok(())
}
/// Tests extensibility of WebXDC updates.
///
/// If an update sent by WebXDC contains unknown properties,
/// such as `aNewUnknownProperty` or a reserved property
/// like `serial` or `max_serial`,
/// they are silently dropped and are not sent over the wire.
///
/// This ensures new WebXDC can try to send new properties
/// added in later revisions of the WebXDC API
/// and this will not result in a failure to send the whole update.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_webxdc_status_update_extensibility() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await;
alice
.send_webxdc_status_update(
alice_instance.id,
r#"{"payload":"p","info":"i","aNewUnknownProperty":"x","max_serial":123}"#,
"Some description",
)
.await?;
alice.flush_status_updates().await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":"p","info":"i","serial":1,"max_serial":1}]"#
);
Ok(())
}
}

View File

@@ -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): Heyho from DC [FRESH]
Msg#12: Me (Contact#Contact#Self): One classical MUA message √
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
--------------------------------------------------------------------------------

View File

@@ -0,0 +1,12 @@
Single#Chat#10: Bob [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: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#12: (Contact#Contact#10): Message from Thunderbird [FRESH]
Msg#13: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#14🔒: (Contact#Contact#10): Hello from DC [FRESH]
Msg#15: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#16: (Contact#Contact#10): Message from Thunderbird [FRESH]
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#18🔒: (Contact#Contact#10): Hello from DC [FRESH]
--------------------------------------------------------------------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,76 +0,0 @@
X-Mozilla-Status: 0801
X-Mozilla-Status2: 10000000
Content-Type: multipart/mixed; boundary="------------L1v4sF5IlAZ0HirXymXElgpK"
Message-ID: <1e3b3bb0-f34f-71e2-6b86-bce80bef2c6f@example.org>
Date: Thu, 3 Aug 2023 13:31:01 -0300
MIME-Version: 1.0
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
Thunderbird/102.13.0
Content-Language: en-US
To: bob@example.net
From: Alice <alice@example.org>
X-Identity-Key: id3
Fcc: imap://alice%40example.org@in.example.org/Sent
This is a multi-part message in MIME format.
--------------L1v4sF5IlAZ0HirXymXElgpK
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
--------------L1v4sF5IlAZ0HirXymXElgpK
Content-Type: application/octet-stream; name="rusty_deltachat_logo.jpeg"
Content-Disposition: attachment; filename="rusty_deltachat_logo.jpeg"
Content-Transfer-Encoding: base64
/9j/4AAQSkZJRgABAQIAzADMAAD/2wBDAP//////////////////////////////////////
////////////////////////////////////////////////2wBDAf//////////////////
////////////////////////////////////////////////////////////////////wAAR
CAEAAQADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA
AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK
FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG
h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl
5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA
AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk
NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE
hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk
5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCSiiigAooooAKKKKACiiigAooprHAzQA6i
mhgfY0McA0AAYGnVX6U/efagA3Hdnt6VL15qvS5OMZ4oAUt82R26f596lByM1BSgkcA0APZ+
eO3X3qSq9PV8DBoAl6UVCzZ+lPQ8fSgB9FFICD0oAWiiigAooooAKKKKACiiigAooooAKKKK
ACiiigApCcDNLTH6UAKGB9j6UjkYx3qKigAooooAKKKKACiiigAooooAKKKKAClBI6UlFACk
k9akTofrUVKCR0oAnoqEuT7fSnp0P1oAfRRRQAUUUUAFFFFABRRRQAUUUUAFIwyDTd496QuM
cUAMBI6GkJJ60UUAFFFFABRSqMmpgAOlAEYQnrxTtg96fSEgdTQAmxfT+dLtX0FJvX1o3r6/
zoAXavoKTYvp/OlBB6GloAZsHvTSh7HNS0UAQEEdRSVYqNk7j8qAI6KKKACnq2ODTKKAJS47
CnDkA1AOtWKACiiigAooooAKKKKACimscDOM03zPb9f/AK1ADXGD9eabSk5OTSUAFFFFABRR
RQBMowB7806kXoPpQeh+hoAjZ+w/OmUUUAFFFFABTw/r+dMooAsdaKhDEfT0qUEHkUALRRRQ
BG69x+NR1O33T9KgoAKKKKACnBiBim0UASKSTyeKkpiYwfWn0AFFFFABRRTWbb2zQA4jPFV6
eX46Y/GmUAFFFFABRRRQAUUUUASIe35VJVepVbPB6/zoAay45HT+VMqxTCgPTg0ARUUpBHWk
oAKKKKAClBI6UlFAEocd+KXevr+hqGigBzNn6U2iigAooooAKKKKAAEjpTgWJxk02pExk+tA
ElFFFABSEZGKCQOtNLjtzQBF0ooooAKKKKACiiigAooooAKKKKAJFfsfz/xqSq9OViPcUATY
z1qMp6flTwQelLQBX6UVOQD1qMoR05/nQAyiiigAooooAKKKKACiiigAooooAKAcdKKKAHBm
+tTUxAMZ70+gBCARioSCDzU+cdahY5PHagBtFFFABRRRQAUUUUAFFFFABRRRQAUUdelPCHvx
QA0EjpUqsD9fSkCD3NOCgdqAFooooAaVB+vrURUj/Gp6KAK9FSMncfl/hUdABRRRQAUUUUAF
FFFABRRRQA4MR0p6tnjHNRgZOKmCgUARsp69R/KmVYqA9Tj1oASiiigAooooAKKKKACiilAJ
4FACVIE9fypwUD6+tOoAQADpS0UhIHJoAWkLAdTUZcnpwKZQBIXHYUB+eelR0UAWKKiRux/C
paACmMueR1/nT6KAK9FSsueR1/nUVABRRRQAUUUUAFFFFACg4OalDA+31qGpFTufyoAazE8d
B6U2pyAetQsMEigBKKKKACiiigAooooABzxU4GBTEHf8qkoAKKKKADpUDHJ/lT3Pb8TUdABR
RRQAUUUUAFTKcj3FQ05Tg/pQBNRRRQAVE4wc9j/OpaQjIIoAgooooAKKKKACiiigBRwQanqv
T03Z9vegB7Nj61DUxUGmMuKAGUUUUAFFFFABRRSr1H1oAmAwAKWiigAooooAgY5JpKKKACii
igAooooAKKKKAJwcgGlpifdp9ABRRRQBCwwx/Om09+o+lMoAKKKKACiiigCRB1qSoAcHNTA5
GaAFqJ2zwPxqWo9h9aAI6KUjBwaSgAooooAKcv3hTaUdR9aAJ6KKKACkPQ/Q0tFAFeiiigAo
oooAKKKKACiiigCVOh+v9BT6an3adQAUUUUARP1H0plOc5Y/lTaACiiigApVGSBSUdOaAJ8D
0FLSA5GaWgAoooPTjrQBE5yfpTKfsb2ppBBwaAEooooAKKKKAJ1OQDS1Ehwcev8AOpaACiii
gCAjBP1pKe45z60ygAooooAKKKKACiinIMn2FAEoGABS0UUAFITgZpaidsnHYUAMooooAKKK
KACpti+n6moalVs8Hr/OgBwGOBS0UUAFFFFABUb9hUlRlCSTkUAR0U8oQM9aZQAUUUUAFSq2
eD1/nUVFAFiimK2eD1/nT6AEIyMVCQQcGp6QgHrQBBRTyh7c03afQ0AJRS7T6GnBD34oAaAS
cCpgMDFAAHSloAKKKjZ+w/P/AAoAGbsPxqOiigAooooAKKKKAFUZOKlCgf41EDg5FSBwevFA
D6KKKACiiigAooooAQnAJqCpyMjFIEUds/WgCGinuoHI/KmUAFFFFABTw5HXn+dMooAnBB6G
lqvTgxHegCaiot59BS+Z7frQBJRUfme1JvPsKAJaaXA96iJJ6mkoAcWJ+npTaKKACiiigAoo
ooAKKKKACpVXHJ61FUocYGetAD6KQEHoaWgAooooAKKKKACiiigBj9B9aaEJ68VLRQBAQR1p
KlfoPrTApNADaKUgjrSUAFFFFABRRRQAUUUUAFFFFABRRSkEdRQAlFKBkgVKUGOOKAIaKXBz
ipto9BQBBRQRg4p+w4z39KAGUUUUASouOe5/lT6QHIzS0AFFFFABRRRQAUUUUAFFFFACEA9a
WiigBr/d+lRqufpU1FAEbJgZFR1YqNU557dKAG7W9KbVioip3YHegBoBPQUlTgYGKay55HX+
dADFXdSshHPWnqMD3p1AESdfwqWkAA6d6WgBgQA5H5U+iigBMDOe9LRRQAm0ZzS0UUARFTu9
jzmpNoxjFLRQAgGOlLRRQB//2Q==
--------------L1v4sF5IlAZ0HirXymXElgpK--