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
45 changed files with 1312 additions and 786 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.70.0
RUSTUP_TOOLCHAIN: 1.71.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -80,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,5 +1,11 @@
# Changelog
## Unreleased
### Fixes
- deltachat-rpc-client: fix bug in client.py and add missing `EventType.MSG_DELETED`
## [1.118.0] - 2023-07-07
### API-Changes

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.

287
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",
@@ -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",
@@ -584,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",
]
@@ -713,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",
@@ -770,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"
@@ -796,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"
@@ -830,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",
]
@@ -977,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"
@@ -1025,7 +1015,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1258,7 +1248,7 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1458,7 +1448,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1486,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"
@@ -1617,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",
]
@@ -1758,7 +1748,7 @@ dependencies = [
"num-traits",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -1776,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"
@@ -1876,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",
]
@@ -2055,7 +2045,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -2567,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",
]
@@ -2587,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"
@@ -3005,7 +2995,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -3119,7 +3109,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -3167,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"
@@ -3259,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"
@@ -3364,7 +3345,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -3477,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",
@@ -3512,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",
]
@@ -3567,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",
]
@@ -3680,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",
]
@@ -3817,7 +3796,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b"
dependencies = [
"pem",
"ring",
"time 0.3.22",
"time 0.3.23",
"yasna",
]
@@ -3858,7 +3837,7 @@ checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.2",
"regex-syntax 0.7.4",
]
[[package]]
@@ -3878,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"
@@ -4068,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",
@@ -4082,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",
@@ -4127,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"
@@ -4156,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"
@@ -4220,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"
@@ -4250,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",
@@ -4293,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",
]
@@ -4320,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]]
@@ -4351,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",
@@ -4362,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",
@@ -4502,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"
@@ -4648,7 +4627,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -4670,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",
@@ -4734,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"
@@ -4748,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",
]
@@ -4788,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]]
@@ -4829,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",
@@ -4847,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",
]
@@ -4917,7 +4896,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -4973,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",
@@ -4999,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",
@@ -5020,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",
@@ -5080,7 +5059,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]
[[package]]
@@ -5185,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",
@@ -5219,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",
@@ -5229,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",
@@ -5255,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"
@@ -5327,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",
@@ -5417,7 +5396,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
"wasm-bindgen-shared",
]
@@ -5451,7 +5430,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -5721,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",
]
@@ -5773,7 +5752,7 @@ dependencies = [
"oid-registry",
"rusticata-macros",
"thiserror",
"time 0.3.22",
"time 0.3.23",
]
[[package]]
@@ -5797,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]]
@@ -5853,5 +5832,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.26",
]

View File

@@ -3,7 +3,7 @@ name = "deltachat"
version = "1.118.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
rust-version = "1.66"
[profile.dev]
debug = 0
@@ -118,11 +118,6 @@ members = [
"format-flowed",
]
[[example]]
name = "simple"
path = "examples/simple.rs"
[[bench]]
name = "create_account"
harness = false

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().
@@ -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

@@ -1709,6 +1709,20 @@ impl CommandApi {
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
/// Resend messages and make information available for newly added chat members.
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
/// Clients that already have the original message can still ignore the resent message as
/// they have tracked the state by dedicated updates.
///
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
///
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
chat::resend_msgs(&ctx, &message_ids).await
}
async fn send_sticker(
&self,
account_id: u32,

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

@@ -45,6 +45,7 @@ class EventType(str, Enum):
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"

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

@@ -9,7 +9,7 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.70.0
export RUSTUP_TOOLCHAIN=1.71.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e

View File

@@ -8,7 +8,7 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.70.0
export RUSTUP_TOOLCHAIN=1.71.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e

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?,
@@ -2100,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) ...
@@ -2202,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);
}
@@ -2850,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?;
@@ -2873,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)
@@ -5131,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;

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

@@ -131,7 +131,7 @@ async fn on_configure_completed(
) -> Result<()> {
if let Some(provider) = param.provider {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
for def in config_defaults {
if !context.config_exists(def.key).await? {
info!(context, "apply config_defaults {}={}", def.key, def.value);
context.set_config(def.key, Some(def.value)).await?;
@@ -656,7 +656,7 @@ async fn try_smtp_one_param(
})
} else {
info!(context, "success: {}", inf);
smtp.disconnect().await;
smtp.disconnect();
Ok(())
}
}

View File

@@ -234,7 +234,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
let res = moz_ac
.incoming_servers
.into_iter()
.chain(moz_ac.outgoing_servers.into_iter())
.chain(moz_ac.outgoing_servers)
.filter_map(|server| {
let protocol = match server.typ.as_ref() {
"imap" => Some(Protocol::Imap),

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()
}

View File

@@ -291,7 +291,7 @@ mod tests {
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -299,7 +299,7 @@ mod tests {
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"##
"#
);
}
@@ -310,7 +310,7 @@ This message does not have Content-Type nor Subject.<br/>
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -318,7 +318,7 @@ This message does not have Content-Type nor Subject.<br/>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"##
"#
);
}
@@ -330,7 +330,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
assert!(parser.plain.unwrap().flowed);
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -341,7 +341,7 @@ This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"##
"#
);
}
@@ -352,7 +352,7 @@ and will be wrapped as usual.<br/>
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -363,7 +363,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
<br/>
<br/>
</body></html>
"##
"#
);
}

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,

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

@@ -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()));

View File

@@ -1892,7 +1892,7 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
} 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
@@ -3090,7 +3090,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn parse_outlook_html_embedded_image() {
let context = TestContext::new_alice().await;
let raw = br##"From: Anonymous <anonymous@example.org>
let raw = br#"From: Anonymous <anonymous@example.org>
To: Anonymous <anonymous@example.org>
Subject: Delta Chat is great stuff!
Date: Tue, 5 May 2020 01:23:45 +0000
@@ -3145,7 +3145,7 @@ K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
------=_NextPart_000_0003_01D622B3.CA753E60--
"##;
"#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
@@ -3520,7 +3520,7 @@ On 2020-10-25, Bob wrote:
// A message with a long Message-ID.
// Long message-IDs are generated by Mailjet.
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
To: Bob <bob@example.org>
@@ -3528,11 +3528,11 @@ From: Alice <alice@example.org>
Subject: ...
Some quote.
"###;
";
receive_imf(&t, raw, false).await?;
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
let raw = br###"In-Reply-To:
let raw = br"In-Reply-To:
<ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
@@ -3544,7 +3544,7 @@ Subject: ...
> Some quote.
Some reply
"###;
";
receive_imf(&t, raw, false).await?;
@@ -3562,7 +3562,7 @@ Some reply
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <foobarbaz@example.org>
To: Bob <bob@example.org>
@@ -3571,7 +3571,7 @@ Subject: subject
Chat-Disposition-Notification-To: alice@example.org
Message.
"###;
";
// Bob receives message.
receive_imf(&bob, raw, false).await?;

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

@@ -26,10 +26,10 @@ impl PlainText {
/// The function handles quotes, links, fixed and floating text paragraphs.
pub fn to_html(&self) -> String {
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r#"\b([\w.\-+]+@[\w.\-]+)\b"#).unwrap());
Lazy::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
static LINKIFY_URL_RE: Lazy<regex::Regex> = Lazy::new(|| {
regex::Regex::new(r#"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)"#).unwrap()
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let lines = split_lines(&self.text);
@@ -127,7 +127,7 @@ http://link-at-start-of-line.org
.to_html();
assert_eq!(
html,
r##"<!DOCTYPE html>
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -138,7 +138,7 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
<br/>
</body></html>
"##
"#
);
}

View File

@@ -223,7 +223,7 @@ pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'sta
}
if let Ok(mx_domains) = resolver.mx_lookup(fqdn).await {
for (provider_domain, provider) in PROVIDER_DATA.iter() {
for (provider_domain, provider) in &*PROVIDER_DATA {
if provider.id != "gmail" {
// MX lookup is limited to Gmail for security reasons
continue;

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;
@@ -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.
@@ -2345,7 +2354,7 @@ async fn add_or_lookup_contacts_by_address_list(
origin: Origin,
) -> Result<Vec<ContactId>> {
let mut contact_ids = HashSet::new();
for info in address_list.iter() {
for info in address_list {
let addr = &info.addr;
if !may_be_valid_addr(addr) {
continue;

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

@@ -55,7 +55,7 @@ impl Smtp {
}
/// Disconnect the SMTP transport and drop it entirely.
pub async fn disconnect(&mut self) {
pub fn disconnect(&mut self) {
if let Some(mut transport) = self.transport.take() {
// Closing connection with a QUIT command may take some time, especially if it's a
// stale connection and an attempt to send the command times out. Send a command in a
@@ -88,7 +88,7 @@ impl Smtp {
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
if self.has_maybe_stale_connection() {
info!(context, "Closing stale connection");
self.disconnect().await;
self.disconnect();
}
if self.is_connected() {
@@ -465,13 +465,13 @@ pub(crate) async fn smtp_send(
// this clears last_success info
info!(context, "Failed to send message over SMTP, disconnecting");
smtp.disconnect().await;
smtp.disconnect();
res
}
Err(crate::smtp::send::Error::Envelope(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
smtp.disconnect();
warn!(context, "SMTP job is invalid: {}", err);
SendResult::Failure(err)
}
@@ -483,7 +483,7 @@ pub(crate) async fn smtp_send(
}
Err(crate::smtp::send::Error::Other(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
smtp.disconnect();
warn!(context, "unable to load job: {}", err);
SendResult::Failure(err)
}

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

@@ -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

@@ -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]
--------------------------------------------------------------------------------