mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
Compare commits
136 Commits
r10s/can-e
...
v1.158.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3efd94914c | ||
|
|
99a6756d28 | ||
|
|
3310315865 | ||
|
|
a7729e3548 | ||
|
|
dc2e4df286 | ||
|
|
386b91a9a7 | ||
|
|
d4847206cf | ||
|
|
7624a50cb1 | ||
|
|
568c044a90 | ||
|
|
a8f8d34c25 | ||
|
|
a308766e47 | ||
|
|
0df86b6308 | ||
|
|
e951a697ec | ||
|
|
1ebaa2a718 | ||
|
|
6cb6daaab2 | ||
|
|
d25fb4770c | ||
|
|
e4e738ec5f | ||
|
|
8a5a67d6f2 | ||
|
|
ee68b9c7ba | ||
|
|
a51b2fa751 | ||
|
|
4c4646e72c | ||
|
|
2ca866b644 | ||
|
|
ed7dfd6b65 | ||
|
|
de79cd1583 | ||
|
|
0e84cfd8ad | ||
|
|
8a9e60afc3 | ||
|
|
b5fa6553af | ||
|
|
5280448cd3 | ||
|
|
891e166996 | ||
|
|
df24532503 | ||
|
|
b82fa19c6f | ||
|
|
8cb136ab9d | ||
|
|
73095bcaff | ||
|
|
ea5f778cc0 | ||
|
|
14a7e39625 | ||
|
|
4a2bfe03da | ||
|
|
8fd972a2f9 | ||
|
|
5d334ee6ee | ||
|
|
dc17f2692c | ||
|
|
94187f7ee1 | ||
|
|
fa7bf179fb | ||
|
|
9bca0b3b90 | ||
|
|
4c93feeddb | ||
|
|
3d061d1dbd | ||
|
|
156f9642fe | ||
|
|
ef008d4ca0 | ||
|
|
0931d9326e | ||
|
|
65ea456bd8 | ||
|
|
7f55613607 | ||
|
|
03b0185b8e | ||
|
|
1fa9707317 | ||
|
|
e10f95b3ea | ||
|
|
82f61035d4 | ||
|
|
4ec20ab9dc | ||
|
|
296d2aa7f4 | ||
|
|
10e711621c | ||
|
|
1e3c894827 | ||
|
|
da4f1b2a98 | ||
|
|
51bbdadfad | ||
|
|
339f695bd6 | ||
|
|
f8c4662c9a | ||
|
|
c825b2584b | ||
|
|
c12c4f64c4 | ||
|
|
b5acbaa31c | ||
|
|
b5de5d0dc0 | ||
|
|
fa4de8f72e | ||
|
|
3b3d5767b0 | ||
|
|
e5a3eae531 | ||
|
|
10633531e5 | ||
|
|
d69db8f336 | ||
|
|
491d6abe49 | ||
|
|
8e9c79061f | ||
|
|
94f57e786d | ||
|
|
db1a7f6084 | ||
|
|
25df14707e | ||
|
|
26672900d5 | ||
|
|
82573dc78c | ||
|
|
35d4eb5168 | ||
|
|
b6d4d10025 | ||
|
|
53fa9ebf11 | ||
|
|
287829d385 | ||
|
|
58b7efe006 | ||
|
|
d2e1e57890 | ||
|
|
6a29cca349 | ||
|
|
c51f7a4249 | ||
|
|
71dfcaa81c | ||
|
|
8e25639126 | ||
|
|
c4e6823396 | ||
|
|
8e5f4a2d53 | ||
|
|
dd6e3973d2 | ||
|
|
33b9a582f3 | ||
|
|
0913b6707b | ||
|
|
476224b980 | ||
|
|
b699ac1aca | ||
|
|
97d8bd89bf | ||
|
|
9a915b2a95 | ||
|
|
d6209e08e6 | ||
|
|
b9acd603a5 | ||
|
|
df61905455 | ||
|
|
71582304f3 | ||
|
|
c6b6967fec | ||
|
|
9b4e49e979 | ||
|
|
e3dac9abbb | ||
|
|
1bc97385b9 | ||
|
|
ffb903092a | ||
|
|
490171650a | ||
|
|
586aae690c | ||
|
|
ba0a7f1f0b | ||
|
|
a50b43598f | ||
|
|
02a18420e5 | ||
|
|
ef6c5870bb | ||
|
|
efbc4780f2 | ||
|
|
5c49706dfd | ||
|
|
36e6b2306b | ||
|
|
4942303c19 | ||
|
|
2168e39156 | ||
|
|
2ee83bf786 | ||
|
|
43a40b9349 | ||
|
|
43a3e40bc7 | ||
|
|
33f96d4010 | ||
|
|
b5e9a5ebb6 | ||
|
|
44f72e7f85 | ||
|
|
08be98f693 | ||
|
|
483f4eaa17 | ||
|
|
8c2207d15e | ||
|
|
3b51e22b2e | ||
|
|
ae1bc54b69 | ||
|
|
a9fbdafda5 | ||
|
|
c58f6107ba | ||
|
|
a4e478a071 | ||
|
|
8ffdd55f79 | ||
|
|
9f67d0f905 | ||
|
|
c5cf16f32a | ||
|
|
3df693a1bb | ||
|
|
1cabca34db | ||
|
|
7b3a1b88e6 |
4
.github/workflows/deltachat-rpc-server.yml
vendored
4
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -248,6 +248,10 @@ jobs:
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win32-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-source
|
||||
cp result/*.tar.gz dist/
|
||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||
|
||||
3
.github/workflows/jsonrpc.yml
vendored
3
.github/workflows/jsonrpc.yml
vendored
@@ -37,9 +37,6 @@ jobs:
|
||||
run: npm run test
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
- name: make sure websocket server version still builds
|
||||
working-directory: deltachat-jsonrpc
|
||||
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||
- name: Run linter
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run prettier:check
|
||||
|
||||
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
@@ -55,7 +55,9 @@ jobs:
|
||||
- deltachat-rpc-server-aarch64-linux
|
||||
- deltachat-rpc-server-aarch64-linux-wheel
|
||||
- deltachat-rpc-server-arm64-v8a-android
|
||||
- deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
- deltachat-rpc-server-armeabi-v7a-android
|
||||
- deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
- deltachat-rpc-server-armv6l-linux
|
||||
- deltachat-rpc-server-armv6l-linux-wheel
|
||||
- deltachat-rpc-server-armv7l-linux
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,9 @@
|
||||
/target
|
||||
target/
|
||||
**/*.rs.bk
|
||||
/build
|
||||
/dist
|
||||
/fuzz/fuzz_targets/corpus/
|
||||
/fuzz/fuzz_targets/crashes/
|
||||
|
||||
# ignore vi temporaries
|
||||
*~
|
||||
|
||||
1207
CHANGELOG.md
1207
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
## Bug reports
|
||||
|
||||
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
||||
If you found a bug, [report it on GitHub](https://github.com/chatmail/core/issues).
|
||||
If the bug you found is specific to
|
||||
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||
@@ -67,7 +67,7 @@ If you want to contribute a code, follow this guide.
|
||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||
```
|
||||
|
||||
4. [**Open a Pull Request**](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||
4. [**Open a Pull Request**](https://github.com/chatmail/core/pulls).
|
||||
|
||||
Refer to the corresponding issue.
|
||||
|
||||
@@ -116,7 +116,7 @@ For other ways to contribute, refer to the [website](https://delta.chat/en/contr
|
||||
|
||||
You can find the list of good first issues
|
||||
and a link to this guide
|
||||
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||
on the contributing page: <https://github.com/chatmail/core/contribute>
|
||||
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||
[git-cliff]: https://git-cliff.org/
|
||||
|
||||
922
Cargo.lock
generated
922
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.155.6"
|
||||
version = "1.158.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.81"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -41,7 +41,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.10.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -50,25 +50,24 @@ brotli = { version = "7", default-features=false, features = ["std"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
data-encoding = "2.7.0"
|
||||
encoded-words = "0.2"
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.10"
|
||||
fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "=0.25.0-alpha.4"
|
||||
hickory-resolver = "=0.25.0-alpha.5"
|
||||
http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.10"
|
||||
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.32", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.32", default-features = false }
|
||||
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.33", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", branch = "main", default-features = false }
|
||||
mailparse = "0.16"
|
||||
mail-builder = { version = "0.4.2", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
@@ -94,14 +93,14 @@ serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
smallvec = "1.14.0"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-rustls = { version = "0.26.1", default-features = false }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = { workspace = true }
|
||||
@@ -110,7 +109,7 @@ toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.8"
|
||||
blake3 = "1.5.5"
|
||||
blake3 = "1.6.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -135,6 +134,7 @@ members = [
|
||||
"deltachat-time",
|
||||
"format-flowed",
|
||||
"deltachat-contact-tools",
|
||||
"fuzz",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
@@ -174,7 +174,7 @@ harness = false
|
||||
anyhow = "1"
|
||||
async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.39", default-features = false }
|
||||
chrono = { version = "0.4.40", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
@@ -182,6 +182,7 @@ futures = "0.3.31"
|
||||
futures-lite = "2.6.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.46"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.20.2"
|
||||
|
||||
42
README.md
42
README.md
@@ -1,19 +1,41 @@
|
||||
<p align="center">
|
||||
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
||||
<img alt="Chatmail logo" src="https://github.com/user-attachments/assets/25742da7-a837-48cd-a503-b303af55f10d" width="300" style="float:middle;" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||
<a href="https://github.com/chatmail/core/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/chatmail/core/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
||||
<a href="https://deps.rs/repo/github/chatmail/core">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/chatmail/core/status.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
The core library for Delta Chat, written in Rust
|
||||
</p>
|
||||
The chatmail core library implements low-level network and encryption protocols,
|
||||
integrated by many chat bots and higher level applications,
|
||||
allowing to securely participate in the globally scaled e-mail server network.
|
||||
We provide reproducibly-built `deltachat-rpc-server` static binaries
|
||||
that offer a stdio-based high-level JSON-RPC API for instant messaging purposes.
|
||||
|
||||
The following protocols are handled without requiring API users to know much about them:
|
||||
|
||||
- secure TLS setup with DNS caching and shadowsocks/proxy support
|
||||
|
||||
- robust [SMTP](https://github.com/chatmail/async-imap)
|
||||
and [IMAP](https://github.com/chatmail/async-smtp) handling
|
||||
|
||||
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
|
||||
and [MIME building](https://github.com/stalwartlabs/mail-builder).
|
||||
|
||||
- security-audited end-to-end encryption with [rPGP](https://github.com/rpgp/rpgp)
|
||||
and [Autocrypt and SecureJoin protocols](https://securejoin.rtfd.io)
|
||||
|
||||
- ephemeral [Peer-to-Peer networking using Iroh](https://iroh.computer) for multi-device setup and
|
||||
[webxdc realtime data](https://delta.chat/en/2024-11-20-webxdc-realtime).
|
||||
|
||||
- a simulation- and real-world tested [P2P group membership
|
||||
protocol without requiring server state](https://github.com/chatmail/models/tree/main/group-membership).
|
||||
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
@@ -104,7 +126,7 @@ For more commands type:
|
||||
## Installing libdeltachat system wide
|
||||
|
||||
```
|
||||
$ git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||
$ git clone https://github.com/chatmail/core.git
|
||||
$ cd deltachat-core-rust
|
||||
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
||||
$ cmake --build build
|
||||
@@ -139,7 +161,7 @@ $ cargo test -- --ignored
|
||||
|
||||
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
|
||||
```sh
|
||||
$ cargo install cargo-bolero
|
||||
$ cargo install cargo-bolero@0.8.0
|
||||
```
|
||||
|
||||
Run fuzzing tests with
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||
1. Resolve all [blocker issues](https://github.com/chatmail/core/labels/blocker).
|
||||
|
||||
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
3. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
||||
`[1.116.0]: https://github.com/chatmail/core/compare/v1.115.2...v1.116.0`
|
||||
|
||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ filter_unconventional = false
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/chatmail/core/pull/${2}))"}, # replace pull request / issue numbers
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
@@ -82,11 +82,11 @@ footer = """
|
||||
{% if release.version -%}
|
||||
{% if release.previous.version -%}
|
||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||
https://github.com/deltachat/deltachat-core-rust\
|
||||
https://github.com/chatmail/core\
|
||||
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
||||
[unreleased]: https://github.com/chatmail/core\
|
||||
/compare/{{ release.previous.version }}..HEAD
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.155.6"
|
||||
version = "1.158.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -220,7 +220,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
* - Strings in function arguments or return values are usually UTF-8 encoded.
|
||||
*
|
||||
* - The issue-tracker for the core library is here:
|
||||
* <https://github.com/deltachat/deltachat-core-rust/issues>
|
||||
* <https://github.com/chatmail/core/issues>
|
||||
*
|
||||
* If you need further assistance,
|
||||
* please do not hesitate to contact us
|
||||
@@ -440,17 +440,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts (default).
|
||||
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
||||
* generate recommended key type (default),
|
||||
* DC_KEY_GEN_RSA2048 (1)=
|
||||
* generate RSA 2048 keypair
|
||||
* DC_KEY_GEN_ED25519 (2)=
|
||||
* generate Curve25519 keypair
|
||||
* DC_KEY_GEN_RSA4096 (3)=
|
||||
* generate RSA 4096 keypair
|
||||
* - `save_mime_headers` = 1=save mime headers
|
||||
* and make dc_get_mime_headers() work for subsequent calls,
|
||||
* 0=do not save mime headers (default)
|
||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
@@ -1039,20 +1028,6 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
|
||||
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a message can be edited using dc_send_edit_request().
|
||||
*
|
||||
* Messages that cannot be edited are eg. info messages or messages not sent by self.
|
||||
* UI will usually check this function whether to display an "Edit" option or not.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID to check.
|
||||
* @return 1=message can be edited, 0=message cannot be edited.
|
||||
*/
|
||||
int dc_can_send_edit_request (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Send chat members a request to edit the given message's text.
|
||||
*
|
||||
@@ -1070,6 +1045,21 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
void dc_send_edit_request (dc_context_t* context, uint32_t msg_id, const char* new_text);
|
||||
|
||||
|
||||
/**
|
||||
* Send chat members a request to delete the given messages.
|
||||
*
|
||||
* Only outgoing messages can be deleted this way
|
||||
* and all messages must be in the same chat.
|
||||
* No tombstone or sth. like that is left.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_ids An array of uint32_t containing all message IDs to delete.
|
||||
* @param msg_cnt The number of messages IDs in the msg_ids array.
|
||||
*/
|
||||
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Send invitation to a videochat.
|
||||
*
|
||||
@@ -1960,24 +1950,7 @@ void dc_download_full_msg (dc_context_t* context, int msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Get the raw mime-headers of the given message.
|
||||
* Raw headers are saved for incoming messages
|
||||
* only if `dc_set_config(context, "save_mime_headers", "1")`
|
||||
* was called before.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID, must be the ID of an incoming message.
|
||||
* @return Raw headers as a multi-line string, must be released using dc_str_unref() after usage.
|
||||
* Returns NULL if there are no headers saved for the given message,
|
||||
* e.g. because of save_mime_headers is not set
|
||||
* or the message is not incoming.
|
||||
*/
|
||||
char* dc_get_mime_headers (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Delete messages. The messages are deleted on the current device and
|
||||
* Delete messages. The messages are deleted on all devices and
|
||||
* on the IMAP server.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2490,8 +2463,9 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP 251
|
||||
#define DC_QR_BACKUP 251 // deprecated
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
@@ -2538,11 +2512,14 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to create an account on the given domain,
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* - DC_QR_BACKUP:
|
||||
* - DC_QR_BACKUP2:
|
||||
* ask the user if they want to set up a new device.
|
||||
* If so, pass the qr-code to dc_receive_backup().
|
||||
*
|
||||
* - DC_QR_BACKUP_TOO_NEW:
|
||||
* show a hint to the user that this backup comes from a newer Delta Chat version
|
||||
* and this device needs an update
|
||||
*
|
||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
@@ -6309,6 +6286,18 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
|
||||
|
||||
|
||||
/**
|
||||
* Chat was deleted.
|
||||
* This event is emitted in response to dc_delete_chat()
|
||||
* called on this or another device.
|
||||
* The event is a good place to remove notifications or homescreen shortcuts.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) 0
|
||||
*/
|
||||
#define DC_EVENT_CHAT_DELETED 2023
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
*
|
||||
@@ -6549,15 +6538,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_MEDIA_QUALITY_WORSE 1
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("key_gen_type")
|
||||
*/
|
||||
#define DC_KEY_GEN_DEFAULT 0
|
||||
#define DC_KEY_GEN_RSA2048 1
|
||||
#define DC_KEY_GEN_ED25519 2
|
||||
#define DC_KEY_GEN_RSA4096 3
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
|
||||
*
|
||||
|
||||
@@ -544,6 +544,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::MsgDeleted { .. } => 2016,
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ChatDeleted { .. } => 2023,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
EventType::LocationChanged(_) => 2035,
|
||||
EventType::ConfigureProgress { .. } => 2041,
|
||||
@@ -610,7 +611,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgRead { chat_id, .. }
|
||||
| EventType::MsgDeleted { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. }
|
||||
| EventType::ChatDeleted { chat_id } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id.to_u32() as libc::c_int
|
||||
@@ -676,6 +678,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
@@ -767,6 +770,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged
|
||||
@@ -1041,22 +1045,6 @@ pub unsafe extern "C" fn dc_send_text_msg(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_can_send_edit_request(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_can_send_edit_request()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(chat::can_send_edit_request(ctx, MsgId::new(msg_id)))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_edit_request(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1074,6 +1062,25 @@ pub unsafe extern "C" fn dc_send_edit_request(
|
||||
.unwrap_or_log_default(ctx, "Failed to send text edit")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_delete_request(
|
||||
context: *mut dc_context_t,
|
||||
msg_ids: *const u32,
|
||||
msg_cnt: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
|
||||
eprintln!("ignoring careless call to dc_send_delete_request()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
block_on(message::delete_msgs_ex(ctx, &msg_ids, true))
|
||||
.context("failed dc_send_delete_request() call")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1651,6 +1658,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let context: Context = ctx.clone();
|
||||
|
||||
block_on(async move {
|
||||
match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
|
||||
@@ -1946,28 +1954,6 @@ pub unsafe extern "C" fn dc_get_msg_html(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_mime_headers(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_mime_headers()");
|
||||
return ptr::null_mut(); // NULL explicitly defined as "no mime headers"
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let mime = message::get_mime_headers(ctx, MsgId::new(msg_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get mime headers");
|
||||
if mime.is_empty() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
mime.strdup()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_delete_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2997,7 +2983,7 @@ pub unsafe extern "C" fn dc_chatlist_get_context(
|
||||
/// context, but the Rust API does not, so the FFI layer needs to glue
|
||||
/// these together.
|
||||
pub struct ChatWrapper {
|
||||
context: *const dc_context_t,
|
||||
context: Context,
|
||||
chat: chat::Chat,
|
||||
}
|
||||
|
||||
@@ -3064,14 +3050,13 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
|
||||
return ptr::null_mut(); // NULL explicitly defined as "no image"
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(async move {
|
||||
match ffi_chat.chat.get_profile_image(ctx).await {
|
||||
match ffi_chat.chat.get_profile_image(&ffi_chat.context).await {
|
||||
Ok(Some(p)) => p.to_string_lossy().strdup(),
|
||||
Ok(None) => ptr::null_mut(),
|
||||
Err(err) => {
|
||||
error!(ctx, "failed to get profile image: {err:#}");
|
||||
error!(ffi_chat.context, "failed to get profile image: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
@@ -3085,9 +3070,9 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(ffi_chat.chat.get_color(ctx)).unwrap_or_log_default(ctx, "Failed get_color")
|
||||
block_on(ffi_chat.chat.get_color(&ffi_chat.context))
|
||||
.unwrap_or_log_default(&ffi_chat.context, "Failed get_color")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3151,10 +3136,9 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
block_on(ffi_chat.chat.can_send(ctx))
|
||||
block_on(ffi_chat.chat.can_send(&ffi_chat.context))
|
||||
.context("can_send failed")
|
||||
.log_err(ctx)
|
||||
.log_err(&ffi_chat.context)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::BackupTooNew { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||
@@ -103,6 +104,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Proxy { .. } => LotState::QrProxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
@@ -129,6 +131,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::BackupTooNew { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
@@ -178,10 +181,10 @@ pub enum LotState {
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
QrBackup = 251,
|
||||
|
||||
QrBackup2 = 252,
|
||||
|
||||
QrBackupTooNew = 255,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.155.6"
|
||||
version = "1.158.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-jsonrpc-server"
|
||||
path = "src/webserver.rs"
|
||||
required-features = ["webserver"]
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true }
|
||||
deltachat-contact-tools = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
schemars = "0.8.21"
|
||||
schemars = "0.8.22"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tempfile = { workspace = true }
|
||||
log = { workspace = true }
|
||||
@@ -31,15 +25,10 @@ sanitize-filename = { workspace = true }
|
||||
walkdir = "2.5.0"
|
||||
base64 = { workspace = true }
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.11.6", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
webserver = ["dep:env_logger", "dep:axum", "tokio/full", "yerpc/support-axum"]
|
||||
vendored = ["deltachat/vendored"]
|
||||
|
||||
@@ -4,46 +4,16 @@ This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) inte
|
||||
|
||||
The JSON-RPC API is exposed in two fashions:
|
||||
|
||||
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
|
||||
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
||||
* A executable `deltachat-rpc-server` that exposes the JSON-RPC API through stdio.
|
||||
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). It exposes the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
||||
|
||||
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
|
||||
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder.
|
||||
|
||||
## Usage
|
||||
|
||||
#### Running the WebSocket server
|
||||
|
||||
From within this folder, you can start the WebSocket server with the following command:
|
||||
|
||||
```sh
|
||||
cargo run --features webserver
|
||||
```
|
||||
|
||||
If you want to use the server in a production setup, first build it in release mode:
|
||||
|
||||
```sh
|
||||
cargo build --features webserver --release
|
||||
```
|
||||
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
|
||||
|
||||
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
|
||||
|
||||
The server can be configured with environment variables:
|
||||
|
||||
|variable|default|description|
|
||||
|-|-|-|
|
||||
|`DC_PORT`|`20808`|port to listen on|
|
||||
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
|
||||
|
||||
If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
|
||||
|
||||
```sh
|
||||
cross build --features=webserver --target armv7-linux-androideabi --release
|
||||
```
|
||||
|
||||
#### Using the TypeScript/JavaScript client
|
||||
|
||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
|
||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/chatmail/yerpc)). Find the source in the [`typescript`](typescript) folder.
|
||||
|
||||
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
||||
```sh
|
||||
@@ -52,15 +22,7 @@ npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
|
||||
|
||||
|
||||
```typescript
|
||||
import { DeltaChat } from './deltachat.bundle.js'
|
||||
const dc = new DeltaChat('ws://localhost:20808/ws')
|
||||
const accounts = await dc.rpc.getAllAccounts()
|
||||
console.log('accounts', accounts)
|
||||
```
|
||||
The JavaScript client is [published on NPM](https://www.npmjs.com/package/@deltachat/jsonrpc-client).
|
||||
|
||||
A script is included to build autogenerated documentation, which includes all RPC methods:
|
||||
```sh
|
||||
@@ -73,18 +35,6 @@ Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
|
||||
|
||||
#### Running the example app
|
||||
|
||||
We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this.
|
||||
|
||||
```sh
|
||||
cd typescript
|
||||
npm run build
|
||||
npm run example:build
|
||||
npm run example:start
|
||||
```
|
||||
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
|
||||
|
||||
Run `npm run example:dev` to live-rebuild the example app when files changes.
|
||||
|
||||
### Testing
|
||||
|
||||
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
|
||||
@@ -104,14 +54,12 @@ cd typescript
|
||||
npm run test
|
||||
```
|
||||
|
||||
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
|
||||
This will build the `deltachat-jsonrpc-server` binary and then run a test suite.
|
||||
|
||||
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
||||
|
||||
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
||||
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
||||
|
||||
```
|
||||
CHATMAIL_DOMAIN=chat.example.org npm run test
|
||||
CHATMAIL_DOMAIN=ci-chatmail.testrun.org npm run test
|
||||
```
|
||||
|
||||
#### Test Coverage
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# TODO
|
||||
|
||||
- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer
|
||||
|
||||
## MVP - Websocket server&client
|
||||
|
||||
For kaiOS and other experiments, like a deltachat "web" over network from an android phone.
|
||||
|
||||
- [ ] coverage for a majority of the API
|
||||
- [ ] Blobs served
|
||||
- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on)
|
||||
- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node
|
||||
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
|
||||
|
||||
### Other Ideas for the Websocket server
|
||||
|
||||
- [ ] make sure there can only be one connection at a time to the ws
|
||||
- why? , it could give problems if its commanded from multiple connections
|
||||
- [ ] encrypted connection?
|
||||
- [ ] authenticated connection?
|
||||
- [ ] Look into unit-testing for the proc macros?
|
||||
- [ ] proc macro taking over doc comments to generated typescript file
|
||||
|
||||
## Desktop Apis
|
||||
|
||||
Incomplete todo for desktop api porting, just some remainders for points that might need more work:
|
||||
|
||||
- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -7,6 +7,7 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
@@ -21,7 +22,7 @@ use deltachat::ephemeral::Timer;
|
||||
use deltachat::location;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
use deltachat::message::{
|
||||
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
@@ -38,6 +39,7 @@ use deltachat::{imex, info};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
@@ -341,11 +343,19 @@ impl CommandApi {
|
||||
ctx.get_info().await
|
||||
}
|
||||
|
||||
/// Get the blob dir.
|
||||
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
||||
}
|
||||
|
||||
/// Copy file to blob dir.
|
||||
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let file = Path::new(&path);
|
||||
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
|
||||
}
|
||||
|
||||
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.draft_self_report().await?.to_u32())
|
||||
@@ -422,6 +432,9 @@ impl CommandApi {
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
/// Setup the credential config before calling this.
|
||||
///
|
||||
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
|
||||
/// or `add_transport()` instead.
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
@@ -436,6 +449,69 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configures a new email account using the provided parameters
|
||||
/// and adds it as a transport.
|
||||
///
|
||||
/// If the email address is the same as an existing transport,
|
||||
/// then this existing account will be reconfigured instead of a new one being added.
|
||||
///
|
||||
/// This function stops and starts IO as needed.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `password`,
|
||||
/// and all the other settings will be autoconfigured.
|
||||
///
|
||||
/// During configuration, ConfigureProgress events are emitted;
|
||||
/// they indicate a successful configuration as well as errors
|
||||
/// and may be used to create a progress bar.
|
||||
/// This function will return after configuration is finished.
|
||||
///
|
||||
/// If configuration is successful,
|
||||
/// the working server parameters will be saved
|
||||
/// and used for connecting to the server.
|
||||
/// The parameters entered by the user will be saved separately
|
||||
/// so that they can be prefilled when the user opens the server-configuration screen again.
|
||||
///
|
||||
/// See also:
|
||||
/// - [Self::is_configured()] to check whether there is
|
||||
/// at least one working transport.
|
||||
/// - [Self::add_transport_from_qr()] to add a transport
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.add_transport(¶m.try_into()?).await
|
||||
}
|
||||
|
||||
/// Adds a new email account as a transport
|
||||
/// using the server encoded in the QR code.
|
||||
/// See [Self::add_transport].
|
||||
async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.add_transport_from_qr(&qr).await
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let res = ctx
|
||||
.list_transports()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|t| t.into())
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.delete_transport(&addr).await
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1205,7 +1281,15 @@ impl CommandApi {
|
||||
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||
delete_msgs(&ctx, &msgs).await
|
||||
delete_msgs_ex(&ctx, &msgs, false).await
|
||||
}
|
||||
|
||||
/// Delete messages. The messages are deleted on the current device,
|
||||
/// on the IMAP server and also for all chat members
|
||||
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||
delete_msgs_ex(&ctx, &msgs, true).await
|
||||
}
|
||||
|
||||
/// Get an informational text for a single message. The text is multiline and may
|
||||
@@ -1453,6 +1537,7 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets display name for existing contact.
|
||||
async fn change_contact_name(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1461,9 +1546,7 @@ impl CommandApi {
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
let contact = Contact::get_by_id(&ctx, contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
Contact::create(&ctx, &name, addr).await?;
|
||||
contact_id.set_name(&ctx, &name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1518,6 +1601,18 @@ impl CommandApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Imports contacts from a vCard.
|
||||
///
|
||||
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
|
||||
async fn import_vcard_contents(&self, account_id: u32, vcard: String) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(deltachat::contact::import_vcard(&ctx, &vcard)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|c| c.to_u32())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns a vCard containing contacts with the given ids.
|
||||
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1851,13 +1946,9 @@ impl CommandApi {
|
||||
|
||||
/// Get href from a WebxdcInfoMessage which might include a hash holding
|
||||
/// information about a specific position or state in a webxdc app (optional)
|
||||
async fn get_webxdc_href(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
) -> Result<Option<String>> {
|
||||
async fn get_webxdc_href(&self, account_id: u32, info_msg_id: u32) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(info_msg_id)).await?;
|
||||
Ok(message.get_webxdc_href())
|
||||
}
|
||||
|
||||
@@ -2004,6 +2095,16 @@ impl CommandApi {
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
async fn send_edit_request(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
new_text: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::send_edit_request(&ctx, MsgId::new(msg_id), new_text).await
|
||||
}
|
||||
|
||||
/// Checks if messages can be sent to a given chat.
|
||||
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -243,6 +243,12 @@ pub enum EventType {
|
||||
timer: u32,
|
||||
},
|
||||
|
||||
/// Chat deleted.
|
||||
ChatDeleted {
|
||||
/// Chat ID.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged {
|
||||
@@ -499,6 +505,9 @@ impl From<CoreEventType> for EventType {
|
||||
timer: timer.to_u32(),
|
||||
}
|
||||
}
|
||||
CoreEventType::ChatDeleted { chat_id } => ChatDeleted {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
||||
contact_id: contact.map(|c| c.to_u32()),
|
||||
},
|
||||
|
||||
201
deltachat-jsonrpc/src/api/types/login_param.rs
Normal file
201
deltachat-jsonrpc/src/api/types/login_param.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::login_param as dc;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use yerpc::TypeDef;
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnteredLoginParam {
|
||||
/// Email address.
|
||||
pub addr: String,
|
||||
|
||||
/// Password.
|
||||
pub password: String,
|
||||
|
||||
/// Imap server hostname or IP address.
|
||||
pub imap_server: Option<String>,
|
||||
|
||||
/// Imap server port.
|
||||
pub imap_port: Option<u16>,
|
||||
|
||||
/// Imap socket security.
|
||||
pub imap_security: Option<Socket>,
|
||||
|
||||
/// Imap username.
|
||||
pub imap_user: Option<String>,
|
||||
|
||||
/// SMTP server hostname or IP address.
|
||||
pub smtp_server: Option<String>,
|
||||
|
||||
/// SMTP server port.
|
||||
pub smtp_port: Option<u16>,
|
||||
|
||||
/// SMTP socket security.
|
||||
pub smtp_security: Option<Socket>,
|
||||
|
||||
/// SMTP username.
|
||||
pub smtp_user: Option<String>,
|
||||
|
||||
/// SMTP Password.
|
||||
///
|
||||
/// Only needs to be specified if different than IMAP password.
|
||||
pub smtp_password: Option<String>,
|
||||
|
||||
/// TLS options: whether to allow invalid certificates and/or
|
||||
/// invalid hostnames.
|
||||
/// Default: Automatic
|
||||
pub certificate_checks: Option<EnteredCertificateChecks>,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore).
|
||||
/// Default: false
|
||||
pub oauth2: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||
let imap_security: Socket = param.imap.security.into();
|
||||
let smtp_security: Socket = param.smtp.security.into();
|
||||
let certificate_checks: EnteredCertificateChecks = param.certificate_checks.into();
|
||||
Self {
|
||||
addr: param.addr,
|
||||
password: param.imap.password,
|
||||
imap_server: param.imap.server.into_option(),
|
||||
imap_port: param.imap.port.into_option(),
|
||||
imap_security: imap_security.into_option(),
|
||||
imap_user: param.imap.user.into_option(),
|
||||
smtp_server: param.smtp.server.into_option(),
|
||||
smtp_port: param.smtp.port.into_option(),
|
||||
smtp_security: smtp_security.into_option(),
|
||||
smtp_user: param.smtp.user.into_option(),
|
||||
smtp_password: param.smtp.password.into_option(),
|
||||
certificate_checks: certificate_checks.into_option(),
|
||||
oauth2: param.oauth2.into_option(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
||||
Ok(Self {
|
||||
addr: param.addr,
|
||||
imap: dc::EnteredServerLoginParam {
|
||||
server: param.imap_server.unwrap_or_default(),
|
||||
port: param.imap_port.unwrap_or_default(),
|
||||
security: param.imap_security.unwrap_or_default().into(),
|
||||
user: param.imap_user.unwrap_or_default(),
|
||||
password: param.password,
|
||||
},
|
||||
smtp: dc::EnteredServerLoginParam {
|
||||
server: param.smtp_server.unwrap_or_default(),
|
||||
port: param.smtp_port.unwrap_or_default(),
|
||||
security: param.smtp_security.unwrap_or_default().into(),
|
||||
user: param.smtp_user.unwrap_or_default(),
|
||||
password: param.smtp_password.unwrap_or_default(),
|
||||
},
|
||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||
oauth2: param.oauth2.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Socket {
|
||||
/// Unspecified socket security, select automatically.
|
||||
#[default]
|
||||
Automatic,
|
||||
|
||||
/// TLS connection.
|
||||
Ssl,
|
||||
|
||||
/// STARTTLS connection.
|
||||
Starttls,
|
||||
|
||||
/// No TLS, plaintext connection.
|
||||
Plain,
|
||||
}
|
||||
|
||||
impl From<dc::Socket> for Socket {
|
||||
fn from(value: dc::Socket) -> Self {
|
||||
match value {
|
||||
dc::Socket::Automatic => Self::Automatic,
|
||||
dc::Socket::Ssl => Self::Ssl,
|
||||
dc::Socket::Starttls => Self::Starttls,
|
||||
dc::Socket::Plain => Self::Plain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Socket> for dc::Socket {
|
||||
fn from(value: Socket) -> Self {
|
||||
match value {
|
||||
Socket::Automatic => Self::Automatic,
|
||||
Socket::Ssl => Self::Ssl,
|
||||
Socket::Starttls => Self::Starttls,
|
||||
Socket::Plain => Self::Plain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EnteredCertificateChecks {
|
||||
/// `Automatic` means that provider database setting should be taken.
|
||||
/// If there is no provider database setting for certificate checks,
|
||||
/// check certificates strictly.
|
||||
#[default]
|
||||
Automatic,
|
||||
|
||||
/// Ensure that TLS certificate is valid for the server hostname.
|
||||
Strict,
|
||||
|
||||
/// Accept certificates that are expired, self-signed
|
||||
/// or otherwise not valid for the server hostname.
|
||||
AcceptInvalidCertificates,
|
||||
}
|
||||
|
||||
impl From<dc::EnteredCertificateChecks> for EnteredCertificateChecks {
|
||||
fn from(value: dc::EnteredCertificateChecks) -> Self {
|
||||
match value {
|
||||
dc::EnteredCertificateChecks::Automatic => Self::Automatic,
|
||||
dc::EnteredCertificateChecks::Strict => Self::Strict,
|
||||
dc::EnteredCertificateChecks::AcceptInvalidCertificates => {
|
||||
Self::AcceptInvalidCertificates
|
||||
}
|
||||
dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => {
|
||||
Self::AcceptInvalidCertificates
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EnteredCertificateChecks> for dc::EnteredCertificateChecks {
|
||||
fn from(value: EnteredCertificateChecks) -> Self {
|
||||
match value {
|
||||
EnteredCertificateChecks::Automatic => Self::Automatic,
|
||||
EnteredCertificateChecks::Strict => Self::Strict,
|
||||
EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoOption<T> {
|
||||
fn into_option(self) -> Option<T>;
|
||||
}
|
||||
impl<T> IntoOption<T> for T
|
||||
where
|
||||
T: Default + std::cmp::PartialEq,
|
||||
{
|
||||
fn into_option(self) -> Option<T> {
|
||||
if self == T::default() {
|
||||
None
|
||||
} else {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,7 +673,6 @@ pub struct MessageReadReceipt {
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageInfo {
|
||||
rawtext: String,
|
||||
ephemeral_timer: EphemeralTimer,
|
||||
/// When message is ephemeral this contains the timestamp of the message expiry
|
||||
ephemeral_timestamp: Option<i64>,
|
||||
@@ -686,7 +685,6 @@ pub struct MessageInfo {
|
||||
impl MessageInfo {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
let rawtext = msg_id.rawtext(context).await?;
|
||||
let ephemeral_timer = message.get_ephemeral_timer().into();
|
||||
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
||||
deltachat::ephemeral::Timer::Disabled => None,
|
||||
@@ -699,7 +697,6 @@ impl MessageInfo {
|
||||
let hop_info = msg_id.hop_info(context).await?;
|
||||
|
||||
Ok(Self {
|
||||
rawtext,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp,
|
||||
error: message.error(),
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod contact;
|
||||
pub mod events;
|
||||
pub mod http;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
pub mod provider_info;
|
||||
pub mod qr;
|
||||
|
||||
@@ -63,6 +63,7 @@ pub enum QrObject {
|
||||
/// Iroh node address.
|
||||
node_addr: String,
|
||||
},
|
||||
BackupTooNew {},
|
||||
/// Ask the user if they want to use the given service for video chats.
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
@@ -100,11 +101,15 @@ pub enum QrObject {
|
||||
/// URL scanned.
|
||||
///
|
||||
/// Ask the user if they want to open a browser or copy the URL to clipboard.
|
||||
Url { url: String },
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
/// Text scanned.
|
||||
///
|
||||
/// Ask the user if they want to copy the text to clipboard.
|
||||
Text { text: String },
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
/// Ask the user if they want to withdraw their own QR code.
|
||||
WithdrawVerifyContact {
|
||||
/// Contact ID.
|
||||
@@ -160,7 +165,9 @@ pub enum QrObject {
|
||||
/// `dclogin:` scheme parameters.
|
||||
///
|
||||
/// Ask the user if they want to login with the email address.
|
||||
Login { address: String },
|
||||
Login {
|
||||
address: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
@@ -217,6 +224,7 @@ impl From<Qr> for QrObject {
|
||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||
auth_token,
|
||||
},
|
||||
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
||||
use yerpc::axum::handle_ws_rpc;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
mod api;
|
||||
use api::{Accounts, CommandApi};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20808;
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string());
|
||||
let port = std::env::var("DC_PORT")
|
||||
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/ws", get(handler))
|
||||
.layer(Extension(state.clone()));
|
||||
|
||||
tokio::spawn(async move {
|
||||
state.accounts.write().await.start_io().await;
|
||||
});
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||
let (client, out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), api.clone());
|
||||
handle_ws_rpc(ws, out_receiver, session).await
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>DeltaChat JSON-RPC example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
background: black;
|
||||
color: grey;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-template-areas: "a a" "b c";
|
||||
}
|
||||
.message {
|
||||
color: red;
|
||||
}
|
||||
#header {
|
||||
grid-area: a;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
#header a {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
#main {
|
||||
grid-area: b;
|
||||
color: green;
|
||||
}
|
||||
#main h2,
|
||||
#main h3 {
|
||||
color: blue;
|
||||
}
|
||||
#side {
|
||||
grid-area: c;
|
||||
color: #777;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="dist/example.bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DeltaChat JSON-RPC example</h1>
|
||||
<div class="grid">
|
||||
<div id="header"></div>
|
||||
<div id="main"></div>
|
||||
<div id="side"><h2>log</h2></div>
|
||||
</div>
|
||||
<p>
|
||||
Tip: open the dev console and use the client with
|
||||
<code>window.client</code>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,109 +0,0 @@
|
||||
import { DcEvent, DeltaChat } from "../deltachat.js";
|
||||
|
||||
var SELECTED_ACCOUNT = 0;
|
||||
|
||||
window.addEventListener("DOMContentLoaded", (_event) => {
|
||||
(window as any).selectDeltaAccount = (id: string) => {
|
||||
SELECTED_ACCOUNT = Number(id);
|
||||
window.dispatchEvent(new Event("account-changed"));
|
||||
};
|
||||
console.log("launch run script...");
|
||||
run().catch((err) => console.error("run failed", err));
|
||||
});
|
||||
|
||||
async function run() {
|
||||
const $main = document.getElementById("main")!;
|
||||
const $side = document.getElementById("side")!;
|
||||
const $head = document.getElementById("header")!;
|
||||
|
||||
const client = new DeltaChat("ws://localhost:20808/ws");
|
||||
|
||||
(window as any).client = client.rpc;
|
||||
|
||||
client.on("ALL", (accountId, event) => {
|
||||
onIncomingEvent(accountId, event);
|
||||
});
|
||||
|
||||
window.addEventListener("account-changed", async (_event: Event) => {
|
||||
listChatsForSelectedAccount();
|
||||
});
|
||||
|
||||
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
|
||||
|
||||
async function loadAccountsInHeader() {
|
||||
console.log("load accounts");
|
||||
const accounts = await client.rpc.getAllAccounts();
|
||||
console.log("accounts loaded", accounts);
|
||||
for (const account of accounts) {
|
||||
if (account.kind === "Configured") {
|
||||
write(
|
||||
$head,
|
||||
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
||||
${account.id}: ${account.addr!}
|
||||
</a> `
|
||||
);
|
||||
} else {
|
||||
write(
|
||||
$head,
|
||||
`<a href="#">
|
||||
${account.id}: (unconfigured)
|
||||
</a> `
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listChatsForSelectedAccount() {
|
||||
clear($main);
|
||||
const selectedAccount = SELECTED_ACCOUNT;
|
||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||
if (info.kind !== "Configured") {
|
||||
return write($main, "Account is not configured");
|
||||
}
|
||||
write($main, `<h2>${info.addr!}</h2>`);
|
||||
const chats = await client.rpc.getChatlistEntries(
|
||||
selectedAccount,
|
||||
0,
|
||||
null,
|
||||
null
|
||||
);
|
||||
for (const chatId of chats) {
|
||||
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
||||
write($main, `<h3>${chat.name}</h3>`);
|
||||
const messageIds = await client.rpc.getMessageIds(
|
||||
selectedAccount,
|
||||
chatId,
|
||||
false,
|
||||
false
|
||||
);
|
||||
const messages = await client.rpc.getMessages(
|
||||
selectedAccount,
|
||||
messageIds
|
||||
);
|
||||
for (const [_messageId, message] of Object.entries(messages)) {
|
||||
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
||||
else write($main, `<p>loading error: ${message.error}</p>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onIncomingEvent(accountId: number, event: DcEvent) {
|
||||
write(
|
||||
$side,
|
||||
`
|
||||
<p class="message">
|
||||
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(
|
||||
Object.assign({}, event, { kind: undefined })
|
||||
)}
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function write(el: HTMLElement, html: string) {
|
||||
el.innerHTML += html;
|
||||
}
|
||||
function clear(el: HTMLElement) {
|
||||
el.innerHTML = "";
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { DeltaChat } from "../dist/deltachat.js";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const delta = new DeltaChat("ws://localhost:20808/ws");
|
||||
delta.on("event", (event) => {
|
||||
console.log("event", event.data);
|
||||
});
|
||||
|
||||
const email = process.argv[2];
|
||||
const password = process.argv[3];
|
||||
if (!email || !password)
|
||||
throw new Error(
|
||||
"USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>"
|
||||
);
|
||||
console.log(`creating account for ${email}`);
|
||||
const id = await delta.rpc.addAccount();
|
||||
console.log(`created account id ${id}`);
|
||||
await delta.rpc.setConfig(id, "addr", email);
|
||||
await delta.rpc.setConfig(id, "mail_pw", password);
|
||||
console.log("configuration updated");
|
||||
await delta.rpc.configure(id);
|
||||
console.log("account configured!");
|
||||
|
||||
const accounts = await delta.rpc.getAllAccounts();
|
||||
console.log("accounts", accounts);
|
||||
console.log("waiting for events...");
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { DeltaChat } from "../dist/deltachat.js";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const delta = new DeltaChat();
|
||||
delta.on("event", (event) => {
|
||||
console.log("event", event.data);
|
||||
});
|
||||
|
||||
const accounts = await delta.rpc.getAllAccounts();
|
||||
console.log("accounts", accounts);
|
||||
console.log("waiting for events...");
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
"url": "https://github.com/chatmail/core.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
||||
@@ -42,10 +42,6 @@
|
||||
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
|
||||
"build:tsc": "tsc",
|
||||
"docs": "typedoc --out docs deltachat.ts",
|
||||
"example": "run-s build example:build example:start",
|
||||
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
|
||||
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
||||
"example:start": "http-server .",
|
||||
"extract-constants": "node ./scripts/generate-constants.js",
|
||||
"generate-bindings": "cargo test",
|
||||
"prettier:check": "prettier --check .",
|
||||
@@ -58,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.155.6"
|
||||
"version": "1.158.0"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as T from "../generated/types.js";
|
||||
import { EventType } from "../generated/types.js";
|
||||
import * as RPC from "../generated/jsonrpc.js";
|
||||
import { RawClient } from "../generated/client.js";
|
||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
|
||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
@@ -74,34 +74,6 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
export type Opts = {
|
||||
url: string;
|
||||
startEventLoop: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_OPTS: Opts = {
|
||||
url: "ws://localhost:20808/ws",
|
||||
startEventLoop: true,
|
||||
};
|
||||
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||
opts: Opts;
|
||||
close() {
|
||||
this.transport.close();
|
||||
}
|
||||
constructor(opts?: Opts | string) {
|
||||
if (typeof opts === "string") {
|
||||
opts = { ...DEFAULT_OPTS, url: opts };
|
||||
} else if (opts) {
|
||||
opts = { ...DEFAULT_OPTS, ...opts };
|
||||
} else {
|
||||
opts = { ...DEFAULT_OPTS };
|
||||
}
|
||||
const transport = new WebsocketTransport(opts.url);
|
||||
super(transport, opts.startEventLoop);
|
||||
this.opts = opts;
|
||||
}
|
||||
}
|
||||
|
||||
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||
close() {}
|
||||
constructor(input: any, output: any, startEventLoop: boolean) {
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
"noImplicitAny": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["*.ts", "example/*.ts", "test/*.ts"],
|
||||
"include": ["*.ts", "test/*.ts"],
|
||||
"compileOnSave": false
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.155.6"
|
||||
version = "1.158.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -92,7 +92,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
@@ -126,7 +126,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
real_spec = rs.unwrap();
|
||||
}
|
||||
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
||||
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
|
||||
if suffix == "eml" && poke_eml_file(context, Path::new(&real_spec)).await.is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
} else {
|
||||
@@ -140,7 +140,10 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
if name.ends_with(".eml") {
|
||||
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||
println!("Import: {path_plus_name}");
|
||||
if poke_eml_file(context, path_plus_name).await.is_ok() {
|
||||
if poke_eml_file(context, Path::new(&path_plus_name))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
read_cnt += 1
|
||||
}
|
||||
}
|
||||
@@ -1278,7 +1281,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
if let Ok(buf) = read_file(&context, &arg1).await {
|
||||
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.155.6"
|
||||
version = "1.158.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -70,3 +70,11 @@ line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"imap-tools",
|
||||
"pytest",
|
||||
"pytest-timeout",
|
||||
"pytest-xdist",
|
||||
]
|
||||
|
||||
@@ -26,9 +26,12 @@ class Account:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.manager.rpc
|
||||
|
||||
def wait_for_event(self) -> AttrDict:
|
||||
def wait_for_event(self, event_type=None) -> AttrDict:
|
||||
"""Wait until the next event and return it."""
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
while True:
|
||||
next_event = AttrDict(self._rpc.wait_for_event(self.id))
|
||||
if event_type is None or next_event.kind == event_type:
|
||||
return next_event
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
@@ -38,6 +41,16 @@ class Account:
|
||||
"""Remove the account."""
|
||||
self._rpc.remove_account(self.id)
|
||||
|
||||
def clone(self) -> "Account":
|
||||
"""Clone given account.
|
||||
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
|
||||
future = self._rpc.provide_backup.future(self.id)
|
||||
qr = self._rpc.get_backup_qr(self.id)
|
||||
new_account = self.manager.add_account()
|
||||
new_account._rpc.get_backup(new_account.id, qr)
|
||||
future()
|
||||
return new_account
|
||||
|
||||
def start_io(self) -> None:
|
||||
"""Start the account I/O."""
|
||||
self._rpc.start_io(self.id)
|
||||
@@ -83,6 +96,10 @@ class Account:
|
||||
return self.get_config("selfavatar")
|
||||
|
||||
def check_qr(self, qr):
|
||||
"""Parse QR code contents.
|
||||
|
||||
This function takes the raw text scanned
|
||||
and checks what can be done with it."""
|
||||
return self._rpc.check_qr(self.id, qr)
|
||||
|
||||
def set_config_from_qr(self, qr: str):
|
||||
@@ -96,12 +113,9 @@ class Account:
|
||||
def bring_online(self):
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
self.start_io()
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
@@ -109,19 +123,41 @@ class Account:
|
||||
with that e-mail address, it is unblocked and its display
|
||||
name is updated if specified.
|
||||
|
||||
:param obj: email-address or contact id.
|
||||
:param obj: email-address, contact id or account.
|
||||
:param name: (optional) display name for this contact.
|
||||
"""
|
||||
if isinstance(obj, Account):
|
||||
vcard = obj.self_contact.make_vcard()
|
||||
[contact] = self.import_vcard(vcard)
|
||||
if name:
|
||||
contact.set_name(name)
|
||||
return contact
|
||||
if isinstance(obj, int):
|
||||
obj = Contact(self, obj)
|
||||
if isinstance(obj, Contact):
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def make_vcard(self, contacts: list[Contact]) -> str:
|
||||
"""Create vCard with the given contacts."""
|
||||
assert all(contact.account == self for contact in contacts)
|
||||
contact_ids = [contact.id for contact in contacts]
|
||||
return self._rpc.make_vcard(self.id, contact_ids)
|
||||
|
||||
def import_vcard(self, vcard: str) -> list[Contact]:
|
||||
"""Import vCard.
|
||||
|
||||
Return created or modified contacts in the order they appear in vCard."""
|
||||
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
|
||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
addr = account.get_config("addr")
|
||||
contact = self.create_contact(addr)
|
||||
return contact.create_chat()
|
||||
"""Create a 1:1 chat with another account."""
|
||||
return self.create_contact(account).create_chat()
|
||||
|
||||
def get_device_chat(self) -> Chat:
|
||||
"""Return device chat."""
|
||||
return self.device_contact.create_chat()
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
@@ -183,6 +219,11 @@ class Account:
|
||||
"""This account's identity as a Contact."""
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
@property
|
||||
def device_contact(self) -> Chat:
|
||||
"""This account's device contact."""
|
||||
return Contact(self, SpecialContactId.DEVICE)
|
||||
|
||||
def get_chatlist(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
@@ -296,10 +337,15 @@ class Account:
|
||||
|
||||
def wait_for_incoming_msg_event(self):
|
||||
"""Wait for incoming message event and return it."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
return self.wait_for_event(EventType.INCOMING_MSG)
|
||||
|
||||
def wait_for_msgs_changed_event(self):
|
||||
"""Wait for messages changed event and return it."""
|
||||
return self.wait_for_event(EventType.MSGS_CHANGED)
|
||||
|
||||
def wait_for_msgs_noticed_event(self):
|
||||
"""Wait for messages noticed event and return it."""
|
||||
return self.wait_for_event(EventType.MSGS_NOTICED)
|
||||
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
@@ -320,10 +366,7 @@ class Account:
|
||||
break
|
||||
|
||||
def wait_for_reactions_changed(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.REACTIONS_CHANGED:
|
||||
return event
|
||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
@@ -352,3 +395,7 @@ class Account:
|
||||
"""Import keys."""
|
||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||
|
||||
def initiate_autocrypt_key_transfer(self) -> None:
|
||||
"""Send Autocrypt Setup Message."""
|
||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||
|
||||
@@ -66,4 +66,4 @@ class Contact:
|
||||
)
|
||||
|
||||
def make_vcard(self) -> str:
|
||||
return self._rpc.make_vcard(self.account.id, [self.id])
|
||||
return self.account.make_vcard([self])
|
||||
|
||||
@@ -52,6 +52,9 @@ class Message:
|
||||
"""Mark the message as seen."""
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
||||
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import random
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
@@ -11,14 +12,6 @@ from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
def get_temp_credentials() -> dict:
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
password = f"{username}${username}"
|
||||
addr = f"{username}@{domain}"
|
||||
return {"email": addr, "password": password}
|
||||
|
||||
|
||||
class ACFactory:
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
self.deltachat = deltachat
|
||||
@@ -31,26 +24,25 @@ class ACFactory:
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
def new_preconfigured_account(self) -> Account:
|
||||
"""Make a new account with configuration options set, but configuration not started."""
|
||||
credentials = get_temp_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
account.set_config("addr", credentials["email"])
|
||||
account.set_config("mail_pw", credentials["password"])
|
||||
assert not account.is_configured()
|
||||
return account
|
||||
def get_credentials(self) -> (str, str):
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
return f"{username}@{domain}", f"{username}${username}"
|
||||
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
account = self.new_preconfigured_account()
|
||||
yield account.configure.future()
|
||||
addr, password = self.get_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
params = {"addr": addr, "password": password}
|
||||
yield account._rpc.add_transport.future(account.id, params)
|
||||
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
def new_configured_bot(self) -> Bot:
|
||||
credentials = get_temp_credentials()
|
||||
addr, password = self.get_credentials()
|
||||
bot = self.get_unconfigured_bot()
|
||||
bot.configure(credentials["email"], credentials["password"])
|
||||
bot.configure(addr, password)
|
||||
return bot
|
||||
|
||||
@futuremethod
|
||||
@@ -124,3 +116,50 @@ def rpc(tmp_path) -> AsyncGenerator:
|
||||
@pytest.fixture
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
"""Test data."""
|
||||
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join("test-data")
|
||||
if datadir.isdir():
|
||||
self.path = datadir
|
||||
return
|
||||
raise Exception("Data path cannot be found")
|
||||
|
||||
def get_path(self, bn):
|
||||
"""return path of file or None if it doesn't exist."""
|
||||
fn = os.path.join(self.path, *bn.split("/"))
|
||||
assert os.path.exists(fn)
|
||||
return fn
|
||||
|
||||
def read_path(self, bn, mode="r"):
|
||||
fn = self.get_path(bn)
|
||||
if fn is not None:
|
||||
with open(fn, mode) as f:
|
||||
return f.read()
|
||||
return None
|
||||
|
||||
return Data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def log():
|
||||
"""Log printer fixture."""
|
||||
|
||||
class Printer:
|
||||
def section(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
@@ -13,10 +13,11 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
|
||||
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
||||
"""
|
||||
|
||||
account = acfactory.new_preconfigured_account()
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.clear_all_events()
|
||||
assert not account.is_configured()
|
||||
future = account.configure.future()
|
||||
future = account._rpc.add_transport.future(account.id, {"addr": addr, "password": password})
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
|
||||
|
||||
@@ -48,8 +48,7 @@ def test_delivery_status(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
alice.clear_all_events()
|
||||
@@ -119,8 +118,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
@@ -150,18 +148,13 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
bob.wait_for_incoming_msg_event()
|
||||
|
||||
alice_second_device: Account = acfactory.get_unconfigured_account()
|
||||
|
||||
alice._rpc.provide_backup.future(alice.id)
|
||||
backup_code = alice._rpc.get_backup_qr(alice.id)
|
||||
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
|
||||
alice_second_device = alice.clone()
|
||||
alice_second_device.start_io()
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
|
||||
@@ -175,17 +175,11 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
n = int(bytes(event.data).decode())
|
||||
break
|
||||
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||
n = int(bytes(event.data).decode())
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
break
|
||||
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
|
||||
|
||||
def test_no_reordering(acfactory, path_to_webxdc):
|
||||
@@ -229,8 +223,5 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
ac2_hello_msg_snapshot.chat.accept()
|
||||
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
while 1:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
break
|
||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
|
||||
53
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
53
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def wait_for_autocrypt_setup_message(account):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
|
||||
msg_id = event.msg_id
|
||||
msg = account.get_message_by_id(msg_id)
|
||||
if msg.get_snapshot().is_setupmessage:
|
||||
return msg
|
||||
|
||||
|
||||
def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.bring_online()
|
||||
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Test that entering wrong code returns an error.
|
||||
with pytest.raises(JsonRpcError):
|
||||
msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
|
||||
|
||||
def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.bring_online()
|
||||
|
||||
# Send the first Autocrypt Setup Message and ignore it.
|
||||
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Send the second Autocrypt Setup Message and import it.
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
@@ -4,6 +4,7 @@ import time
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
@@ -26,17 +27,21 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Test that if Bob changes the key, backwards verification is lost.
|
||||
# Test that if Bob imports a key,
|
||||
# backwards verification is not lost
|
||||
# because default key is not changed.
|
||||
logging.info("Bob 2 is created")
|
||||
bob2 = acfactory.new_configured_account()
|
||||
bob2.export_self_keys(tmp_path)
|
||||
|
||||
logging.info("Bob imports a key")
|
||||
bob.import_self_keys(tmp_path)
|
||||
logging.info("Bob tries to import a key")
|
||||
# Importing a second key is not allowed.
|
||||
with pytest.raises(JsonRpcError):
|
||||
bob.import_self_keys(tmp_path)
|
||||
|
||||
assert bob.get_config("key_id") == "2"
|
||||
assert bob.get_config("key_id") == "1"
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert not bob_contact_alice_snapshot.is_verified
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
@@ -55,15 +60,12 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
def test_qr_securejoin(acfactory, protect):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
alice.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
alice2 = alice.clone()
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group", protect=protect)
|
||||
@@ -74,17 +76,11 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
# Alice deletes "vg-request".
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
alice.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
|
||||
for ac in [alice, bob]:
|
||||
while True:
|
||||
event = ac.wait_for_event()
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
@@ -121,8 +117,7 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -159,11 +154,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
logging.info("Alice creates a verified group")
|
||||
group = alice.create_group("Group", protect=True)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
charlie_addr = charlie.get_config("addr")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.add_contact(alice_contact_charlie)
|
||||
@@ -190,7 +182,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
charlie_snapshot = charlie_message.get_snapshot()
|
||||
assert charlie_snapshot.text == "Hi from Bob!"
|
||||
|
||||
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
|
||||
bob_contact_charlie = bob.create_contact(charlie, "Charlie")
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
logging.info("Charlie reads Bob's message")
|
||||
@@ -461,12 +453,12 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="AEAP is disabled for now")
|
||||
def test_aeap_flow_verified(acfactory):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# ac1new is only used to get a new address.
|
||||
ac1new = acfactory.new_preconfigured_account()
|
||||
addr, password = acfactory.get_credentials()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
@@ -486,8 +478,8 @@ def test_aeap_flow_verified(acfactory):
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
logging.info("changing email account")
|
||||
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||
ac1.set_config("addr", addr)
|
||||
ac1.set_config("mail_pw", password)
|
||||
ac1.stop_io()
|
||||
ac1.configure()
|
||||
ac1.start_io()
|
||||
@@ -500,11 +492,9 @@ def test_aeap_flow_verified(acfactory):
|
||||
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||
assert msg_in_2_snapshot.text == msg_out.text
|
||||
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
|
||||
assert msg_in_2.get_sender_contact().get_snapshot().address == addr
|
||||
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
|
||||
assert ac1new.get_config("addr") in [
|
||||
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
|
||||
]
|
||||
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
||||
|
||||
|
||||
def test_gossip_verification(acfactory) -> None:
|
||||
@@ -520,9 +510,9 @@ def test_gossip_verification(acfactory) -> None:
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
|
||||
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
|
||||
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
|
||||
bob_contact_alice = bob.create_contact(alice, "Alice")
|
||||
bob_contact_carol = bob.create_contact(carol, "Carol")
|
||||
carol_contact_alice = carol.create_contact(alice, "Alice")
|
||||
|
||||
logging.info("Bob creates an Autocrypt group")
|
||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||
@@ -582,7 +572,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 is verified for ac2.
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
# ac1 resetups the account.
|
||||
@@ -597,7 +587,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
# header sent by old ac1.
|
||||
while True:
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2, "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
|
||||
@@ -653,12 +643,14 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||
|
||||
logging.info("Alice withdraws QR code.")
|
||||
|
||||
@@ -61,61 +61,83 @@ def test_acfactory(acfactory) -> None:
|
||||
|
||||
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
# Use STARTTLS
|
||||
account.set_config("mail_security", "2")
|
||||
account.set_config("send_security", "2")
|
||||
account.configure()
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
"imapSecurity": "starttls",
|
||||
"smtpSecurity": "starttls",
|
||||
},
|
||||
)
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
|
||||
|
||||
domain = account.get_config("addr").rsplit("@")[-1]
|
||||
ip_address = socket.gethostbyname(domain)
|
||||
|
||||
# This should fail TLS check.
|
||||
account.set_config("mail_server", ip_address)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.configure()
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
# This should fail TLS check.
|
||||
"imapServer": ip_address,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_configure_alternative_port(acfactory) -> None:
|
||||
"""Test that configuration with alternative port 443 works."""
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
account.set_config("mail_port", "443")
|
||||
account.set_config("send_port", "443")
|
||||
|
||||
account.configure()
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
"imapPort": 443,
|
||||
"smtpPort": 443,
|
||||
},
|
||||
)
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_configure_username(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
addr = account.get_config("addr")
|
||||
account.set_config("mail_user", addr)
|
||||
account.configure()
|
||||
|
||||
assert account.get_config("configured_mail_user") == addr
|
||||
def test_list_transports(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
"imapUser": addr,
|
||||
},
|
||||
)
|
||||
transports = account._rpc.list_transports(account.id)
|
||||
assert len(transports) == 1
|
||||
params = transports[0]
|
||||
assert params["addr"] == addr
|
||||
assert params["password"] == password
|
||||
assert params["imapUser"] == addr
|
||||
|
||||
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
@@ -174,8 +196,7 @@ def test_account(acfactory) -> None:
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -241,7 +262,7 @@ def test_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
assert repr(alice_contact_bob)
|
||||
@@ -258,8 +279,7 @@ def test_contact(acfactory) -> None:
|
||||
def test_message(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -287,16 +307,37 @@ def test_message(acfactory) -> None:
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
def test_reaction_seen_on_another_dev(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
log.section("Alice adds a second device")
|
||||
alice2 = alice.clone()
|
||||
|
||||
log.section("Second device goes online")
|
||||
alice2.start_io()
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
log.section("First device changes avatar")
|
||||
image = data.get_path("image/avatar1000x1000.jpg")
|
||||
alice.set_config("selfavatar", image)
|
||||
avatar_config = alice.get_config("selfavatar")
|
||||
avatar_hash = os.path.basename(avatar_config)
|
||||
print("Info: avatar hash is ", avatar_hash)
|
||||
|
||||
log.section("First device receives avatar change")
|
||||
alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
|
||||
avatar_config2 = alice2.get_config("selfavatar")
|
||||
avatar_hash2 = os.path.basename(avatar_config2)
|
||||
print("Info: avatar hash on second device is ", avatar_hash2)
|
||||
assert avatar_hash == avatar_hash2
|
||||
assert avatar_config != avatar_config2
|
||||
|
||||
|
||||
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -308,20 +349,12 @@ def test_reaction_seen_on_another_dev(acfactory, tmp_path) -> None:
|
||||
snapshot.chat.accept()
|
||||
message.send_reaction("😎")
|
||||
for a in [alice, alice2]:
|
||||
while True:
|
||||
event = a.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_REACTION:
|
||||
break
|
||||
a.wait_for_event(EventType.INCOMING_REACTION)
|
||||
|
||||
alice2.clear_all_events()
|
||||
alice_chat_bob.mark_noticed()
|
||||
while True:
|
||||
event = alice2.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
chat_id = event.chat_id
|
||||
break
|
||||
alice2_contact_bob = alice2.get_contact_by_addr(bob_addr)
|
||||
alice2_chat_bob = alice2_contact_bob.create_chat()
|
||||
chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
|
||||
alice2_chat_bob = alice2.create_chat(bob)
|
||||
assert chat_id == alice2_chat_bob.id
|
||||
|
||||
|
||||
@@ -329,24 +362,19 @@ def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
# Alice becomes a bot.
|
||||
alice.set_config("bot", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == event.chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert snapshot.is_bot
|
||||
break
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == event.chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert snapshot.is_bot
|
||||
|
||||
|
||||
def test_bot(acfactory) -> None:
|
||||
@@ -393,9 +421,11 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
bot = acfactory.new_preconfigured_account()
|
||||
addr, password = acfactory.get_credentials()
|
||||
bot = acfactory.get_unconfigured_account()
|
||||
bot.set_config("bot", "1")
|
||||
bot.configure()
|
||||
bot._rpc.add_transport(bot.id, {"addr": addr, "password": password})
|
||||
assert bot.is_configured()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not bot.wait_next_messages()
|
||||
@@ -404,8 +434,7 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||
|
||||
bot_addr = bot.get_config("addr")
|
||||
alice_contact_bot = alice.create_contact(bot_addr, "Bot")
|
||||
alice_contact_bot = alice.create_contact(bot, "Bot")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
@@ -429,9 +458,7 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
@@ -481,9 +508,7 @@ def test_provider_info(rpc) -> None:
|
||||
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
# Bob creates chat manually so chat with Alice is accepted.
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
@@ -507,10 +532,7 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
|
||||
# Alice reads Bob's message.
|
||||
message.mark_seen()
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.MSG_READ:
|
||||
break
|
||||
bob.wait_for_event(EventType.MSG_READ)
|
||||
|
||||
# Bob sends a message to Alice, it should also be encrypted.
|
||||
bob_chat_alice.send_text("Hi Alice!")
|
||||
@@ -582,9 +604,13 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_preconfigured_account()
|
||||
ac2.configure()
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2._rpc.add_transport(ac2.id, {"addr": addr, "password": password})
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
assert ac2.is_configured()
|
||||
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
@@ -628,9 +654,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
chat.send_text("Hello Alice!")
|
||||
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||
|
||||
contact_addr = account.get_config("addr")
|
||||
contact = alice.create_contact(contact_addr, "")
|
||||
|
||||
contact = alice.create_contact(account)
|
||||
alice_group.add_contact(contact)
|
||||
|
||||
if n_accounts == 2:
|
||||
@@ -661,7 +685,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
assert snapshot.chat == bob_chat_alice
|
||||
|
||||
|
||||
def test_markseen_contact_request(acfactory, tmp_path):
|
||||
def test_markseen_contact_request(acfactory):
|
||||
"""
|
||||
Test that seen status is synchronized for contact request messages
|
||||
even though read receipt is not sent.
|
||||
@@ -669,10 +693,7 @@ def test_markseen_contact_request(acfactory, tmp_path):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets up a second device.
|
||||
bob.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
bob2 = acfactory.get_unconfigured_account()
|
||||
bob2.import_backup(files[0])
|
||||
bob2 = bob.clone()
|
||||
bob2.start_io()
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
@@ -683,10 +704,7 @@ def test_markseen_contact_request(acfactory, tmp_path):
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
message.mark_seen()
|
||||
while True:
|
||||
event = bob2.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
break
|
||||
bob2.wait_for_event(EventType.MSGS_NOTICED)
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
@@ -716,3 +734,66 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert configured_certificate_checks != "0"
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.start_io()
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
|
||||
logging.info("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1.wait_for_incoming_msg_event()
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
|
||||
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone_chat.send_text("Hi back")
|
||||
ev = ac1.wait_for_msgs_noticed_event()
|
||||
|
||||
assert ev.chat_id == first_msg.get_snapshot().chat_id
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
|
||||
def test_rename_synchronization(acfactory):
|
||||
"""Test synchronization of contact renaming."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.bring_online()
|
||||
|
||||
bob.set_config("displayname", "Bob")
|
||||
bob.create_chat(alice).send_text("Hello!")
|
||||
alice_msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||
alice2_msg = alice2.wait_for_incoming_msg().get_snapshot()
|
||||
|
||||
assert alice2_msg.sender.get_snapshot().display_name == "Bob"
|
||||
alice_msg.sender.set_name("Bobby")
|
||||
alice2.wait_for_event(EventType.CONTACTS_CHANGED)
|
||||
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
|
||||
|
||||
|
||||
def test_rename_group(acfactory):
|
||||
"""Test renaming the group."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_group = alice.create_group("Test group")
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_group.add_contact(alice_contact_bob)
|
||||
alice_group.send_text("Hello!")
|
||||
|
||||
bob_msg = bob.wait_for_incoming_msg()
|
||||
bob_chat = bob_msg.get_snapshot().chat
|
||||
assert bob_chat.get_basic_snapshot().name == "Test group"
|
||||
|
||||
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||
alice_group.set_name(name)
|
||||
bob.wait_for_incoming_msg_event()
|
||||
assert bob_chat.get_basic_snapshot().name == name
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
def test_webxdc(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -9,12 +6,9 @@ def test_webxdc(acfactory) -> None:
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
break
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
|
||||
webxdc_info = message.get_webxdc_info()
|
||||
assert webxdc_info == {
|
||||
|
||||
@@ -12,11 +12,8 @@ setenv =
|
||||
RUST_MIN_STACK=8388608
|
||||
passenv =
|
||||
CHATMAIL_DOMAIN
|
||||
deps =
|
||||
pytest
|
||||
pytest-timeout
|
||||
pytest-xdist
|
||||
imap-tools
|
||||
dependency_groups =
|
||||
dev
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.155.6"
|
||||
version = "1.158.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -5,13 +5,13 @@ over standard I/O.
|
||||
|
||||
## Install
|
||||
|
||||
To download binary pre-builds check the [releases page](https://github.com/deltachat/deltachat-core-rust/releases).
|
||||
To download binary pre-builds check the [releases page](https://github.com/chatmail/core/releases).
|
||||
Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`.
|
||||
|
||||
To install from source run:
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server
|
||||
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||
```
|
||||
|
||||
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
"url": "https://github.com/chatmail/core.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "node scripts/update_optional_dependencies_and_version.js"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.155.6"
|
||||
"version": "1.158.0"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ def write_package_json(platform_path, rust_target, my_binary_name):
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git",
|
||||
"url": "https://github.com/chatmail/core.git",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ENV_VAR_NAME } from "./const.js";
|
||||
|
||||
const cargoInstallCommand =
|
||||
"cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server";
|
||||
"cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
|
||||
|
||||
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
|
||||
return `deltachat-rpc-server not found:
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn main() {
|
||||
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||
// until the user presses enter."
|
||||
if let Err(error) = &r {
|
||||
log::error!("Fatal error: {error:#}.")
|
||||
log::error!("Error: {error:#}.")
|
||||
}
|
||||
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||
}
|
||||
|
||||
27
deny.toml
27
deny.toml
@@ -10,8 +10,11 @@ ignore = [
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# DNSSEC validation that we don't use anyway.
|
||||
"RUSTSEC-2025-0006",
|
||||
# Unmaintained backoff
|
||||
"RUSTSEC-2025-0012",
|
||||
|
||||
# Unmaintained paste
|
||||
"RUSTSEC-2024-0436",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -27,24 +30,28 @@ skip = [
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "nix", version = "0.27.1" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rtnetlink", version = "0.13.1" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "tokio-tungstenite", version = "0.21.0" },
|
||||
{ name = "tungstenite", version = "0.21.0" },
|
||||
{ name = "unicode-width", version = "0.1.11" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
{ name = "windows_aarch64_msvc" },
|
||||
@@ -61,6 +68,7 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "zerocopy", version = "0.7.32" },
|
||||
]
|
||||
|
||||
|
||||
@@ -75,7 +83,6 @@ allow = [
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
@@ -87,9 +94,3 @@ expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[sources.allow-org]
|
||||
# Organisations which we allow git sources from.
|
||||
github = [
|
||||
"stalwartlabs",
|
||||
]
|
||||
|
||||
47
flake.nix
47
flake.nix
@@ -18,9 +18,9 @@
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest;
|
||||
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837";
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
@@ -30,6 +30,7 @@
|
||||
include = [
|
||||
./benches
|
||||
./assets
|
||||
./fuzz
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
./CMakeLists.txt
|
||||
@@ -87,9 +88,6 @@
|
||||
};
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"mail-builder-0.4.1" = "sha256-1hnsU76ProcX7iXT2UBjHnHbJ/ROT3077sLi3+yAV58=";
|
||||
};
|
||||
};
|
||||
mkRustPackage = packageName:
|
||||
naersk'.buildPackage {
|
||||
@@ -311,10 +309,41 @@
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch: {
|
||||
"deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
};
|
||||
mkAndroidPackages = arch:
|
||||
let
|
||||
rpc-server = mkAndroidRustPackage arch "deltachat-rpc-server";
|
||||
in
|
||||
{
|
||||
"deltachat-rpc-server-${arch}-android" = rpc-server;
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}-android-wheel" =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-android-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch}-android tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
let
|
||||
|
||||
6836
fuzz/Cargo.lock
generated
6836
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,28 +3,21 @@ name = "deltachat-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.8"
|
||||
|
||||
[dependencies]
|
||||
mailparse = "0.13"
|
||||
mailparse = { workspace = true }
|
||||
deltachat = { path = ".." }
|
||||
format-flowed = { path = "../format-flowed" }
|
||||
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[test]]
|
||||
name = "fuzz_dateparse"
|
||||
path = "fuzz_targets/fuzz_dateparse.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "fuzz_simplify"
|
||||
path = "fuzz_targets/fuzz_simplify.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "fuzz_mailparse"
|
||||
path = "fuzz_targets/fuzz_mailparse.rs"
|
||||
|
||||
@@ -9,7 +9,7 @@ fn round_trip(input: &str) -> String {
|
||||
|
||||
fn main() {
|
||||
check!().for_each(|data: &[u8]| {
|
||||
if let Ok(input) = std::str::from_utf8(data.into()) {
|
||||
if let Ok(input) = std::str::from_utf8(data) {
|
||||
let input = input.trim().to_string();
|
||||
|
||||
// Only consider inputs that are the result of unformatting format=flowed text.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use bolero::check;
|
||||
|
||||
use deltachat::fuzzing::simplify;
|
||||
|
||||
fn main() {
|
||||
check!().for_each(|data: &[u8]| match String::from_utf8(data.to_vec()) {
|
||||
Ok(input) => {
|
||||
simplify(input.clone(), true);
|
||||
simplify(input, false);
|
||||
}
|
||||
Err(_err) => {}
|
||||
});
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
CFFI Python Bindings
|
||||
============================
|
||||
|
||||
This package provides `Python bindings`_ to the `deltachat-core library`_
|
||||
This package provides `Python bindings`_ to the `chatmail core library`_
|
||||
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
|
||||
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
|
||||
.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`chatmail core library`: https://github.com/chatmail/core
|
||||
.. _`Python bindings`: https://py.delta.chat/
|
||||
|
||||
@@ -43,7 +43,7 @@ Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
Then clone the deltachat-core-rust repo::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
git clone https://github.com/chatmail/core
|
||||
cd deltachat-core-rust
|
||||
|
||||
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||
|
||||
@@ -2,7 +2,7 @@ Delta Chat Python bindings, new and old
|
||||
=======
|
||||
|
||||
`Delta Chat <https://delta.chat/>`_ provides two kinds of Python bindings
|
||||
to the `Rust Core <https://github.com/deltachat/deltachat-core-rust>`_:
|
||||
to the `Rust Core <https://github.com/chatmail/core>`_:
|
||||
JSON-RPC bindings and CFFI bindings.
|
||||
When starting a new project it is recommended to use JSON-RPC bindings,
|
||||
which are used in the Delta Chat Desktop app through generated Typescript-bindings.
|
||||
@@ -41,4 +41,4 @@ as the CFFI bindings are increasingly in maintenance-only mode.
|
||||
.. _virtualenv: http://pypi.org/project/virtualenv/
|
||||
.. _merlinux: http://merlinux.eu
|
||||
.. _pypi: http://pypi.org/
|
||||
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`issue-tracker`: https://github.com/chatmail/core
|
||||
|
||||
@@ -3,9 +3,9 @@ Development
|
||||
===========
|
||||
|
||||
To develop JSON-RPC bindings,
|
||||
clone the `deltachat-core-rust <https://github.com/deltachat/deltachat-core-rust/>`_ repository::
|
||||
clone the `chatmail core <https://github.com/chatmail/core/>`_ repository::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||
git clone https://github.com/chatmail/core.git
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
@@ -17,8 +17,8 @@ Install ``deltachat-rpc-server``
|
||||
To get ``deltachat-rpc-server`` binary you have three options:
|
||||
|
||||
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
|
||||
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server``.
|
||||
3. Download prebuilt release from https://github.com/deltachat/deltachat-core-rust/releases and install it into ``PATH``.
|
||||
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server``.
|
||||
3. Download prebuilt release from https://github.com/chatmail/core/releases and install it into ``PATH``.
|
||||
|
||||
Check that ``deltachat-rpc-server`` is installed and can run::
|
||||
|
||||
@@ -33,4 +33,4 @@ Install ``deltachat-rpc-client``
|
||||
To get ``deltachat-rpc-client`` Python library you can:
|
||||
|
||||
1. Install ``deltachat-rpc-client`` from PyPI using ``pip install deltachat-rpc-client``.
|
||||
2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/deltachat/deltachat-core-rust.git@main#subdirectory=deltachat-rpc-client``.
|
||||
2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/chatmail/core.git@main#subdirectory=deltachat-rpc-client``.
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.155.6"
|
||||
version = "1.158.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
@@ -29,8 +29,8 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
||||
"Home" = "https://github.com/chatmail/core/"
|
||||
"Bug Tracker" = "https://github.com/chatmail/core/issues"
|
||||
"Documentation" = "https://py.delta.chat/"
|
||||
"Mastodon" = "https://chaos.social/@delta"
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ class Message:
|
||||
"""extract key and use it as primary key for this account."""
|
||||
res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code))
|
||||
if res == 0:
|
||||
raise ValueError("could not decrypt")
|
||||
raise ValueError("Importing the key from Autocrypt Setup Message failed")
|
||||
|
||||
@props.with_doc
|
||||
def time_sent(self):
|
||||
@@ -285,23 +285,6 @@ class Message:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
"""return mime-header object for an incoming message.
|
||||
|
||||
This only returns a non-None object if ``save_mime_headers``
|
||||
config option was set and the message is incoming.
|
||||
|
||||
:returns: email-mime message object (with headers only, no body).
|
||||
"""
|
||||
import email
|
||||
|
||||
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
|
||||
if mime_headers:
|
||||
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
|
||||
if isinstance(s, bytes):
|
||||
return email.message_from_bytes(s)
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message."""
|
||||
|
||||
@@ -423,8 +423,6 @@ class ACFactory:
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
if self.pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
|
||||
@@ -510,6 +510,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
for ac in [ac1, ac1_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
@@ -560,6 +561,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
missing, cannot encrypt".
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
@@ -615,6 +617,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
- Now the seconds device has all members verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
|
||||
@@ -31,37 +31,6 @@ def test_basic_imap_api(acfactory, tmp_path):
|
||||
imap2.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.ignored()
|
||||
def test_configure_generate_key(acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
|
||||
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
|
||||
acfactory.bring_accounts_online()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send unencrypted message to ac2")
|
||||
chat.send_text("message1")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message1"
|
||||
assert not msg_in.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send encrypted message to ac1")
|
||||
msg_in.chat.send_text("message2")
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
msg2_in = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg2_in.text == "message2"
|
||||
assert msg2_in.is_encrypted()
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
msg2_in.chat.send_text("message3")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg3_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg3_in.text == "message3"
|
||||
assert msg3_in.is_encrypted()
|
||||
|
||||
|
||||
def test_configure_canceled(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac1.stop_ongoing()
|
||||
@@ -85,27 +54,6 @@ def test_configure_unref(tmp_path):
|
||||
lib.dc_context_unref(dc_context)
|
||||
|
||||
|
||||
def test_export_import_self_keys(acfactory, tmp_path, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
export_files = ac1.export_self_keys(str(dir))
|
||||
assert len(export_files) == 2
|
||||
for x in export_files:
|
||||
assert x.startswith(str(dir))
|
||||
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
|
||||
ac1._evtracker.consume_events()
|
||||
|
||||
lp.sec("exported keys (private and public)")
|
||||
for name in dir.iterdir():
|
||||
lp.indent(str(dir / name))
|
||||
lp.sec("importing into existing account")
|
||||
ac2.import_self_keys(str(dir))
|
||||
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
|
||||
assert key_id2 == key_id
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
@@ -971,67 +919,8 @@ def test_gossip_optimization(acfactory, lp):
|
||||
assert gossiped_timestamp == int(msg.time_sent.timestamp())
|
||||
|
||||
|
||||
def test_gossip_encryption_preference(acfactory, lp):
|
||||
"""Test that encryption preference of group members is gossiped to new members.
|
||||
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
|
||||
SHOULD NOT contain encryption preference.
|
||||
"""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
lp.sec("ac1 learns that ac2 prefers encryption")
|
||||
ac1.create_chat(ac2)
|
||||
msg = ac2.create_chat(ac1).send_text("first message")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "first message"
|
||||
assert not msg.is_encrypted()
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
|
||||
assert msg.chat.get_encryption_info() == res
|
||||
lp.sec("ac2 learns that ac3 prefers encryption")
|
||||
ac2.create_chat(ac3)
|
||||
msg = ac3.create_chat(ac2).send_text("I prefer encryption")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "I prefer encryption"
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
lp.sec("ac3 does not know that ac1 prefers encryption")
|
||||
ac1.create_chat(ac3)
|
||||
chat = ac3.create_chat(ac1)
|
||||
res = "No encryption:\n{}".format(ac1.get_config("addr"))
|
||||
assert chat.get_encryption_info() == res
|
||||
msg = chat.send_text("not encrypted")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "not encrypted"
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
lp.sec("ac1 creates a group chat with ac2")
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
encryption_info = group_chat.get_encryption_info()
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
|
||||
assert encryption_info == res
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.text == "hi"
|
||||
|
||||
lp.sec("ac2 adds ac3 to the group")
|
||||
msg.chat.add_contact(ac3)
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac3 learns that ac1 prefers encryption")
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
encryption_info = msg.chat.get_encryption_info().splitlines()
|
||||
assert encryption_info[0] == "End-to-end encryption preferred:"
|
||||
assert ac1.get_config("addr") in encryption_info[1:]
|
||||
assert ac2.get_config("addr") in encryption_info[1:]
|
||||
msg = chat.send_text("encrypted")
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.set_config("save_mime_headers", "1")
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1220,93 +1109,6 @@ def test_dont_show_emails(acfactory, lp):
|
||||
assert len(msg.chat.get_messages()) == 3
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac1_clone.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "0")
|
||||
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone.create_chat(ac2)
|
||||
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
|
||||
lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg_id = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone.create_chat(ac2).send_text("Hi back")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
|
||||
assert ev.data1 == first_msg_id.chat.id
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
|
||||
def test_prefer_encrypt(acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
ac2 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
ac3 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "1")
|
||||
ac3.set_config("e2ee_enabled", "0")
|
||||
|
||||
# Make sure we do not send a copy to ourselves. This is to
|
||||
# test that we count own preference even when we are not in
|
||||
# the recipient list.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
ac2.set_config("bcc_self", "0")
|
||||
ac3.set_config("bcc_self", "0")
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2, ac3])
|
||||
|
||||
lp.sec("ac1: sending message to ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
msg1 = chat1.send_text("message1")
|
||||
assert not msg1.is_encrypted()
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac2: sending message to ac1")
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
msg2 = chat2.send_text("message2")
|
||||
# Own preference is `Mutual` and we have the peer's key.
|
||||
assert msg2.is_encrypted()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: sending message to group chat with ac2 and ac3")
|
||||
group = ac1.create_group_chat("hello")
|
||||
group.add_contact(ac2)
|
||||
group.add_contact(ac3)
|
||||
msg3 = group.send_text("message3")
|
||||
assert not msg3.is_encrypted()
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
ac3._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac3: start preferring encryption and inform ac1")
|
||||
ac3.set_config("e2ee_enabled", "1")
|
||||
chat3 = ac3.create_chat(ac1)
|
||||
msg4 = chat3.send_text("message4")
|
||||
# Own preference is `Mutual` and we have the peer's key.
|
||||
assert msg4.is_encrypted()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: sending another message to group chat with ac2 and ac3")
|
||||
msg5 = group.send_text("message5")
|
||||
# Majority prefers encryption now
|
||||
assert msg5.is_encrypted()
|
||||
|
||||
|
||||
def test_bot(acfactory, lp):
|
||||
"""Test that bot messages can be identified as such"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1335,59 +1137,6 @@ def test_bot(acfactory, lp):
|
||||
assert msg_in.is_bot()
|
||||
|
||||
|
||||
def test_quote_encrypted(acfactory, lp):
|
||||
"""Test that replies to encrypted messages with quotes are encrypted."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg1 = chat.send_text("message1")
|
||||
assert not msg1.is_encrypted()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert not msg2.is_encrypted()
|
||||
|
||||
lp.sec("create new chat with contact and send back (encrypted) message")
|
||||
msg2.create_chat().send_text("message-back")
|
||||
|
||||
lp.sec("wait for ac1 to receive message")
|
||||
msg3 = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg3.text == "message-back"
|
||||
assert msg3.is_encrypted()
|
||||
|
||||
lp.sec("ac1: e2ee_enabled=0 and see if reply is encrypted")
|
||||
print("ac1: e2ee_enabled={}".format(ac1.get_config("e2ee_enabled")))
|
||||
print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled")))
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
|
||||
for quoted_msg in msg1, msg3:
|
||||
# Save the draft with a quote.
|
||||
msg_draft = Message.new_empty(ac1, "text")
|
||||
msg_draft.set_text("message reply")
|
||||
msg_draft.quote = quoted_msg
|
||||
chat.set_draft(msg_draft)
|
||||
|
||||
# Get the draft and send it.
|
||||
msg_draft = chat.get_draft()
|
||||
chat.send_msg(msg_draft)
|
||||
|
||||
chat.set_draft(None)
|
||||
assert chat.get_draft() is None
|
||||
|
||||
# Quote should be replaced with "..." if quoted message is encrypted.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message reply"
|
||||
assert not msg_in.is_encrypted()
|
||||
if quoted_msg.is_encrypted():
|
||||
assert msg_in.quoted_text == "..."
|
||||
else:
|
||||
assert msg_in.quoted_text == quoted_msg.text
|
||||
|
||||
|
||||
def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
"""Test that replies with an attachment and a quote are received correctly."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1421,26 +1170,6 @@ def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
assert open(received_reply.filename).read() == "data to send"
|
||||
|
||||
|
||||
def test_saved_mime_on_received_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("configure ac2 to save mime headers, create ac1/ac2 chat")
|
||||
ac2.set_config("save_mime_headers", "1")
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
ac1._evtracker.wait_msg_delivered(msg_out)
|
||||
assert msg_out.get_mime_headers() is None
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
in_id = ev.data2
|
||||
mime = ac2.get_message_by_id(in_id).get_mime_headers()
|
||||
assert mime.get_all("From")
|
||||
assert mime.get_all("Received")
|
||||
|
||||
|
||||
def test_send_mark_seen_clean_incoming_events(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1597,53 +1326,6 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
assert ac2.get_latest_backupfile(str(backupdir)) == path2
|
||||
|
||||
|
||||
def test_ac_setup_message(acfactory, lp):
|
||||
# note that the receiving account needs to be configured and running
|
||||
# before the setup message is send. DC does not read old messages
|
||||
# as of Jul2019
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("trigger ac setup message and return setupcode")
|
||||
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
|
||||
setup_code = ac1.initiate_key_transfer()
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.is_setup_message()
|
||||
assert msg.get_setupcodebegin() == setup_code[:2]
|
||||
lp.sec("try a bad setup code")
|
||||
with pytest.raises(ValueError):
|
||||
msg.continue_key_transfer(str(reversed(setup_code)))
|
||||
lp.sec("try a good setup code")
|
||||
print("*************** Incoming ASM File at: ", msg.filename)
|
||||
print("*************** Setup Code: ", setup_code)
|
||||
msg.continue_key_transfer(setup_code)
|
||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||
|
||||
|
||||
def test_ac_setup_message_twice(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("trigger ac setup message but ignore")
|
||||
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
|
||||
ac1.initiate_key_transfer()
|
||||
ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
|
||||
lp.sec("trigger second ac setup message, wait for receive ")
|
||||
setup_code2 = ac1.initiate_key_transfer()
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.is_setup_message()
|
||||
assert msg.get_setupcodebegin() == setup_code2[:2]
|
||||
|
||||
lp.sec("process second setup message")
|
||||
msg.continue_key_transfer(setup_code2)
|
||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
|
||||
@@ -105,10 +105,6 @@ class TestOfflineAccountBasic:
|
||||
ac1.update_config({"mvbox_move": False})
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
|
||||
def test_has_savemime(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
|
||||
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-02-17
|
||||
2025-03-29
|
||||
@@ -4,14 +4,14 @@ resources:
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
uri: https://github.com/chatmail/core.git
|
||||
|
||||
- name: deltachat-core-rust-release
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
uri: https://github.com/chatmail/core.git
|
||||
tag_filter: "v*"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -154,6 +154,8 @@ arch2tags = {
|
||||
"armv6l-linux": "linux_armv6l",
|
||||
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
|
||||
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
|
||||
"arm64-v8a-android": "android_21_arm64_v8a",
|
||||
"armeabi-v7a-android": "android_21_armeabi_v7a",
|
||||
"win64": "win_amd64",
|
||||
"win32": "win32",
|
||||
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -73,9 +73,7 @@ impl Accounts {
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
|
||||
let config = Config::from_file(config_file, writable)
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
let stockstrings = StockStrings::new();
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
@@ -460,7 +458,9 @@ impl Config {
|
||||
rx.await?;
|
||||
Ok(())
|
||||
});
|
||||
locked_rx.await?;
|
||||
if locked_rx.await.is_err() {
|
||||
bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
|
||||
};
|
||||
Ok(Some(lock_task))
|
||||
}
|
||||
|
||||
|
||||
82
src/blob.rs
82
src/blob.rs
@@ -1,7 +1,6 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -151,11 +150,11 @@ impl<'a> BlobObject<'a> {
|
||||
let rel_path = path
|
||||
.strip_prefix(context.get_blobdir())
|
||||
.with_context(|| format!("wrong blobdir: {}", path.display()))?;
|
||||
if !BlobObject::is_acceptible_blob_name(rel_path) {
|
||||
let name = rel_path.to_str().context("wrong name")?;
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("bad blob name: {}", rel_path.display()));
|
||||
}
|
||||
let name = rel_path.to_str().context("wrong name")?;
|
||||
BlobObject::from_name(context, name.to_string())
|
||||
BlobObject::from_name(context, name)
|
||||
}
|
||||
|
||||
/// Returns a [BlobObject] for an existing blob.
|
||||
@@ -164,13 +163,13 @@ impl<'a> BlobObject<'a> {
|
||||
/// prefixed, as returned by [BlobObject::as_name]. This is how
|
||||
/// you want to create a [BlobObject] for a filename read from the
|
||||
/// database.
|
||||
pub fn from_name(context: &'a Context, name: String) -> Result<BlobObject<'a>> {
|
||||
let name: String = match name.starts_with("$BLOBDIR/") {
|
||||
true => name.splitn(2, '/').last().unwrap().to_string(),
|
||||
pub fn from_name(context: &'a Context, name: &str) -> Result<BlobObject<'a>> {
|
||||
let name = match name.starts_with("$BLOBDIR/") {
|
||||
true => name.splitn(2, '/').last().unwrap(),
|
||||
false => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||
return Err(format_err!("not an acceptable blob name: {}", &name));
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {}", name));
|
||||
}
|
||||
Ok(BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
@@ -194,6 +193,7 @@ impl<'a> BlobObject<'a> {
|
||||
/// Note that this is NOT the user-visible filename,
|
||||
/// which is only stored in Param::Filename on the message.
|
||||
///
|
||||
#[allow(rustdoc::private_intra_doc_links)]
|
||||
/// [Params]: crate::param::Params
|
||||
pub fn as_name(&self) -> &str {
|
||||
&self.name
|
||||
@@ -216,19 +216,17 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// This is slightly less strict than stanitise_name, presumably
|
||||
/// someone already created a file with such a name so we just
|
||||
/// ensure it's not actually a path in disguise is actually utf-8.
|
||||
fn is_acceptible_blob_name(name: impl AsRef<OsStr>) -> bool {
|
||||
let uname = match name.as_ref().to_str() {
|
||||
Some(name) => name,
|
||||
None => return false,
|
||||
};
|
||||
if uname.find('/').is_some() {
|
||||
/// ensure it's not actually a path in disguise.
|
||||
///
|
||||
/// Acceptible blob name always have to be valid utf-8.
|
||||
fn is_acceptible_blob_name(name: &str) -> bool {
|
||||
if name.find('/').is_some() {
|
||||
return false;
|
||||
}
|
||||
if uname.find('\\').is_some() {
|
||||
if name.find('\\').is_some() {
|
||||
return false;
|
||||
}
|
||||
if uname.find('\0').is_some() {
|
||||
if name.find('\0').is_some() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@@ -254,26 +252,30 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(blob.as_name().to_string())
|
||||
}
|
||||
|
||||
/// Recode image to avatar size.
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||
let img_wh =
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
|
||||
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
|
||||
MediaQuality::Balanced => (
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_BYTES,
|
||||
),
|
||||
MediaQuality::Worse => {
|
||||
(constants::WORSE_AVATAR_SIZE, constants::WORSE_AVATAR_BYTES)
|
||||
}
|
||||
};
|
||||
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||
let is_avatar = true;
|
||||
self.recode_to_size(
|
||||
context,
|
||||
None, // The name of an avatar doesn't matter
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
20_000,
|
||||
strict_limits,
|
||||
max_bytes,
|
||||
is_avatar,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -302,21 +304,17 @@ impl<'a> BlobObject<'a> {
|
||||
),
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let strict_limits = false;
|
||||
let new_name = self.recode_to_size(
|
||||
context,
|
||||
name,
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
max_bytes,
|
||||
strict_limits,
|
||||
)?;
|
||||
let is_avatar = false;
|
||||
let new_name =
|
||||
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
|
||||
|
||||
Ok(new_name)
|
||||
}
|
||||
|
||||
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
|
||||
/// proceed with the result.
|
||||
/// Recodes the image so that it fits into limits on width/height and byte size.
|
||||
///
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
|
||||
/// with the result without rechecking.
|
||||
///
|
||||
/// This modifies the blob object in-place.
|
||||
///
|
||||
@@ -331,10 +329,10 @@ impl<'a> BlobObject<'a> {
|
||||
maybe_sticker: &mut bool,
|
||||
mut img_wh: u32,
|
||||
max_bytes: usize,
|
||||
strict_limits: bool,
|
||||
is_avatar: bool,
|
||||
) -> Result<String> {
|
||||
// Add white background only to avatars to spare the CPU.
|
||||
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
|
||||
let mut add_white_bg = is_avatar;
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let mut name = name.unwrap_or_else(|| self.name.clone());
|
||||
@@ -405,7 +403,7 @@ impl<'a> BlobObject<'a> {
|
||||
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
|
||||
// images.
|
||||
let do_scale = exceeds_max_bytes
|
||||
|| strict_limits
|
||||
|| is_avatar
|
||||
&& (exceeds_wh
|
||||
|| exif.is_some() && {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
@@ -442,7 +440,7 @@ impl<'a> BlobObject<'a> {
|
||||
ofmt.clone(),
|
||||
max_bytes,
|
||||
&mut encoded,
|
||||
)? && strict_limits
|
||||
)? && is_avatar
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
@@ -492,7 +490,7 @@ impl<'a> BlobObject<'a> {
|
||||
match res {
|
||||
Ok(_) => res,
|
||||
Err(err) => {
|
||||
if !strict_limits && no_exif {
|
||||
if !is_avatar && no_exif {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}.",
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn test_create_long_names() {
|
||||
let t = TestContext::new().await;
|
||||
let s = format!("file.{}", "a".repeat(100));
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
|
||||
let blobname = blob.as_name().split('/').last().unwrap();
|
||||
let blobname = blob.as_name().split('/').next_back().unwrap();
|
||||
assert!(blobname.len() < 70);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ async fn test_create_from_name_long() {
|
||||
fn test_is_blob_name() {
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
||||
assert!(BlobObject::is_acceptible_blob_name(&"f".repeat(128)));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||
@@ -174,7 +174,7 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
let avatar_path = Path::new(&avatar_blob);
|
||||
assert!(
|
||||
avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
|
||||
avatar_blob.ends_with("009161310a6afc319163e4bcabd23b9.jpg"),
|
||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||
);
|
||||
let scaled_avatar_size = file_size(avatar_path).await;
|
||||
@@ -226,7 +226,7 @@ async fn test_selfavatar_in_blobdir() {
|
||||
.unwrap();
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert!(
|
||||
avatar_cfg.ends_with("fa7418e646301203538041f60d03190.png"),
|
||||
avatar_cfg.ends_with("ec054c444a5755adf2b0aaea40209f2.png"),
|
||||
"Avatar file name {avatar_cfg} should end with its hash"
|
||||
);
|
||||
|
||||
|
||||
149
src/chat.rs
149
src/chat.rs
@@ -434,7 +434,7 @@ impl ChatId {
|
||||
.ok();
|
||||
}
|
||||
if delete {
|
||||
self.delete(context).await?;
|
||||
self.delete_ex(context, Nosync).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -773,6 +773,10 @@ impl ChatId {
|
||||
|
||||
/// Deletes a chat.
|
||||
pub async fn delete(self, context: &Context) -> Result<()> {
|
||||
self.delete_ex(context, Sync).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be a special chat: {}",
|
||||
@@ -780,10 +784,23 @@ impl ChatId {
|
||||
);
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
let delete_msgs_target = context.get_delete_msgs_target().await?;
|
||||
let sync_id = match sync {
|
||||
Nosync => None,
|
||||
Sync => chat.get_sync_id(context).await?,
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
transaction.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)",
|
||||
(delete_msgs_target, self,),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
|
||||
(self,),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
|
||||
(self,),
|
||||
@@ -795,13 +812,15 @@ impl ChatId {
|
||||
})
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatDeleted { chat_id: self });
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
if let Some(id) = sync_id {
|
||||
self::sync(context, id, SyncAction::Delete)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
if chat.is_self_talk() {
|
||||
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
|
||||
@@ -809,6 +828,11 @@ impl ChatId {
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1048,7 +1072,16 @@ impl ChatId {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat.
|
||||
pub(crate) async fn created_timestamp(self, context: &Context) -> Result<i64> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat,
|
||||
/// including hidden messages or a draft if there is one.
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
let timestamp = context
|
||||
.sql
|
||||
@@ -1281,8 +1314,7 @@ impl ChatId {
|
||||
///
|
||||
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
|
||||
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
|
||||
let mut ret_mutual = String::new();
|
||||
let mut ret_nopreference = String::new();
|
||||
let mut ret_available = String::new();
|
||||
let mut ret_reset = String::new();
|
||||
|
||||
for contact_id in get_chat_contacts(context, self)
|
||||
@@ -1298,8 +1330,9 @@ impl ChatId {
|
||||
.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
.map(|peerstate| peerstate.prefer_encrypt)
|
||||
{
|
||||
Some(EncryptPreference::Mutual) => ret_mutual += &format!("{addr}\n"),
|
||||
Some(EncryptPreference::NoPreference) => ret_nopreference += &format!("{addr}\n"),
|
||||
Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => {
|
||||
ret_available += &format!("{addr}\n")
|
||||
}
|
||||
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
|
||||
};
|
||||
}
|
||||
@@ -1311,23 +1344,14 @@ impl ChatId {
|
||||
ret.push('\n');
|
||||
ret += &ret_reset;
|
||||
}
|
||||
if !ret_nopreference.is_empty() {
|
||||
if !ret_available.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_available(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_nopreference;
|
||||
}
|
||||
if !ret_mutual.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_preferred(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_mutual;
|
||||
ret += &ret_available;
|
||||
}
|
||||
|
||||
Ok(ret.trim().to_string())
|
||||
@@ -1966,13 +1990,7 @@ impl Chat {
|
||||
if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) {
|
||||
Ok(member_list_timestamp)
|
||||
} else {
|
||||
let creation_timestamp: i64 = context
|
||||
.sql
|
||||
.query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self.id,))
|
||||
.await
|
||||
.context("SQL error querying created_timestamp")?
|
||||
.context("Chat not found")?;
|
||||
Ok(creation_timestamp)
|
||||
Ok(self.id.created_timestamp(context).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2986,6 +3004,12 @@ async fn prepare_send_msg(
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
|
||||
msg.chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
@@ -3006,10 +3030,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
// disabled by default is fine.
|
||||
//
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
if (context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
|
||||
&& (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
|
||||
{
|
||||
recipients.push(from);
|
||||
}
|
||||
@@ -3131,39 +3155,24 @@ pub async fn send_text_msg(
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
/// Returns true if the message can be edited.
|
||||
pub async fn can_send_edit_request(context: &Context, msg_id: MsgId) -> Result<bool> {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
if !can_send_edit_request_for_msg(&msg) {
|
||||
return Ok(false);
|
||||
}
|
||||
let chat = Chat::load_from_db(context, msg.get_chat_id()).await?;
|
||||
let can_send = chat.can_send(context).await?;
|
||||
Ok(can_send)
|
||||
}
|
||||
|
||||
fn can_send_edit_request_for_msg(msg: &Message) -> bool {
|
||||
if msg.from_id != ContactId::SELF
|
||||
|| msg.is_info()
|
||||
|| msg.viewtype == Viewtype::VideochatInvitation
|
||||
|| msg.has_html()
|
||||
|| msg.text.is_empty()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Sends chat members a request to edit the given message's text.
|
||||
pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
|
||||
ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty");
|
||||
|
||||
let mut original_msg = Message::load_from_db(context, msg_id).await?;
|
||||
ensure!(
|
||||
can_send_edit_request_for_msg(&original_msg),
|
||||
"Message cannot be edited"
|
||||
original_msg.from_id == ContactId::SELF,
|
||||
"Can edit only own messages"
|
||||
);
|
||||
|
||||
ensure!(!original_msg.is_info(), "Cannot edit info messages");
|
||||
ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
|
||||
ensure!(
|
||||
original_msg.viewtype != Viewtype::VideochatInvitation,
|
||||
"Cannot edit videochat invitations"
|
||||
);
|
||||
ensure!(
|
||||
!original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
|
||||
"Cannot add text"
|
||||
);
|
||||
ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty");
|
||||
if original_msg.text == new_text {
|
||||
info!(context, "Text unchanged.");
|
||||
return Ok(());
|
||||
@@ -4381,7 +4390,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
context.send_sync_msg().await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4407,7 +4416,7 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
bail!("message already saved.");
|
||||
}
|
||||
|
||||
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, txt_raw, \
|
||||
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
|
||||
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -4601,17 +4610,7 @@ pub async fn add_device_msg_with_importance(
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
let mut timestamp_sort = timestamp_sent;
|
||||
if let Some(last_msg_time) = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=?
|
||||
HAVING COUNT(*) > 0",
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
|
||||
if timestamp_sort <= last_msg_time {
|
||||
timestamp_sort = last_msg_time + 1;
|
||||
}
|
||||
@@ -4877,6 +4876,7 @@ pub(crate) enum SyncAction {
|
||||
Rename(String),
|
||||
/// Set chat contacts by their addresses.
|
||||
SetContacts(Vec<String>),
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -4936,6 +4936,7 @@ impl Context {
|
||||
}
|
||||
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
|
||||
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
||||
SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -298,13 +298,11 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Disable encryption so we can inspect raw message contents.
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
let fiona = tcm.fiona().await;
|
||||
|
||||
// Create contact for Bob on the Alice side with name "robert".
|
||||
let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
alice_bob_contact_id.set_name(&alice, "robert").await?;
|
||||
|
||||
// Set Bob authname to "Bob" and send it to Alice.
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
@@ -324,19 +322,15 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
// Create and promote a group.
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
let sent = alice
|
||||
alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
assert!(sent.payload.contains("Hi! I created a group."));
|
||||
|
||||
// Alice adds Bob to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent
|
||||
.payload
|
||||
.contains("I added member Bob (bob@example.net)."));
|
||||
// Locally set name "robert" should not leak.
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
assert_eq!(
|
||||
@@ -347,9 +341,6 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent
|
||||
.payload
|
||||
.contains("I removed member Bob (bob@example.net)."));
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
assert_eq!(
|
||||
sent.load_from_db().await.get_text(),
|
||||
@@ -359,7 +350,6 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
// Alice leaves the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("I left the group."));
|
||||
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
|
||||
|
||||
Ok(())
|
||||
@@ -372,13 +362,12 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let charlie = tcm.charlie().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
|
||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
|
||||
let alice_claire_contact_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
|
||||
// Create and promote a group.
|
||||
let alice_chat_id =
|
||||
@@ -393,8 +382,8 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Alice adds Claire to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
|
||||
// Alice adds Charlie to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?;
|
||||
let alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob leaves the chat.
|
||||
@@ -681,12 +670,13 @@ async fn test_modify_chat_lost() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Create group chat with Bob.
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let bob_contact = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
// Alice sends first message to group.
|
||||
@@ -695,16 +685,45 @@ async fn test_leave_group() -> Result<()> {
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
// Clear events so that we can later check
|
||||
// that the 'Group left' message didn't trigger IncomingMsg:
|
||||
alice.evtracker.clear_events();
|
||||
|
||||
// Shift the time so that we can later check the 'Group left' message's timestamp:
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
|
||||
// Bob leaves the group.
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
let leave_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&leave_msg).await;
|
||||
let rcvd_leave_msg = alice.recv_msg(&leave_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InSeen);
|
||||
|
||||
alice.emit_event(EventType::Test);
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|ev| match ev {
|
||||
EventType::Test => true,
|
||||
EventType::IncomingMsg { .. } => panic!("'Group left' message should be silent"),
|
||||
EventType::MsgsNoticed(..) => {
|
||||
panic!("'Group left' message shouldn't clear notifications")
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// The 'Group left' message timestamp should be the same as the previous message in the chat
|
||||
// so that the chat is not popped up in the chatlist:
|
||||
assert_eq!(
|
||||
sent_msg.load_from_db().await.timestamp_sort,
|
||||
rcvd_leave_msg.timestamp_sort
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1370,20 +1389,52 @@ async fn test_pinned_after_new_msgs() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_chat_name() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
|
||||
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
|
||||
"foo"
|
||||
);
|
||||
|
||||
set_chat_name(&t, chat_id, "bar").await.unwrap();
|
||||
set_chat_name(alice, chat_id, "bar").await.unwrap();
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
|
||||
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
|
||||
"bar"
|
||||
);
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, chat_id, bob_contact_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let sent_msg = alice.send_text(chat_id, "Hi").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat_id = received_msg.chat_id;
|
||||
|
||||
for new_name in [
|
||||
"Baz",
|
||||
"xyzzy",
|
||||
"Quux",
|
||||
"another name",
|
||||
"something different",
|
||||
] {
|
||||
set_chat_name(alice, chat_id, new_name).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(received_msg.chat_id, bob_chat_id);
|
||||
assert_eq!(
|
||||
Chat::load_from_db(bob, bob_chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.get_name(),
|
||||
new_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1593,58 +1644,6 @@ async fn test_lookup_self_by_contact_id() {
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_with_removed_message_id() -> Result<()> {
|
||||
// Alice creates a group with Bob, sends a message to bob
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_bob_contact = alice.add_or_lookup_contact(&bob).await;
|
||||
let contact_id = alice_bob_contact.id;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, contact_id).await?;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?;
|
||||
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
assert_eq!(msg.match_indices("Message-ID: <").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
|
||||
assert_eq!(msg.match_indices("References: <").count(), 1);
|
||||
|
||||
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
|
||||
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
let msg = bob.get_last_msg().await;
|
||||
|
||||
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(bob_chat.grpid, alice_chat.grpid);
|
||||
|
||||
// Bob accepts contact request.
|
||||
bob_chat.id.unblock(&bob).await?;
|
||||
|
||||
// Bob answers - simulate a normal MUA by not setting `Chat-*`-headers;
|
||||
// moreover, Bob's SMTP-server also replaces the `Message-ID:`-header
|
||||
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
|
||||
let msg = msg.replace("Chat-", "XXXX-");
|
||||
assert_eq!(msg.match_indices("Chat-").count(), 0);
|
||||
|
||||
// Alice receives this message - she can still detect the group by the `References:`-header
|
||||
receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
assert_eq!(msg.text, "ho!".to_string());
|
||||
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_marknoticed_chat() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -1854,10 +1853,6 @@ async fn test_sticker(
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some(filename), None)?;
|
||||
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let mime = sent_msg.payload();
|
||||
if res_viewtype == Viewtype::Sticker {
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
}
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
@@ -2073,8 +2068,9 @@ async fn test_forward_quote() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
@@ -2296,11 +2292,13 @@ async fn test_save_from_saved_to_saved_failing() -> Result<()> {
|
||||
async fn test_resend_own_message() -> Result<()> {
|
||||
// Alice creates group with Bob and sends an initial message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
alice.add_or_lookup_contact_id(&bob).await,
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
@@ -2309,7 +2307,7 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "claire@example.org").await?,
|
||||
alice.add_or_lookup_contact_id(&fiona).await,
|
||||
)
|
||||
.await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
@@ -2353,15 +2351,13 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
|
||||
// Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
let claire = TestContext::new().await;
|
||||
claire.configure_addr("claire@example.org").await;
|
||||
claire.recv_msg(&sent2).await;
|
||||
let msg = claire.recv_msg(&sent3).await;
|
||||
// Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
fiona.recv_msg(&sent2).await;
|
||||
let msg = fiona.recv_msg(&sent3).await;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
|
||||
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
assert!(sent1_ts_sent < msg.timestamp_sent);
|
||||
|
||||
@@ -2652,11 +2648,10 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption preferred:\n\
|
||||
End-to-end encryption available:\n\
|
||||
bob@example.net"
|
||||
);
|
||||
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
@@ -2846,12 +2841,7 @@ async fn test_blob_renaming() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
chat_id,
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?;
|
||||
let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe");
|
||||
fs::write(&file, "aaa").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
@@ -2883,7 +2873,7 @@ async fn test_sync_blocked() -> Result<()> {
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await;
|
||||
|
||||
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
@@ -2938,26 +2928,36 @@ async fn test_sync_blocked() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_accept_before_first_msg() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
let rcvd_msg = alice0.recv_msg(&sent_msg).await;
|
||||
let a0b_chat_id = rcvd_msg.chat_id;
|
||||
let a0b_contact_id = rcvd_msg.from_id;
|
||||
assert_eq!(
|
||||
Chat::load_from_db(alice0, a0b_chat_id).await?.blocked,
|
||||
Blocked::Request
|
||||
);
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
|
||||
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
|
||||
assert_eq!(a0b_contact.origin, Origin::CreateChat);
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
|
||||
assert_eq!(alice0.get_chat(bob).await.blocked, Blocked::Not);
|
||||
|
||||
sync(alice0, alice1).await;
|
||||
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
|
||||
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
|
||||
assert_eq!(alice1_contacts.len(), 1);
|
||||
let a1b_contact_id = alice1_contacts[0];
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.get_addr(), "bob@example.net");
|
||||
assert_eq!(a1b_contact.origin, Origin::CreateChat);
|
||||
let a1b_chat = alice1.get_chat(&bob).await;
|
||||
let a1b_chat = alice1.get_chat(bob).await;
|
||||
assert_eq!(a1b_chat.blocked, Blocked::Not);
|
||||
let chats = Chatlist::try_load(alice1, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
@@ -2978,22 +2978,22 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
let rcvd_msg = alice0.recv_msg(&sent_msg).await;
|
||||
let a0b_chat_id = rcvd_msg.chat_id;
|
||||
let a0b_contact_id = rcvd_msg.from_id;
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.block(alice0).await?;
|
||||
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
|
||||
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
|
||||
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
|
||||
|
||||
sync(alice0, alice1).await;
|
||||
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(a1b_contact.origin, Origin::Hidden);
|
||||
assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id)
|
||||
.await?
|
||||
.is_none());
|
||||
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
|
||||
assert_eq!(alice1_contacts.len(), 0);
|
||||
|
||||
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
|
||||
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
|
||||
let a1b_contact_id = rcvd_msg.from_id;
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
|
||||
let a1b_chat = alice1.get_chat(&bob).await;
|
||||
assert_eq!(a1b_chat.blocked, Blocked::Yes);
|
||||
@@ -3001,6 +3001,48 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_delete_chat() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
let a1b_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
a0b_chat_id.delete(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
alice1.assert_no_chat(a1b_chat_id).await;
|
||||
alice1
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
|
||||
.await;
|
||||
|
||||
let bob_grp_chat_id = bob
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
|
||||
.await;
|
||||
let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await;
|
||||
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
|
||||
a0_grp_chat_id.accept(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
a0_grp_chat_id.delete(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
alice1.assert_no_chat(a1_grp_chat_id).await;
|
||||
alice0
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_adhoc_grp() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
@@ -3051,8 +3093,9 @@ async fn test_sync_adhoc_grp() -> Result<()> {
|
||||
/// - That sync messages don't unarchive the self-chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_visibility() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
@@ -3104,8 +3147,9 @@ async fn test_sync_device_messages_visibility() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_muted() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
@@ -3139,8 +3183,9 @@ async fn test_sync_muted() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_broadcast() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
@@ -3183,6 +3228,10 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
assert!(get_past_chat_contacts(alice1, a1_broadcast_id)
|
||||
.await?
|
||||
.is_empty());
|
||||
|
||||
a0_broadcast_id.delete(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
alice1.assert_no_chat(a1_broadcast_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3325,7 +3374,8 @@ async fn test_past_members() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3337,8 +3387,7 @@ async fn test_past_members() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
let add_message = alice.pop_sent_msg().await;
|
||||
@@ -3357,8 +3406,7 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3370,7 +3418,8 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
let bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
// Alice removes Bob and Bob adds Fiona at the same time.
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
@@ -3392,11 +3441,10 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
let fiona_addr = "fiona@example.net";
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3412,10 +3460,6 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
let sent = alice.send_text(alice_chat_id, "Hello group!").await;
|
||||
let payload = sent.payload();
|
||||
assert_eq!(payload.contains("Hello group!"), true);
|
||||
assert_eq!(payload.contains(&bob_addr), true);
|
||||
assert_eq!(payload.contains(fiona_addr), false);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
@@ -3431,8 +3475,8 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let fiona_addr = "fiona@example.net";
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3447,12 +3491,10 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
let add_message = alice.pop_sent_msg().await;
|
||||
assert_eq!(add_message.payload.contains(fiona_addr), false);
|
||||
let bob_add_message = bob.recv_msg(&add_message).await;
|
||||
let bob_chat_id = bob_add_message.chat_id;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
@@ -3526,13 +3568,11 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
|
||||
let charlie_addr = "charlie@example.com";
|
||||
let alice_charlie_contact_id = Contact::create(alice, "Charlie", charlie_addr).await?;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3554,7 +3594,6 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
let remove_message = alice.pop_sent_msg().await;
|
||||
assert_eq!(remove_message.payload.contains(charlie_addr), true);
|
||||
bob.recv_msg(&remove_message).await;
|
||||
|
||||
// 60 days pass.
|
||||
@@ -3563,8 +3602,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
// Bob adds Fiona to the chat.
|
||||
let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_fiona_contact_id = Contact::create(bob, "Fiona", &fiona_addr).await?;
|
||||
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?;
|
||||
|
||||
let add_message = bob.pop_sent_msg().await;
|
||||
@@ -3628,7 +3666,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
// Charlie is not part of the chat.
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
|
||||
assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 0);
|
||||
let bob_charlie_contact_id = Contact::create(bob, "Charlie", charlie_addr).await?;
|
||||
let bob_charlie_contact_id = bob.add_or_lookup_contact_id(charlie).await;
|
||||
assert!(!is_contact_in_chat(bob, bob_chat_id, bob_charlie_contact_id).await?);
|
||||
|
||||
assert_eq!(get_chat_contacts(fiona, fiona_chat_id).await?.len(), 3);
|
||||
@@ -3646,58 +3684,6 @@ async fn test_one_to_one_chat_no_group_member_timestamps() {
|
||||
assert!(!payload.contains("Chat-Group-Member-Timestamps:"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn text_can_send_edit_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
|
||||
// Alice can edit her message
|
||||
let sent1 = alice.send_text(chat_id, "foo").await;
|
||||
assert!(can_send_edit_request(&alice, sent1.sender_msg_id).await?);
|
||||
|
||||
// Bob cannot edit Alice's message
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(!can_send_edit_request(&bob, msg.id).await?);
|
||||
|
||||
// HTML messages cannot be edited
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
let sent2 = alice.send_msg(chat_id, &mut msg).await;
|
||||
assert!(!can_send_edit_request(&alice, sent2.sender_msg_id).await?);
|
||||
|
||||
// Info messages cannot be edited
|
||||
set_chat_name(&alice, chat_id, "bar").await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
assert!(!can_send_edit_request(&alice, msg.id).await?);
|
||||
|
||||
// Videochat invitations cannot be edited
|
||||
alice
|
||||
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
|
||||
.await?;
|
||||
let msg_id = send_videochat_invitation(&alice, chat_id).await?;
|
||||
assert!(!can_send_edit_request(&alice, msg_id).await?);
|
||||
|
||||
// If not text was given initally, there is nothing to edit
|
||||
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.make_vcard(&alice, &[ContactId::SELF]).await?;
|
||||
let sent3 = alice.send_msg(chat_id, &mut msg).await;
|
||||
assert!(msg.text.is_empty());
|
||||
assert!(!can_send_edit_request(&alice, sent3.sender_msg_id).await?);
|
||||
|
||||
// Messages in left groups cannot be edited any longer (as one cannot send to that group)
|
||||
remove_contact_from_chat(&alice, chat_id, ContactId::SELF).await?;
|
||||
assert!(!can_send_edit_request(&alice, sent1.sender_msg_id).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_edit_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3764,19 +3750,122 @@ async fn test_receive_edit_request_after_removal() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cannot_edit_html() -> Result<()> {
|
||||
async fn test_cannot_send_edit_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[bob])
|
||||
.await;
|
||||
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
send_msg(alice, chat.id, &mut msg).await.unwrap();
|
||||
assert!(msg.has_html());
|
||||
assert!(send_edit_request(alice, msg.id, "foo".to_string())
|
||||
// Alice can edit her message
|
||||
let sent1 = alice.send_text(chat_id, "foo").await;
|
||||
send_edit_request(alice, sent1.sender_msg_id, "bar".to_string()).await?;
|
||||
|
||||
// Bob cannot edit Alice's message
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(send_edit_request(bob, msg.id, "bar".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// HTML messages cannot be edited
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
let sent2 = alice.send_msg(chat_id, &mut msg).await;
|
||||
assert!(msg.has_html());
|
||||
assert!(
|
||||
send_edit_request(alice, sent2.sender_msg_id, "foo".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Info messages cannot be edited
|
||||
set_chat_name(alice, chat_id, "bar").await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
assert!(send_edit_request(alice, msg.id, "bar".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// Videochat invitations cannot be edited
|
||||
alice
|
||||
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
|
||||
.await?;
|
||||
let msg_id = send_videochat_invitation(alice, chat_id).await?;
|
||||
assert!(send_edit_request(alice, msg_id, "bar".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// If not text was given initally, there is nothing to edit
|
||||
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.make_vcard(alice, &[ContactId::SELF]).await?;
|
||||
let sent3 = alice.send_msg(chat_id, &mut msg).await;
|
||||
assert!(msg.text.is_empty());
|
||||
assert!(
|
||||
send_edit_request(alice, sent3.sender_msg_id, "bar".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_delete_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let bob_chat = bob.create_chat(alice).await;
|
||||
|
||||
// Bobs sends a message to Alice, so Alice learns Bob's key
|
||||
let sent0 = bob.send_text(bob_chat.id, "¡ola!").await;
|
||||
alice.recv_msg(&sent0).await;
|
||||
|
||||
// Alice sends a message, then sends a deletion request
|
||||
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
|
||||
let alice_msg = sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);
|
||||
|
||||
message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
|
||||
// Bob receives both messages and has nothing the end
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "wtf");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
|
||||
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
|
||||
|
||||
// Alice has another device, and there is also nothing at the end
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice2.recv_msg(&sent0).await;
|
||||
let alice2_msg = alice2.recv_msg(&sent1).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);
|
||||
|
||||
alice2.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_delete_request_no_encryption() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_email_chat(bob).await;
|
||||
|
||||
// Alice sends a message, then tries to send a deletion request which fails.
|
||||
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
|
||||
assert!(message::delete_msgs_ex(alice, &[sent1.sender_msg_id], true)
|
||||
.await
|
||||
.is_err());
|
||||
sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -407,16 +407,17 @@ impl Chatlist {
|
||||
let lastcontact = if let Some(lastmsg) = &lastmsg {
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
|
||||
.await
|
||||
.context("loading contact failed")?;
|
||||
Some(lastcontact)
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
|
||||
.await
|
||||
.context("loading contact failed")?;
|
||||
Some(lastcontact)
|
||||
}
|
||||
Chattype::Single => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -479,6 +480,7 @@ pub async fn get_last_message_for_chat(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg, ProtectionStatus,
|
||||
@@ -486,6 +488,7 @@ mod tests {
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_load() {
|
||||
@@ -787,6 +790,31 @@ mod tests {
|
||||
assert!(summary_res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_summary_for_saved_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
|
||||
send_text_msg(&alice, chat_alice.id, "hi".into()).await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
save_msgs(&alice, &[sent1.sender_msg_id]).await?;
|
||||
let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&alice, 0, None).await?;
|
||||
assert_eq!(summary.prefix.unwrap().to_string(), "Me");
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
save_msgs(&bob, &[msg.id]).await?;
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
assert_eq!(summary.prefix.unwrap().to_string(), "alice@example.org");
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
@@ -193,10 +193,6 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
FetchedExistingMsgs,
|
||||
|
||||
/// Type of the OpenPGP key to generate.
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
@@ -220,9 +216,6 @@ pub enum Config {
|
||||
/// `ProviderOptions::delete_to_trash`.
|
||||
DeleteToTrash,
|
||||
|
||||
/// Save raw MIME messages with headers in the database if true.
|
||||
SaveMimeHeaders,
|
||||
|
||||
/// The primary email address. Also see `SecondaryAddrs`.
|
||||
ConfiguredAddr,
|
||||
|
||||
@@ -716,7 +709,6 @@ impl Context {
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::FetchExistingMsgs
|
||||
| Config::DeleteToTrash
|
||||
| Config::SaveMimeHeaders
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
|
||||
@@ -86,7 +86,7 @@ async fn test_set_config_bool() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// We need some config that defaults to true
|
||||
let c = Config::E2eeEnabled;
|
||||
let c = Config::MdnsEnabled;
|
||||
assert_eq!(t.get_config_bool(c).await?, true);
|
||||
t.set_config_bool(c, false).await?;
|
||||
assert_eq!(t.get_config_bool(c).await?, false);
|
||||
|
||||
150
src/configure.rs
150
src/configure.rs
@@ -28,17 +28,19 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::LogExt;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
|
||||
ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
|
||||
};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::qr::set_account_from_qr;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use crate::{chat, provider};
|
||||
use crate::{stock_str, EventType};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
@@ -64,8 +66,59 @@ impl Context {
|
||||
self.sql.get_raw_config_bool("configured").await
|
||||
}
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
/// Configures this account with the currently provided parameters.
|
||||
///
|
||||
/// Deprecated since 2025-02; use `add_transport_from_qr()`
|
||||
/// or `add_transport()` instead.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
|
||||
self.add_transport_inner(¶m).await
|
||||
}
|
||||
|
||||
/// Configures a new email account using the provided parameters
|
||||
/// and adds it as a transport.
|
||||
///
|
||||
/// If the email address is the same as an existing transport,
|
||||
/// then this existing account will be reconfigured instead of a new one being added.
|
||||
///
|
||||
/// This function stops and starts IO as needed.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `imap.password`,
|
||||
/// and all the other settings will be autoconfigured.
|
||||
///
|
||||
/// During configuration, ConfigureProgress events are emitted;
|
||||
/// they indicate a successful configuration as well as errors
|
||||
/// and may be used to create a progress bar.
|
||||
/// This function will return after configuration is finished.
|
||||
///
|
||||
/// If configuration is successful,
|
||||
/// the working server parameters will be saved
|
||||
/// and used for connecting to the server.
|
||||
/// The parameters entered by the user will be saved separately
|
||||
/// so that they can be prefilled when the user opens the server-configuration screen again.
|
||||
///
|
||||
/// See also:
|
||||
/// - [Self::is_configured()] to check whether there is
|
||||
/// at least one working transport.
|
||||
/// - [Self::add_transport_from_qr()] to add a transport
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
pub async fn add_transport(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
let result = self.add_transport_inner(param).await;
|
||||
if result.is_err() {
|
||||
if let Ok(true) = self.is_configured().await {
|
||||
self.start_io().await;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_transport_inner(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
ensure!(
|
||||
!self.scheduler.is_running().await,
|
||||
"cannot configure, already running"
|
||||
@@ -74,42 +127,77 @@ impl Context {
|
||||
self.sql.is_open().await,
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) {
|
||||
bail!("Adding a new transport is not supported right now. Check back in a few months!");
|
||||
}
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
|
||||
let res = self
|
||||
.inner_configure()
|
||||
.inner_configure(param)
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
|
||||
if let Err(err) = res.as_ref() {
|
||||
progress!(
|
||||
self,
|
||||
0,
|
||||
Some(
|
||||
stock_str::configuration_failed(
|
||||
self,
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
&format!("{err:#}"),
|
||||
)
|
||||
.await
|
||||
)
|
||||
);
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
|
||||
progress!(self, 0, Some(error_msg.clone()));
|
||||
bail!(error_msg);
|
||||
} else {
|
||||
param.save(self).await?;
|
||||
progress!(self, 1000);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
/// Adds a new email account as a transport
|
||||
/// using the server encoded in the QR code.
|
||||
/// See [Self::add_transport].
|
||||
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
|
||||
let result = async move {
|
||||
set_account_from_qr(self, qr).await?;
|
||||
self.configure().await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
if let Ok(true) = self.is_configured().await {
|
||||
self.start_io().await;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
|
||||
Ok(vec![param])
|
||||
}
|
||||
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
#[expect(clippy::unused_async)]
|
||||
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
|
||||
bail!("Adding and removing additional transports is not supported yet. Check back in a few months!")
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
let configured_param = configure(self, ¶m).await?;
|
||||
let configured_param = configure(self, param).await?;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, configured_param, old_addr).await?;
|
||||
@@ -185,8 +273,7 @@ async fn get_configured_param(
|
||||
param.smtp.password.clone()
|
||||
};
|
||||
|
||||
let proxy_config = param.proxy_config.clone();
|
||||
let proxy_enabled = proxy_config.is_some();
|
||||
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
|
||||
|
||||
let mut addr = param.addr.clone();
|
||||
if param.oauth2 {
|
||||
@@ -345,7 +432,6 @@ async fn get_configured_param(
|
||||
.collect(),
|
||||
smtp_user: param.smtp.user.clone(),
|
||||
smtp_password,
|
||||
proxy_config: param.proxy_config.clone(),
|
||||
provider,
|
||||
certificate_checks: match param.certificate_checks {
|
||||
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
|
||||
@@ -367,7 +453,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
let configured_param = get_configured_param(ctx, param).await?;
|
||||
let strict_tls = configured_param.strict_tls();
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
|
||||
|
||||
progress!(ctx, 550);
|
||||
|
||||
@@ -377,15 +464,15 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let smtp_param = configured_param.smtp.clone();
|
||||
let smtp_password = configured_param.smtp_password.clone();
|
||||
let smtp_addr = configured_param.addr.clone();
|
||||
let proxy_config = configured_param.proxy_config.clone();
|
||||
|
||||
let proxy_config2 = proxy_config.clone();
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp = Smtp::new();
|
||||
smtp.connect(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&smtp_password,
|
||||
&proxy_config,
|
||||
&proxy_config2,
|
||||
&smtp_addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
@@ -403,7 +490,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let mut imap = Imap::new(
|
||||
configured_param.imap.clone(),
|
||||
configured_param.imap_password.clone(),
|
||||
configured_param.proxy_config.clone(),
|
||||
proxy_config,
|
||||
&configured_param.addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
@@ -412,7 +499,10 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let configuring = true;
|
||||
let mut imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
|
||||
Err(err) => bail!(
|
||||
"{}",
|
||||
nicer_configuration_error(ctx, format!("{err:#}")).await
|
||||
),
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
@@ -442,7 +532,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
ctx.set_config(Config::MvboxMove, Some("0")).await?;
|
||||
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
|
||||
ctx.set_config(Config::ShowEmails, None).await?;
|
||||
ctx.set_config(Config::E2eeEnabled, Some("1")).await?;
|
||||
}
|
||||
|
||||
let create_mvbox = !is_chatmail;
|
||||
@@ -473,9 +562,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
|
||||
progress!(ctx, 920);
|
||||
|
||||
e2ee::ensure_secret_key_exists(ctx).await?;
|
||||
info!(ctx, "key generation completed");
|
||||
|
||||
ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
|
||||
.await?;
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
@@ -58,25 +58,6 @@ pub enum MediaQuality {
|
||||
Worse = 1,
|
||||
}
|
||||
|
||||
/// Type of the key to generate.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
#[default]
|
||||
Default = 0,
|
||||
|
||||
/// 2048-bit RSA.
|
||||
Rsa2048 = 1,
|
||||
|
||||
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
|
||||
Ed25519 = 2,
|
||||
|
||||
/// 4096-bit RSA.
|
||||
Rsa4096 = 3,
|
||||
}
|
||||
|
||||
/// Video chat URL type.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
@@ -204,9 +185,11 @@ pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
|
||||
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
|
||||
pub const WORSE_IMAGE_BYTES: usize = 130_000;
|
||||
|
||||
// max. width/height of an avatar
|
||||
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
|
||||
// max. width/height and bytes of an avatar
|
||||
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;
|
||||
pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000;
|
||||
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
|
||||
pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k.
|
||||
|
||||
// max. width/height of images scaled down because of being too huge
|
||||
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||
@@ -253,16 +236,6 @@ mod tests {
|
||||
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keygentype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::default());
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
|
||||
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_showemails_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
256
src/contact.rs
256
src/contact.rs
@@ -95,6 +95,50 @@ impl ContactId {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Sets display name for existing contact.
|
||||
///
|
||||
/// Display name may be an empty string,
|
||||
/// in which case the name displayed in the UI
|
||||
/// for this contact will switch to the
|
||||
/// contact's authorized name.
|
||||
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
|
||||
let addr = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let is_changed = transaction.execute(
|
||||
"UPDATE contacts SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
(name, self),
|
||||
)? > 0;
|
||||
if is_changed {
|
||||
update_chat_names(context, transaction, self)?;
|
||||
let addr = transaction.query_row(
|
||||
"SELECT addr FROM contacts WHERE id=?",
|
||||
(self,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)?;
|
||||
Ok(Some(addr))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(addr) = addr {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr.to_string()),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark contact as bot.
|
||||
pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
|
||||
context
|
||||
@@ -843,44 +887,48 @@ impl Contact {
|
||||
|
||||
let mut update_addr = false;
|
||||
|
||||
let row_id = context.sql.transaction(|transaction| {
|
||||
let row = transaction.query_row(
|
||||
"SELECT id, name, addr, origin, authname
|
||||
let row_id = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let row = transaction
|
||||
.query_row(
|
||||
"SELECT id, name, addr, origin, authname
|
||||
FROM contacts WHERE addr=? COLLATE NOCASE",
|
||||
[addr.to_string()],
|
||||
|row| {
|
||||
let row_id: isize = row.get(0)?;
|
||||
let row_name: String = row.get(1)?;
|
||||
let row_addr: String = row.get(2)?;
|
||||
let row_origin: Origin = row.get(3)?;
|
||||
let row_authname: String = row.get(4)?;
|
||||
(addr,),
|
||||
|row| {
|
||||
let row_id: u32 = row.get(0)?;
|
||||
let row_name: String = row.get(1)?;
|
||||
let row_addr: String = row.get(2)?;
|
||||
let row_origin: Origin = row.get(3)?;
|
||||
let row_authname: String = row.get(4)?;
|
||||
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
}).optional()?;
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
|
||||
let row_id;
|
||||
if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
|
||||
let update_name = manual && name != row_name;
|
||||
let update_authname = !manual
|
||||
&& name != row_authname
|
||||
&& !name.is_empty()
|
||||
&& (origin >= row_origin
|
||||
|| origin == Origin::IncomingUnknownFrom
|
||||
|| row_authname.is_empty());
|
||||
let row_id;
|
||||
if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
|
||||
let update_name = manual && name != row_name;
|
||||
let update_authname = !manual
|
||||
&& name != row_authname
|
||||
&& !name.is_empty()
|
||||
&& (origin >= row_origin
|
||||
|| origin == Origin::IncomingUnknownFrom
|
||||
|| row_authname.is_empty());
|
||||
|
||||
row_id = u32::try_from(id)?;
|
||||
if origin >= row_origin && addr.as_ref() != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
let new_name = if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
row_name
|
||||
};
|
||||
row_id = id;
|
||||
if origin >= row_origin && addr.as_ref() != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
let new_name = if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
row_name
|
||||
};
|
||||
|
||||
transaction
|
||||
.execute(
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
(
|
||||
new_name,
|
||||
@@ -899,88 +947,38 @@ impl Contact {
|
||||
} else {
|
||||
row_authname
|
||||
},
|
||||
row_id
|
||||
row_id,
|
||||
),
|
||||
)?;
|
||||
|
||||
if update_name || update_authname {
|
||||
// Update the contact name also if it is used as a group name.
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
let chat_id: Option<ChatId> = transaction.query_row(
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
(Chattype::Single, isize::try_from(row_id)?),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
).optional()?;
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
if update_name || update_authname {
|
||||
let contact_id = ContactId::new(row_id);
|
||||
let (addr, name, authname) =
|
||||
transaction.query_row(
|
||||
"SELECT addr, name, authname
|
||||
FROM contacts
|
||||
WHERE id=?",
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
let authname: String = row.get(2)?;
|
||||
Ok((addr, name, authname))
|
||||
})?;
|
||||
|
||||
let chat_name = if !name.is_empty() {
|
||||
name
|
||||
} else if !authname.is_empty() {
|
||||
authname
|
||||
} else {
|
||||
addr
|
||||
};
|
||||
|
||||
let count = transaction.execute(
|
||||
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
(chat_name, chat_id))?;
|
||||
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
|
||||
}
|
||||
update_chat_names(context, transaction, contact_id)?;
|
||||
}
|
||||
sth_modified = Modifier::Modified;
|
||||
}
|
||||
sth_modified = Modifier::Modified;
|
||||
}
|
||||
} else {
|
||||
let update_name = manual;
|
||||
let update_authname = !manual;
|
||||
} else {
|
||||
let update_name = manual;
|
||||
let update_authname = !manual;
|
||||
|
||||
transaction
|
||||
.execute(
|
||||
transaction.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
(
|
||||
if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
(
|
||||
if update_name { &name } else { "" },
|
||||
&addr,
|
||||
origin,
|
||||
if update_authname {
|
||||
name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
if update_authname { &name } else { "" },
|
||||
),
|
||||
)?;
|
||||
|
||||
sth_modified = Modifier::Created;
|
||||
row_id = u32::try_from(transaction.last_insert_rowid())?;
|
||||
info!(context, "Added contact id={row_id} addr={addr}.");
|
||||
}
|
||||
Ok(row_id)
|
||||
}).await?;
|
||||
sth_modified = Modifier::Created;
|
||||
row_id = u32::try_from(transaction.last_insert_rowid())?;
|
||||
info!(context, "Added contact id={row_id} addr={addr}.");
|
||||
}
|
||||
Ok(row_id)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let contact_id = ContactId::new(row_id);
|
||||
|
||||
@@ -1435,7 +1433,7 @@ impl Contact {
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if self.id == ContactId::SELF {
|
||||
if let Some(p) = context.get_config(Config::Selfavatar).await? {
|
||||
return Ok(Some(PathBuf::from(p)));
|
||||
return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already
|
||||
}
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
@@ -1606,6 +1604,60 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the names of the chats which use the contact name.
|
||||
//
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
fn update_chat_names(
|
||||
context: &Context,
|
||||
transaction: &rusqlite::Connection,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let chat_id: Option<ChatId> = transaction.query_row(
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
(Chattype::Single, contact_id),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
).optional()?;
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
let (addr, name, authname) = transaction.query_row(
|
||||
"SELECT addr, name, authname
|
||||
FROM contacts
|
||||
WHERE id=?",
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
let authname: String = row.get(2)?;
|
||||
Ok((addr, name, authname))
|
||||
},
|
||||
)?;
|
||||
|
||||
let chat_name = if !name.is_empty() {
|
||||
name
|
||||
} else if !authname.is_empty() {
|
||||
authname
|
||||
} else {
|
||||
addr
|
||||
};
|
||||
|
||||
let count = transaction.execute(
|
||||
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
(chat_name, chat_id),
|
||||
)?;
|
||||
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_blocked(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
|
||||
@@ -1050,8 +1050,9 @@ async fn test_sync_create() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_make_n_import_vcard() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
let avatar_path = bob.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
@@ -1207,16 +1208,16 @@ async fn test_reset_encryption() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
let msg = tcm.send_recv(bob, alice, "Hi!").await;
|
||||
let msg = tcm.send_recv_accept(bob, alice, "Hi!").await;
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
|
||||
let alice_bob_chat_id = msg.chat_id;
|
||||
let alice_bob_contact_id = msg.from_id;
|
||||
|
||||
alice_bob_contact_id.reset_encryption(alice).await?;
|
||||
|
||||
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
|
||||
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
@@ -1235,6 +1236,7 @@ async fn test_reset_verified_encryption() -> Result<()> {
|
||||
|
||||
let alice_bob_chat_id = msg.chat_id;
|
||||
let alice_bob_contact_id = msg.from_id;
|
||||
|
||||
alice_bob_contact_id.reset_encryption(alice).await?;
|
||||
|
||||
// Check that the contact is still verified after resetting encryption.
|
||||
@@ -1250,7 +1252,8 @@ async fn test_reset_verified_encryption() -> Result<()> {
|
||||
"bob@example.net sent a message from another device."
|
||||
);
|
||||
|
||||
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
|
||||
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -919,12 +919,6 @@ impl Context {
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"save_mime_headers",
|
||||
self.get_config_bool(Config::SaveMimeHeaders)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"download_limit",
|
||||
self.get_config_int(Config::DownloadLimit)
|
||||
@@ -944,10 +938,6 @@ impl Context {
|
||||
res.insert("configured_trash_folder", configured_trash_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert(
|
||||
"key_gen_type",
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
|
||||
196
src/e2ee.rs
196
src/e2ee.rs
@@ -1,8 +1,9 @@
|
||||
//! End-to-end encryption support.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use anyhow::{bail, Result};
|
||||
use mail_builder::mime::MimePart;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -42,88 +43,76 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
/// Determines if we can and should encrypt.
|
||||
///
|
||||
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
|
||||
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
|
||||
///
|
||||
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
|
||||
pub(crate) async fn should_encrypt(
|
||||
&self,
|
||||
context: &Context,
|
||||
e2ee_guaranteed: bool,
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<bool> {
|
||||
let is_chatmail = context.is_chatmail().await?;
|
||||
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for (peerstate, addr) in peerstates {
|
||||
match peerstate {
|
||||
Some(peerstate) => {
|
||||
let prefer_encrypt = peerstate.prefer_encrypt;
|
||||
info!(context, "Peerstate for {addr:?} is {prefer_encrypt}.");
|
||||
if match peerstate.prefer_encrypt {
|
||||
EncryptPreference::NoPreference | EncryptPreference::Reset => {
|
||||
(peerstate.prefer_encrypt != EncryptPreference::Reset || is_chatmail)
|
||||
&& self.prefer_encrypt == EncryptPreference::Mutual
|
||||
}
|
||||
EncryptPreference::Mutual => true,
|
||||
} {
|
||||
prefer_encrypt_count += 1;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let msg = format!("Peerstate for {addr:?} missing, cannot encrypt");
|
||||
if e2ee_guaranteed {
|
||||
return Err(format_err!("{msg}"));
|
||||
} else {
|
||||
info!(context, "{msg}.");
|
||||
return Ok(false);
|
||||
}
|
||||
for (peerstate, _addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
// For chatmail we ignore the encryption preference,
|
||||
// because we can either send encrypted or not at all.
|
||||
if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Count number of recipients, including self.
|
||||
// This does not depend on whether we send a copy to self or not.
|
||||
let recipients_count = peerstates.len() + 1;
|
||||
|
||||
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
pub async fn encrypt(
|
||||
self,
|
||||
/// Constructs a vector of public keys for given peerstates.
|
||||
///
|
||||
/// In addition returns the set of recipient addresses
|
||||
/// for which there is no key available.
|
||||
///
|
||||
/// Returns an error if there are recipients
|
||||
/// other than self, but no recipient keys are available.
|
||||
pub(crate) fn encryption_keyring(
|
||||
&self,
|
||||
context: &Context,
|
||||
verified: bool,
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
peerstates: Vec<(Option<Peerstate>, String)>,
|
||||
compress: bool,
|
||||
) -> Result<String> {
|
||||
let mut keyring: Vec<SignedPublicKey> = Vec::new();
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<(Vec<SignedPublicKey>, BTreeSet<String>)> {
|
||||
// Encrypt to self unconditionally,
|
||||
// even for a single-device setup.
|
||||
let mut keyring = vec![self.public_key.clone()];
|
||||
let mut missing_key_addresses = BTreeSet::new();
|
||||
|
||||
if peerstates.is_empty() {
|
||||
return Ok((keyring, missing_key_addresses));
|
||||
}
|
||||
|
||||
let mut verifier_addresses: Vec<&str> = Vec::new();
|
||||
|
||||
for (peerstate, addr) in peerstates
|
||||
.iter()
|
||||
.filter_map(|(state, addr)| state.clone().map(|s| (s, addr)))
|
||||
{
|
||||
let key = peerstate
|
||||
.take_key(verified)
|
||||
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
|
||||
keyring.push(key);
|
||||
verifier_addresses.push(addr);
|
||||
for (peerstate, addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Some(key) = peerstate.clone().take_key(verified) {
|
||||
keyring.push(key);
|
||||
verifier_addresses.push(addr);
|
||||
} else {
|
||||
warn!(context, "Encryption key for {addr} is missing.");
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Peerstate for {addr} is missing.");
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt to self.
|
||||
keyring.push(self.public_key.clone());
|
||||
debug_assert!(
|
||||
!keyring.is_empty(),
|
||||
"At least our own key is in the keyring"
|
||||
);
|
||||
if keyring.len() <= 1 {
|
||||
bail!("No recipient keys are available, cannot encrypt");
|
||||
}
|
||||
|
||||
// Encrypt to secondary verified keys
|
||||
// if we also encrypt to the introducer ("verifier") of the key.
|
||||
if verified {
|
||||
for (peerstate, _addr) in &peerstates {
|
||||
for (peerstate, _addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let (Some(key), Some(verifier)) = (
|
||||
peerstate.secondary_verified_key.as_ref(),
|
||||
@@ -137,6 +126,17 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
Ok((keyring, missing_key_addresses))
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
pub async fn encrypt(
|
||||
self,
|
||||
context: &Context,
|
||||
keyring: Vec<SignedPublicKey>,
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
compress: bool,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
let mut raw_message = Vec::new();
|
||||
@@ -176,6 +176,7 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::config::Config;
|
||||
use crate::key::DcKey;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
@@ -228,8 +229,8 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
let chat_alice = alice.create_email_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_email_chat(&alice).await.id;
|
||||
|
||||
// Alice sends unencrypted message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
@@ -329,81 +330,20 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_should_encrypt() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert!(t.get_config_bool(Config::E2eeEnabled).await?);
|
||||
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
// Own preference is `Mutual` and we have the peer's key.
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Mutual);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
|
||||
// test with missing peerstate
|
||||
let ps = vec![(None, "bob@foo.bar".to_string())];
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_should_encrypt_e2ee_disabled() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
t.set_config_bool(Config::E2eeEnabled, false).await?;
|
||||
let encrypt_helper = EncryptHelper::new(t).await.unwrap();
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(encrypt_helper.should_encrypt(t, true, &ps).await?);
|
||||
|
||||
let mut ps = new_peerstates(EncryptPreference::Mutual);
|
||||
// Own preference is `NoPreference` and there's no majority with `Mutual`.
|
||||
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
// Now the majority wants to encrypt. Let's encrypt, anyway there are other cases when we
|
||||
// can't send unencrypted, e.g. protected groups.
|
||||
ps.push(ps[0].clone());
|
||||
assert!(encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
|
||||
// Test with missing peerstate.
|
||||
let ps = vec![(None, "bob@foo.bar".to_string())];
|
||||
assert!(encrypt_helper.should_encrypt(t, true, &ps).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chatmail_prefers_to_encrypt() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::IsChatmail, true).await?;
|
||||
|
||||
let bob_chat_id = tcm
|
||||
.send_recv_accept(alice, bob, "Hello from DC")
|
||||
.await
|
||||
.chat_id;
|
||||
receive_imf(
|
||||
bob,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 3000 22:37:58 +0000\n\
|
||||
\n\
|
||||
Hello from another MUA\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(bob, bob_chat_id, "hi".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,12 @@ pub enum EventType {
|
||||
timer: EphemeralTimer,
|
||||
},
|
||||
|
||||
/// Chat was deleted.
|
||||
ChatDeleted {
|
||||
/// Chat ID.
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked, deleted or changed their "recently seen" status.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
//! # Fuzzing module.
|
||||
//!
|
||||
//! This module exposes private APIs for fuzzing.
|
||||
|
||||
/// Fuzzing target for simplify().
|
||||
///
|
||||
/// Calls simplify() and panics if simplify() panics.
|
||||
/// Does not return any value to avoid exposing internal crate types.
|
||||
#[cfg(fuzzing)]
|
||||
pub fn simplify(input: String, is_chat_message: bool) {
|
||||
crate::simplify::simplify(input, is_chat_message);
|
||||
}
|
||||
@@ -57,6 +57,7 @@ pub enum HeaderDef {
|
||||
ChatGroupId,
|
||||
ChatGroupName,
|
||||
ChatGroupNameChanged,
|
||||
ChatGroupNameTimestamp,
|
||||
ChatVerified,
|
||||
ChatGroupAvatar,
|
||||
ChatUserAvatar,
|
||||
@@ -80,6 +81,9 @@ pub enum HeaderDef {
|
||||
ChatDispositionNotificationTo,
|
||||
ChatWebrtcRoom,
|
||||
|
||||
/// This message deletes the messages listed in the value by rfc724_mid.
|
||||
ChatDelete,
|
||||
|
||||
/// This message obsoletes the text of the message defined here by rfc724_mid.
|
||||
ChatEdit,
|
||||
|
||||
|
||||
43
src/html.rs
43
src/html.rs
@@ -285,7 +285,7 @@ mod tests {
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_htmlparse_plain_unspecified() {
|
||||
@@ -442,24 +442,25 @@ test some special html-characters as < > and & but also " and &#x
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_forwarding() {
|
||||
// alice receives a non-delta html-message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
receive_imf(&alice, raw, false).await.unwrap();
|
||||
receive_imf(alice, raw, false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::No);
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
// alice: create chat with bob and forward received html-message there
|
||||
let chat = alice.create_chat_with_contact("", "bob@example.net").await;
|
||||
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
|
||||
forward_msgs(alice, &[msg.get_id()], chat.get_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
@@ -468,11 +469,11 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(chat.id, msg.chat_id);
|
||||
@@ -481,7 +482,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
@@ -519,10 +520,11 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_forwarding_encrypted() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
// Alice receives a non-delta html-message
|
||||
// (`ShowEmails=AcceptedContacts` lets Alice actually receive non-delta messages for known
|
||||
// contacts, the contact is marked as known by creating a chat using `chat_with_contact()`)
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice = &tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("1"))
|
||||
.await
|
||||
@@ -531,19 +533,19 @@ test some special html-characters as < > and & but also " and &#x
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
receive_imf(&alice, raw, false).await.unwrap();
|
||||
receive_imf(alice, raw, false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// forward the message to saved-messages,
|
||||
// this will encrypt the message as new_alice() has set up keys
|
||||
let chat = alice.get_self_chat().await;
|
||||
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
|
||||
forward_msgs(alice, &[msg.get_id()], chat.get_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
|
||||
// receive the message on another device
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice = &tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("0"))
|
||||
.await
|
||||
@@ -556,38 +558,39 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_html() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// alice sends a message with html-part to bob
|
||||
let chat_id = alice.create_chat(&bob).await.id;
|
||||
let chat_id = alice.create_chat(bob).await.id;
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
assert!(msg.mime_modified);
|
||||
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
|
||||
chat::send_msg(alice, chat_id, &mut msg).await.unwrap();
|
||||
|
||||
// check the message is written correctly to alice's db
|
||||
let msg = alice.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_text(), "plain text");
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("<b>html</b> text"));
|
||||
|
||||
// let bob receive the message
|
||||
let chat_id = bob.create_chat(&alice).await.id;
|
||||
let chat_id = bob.create_chat(alice).await.id;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.chat_id, chat_id);
|
||||
assert_eq!(msg.get_text(), "plain text");
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("<b>html</b> text"));
|
||||
}
|
||||
|
||||
|
||||
@@ -271,12 +271,14 @@ impl Imap {
|
||||
let param = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
let strict_tls = param.strict_tls(proxy_config.is_some());
|
||||
let imap = Self::new(
|
||||
param.imap.clone(),
|
||||
param.imap_password.clone(),
|
||||
param.proxy_config.clone(),
|
||||
proxy_config,
|
||||
¶m.addr,
|
||||
param.strict_tls(),
|
||||
strict_tls,
|
||||
param.oauth2,
|
||||
idle_interrupt_receiver,
|
||||
);
|
||||
@@ -348,10 +350,11 @@ impl Imap {
|
||||
connection_candidate,
|
||||
)
|
||||
.await
|
||||
.context("IMAP failed to connect")
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
warn!(context, "IMAP failed to connect: {err:#}.");
|
||||
warn!(context, "{err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
continue;
|
||||
}
|
||||
|
||||
65
src/imex.rs
65
src/imex.rs
@@ -23,6 +23,7 @@ use crate::events::EventType;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::pgp;
|
||||
use crate::qr::DCBACKUP_VERSION;
|
||||
use crate::sql;
|
||||
use crate::tools::{
|
||||
create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file, TempPathGuard,
|
||||
@@ -138,7 +139,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Result<()> {
|
||||
async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
|
||||
// try hard to only modify key-state
|
||||
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
|
||||
let public_key = private_key.split_public_key()?;
|
||||
@@ -170,16 +171,7 @@ async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Re
|
||||
public: public_key,
|
||||
secret: private_key,
|
||||
};
|
||||
key::store_self_keypair(
|
||||
context,
|
||||
&keypair,
|
||||
if set_default {
|
||||
key::KeyPairUse::Default
|
||||
} else {
|
||||
key::KeyPairUse::ReadOnly
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
key::store_self_keypair(context, &keypair).await?;
|
||||
|
||||
info!(context, "stored self key: {:?}", keypair.secret.key_id());
|
||||
Ok(())
|
||||
@@ -209,7 +201,7 @@ async fn imex_inner(
|
||||
.await
|
||||
.context("Cannot create private key or private key not available")?;
|
||||
|
||||
create_folder(context, &path).await?;
|
||||
create_folder(context, path).await?;
|
||||
}
|
||||
|
||||
match what {
|
||||
@@ -401,6 +393,9 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
.await
|
||||
.context("cannot import unpacked database");
|
||||
}
|
||||
if res.is_ok() {
|
||||
res = check_backup_version(context).await;
|
||||
}
|
||||
if res.is_ok() {
|
||||
res = adjust_bcc_self(context).await;
|
||||
}
|
||||
@@ -599,10 +594,10 @@ where
|
||||
}
|
||||
|
||||
/// Imports secret key from a file.
|
||||
async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> {
|
||||
let buf = read_file(context, &path).await?;
|
||||
async fn import_secret_key(context: &Context, path: &Path) -> Result<()> {
|
||||
let buf = read_file(context, path).await?;
|
||||
let armored = std::string::String::from_utf8_lossy(&buf);
|
||||
set_self_key(context, &armored, set_default).await?;
|
||||
set_self_key(context, &armored).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -624,8 +619,7 @@ async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
|
||||
"Importing secret key from {} as the default key.",
|
||||
path.display()
|
||||
);
|
||||
let set_default = true;
|
||||
import_secret_key(context, path, set_default).await?;
|
||||
import_secret_key(context, path).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -643,14 +637,13 @@ async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let set_default = !name_f.contains("legacy");
|
||||
info!(
|
||||
context,
|
||||
"Considering key file: {}.",
|
||||
path_plus_name.display()
|
||||
);
|
||||
|
||||
if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await {
|
||||
if let Err(err) = import_secret_key(context, &path_plus_name).await {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to import secret key from {}: {:#}.",
|
||||
@@ -787,6 +780,10 @@ async fn export_database(
|
||||
.sql
|
||||
.set_raw_config_int("backup_time", timestamp)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("backup_version", DCBACKUP_VERSION)
|
||||
.await?;
|
||||
sql::housekeeping(context).await.log_err(context).ok();
|
||||
context
|
||||
.sql
|
||||
@@ -824,6 +821,15 @@ async fn adjust_bcc_self(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_backup_version(context: &Context) -> Result<()> {
|
||||
let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
|
||||
ensure!(
|
||||
version <= DCBACKUP_VERSION,
|
||||
"Backup too new, please update Delta Chat"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
@@ -871,7 +877,7 @@ mod tests {
|
||||
|
||||
assert_eq!(bytes, key.to_asc(None).into_bytes());
|
||||
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let alice = &TestContext::new().await;
|
||||
if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await {
|
||||
panic!("got error on import: {err:#}");
|
||||
}
|
||||
@@ -893,7 +899,7 @@ mod tests {
|
||||
panic!("got error on export: {err:#}");
|
||||
}
|
||||
|
||||
let context2 = TestContext::new_alice().await;
|
||||
let context2 = TestContext::new().await;
|
||||
if let Err(err) = imex(
|
||||
&context2.ctx,
|
||||
ImexMode::ImportSelfKeys,
|
||||
@@ -920,15 +926,18 @@ mod tests {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let old_key = key::load_self_secret_key(alice).await?;
|
||||
|
||||
imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
|
||||
let new_key = key::load_self_secret_key(alice).await?;
|
||||
assert_ne!(new_key, old_key);
|
||||
assert_eq!(
|
||||
key::load_self_secret_keyring(alice).await?,
|
||||
vec![new_key, old_key]
|
||||
assert!(
|
||||
imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Importing a second key is not allowed anymore,
|
||||
// even as a non-default key.
|
||||
assert_eq!(key::load_self_secret_key(alice).await?, old_key);
|
||||
|
||||
assert_eq!(key::load_self_secret_keyring(alice).await?, vec![old_key]);
|
||||
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
|
||||
|
||||
@@ -46,11 +46,11 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
|
||||
// Enable BCC-self, because transferring a key
|
||||
// means we have a multi-device setup.
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
Ok(setup_code)
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn continue_key_transfer(
|
||||
let file = open_file_std(context, filename)?;
|
||||
let sc = normalize_setup_code(setup_code);
|
||||
let armored_key = decrypt_setup_file(&sc, file).await?;
|
||||
set_self_key(context, &armored_key, true).await?;
|
||||
set_self_key(context, &armored_key).await?;
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -315,18 +315,19 @@ mod tests {
|
||||
alice2.recv_msg(&sent).await;
|
||||
let msg = alice2.get_last_msg().await;
|
||||
assert!(msg.is_setupmessage());
|
||||
|
||||
// Send a message that cannot be decrypted because the keys are
|
||||
// not synchronized yet.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
let trashed_message = alice.recv_msg_opt(&sent).await;
|
||||
assert!(trashed_message.is_none());
|
||||
assert_ne!(alice.get_last_msg().await.get_text(), "Test");
|
||||
assert_eq!(
|
||||
crate::key::load_self_secret_keyring(&alice2).await?.len(),
|
||||
0
|
||||
);
|
||||
|
||||
// Transfer the key.
|
||||
alice2.set_config(Config::BccSelf, Some("0")).await?;
|
||||
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
|
||||
assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(
|
||||
crate::key::load_self_secret_keyring(&alice2).await?.len(),
|
||||
1
|
||||
);
|
||||
|
||||
// Alice sends a message to self from the new device.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::message::Message;
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{create_id, time, TempPathGuard};
|
||||
use crate::EventType;
|
||||
use crate::{e2ee, EventType};
|
||||
|
||||
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
|
||||
|
||||
@@ -109,6 +109,11 @@ impl BackupProvider {
|
||||
.parent()
|
||||
.context("Context dir not found")?;
|
||||
|
||||
// before we export, make sure the private key exists
|
||||
e2ee::ensure_secret_key_exists(context)
|
||||
.await
|
||||
.context("Cannot create private key or private key not available")?;
|
||||
|
||||
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
|
||||
if fs::metadata(&dbfile).await.is_ok() {
|
||||
fs::remove_file(&dbfile).await?;
|
||||
|
||||
76
src/key.rs
76
src/key.rs
@@ -7,7 +7,6 @@ use std::io::Cursor;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
use pgp::ser::Serialize;
|
||||
@@ -15,8 +14,6 @@ use pgp::types::{PublicKeyTrait, SecretKeyTrait};
|
||||
use rand::thread_rng;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::pgp::KeyPair;
|
||||
@@ -282,14 +279,12 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
Some(key_pair) => Ok(key_pair),
|
||||
None => {
|
||||
let start = tools::Time::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
info!(context, "Generating keypair.");
|
||||
let keypair = Handle::current()
|
||||
.spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
|
||||
.spawn_blocking(move || crate::pgp::create_keypair(addr))
|
||||
.await??;
|
||||
|
||||
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
|
||||
store_self_keypair(context, &keypair).await?;
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
@@ -326,18 +321,6 @@ pub(crate) async fn load_keypair(context: &Context) -> Result<Option<KeyPair>> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Use of a key pair for encryption or decryption.
|
||||
///
|
||||
/// This is used by `store_self_keypair` to know what kind of key is
|
||||
/// being saved.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum KeyPairUse {
|
||||
/// The default key used to encrypt new messages.
|
||||
Default,
|
||||
/// Only used to decrypt existing message.
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
/// Store the keypair as an owned keypair for addr in the database.
|
||||
///
|
||||
/// This will save the keypair as keys for the given address. The
|
||||
@@ -350,11 +333,7 @@ pub enum KeyPairUse {
|
||||
/// same key again overwrites it.
|
||||
///
|
||||
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
|
||||
pub(crate) async fn store_self_keypair(
|
||||
context: &Context,
|
||||
keypair: &KeyPair,
|
||||
default: KeyPairUse,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) -> Result<()> {
|
||||
let mut config_cache_lock = context.sql.config_cache.write().await;
|
||||
let new_key_id = context
|
||||
.sql
|
||||
@@ -362,29 +341,28 @@ pub(crate) async fn store_self_keypair(
|
||||
let public_key = DcKey::to_bytes(&keypair.public);
|
||||
let secret_key = DcKey::to_bytes(&keypair.secret);
|
||||
|
||||
let is_default = match default {
|
||||
KeyPairUse::Default => true,
|
||||
KeyPairUse::ReadOnly => false,
|
||||
};
|
||||
|
||||
// private_key and public_key columns
|
||||
// are UNIQUE since migration 107,
|
||||
// so this fails if we already have this key.
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO keypairs (public_key, private_key)
|
||||
"INSERT INTO keypairs (public_key, private_key)
|
||||
VALUES (?,?)",
|
||||
(&public_key, &secret_key),
|
||||
)
|
||||
.context("Failed to insert keypair")?;
|
||||
|
||||
if is_default {
|
||||
let new_key_id = transaction.last_insert_rowid();
|
||||
transaction.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('key_id', ?)",
|
||||
(new_key_id,),
|
||||
)?;
|
||||
Ok(Some(new_key_id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
let new_key_id = transaction.last_insert_rowid();
|
||||
|
||||
// This will fail if we already have `key_id`.
|
||||
//
|
||||
// Setting default key is only possible if we don't
|
||||
// have a key already.
|
||||
transaction.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES ('key_id', ?)",
|
||||
(new_key_id,),
|
||||
)?;
|
||||
Ok(Some(new_key_id))
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -405,7 +383,7 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
|
||||
let secret = SignedSecretKey::from_asc(secret_data)?.0;
|
||||
let public = secret.split_public_key()?;
|
||||
let keypair = KeyPair { public, secret };
|
||||
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
|
||||
store_self_keypair(context, &keypair).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -483,6 +461,7 @@ mod tests {
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
|
||||
@@ -700,6 +679,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
assert_eq!(pubkey.primary_key, KEYPAIR.public.primary_key);
|
||||
}
|
||||
|
||||
/// Tests that setting a default key second time is not allowed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_self_key_twice() {
|
||||
// Saving the same key twice should result in only one row in
|
||||
@@ -714,13 +694,13 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(nrows().await, 0);
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
.await
|
||||
.unwrap();
|
||||
store_self_keypair(&ctx, &KEYPAIR).await.unwrap();
|
||||
assert_eq!(nrows().await, 1);
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Saving a second key fails.
|
||||
let res = store_self_keypair(&ctx, &KEYPAIR).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
assert_eq!(nrows().await, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) mod events;
|
||||
pub use events::*;
|
||||
|
||||
mod aheader;
|
||||
mod blob;
|
||||
pub mod blob;
|
||||
pub mod chat;
|
||||
pub mod chatlist;
|
||||
pub mod config;
|
||||
@@ -68,7 +68,7 @@ mod imap;
|
||||
pub mod imex;
|
||||
pub mod key;
|
||||
pub mod location;
|
||||
mod login_param;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
mod mimefactory;
|
||||
pub mod mimeparser;
|
||||
@@ -116,6 +116,3 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||
mod test_utils;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(fuzzing)]
|
||||
pub mod fuzzing;
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::fmt;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use num_traits::ToPrimitive as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -11,9 +12,11 @@ use crate::configure::server_params::{expand_param_vector, ServerParams};
|
||||
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
|
||||
use crate::context::Context;
|
||||
use crate::net::load_connection_timestamp;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
|
||||
pub use crate::net::proxy::ProxyConfig;
|
||||
pub use crate::provider::Socket;
|
||||
use crate::provider::{Protocol, Provider, UsernamePattern};
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::ToOption;
|
||||
|
||||
/// User-entered setting for certificate checks.
|
||||
///
|
||||
@@ -44,7 +47,7 @@ pub enum EnteredCertificateChecks {
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ConfiguredCertificateChecks {
|
||||
pub(crate) enum ConfiguredCertificateChecks {
|
||||
/// Use configuration from the provider database.
|
||||
/// If there is no provider database setting for certificate checks,
|
||||
/// accept invalid certificates.
|
||||
@@ -116,15 +119,13 @@ pub struct EnteredLoginParam {
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: EnteredCertificateChecks,
|
||||
|
||||
/// Proxy configuration.
|
||||
pub proxy_config: Option<ProxyConfig>,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore)
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
impl EnteredLoginParam {
|
||||
/// Loads entered account settings.
|
||||
pub async fn load(context: &Context) -> Result<Self> {
|
||||
pub(crate) async fn load(context: &Context) -> Result<Self> {
|
||||
let addr = context
|
||||
.get_config(Config::Addr)
|
||||
.await?
|
||||
@@ -196,8 +197,6 @@ impl EnteredLoginParam {
|
||||
.unwrap_or_default();
|
||||
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
|
||||
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
|
||||
Ok(EnteredLoginParam {
|
||||
addr,
|
||||
imap: EnteredServerLoginParam {
|
||||
@@ -215,10 +214,71 @@ impl EnteredLoginParam {
|
||||
password: send_pw,
|
||||
},
|
||||
certificate_checks,
|
||||
proxy_config,
|
||||
oauth2,
|
||||
})
|
||||
}
|
||||
|
||||
/// Saves entered account settings,
|
||||
/// so that they can be prefilled if the user wants to configure the server again.
|
||||
pub(crate) async fn save(&self, context: &Context) -> Result<()> {
|
||||
context.set_config(Config::Addr, Some(&self.addr)).await?;
|
||||
|
||||
context
|
||||
.set_config(Config::MailServer, self.imap.server.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailPort, self.imap.port.to_option().as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(
|
||||
Config::MailSecurity,
|
||||
self.imap.security.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailUser, self.imap.user.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailPw, self.imap.password.to_option())
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(Config::SendServer, self.smtp.server.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendPort, self.smtp.port.to_option().as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(
|
||||
Config::SendSecurity,
|
||||
self.smtp.security.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendUser, self.smtp.user.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendPw, self.smtp.password.to_option())
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(
|
||||
Config::ImapCertificateChecks,
|
||||
self.certificate_checks.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server_flags = if self.oauth2 {
|
||||
Some(DC_LP_AUTH_OAUTH2.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
context
|
||||
.set_config(Config::ServerFlags, server_flags.as_deref())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EnteredLoginParam {
|
||||
@@ -319,7 +379,7 @@ impl TryFrom<Socket> for ConnectionSecurity {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConfiguredServerLoginParam {
|
||||
pub(crate) struct ConfiguredServerLoginParam {
|
||||
pub connection: ConnectionCandidate,
|
||||
|
||||
/// Username.
|
||||
@@ -357,7 +417,7 @@ pub(crate) async fn prioritize_server_login_params(
|
||||
/// Login parameters saved to the database
|
||||
/// after successful configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConfiguredLoginParam {
|
||||
pub(crate) struct ConfiguredLoginParam {
|
||||
/// `From:` address that was used at the time of configuration.
|
||||
pub addr: String,
|
||||
|
||||
@@ -381,15 +441,13 @@ pub struct ConfiguredLoginParam {
|
||||
|
||||
pub smtp_password: String,
|
||||
|
||||
/// Proxy configuration.
|
||||
pub proxy_config: Option<ProxyConfig>,
|
||||
|
||||
pub provider: Option<&'static Provider>,
|
||||
|
||||
/// TLS options: whether to allow invalid certificates and/or
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: ConfiguredCertificateChecks,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore)
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
@@ -428,7 +486,7 @@ impl ConfiguredLoginParam {
|
||||
/// Load configured account settings from the database.
|
||||
///
|
||||
/// Returns `None` if account is not configured.
|
||||
pub async fn load(context: &Context) -> Result<Option<Self>> {
|
||||
pub(crate) async fn load(context: &Context) -> Result<Option<Self>> {
|
||||
if !context.get_config_bool(Config::Configured).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -681,8 +739,6 @@ impl ConfiguredLoginParam {
|
||||
}];
|
||||
}
|
||||
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
|
||||
Ok(Some(ConfiguredLoginParam {
|
||||
addr,
|
||||
imap,
|
||||
@@ -693,13 +749,12 @@ impl ConfiguredLoginParam {
|
||||
smtp_password: send_pw,
|
||||
certificate_checks,
|
||||
provider,
|
||||
proxy_config,
|
||||
oauth2,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Save this loginparam to the database.
|
||||
pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
|
||||
pub(crate) async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
|
||||
context.set_primary_self_addr(&self.addr).await?;
|
||||
|
||||
context
|
||||
@@ -776,11 +831,11 @@ impl ConfiguredLoginParam {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn strict_tls(&self) -> bool {
|
||||
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
|
||||
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
|
||||
match self.certificate_checks {
|
||||
ConfiguredCertificateChecks::OldAutomatic => {
|
||||
provider_strict_tls.unwrap_or(self.proxy_config.is_some())
|
||||
provider_strict_tls.unwrap_or(connected_through_proxy)
|
||||
}
|
||||
ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
|
||||
ConfiguredCertificateChecks::Strict => true,
|
||||
@@ -839,6 +894,42 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_entered_login_param() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let param = EnteredLoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
imap: EnteredServerLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 0,
|
||||
security: Socket::Starttls,
|
||||
user: "".to_string(),
|
||||
password: "foobar".to_string(),
|
||||
},
|
||||
smtp: EnteredServerLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 2947,
|
||||
security: Socket::default(),
|
||||
user: "".to_string(),
|
||||
password: "".to_string(),
|
||||
},
|
||||
certificate_checks: Default::default(),
|
||||
oauth2: false,
|
||||
};
|
||||
param.save(&t).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
assert_eq!(t.get_config(Config::MailPw).await?.unwrap(), "foobar");
|
||||
assert_eq!(t.get_config(Config::SendPw).await?, None);
|
||||
assert_eq!(t.get_config_int(Config::SendPort).await?, 2947);
|
||||
|
||||
assert_eq!(EnteredLoginParam::load(&t).await?, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_load_login_param() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -865,8 +956,6 @@ mod tests {
|
||||
}],
|
||||
smtp_user: "".to_string(),
|
||||
smtp_password: "bar".to_string(),
|
||||
// proxy_config is not saved by `save_to_database`, using default value
|
||||
proxy_config: None,
|
||||
provider: None,
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
@@ -969,7 +1058,6 @@ mod tests {
|
||||
],
|
||||
smtp_user: "alice@posteo.de".to_string(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
proxy_config: None,
|
||||
provider: get_provider_by_id("posteo"),
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
|
||||
194
src/message.rs
194
src/message.rs
@@ -1,6 +1,7 @@
|
||||
//! # Messages and their identifiers.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
|
||||
@@ -11,11 +12,11 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::{fs, io};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility};
|
||||
use crate::chat::{send_msg, Chat, ChatId, ChatIdBlocked, ChatVisibility};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
|
||||
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
@@ -31,9 +32,10 @@ use crate::pgp::split_armored_data;
|
||||
use crate::reaction::get_msg_reactions;
|
||||
use crate::sql;
|
||||
use crate::summary::Summary;
|
||||
use crate::sync::SyncData;
|
||||
use crate::tools::{
|
||||
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
|
||||
sanitize_filename, time, timestamp_to_str, truncate,
|
||||
sanitize_filename, time, timestamp_to_str,
|
||||
};
|
||||
|
||||
/// Message ID, including reserved IDs.
|
||||
@@ -172,15 +174,6 @@ impl MsgId {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns raw text of a message, used for message info
|
||||
pub async fn rawtext(self, context: &Context) -> Result<String> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns server foldernames and UIDs of a message, used for message info
|
||||
pub async fn get_info_server_urls(
|
||||
context: &Context,
|
||||
@@ -217,12 +210,9 @@ impl MsgId {
|
||||
/// Returns detailed message information in a multi-line text form.
|
||||
pub async fn get_info(self, context: &Context) -> Result<String> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
let rawtxt: String = self.rawtext(context).await?;
|
||||
|
||||
let mut ret = String::new();
|
||||
|
||||
let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
|
||||
|
||||
let fts = timestamp_to_str(msg.get_timestamp());
|
||||
ret += &format!("Sent: {fts}");
|
||||
|
||||
@@ -333,9 +323,6 @@ impl MsgId {
|
||||
if duration != 0 {
|
||||
ret += &format!("Duration: {duration} ms\n",);
|
||||
}
|
||||
if !rawtxt.is_empty() {
|
||||
ret += &format!("\n{rawtxt}\n");
|
||||
}
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
|
||||
|
||||
@@ -973,7 +960,7 @@ impl Message {
|
||||
}
|
||||
|
||||
if let Some(filename) = self.get_file(context) {
|
||||
if let Ok(ref buf) = read_file(context, filename).await {
|
||||
if let Ok(ref buf) = read_file(context, &filename).await {
|
||||
if let Ok((typ, headers, _)) = split_armored_data(buf) {
|
||||
if typ == pgp::armor::BlockType::Message {
|
||||
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
|
||||
@@ -1360,7 +1347,7 @@ impl Message {
|
||||
/// * Lack of valid signature on an e2ee message, usually for received messages.
|
||||
/// * Failure to decrypt an e2ee message, usually for received messages.
|
||||
/// * When a message could not be delivered to one or more recipients the non-delivery
|
||||
/// notification text can be stored in the error status.
|
||||
/// notification text can be stored in the error status.
|
||||
pub fn error(&self) -> Option<String> {
|
||||
self.error.clone()
|
||||
}
|
||||
@@ -1594,14 +1581,12 @@ pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &
|
||||
}
|
||||
|
||||
/// Get the raw mime-headers of the given message.
|
||||
/// Raw headers are saved for incoming messages
|
||||
/// only if `set_config(context, "save_mime_headers", "1")`
|
||||
/// was called before.
|
||||
/// Raw headers are saved for large messages
|
||||
/// that need a "Show full message..."
|
||||
/// to see HTML part.
|
||||
///
|
||||
/// Returns an empty vector if there are no headers saved for the given message,
|
||||
/// e.g. because of save_mime_headers is not set
|
||||
/// or the message is not incoming.
|
||||
pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
|
||||
/// Returns an empty vector if there are no headers saved for the given message.
|
||||
pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
|
||||
let (headers, compressed) = context
|
||||
.sql
|
||||
.query_row(
|
||||
@@ -1651,34 +1636,94 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
/// Deletes requested messages
|
||||
/// by moving them to the trash chat
|
||||
/// and scheduling for deletion on IMAP.
|
||||
/// Delete a single message from the database, including references in other tables.
|
||||
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
|
||||
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
let on_server = true;
|
||||
msg.id
|
||||
.trash(context, on_server)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {}", msg.id))?;
|
||||
|
||||
context.emit_event(EventType::MsgDeleted {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
|
||||
}
|
||||
|
||||
let logging_xdc_id = context
|
||||
.debug_logging
|
||||
.read()
|
||||
.expect("RwLock is poisoned")
|
||||
.as_ref()
|
||||
.map(|dl| dl.msg_id);
|
||||
if let Some(id) = logging_xdc_id {
|
||||
if id == msg.id {
|
||||
set_debug_logging_xdc(context, None).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Do final events and jobs after batch deletion using calls to delete_msg_locally().
|
||||
/// To avoid additional database queries, collecting data is up to the caller.
|
||||
pub(crate) async fn delete_msgs_locally_done(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
modified_chat_ids: HashSet<ChatId>,
|
||||
) -> Result<()> {
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
|
||||
}
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
// Run housekeeping to delete unused blobs.
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete messages on all devices and on IMAP.
|
||||
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
delete_msgs_ex(context, msg_ids, false).await
|
||||
}
|
||||
|
||||
/// Delete messages on all devices, on IMAP and optionally for all chat members.
|
||||
/// Deleted messages are moved to the trash chat and scheduling for deletion on IMAP.
|
||||
/// When deleting messages for others, all messages must be self-sent and in the same chat.
|
||||
pub async fn delete_msgs_ex(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
delete_for_all: bool,
|
||||
) -> Result<()> {
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut deleted_rfc724_mid = Vec::new();
|
||||
let mut res = Ok(());
|
||||
|
||||
for &msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
let on_server = true;
|
||||
msg_id
|
||||
.trash(context, on_server)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {msg_id}"))?;
|
||||
|
||||
context.emit_event(EventType::MsgDeleted {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id,
|
||||
});
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
|
||||
}
|
||||
ensure!(
|
||||
!delete_for_all || msg.from_id == ContactId::SELF,
|
||||
"Can delete only own messages for others"
|
||||
);
|
||||
ensure!(
|
||||
!delete_for_all || msg.get_showpadlock(),
|
||||
"Cannot request deletion of unencrypted message for others"
|
||||
);
|
||||
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||
|
||||
let target = context.get_delete_msgs_target().await?;
|
||||
let update_db = |trans: &mut rusqlite::Transaction| {
|
||||
@@ -1694,38 +1739,43 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
res = Err(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
let logging_xdc_id = context
|
||||
.debug_logging
|
||||
.read()
|
||||
.expect("RwLock is poisoned")
|
||||
.as_ref()
|
||||
.map(|dl| dl.msg_id);
|
||||
|
||||
if let Some(id) = logging_xdc_id {
|
||||
if id == msg_id {
|
||||
set_debug_logging_xdc(context, None).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
res?;
|
||||
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
|
||||
}
|
||||
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
// Run housekeeping to delete unused blobs.
|
||||
if delete_for_all {
|
||||
ensure!(
|
||||
modified_chat_ids.len() == 1,
|
||||
"Can delete only from same chat."
|
||||
);
|
||||
if let Some(chat_id) = modified_chat_ids.iter().next() {
|
||||
let mut msg = Message::new_text("🚮".to_owned());
|
||||
// We don't want to send deletion requests in chats w/o encryption:
|
||||
// - These are usually chats with non-DC clients who won't respect deletion requests
|
||||
// anyway and display a weird trash bin message instead.
|
||||
// - Deletion of world-visible unencrypted messages seems not very useful.
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
msg.param
|
||||
.set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
|
||||
msg.hidden = true;
|
||||
send_msg(context, *chat_id, &mut msg).await?;
|
||||
}
|
||||
} else {
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.add_sync_item(SyncData::DeleteMessages {
|
||||
msgs: deleted_rfc724_mid,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Interrupt Inbox loop to start message deletion and run housekeeping.
|
||||
for &msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
delete_msg_locally(context, &msg).await?;
|
||||
}
|
||||
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
|
||||
|
||||
// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
@@ -106,7 +106,7 @@ async fn test_create_webrtc_instance_noroom() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_width_height() {
|
||||
let t = test::TestContext::new().await;
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// test that get_width() and get_height() are returning some dimensions for images;
|
||||
// (as the device-chat contains a welcome-images, we check that)
|
||||
@@ -136,7 +136,7 @@ async fn test_get_width_height() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote() {
|
||||
let d = test::TestContext::new().await;
|
||||
let d = TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
|
||||
@@ -756,6 +756,37 @@ async fn test_delete_msgs_offline() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_msgs_sync() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice.create_chat(bob).await.id;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
bob.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice sends a messsage and receives it on the other device
|
||||
let sent1 = alice.send_text(alice_chat_id, "foo").await;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1);
|
||||
|
||||
let msg = alice2.recv_msg(&sent1).await;
|
||||
let alice2_chat_id = msg.chat_id;
|
||||
assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
|
||||
// Alice deletes the message; this should happen on both devices as well
|
||||
delete_msgs(alice, &[sent1.sender_msg_id]).await?;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0);
|
||||
|
||||
test_utils::sync(alice, alice2).await;
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sanitize_filename_message() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user