mirror of
https://github.com/chatmail/core.git
synced 2026-10-11 23:50:16 +03:00
Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8b9fd5005 | ||
|
|
694ef8ba76 | ||
|
|
de6f469710 | ||
|
|
81140d514f | ||
|
|
a8b6246052 | ||
|
|
4ea5cddc09 | ||
|
|
f8edd9136a | ||
|
|
697304e98a | ||
|
|
40c0954e42 | ||
|
|
3a79324728 | ||
|
|
6ab0cd513a | ||
|
|
0bee156449 | ||
|
|
9ce20d7763 | ||
|
|
d7376e32b2 | ||
|
|
6378533b6a | ||
|
|
1451478911 | ||
|
|
8468f5b5dd | ||
|
|
dbba555a63 | ||
|
|
8cedea1958 | ||
|
|
70730490b2 | ||
|
|
9578c2c054 | ||
|
|
9e2d668490 | ||
|
|
d18718d2b9 | ||
|
|
77bb77c33e | ||
|
|
1143317fd1 | ||
|
|
83211e440e | ||
|
|
7cdfb234a3 | ||
|
|
2a9bc732bf | ||
|
|
90387fdbea | ||
|
|
29006e47c5 | ||
|
|
79bf0c79a5 | ||
|
|
e5d1bf7504 | ||
|
|
32d40cbb14 | ||
|
|
4e83357371 | ||
|
|
88deea5b18 | ||
|
|
8883e5ada0 | ||
|
|
ca2a40e210 | ||
|
|
1cd2957a4d | ||
|
|
4d57b8bf32 | ||
|
|
e203ae9571 | ||
|
|
a37899b365 | ||
|
|
3173487c9f | ||
|
|
aade549ccc | ||
|
|
736979b451 | ||
|
|
5a882b0aca | ||
|
|
1e36fb74be | ||
|
|
ebf1c87483 | ||
|
|
172ebc08f9 | ||
|
|
eeb9337a56 | ||
|
|
a1eb593d81 | ||
|
|
7e070efc28 | ||
|
|
5d3145d165 | ||
|
|
1605b971ba | ||
|
|
22578ea4b6 | ||
|
|
076f83f320 | ||
|
|
8ab98019a7 | ||
|
|
76d664f9cb | ||
|
|
f0e8e80894 | ||
|
|
3756537a13 | ||
|
|
2ad4301f44 | ||
|
|
6f4587a030 | ||
|
|
c41cac76d2 | ||
|
|
3de65e4f0e | ||
|
|
2a5058a308 | ||
|
|
b79125b947 | ||
|
|
ed647aa49c | ||
|
|
8858c01f3f | ||
|
|
54b093d89f | ||
|
|
b6f116d26a | ||
|
|
362cc76c83 | ||
|
|
aa0800b6e0 | ||
|
|
5a5597e990 | ||
|
|
94cc8a3858 | ||
|
|
6b62fed6fa | ||
|
|
b766a9a8fa | ||
|
|
3d3a6352e9 | ||
|
|
0eddb98354 | ||
|
|
72e711e59a | ||
|
|
a144a450e1 | ||
|
|
b87cd8937a | ||
|
|
fae6a63023 | ||
|
|
54e16429b8 | ||
|
|
946120e427 | ||
|
|
e1d58706ce | ||
|
|
7876115d86 | ||
|
|
0a7e697f50 | ||
|
|
06150d6dc2 | ||
|
|
310dda494c | ||
|
|
8f39d7510f | ||
|
|
44ab013351 | ||
|
|
f7c97d8557 | ||
|
|
78caa6f53c | ||
|
|
349c9650e3 | ||
|
|
96e84d85a5 | ||
|
|
57b2c000e6 | ||
|
|
8014c2af2f | ||
|
|
5db55ac4de |
+17
-14
@@ -20,16 +20,21 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.98.1
|
||||
RUST_VERSION: 1.99.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
|
||||
PYTHON_VERSION: 3.15
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
# Tests always unwind: match it so dependencies are only checked once.
|
||||
CARGO_PROFILE_DEV_PANIC: unwind
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -48,8 +53,6 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
run: scripts/clippy.sh
|
||||
- name: Check with all features
|
||||
run: cargo check --workspace --all-targets --all-features
|
||||
- name: Check with only default features
|
||||
run: cargo check --all-targets
|
||||
|
||||
@@ -132,19 +135,19 @@ jobs:
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
|
||||
uses: taiki-e/install-action@9983c65e42da123ff25d1f78505eb6de315aa172
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo nextest run --workspace --locked
|
||||
run: cargo nextest run --workspace --exclude deltachat-jsonrpc-bindings --locked
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --locked --doc
|
||||
run: cargo test --workspace --exclude deltachat-jsonrpc-bindings --locked --doc
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
@@ -267,11 +270,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
# Currently used Python version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -302,7 +305,7 @@ jobs:
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
@@ -323,11 +326,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
- os: windows-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -350,7 +353,7 @@ jobs:
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- run: nix fmt flake.nix nix/ -- --check
|
||||
|
||||
build:
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -106,5 +106,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
|
||||
@@ -23,4 +23,4 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||
|
||||
+189
@@ -1,5 +1,191 @@
|
||||
# Changelog
|
||||
|
||||
## [2.63.0] - 2026-10-09
|
||||
|
||||
### API-Changes
|
||||
|
||||
- add `PinnedMessagesChanged` event.
|
||||
- [**breaking**]: remove unused `dc_chatlist_get_summary2()` ([#8812](https://github.com/chatmail/core/pull/8812))
|
||||
- new `dc_msg_get_summary_text()` ([#8813](https://github.com/chatmail/core/pull/8813)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- connect to the most recently successfully used SMTP transport first.
|
||||
- re-add "second device added" message if deleted soon after transfer ([#8758](https://github.com/chatmail/core/pull/8758)).
|
||||
- do not request MDNs for unencrypted messages.
|
||||
- do not send unencrypted MDNs.
|
||||
- add context to HTTP(S) proxy connection errors.
|
||||
- deltachat-repl: remove "reset" command.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Correctly percent-encode addresses in securejoin invite codes ([#8747](https://github.com/chatmail/core/pull/8747)).
|
||||
- regenerate deltachat.pc if env changed ([#8752](https://github.com/chatmail/core/pull/8752)).
|
||||
- delete handled securejoin messages on all relays.
|
||||
- add headers to unencrypted MDNs.
|
||||
- In SMTP loop, reset `transport_id` and `from` when disconnecting ([#8797](https://github.com/chatmail/core/pull/8797)).
|
||||
- deltachat-rpc-client: shutdown all threads after RPC server crash.
|
||||
- (linux-only) make sure large attachments return memory to kernel.
|
||||
- do not reencode animated WebPs into JPEG.
|
||||
|
||||
### CI
|
||||
|
||||
- update Rust to 1.99.0.
|
||||
- speedup lint job and the Rust test builds
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] stop saving legacy config values when configuring a transport.
|
||||
- core does not set deprecated config values `addr`, `mail_pw` etc. Use `list_transports()` API instead.
|
||||
- get rid of dead sticker-related code in check_or_recode_to_size().
|
||||
- factor add_headers_to_part() out of mimefactory::add_headers_to_protected_part().
|
||||
- replace MimeFactory::from_mdn() with a standalone mimefactory::mdn().
|
||||
- take securejoin addresses from transport list.
|
||||
- deltachat-repl: move all commands to cmdline.rs.
|
||||
- do not guess image format from file extension on I/O error.
|
||||
- reduce macro-generated lines by >70%, and drop tracing.
|
||||
- stop excluding `COPYUID` responses from the logs.
|
||||
|
||||
### Tests
|
||||
|
||||
- move pinned messages tests into pinned_messages_tests module.
|
||||
- Add basic tests for rendering unencrypted messages ([#8736](https://github.com/chatmail/core/pull/8736)).
|
||||
- do not ignore errors in add_pseudo_transport().
|
||||
- test sending and receiving long filename with emojis.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- update rPGP from 0.20.0 to 0.21.0.
|
||||
- reduce noise created by key-contact migration.
|
||||
- enable `clippy::unnecessary_wraps`, fix `clippy::redundant_clone` and `clippy::string_lit_as_bytes` suggestions.
|
||||
- add chatmail.au.
|
||||
- add chatmail.cc ([#8756](https://github.com/chatmail/core/pull/8756)).
|
||||
- remove some `unwrap()` calls.
|
||||
- remove unused "sdp" dependency.
|
||||
- cargo: bump smallvec from 1.15.2 to 1.16.1.
|
||||
- cargo: bump syn from 3.0.4 to 3.0.6.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.6.2 to 0.6.4.
|
||||
- cargo: bump mailparse from 0.16.1 to 0.17.0.
|
||||
- cargo: bump uuid from 1.25.0 to 1.26.1.
|
||||
- deps: bump cachix/install-nix-action from 31.11.0 to 31.11.1.
|
||||
- cargo: bump hyper from 1.10.1 to 1.11.1.
|
||||
- cargo: bump tokio-rustls from 0.26.4 to 0.26.5.
|
||||
- cargo: bump thiserror from 2.0.20 to 2.0.21.
|
||||
- deps: bump taiki-e/install-action from 2.86.7 to 2.87.20.
|
||||
- cargo: bump hyper-util from 0.1.20 to 0.1.21.
|
||||
- cargo: bump dirs from 6.0.0 to 7.0.0.
|
||||
- move deltachat-time to dev dependencies.
|
||||
- update astral-tokio-tar from 0.6.4 to 0.7.0.
|
||||
- update async-imap to 0.12.0.
|
||||
- update mail-builder to 1.0.
|
||||
|
||||
## [2.62.0] - 2026-09-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Remove unused ImapMessageMoved event.
|
||||
- [**breaking**] get rid of `Qr::FprWithoutAddr` and `Qr::FprMismatch` variants.
|
||||
- removed `DC_QR_FPR_WITHOUT_ADDR` and `DC_QR_FPR_MISMATCH` constants from CFFI
|
||||
- removed FprWithoutAddr and FprMismatch variants from JSON-RPC QrObject returned by `check_qr`
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Don't show "member added" messages in channels ([#8730](https://github.com/chatmail/core/pull/8730)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't notify about a reaction sent by a blocked contact ([#8729](https://github.com/chatmail/core/pull/8729)).
|
||||
- keep a message read status if a failure arrives later.
|
||||
- prefer login errors over connection errors in the log message ([#8727](https://github.com/chatmail/core/pull/8727)).
|
||||
- blocked or email contacts contacts are never recently seen or old ([#8733](https://github.com/chatmail/core/pull/8733)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove code that moves messages between IMAP folders.
|
||||
|
||||
## [2.61.0] - 2026-09-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- add `init_transports()` for multi-relay onboarding.
|
||||
- add JSON-RPC API `is_sending_finished()`.
|
||||
- [**breaking**] remove verification methods from the FFI and JSON-RPC APIs.
|
||||
- `dc_contact_is_verified()` and `dc_contact_get_verifier_id()` are removed.
|
||||
- the JSON-RPC Contact object loses the `isVerified` and `verifierId` fields. A bot reading `snapshot.is_verified` now gets an `AttributeError` at runtime.
|
||||
- the Python bindings lose `Contact.is_verified()` and `Contact.get_verifier()`.
|
||||
- `DC_STR_CONTACT_VERIFIED` (35) is removed, so UIs should stop registering a translation for it. A stock id core does not know is logged and otherwise ignored, so an un-updated client keeps working.
|
||||
- [**breaking**] remove default value for "addr" config.
|
||||
- [**breaking**] remove `addr` field from Account objects in JSON-RPC APIs.
|
||||
- `list_transports()` should be used instead.
|
||||
- [**breaking**] remove `is_chatmail` and the XCHATMAIL capability.
|
||||
- `is_chatmail` is no longer a known config key.
|
||||
- [**breaking**] remove `Contact.get_name_n_addr()` and related APIs.
|
||||
- `dc_contact_get_name_n_addr()` CFFI is removed
|
||||
- JSON-RPC contact objects don't have nameAndAddr field anymore
|
||||
- [**breaking**] replace `was_seen_recently` by `freshness` in contact object.
|
||||
- use contact's `freshness` instead of `seen_recently`
|
||||
|
||||
### Fixes
|
||||
|
||||
- always emit `AccountsBackgroundFetchDone`.
|
||||
- make `background_fetch` not wait on or trigger SMTP connections.
|
||||
- do not send a sync message when changing `configured_addr`.
|
||||
- emit `SmtpMessageSent` event after deleting the message from SMTP queue.
|
||||
- don't use extra STUN nine server for fallback.
|
||||
- use `max_smtp_rcpt_to` chunking for the actual transport we are sending from.
|
||||
- use correct `From` address when sending MDNs.
|
||||
- use correct address for Bcc-self in unencrypted mails.
|
||||
- don't emit configure progress events during background relay additions.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- better quality of image recoding ([#8682](https://github.com/chatmail/core/pull/8682)).
|
||||
- perform background fetch from all transports.
|
||||
- queue messages for SMTP before encryption.
|
||||
- [**breaking**] stop tracking contact verification.
|
||||
- the statistics JSON sent to the self-reporting-bot on Android changes: Contacts have `encrypted` instead of `verified` and lose `transitive_chain` properties and message stats have `encrypted` instead of `verified` and `unverified_encrypted`, and securejoin invites lose `already_verified`. The collecting bot stores incoming reports verbatim but analysis will have to make sense of older and newer reports.
|
||||
- remove last usage of XDELTAPUSH capability.
|
||||
- base server-side message deletion on `force_encryption`.
|
||||
- do not restart I/O when setting `configured_addr`.
|
||||
- do not use ConfiguredAddr when connecting to SMTP.
|
||||
- mark autorelays for relay operators ([#8701](https://github.com/chatmail/core/pull/8701)).
|
||||
- try fasted relays to attempt first configure on.
|
||||
- do not mark message as failed for which we got a read receipt before.
|
||||
- a single NDN does not mark a group message as failed.
|
||||
|
||||
### Documentation
|
||||
|
||||
- update `sys.msgsize_max_recommended` documentation.
|
||||
- add hint about how to reset an invitation ([#8160](https://github.com/chatmail/core/pull/8160)).
|
||||
- mention `get_app_version()` API in the changelog for 2.59.0.
|
||||
- remove `protect_autocrypt` setting.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cleanup "primary" wording in comments.
|
||||
- update rustls to 0.23.45.
|
||||
- fix nightly "cargo" warnings.
|
||||
- fix some types in `deltachat_rpc_client`.
|
||||
|
||||
### Refactor
|
||||
|
||||
- add `Encryption.is_encrypted()`.
|
||||
- separate QueuedEncryption.
|
||||
- sql: disable double-quoted string literals.
|
||||
- add smtp::queue module.
|
||||
- substitute configure progress macro with simple function call.
|
||||
|
||||
### Tests
|
||||
|
||||
- cleanup `get_smtp_rows_for_msg()`.
|
||||
- cross-core securejoin invites for every chat type.
|
||||
- explicitly empty url in appversion updates ([#8702](https://github.com/chatmail/core/pull/8702)).
|
||||
- do not talk about "inconsistent key state" in `test_securejoin_after_contact_resetup`.
|
||||
- rename `test_aeap_transition_{0,1}` to `test_aeap_transition_{single,group}`.
|
||||
- do not fetch all messages when `direct_imap` is created.
|
||||
- `direct_imap`: always pass `mark_seen=False` to fetch().
|
||||
- add pseudo transport explicitly rather than by setting ConfiguredAddr.
|
||||
|
||||
## [2.60.0] - 2026-09-11
|
||||
|
||||
### API-Changes
|
||||
@@ -8794,3 +8980,6 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.58.0]: https://github.com/chatmail/core/compare/v2.57.0..v2.58.0
|
||||
[2.59.0]: https://github.com/chatmail/core/compare/v2.58.0..v2.59.0
|
||||
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0
|
||||
[2.61.0]: https://github.com/chatmail/core/compare/v2.60.0..v2.61.0
|
||||
[2.62.0]: https://github.com/chatmail/core/compare/v2.61.0..v2.62.0
|
||||
[2.63.0]: https://github.com/chatmail/core/compare/v2.62.0..v2.63.0
|
||||
|
||||
Generated
+110
-104
@@ -194,9 +194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.6.4"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463"
|
||||
checksum = "6f2e989b33246fe9240d39accf4dd9a01e0b6c1f3ce9dd095e0a47fa02505523"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"libc",
|
||||
@@ -205,6 +205,7 @@ dependencies = [
|
||||
"rustix 1.1.4",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"zerocopy 0.8.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -271,19 +272,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.11.3"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a6728e0f7931b36d725ac234fcb02539e9f7888dbeaaa8a18d9ea5792181570"
|
||||
checksum = "f97b87216c9f0ccc63c516263169085fa34bba25633f6d96c6a9b9ca1fd5f70a"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-compression",
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"imap-proto",
|
||||
"log",
|
||||
"nom 7.1.3",
|
||||
"nom 8.0.0",
|
||||
"pin-project",
|
||||
"pin-utils",
|
||||
"self_cell",
|
||||
@@ -310,7 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -322,12 +323,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55219982f938e74491ba85dc4e49cefe8096b1e8f49348c67180a7d244988dca"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"futures",
|
||||
"log",
|
||||
"nom 8.0.0",
|
||||
"pin-project",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -363,7 +364,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"futures-lite",
|
||||
"pin-project",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -435,6 +436,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
@@ -459,7 +466,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -832,7 +839,7 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
@@ -1327,7 +1334,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1337,7 +1344,7 @@ dependencies = [
|
||||
"async-native-tls",
|
||||
"async-smtp",
|
||||
"async_zip",
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"blake3",
|
||||
"brotli",
|
||||
"bytes",
|
||||
@@ -1386,7 +1393,6 @@ dependencies = [
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"sanitize-filename",
|
||||
"sdp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha-1",
|
||||
@@ -1399,14 +1405,13 @@ dependencies = [
|
||||
"tempfile",
|
||||
"testdir",
|
||||
"textwrap",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-io-timeout",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
@@ -1435,11 +1440,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"deltachat",
|
||||
"deltachat-contact-tools",
|
||||
"futures",
|
||||
@@ -1456,14 +1461,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
dependencies = [
|
||||
"deltachat-jsonrpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1479,12 +1484,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
"deltachat-jsonrpc",
|
||||
"futures-lite",
|
||||
"libc",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1503,12 +1509,12 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1518,7 +1524,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.9.4",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"yerpc",
|
||||
]
|
||||
@@ -1677,9 +1683,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
@@ -2222,7 +2228,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2465,7 +2471,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2488,7 +2494,7 @@ dependencies = [
|
||||
"rand 0.9.4",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2654,9 +2660,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -2694,15 +2700,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"hyper",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
@@ -2940,11 +2947,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imap-proto"
|
||||
version = "0.16.5"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de555d9526462b6f9ece826a26fb7c67eca9a0245bd9ff84fa91972a5d5d8856"
|
||||
checksum = "5ccf963d57074747b455398a1763d174da80bcaba6f51e3671a82252b531a68b"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
"nom 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3045,7 +3052,7 @@ dependencies = [
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"surge-ping",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -3070,7 +3077,7 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -3112,7 +3119,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"serde-error",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -3157,7 +3164,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3177,7 +3184,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3232,7 +3239,7 @@ dependencies = [
|
||||
"sha1",
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
@@ -3444,15 +3451,15 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
|
||||
|
||||
[[package]]
|
||||
name = "mail-builder"
|
||||
version = "0.5.0"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
|
||||
checksum = "ccbc27cb02a58a311a404769a22f5abf63fe3173b290d80be7c534f9c7023151"
|
||||
|
||||
[[package]]
|
||||
name = "mailparse"
|
||||
version = "0.16.1"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f"
|
||||
checksum = "8469955699e572455f8e1a40689e07ab309911d659632f65062598192254c091"
|
||||
dependencies = [
|
||||
"charset",
|
||||
"data-encoding",
|
||||
@@ -3717,7 +3724,7 @@ dependencies = [
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-sys",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4180,7 +4187,7 @@ version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4206,7 +4213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@@ -4246,16 +4253,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pgp"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfa4743b28656065ff4c0ba09e46b357a65e8c00fc2341e89084b82f87cbdf1"
|
||||
checksum = "ae70f4d9325a391db30d115d6191a7bb67cec856aa0bba131290f0c4e09a532a"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"aes-kw",
|
||||
"argon2",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitfields",
|
||||
"block-padding",
|
||||
"blowfish",
|
||||
@@ -4382,7 +4389,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sha1_smol",
|
||||
"simple-dns",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -4534,7 +4541,7 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"derive_more 1.0.0",
|
||||
"futures-lite",
|
||||
@@ -4595,7 +4602,7 @@ version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4780,7 +4787,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -4801,7 +4808,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -5040,7 +5047,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5090,7 +5097,7 @@ version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -5444,18 +5451,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdp"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22c3b0257608d7de4de4c4ea650ccc2e6e3e45e3cd80039fcdee768bcb449253"
|
||||
dependencies = [
|
||||
"rand 0.9.4",
|
||||
"substring",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -5561,7 +5556,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5696,7 +5691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78db9c9912c90ea7487f49bc149b329b535806bfa12b740fbade73f573a3d9f"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"blake3",
|
||||
"byte_string",
|
||||
"bytes",
|
||||
@@ -5717,7 +5712,7 @@ dependencies = [
|
||||
"shadowsocks-crypto",
|
||||
"socket2 0.5.9",
|
||||
"spin 0.10.1",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-tfo",
|
||||
"trait-variant",
|
||||
@@ -5824,14 +5819,14 @@ dependencies = [
|
||||
"sha3",
|
||||
"signature",
|
||||
"typenum",
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
@@ -5996,7 +5991,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bounded-integer",
|
||||
"byteorder",
|
||||
"crc",
|
||||
@@ -6014,15 +6009,6 @@ dependencies = [
|
||||
"rand 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "substring"
|
||||
version = "1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -6069,9 +6055,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.4"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6215,11 +6201,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.20",
|
||||
"thiserror-impl 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6235,13 +6221,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6361,9 +6347,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
@@ -6420,7 +6406,7 @@ version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -6763,9 +6749,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.25.0"
|
||||
version = "1.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
|
||||
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -7429,7 +7415,7 @@ dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"windows 0.59.0",
|
||||
"windows-core 0.59.0",
|
||||
]
|
||||
@@ -7603,7 +7589,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
"zerocopy-derive 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7617,6 +7612,17 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.5"
|
||||
|
||||
+8
-10
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -36,7 +36,6 @@ strip = true
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
deltachat-contact-tools = { workspace = true }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
@@ -44,7 +43,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.12.0", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -70,7 +69,7 @@ iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.5.0", default-features = false }
|
||||
mail-builder = { version = "1.0.0", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.17"
|
||||
@@ -78,7 +77,7 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.20.0", features = ["draft-pqc"], default-features = false }
|
||||
pgp = { version = "0.21.0", features = ["pqc"], default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.41", features = ["escape-html"] }
|
||||
@@ -87,7 +86,6 @@ rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.17.1"
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
@@ -102,11 +100,10 @@ thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.3", default-features = false }
|
||||
astral-tokio-tar = { version = "0.7.0", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
tracing = "0.1.41"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
walkdir = "2.5.0"
|
||||
@@ -115,6 +112,7 @@ webpki-roots = "0.26.8"
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -180,7 +178,7 @@ harness = false
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
base64 = { version = "0.23", default-features = false, features = ["std"] }
|
||||
chrono = { version = "0.4.44", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
@@ -189,7 +187,7 @@ futures = "0.3.32"
|
||||
futures-lite = "2.6.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
mailparse = "0.17.0"
|
||||
nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.9"
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
let mut name = sanitize_name(name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)".
|
||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||
if name == addr {
|
||||
name = "".to_string();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -21,9 +21,9 @@ fn main() {
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
||||
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
||||
prefix = option_env!("PREFIX").unwrap_or_else(|| "/usr/local"),
|
||||
libdir = option_env!("LIBDIR").unwrap_or_else(|| "/usr/local/lib"),
|
||||
includedir = option_env!("INCLUDEDIR").unwrap_or_else(|| "/usr/local/include"),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
|
||||
+113
-77
@@ -453,18 +453,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* always auto-downloaded.
|
||||
* 0 = no limit (default).
|
||||
* Changes affect future messages only.
|
||||
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
|
||||
* This is an experimental option not compatible to other MUAs
|
||||
* and older Delta Chat versions.
|
||||
* 1 = enable.
|
||||
* 0 = disable (default).
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* This is deprecated, UIs should not behave differently
|
||||
* for chatmail relays and classical email servers.
|
||||
* Most usages in UIs can be replaced by `force_encryption`.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
@@ -697,6 +688,21 @@ char* dc_get_connectivity_html (dc_context_t* context);
|
||||
void dc_configure (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Add fake transport that cannot be used to connect.
|
||||
*
|
||||
* Used for offline tests only.
|
||||
*
|
||||
* To add a transport, use JSON-RPC calls `add_or_update_transport`
|
||||
* and `add_transport_from_qr` instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param addr The email address of the new transport.
|
||||
*/
|
||||
void dc_add_pseudo_transport (dc_context_t* context, const char *addr);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the context is already configured.
|
||||
*
|
||||
@@ -2449,8 +2455,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
|
||||
#define DC_QR_ASK_VERIFYBROADCAST 204 // text1=broadcast name
|
||||
#define DC_QR_FPR_OK 210 // id=contact
|
||||
#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_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
@@ -2491,13 +2495,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to start chatting;
|
||||
* if so, call dc_create_chat_by_contact_id().
|
||||
*
|
||||
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
|
||||
* scanned fingerprint does not match last seen fingerprint.
|
||||
*
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint
|
||||
* the scanned QR code contains a fingerprint but no e-mail address;
|
||||
* suggest the user to establish an encrypted connection first.
|
||||
*
|
||||
* - DC_QR_ACCOUNT dc_lot_t::text1=domain:
|
||||
* 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().
|
||||
@@ -3187,17 +3184,28 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
|
||||
/**
|
||||
* Perform a background fetch for all accounts in parallel with a timeout.
|
||||
* Pauses the scheduler, fetches from all transports at once and then resumes the scheduler.
|
||||
* The fetch for an account ends as soon as one of its transports received messages.
|
||||
*
|
||||
* dc_accounts_background_fetch() was created for the iOS Background fetch.
|
||||
* For an account with IO stopped, the scheduler is paused
|
||||
* and every transport is fetched concurrently on a dedicated connection.
|
||||
* The account is done as soon as one transport received messages, the others stop.
|
||||
* Only one batch of messages is fetched per transport this way,
|
||||
* so a larger backlog is left to the next call or to started IO.
|
||||
*
|
||||
* For an account with IO running, IMAP IDLE is interrupted on every transport
|
||||
* and the account is done once every transport is.
|
||||
*
|
||||
* The call never waits for outgoing messages and never triggers sending them itself.
|
||||
* Received messages may still queue replies, securejoin handshakes for example,
|
||||
* which go out only while IO is running.
|
||||
*
|
||||
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end,
|
||||
* also on timeout, when another background fetch is already running
|
||||
* and when the call is ignored because the timeout is too small,
|
||||
* so it is safe to wait for the event whenever `accounts` is not NULL.
|
||||
* Process all events until you get this one and you can safely return to the background
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
* without forgetting to create a generic notification if no message was fetched.
|
||||
* The event carries no data identifying the call it belongs to,
|
||||
* so it marks your own call only if no concurrent background fetch is happening.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
@@ -3535,28 +3543,6 @@ uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, siz
|
||||
dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, size_t index, dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Create a chatlist summary item when the chatlist object is already unref()'d.
|
||||
*
|
||||
* This function is similar to dc_chatlist_get_summary(), however,
|
||||
* it takes the chat ID and the message ID as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
|
||||
* as arguments. The chatlist object itself is not needed directly.
|
||||
*
|
||||
* This maybe useful if you convert the complete object into a different representation
|
||||
* as done e.g. in the node-bindings.
|
||||
* If you have access to the chatlist object in some way, using this function is not recommended,
|
||||
* use dc_chatlist_get_summary() in this case instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to get a summary for.
|
||||
* @param msg_id The message ID to get a summary for.
|
||||
* @return The summary as an dc_lot_t object, see dc_chatlist_get_summary() for details.
|
||||
* Must be freed using dc_lot_unref(). NULL is never returned.
|
||||
*/
|
||||
dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t chat_id, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Get info summary for a chat, in JSON format.
|
||||
*
|
||||
@@ -4305,6 +4291,7 @@ dc_lot_t* dc_msg_get_summary (const dc_msg_t* msg, const dc_cha
|
||||
* notifications.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @deprecated 2026-10-08, use dc_msg_get_summary_text(msg, 1, 1, approx_chars) instead
|
||||
* @param msg The message object.
|
||||
* @param approx_characters A rough length of the expected string.
|
||||
* @return A summary for the given messages.
|
||||
@@ -4314,6 +4301,34 @@ dc_lot_t* dc_msg_get_summary (const dc_msg_t* msg, const dc_cha
|
||||
char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_characters);
|
||||
|
||||
|
||||
/**
|
||||
* Get a message summary.
|
||||
* Used for notifications, pinned messages and for copying texts to clipboard.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @param add_forwarded 1=add a hint if a message was forwarded.
|
||||
* 0=do not add the hint;
|
||||
* this is allowed only if the message is shown without a username, e.g. for pinned messages.
|
||||
* @param add_type_emoji 1=add the type of the message as an emoji prefix.
|
||||
* 0=do not add type emoji;
|
||||
* this is allowed only if the message type is visible otherwise,
|
||||
* close to where the emoji would be displayed otherwise
|
||||
* (eg. images in pinned messages preceding summary)
|
||||
* @param approx_chars >0=convert linebreaks to spaces and truncate the line at about the given number of characters;
|
||||
* for single-line UI elements such as pinned messages or notifications;
|
||||
* additionally, UIs will most times clip the text to the available space.
|
||||
* 0=leave linebreaks as is and return the full text, useful e.g. for copying to clipboard.
|
||||
* @return A summary for the given message.
|
||||
* The returned string must be released using dc_str_unref().
|
||||
* Returns an empty string on errors, never returns NULL.
|
||||
*/
|
||||
char* dc_msg_get_summary_text (const dc_msg_t* msg, int add_forwarded, int add_type_emoji, int approx_chars);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the name that should be shown over the message (in a group chat) instead of the contact
|
||||
* display name, or NULL.
|
||||
@@ -4895,6 +4910,38 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
int dc_msg_is_pinned (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_FRESHNESS DC_FRESHNESS
|
||||
*
|
||||
* These constants describe the freshness of a contact,
|
||||
* as returned by dc_contact_get_freshness().
|
||||
*
|
||||
* @addtogroup DC_FRESHNESS
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contact shall not be highlighted, e.g. neither shown with a "seen recently" dot
|
||||
* nor with a "not seen for a long time" hint.
|
||||
*/
|
||||
#define DC_FRESHNESS_NORMAL 0
|
||||
|
||||
/**
|
||||
* Contact was seen recently, the UI shall highlight it e.g. with a little green dot on the avatar.
|
||||
*/
|
||||
#define DC_FRESHNESS_RECENTLY_SEEN 1
|
||||
|
||||
/**
|
||||
* Contact was not seen for a long time, the UI shall highlight it e.g. with a string
|
||||
* below the contact name (e.g. "Seen 2 months ago").
|
||||
*/
|
||||
#define DC_FRESHNESS_OLD 2
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
*
|
||||
@@ -4910,7 +4957,6 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
* By default, these names are equal,
|
||||
* but functions working with contact names
|
||||
* (e.g. dc_contact_get_name(), dc_contact_get_display_name(),
|
||||
* dc_contact_get_name_n_addr(),
|
||||
* dc_create_contact() or dc_add_address_book())
|
||||
* only affect the given-name.
|
||||
*/
|
||||
@@ -4960,7 +5006,7 @@ char* dc_contact_get_addr (const dc_contact_t* contact);
|
||||
* The function does not return the contact name as received from the network.
|
||||
*
|
||||
* This name is typically used in a form where the user can edit the name of a contact.
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name() or dc_contact_get_name_n_addr().
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5015,23 +5061,6 @@ char* dc_contact_get_display_name (const dc_contact_t* contact);
|
||||
#define dc_contact_get_first_name dc_contact_get_display_name
|
||||
|
||||
|
||||
/**
|
||||
* Get a summary of name and address.
|
||||
*
|
||||
* The returned string is either "Name (email@domain.com)" or just
|
||||
* "email@domain.com" if the name is unset.
|
||||
*
|
||||
* The summary is typically used when asking the user something about the contact.
|
||||
* The attached e-mail address makes the question unique, e.g. "Chat with Alan Miller (am@uniquedomain.com)?"
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return A summary string, must be released using dc_str_unref().
|
||||
* Never returns NULL.
|
||||
*/
|
||||
char* dc_contact_get_name_n_addr (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Get the contact's profile image.
|
||||
* This is the image set by each remote user on their own
|
||||
@@ -5085,18 +5114,18 @@ int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the contact was seen recently.
|
||||
* Get the contact's freshness.
|
||||
*
|
||||
* The UI shall hightlight contacts that are recently seen by a little green dot on the avatar
|
||||
* and contacts that were not seen for a long time by a string below the contact name (e.g. "Seen 2 months ago")
|
||||
*
|
||||
* The UI may highlight these contacts,
|
||||
* eg. draw a little green dot on the avatars of the users recently seen.
|
||||
* DC_CONTACT_ID_SELF and other special contact IDs are defined as never seen recently (they should not get a dot).
|
||||
* To get the time a contact was seen, use dc_contact_get_last_seen().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 1=contact seen recently, 0=contact not seen recently.
|
||||
* @return One of the @ref DC_FRESHNESS constants.
|
||||
*/
|
||||
int dc_contact_was_seen_recently (const dc_contact_t* contact);
|
||||
int dc_contact_get_freshness (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
@@ -5908,14 +5937,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
|
||||
|
||||
/**
|
||||
* Emitted when a message was successfully moved on IMAP.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) Info string in English language.
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
|
||||
|
||||
/**
|
||||
* Emitted before going into IDLE on the Inbox folder.
|
||||
*
|
||||
@@ -6164,6 +6185,17 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_CHAT_DELETED 2023
|
||||
|
||||
|
||||
/**
|
||||
* The list of pinned messages for the chat has changed.
|
||||
*
|
||||
* Some message got pinned, or pinned message is unpinned or deleted.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) 0
|
||||
*/
|
||||
#define DC_EVENT_PINNED_MESSAGES_CHANGED 2024
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
*
|
||||
@@ -6328,6 +6360,10 @@ void dc_event_unref(dc_event_t* event);
|
||||
* A call made while another background fetch is running gets the event immediately,
|
||||
* and the running fetch keeps emitting events until its own marker.
|
||||
*
|
||||
* The event carries no data identifying the call it belongs to,
|
||||
* so it marks your own call only if no concurrent background fetch is happening.
|
||||
* Your own call has finished when dc_accounts_background_fetch() returns.
|
||||
*
|
||||
* This event is only emitted by the account manager
|
||||
*/
|
||||
|
||||
|
||||
+57
-59
@@ -12,7 +12,6 @@
|
||||
extern crate human_panic;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Write;
|
||||
use std::future::Future;
|
||||
use std::mem::ManuallyDrop;
|
||||
@@ -31,6 +30,7 @@ use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::transport::add_pseudo_transport;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
@@ -46,7 +46,6 @@ mod dc_array;
|
||||
mod lot;
|
||||
|
||||
mod string;
|
||||
use deltachat::chatlist::Chatlist;
|
||||
|
||||
use self::string::*;
|
||||
|
||||
@@ -414,6 +413,21 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
|
||||
spawn_configure(ctx.clone());
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_add_pseudo_transport(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_add_pseudo_transport()");
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx = unsafe { &*context };
|
||||
let addr = to_string_lossy(addr);
|
||||
block_on(add_pseudo_transport(ctx, &addr)).log_err(ctx).ok();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -477,7 +491,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ImapConnected(_) => 102,
|
||||
EventType::SmtpMessageSent(_) => 103,
|
||||
EventType::ImapMessageDeleted(_) => 104,
|
||||
EventType::ImapMessageMoved(_) => 105,
|
||||
EventType::ImapInboxIdle => 106,
|
||||
EventType::NewBlobFile(_) => 150,
|
||||
EventType::DeletedBlobFile(_) => 151,
|
||||
@@ -499,6 +512,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ChatDeleted { .. } => 2023,
|
||||
EventType::PinnedMessagesChanged { .. } => 2024,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
EventType::LocationChanged(_) => 2035,
|
||||
EventType::ConfigureProgress { .. } => 2041,
|
||||
@@ -544,7 +558,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ImapConnected(_)
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapInboxIdle
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
@@ -573,7 +586,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgReadCountChanged { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. }
|
||||
| EventType::ChatDeleted { chat_id } => chat_id.to_u32() as libc::c_int,
|
||||
| EventType::ChatDeleted { chat_id }
|
||||
| EventType::PinnedMessagesChanged { 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
|
||||
@@ -619,7 +633,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ImapConnected(_)
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapInboxIdle
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
@@ -648,7 +661,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
| EventType::TransportsModified => 0,
|
||||
| EventType::TransportsModified
|
||||
| EventType::PinnedMessagesChanged { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
@@ -714,7 +728,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ImapConnected(msg)
|
||||
| EventType::SmtpMessageSent(msg)
|
||||
| EventType::ImapMessageDeleted(msg)
|
||||
| EventType::ImapMessageMoved(msg)
|
||||
| EventType::NewBlobFile(msg)
|
||||
| EventType::DeletedBlobFile(msg)
|
||||
| EventType::Warning(msg)
|
||||
@@ -750,7 +763,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::TransportsModified => ptr::null_mut(),
|
||||
| EventType::TransportsModified
|
||||
| EventType::PinnedMessagesChanged { .. } => ptr::null_mut(),
|
||||
EventType::IncomingCall {
|
||||
place_call_info, ..
|
||||
} => place_call_info.strdup(),
|
||||
@@ -2828,34 +2842,6 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_chatlist_get_summary2(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
) -> *mut dc_lot_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chatlist_get_summary2()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = unsafe { &*context };
|
||||
let msg_id = if msg_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(MsgId::new(msg_id))
|
||||
};
|
||||
let summary = block_on(Chatlist::get_summary2(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
msg_id,
|
||||
None,
|
||||
))
|
||||
.context("get_summary2 failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
}
|
||||
|
||||
// dc_chat_t
|
||||
|
||||
/// FFI struct for [dc_chat_t]
|
||||
@@ -3486,6 +3472,7 @@ pub unsafe extern "C" fn dc_msg_get_summary(
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
}
|
||||
|
||||
// deprecated, use dc_msg_get_summary_text instead
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_msg_get_summarytext(
|
||||
msg: *mut dc_msg_t,
|
||||
@@ -3497,14 +3484,37 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
|
||||
}
|
||||
let ffi_msg = unsafe { &mut *msg };
|
||||
|
||||
let summary = block_on(ffi_msg.message.get_summary(&ffi_msg.context, None))
|
||||
.context("dc_msg_get_summarytext failed")
|
||||
.log_err(&ffi_msg.context)
|
||||
.unwrap_or_default();
|
||||
match usize::try_from(approx_characters) {
|
||||
Ok(chars) => summary.truncated_text(chars).strdup(),
|
||||
Err(_) => summary.text.strdup(),
|
||||
let add_forwarded = true;
|
||||
let add_type_emoji = true;
|
||||
block_on(ffi_msg.message.get_summary_text_ext(
|
||||
&ffi_msg.context,
|
||||
add_forwarded,
|
||||
add_type_emoji,
|
||||
usize::try_from(approx_characters).unwrap_or_default(),
|
||||
))
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_msg_get_summary_text(
|
||||
msg: *mut dc_msg_t,
|
||||
add_forwarded: libc::c_int,
|
||||
add_type_emoji: libc::c_int,
|
||||
approx_chars: libc::c_int,
|
||||
) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_summary_text()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = unsafe { &mut *msg };
|
||||
|
||||
block_on(ffi_msg.message.get_summary_text_ext(
|
||||
&ffi_msg.context,
|
||||
add_forwarded != 0,
|
||||
add_type_emoji != 0,
|
||||
usize::try_from(approx_chars).unwrap_or(0),
|
||||
))
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -3963,18 +3973,6 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
|
||||
ffi_contact.contact.get_display_name().strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_name_n_addr(
|
||||
contact: *mut dc_contact_t,
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
ffi_contact.contact.get_name_n_addr().strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_profile_image(
|
||||
contact: *mut dc_contact_t,
|
||||
@@ -4030,13 +4028,13 @@ pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) ->
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_was_seen_recently(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_contact_get_freshness(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_was_seen_recently()");
|
||||
eprintln!("ignoring careless call to dc_contact_get_freshness()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
ffi_contact.contact.was_seen_recently() as libc::c_int
|
||||
u32::from(ffi_contact.contact.get_freshness()) as libc::c_int
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
|
||||
@@ -47,8 +47,6 @@ impl Lot {
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::BackupTooNew { .. } => None,
|
||||
@@ -103,8 +101,6 @@ impl Lot {
|
||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||
@@ -132,8 +128,6 @@ impl Lot {
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::AskJoinBroadcast { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::BackupTooNew { .. } => Default::default(),
|
||||
@@ -181,12 +175,6 @@ pub enum LotState {
|
||||
/// id=contact
|
||||
QrFprOk = 210,
|
||||
|
||||
/// id=contact
|
||||
QrFprMismatch = 220,
|
||||
|
||||
/// text1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.61.0-dev"
|
||||
"version": "2.64.0-dev"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -278,10 +278,26 @@ impl CommandApi {
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// For an account with IO stopped, the scheduler is paused
|
||||
/// and every transport is fetched concurrently on a dedicated connection.
|
||||
/// The account is done as soon as one transport received messages, the others stop.
|
||||
/// Only one batch of messages is fetched per transport this way,
|
||||
/// so a larger backlog is left to the next call or to started IO.
|
||||
///
|
||||
/// For an account with IO running, IMAP IDLE is interrupted on every transport
|
||||
/// and the account is done once every transport is.
|
||||
///
|
||||
/// The call never waits for outgoing messages and never triggers sending them itself.
|
||||
/// Received messages may still queue replies, securejoin handshakes for example,
|
||||
/// which go out only while IO is running.
|
||||
/// Use `is_sending_finished()` to tell whether the outgoing queue is empty.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout,
|
||||
/// and immediately if another background fetch is already running.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
/// without forgetting to create a generic notification if no message was fetched.
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it marks your own call only if no concurrent background fetch is happening.
|
||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
@@ -292,6 +308,11 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops an ongoing `background_fetch()` call, making it return early
|
||||
/// without waiting for the remaining transports or for the timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted as usual.
|
||||
/// Does nothing if no background fetch is running.
|
||||
async fn stop_background_fetch(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_background_fetch();
|
||||
Ok(())
|
||||
@@ -525,6 +546,18 @@ impl CommandApi {
|
||||
ctx.add_transport_from_qr(&qr).await
|
||||
}
|
||||
|
||||
/// Adds an initial transport on the chatmail relay that answers fastest
|
||||
/// and lets the profile add further ones in the background.
|
||||
///
|
||||
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||
/// while securejoin codes add the inviter's relays to the candidates.
|
||||
///
|
||||
/// Does nothing if the profile already has a transport.
|
||||
async fn init_transports(&self, account_id: u32, qr: Option<String>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.init_transports(qr.as_deref()).await
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to remove a transport.
|
||||
|
||||
@@ -13,7 +13,6 @@ pub enum Account {
|
||||
Configured {
|
||||
id: u32,
|
||||
display_name: Option<String>,
|
||||
addr: Option<String>,
|
||||
// size: u32,
|
||||
profile_image: Option<String>,
|
||||
color: String,
|
||||
@@ -29,7 +28,6 @@ impl Account {
|
||||
pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result<Self> {
|
||||
if ctx.is_configured().await? {
|
||||
let display_name = ctx.get_config(Config::Displayname).await?;
|
||||
let addr = ctx.get_config(Config::Addr).await?;
|
||||
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
||||
let color = color_int_to_hex_string(
|
||||
Contact::get_by_id(ctx, ContactId::SELF)
|
||||
@@ -41,7 +39,6 @@ impl Account {
|
||||
Ok(Account::Configured {
|
||||
id,
|
||||
display_name,
|
||||
addr,
|
||||
profile_image,
|
||||
color,
|
||||
private_tag,
|
||||
|
||||
@@ -9,6 +9,8 @@ use deltachat::context::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use crate::api::types::contact::ContactFreshness;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
@@ -69,7 +71,7 @@ pub struct FullChat {
|
||||
is_muted: bool,
|
||||
ephemeral_timer: u32,
|
||||
can_send: bool,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
mailing_list_address: Option<String>,
|
||||
}
|
||||
|
||||
@@ -92,16 +94,17 @@ impl FullChat {
|
||||
|
||||
let can_send = chat.can_send(context).await?;
|
||||
|
||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||
let freshness = if chat.get_type() == Chattype::Single {
|
||||
match contact_ids.first() {
|
||||
Some(contact) => Contact::get_by_id(context, *contact)
|
||||
.await
|
||||
.context("failed to load contact for was_seen_recently")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
.context("failed to load contact for get_freshness")?
|
||||
.get_freshness()
|
||||
.into(),
|
||||
None => ContactFreshness::Normal,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
ContactFreshness::Normal
|
||||
};
|
||||
|
||||
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
|
||||
@@ -126,7 +129,7 @@ impl FullChat {
|
||||
is_muted: chat.is_muted(),
|
||||
ephemeral_timer,
|
||||
can_send,
|
||||
was_seen_recently,
|
||||
freshness,
|
||||
mailing_list_address,
|
||||
})
|
||||
}
|
||||
@@ -137,7 +140,7 @@ impl FullChat {
|
||||
/// - fresh_message_counter
|
||||
/// - ephemeral_timer
|
||||
/// - self_in_group
|
||||
/// - was_seen_recently
|
||||
/// - freshness
|
||||
/// - can_send
|
||||
///
|
||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
|
||||
@@ -11,6 +11,8 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use crate::api::types::contact::ContactFreshness;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
@@ -29,8 +31,6 @@ pub enum ChatListItemFetchResult {
|
||||
summary_text1: String,
|
||||
summary_text2: String,
|
||||
summary_status: u32,
|
||||
/// showing preview if last chat message is image
|
||||
summary_preview_image: Option<String>,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
@@ -68,7 +68,7 @@ pub enum ChatListItemFetchResult {
|
||||
is_contact_request: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
last_message_type: Option<MessageViewtype>,
|
||||
last_message_id: Option<u32>,
|
||||
},
|
||||
@@ -101,8 +101,6 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
||||
let summary_text2 = summary.text.to_owned();
|
||||
|
||||
let summary_preview_image = summary.thumbnail_path;
|
||||
|
||||
let visibility = chat.get_visibility();
|
||||
|
||||
let avatar_path = chat
|
||||
@@ -127,22 +125,20 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||
let (dm_chat_contact, freshness) = if chat.get_type() == Chattype::Single {
|
||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||
let contact = chat_contacts.first();
|
||||
let was_seen_recently = match contact {
|
||||
let freshness = match contact {
|
||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||
.await
|
||||
.context("contact")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
.get_freshness()
|
||||
.into(),
|
||||
None => ContactFreshness::Normal,
|
||||
};
|
||||
(
|
||||
contact.map(|contact_id| contact_id.to_u32()),
|
||||
was_seen_recently,
|
||||
)
|
||||
(contact.map(|contact_id| contact_id.to_u32()), freshness)
|
||||
} else {
|
||||
(None, false)
|
||||
(None, ContactFreshness::Normal)
|
||||
};
|
||||
|
||||
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
|
||||
@@ -157,7 +153,6 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
summary_text1,
|
||||
summary_text2,
|
||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||
summary_preview_image,
|
||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||
is_group: chat.get_type() == Chattype::Group,
|
||||
fresh_message_counter,
|
||||
@@ -170,7 +165,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
dm_chat_contact,
|
||||
was_seen_recently,
|
||||
freshness,
|
||||
last_message_type: message_type,
|
||||
last_message_id: last_msgid.map(|id| id.to_u32()),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::key::{DcKey, SignedPublicKey};
|
||||
use serde::Serialize;
|
||||
@@ -6,6 +7,27 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
/// Freshness of a contact, based on when it was last seen.
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum ContactFreshness {
|
||||
/// Contact shall not be highlighted.
|
||||
Normal,
|
||||
/// Contact was seen recently.
|
||||
RecentlySeen,
|
||||
/// Contact was not seen for a long time.
|
||||
Old,
|
||||
}
|
||||
|
||||
impl From<contact::Freshness> for ContactFreshness {
|
||||
fn from(freshness: contact::Freshness) -> Self {
|
||||
match freshness {
|
||||
contact::Freshness::Normal => ContactFreshness::Normal,
|
||||
contact::Freshness::RecentlySeen => ContactFreshness::RecentlySeen,
|
||||
contact::Freshness::Old => ContactFreshness::Old,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||
pub struct ContactObject {
|
||||
@@ -17,7 +39,6 @@ pub struct ContactObject {
|
||||
id: u32,
|
||||
name: String,
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
|
||||
/// Is the contact a key contact.
|
||||
@@ -33,7 +54,7 @@ pub struct ContactObject {
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
|
||||
/// If the contact is a bot.
|
||||
is_bot: bool,
|
||||
@@ -57,12 +78,11 @@ impl ContactObject {
|
||||
id: contact.id.to_u32(),
|
||||
name: contact.get_name().to_owned(),
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_key_contact: contact.is_key_contact(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
freshness: contact.get_freshness().into(),
|
||||
is_bot: contact.is_bot(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,9 +44,6 @@ pub enum EventType {
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted { msg: String },
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved { msg: String },
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
@@ -261,6 +258,15 @@ pub enum EventType {
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// The list of pinned messages for the chat has changed.
|
||||
///
|
||||
/// Some message got pinned, or pinned message is unpinned or deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PinnedMessagesChanged {
|
||||
/// ID of the chat where the list of pinned messages changed.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged {
|
||||
@@ -496,7 +502,6 @@ impl From<CoreEventType> for EventType {
|
||||
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
|
||||
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||
CoreEventType::ImapInboxIdle => ImapInboxIdle,
|
||||
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
|
||||
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||
@@ -516,6 +521,9 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
CoreEventType::PinnedMessagesChanged { chat_id } => PinnedMessagesChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingReaction {
|
||||
chat_id,
|
||||
contact_id,
|
||||
|
||||
@@ -68,16 +68,6 @@ pub enum QrObject {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
},
|
||||
/// Scanned fingerprint does not match the last seen fingerprint.
|
||||
FprMismatch {
|
||||
/// Contact ID.
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
/// The scanned QR code contains a fingerprint but no e-mail address.
|
||||
FprWithoutAddr {
|
||||
/// Key fingerprint.
|
||||
fingerprint: String,
|
||||
},
|
||||
/// Ask the user if they want to create an account on the given domain.
|
||||
Account {
|
||||
/// Server domain name.
|
||||
@@ -296,11 +286,6 @@ impl From<Qr> for QrObject {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::FprOk { contact_id }
|
||||
}
|
||||
Qr::FprMismatch { contact_id } => {
|
||||
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
|
||||
QrObject::FprMismatch { contact_id }
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
Qr::Backup2 {
|
||||
ref node_addr,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
@@ -8,7 +8,7 @@ repository = "https://github.com/chatmail/core"
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "6"
|
||||
dirs = "7"
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
|
||||
@@ -20,62 +20,13 @@ use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::qr::*;
|
||||
use deltachat::qr_code_generator::create_qr_svg;
|
||||
use deltachat::qr_code_generator::{create_qr_svg, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
@@ -304,7 +255,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
============================================="
|
||||
),
|
||||
@@ -396,9 +346,57 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
============================================="
|
||||
),
|
||||
},
|
||||
"connect" => {
|
||||
context.start_io().await;
|
||||
}
|
||||
"disconnect" => {
|
||||
context.stop_io().await;
|
||||
}
|
||||
"fetch" => {
|
||||
context.background_fetch().await?;
|
||||
}
|
||||
"configure" => {
|
||||
context.configure().await?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
}
|
||||
"clear" => {
|
||||
println!("\n\n\n");
|
||||
print!("\x1b[1;1H\x1b[2J");
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
context.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = deltachat::securejoin::get_securejoin_qr(&context, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{qr}");
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
context.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&context, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg).await?;
|
||||
println!("QR code svg written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"joinqr" => {
|
||||
context.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
deltachat::securejoin::join_securejoin(&context, arg1).await?;
|
||||
}
|
||||
}
|
||||
"export-backup" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(
|
||||
@@ -444,15 +442,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
@@ -1113,11 +1102,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
|
||||
let mut res = format!(
|
||||
"Contact info for: {}:\nIcon: {}\n",
|
||||
name_n_addr,
|
||||
"Contact info for: {name} ({addr}):\nIcon: {}\n",
|
||||
match contact.get_profile_image(&context).await? {
|
||||
Some(image) => image.to_str().unwrap().to_string(),
|
||||
None => "NoIcon".to_string(),
|
||||
|
||||
@@ -13,8 +13,6 @@ use anyhow::{Error, bail};
|
||||
use deltachat::EventType;
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::context::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
@@ -25,7 +23,6 @@ use rustyline::validate::Validator;
|
||||
use rustyline::{
|
||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -147,7 +144,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
const IMEX_COMMANDS: [&str; 9] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -156,7 +153,6 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
@@ -353,23 +349,16 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
Ok(line) => {
|
||||
// TODO: ignore "set mail_pw"
|
||||
rl.add_history_entry(line.as_str())?;
|
||||
let should_continue = Handle::current().block_on(async {
|
||||
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
|
||||
Ok(ExitResult::Continue) => true,
|
||||
Ok(ExitResult::Exit) => {
|
||||
println!("Exiting ...");
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err:#}");
|
||||
true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !should_continue {
|
||||
let line = line.trim();
|
||||
if matches!(line, "exit" | "quit") {
|
||||
println!("Exiting ...");
|
||||
break;
|
||||
}
|
||||
if let Err(err) =
|
||||
Handle::current().block_on(cmdline(ctx.clone(), line, &mut selected_chat))
|
||||
{
|
||||
eprintln!("Error: {err:#}");
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||
println!("Exiting...");
|
||||
@@ -393,77 +382,6 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ExitResult {
|
||||
Continue,
|
||||
Exit,
|
||||
}
|
||||
|
||||
async fn handle_cmd(
|
||||
line: &str,
|
||||
ctx: Context,
|
||||
selected_chat: &mut ChatId,
|
||||
) -> Result<ExitResult, Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
let arg0 = args.next().unwrap_or_default();
|
||||
let arg1 = args.next().unwrap_or_default();
|
||||
|
||||
match arg0 {
|
||||
"connect" => {
|
||||
ctx.start_io().await;
|
||||
}
|
||||
"disconnect" => {
|
||||
ctx.stop_io().await;
|
||||
}
|
||||
"fetch" => {
|
||||
ctx.background_fetch().await?;
|
||||
}
|
||||
"configure" => {
|
||||
ctx.configure().await?;
|
||||
}
|
||||
"clear" => {
|
||||
println!("\n\n\n");
|
||||
print!("\x1b[1;1H\x1b[2J");
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{qr}");
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg).await?;
|
||||
println!("QR code svg written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
join_securejoin(&ctx, arg1).await?;
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
_ => cmdline(ctx.clone(), line, selected_chat).await?,
|
||||
}
|
||||
|
||||
Ok(ExitResult::Continue)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
tracing_subscriber::fmt()
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -139,6 +139,18 @@ class Account:
|
||||
"""Add a new transport using a QR code."""
|
||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def init_transports(self, qr: Optional[str] = None):
|
||||
"""Add an initial transport on the chatmail relay that answers fastest.
|
||||
|
||||
The profile then adds further ones in the background.
|
||||
A ``DCACCOUNT:`` or ``DCLOGIN:`` ``qr`` code adds a single transport
|
||||
while securejoin codes add the inviter's relays to the candidates.
|
||||
|
||||
Does nothing if the profile already has a transport.
|
||||
"""
|
||||
yield self._rpc.init_transports.future(self.id, qr)
|
||||
|
||||
def delete_transport(self, addr: str):
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
@@ -38,7 +38,6 @@ class EventType(str, Enum):
|
||||
IMAP_CONNECTED = "ImapConnected"
|
||||
SMTP_MESSAGE_SENT = "SmtpMessageSent"
|
||||
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
|
||||
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
|
||||
IMAP_INBOX_IDLE = "ImapInboxIdle"
|
||||
NEW_BLOB_FILE = "NewBlobFile"
|
||||
DELETED_BLOB_FILE = "DeletedBlobFile"
|
||||
@@ -70,6 +69,7 @@ class EventType(str, Enum):
|
||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
ACCOUNTS_BACKGROUND_FETCH_DONE = "AccountsBackgroundFetchDone"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||
|
||||
@@ -48,6 +48,13 @@ class DeltaChat:
|
||||
"""Stop ongoing background fetch."""
|
||||
self.rpc.stop_background_fetch()
|
||||
|
||||
def wait_for_event(self, event_type=None) -> AttrDict:
|
||||
"""Wait until the next account manager event and return it."""
|
||||
while True:
|
||||
next_event = AttrDict(self.rpc.wait_for_event(0))
|
||||
if event_type is None or next_event.kind == event_type:
|
||||
return next_event
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network conditions might have changed."""
|
||||
self.rpc.maybe_network()
|
||||
|
||||
@@ -149,7 +149,10 @@ class Rpc:
|
||||
def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
self.stop_io_for_all_accounts()
|
||||
# JSON-RPC error may happen if RPC server process has crashed already.
|
||||
# We still want to shutdown all threads in this case.
|
||||
with contextlib.suppress(JsonRpcError):
|
||||
self.stop_io_for_all_accounts()
|
||||
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
|
||||
# otherwise it might exit through an "RPC server closed" error instead.
|
||||
self.events_thread.join()
|
||||
|
||||
@@ -24,8 +24,16 @@ class DirectImap:
|
||||
|
||||
def __init__(self, account: Account, addr=None, password=None) -> None:
|
||||
self.account = account
|
||||
self.addr = addr or account.get_config("addr")
|
||||
self.password = password or account.get_config("mail_pw")
|
||||
if addr is None or password is None:
|
||||
transport = account.list_transports()[-1]
|
||||
if addr is None:
|
||||
self.addr = transport["addr"]
|
||||
else:
|
||||
self.addr = addr
|
||||
if password is None:
|
||||
self.password = transport["password"]
|
||||
else:
|
||||
self.password = password
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
self.connect()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def anonymous_mib(pid):
|
||||
with open(f"/proc/{pid}/smaps_rollup") as f:
|
||||
for line in f:
|
||||
if line.startswith("Anonymous:"):
|
||||
return int(line.split()[1]) // 1024
|
||||
raise LookupError("Anonymous")
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="reads /proc")
|
||||
def test_attachment_memory_is_returned(acf, rpc, tmp_path):
|
||||
# See also comments for `tune_malloc` in `deltachat-rpc-server/src/main.rs`
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
chat1 = acf.get_accepted_chat(ac1, ac2)
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
blob = tmp_path / "blob.bin"
|
||||
blob.write_bytes(os.urandom(20 << 20))
|
||||
before = anonymous_mib(rpc.process.pid)
|
||||
|
||||
for sender_chat, receiver in ((chat1, ac2), (chat2, ac1)):
|
||||
sender_chat.send_file(str(blob))
|
||||
event = receiver.wait_for_incoming_msg_event()
|
||||
assert receiver.get_message_by_id(event.msg_id).get_snapshot().file_bytes == 20 << 20
|
||||
for ac in (ac1, ac2):
|
||||
rpc.wait_for_all_work_done(ac.id)
|
||||
|
||||
grown = anonymous_mib(rpc.process.pid) - before
|
||||
assert grown < 64, f"the server kept {grown} MiB after two 20 MiB attachments"
|
||||
@@ -24,6 +24,12 @@ def wait_for_imap_message(imap):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_init_transports(acf):
|
||||
account = acf.get_unconfigured_account()
|
||||
account.init_transports(acf.get_account_qr())
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
|
||||
def test_add_second_address(acf) -> None:
|
||||
account = acf.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
@@ -52,10 +58,9 @@ def test_add_second_address(acf) -> None:
|
||||
|
||||
|
||||
def test_change_address(acf) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
"""Test Alice configuring a second transport and removing the first one."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
@@ -65,22 +70,14 @@ def test_change_address(acf) -> None:
|
||||
sender_addr1 = msg1.sender.get_snapshot().address
|
||||
|
||||
alice.stop_io()
|
||||
old_alice_addr = alice.get_config("configured_addr")
|
||||
old_alice_addr = alice.list_transports()[0]["addr"]
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr in alice_vcard
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
with pytest.raises(JsonRpcError):
|
||||
# Cannot use the address that is not
|
||||
# configured for any transport.
|
||||
alice.set_config("configured_addr", bob_addr)
|
||||
|
||||
# Load old address so it is cached.
|
||||
assert alice.get_config("configured_addr") == old_alice_addr
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
# Make sure that setting `configured_addr` invalidated the cache.
|
||||
assert alice.get_config("configured_addr") == new_alice_addr
|
||||
alice.delete_transport(old_alice_addr)
|
||||
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr not in alice_vcard
|
||||
@@ -228,10 +225,6 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
@@ -358,12 +351,11 @@ def test_qr_works_after_removing_primary_transport(acf, log) -> None:
|
||||
log.section("Alice creates a QR code")
|
||||
chat_qr = alice.get_qr_code()
|
||||
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
|
||||
assert f"&a={first_addr}" in chat_qr_unquoted
|
||||
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted
|
||||
assert f"&a={third_addr}" in chat_qr_unquoted
|
||||
assert f"&r={second_addr},{first_addr}" in chat_qr_unquoted
|
||||
|
||||
log.section("Alice removes first and second transport")
|
||||
alice.set_config("configured_addr", third_addr)
|
||||
alice.delete_transport(first_addr)
|
||||
log.section("Alice removes the transport named by the a= parameter")
|
||||
alice.delete_transport(third_addr)
|
||||
alice.delete_transport(second_addr)
|
||||
|
||||
log.section("Bob scans the QR code, which still works")
|
||||
@@ -402,3 +394,14 @@ def test_background_fetch_no_duplicates(acf, direct_imap, dc):
|
||||
|
||||
dc.background_fetch(300)
|
||||
assert len(messages_with_text(alice_chat, "hello")) == 1
|
||||
|
||||
|
||||
def test_multitransport_mdn(acf):
|
||||
"""Test sending an MDN right after configuring two transports."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice.create_chat(bob)
|
||||
bob_msg = bob.create_chat(alice).send_text("Hello!")
|
||||
alice.wait_for_incoming_msg().mark_seen()
|
||||
assert bob.wait_for_event(EventType.MSG_READ).msg_id == bob_msg.id
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -27,7 +28,7 @@ def test_qr_setup_contact(acf) -> None:
|
||||
|
||||
def test_qr_setup_contact_svg(acf) -> None:
|
||||
alice = acf.new_configured_account()
|
||||
_, _, domain = alice.get_config("addr").rpartition("@")
|
||||
_, _, domain = alice.list_transports()[0]["addr"].rpartition("@")
|
||||
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
|
||||
@@ -43,6 +44,7 @@ def test_qr_setup_contact_svg(acf) -> None:
|
||||
def test_qr_securejoin(acf):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
|
||||
alice.set_config("displayname", "Alice")
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
alice2 = alice.clone()
|
||||
@@ -67,7 +69,7 @@ def test_qr_securejoin(acf):
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "You were added by Alice."
|
||||
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
@@ -89,6 +91,30 @@ def test_qr_securejoin(acf):
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_qr_securejoin_request_deleted_on_all_transports(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice_chat = alice.create_group("Group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
alice2 = alice.clone()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
alice_chat.remove_contact(bob)
|
||||
# Stop and start io in order to wait until the remove message is sent out.
|
||||
alice.stop_io()
|
||||
alice.bring_online()
|
||||
|
||||
# Membership timestamps have a resolution of one second.
|
||||
time.sleep(1)
|
||||
# By now, the securejoin message sent by Bob must be deleted on Alice's relays.
|
||||
# Otherwise, alice2 would execute securejoin again and re-add Bob.
|
||||
alice2.bring_online()
|
||||
alice2_chat = alice2.get_chat_by_id(alice_chat.id)
|
||||
assert alice2.create_contact(bob) not in alice2_chat.get_contacts()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
@@ -146,17 +172,13 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
if inviter_side:
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
assert member_added_msg.info_contact_id == contact_snapshot.id
|
||||
else:
|
||||
if not inviter_side:
|
||||
if chat_msgs[0].get_snapshot().text == "You joined the channel.":
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
else:
|
||||
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
assert member_added_msg.is_info
|
||||
|
||||
hello_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert hello_msg.text == "Hello everyone!"
|
||||
@@ -221,7 +243,7 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You joined the channel."
|
||||
|
||||
get_broadcast(alice2).get_messages()[2].resend()
|
||||
get_broadcast(alice2).get_messages()[1].resend()
|
||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello everyone!"
|
||||
|
||||
@@ -503,11 +525,9 @@ def test_aeap_flow(acf):
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
logging.info("changing email account")
|
||||
ac1.set_config("addr", addr)
|
||||
ac1.set_config("mail_pw", password)
|
||||
ac1.stop_io()
|
||||
ac1.configure()
|
||||
ac1.start_io()
|
||||
old_addr = ac1.list_transports()[0]["addr"]
|
||||
ac1.add_transport_from_qr(acf.get_account_qr())
|
||||
ac1.delete_transport(old_addr)
|
||||
|
||||
logging.info("sending second message")
|
||||
msg_out = chat.send_text("changed address").get_snapshot()
|
||||
@@ -529,6 +549,7 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
but different key fingerprint while a securejoin with that contact is still pending.
|
||||
"""
|
||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
||||
ac3.set_config("displayname", "ac3")
|
||||
|
||||
# ac3 creates a group with ac1.
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
@@ -540,7 +561,7 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
|
||||
assert snapshot.text == "You were added by ac3."
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 sets up contact with ac1
|
||||
@@ -580,6 +601,8 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
|
||||
def test_withdraw_securejoin_qr(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.set_config("displayname", "Alice")
|
||||
bob.set_config("displayname", "Bob")
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
@@ -592,11 +615,11 @@ def test_withdraw_securejoin_qr(acf):
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "You were added by Alice."
|
||||
bob_chat.leave()
|
||||
|
||||
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"))
|
||||
assert snapshot.text == "Group left by Bob."
|
||||
|
||||
logging.info("Alice withdraws QR code.")
|
||||
qr = alice.check_qr(qr_code)
|
||||
|
||||
@@ -35,6 +35,19 @@ def test_sleep(rpc) -> None:
|
||||
assert sleep_5_future in pending
|
||||
|
||||
|
||||
def test_shutdown(rpc) -> None:
|
||||
"""Test RPC client shutdown if RPC server process is terminated.
|
||||
|
||||
This is a regression test, at the time of adding it RPC client
|
||||
did not terminate all threads and python process did not exit.
|
||||
Shutting down all threads is tested implicitly by pytest-timeout.
|
||||
"""
|
||||
rpc.process.kill()
|
||||
rpc.process.wait()
|
||||
with pytest.raises(JsonRpcError):
|
||||
rpc.get_system_info()
|
||||
|
||||
|
||||
def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
@@ -155,7 +168,7 @@ def test_list_transports(acf) -> None:
|
||||
def test_account(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
@@ -318,7 +331,7 @@ def test_chat(acf) -> None:
|
||||
def test_contact(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
@@ -421,7 +434,6 @@ def test_dont_move_sync_msgs(acf, direct_imap):
|
||||
addr, password = acf.get_credentials()
|
||||
ac1 = acf.get_unconfigured_account()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.set_config("fix_is_chatmail", "1")
|
||||
ac1.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac1.start_io()
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
@@ -605,7 +617,6 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
||||
(ac1, some1) = acf.get_online_accounts(2)
|
||||
|
||||
log.section("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
@@ -623,7 +634,6 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
||||
contacts = ac.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.get_snapshot().address == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
@@ -1188,7 +1198,6 @@ def test_leave_broadcast(acf, all_devices_online):
|
||||
|
||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||
chat = get_broadcast(ac)
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
chat_msgs = chat.get_messages()
|
||||
|
||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||
@@ -1200,14 +1209,11 @@ def test_leave_broadcast(acf, all_devices_online):
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
if inviter_side:
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
else:
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
if not inviter_side:
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
leave_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert leave_msg.text == "You left the channel."
|
||||
|
||||
@@ -1354,6 +1360,22 @@ def test_background_fetch(acf, dc):
|
||||
break
|
||||
|
||||
|
||||
def test_background_fetch_does_not_wait_for_sending(dc, acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
|
||||
alice.stop_io()
|
||||
text = "x" * 200_000
|
||||
for _ in range(50):
|
||||
alice_chat_bob.send_text(text)
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
alice.start_io()
|
||||
dc.background_fetch(50)
|
||||
dc.wait_for_event(EventType.ACCOUNTS_BACKGROUND_FETCH_DONE)
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
|
||||
def test_message_exists(acf):
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
@@ -14,6 +14,7 @@ deltachat = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.61.0-dev"
|
||||
"version": "2.64.0-dev"
|
||||
}
|
||||
|
||||
@@ -22,8 +22,32 @@ use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
/// Pins Linux glibc's mmap threshold so that freed message buffers go back to the kernel.
|
||||
///
|
||||
/// See M_MMAP_THRESHOLD in <https://man7.org/linux/man-pages/man3/mallopt.3.html>:
|
||||
/// glibc by default starts with a M_MMAP_THRESHOLD threshold of 128 KiB
|
||||
/// but raises it to the size of every freed block that exceeds it,
|
||||
/// up to 32 MiB on 64-bit systems,
|
||||
/// and trims the heap only from its top end once twice that much is free.
|
||||
/// Fixating the threshold disables the adjustment: allocations at or above it
|
||||
/// that the free list cannot satisfy are mmapped and unmapped on free,
|
||||
/// at the price of the kernel zeroing each such buffer after unmap.
|
||||
/// Large message processing (allocations above 128KiB) very slightly slows
|
||||
/// down to the kernel zeroing the buffers, but it's hardly measurable,
|
||||
/// while overall process memory allocation behaviour significantly improves.
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||
fn tune_malloc() {
|
||||
unsafe {
|
||||
libc::mallopt(libc::M_MMAP_THRESHOLD, 128 * 1024);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
|
||||
fn tune_malloc() {}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
tune_malloc();
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
||||
|
||||
@@ -63,6 +63,7 @@ ignore = [
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base64", version = "0.22.1" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "constant_time_eq", version = "0.3.1" },
|
||||
{ name = "cpufeatures", version = "0.2.17" },
|
||||
@@ -113,6 +114,8 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "zerocopy-derive", version = "0.7.35" },
|
||||
{ name = "zerocopy", version = "0.7.35" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
+24
-2
@@ -456,6 +456,21 @@ CREATE TABLE smtp_status_updates (
|
||||
descr TEXT NOT NULL -- text to send along with the updates
|
||||
);
|
||||
|
||||
-- Table to record the successful usage transports for sending.
|
||||
-- Sorting the table by rowid in descending order
|
||||
-- returns most recently successfully used transport first.
|
||||
CREATE TABLE smtp_success (
|
||||
-- Sequentially increasing ID of the success.
|
||||
-- Transport with the highest ID is to be used first.
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
|
||||
-- ID of the transport that was used to send a message.
|
||||
transport_id INTEGER UNIQUE NOT NULL,
|
||||
|
||||
-- Delete `smtp_success` rows when the transport is deleted.
|
||||
FOREIGN KEY(transport_id) REFERENCES transports(id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
-- Table of "sync items" to be grouped into sync messages
|
||||
-- and sent to own devices.
|
||||
CREATE TABLE multi_device_sync (
|
||||
@@ -630,9 +645,12 @@ CREATE TABLE broadcast_secrets(
|
||||
|
||||
|
||||
-- Candidate chatmail relays for automatic relay management.
|
||||
-- Holds the hosts a QR code contributed and the default relays already tried;
|
||||
-- the default list itself lives in `autorelay.rs` and is not stored here.
|
||||
CREATE TABLE relay_candidates(
|
||||
host TEXT PRIMARY KEY NOT NULL,
|
||||
last_tried INTEGER NOT NULL DEFAULT 0 -- Timestamp of the last connection attempt.
|
||||
-- Timestamp of the last connection attempt, 0 if the host was never tried.
|
||||
last_tried INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE transports (
|
||||
@@ -680,7 +698,11 @@ CREATE TABLE imap (
|
||||
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID header
|
||||
folder TEXT NOT NULL, -- IMAP folder
|
||||
target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
|
||||
|
||||
-- Destination folder. Empty string means that the message shall be deleted.
|
||||
-- Since we don't move messages between IMAP folders anymore,
|
||||
-- this is always either empty or equal to `folder`.
|
||||
target TEXT NOT NULL,
|
||||
uid INTEGER NOT NULL, -- UID
|
||||
uidvalidity INTEGER NOT NULL,
|
||||
UNIQUE (transport_id, folder, uid, uidvalidity)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.64.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -188,7 +188,7 @@ class Message:
|
||||
|
||||
def get_summarytext(self, width: int) -> str:
|
||||
"""Get a message summary as a single line of text. Typically used for notifications."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_summarytext(self._dc_msg, width))
|
||||
return from_dc_charpointer(lib.dc_msg_get_summary_text(self._dc_msg, 1, 1, width))
|
||||
|
||||
@props.with_doc
|
||||
def time_sent(self):
|
||||
|
||||
@@ -11,6 +11,9 @@ import random
|
||||
from queue import Queue
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from .capi import lib
|
||||
from .cutil import as_dc_charpointer
|
||||
|
||||
import pytest
|
||||
from _pytest._code import Source
|
||||
|
||||
@@ -366,6 +369,7 @@ class ACFactory:
|
||||
ac.open(passphrase)
|
||||
acname = ac._logid
|
||||
addr = f"{acname}@offline.org"
|
||||
lib.dc_add_pseudo_transport(ac._dc_context, as_dc_charpointer(addr))
|
||||
ac.update_config(
|
||||
{
|
||||
"configured_addr": addr,
|
||||
@@ -374,6 +378,7 @@ class ACFactory:
|
||||
)
|
||||
self._preconfigure_key(ac)
|
||||
self._acsetup.init_logging(ac)
|
||||
assert ac.is_configured(), "Pseudo configured account should look like if it is configured"
|
||||
return ac
|
||||
|
||||
def new_online_configuring_account(self, cloned_from=None, **kwargs) -> Account:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026-09-11
|
||||
2026-10-09
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
#
|
||||
# To automatically fix warnings, run
|
||||
# scripts/clippy.sh --fix --allow-dirty
|
||||
cargo clippy --locked --workspace --all-targets --all-features "$@" -- -D warnings
|
||||
cargo clippy --locked --workspace --exclude deltachat-jsonrpc-bindings --all-targets --all-features "$@" -- -D warnings
|
||||
|
||||
+8
-23
@@ -76,12 +76,8 @@ impl Accounts {
|
||||
Accounts::open(events, dir, writable).await
|
||||
}
|
||||
|
||||
/// Get the ID used to log events.
|
||||
///
|
||||
/// Account manager logs events with ID 0
|
||||
/// which is not used by any accounts.
|
||||
fn get_id(&self) -> u32 {
|
||||
0
|
||||
fn log_info(&self, file: &str, line: u32, msg: String) {
|
||||
self.emit_event(EventType::Info(format!("{file}:{line}: {msg}")));
|
||||
}
|
||||
|
||||
/// Ensures the accounts directory and config file exist.
|
||||
@@ -395,11 +391,6 @@ impl Accounts {
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::INFO,
|
||||
account_id = 0,
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
);
|
||||
let mut set = JoinSet::new();
|
||||
for account in accounts {
|
||||
set.spawn(async move {
|
||||
@@ -415,11 +406,6 @@ impl Accounts {
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::INFO,
|
||||
account_id = 0,
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
);
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
@@ -462,11 +448,6 @@ impl Accounts {
|
||||
id: 0,
|
||||
typ: EventType::Warning("Background fetch timed out.".to_string()),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::WARN,
|
||||
account_id = 0,
|
||||
"Background fetch timed out."
|
||||
);
|
||||
}
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
@@ -482,11 +463,15 @@ impl Accounts {
|
||||
/// return immediately even before the timeout expiration
|
||||
/// or finishing fetching.
|
||||
///
|
||||
/// Pending outgoing messages are not waited for and not triggered.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
/// If another background fetch is already running,
|
||||
/// nothing is fetched and the event is emitted immediately.
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it only safely refers to your call if no concurrent background fetch is happening.
|
||||
///
|
||||
/// Returns a future that resolves when background fetch is done,
|
||||
/// but does not capture `&self`.
|
||||
@@ -524,7 +509,7 @@ impl Accounts {
|
||||
pub async fn is_sending_finished(&self) -> Result<bool> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
for account in accounts {
|
||||
if !smtp::is_queue_empty(&account).await? {
|
||||
if !smtp::queue::is_empty(&account).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
@@ -545,7 +530,7 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
/// Emits a single event with ID 0, which is not used by any accounts.
|
||||
pub fn emit_event(&self, event: EventType) {
|
||||
self.events.emit(Event { id: 0, typ: event })
|
||||
}
|
||||
|
||||
+147
-46
@@ -1,29 +1,24 @@
|
||||
//! # Automatic relay handling (experimental, still in development)
|
||||
//! # Automatic multi-relay onboarding
|
||||
//!
|
||||
//! Chatmail relays create an account on first login,
|
||||
//! so a profile can add further transports on its own without user interaction.
|
||||
//! Candidate hosts come from the `relay_candidates` table,
|
||||
//! which migrations seed with a list of known chatmail relays.
|
||||
//!
|
||||
//! Status of implementation:
|
||||
//! Additions are attempted right before going into IMAP IDLE,
|
||||
//! i.e. only while connected and with nothing more important to do,
|
||||
//! and only if a UI opted in via [`Config::Autorelay`].
|
||||
//! Once a profile has reached `NUM_TRANSPORTS_TARGET` transports,
|
||||
//! [`Config::AutorelayFinished`] is set and nothing is ever added again,
|
||||
//! so deleting a transport later does not pull in a replacement.
|
||||
//! Support for automatically onboarding a profile on transport
|
||||
//! candidates without the user choosing a relay.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, format_err};
|
||||
use deltachat_contact_tools::addr_normalize;
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use rand::seq::IndexedRandom;
|
||||
use rand::seq::{IndexedRandom, SliceRandom};
|
||||
use rusqlite::Transaction;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::configure::{EnteredLoginParam, SILENT_PROGRESS, configure};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam};
|
||||
use crate::{configure::EnteredLoginParam, context::Context, tools::time};
|
||||
use crate::net::{connect_tcp, proxy::ProxyConfig};
|
||||
use crate::{context::Context, tools::time};
|
||||
|
||||
/// The target number of transports.
|
||||
const NUM_TRANSPORTS_TARGET: usize = 3;
|
||||
@@ -32,6 +27,89 @@ const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour
|
||||
/// How long we ignore a relay candidate after failing to connect to it:
|
||||
const BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY: i64 = 60 * 60 * 24 * 7; // one week
|
||||
|
||||
/// Sorted relay list a profile can attempt to onboard on without the user choosing one.
|
||||
const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||
"chat.adminforge.de", // iroh relay 404s
|
||||
"chat.me.ke",
|
||||
"chat.nuvon.app",
|
||||
"chat.tinydispatch.org",
|
||||
"chat.vim.wtf",
|
||||
"chatmail.au",
|
||||
"chatmail.cc",
|
||||
"chatmail.uk",
|
||||
"chtml.ca",
|
||||
"deltachat.me",
|
||||
"e2e.sus.fr",
|
||||
"e2ee.wang",
|
||||
"mailchat.pl",
|
||||
"nchrcht.la10cy.net",
|
||||
"nine.testrun.org",
|
||||
"sweetfern.net",
|
||||
"tarpit.fun",
|
||||
];
|
||||
|
||||
/// Records the hosts of `addrs` as relay candidates.
|
||||
pub(crate) async fn add_relay_candidates(context: &Context, addrs: &[String]) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| hosts_of(addrs).try_for_each(|host| save_relay_candidate(tx, host, 0)))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds a first transport on the relay candidate that answers fastest.
|
||||
///
|
||||
/// All candidates are probed at once with a TCP connection to their HTTPS port
|
||||
/// and configured in the order in which the connections complete,
|
||||
/// stopping at the first success. Candidates that fail the probe are skipped.
|
||||
/// Answering TCP fastest is used as a network proximity measure,
|
||||
/// which keeps latency low for initial onboarding,
|
||||
/// and it avoids relays that are down or black-holing traffic.
|
||||
pub(crate) async fn add_transport_from_candidates(
|
||||
context: &Context,
|
||||
skip_network: bool,
|
||||
) -> Result<()> {
|
||||
let mut candidates = triable_relay_candidates(context, time()).await?;
|
||||
candidates.shuffle(&mut rand::rng());
|
||||
let mut probes = JoinSet::new();
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
let load_cache = false;
|
||||
for host in candidates {
|
||||
let ctx = context.clone();
|
||||
let proxy_config = proxy_config.clone();
|
||||
probes.spawn(async move {
|
||||
let res = match proxy_config {
|
||||
_ if skip_network => Ok(()),
|
||||
Some(proxy) => proxy.connect(&ctx, &host, 443, load_cache).await.map(drop),
|
||||
None => connect_tcp(&ctx, &host, 443, load_cache).await.map(drop),
|
||||
};
|
||||
(host, res)
|
||||
});
|
||||
}
|
||||
|
||||
let mut last_err = format_err!("No relay candidates");
|
||||
let mark_as_autorelay = true;
|
||||
while let Some(res) = probes.join_next().await {
|
||||
let (host, res) = res?;
|
||||
if let Err(err) = res {
|
||||
warn!(context, "Failed to connect to relay {host}: {err:#}.");
|
||||
last_err = err;
|
||||
continue;
|
||||
}
|
||||
let param = login_param_from_host(&host, mark_as_autorelay);
|
||||
match configure(context, ¶m, skip_network).await {
|
||||
Ok(()) => {
|
||||
info!(context, "Added a transport on relay {host}.");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to add relay {host}: {err:#}.");
|
||||
last_err = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_add_additional_relays(
|
||||
context: Context,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
@@ -87,16 +165,13 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
for _ in 0..NUM_TRANSPORTS_TARGET {
|
||||
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
|
||||
context
|
||||
.set_config_internal(Config::AutorelayFinished, config::from_bool(true))
|
||||
.set_config_internal(Config::AutorelayFinished, Some(config::from_bool(true)))
|
||||
.await?;
|
||||
|
||||
return Ok(relay_added);
|
||||
}
|
||||
|
||||
// First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY` seconds.
|
||||
// Hosts that are already used are excluded.
|
||||
let candidates = load_relay_candidates(context, now).await?;
|
||||
|
||||
let candidates = triable_relay_candidates(context, now).await?;
|
||||
let Some(host) = candidates.choose(&mut rand::rng()) else {
|
||||
info!(
|
||||
context,
|
||||
@@ -113,13 +188,13 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE relay_candidates SET last_tried=? WHERE host=?",
|
||||
(now, host),
|
||||
)
|
||||
.transaction(|tx| save_relay_candidate(tx, host, now))
|
||||
.await?;
|
||||
let param = login_param_from_host(host);
|
||||
let res = crate::configure::configure(context, ¶m, skip_network).await;
|
||||
let mark_as_autorelay = true;
|
||||
let param = login_param_from_host(host, mark_as_autorelay);
|
||||
let res = SILENT_PROGRESS
|
||||
.scope((), configure(context, ¶m, skip_network))
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
context,
|
||||
@@ -134,37 +209,63 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
Ok(relay_added)
|
||||
}
|
||||
|
||||
async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>, anyhow::Error> {
|
||||
async fn triable_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>> {
|
||||
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
||||
let candidates: Vec<String> = context
|
||||
let mut last_tried: BTreeMap<String, i64> = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
// This also selects candidates which have last_tried in the future,
|
||||
// essentially treating them as never tried,
|
||||
// so if some timestamp far in the future is accidentally stored,
|
||||
// we are not stuck never trying the candidate.
|
||||
// After trying the candidate, last_tried will be corrected to the current time.
|
||||
"SELECT host FROM relay_candidates WHERE (last_tried<? OR last_tried>?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM transports
|
||||
WHERE substr(addr, instr(addr, '@') + 1) = host
|
||||
)",
|
||||
(cutoff_timestamp, now),
|
||||
|row| Ok(row.get::<_, String>(0)?),
|
||||
)
|
||||
.query_map_collect("SELECT host, last_tried FROM relay_candidates", (), |row| {
|
||||
Ok((row.get(0)?, row.get(1)?))
|
||||
})
|
||||
.await?;
|
||||
for host in DEFAULT_RELAY_CANDIDATES {
|
||||
last_tried.entry(host.to_string()).or_insert(0);
|
||||
}
|
||||
let self_addrs = context.get_self_addrs().await?;
|
||||
let used_hosts: Vec<&str> = hosts_of(&self_addrs).collect();
|
||||
|
||||
// We also try candidates which have `last_tried` in the future,
|
||||
// which on next failure get `last_tried` reset to the current time.
|
||||
let candidates = last_tried
|
||||
.into_iter()
|
||||
.filter(|(host, last_tried)| {
|
||||
(*last_tried < cutoff_timestamp || *last_tried > now)
|
||||
&& !used_hosts.contains(&host.as_str())
|
||||
})
|
||||
.map(|(host, _)| host)
|
||||
.collect();
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str) -> EnteredLoginParam {
|
||||
/// Returns the host of each address in `addrs`.
|
||||
fn hosts_of(addrs: &[String]) -> impl Iterator<Item = &str> {
|
||||
addrs.iter().filter_map(|a| Some(a.rsplit_once('@')?.1))
|
||||
}
|
||||
|
||||
/// Records `host` as a relay candidate, overwriting a stored `last_tried`.
|
||||
fn save_relay_candidate(tx: &Transaction, host: &str, last_tried: i64) -> Result<()> {
|
||||
tx.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)
|
||||
ON CONFLICT(host) DO UPDATE SET last_tried=excluded.last_tried",
|
||||
(host, last_tried),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str, mark_as_autorelay: bool) -> EnteredLoginParam {
|
||||
let rng = &mut rand::rng();
|
||||
let username = Alphanumeric.sample_string(rng, 9);
|
||||
let addr = username + "@" + host;
|
||||
let addr = addr_normalize(&addr);
|
||||
|
||||
// `mark_as_autorelay` is a temporary precaution hack
|
||||
// while introducing onboarding on multiple community relays from a list:
|
||||
// though relay operators were asked to get on that list, unexpected things can happen,
|
||||
// and they want to return to allow only manual onboarding.
|
||||
// this is possible by failing on `password_len == 23`.
|
||||
|
||||
// 22 * log2(26 * 2 + 10) = 130 bits of entropy
|
||||
let password = Alphanumeric.sample_string(rng, 22);
|
||||
let password = Alphanumeric.sample_string(rng, if mark_as_autorelay { 23 } else { 22 });
|
||||
|
||||
EnteredLoginParam {
|
||||
addr,
|
||||
|
||||
@@ -1,42 +1,150 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::EventType;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
/// Tests that the default relays are candidates without a row in the table.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
async fn test_triable_relay_candidates_defaults() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
let now = time();
|
||||
|
||||
assert!(DEFAULT_RELAY_CANDIDATES.is_sorted());
|
||||
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
assert_eq!(candidates, DEFAULT_RELAY_CANDIDATES);
|
||||
|
||||
let tried = DEFAULT_RELAY_CANDIDATES[0];
|
||||
save_relay_candidates(t, &[tried], now).await?;
|
||||
let candidates = triable_relay_candidates(t, now).await?;
|
||||
assert_eq!(candidates.len(), DEFAULT_RELAY_CANDIDATES.len() - 1);
|
||||
assert!(!candidates.contains(&tried.to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a transport is added on a candidate from the given addresses.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_transport_from_candidates() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
mark_defaults_tried(t, time()).await?;
|
||||
let addrs_from_qr = [
|
||||
"alice@example.org".to_string(),
|
||||
"bob@example.org".to_string(),
|
||||
];
|
||||
let skip_network = true;
|
||||
add_relay_candidates(t, &addrs_from_qr).await?;
|
||||
add_transport_from_candidates(t, skip_network).await?;
|
||||
|
||||
let transports = t.list_transports().await?;
|
||||
assert_eq!(transports.len(), 1);
|
||||
assert!(transports[0].addr.ends_with("@example.org"));
|
||||
let untried = untried_relay_candidates(t).await?;
|
||||
assert_eq!(untried, ["example.org"]);
|
||||
assert!(configure_progress_emitted(t).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests correct add_transport_from_candidates error handling.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_transport_from_candidates_failure() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
mark_defaults_tried(t, time()).await?;
|
||||
save_relay_candidates(t, &["bad host", "worse host"], 0).await?;
|
||||
|
||||
let skip_network = true;
|
||||
let err = add_transport_from_candidates(t, skip_network)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(format!("{err:#}").contains("Bad email-address"));
|
||||
assert!(!t.is_configured().await?);
|
||||
let untried = untried_relay_candidates(t).await?;
|
||||
assert_eq!(untried, ["bad host", "worse host"]);
|
||||
t.assert_warns_or_errors(&[
|
||||
"Failed to add relay bad host",
|
||||
"Failed to add relay worse host",
|
||||
])
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn untried_relay_candidates(t: &TestContext) -> Result<Vec<String>> {
|
||||
t.sql
|
||||
.query_map_vec(
|
||||
"SELECT host FROM relay_candidates WHERE last_tried=0 ORDER BY host",
|
||||
(),
|
||||
|row| Ok(row.get(0)?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn save_relay_candidates(t: &TestContext, hosts: &[&str], last_tried: i64) -> Result<()> {
|
||||
t.sql
|
||||
.transaction(|tx| {
|
||||
for host in hosts {
|
||||
save_relay_candidate(tx, host, last_tried)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Keeps the default relays out of `triable_relay_candidates()`.
|
||||
async fn mark_defaults_tried(t: &TestContext, now: i64) -> Result<()> {
|
||||
save_relay_candidates(t, DEFAULT_RELAY_CANDIDATES, now).await
|
||||
}
|
||||
|
||||
/// Consumes emitted events, telling whether a configure progress is among them.
|
||||
async fn configure_progress_emitted(t: &TestContext) -> bool {
|
||||
t.evtracker
|
||||
.get_matching_opt(t, |evt| matches!(evt, EventType::ConfigureProgress { .. }))
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// Tests that saving a candidate overwrites its stored timestamp.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_relay_candidate() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
let now = time();
|
||||
|
||||
for last_tried in [0, now, 0] {
|
||||
t.sql
|
||||
.transaction(|tx| save_relay_candidate(tx, "relay.example", last_tried))
|
||||
.await?;
|
||||
let stored: Option<i64> = t
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT last_tried FROM relay_candidates WHERE host=?",
|
||||
("relay.example",),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(stored, Some(last_tried));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_triable_relay_candidates_single() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
|
||||
// This host should be returned by load_relay_candidates():
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("never_tried.example", 0),
|
||||
)
|
||||
.await?;
|
||||
save_relay_candidates(t, &["never_tried.example", "example.org"], 0).await?;
|
||||
save_relay_candidates(t, &["recent.example"], now).await?;
|
||||
|
||||
// This host was recently tried and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("recent.example", now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This host is already in use (alice@example.org) and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("example.org", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let candidates = load_relay_candidates(t, now).await?;
|
||||
let candidates = triable_relay_candidates(t, now).await?;
|
||||
|
||||
assert_eq!(candidates, vec!["never_tried.example".to_string()]);
|
||||
|
||||
@@ -44,22 +152,15 @@ async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_multiple() -> Result<()> {
|
||||
async fn test_triable_relay_candidates_multiple() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["a.example", "b.example", "c.example"], 0).await?;
|
||||
|
||||
let mut candidates = load_relay_candidates(t, now).await?;
|
||||
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
|
||||
assert_eq!(
|
||||
@@ -160,13 +261,8 @@ async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("relay.example", 0),
|
||||
)
|
||||
.await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["relay.example"], 0).await?;
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
|
||||
@@ -179,6 +275,7 @@ async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, transports_before + 1);
|
||||
assert!(!configure_progress_emitted(t).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -189,15 +286,8 @@ async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example", "d.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["a.example", "b.example", "c.example", "d.example"], 0).await?;
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
@@ -218,14 +308,9 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
for i in 1..10 {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(format!("{i}.invalid.example"), 0),
|
||||
)
|
||||
.await?;
|
||||
save_relay_candidates(t, &[format!("{i}.invalid.example").as_str()], 0).await?;
|
||||
}
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
@@ -254,7 +339,7 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
|
||||
// ...but not all, because there might be many relay candidates
|
||||
// and we don't want to try all of them in a single call:
|
||||
assert_eq!(load_relay_candidates(t, now).await?.is_empty(), false);
|
||||
assert_eq!(triable_relay_candidates(t, now).await?.is_empty(), false);
|
||||
|
||||
t.assert_warns_or_errors(&[
|
||||
"DNS lookup with memory cache failure",
|
||||
|
||||
+26
-31
@@ -164,9 +164,9 @@ impl<'a> BlobObject<'a> {
|
||||
/// you want to create a [BlobObject] for a filename read from the
|
||||
/// database.
|
||||
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,
|
||||
let name = match name.strip_prefix("$BLOBDIR/") {
|
||||
Some(name) => name,
|
||||
None => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {name}"));
|
||||
@@ -289,6 +289,7 @@ impl<'a> BlobObject<'a> {
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
) -> Result<String> {
|
||||
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
|
||||
let (max_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
@@ -326,6 +327,7 @@ impl<'a> BlobObject<'a> {
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
) -> Result<String> {
|
||||
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
|
||||
// Add white background only to avatars to spare the CPU.
|
||||
let mut add_white_bg = is_avatar;
|
||||
let mut no_exif = false;
|
||||
@@ -340,45 +342,38 @@ impl<'a> BlobObject<'a> {
|
||||
// It's strange that BufReader modifies a file position while it takes a non-mut
|
||||
// reference. Ok, just rewind it.
|
||||
file.rewind()?;
|
||||
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
|
||||
let imgreader = match imgreader {
|
||||
Ok(ir) => ir,
|
||||
_ => {
|
||||
file.rewind()?;
|
||||
ImageReader::with_format(
|
||||
std::io::BufReader::new(&file),
|
||||
ImageFormat::from_path(self.to_abs_path())?,
|
||||
)
|
||||
}
|
||||
};
|
||||
let imgreader = ImageReader::new(std::io::BufReader::new(&file))
|
||||
.with_guessed_format()
|
||||
.context("Failed to guess image format due to I/O error")?;
|
||||
let fmt = imgreader.format().context("Unknown format")?;
|
||||
if *vt == Viewtype::File {
|
||||
*vt = Viewtype::Image;
|
||||
return Ok(name);
|
||||
}
|
||||
let mut img = imgreader.decode().context("image decode failure")?;
|
||||
|
||||
let mut img = match fmt {
|
||||
image::ImageFormat::WebP => {
|
||||
// `with_guessed_format()` restores file position,
|
||||
// so `buf_reader` is at the beginning of the file.
|
||||
let buf_reader = imgreader.into_inner();
|
||||
let webp_decoder = image::codecs::webp::WebPDecoder::new(buf_reader)
|
||||
.context("Failed to create WebP decoder")?;
|
||||
|
||||
// If WebP has animation, do not try to recode it.
|
||||
// Recoding into JPEG will result in losing the animation.
|
||||
if !is_avatar && webp_decoder.has_animation() {
|
||||
return Ok(name);
|
||||
}
|
||||
DynamicImage::from_decoder(webp_decoder)?
|
||||
}
|
||||
_ => imgreader.decode().context("Failed to decode image")?,
|
||||
};
|
||||
let orientation = exif
|
||||
.as_ref()
|
||||
.map(|exif| exif_orientation(exif, context))
|
||||
.unwrap_or(Orientation::NoTransforms);
|
||||
let mut encoded = Vec::new();
|
||||
|
||||
if *vt == Viewtype::Sticker {
|
||||
let x_max = img.width().saturating_sub(1);
|
||||
let y_max = img.height().saturating_sub(1);
|
||||
if !img.in_bounds(x_max, y_max)
|
||||
|| !(img.get_pixel(0, 0).0[3] == 0
|
||||
|| img.get_pixel(x_max, 0).0[3] == 0
|
||||
|| img.get_pixel(0, y_max).0[3] == 0
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0)
|
||||
{
|
||||
*vt = Viewtype::Image;
|
||||
} else {
|
||||
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
|
||||
// from UIs shouldn't contain sensitive Exif info.
|
||||
return Ok(name);
|
||||
}
|
||||
}
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit,
|
||||
|
||||
+67
-1
@@ -4,7 +4,9 @@ use super::*;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::sql;
|
||||
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
|
||||
use crate::test_utils::{
|
||||
self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||
@@ -738,6 +740,70 @@ async fn test_send_gif_as_sticker() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that animated WebP is sent without reencoding.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_animated_webp_as_image() -> Result<()> {
|
||||
let bytes = include_bytes!("../../test-data/image/animated.webp");
|
||||
assert!(
|
||||
bytes.len() > constants::WORSE_IMAGE_SIZE as usize,
|
||||
"Animated WebP should be large enough to trigger reencoding"
|
||||
);
|
||||
let (width, height) = (900u32, 900u32);
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
alice
|
||||
.set_config(
|
||||
Config::MediaQuality,
|
||||
Some(&(MediaQuality::Worse as i32).to_string()),
|
||||
)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension("webp");
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("Failed to write file")?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("file.webp"), None)?;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, width);
|
||||
assert_eq!(bob_msg.get_height() as u32, height);
|
||||
assert_eq!(
|
||||
bob_msg.get_filebytes(bob).await?.unwrap(),
|
||||
bytes.len() as u64
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if user sets animated WebP as an avatar, it may be recoded.
|
||||
///
|
||||
/// We don't want to recode animated WebPs into JPEG and lose animation,
|
||||
/// but for avatars we don't want animation and transparency anyway.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_animated_webp_avatar() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
|
||||
let avatar_src = t.dir.path().join("avatar.webp");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/animated.webp");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await?;
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await?.unwrap();
|
||||
assert!(avatar_blob.ends_with(".jpg"));
|
||||
|
||||
let scaled_avatar_size = fs::metadata(&avatar_blob).await.unwrap().len();
|
||||
assert!(
|
||||
scaled_avatar_size < avatar_bytes.len() as u64,
|
||||
"Animated WebP avatar must be recoded"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_deduplicate() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
+13
-46
@@ -42,7 +42,7 @@ const RINGING_SECONDS: i64 = 120;
|
||||
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
|
||||
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
|
||||
|
||||
const STUN_PORT: u16 = 3478;
|
||||
const TURN_PORT: u16 = 3478;
|
||||
|
||||
/// Set if incoming call was ended explicitly
|
||||
/// by the other side before we accepted it.
|
||||
@@ -658,12 +658,9 @@ pub(crate) async fn create_ice_servers_from_metadata(
|
||||
Ok((expiration_timestamp, ice_servers))
|
||||
}
|
||||
|
||||
/// STUN or TURN server with unresolved DNS name.
|
||||
/// TURN server with unresolved DNS name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum UnresolvedIceServer {
|
||||
/// STUN server.
|
||||
Stun { hostname: String, port: u16 },
|
||||
|
||||
/// TURN server with the username and password.
|
||||
Turn {
|
||||
hostname: String,
|
||||
@@ -688,28 +685,6 @@ pub(crate) async fn resolve_ice_servers(
|
||||
|
||||
for unresolved_ice_server in unresolved_ice_servers {
|
||||
match unresolved_ice_server {
|
||||
UnresolvedIceServer::Stun { hostname, port } => {
|
||||
match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
let stun_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
result.push(stun_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve STUN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname,
|
||||
port,
|
||||
@@ -742,25 +717,17 @@ pub(crate) async fn resolve_ice_servers(
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
// which was previously stunserver2024.stunprotocol.org)
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
vec![
|
||||
UnresolvedIceServer::Stun {
|
||||
hostname: "nine.testrun.org".to_string(),
|
||||
port: STUN_PORT,
|
||||
},
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname: "turn.delta.chat".to_string(),
|
||||
port: STUN_PORT,
|
||||
username: "public".to_string(),
|
||||
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
|
||||
},
|
||||
]
|
||||
// As long as we can't rely on most chat profiles
|
||||
// having a multi-relay setup and offering TURN servers,
|
||||
// fall back to a service run by Delta Chat developers
|
||||
// which ensures that no IP addresses or other metadata is
|
||||
// persistently logged.
|
||||
vec![UnresolvedIceServer::Turn {
|
||||
hostname: "turn.delta.chat".to_string(),
|
||||
port: TURN_PORT,
|
||||
username: "public".to_string(),
|
||||
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
|
||||
}]
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
|
||||
@@ -779,3 +779,13 @@ async fn test_end_text_call() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that fallback ice servers just carry the Turn one
|
||||
#[test]
|
||||
fn test_fallback_ice_servers() {
|
||||
let hostnames: Vec<String> = create_fallback_ice_servers()
|
||||
.into_iter()
|
||||
.map(|UnresolvedIceServer::Turn { hostname, .. }| hostname)
|
||||
.collect();
|
||||
assert_eq!(hostnames, ["turn.delta.chat"]);
|
||||
}
|
||||
|
||||
+28
-140
@@ -3,7 +3,6 @@
|
||||
use std::cmp;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::marker::Sync;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
@@ -30,17 +29,17 @@ use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD};
|
||||
use crate::ensure_and_debug_assert_eq;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key::{DcKey as _, Fingerprint, self_fingerprint};
|
||||
use crate::key::{Fingerprint, self_fingerprint};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory;
|
||||
use crate::mimefactory::{MimeFactory, QueueSideEffects, QueuedMail, ToBeQueuedMail};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::addresses_from_public_key;
|
||||
use crate::reaction::broadcast_reactions;
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::queue::{ToBeQueuedMail, enqueue_mail};
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
@@ -331,7 +330,7 @@ impl ChatId {
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
fn set_selfavatar_timestamp(
|
||||
pub(crate) fn set_selfavatar_timestamp(
|
||||
self,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
timestamp: i64,
|
||||
@@ -1918,8 +1917,7 @@ impl Chat {
|
||||
let new_mime_headers: Option<String> = new_mime_headers.map(|s| {
|
||||
let html_part = MimePart::new("text/html", s);
|
||||
let mut buffer = Vec::new();
|
||||
let cursor = Cursor::new(&mut buffer);
|
||||
html_part.write_part(cursor).ok();
|
||||
html_part.write_part(&mut buffer);
|
||||
String::from_utf8_lossy(&buffer).to_string()
|
||||
});
|
||||
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
|
||||
@@ -2815,99 +2813,6 @@ async fn render_mime_message_and_pre_message(
|
||||
}
|
||||
}
|
||||
|
||||
/// Process side effects and store queued mail.
|
||||
pub(crate) fn enqueue_mail(
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
now: i64,
|
||||
msg_id: MsgId,
|
||||
queued_mail: &QueuedMail,
|
||||
side_effects: Option<&QueueSideEffects>,
|
||||
) -> Result<i64> {
|
||||
if let Some(side_effects) = side_effects {
|
||||
if let Some(last_added_location_timestamp) = side_effects.last_added_location_timestamp {
|
||||
transaction.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
(last_added_location_timestamp, side_effects.chat_id),
|
||||
)?;
|
||||
}
|
||||
|
||||
if side_effects.avatar_is_attached {
|
||||
side_effects
|
||||
.chat_id
|
||||
.set_selfavatar_timestamp(transaction, now)
|
||||
.context("Failed to set selfavatar timestamp")?;
|
||||
}
|
||||
|
||||
if let Some(ref sync_ids) = side_effects.sync_ids_to_delete {
|
||||
transaction.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Store mail into queue.
|
||||
let all_recipients = queued_mail.recipients.join(" ");
|
||||
let is_encrypted = queued_mail.encryption.is_encrypted();
|
||||
|
||||
transaction
|
||||
.execute(
|
||||
"
|
||||
INSERT INTO smtp2 (
|
||||
display_name,
|
||||
rfc724_mid,
|
||||
mime,
|
||||
should_attach_pubkey,
|
||||
should_compress,
|
||||
should_sign,
|
||||
msg_id,
|
||||
recipients,
|
||||
bcc_self,
|
||||
is_encrypted,
|
||||
shared_secret,
|
||||
encryption_fingerprints
|
||||
)
|
||||
VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
",
|
||||
(
|
||||
&queued_mail.display_name,
|
||||
&queued_mail.rfc724_mid,
|
||||
&queued_mail.raw_message,
|
||||
queued_mail.should_attach_pubkey,
|
||||
queued_mail.should_compress,
|
||||
queued_mail.should_sign,
|
||||
msg_id,
|
||||
&all_recipients,
|
||||
queued_mail.bcc_self,
|
||||
is_encrypted,
|
||||
if let mimefactory::QueuedEncryption::Symmetric { ref shared_secret } =
|
||||
queued_mail.encryption
|
||||
{
|
||||
shared_secret
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if let mimefactory::QueuedEncryption::Asymmetric {
|
||||
ref encryption_pubkeys,
|
||||
} = queued_mail.encryption
|
||||
{
|
||||
let res: Vec<String> = encryption_pubkeys
|
||||
.iter()
|
||||
.map(|pubkey| pubkey.dc_fingerprint().hex())
|
||||
.collect();
|
||||
res.join(" ")
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
),
|
||||
)
|
||||
.context("Failed to insert a row into smtp2 table")?;
|
||||
let row_id = transaction.last_insert_rowid();
|
||||
Ok(row_id)
|
||||
}
|
||||
|
||||
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
|
||||
///
|
||||
/// Updates the message `GuaranteeE2ee` parameter and persists it
|
||||
@@ -3017,9 +2922,7 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref side_effects) = side_effects {
|
||||
msg.subject.clone_from(&side_effects.subject);
|
||||
}
|
||||
msg.subject.clone_from(&side_effects.subject);
|
||||
if is_encrypted {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
} else {
|
||||
@@ -3061,13 +2964,13 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
now,
|
||||
msg.id,
|
||||
&queued_pre_msg,
|
||||
pre_side_effects.as_ref(),
|
||||
Some(&pre_side_effects),
|
||||
)
|
||||
.context("Failed to enqueue pre-message")?;
|
||||
row_ids.push(row_id)
|
||||
}
|
||||
row_ids.push(
|
||||
enqueue_mail(transaction, now, msg.id, &queued_msg, side_effects.as_ref())
|
||||
enqueue_mail(transaction, now, msg.id, &queued_msg, Some(&side_effects))
|
||||
.context("Failed to enqueue message")?,
|
||||
);
|
||||
Ok(row_ids)
|
||||
@@ -3732,7 +3635,7 @@ pub(crate) async fn create_out_broadcast_ext(
|
||||
)?;
|
||||
ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}");
|
||||
let mut params: Params = Params::new();
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time())?;
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time());
|
||||
|
||||
t.execute(
|
||||
"INSERT INTO chats
|
||||
@@ -4011,15 +3914,7 @@ pub(crate) async fn add_contact_to_chat_ext(
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
let contact_addr = contact.get_addr().to_lowercase();
|
||||
let added_by = if from_handshake && chat.typ == Chattype::OutBroadcast {
|
||||
// The contact was added via a QR code rather than explicit user action,
|
||||
// so it could be confusing to say 'You added member Alice'.
|
||||
// And in a broadcast, SELF is the only one who can add members,
|
||||
// so, no information is lost by just writing 'Member Alice added' instead.
|
||||
ContactId::UNDEFINED
|
||||
} else {
|
||||
ContactId::SELF
|
||||
};
|
||||
let added_by = ContactId::SELF;
|
||||
msg.text = stock_str::msg_add_member_local(context, contact.id, added_by).await;
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact_addr);
|
||||
@@ -4033,6 +3928,11 @@ pub(crate) async fn add_contact_to_chat_ext(
|
||||
.await?
|
||||
.context("Failed to find broadcast shared secret")?;
|
||||
msg.param.set(PARAM_BROADCAST_SECRET, secret);
|
||||
|
||||
// We don't show "member added" info-messages in channels,
|
||||
// because there can be a lot members added,
|
||||
// and these messages would clutter the timeline.
|
||||
msg.hidden = true;
|
||||
}
|
||||
send_msg(context, chat_id, &mut msg).await?;
|
||||
|
||||
@@ -4891,12 +4791,14 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
///
|
||||
/// Optional `label` can be provided to ensure that message is added only once.
|
||||
/// If `important` is true, a notification will be sent.
|
||||
/// `timestamp_sent` is the time shown on the message; it does not affect ordering.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn add_device_msg_with_importance(
|
||||
context: &Context,
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
important: bool,
|
||||
timestamp_sent: i64,
|
||||
) -> Result<MsgId> {
|
||||
ensure!(
|
||||
label.is_some() || msg.is_some(),
|
||||
@@ -4916,11 +4818,10 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let timestamp_sent = time();
|
||||
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
msg.timestamp_sort = timestamp_sent;
|
||||
msg.timestamp_sort = time();
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
|
||||
&& msg.timestamp_sort <= last_msg_time
|
||||
{
|
||||
@@ -4988,7 +4889,7 @@ pub async fn add_device_msg(
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
) -> Result<MsgId> {
|
||||
add_device_msg_with_importance(context, label, msg, false).await
|
||||
add_device_msg_with_importance(context, label, msg, false, time()).await
|
||||
}
|
||||
|
||||
/// Returns true if device message with a given label was ever added to the device chat.
|
||||
@@ -5016,6 +4917,12 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
|
||||
.await?;
|
||||
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
|
||||
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
|
||||
context
|
||||
@@ -5200,7 +5107,7 @@ async fn set_contacts_by_fingerprints(
|
||||
if contacts == contacts_old {
|
||||
return Ok(());
|
||||
}
|
||||
let broadcast_contacts_added = context
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
// For broadcast channels, we only add members,
|
||||
@@ -5217,31 +5124,12 @@ async fn set_contacts_by_fingerprints(
|
||||
let mut statement = transaction.prepare(
|
||||
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)",
|
||||
)?;
|
||||
let mut broadcast_contacts_added = Vec::new();
|
||||
for contact_id in &contacts {
|
||||
if statement.execute((id, contact_id))? > 0 && chat.typ == Chattype::OutBroadcast {
|
||||
broadcast_contacts_added.push(*contact_id);
|
||||
}
|
||||
statement.execute((id, contact_id))?;
|
||||
}
|
||||
Ok(broadcast_contacts_added)
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
let timestamp = time();
|
||||
for added_id in broadcast_contacts_added {
|
||||
let msg = stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED).await;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
id,
|
||||
&msg,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
Some(timestamp),
|
||||
timestamp,
|
||||
None,
|
||||
Some(ContactId::SELF),
|
||||
Some(added_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+15
-29
@@ -3003,13 +3003,13 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
|
||||
tcm.section("Bob receives the name-change system message");
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.subject, "Re: My great broadcast");
|
||||
assert_eq!(msg.subject, "My great broadcast");
|
||||
let bob_chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert_eq!(bob_chat.name, "My great broadcast");
|
||||
|
||||
tcm.section("Fiona receives the name-change system message");
|
||||
let msg = fiona.recv_msg(&sent).await;
|
||||
assert_eq!(msg.subject, "Re: My great broadcast");
|
||||
assert_eq!(msg.subject, "My great broadcast");
|
||||
let fiona_chat = Chat::load_from_db(fiona, msg.chat_id).await?;
|
||||
assert_eq!(fiona_chat.name, "My great broadcast");
|
||||
}
|
||||
@@ -3343,41 +3343,27 @@ async fn test_broadcast_recipients_sync1() -> Result<()> {
|
||||
alice2.assert_warn("unknown grpid").await;
|
||||
|
||||
let member_added = alice1.pop_sent_msg().await;
|
||||
let a2_charlie_added = alice2.recv_msg(&member_added).await;
|
||||
alice2.recv_msg_trash(&member_added).await;
|
||||
let _c_member_added = charlie.recv_msg(&member_added).await;
|
||||
let a2_chatlist = Chatlist::try_load(alice2, 0, Some("Channel"), None).await?;
|
||||
assert_eq!(a2_chatlist.get_msg_id(0)?.unwrap(), a2_charlie_added.id);
|
||||
|
||||
// Alice1 will now sync the full member list to Alice2:
|
||||
sync(alice1, alice2).await;
|
||||
let a2_bob_contact = alice2.add_or_lookup_contact_id(bob).await;
|
||||
let a2_charlie_contact = alice2.add_or_lookup_contact_id(charlie).await;
|
||||
let a2_chatlist = Chatlist::try_load(alice2, 0, Some("Channel"), None).await?;
|
||||
let msg_id = a2_chatlist.get_msg_id(0)?.unwrap();
|
||||
let a2_bob_added = Message::load_from_db(alice2, msg_id).await?;
|
||||
assert_ne!(a2_bob_added.id, a2_charlie_added.id);
|
||||
assert_eq!(
|
||||
a2_bob_added.text,
|
||||
stock_str::msg_add_member_local(alice2, a2_bob_contact, ContactId::UNDEFINED).await
|
||||
);
|
||||
assert_eq!(a2_bob_added.from_id, ContactId::SELF);
|
||||
assert_eq!(
|
||||
a2_bob_added.param.get_cmd(),
|
||||
SystemMessage::MemberAddedToGroup
|
||||
);
|
||||
assert_eq!(
|
||||
ContactId::new(
|
||||
a2_bob_added
|
||||
.param
|
||||
.get_int(Param::ContactAddedRemoved)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
),
|
||||
a2_bob_contact
|
||||
);
|
||||
let a2_chat_id = a2_chatlist.get_chat_id(0).unwrap();
|
||||
|
||||
let a2_chat_members = get_chat_contacts(alice2, a2_charlie_added.chat_id).await?;
|
||||
// Also for Alice2, no info message should be shown;
|
||||
// she should see only the "Messages are end-to-end encrypted" message.
|
||||
let a2_chat_msgs = get_chat_msgs(alice2, a2_chat_id).await?;
|
||||
assert_eq!(a2_chat_msgs.len(), 1);
|
||||
let ChatItem::Message { msg_id } = a2_chat_msgs[0] else {
|
||||
unreachable!()
|
||||
};
|
||||
let a2_msg = Message::load_from_db(alice2, msg_id).await?;
|
||||
assert_eq!(a2_msg.get_info_type(), SystemMessage::ChatE2ee);
|
||||
|
||||
let a2_chat_members = get_chat_contacts(alice2, a2_chat_id).await?;
|
||||
assert!(a2_chat_members.contains(&a2_bob_contact));
|
||||
assert!(a2_chat_members.contains(&a2_charlie_contact));
|
||||
assert_eq!(a2_chat_members.len(), 2);
|
||||
|
||||
+44
-81
@@ -18,8 +18,8 @@ use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{get_abs_path, time};
|
||||
use crate::transport::{add_pseudo_transport, send_sync_transports, transport_addrs};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::transport::transport_addrs;
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
@@ -42,10 +42,12 @@ use crate::{constants, stats};
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Config {
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredAddr, [`crate::login_param::EnteredLoginParam`],
|
||||
/// or add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Email address, used in the `From:` field.
|
||||
/// Email address used by the deprecated configure() procedure.
|
||||
///
|
||||
/// Use add_transport{from_qr}() to configure new transports,
|
||||
/// Use list_transports() to learn about configured transports,
|
||||
/// including their addresses.
|
||||
Addr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
@@ -195,9 +197,9 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
/// The address of the transport used for sending.
|
||||
/// Deprecated(2026-09).
|
||||
///
|
||||
/// Device-local, other devices choose their own sending transport.
|
||||
/// Use ConfiguredLoginParam and list_transports() instead.
|
||||
ConfiguredAddr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
@@ -305,18 +307,6 @@ pub enum Config {
|
||||
/// True if account is configured.
|
||||
Configured,
|
||||
|
||||
/// Deprecated, we are trying to get rid of this global setting.
|
||||
/// It is possible to configure a profile with both chatmail relays
|
||||
/// and classical email servers.
|
||||
///
|
||||
/// Most usages in UIs can be replaced by `force_encryption`.
|
||||
///
|
||||
/// True if account is a chatmail account.
|
||||
IsChatmail,
|
||||
|
||||
/// True if `IsChatmail` mustn't be autoconfigured. For tests.
|
||||
FixIsChatmail,
|
||||
|
||||
/// True if account is muted.
|
||||
IsMuted,
|
||||
|
||||
@@ -346,6 +336,17 @@ pub enum Config {
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// ID of the device message added after backup transfer.
|
||||
///
|
||||
/// If the message does not exist on the first housekeeping
|
||||
/// after `READD_BACKUP_TRANSFER_MSG_DELAY`, it is re-added.
|
||||
BackupTransferMsgId,
|
||||
|
||||
/// Timestamp of the device message added after backup transfer.
|
||||
///
|
||||
/// Needed to check against `READD_BACKUP_TRANSFER_MSG_DELAY`.
|
||||
BackupTransferTimestamp,
|
||||
|
||||
/// Timestamp of the last time accumulated broadcast channel reactions were sent
|
||||
LastReactionsBroadcast,
|
||||
|
||||
@@ -499,11 +500,6 @@ impl Config {
|
||||
| Self::ForceEncryption,
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(self, Config::ConfiguredAddr)
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -682,10 +678,6 @@ impl Context {
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self).await?,
|
||||
_ => Default::default(),
|
||||
};
|
||||
if key == Config::StatsSending {
|
||||
let old_value = self.get_config(key).await?;
|
||||
let old_value = bool_from_config(old_value.as_deref());
|
||||
@@ -765,57 +757,28 @@ impl Context {
|
||||
bail!("Cannot unset configured_addr");
|
||||
};
|
||||
|
||||
if !self.is_configured().await? {
|
||||
info!(
|
||||
self,
|
||||
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
|
||||
);
|
||||
add_pseudo_transport(self, addr).await?;
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
|
||||
.await?;
|
||||
} else {
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(addr,),
|
||||
)?;
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('configured_addr', ?)",
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
// The timestamp must strictly increase because
|
||||
// other devices ignore the row update otherwise,
|
||||
// and contacts only adopt the re-signed key
|
||||
// if its signature timestamp increases.
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE transports
|
||||
SET add_timestamp=MAX(?, add_timestamp+1)
|
||||
WHERE addr=?",
|
||||
(time(), addr),
|
||||
)
|
||||
.context(
|
||||
"Failed to update add_timestamp for the new sending transport",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
// Invalidate the cache so the sync message
|
||||
// cannot read a stale sending address.
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
send_sync_transports(self).await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
@@ -857,7 +820,7 @@ impl Context {
|
||||
|
||||
/// Set the given config to a boolean value.
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, from_bool(value)).await?;
|
||||
self.set_config(key, Some(from_bool(value))).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -878,8 +841,8 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
|
||||
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
|
||||
Some(if val { "1" } else { "0" })
|
||||
pub(crate) fn from_bool(val: bool) -> &'static str {
|
||||
if val { "1" } else { "0" }
|
||||
}
|
||||
|
||||
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
|
||||
|
||||
+120
-48
@@ -31,7 +31,7 @@ use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::provider::{self, Protocol, Socket};
|
||||
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::qr::{Qr, check_qr, login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::Nosync;
|
||||
use crate::tools::time;
|
||||
@@ -40,27 +40,30 @@ use crate::transport::{
|
||||
ConnectionCandidate, delete_transport_row, maybe_update_sending_transport,
|
||||
purge_transport_caches, send_sync_transports, transport_addrs,
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{EventType, autorelay, stock_str};
|
||||
|
||||
/// Maximum number of relays.
|
||||
///
|
||||
/// See <https://github.com/chatmail/core/issues/7608>.
|
||||
pub(crate) const MAX_RELAYS: usize = 5;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
$progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::ConfigureProgress {
|
||||
progress: $progress,
|
||||
comment: $comment,
|
||||
});
|
||||
};
|
||||
($context:tt, $progress:expr) => {
|
||||
progress!($context, $progress, None);
|
||||
};
|
||||
tokio::task_local! {
|
||||
pub(crate) static SILENT_PROGRESS: ();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn emit_progress(ctx: &Context, progress: u16) {
|
||||
assert!(
|
||||
progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
if SILENT_PROGRESS.try_with(|_| ()).is_ok() {
|
||||
return;
|
||||
}
|
||||
ctx.emit_event(EventType::ConfigureProgress {
|
||||
progress,
|
||||
comment: None,
|
||||
});
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -124,14 +127,17 @@ impl Context {
|
||||
pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
match self.add_transport_unreported(param).await {
|
||||
Ok(()) => {
|
||||
progress!(self, 1000);
|
||||
emit_progress(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
// 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:#}"));
|
||||
progress!(self, 0, Some(error_msg.clone()));
|
||||
self.emit_event(EventType::ConfigureProgress {
|
||||
progress: 0,
|
||||
comment: Some(error_msg.clone()),
|
||||
});
|
||||
bail!(error_msg);
|
||||
}
|
||||
}
|
||||
@@ -156,9 +162,7 @@ impl Context {
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
res?;
|
||||
|
||||
param.save_legacy(self).await
|
||||
res
|
||||
}
|
||||
|
||||
/// Adds a new email account as a transport
|
||||
@@ -190,6 +194,62 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds an initial transport on the chatmail relay that answers fastest
|
||||
/// and lets the profile add further ones in the background.
|
||||
///
|
||||
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||
/// while securejoin codes add the inviter's relays to the candidates.
|
||||
///
|
||||
/// Does nothing if the profile already has a transport.
|
||||
pub async fn init_transports(&self, qr: Option<&str>) -> Result<()> {
|
||||
if self.is_configured().await? {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(qr) = qr {
|
||||
match check_qr(self, qr).await? {
|
||||
Qr::Account { .. } | Qr::Login { .. } => {
|
||||
return self.add_transport_from_qr(qr).await;
|
||||
}
|
||||
Qr::AskVerifyContact { addrs, .. }
|
||||
| Qr::AskVerifyGroup { addrs, .. }
|
||||
| Qr::AskJoinBroadcast { addrs, .. } => {
|
||||
autorelay::add_relay_candidates(self, &addrs).await?
|
||||
}
|
||||
_ => bail!("QR code does not contain a relay"),
|
||||
}
|
||||
}
|
||||
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
let skip_network = false;
|
||||
let res = autorelay::add_transport_from_candidates(self, skip_network)
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
|
||||
.await;
|
||||
self.free_ongoing().await;
|
||||
|
||||
let configured = self.is_configured().await?;
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) if configured => {
|
||||
warn!(
|
||||
self,
|
||||
"Onboarding interrupted after adding a transport: {err:#}."
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
|
||||
self.emit_event(EventType::ConfigureProgress {
|
||||
progress: 0,
|
||||
comment: Some(error_msg.clone()),
|
||||
});
|
||||
bail!(error_msg);
|
||||
}
|
||||
}
|
||||
self.set_config_bool(Config::Autorelay, true).await?;
|
||||
emit_progress(self, 1000);
|
||||
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_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
@@ -321,7 +381,7 @@ async fn get_configured_param(
|
||||
let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
|
||||
progress!(ctx, 200);
|
||||
emit_progress(ctx, 200);
|
||||
|
||||
let param_autoconfig = if param.imap.server.is_empty()
|
||||
&& param.imap.port == 0
|
||||
@@ -343,7 +403,7 @@ async fn get_configured_param(
|
||||
None
|
||||
};
|
||||
|
||||
progress!(ctx, 500);
|
||||
emit_progress(ctx, 500);
|
||||
|
||||
let mut servers = param_autoconfig.unwrap_or_default();
|
||||
if !servers
|
||||
@@ -437,13 +497,13 @@ pub(crate) async fn configure(
|
||||
param: &EnteredLoginParam,
|
||||
skip_network: bool,
|
||||
) -> Result<()> {
|
||||
progress!(ctx, 1);
|
||||
emit_progress(ctx, 1);
|
||||
|
||||
let configured_param = get_configured_param(ctx, param, skip_network).await?;
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some())?;
|
||||
|
||||
progress!(ctx, 550);
|
||||
emit_progress(ctx, 550);
|
||||
|
||||
if !skip_network {
|
||||
// Spawn SMTP configuration task
|
||||
@@ -469,7 +529,7 @@ pub(crate) async fn configure(
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
||||
progress!(ctx, 600);
|
||||
emit_progress(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
|
||||
@@ -483,23 +543,12 @@ pub(crate) async fn configure(
|
||||
}
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
emit_progress(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
smtp_config_task.await??;
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let is_configured = ctx.is_configured().await?;
|
||||
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
if imap_session.is_chatmail() {
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
|
||||
} else if !is_configured {
|
||||
// Reset the setting that may have been set
|
||||
// during failed configuration.
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
|
||||
}
|
||||
}
|
||||
emit_progress(ctx, 900);
|
||||
|
||||
// Drop the imap connection explicitly
|
||||
// to make sure that it's not forgotten in a future refactoring
|
||||
@@ -507,7 +556,7 @@ pub(crate) async fn configure(
|
||||
drop(imap);
|
||||
}
|
||||
|
||||
progress!(ctx, 910);
|
||||
emit_progress(ctx, 910);
|
||||
|
||||
configured_param
|
||||
.clone()
|
||||
@@ -518,11 +567,11 @@ pub(crate) async fn configure(
|
||||
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
progress!(ctx, 920);
|
||||
emit_progress(ctx, 920);
|
||||
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
emit_progress(ctx, 940);
|
||||
ctx.update_device_chats()
|
||||
.await
|
||||
.context("Failed to update device chats")?;
|
||||
@@ -566,7 +615,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 300);
|
||||
emit_progress(ctx, 300);
|
||||
|
||||
// `?emailaddress=` query string is excluded on purpose.
|
||||
// It is not part of the URL according to <https://datatracker.ietf.org/doc/draft-ietf-mailmaint-autoconfig/06/>.
|
||||
@@ -581,7 +630,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 310);
|
||||
emit_progress(ctx, 310);
|
||||
|
||||
// Outlook uses always SSL but different domains (this comment describes the next two steps)
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
@@ -593,7 +642,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 320);
|
||||
emit_progress(ctx, 320);
|
||||
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
@@ -604,7 +653,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 330);
|
||||
emit_progress(ctx, 330);
|
||||
|
||||
// always SSL for Thunderbird's database
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
@@ -681,7 +730,8 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_early_configure_failure_is_reported() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let mut param = login_param_from_host("example.org");
|
||||
let mark_as_autorelay = false;
|
||||
let mut param = login_param_from_host("example.org", mark_as_autorelay);
|
||||
|
||||
// An ongoing process, e.g. a backup import,
|
||||
// makes configuration fail without ever contacting a relay.
|
||||
@@ -700,6 +750,28 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that init_transports() fails on a bad code
|
||||
/// and does nothing on a profile that already has a transport.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_init_transports() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
assert!(t.init_transports(Some("not a qr code")).await.is_err());
|
||||
assert!(!t.is_configured().await?);
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let invite = "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40invite.example&i=TbnwJ6lSvD5&s=0ejvbdFSQxB";
|
||||
alice.init_transports(Some(invite)).await?;
|
||||
let candidates = alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM relay_candidates", ())
|
||||
.await?;
|
||||
assert_eq!(candidates, 0);
|
||||
assert_eq!(alice.count_transports().await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_configured_param() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
@@ -726,7 +798,7 @@ mod tests {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
|
||||
// Setting ConfiguredAddr on an unconfigured account creates a pseudo transport
|
||||
add_pseudo_transport(t, "primary@example.org").await?;
|
||||
t.set_config(Config::ConfiguredAddr, Some("primary@example.org"))
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, 1);
|
||||
|
||||
@@ -29,7 +29,7 @@ impl ServerParams {
|
||||
if self.username.is_empty() {
|
||||
vec![Self {
|
||||
username: addr.to_string(),
|
||||
..self.clone()
|
||||
..self
|
||||
}]
|
||||
} else {
|
||||
vec![self]
|
||||
|
||||
+45
-28
@@ -40,9 +40,33 @@ use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
|
||||
use crate::{chat, chatlist_events, ensure_and_debug_assert, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
/// If the contact's "last seen" is newer, the contact freshness is set to "recently seen".
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// If the contact's "last seen" is older, the contact freshness is set to "old".
|
||||
const CONTACT_OLD_SECONDS: i64 = 60 * 24 * 60 * 60;
|
||||
|
||||
/// Freshness of a contact, based on when it was last seen.
|
||||
///
|
||||
/// Used by the UI to highlight contacts:
|
||||
/// recently seen contacts get a little green dot on the avatar,
|
||||
/// contacts not seen for a long time get a string below the name (e.g. "Seen 2 months ago").
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Freshness {
|
||||
/// Contact shall not be highlighted.
|
||||
Normal = 0,
|
||||
/// Contact was seen recently.
|
||||
RecentlySeen = 1,
|
||||
/// Contact was not seen for a long time.
|
||||
Old = 2,
|
||||
}
|
||||
|
||||
impl From<Freshness> for u32 {
|
||||
fn from(freshness: Freshness) -> Self {
|
||||
freshness as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
@@ -486,8 +510,8 @@ pub struct Contact {
|
||||
/// The contact ID.
|
||||
pub id: ContactId,
|
||||
|
||||
/// Contact name. It is recommended to use `Contact::get_name`,
|
||||
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
|
||||
/// Contact name. It is recommended to use `Contact::get_name`
|
||||
/// or `Contact::get_display_name` to access this field.
|
||||
/// May be empty, initially set to `authname`.
|
||||
name: String,
|
||||
|
||||
@@ -725,10 +749,23 @@ impl Contact {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Returns `true` if this contact was seen recently.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn was_seen_recently(&self) -> bool {
|
||||
time() - self.last_seen <= SEEN_RECENTLY_SECONDS
|
||||
/// Returns freshness of the contact.
|
||||
pub fn get_freshness(&self) -> Freshness {
|
||||
if self.id.is_special() || !self.is_key_contact() || self.is_blocked() {
|
||||
return Freshness::Normal;
|
||||
}
|
||||
|
||||
let is_old = time().saturating_sub(self.last_seen) > CONTACT_OLD_SECONDS;
|
||||
if is_old || self.last_seen <= 0 {
|
||||
return Freshness::Old;
|
||||
}
|
||||
|
||||
let seen_recently = time().saturating_sub(self.last_seen) <= SEEN_RECENTLY_SECONDS;
|
||||
if seen_recently {
|
||||
return Freshness::RecentlySeen;
|
||||
}
|
||||
|
||||
Freshness::Normal
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
@@ -1576,7 +1613,7 @@ WHERE addr=?
|
||||
/// May be an empty string.
|
||||
///
|
||||
/// This name is typically used in a form where the user can edit the name of a contact.
|
||||
/// To get a fine name to display in lists etc., use `Contact::get_display_name` or `Contact::get_name_n_addr`.
|
||||
/// To get a fine name to display in lists etc., use `Contact::get_display_name`.
|
||||
pub fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
@@ -1596,26 +1633,6 @@ WHERE addr=?
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Get a summary of name and address.
|
||||
///
|
||||
/// The returned string is either "Name (email@domain.com)" or just
|
||||
/// "email@domain.com" if the name is unset.
|
||||
///
|
||||
/// The result should only be used locally and never sent over the network
|
||||
/// as it leaks the local contact name.
|
||||
///
|
||||
/// The summary is typically used when asking the user something about the contact.
|
||||
/// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
|
||||
pub fn get_name_n_addr(&self) -> String {
|
||||
if !self.name.is_empty() {
|
||||
format!("{} ({})", self.name, self.addr)
|
||||
} else if !self.authname.is_empty() {
|
||||
format!("{} ({})", self.authname, self.addr)
|
||||
} else {
|
||||
(&self.addr).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the contact's profile image.
|
||||
/// This is the image set by each remote user on their own
|
||||
/// using set_config(context, "selfavatar", image).
|
||||
|
||||
@@ -253,7 +253,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact.get_authname(), "bla foo");
|
||||
assert_eq!(contact.get_display_name(), "Name one");
|
||||
assert_eq!(contact.get_addr(), "one@eins.org");
|
||||
assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)");
|
||||
|
||||
// modify first added contact
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
@@ -286,7 +285,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "three@drei.sam");
|
||||
assert_eq!(contact.get_addr(), "three@drei.sam");
|
||||
assert_eq!(contact.get_name_n_addr(), "three@drei.sam");
|
||||
|
||||
// add name to third contact from incoming message (this becomes authorized name)
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
@@ -300,7 +298,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact_id, contact_id_test);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
// manually edit name of third contact (does not changed authorized name)
|
||||
@@ -316,7 +313,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "m. serious");
|
||||
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
// Fourth contact:
|
||||
@@ -334,7 +330,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact.get_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_addr(), "alice@w.de");
|
||||
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
|
||||
|
||||
// check SELF
|
||||
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
|
||||
@@ -373,7 +368,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "f@example.org");
|
||||
assert_eq!(contact.get_name_n_addr(), "f@example.org");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
@@ -399,7 +393,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
@@ -429,7 +422,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
@@ -447,7 +439,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "Falk");
|
||||
assert_eq!(contact.get_display_name(), "Falk");
|
||||
assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
|
||||
@@ -1057,7 +1048,7 @@ async fn test_last_seen() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently() -> Result<()> {
|
||||
async fn test_contact_freshness() -> Result<()> {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1070,15 +1061,57 @@ async fn test_was_seen_recently() -> Result<()> {
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Old);
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
|
||||
assert!(contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
|
||||
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
|
||||
assert!(!self_contact.was_seen_recently());
|
||||
assert_eq!(self_contact.get_freshness(), Freshness::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_freshness_blocked() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Alice sends message to Bob. Bob receives message, contact's freshness is "recently seen"
|
||||
let msg = tcm.send_recv(&alice, &bob, "moin").await;
|
||||
|
||||
let contact = Contact::get_by_id(&bob, msg.from_id).await?;
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
|
||||
// Bob blocks Alice, contact's freshness is "normal" now
|
||||
Contact::block(&bob, msg.from_id).await?;
|
||||
let contact = Contact::get_by_id(&bob, msg.from_id).await?;
|
||||
assert!(contact.is_blocked());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_freshness_address_contact() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let bob = tcm.bob().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
|
||||
// Alice sends a message to Bob
|
||||
let alice_chat = alice.create_email_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(alice_chat.id, "moin").await;
|
||||
|
||||
// Bob receives message, contact's freshness is "normal", even though the messages was just received
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, msg.from_id).await?;
|
||||
assert!(!contact.is_key_contact());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1096,11 +1129,11 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_ne!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
@@ -1108,12 +1141,12 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
.interrupt(contact.id, contact.last_seen)
|
||||
.await;
|
||||
|
||||
// Wait for `was_seen_recently()` to turn off.
|
||||
// Wait for "seen recently" to turn off.
|
||||
bob.evtracker.clear_events();
|
||||
SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2));
|
||||
recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Normal);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
|
||||
+14
-29
@@ -568,20 +568,9 @@ impl Context {
|
||||
self.scheduler.maybe_network().await;
|
||||
}
|
||||
|
||||
/// Deprecated, we are trying to get rid of this global setting.
|
||||
/// It is possible to configure a profile with both chatmail relays
|
||||
/// and classical email servers.
|
||||
///
|
||||
/// Returns true if an account is on a chatmail server.
|
||||
pub async fn is_chatmail(&self) -> Result<bool> {
|
||||
self.get_config_bool(Config::IsChatmail).await
|
||||
}
|
||||
|
||||
/// Returns maximum number of recipients a single email can be sent to.
|
||||
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<u32> {
|
||||
let Some((transport_id, param)) = ConfiguredLoginParam::load(self).await? else {
|
||||
bail!("Not configured");
|
||||
};
|
||||
/// Returns maximum number of recipients a single email can be sent to
|
||||
/// over the transport `transport_id`, which sends from `addr`.
|
||||
pub(crate) async fn get_max_smtp_rcpt_to(&self, transport_id: u32, addr: &str) -> Result<u32> {
|
||||
let metadata_limit = self
|
||||
.metadata
|
||||
.read()
|
||||
@@ -591,9 +580,7 @@ impl Context {
|
||||
if let Some(limit) = metadata_limit {
|
||||
return Ok(limit);
|
||||
}
|
||||
if let Some(limit) =
|
||||
crate::provider::legacy_settings_for_addr(¶m.addr)?.max_smtp_rcpt_to
|
||||
{
|
||||
if let Some(limit) = crate::provider::legacy_settings_for_addr(addr)?.max_smtp_rcpt_to {
|
||||
return Ok(limit);
|
||||
}
|
||||
Ok(constants::DEFAULT_MAX_SMTP_RCPT_TO)
|
||||
@@ -601,9 +588,13 @@ impl Context {
|
||||
|
||||
/// Does a single round of fetching messages from all transports and returns.
|
||||
///
|
||||
/// Can be used even if I/O is currently stopped.
|
||||
/// If I/O is stopped, fetches over a dedicated connection per transport
|
||||
/// and returns as soon as one of them fetched messages.
|
||||
/// If IO is stopped, pauses the scheduler and fetches over a dedicated connection
|
||||
/// per transport, returning as soon as one of them fetched messages.
|
||||
/// If IO is running, interrupts IMAP IDLE on all transports
|
||||
/// and waits until they are done fetching.
|
||||
///
|
||||
/// Does not wait for outgoing messages to be sent out,
|
||||
/// use [`crate::accounts::Accounts::is_sending_finished`] for that.
|
||||
pub async fn background_fetch(&self) -> Result<()> {
|
||||
if !(self.is_configured().await?) {
|
||||
return Ok(());
|
||||
@@ -613,8 +604,9 @@ impl Context {
|
||||
info!(self, "background_fetch started.");
|
||||
|
||||
if self.scheduler.is_running().await {
|
||||
self.scheduler.maybe_network().await;
|
||||
self.wait_for_all_work_done().await;
|
||||
self.scheduler.interrupt_inbox_idle().await;
|
||||
let include_smtp = false;
|
||||
self.wait_for_work_done(include_smtp).await;
|
||||
} else {
|
||||
self.scheduler.background_fetch_any(self).await?;
|
||||
}
|
||||
@@ -880,13 +872,6 @@ impl Context {
|
||||
res.insert("imap_server_id", format!("{server_id:?}"));
|
||||
}
|
||||
|
||||
res.insert("is_chatmail", self.is_chatmail().await?.to_string());
|
||||
res.insert(
|
||||
"fix_is_chatmail",
|
||||
self.get_config_bool(Config::FixIsChatmail)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"is_muted",
|
||||
self.get_config_bool(Config::IsMuted).await?.to_string(),
|
||||
|
||||
@@ -5,12 +5,35 @@ use tempfile::tempdir;
|
||||
use super::*;
|
||||
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{Chattype, DEFAULT_MAX_SMTP_RCPT_TO};
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_max_smtp_rcpt_to() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
t.get_max_smtp_rcpt_to(2, "alice@example.org").await?,
|
||||
DEFAULT_MAX_SMTP_RCPT_TO
|
||||
);
|
||||
|
||||
for (transport_id, limit) in [(1, 3), (2, 7)] {
|
||||
t.metadata.write().await.insert(
|
||||
transport_id,
|
||||
ServerMetadata {
|
||||
max_smtp_rcpt_to: Some(limit),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(t.get_max_smtp_rcpt_to(2, "alice@example.org").await?, 7);
|
||||
assert_eq!(t.get_max_smtp_rcpt_to(1, "alice@example.org").await?, 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
@@ -271,6 +294,8 @@ async fn test_get_info_completeness() {
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"backup_transfer_msg_id",
|
||||
"backup_transfer_timestamp",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
|
||||
+1
-3
@@ -6,7 +6,6 @@ use anyhow::{Result, anyhow, bail, ensure};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session;
|
||||
use crate::log::warn;
|
||||
@@ -164,8 +163,7 @@ pub(crate) async fn download_msg(
|
||||
.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)
|
||||
.await?;
|
||||
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
if ephemeral::should_delete_all_downloaded_messages(bcc_self, session.is_chatmail()) {
|
||||
if ephemeral::should_delete_all_downloaded_messages(context).await? {
|
||||
// Now that the message was downloaded, it likely needs to be deleted;
|
||||
// trigger a re-check by interrupting the inbox folder.
|
||||
// This is mainly needed to make the tests pass;
|
||||
|
||||
@@ -6,7 +6,6 @@ mod tests {
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::config::Config;
|
||||
use crate::message::Message;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -61,7 +60,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::IsChatmail, true).await?;
|
||||
bob.allow_unencrypted().await?;
|
||||
let bob_chat_id = receive_imf(
|
||||
bob,
|
||||
|
||||
+28
-14
@@ -363,7 +363,7 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
|
||||
/// Selects messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// For each message a row ID, chat id, viewtype and location ID is returned.
|
||||
/// For each message a row ID, chat id, viewtype, whether the message is pinned and location ID is returned.
|
||||
///
|
||||
/// Unknown viewtypes are returned as `Viewtype::Unknown`
|
||||
/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
|
||||
@@ -371,12 +371,12 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
|
||||
async fn select_expired_messages(
|
||||
context: &Context,
|
||||
now: i64,
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype, u32)>> {
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype, bool, u32)>> {
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
SELECT id, chat_id, type, pinned, location_id
|
||||
FROM msgs
|
||||
WHERE
|
||||
ephemeral_timestamp != 0
|
||||
@@ -392,8 +392,9 @@ WHERE
|
||||
.context("Using default viewtype for ephemeral handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let pinned: bool = row.get("pinned")?;
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
Ok((id, chat_id, viewtype, pinned, location_id))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -414,7 +415,7 @@ WHERE
|
||||
.sql
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
SELECT id, chat_id, type, pinned, location_id
|
||||
FROM msgs
|
||||
WHERE
|
||||
timestamp < ?1
|
||||
@@ -437,8 +438,9 @@ WHERE
|
||||
.context("Using default viewtype for delete-old handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let pinned: bool = row.get("pinned")?;
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
Ok((id, chat_id, viewtype, pinned, location_id))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -463,11 +465,15 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
|
||||
if !rows.is_empty() {
|
||||
info!(context, "Attempting to delete {} messages.", rows.len());
|
||||
|
||||
let (msgs_changed, webxdc_deleted) = context
|
||||
let (msgs_changed, webxdc_deleted, pinned_chat_ids) = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut msgs_changed = Vec::with_capacity(rows.len());
|
||||
let mut webxdc_deleted = Vec::new();
|
||||
|
||||
// IDs of the chats in which pinned messages were deleted.
|
||||
let mut pinned_chat_ids = BTreeSet::new();
|
||||
|
||||
// If you change which information is preserved here, also change `MsgId::trash()`
|
||||
// and other places it references.
|
||||
let mut del_msg_stmt = transaction.prepare(
|
||||
@@ -478,7 +484,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
|
||||
)?;
|
||||
let mut del_location_stmt =
|
||||
transaction.prepare("DELETE FROM locations WHERE independent=1 AND id=?")?;
|
||||
for (msg_id, chat_id, viewtype, location_id) in rows {
|
||||
for (msg_id, chat_id, viewtype, is_pinned, location_id) in rows {
|
||||
del_msg_stmt.execute((msg_id, ChatId::TRASH))?;
|
||||
if location_id > 0 {
|
||||
del_location_stmt.execute((location_id,))?;
|
||||
@@ -488,8 +494,12 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
webxdc_deleted.push(msg_id)
|
||||
}
|
||||
|
||||
if is_pinned {
|
||||
pinned_chat_ids.insert(chat_id);
|
||||
}
|
||||
}
|
||||
Ok((msgs_changed, webxdc_deleted))
|
||||
Ok((msgs_changed, webxdc_deleted, pinned_chat_ids))
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -504,6 +514,10 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
}
|
||||
|
||||
for chat_id in pinned_chat_ids {
|
||||
context.emit_event(EventType::PinnedMessagesChanged { chat_id });
|
||||
}
|
||||
|
||||
for msg_id in webxdc_deleted {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
|
||||
}
|
||||
@@ -654,12 +668,11 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
pub(crate) async fn delete_expired_imap_messages(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
is_chatmail: bool,
|
||||
) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
if should_delete_all_downloaded_messages(bcc_self, is_chatmail) {
|
||||
if should_delete_all_downloaded_messages(context).await? {
|
||||
// This is the only device using this relay.
|
||||
// Mark all downloaded messages for deletion, because they are not needed anymore.
|
||||
//
|
||||
@@ -708,7 +721,7 @@ pub(crate) async fn delete_expired_imap_messages(
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// Single device.
|
||||
// Single device, but unencrypted messages are allowed.
|
||||
// Delete all expired and encrypted messages.
|
||||
context
|
||||
.sql
|
||||
@@ -736,8 +749,9 @@ pub(crate) async fn delete_expired_imap_messages(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn should_delete_all_downloaded_messages(bcc_self: bool, is_chatmail: bool) -> bool {
|
||||
!bcc_self && is_chatmail
|
||||
pub(crate) async fn should_delete_all_downloaded_messages(context: &Context) -> Result<bool> {
|
||||
Ok(!context.get_config_bool(Config::BccSelf).await?
|
||||
&& context.get_config_bool(Config::ForceEncryption).await?)
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
|
||||
@@ -509,7 +509,7 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
.await?;
|
||||
}
|
||||
|
||||
for (is_chatmail, other_transport, bcc_self) in [
|
||||
for (force_encryption, other_transport, bcc_self) in [
|
||||
(false, false, false),
|
||||
(false, false, true),
|
||||
(false, true, false),
|
||||
@@ -520,10 +520,12 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
(true, true, true),
|
||||
] {
|
||||
println!(
|
||||
"Testing combination is_chatmail={is_chatmail}, other_transport={other_transport}, bcc_self={bcc_self}"
|
||||
"Testing combination force_encryption={force_encryption}, other_transport={other_transport}, bcc_self={bcc_self}"
|
||||
);
|
||||
|
||||
t.set_config_bool(Config::BccSelf, bcc_self).await?;
|
||||
t.set_config_bool(Config::ForceEncryption, force_encryption)
|
||||
.await?;
|
||||
|
||||
delete_expired_imap_messages(
|
||||
&t,
|
||||
@@ -532,7 +534,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
} else {
|
||||
transport_id
|
||||
},
|
||||
is_chatmail,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -548,7 +549,7 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
assert_eq!(is_deleted(&t, "no_expire@localhost").await?, !bcc_self);
|
||||
assert_eq!(
|
||||
is_deleted(&t, "no_expire_unencrypted@localhost").await?,
|
||||
is_chatmail && !bcc_self
|
||||
force_encryption && !bcc_self
|
||||
);
|
||||
assert_eq!(is_deleted(&t, "future@localhost").await?, !bcc_self);
|
||||
assert_eq!(is_deleted(&t, "expired_post@localhost").await?, true);
|
||||
@@ -563,9 +564,10 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
reset_targets(&t).await;
|
||||
}
|
||||
|
||||
// With BccSelf=true, non-expired messages are kept even if `is_chatmail` is true
|
||||
// With BccSelf=true, non-expired messages are kept even if `force_encryption` is true
|
||||
t.set_config_bool(Config::BccSelf, true).await?;
|
||||
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||
t.set_config_bool(Config::ForceEncryption, true).await?;
|
||||
delete_expired_imap_messages(&t, transport_id).await?;
|
||||
assert_eq!(is_deleted(&t, "expired@localhost").await?, true);
|
||||
assert_eq!(is_deleted(&t, "no_expire@localhost").await?, false);
|
||||
assert_eq!(is_deleted(&t, "done_pre@localhost").await?, false);
|
||||
@@ -682,6 +684,10 @@ async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
check_msg_will_be_deleted(alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
alice
|
||||
.assert_warn("No SMTP connection candidates provided")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+11
-3
@@ -33,9 +33,6 @@ pub enum EventType {
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted(String),
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved(String),
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
@@ -96,6 +93,14 @@ pub enum EventType {
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// The list of pinned messages for the chat has changed.
|
||||
///
|
||||
/// Some message got pinned, or pinned message is unpinned or deleted.
|
||||
PinnedMessagesChanged {
|
||||
/// ID of the chat where the list of pinned messages changed.
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// A reaction to one's own sent message received.
|
||||
/// Typically, the UI will show a notification for that.
|
||||
///
|
||||
@@ -363,6 +368,9 @@ pub enum EventType {
|
||||
/// A call made while another background fetch is running gets the event immediately,
|
||||
/// and the running fetch keeps emitting events until its own marker.
|
||||
///
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it is unambiguous only if there are no concurrent background fetch calls.
|
||||
///
|
||||
/// This event is only emitted by the account manager.
|
||||
AccountsBackgroundFetchDone,
|
||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
|
||||
+37
-127
@@ -301,7 +301,8 @@ impl Imap {
|
||||
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
|
||||
|
||||
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
|
||||
let mut first_error = None;
|
||||
let mut first_connection_error = None;
|
||||
let mut first_login_error = None;
|
||||
'candidate: for lp in login_params {
|
||||
info!(context, "IMAP trying to connect to {}.", lp.connection);
|
||||
let connection_candidate = lp.connection.clone();
|
||||
@@ -317,7 +318,7 @@ impl Imap {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
warn!(context, "{err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
first_connection_error.get_or_insert(err);
|
||||
continue 'candidate;
|
||||
}
|
||||
};
|
||||
@@ -396,12 +397,14 @@ impl Imap {
|
||||
|
||||
Err(err) => {
|
||||
warn!(context, "{err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
first_login_error.get_or_insert(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
|
||||
Err(first_login_error
|
||||
.or(first_connection_error)
|
||||
.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
|
||||
}
|
||||
|
||||
/// Prepare a new IMAP session.
|
||||
@@ -420,13 +423,13 @@ impl Imap {
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// FETCH-MOVE-DELETE iteration.
|
||||
/// FETCH-and-DELETE iteration.
|
||||
///
|
||||
/// Prefetches headers and downloads new message from the folder, moves messages away from the
|
||||
/// folder and deletes messages in the folder.
|
||||
/// Prefetches headers and downloads new message from the folder
|
||||
/// and deletes messages in the folder.
|
||||
///
|
||||
/// Returns true if at least one message was fetched.
|
||||
pub async fn fetch_move_delete(
|
||||
pub async fn fetch_delete(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
@@ -452,14 +455,14 @@ impl Imap {
|
||||
|
||||
// Mark expired messages for deletion. Note that `delete_expired_imap_messages` is
|
||||
// not well optimized and should not be called before fetching.
|
||||
delete_expired_imap_messages(context, session.transport_id(), session.is_chatmail())
|
||||
delete_expired_imap_messages(context, session.transport_id())
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
session
|
||||
.move_delete_messages(context, watch_folder)
|
||||
.delete_messages(context, watch_folder)
|
||||
.await
|
||||
.context("move_delete_messages")?;
|
||||
.context("delete_messages")?;
|
||||
|
||||
Ok(msgs_fetched)
|
||||
}
|
||||
@@ -570,16 +573,6 @@ impl Imap {
|
||||
.size
|
||||
.context("imap fetch response does not contain size")?;
|
||||
|
||||
// Determine the target folder where the message should be moved to.
|
||||
//
|
||||
// We only move the messages from the INBOX and Spam folders.
|
||||
// This is required to avoid infinite MOVE loop on IMAP servers
|
||||
// that alias `DeltaChat` folder to other names.
|
||||
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
|
||||
// In this case moving from `INBOX.DeltaChat` to `DeltaChat`
|
||||
// results in the messages getting a new UID,
|
||||
// so the messages will be detected as new
|
||||
// in the `INBOX.DeltaChat` folder again.
|
||||
let delete = if let Some(message_id) = &message_id {
|
||||
message::rfc724_mid_exists_ext(context, message_id, "deleted=1")
|
||||
.await?
|
||||
@@ -617,13 +610,7 @@ impl Imap {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Download only the messages which have reached their target folder if there are
|
||||
// multiple devices. This prevents race conditions in multidevice case, where one
|
||||
// device tries to download the message while another device moves the message at the
|
||||
// same time. Even in single device case it is possible to fail downloading the first
|
||||
// message, move it to the movebox and then download the second message before
|
||||
// downloading the first one, if downloading from inbox before moving is allowed.
|
||||
if folder == target
|
||||
if !delete
|
||||
&& prefetch_should_download(context, &headers, &message_id, fetch_response.flags())
|
||||
.await
|
||||
.context("prefetch_should_download")?
|
||||
@@ -860,75 +847,10 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves batch of messages identified by their UID from the currently
|
||||
/// selected folder to the target folder.
|
||||
async fn move_message_batch(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
set: &str,
|
||||
row_ids: Vec<i64>,
|
||||
target: &str,
|
||||
) -> Result<()> {
|
||||
if self.can_move() {
|
||||
match self.uid_mv(set, &target).await {
|
||||
Ok(()) => {
|
||||
// Messages are moved or don't exist, IMAP returns OK response in both cases.
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("Cannot delete moved messages from imap table")?;
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} moved to {target}"
|
||||
)));
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
|
||||
set,
|
||||
target,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Server does not support MOVE or MOVE failed.
|
||||
// Copy messages to the destination folder if needed and mark records for deletion.
|
||||
info!(
|
||||
context,
|
||||
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
|
||||
);
|
||||
self.uid_copy(&set, &target).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("Cannot plan deletion of messages")?;
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} copied to {target}"
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves and deletes messages as planned in the `imap` table.
|
||||
/// Deletes messages as planned in the `imap` table.
|
||||
///
|
||||
/// This is the only place where messages are moved or deleted on the IMAP server.
|
||||
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
/// This is the only place where messages are deleted on the IMAP server.
|
||||
async fn delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
let transport_id = self.transport_id();
|
||||
let rows = context
|
||||
.sql
|
||||
@@ -950,23 +872,21 @@ impl Session {
|
||||
|
||||
for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
// Select folder inside the loop to avoid selecting it if there are no pending
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
let folder_exists = self.select_with_uidvalidity(context, folder).await?;
|
||||
ensure!(folder_exists, "No folder {folder}");
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
// Since we don't move messages between IMAP folders anymore,
|
||||
// `target` is always either empty or equal to `folder`.
|
||||
debug_assert!(target.is_empty() || folder == target);
|
||||
|
||||
if target.is_empty() {
|
||||
self.delete_message_batch(context, &uid_set, rowid_set)
|
||||
.await
|
||||
.with_context(|| format!("cannot delete batch of messages {uid_set:?}"))?;
|
||||
} else {
|
||||
self.move_message_batch(context, &uid_set, rowid_set, &target)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("cannot move batch of messages {uid_set:?} to folder {target:?}",)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1534,10 +1454,9 @@ impl Session {
|
||||
/// or flags have been changed.
|
||||
/// In this case we may want to skip next IDLE and do a round
|
||||
/// of fetching new messages and synchronizing seen flags.
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> bool {
|
||||
use UnsolicitedResponse::*;
|
||||
use async_imap::imap_proto::Response;
|
||||
use async_imap::imap_proto::ResponseCode;
|
||||
|
||||
let folder = self.selected_folder.as_deref().unwrap_or_default();
|
||||
let mut should_refetch = false;
|
||||
@@ -1552,34 +1471,25 @@ impl Session {
|
||||
}
|
||||
|
||||
Expunge(_) | Recent(_) => {}
|
||||
Other(ref response_data) => {
|
||||
match response_data.parsed() {
|
||||
Response::Fetch { .. } => {
|
||||
info!(
|
||||
context,
|
||||
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
|
||||
);
|
||||
should_refetch = true;
|
||||
}
|
||||
|
||||
// We are not interested in the following responses and they are are
|
||||
// sent quite frequently, so, we ignore them without logging them.
|
||||
Response::Done {
|
||||
code: Some(ResponseCode::CopyUid(_, _, _)),
|
||||
..
|
||||
} => {}
|
||||
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
Other(ref response_data) => match response_data.parsed() {
|
||||
Response::Fetch { .. } => {
|
||||
info!(
|
||||
context,
|
||||
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
|
||||
);
|
||||
should_refetch = true;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(should_refetch)
|
||||
should_refetch
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,6 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc2177>
|
||||
pub can_idle: bool,
|
||||
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc6851>
|
||||
pub can_move: bool,
|
||||
|
||||
/// True if the server has QUOTA capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc2087>
|
||||
pub can_check_quota: bool,
|
||||
@@ -25,14 +21,6 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc4978>
|
||||
pub can_compress: bool,
|
||||
|
||||
/// True if the server has an XCHATMAIL capability
|
||||
/// indicating that it is a <https://github.com/deltachat/chatmail> server.
|
||||
///
|
||||
/// This can be used to hide some advanced settings in the UI
|
||||
/// that are only interesting for normal email accounts,
|
||||
/// e.g. the ability to move messages to Delta Chat folder.
|
||||
pub is_chatmail: bool,
|
||||
|
||||
/// Server ID if the server supports ID capability.
|
||||
pub server_id: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
@@ -78,11 +78,9 @@ pub(crate) async fn identify_server(
|
||||
};
|
||||
let capabilities = Capabilities {
|
||||
can_idle: caps.has_str("IDLE"),
|
||||
can_move: caps.has_str("MOVE"),
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_metadata: caps.has_str("METADATA"),
|
||||
can_compress: caps.has_str("COMPRESS=DEFLATE"),
|
||||
is_chatmail: caps.has_str("XCHATMAIL"),
|
||||
server_id,
|
||||
};
|
||||
Ok(capabilities)
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl Session {
|
||||
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
if self.drain_unsolicited_responses(context) {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,10 +99,6 @@ impl Session {
|
||||
self.capabilities.can_idle
|
||||
}
|
||||
|
||||
pub fn can_move(&self) -> bool {
|
||||
self.capabilities.can_move
|
||||
}
|
||||
|
||||
pub fn can_check_quota(&self) -> bool {
|
||||
self.capabilities.can_check_quota
|
||||
}
|
||||
@@ -111,11 +107,6 @@ impl Session {
|
||||
self.capabilities.can_metadata
|
||||
}
|
||||
|
||||
// Returns true if IMAP server has `XCHATMAIL` capability.
|
||||
pub(crate) fn is_chatmail(&self) -> bool {
|
||||
self.capabilities.is_chatmail
|
||||
}
|
||||
|
||||
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
|
||||
/// order of ascending UIDs.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
|
||||
+1
-2
@@ -30,6 +30,7 @@ use crate::tools::{
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub(crate) use transfer::maybe_readd_backup_transfer_msg;
|
||||
pub use transfer::{BackupProvider, get_backup};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
@@ -981,7 +982,6 @@ mod tests {
|
||||
context1.get_config(Config::BccSelf).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
context1.set_config_bool(Config::IsChatmail, true).await?;
|
||||
|
||||
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, false);
|
||||
context1.set_config_bool(Config::IsMuted, true).await?;
|
||||
@@ -1001,7 +1001,6 @@ mod tests {
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
assert!(context2.is_configured().await?);
|
||||
assert!(context2.is_chatmail().await?);
|
||||
for ctx in [context1, context2] {
|
||||
// BccSelf should be enabled automatically when exporting a backup
|
||||
assert_eq!(ctx.get_config_bool(Config::BccSelf).await?, true);
|
||||
|
||||
+97
-7
@@ -40,21 +40,68 @@ use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::EventType;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::key;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{TempPathGuard, create_id};
|
||||
use crate::tools::{TempPathGuard, create_id, time};
|
||||
|
||||
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
|
||||
|
||||
/// ALPN protocol identifier for the backup transfer protocol.
|
||||
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
|
||||
/// Minimum time after a backup transfer before we check
|
||||
/// whether the "second device added" device message still exists.
|
||||
///
|
||||
/// The check is done in the first housekeeping after this delay;
|
||||
/// if the message was deleted until then, it is re-added with the original timestamp.
|
||||
/// After the check, the user can delete the message as usual without it being re-added.
|
||||
///
|
||||
/// This makes it easier to spot unwanted "add second device" actions.
|
||||
/// It is clear that this does not catch all eventualities, it is best-effort.
|
||||
/// First line of defense is device locking and asking for secret explicitly before adding a second device.
|
||||
const READD_BACKUP_TRANSFER_MSG_DELAY: i64 = 60 * 60;
|
||||
|
||||
/// Checks `Config::BackupTransferMsgId`
|
||||
/// and re-adds the backup transfer device message if it does not exist and some time passed.
|
||||
pub(crate) async fn maybe_readd_backup_transfer_msg(context: &Context) -> Result<()> {
|
||||
let Some(msg_id) = context
|
||||
.get_config_parsed::<u32>(Config::BackupTransferMsgId)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let timestamp = context
|
||||
.get_config_i64(Config::BackupTransferTimestamp)
|
||||
.await?;
|
||||
if time() < timestamp.saturating_add(READD_BACKUP_TRANSFER_MSG_DELAY) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
if Message::load_from_db_optional(context, MsgId::new(msg_id))
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(context));
|
||||
add_device_msg_with_importance(context, None, Some(&mut msg), false, timestamp).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provide or send a backup of this device.
|
||||
///
|
||||
/// This creates a backup of the current device and starts a service which offers another
|
||||
@@ -212,8 +259,23 @@ impl BackupProvider {
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let timestamp = time();
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
let msg_id =
|
||||
add_device_msg_with_importance(&context, None, Some(&mut msg), false, timestamp)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferTimestamp,
|
||||
Some(×tamp.to_string()),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferMsgId,
|
||||
Some(&msg_id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -393,14 +455,17 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::message::Viewtype;
|
||||
use crate::chat::{ChatId, ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{Viewtype, delete_msgs};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_receive() {
|
||||
async fn test_send_receive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Create first device.
|
||||
@@ -472,6 +537,31 @@ mod tests {
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
|
||||
// When deleting the backup transfer message on the sending device,
|
||||
// it is re-added once on housekeeping,
|
||||
// but not within the first hour after the transfer.
|
||||
let device_chat_id = ChatId::get_for_contact(&ctx0, ContactId::DEVICE).await?;
|
||||
let original_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_eq!(original_msg.text, backup_transfer_msg_body(&ctx0));
|
||||
delete_msgs(&ctx0, &[original_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
assert!(get_chat_msgs(&ctx0, device_chat_id).await?.is_empty()); // re-adding not done in first hour
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60 * 60));
|
||||
housekeeping(&ctx0).await?;
|
||||
let readded_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_ne!(readded_msg.id, original_msg.id);
|
||||
assert_eq!(readded_msg.get_text(), original_msg.get_text());
|
||||
assert_eq!(readded_msg.get_timestamp(), original_msg.get_timestamp());
|
||||
assert!(readded_msg.get_sort_timestamp() > original_msg.get_sort_timestamp());
|
||||
|
||||
delete_msgs(&ctx0, &[readded_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
let device_msgs = get_chat_msgs(&ctx0, device_chat_id).await?;
|
||||
assert!(device_msgs.is_empty()); // re-adding is done only once
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that trying to accidentally overwrite a profile
|
||||
|
||||
+6
-1
@@ -98,7 +98,8 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let mut buf = Vec::new();
|
||||
self.to_writer(&mut buf).unwrap();
|
||||
self.to_writer(&mut buf)
|
||||
.expect("Writing to Vec<u8> cannot fail");
|
||||
buf
|
||||
}
|
||||
|
||||
@@ -662,6 +663,7 @@ mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{TestContext, TestContextManager, alice_keypair};
|
||||
use crate::tools::SystemTime;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
static KEYPAIR: LazyLock<SignedSecretKey> = LazyLock::new(alice_keypair);
|
||||
|
||||
@@ -811,6 +813,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -821,6 +824,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -833,6 +837,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
use std::thread;
|
||||
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
+3
-2
@@ -15,7 +15,8 @@
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied,
|
||||
clippy::manual_is_variant_and
|
||||
clippy::manual_is_variant_and,
|
||||
clippy::unnecessary_wraps
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
@@ -97,7 +98,7 @@ pub mod stock_str;
|
||||
pub mod storage_usage;
|
||||
mod sync;
|
||||
mod token;
|
||||
mod transport;
|
||||
pub mod transport;
|
||||
mod update_helper;
|
||||
pub mod webxdc;
|
||||
#[macro_use]
|
||||
|
||||
+23
-30
@@ -3,6 +3,7 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
|
||||
mod stream;
|
||||
|
||||
@@ -12,15 +13,9 @@ macro_rules! info {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
info!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
let full = format!("{file}:{line}: {msg}",
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
::tracing::event!(::tracing::Level::INFO, account_id = $ctx.get_id(), "{}", &formatted);
|
||||
$ctx.emit_event($crate::EventType::Info(full));
|
||||
}};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
|
||||
$ctx.log_info(file!(), line!(), format!($msg, $($args),*))
|
||||
};
|
||||
}
|
||||
|
||||
// Workaround for <https://github.com/rust-lang/rust/issues/133708>.
|
||||
@@ -30,15 +25,9 @@ mod warn_macro_mod {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
warn_macro!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
let full = format!("{file}:{line}: {msg}",
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
::tracing::event!(::tracing::Level::WARN, account_id = $ctx.get_id(), "{}", &formatted);
|
||||
$ctx.emit_event($crate::EventType::Warning(full));
|
||||
}};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
|
||||
$ctx.log_warn(file!(), line!(), format!($msg, $($args),*))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use warn_macro;
|
||||
@@ -50,15 +39,25 @@ macro_rules! error {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
error!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
::tracing::event!(::tracing::Level::ERROR, account_id = $ctx.get_id(), "{}", &formatted);
|
||||
$ctx.set_last_error(&formatted);
|
||||
$ctx.emit_event($crate::EventType::Error(formatted));
|
||||
}};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
|
||||
$ctx.log_error(format!($msg, $($args),*))
|
||||
};
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub(crate) fn log_info(&self, file: &str, line: u32, msg: String) {
|
||||
self.emit_event(EventType::Info(format!("{file}:{line}: {msg}")));
|
||||
}
|
||||
|
||||
pub(crate) fn log_warn(&self, file: &str, line: u32, msg: String) {
|
||||
self.emit_event(EventType::Warning(format!("{file}:{line}: {msg}")));
|
||||
}
|
||||
|
||||
pub(crate) fn log_error(&self, msg: String) {
|
||||
self.set_last_error(&msg);
|
||||
self.emit_event(EventType::Error(msg));
|
||||
}
|
||||
|
||||
/// Set last error string.
|
||||
/// Implemented as blocking as used from macros in different, not always async blocks.
|
||||
pub fn set_last_error(&self, error: &str) {
|
||||
@@ -116,12 +115,6 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
|
||||
);
|
||||
// We can't use the warn!() macro here as the file!() and line!() macros
|
||||
// don't work with #[track_caller]
|
||||
tracing::event!(
|
||||
::tracing::Level::WARN,
|
||||
account_id = context.get_id(),
|
||||
"{}",
|
||||
&full
|
||||
);
|
||||
context.emit_event(crate::EventType::Warning(full));
|
||||
};
|
||||
self
|
||||
|
||||
@@ -93,11 +93,6 @@ impl<S: SessionStream> AsyncRead for LoggingStream<S> {
|
||||
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
|
||||
this.metrics.total_read, this.metrics.total_written
|
||||
);
|
||||
tracing::event!(
|
||||
::tracing::Level::WARN,
|
||||
account_id = *this.account_id,
|
||||
log_message
|
||||
);
|
||||
this.events.emit(Event {
|
||||
id: *this.account_id,
|
||||
typ: EventType::Warning(log_message),
|
||||
|
||||
@@ -9,14 +9,12 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use num_traits::ToPrimitive as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
pub use crate::net::proxy::ProxyConfig;
|
||||
pub use crate::provider::Socket;
|
||||
use crate::tools::ToOption;
|
||||
|
||||
/// User-entered setting for certificate checks.
|
||||
///
|
||||
@@ -232,62 +230,6 @@ impl EnteredLoginParam {
|
||||
oauth2: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Saves entered account settings,
|
||||
/// so that they can be prefilled if the user wants to configure the server again.
|
||||
///
|
||||
/// This is needed in case a UI is not yet updated, and still uses `get_config("mail_pw")` etc.
|
||||
/// in order to prefill the entered account settings.
|
||||
pub(crate) async fn save_legacy(&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?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EnteredLoginParam {
|
||||
@@ -369,41 +311,4 @@ 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: EnteredImapLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 0,
|
||||
folder: "".to_string(),
|
||||
security: Socket::Starttls,
|
||||
user: "".to_string(),
|
||||
password: "foobar".to_string(),
|
||||
},
|
||||
smtp: EnteredSmtpLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 2947,
|
||||
security: Socket::default(),
|
||||
user: "".to_string(),
|
||||
password: "".to_string(),
|
||||
},
|
||||
certificate_checks: Default::default(),
|
||||
oauth2: false,
|
||||
};
|
||||
param.save_legacy(&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_legacy(&t).await?, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+37
-21
@@ -580,7 +580,7 @@ impl Message {
|
||||
|
||||
if let Some(msg) = &mut msg {
|
||||
msg.additional_text =
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param)?;
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param);
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
@@ -618,7 +618,7 @@ impl Message {
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
param: &Params,
|
||||
) -> Result<String> {
|
||||
) -> String {
|
||||
if download_state != DownloadState::Done {
|
||||
let file_size = param
|
||||
.get(Param::PostMessageFileBytes)
|
||||
@@ -635,14 +635,14 @@ impl Message {
|
||||
.unwrap_or("?".to_owned());
|
||||
|
||||
return match viewtype {
|
||||
Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
|
||||
Viewtype::File => format!(" [{file_name} – {file_size}]"),
|
||||
_ => {
|
||||
let translated_viewtype = viewtype.to_locale_string(context);
|
||||
Ok(format!(" [{translated_viewtype} – {file_size}]"))
|
||||
format!(" [{translated_viewtype} – {file_size}]")
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(String::new())
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Returns the MIME type of an attached file if it exists.
|
||||
@@ -1445,13 +1445,10 @@ impl std::fmt::Display for MessageState {
|
||||
}
|
||||
|
||||
impl MessageState {
|
||||
/// Returns true if the message can transition to `OutFailed` state from the current state.
|
||||
/// Returns true if the message can be marked as failed in the current state.
|
||||
pub fn can_fail(self) -> bool {
|
||||
use MessageState::*;
|
||||
matches!(
|
||||
self,
|
||||
OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
|
||||
)
|
||||
matches!(self, OutPending | OutDelivered | OutFailed)
|
||||
}
|
||||
|
||||
/// Returns true for any outgoing message states.
|
||||
@@ -1644,11 +1641,15 @@ pub(crate) async fn delete_msgs_locally_done(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
modified_chat_ids: BTreeSet<ChatId>,
|
||||
pinned_messages_changed_chat_ids: BTreeSet<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);
|
||||
}
|
||||
for chat_id in pinned_messages_changed_chat_ids {
|
||||
context.emit_event(EventType::PinnedMessagesChanged { chat_id });
|
||||
}
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
@@ -1674,6 +1675,7 @@ pub async fn delete_msgs_ext(
|
||||
delete_for_all: bool,
|
||||
) -> Result<()> {
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
let mut pinned_messages_changed_chat_ids = BTreeSet::new();
|
||||
let mut deleted_rfc724_mid = Vec::new();
|
||||
let mut res = Ok(());
|
||||
|
||||
@@ -1689,6 +1691,9 @@ pub async fn delete_msgs_ext(
|
||||
);
|
||||
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
if msg.is_pinned() {
|
||||
pinned_messages_changed_chat_ids.insert(msg.chat_id);
|
||||
}
|
||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||
|
||||
let update_db = |trans: &mut rusqlite::Transaction| {
|
||||
@@ -1746,7 +1751,13 @@ pub async fn delete_msgs_ext(
|
||||
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?;
|
||||
delete_msgs_locally_done(
|
||||
context,
|
||||
msg_ids,
|
||||
modified_chat_ids,
|
||||
pinned_messages_changed_chat_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
@@ -1975,15 +1986,15 @@ pub(crate) async fn set_msg_failed(
|
||||
msg: &mut Message,
|
||||
error: &str,
|
||||
) -> Result<()> {
|
||||
if msg.state.can_fail() {
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg.id, error);
|
||||
} else {
|
||||
warn!(
|
||||
if !msg.state.can_fail() {
|
||||
info!(
|
||||
context,
|
||||
"{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
|
||||
)
|
||||
"Ignoring failed {} in state {}: {}", msg.id, msg.state, error
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg.id, error);
|
||||
msg.error = Some(error.to_string());
|
||||
|
||||
let exists = context
|
||||
@@ -2006,14 +2017,19 @@ pub(crate) async fn set_msg_failed(
|
||||
|
||||
/// Inserts a tombstone into `msgs` table
|
||||
/// to prevent downloading the same message in the future.
|
||||
/// With `on_server`, copies seen on IMAP later are deleted on the server.
|
||||
///
|
||||
/// Returns tombstone database row ID.
|
||||
pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
|
||||
pub(crate) async fn insert_tombstone(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
on_server: bool,
|
||||
) -> Result<MsgId> {
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, ChatId::TRASH),
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
|
||||
(rfc724_mid, ChatId::TRASH, on_server),
|
||||
)
|
||||
.await?;
|
||||
let msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
|
||||
@@ -454,6 +454,28 @@ async fn test_get_state() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a failure reported after a read receipt leaves the message untouched.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_msg_failed_after_mdn() -> 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 sent = alice.send_text(alice_chat.id, "hi").await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
alice.recv_mdn(bob, &bob_msg).await?;
|
||||
let mut msg = sent.load_from_db().await;
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
|
||||
set_msg_failed(alice, &mut msg, "relay bounced").await?;
|
||||
let msg = sent.load_from_db().await;
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
assert_eq!(msg.error(), None);
|
||||
let chats = Chatlist::try_load(alice, 0, None, None).await?;
|
||||
assert_eq!(chats.get_msg_id(0)?, Some(msg.id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_bot() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -631,6 +653,10 @@ async fn test_delete_msgs_offline() -> Result<()> {
|
||||
delete_msgs(alice, &[msg.id]).await?;
|
||||
assert!(!alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
alice
|
||||
.assert_warn("No SMTP connection candidates provided")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -756,3 +782,21 @@ async fn test_get_existing_msg_ids() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_fail() {
|
||||
use MessageState::*;
|
||||
|
||||
// states that are not allowed to transition to OutFailed
|
||||
assert!(!Undefined.can_fail());
|
||||
assert!(!InFresh.can_fail());
|
||||
assert!(!InNoticed.can_fail());
|
||||
assert!(!InSeen.can_fail());
|
||||
assert!(!OutDraft.can_fail());
|
||||
assert!(!OutMdnRcvd.can_fail());
|
||||
|
||||
// states that are allowed to transition to OutFailed
|
||||
assert!(OutPending.can_fail());
|
||||
assert!(OutDelivered.can_fail());
|
||||
assert!(OutFailed.can_fail());
|
||||
}
|
||||
|
||||
+351
-529
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ use pgp::armor;
|
||||
use pgp::packet::{Packet, PacketParser};
|
||||
use pretty_assertions::assert_eq;
|
||||
use regex::regex;
|
||||
use std::io::BufReader;
|
||||
use std::io::{BufReader, Cursor};
|
||||
use std::str;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::key::{load_self_secret_key, secret_key_to_public_key};
|
||||
use crate::message;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{self, SentMessage};
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
@@ -30,8 +30,7 @@ fn render_email_address(display_name: &str, addr: &str) -> String {
|
||||
let mut output = Vec::<u8>::new();
|
||||
new_address_with_name(display_name, addr.to_string())
|
||||
.unwrap_address()
|
||||
.write_header(&mut output, 0)
|
||||
.unwrap();
|
||||
.write_header(&mut output, 0);
|
||||
|
||||
String::from_utf8(output).unwrap()
|
||||
}
|
||||
@@ -50,9 +49,7 @@ fn test_render_email_address() {
|
||||
|
||||
let s = render_email_address(display_name, addr);
|
||||
|
||||
println!("{s}");
|
||||
|
||||
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>");
|
||||
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -70,14 +67,14 @@ fn test_render_email_address_noescape() {
|
||||
let s = render_email_address(display_name, addr);
|
||||
|
||||
// Addresses should not be unnecessarily be encoded, see <https://github.com/deltachat/deltachat-core-rust/issues/1575>:
|
||||
assert_eq!(s, r#""a space" <x@y.org>"#);
|
||||
assert_eq!(s, "\"a space\" <x@y.org>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address_duplicated_as_name() {
|
||||
let addr = "x@y.org";
|
||||
let s = render_email_address(addr, addr);
|
||||
assert_eq!(s, "<x@y.org>");
|
||||
assert_eq!(s, "<x@y.org>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -102,8 +99,7 @@ fn render_header_text(text: &str) -> String {
|
||||
// Some non-zero length of the header name.
|
||||
let bytes_written = 20;
|
||||
mail_builder::headers::text::Text::new(text.to_string())
|
||||
.write_header(&mut output, bytes_written)
|
||||
.unwrap();
|
||||
.write_header(&mut output, bytes_written);
|
||||
|
||||
String::from_utf8(output).unwrap()
|
||||
}
|
||||
@@ -281,8 +277,12 @@ async fn test_subject_mdn() {
|
||||
assert_eq!("Re: Hello, Bob", mf.subject_str(t).await.unwrap());
|
||||
}
|
||||
|
||||
/// Tests that MDN for unencrypted message can be created without throwing an error.
|
||||
///
|
||||
/// We do not send unencrypted MDNs, but do not want SMTP loop to get stuck
|
||||
/// if we somehow request the creation of unencrypted MDN.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
async fn test_mdn_create_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
@@ -297,20 +297,43 @@ async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
.await?;
|
||||
bob.set_config_bool(Config::MdnsEnabled, true).await?;
|
||||
|
||||
// MDN for unencrypted message is not encrypted.
|
||||
// MDN for unencrypted message.
|
||||
// Should not happen, but should also not throw an error.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
let chat_alice = alice.create_email_chat(&bob).await.id;
|
||||
let sent = alice.send_msg(chat_alice, &mut msg).await;
|
||||
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||
let mimefactory =
|
||||
MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
|
||||
assert!(!mimefactory.will_be_encrypted());
|
||||
let rendered_msg = mimefactory.render(&bob).await?;
|
||||
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
|
||||
// Only sending MDN to self (if BCC-self is enabled) because address-contact recipient has no key.
|
||||
assert!(queued_mdn.recipients.is_empty());
|
||||
|
||||
// MDNs are always encrypted, even if requested for unencrypted message.
|
||||
assert!(queued_mdn.encryption.is_encrypted());
|
||||
|
||||
bob.assert_warn("has no key, sending to self").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that MDNs sent in reply to encrypted messages are encrypted
|
||||
/// and MDNs for unencrypted messages are not created.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
||||
.await?;
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await?;
|
||||
bob.set_config(Config::Selfstatus, Some("Bob Examplenet"))
|
||||
.await?;
|
||||
bob.set_config_bool(Config::MdnsEnabled, true).await?;
|
||||
|
||||
assert!(!rendered_msg.message.contains("Bob Examplenet"));
|
||||
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
|
||||
let bob_alice_contact = bob.add_or_lookup_contact(&alice).await;
|
||||
assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg");
|
||||
|
||||
@@ -318,9 +341,9 @@ async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await;
|
||||
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
|
||||
assert!(mimefactory.will_be_encrypted());
|
||||
let rendered_msg = mimefactory.render(&bob).await?;
|
||||
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
assert!(queued_mdn.encryption.is_encrypted());
|
||||
let rendered_msg = render_queued_mail_with_context(queued_mdn, &bob).await?;
|
||||
|
||||
assert!(!rendered_msg.message.contains("Bob Examplenet"));
|
||||
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
|
||||
@@ -347,8 +370,8 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
|
||||
)?;
|
||||
import_public_key(alice, &bob_public_key).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_mdn(alice, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
|
||||
let mut recipients = mimefactory.recipients();
|
||||
let queued_mdn = mdn(alice, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
let mut recipients = queued_mdn.recipients;
|
||||
recipients.sort();
|
||||
assert_eq!(recipients, vec!["bob@example.net", "bob@relay2.example"]);
|
||||
|
||||
@@ -362,8 +385,8 @@ async fn test_mdn_autocrypt_throttle() -> Result<()> {
|
||||
alice: &TestContext,
|
||||
rcvd: &Message,
|
||||
) -> Result<bool> {
|
||||
let mf = MimeFactory::from_mdn(bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
|
||||
let rendered_msg = mf.render(bob).await?;
|
||||
let queued_mdn = mdn(bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
let rendered_msg = render_queued_mail_with_context(queued_mdn, bob).await?;
|
||||
let mime = MimeMessage::from_bytes(alice, rendered_msg.message.as_bytes()).await?;
|
||||
Ok(mime.autocrypt_fingerprint.is_some())
|
||||
}
|
||||
@@ -791,7 +814,7 @@ async fn test_protected_headers_directive() -> Result<()> {
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presence of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", b"content", None)?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
|
||||
@@ -928,7 +951,7 @@ async fn test_no_empty_to_header() -> Result<()> {
|
||||
assert!(
|
||||
// It would be equally fine if the payload contained `To: alice@example.org` or similar,
|
||||
// as long as it's a valid header
|
||||
payload.contains("To: \"hidden-recipients\": ;"),
|
||||
payload.contains("To: \"hidden-recipients\":;"),
|
||||
"Payload doesn't contain correct To: header: {payload}"
|
||||
);
|
||||
|
||||
@@ -1018,7 +1041,7 @@ END:VCARD";
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_outer_headers() -> Result<()> {
|
||||
async fn test_render_outer_headers_of_encrypted_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
@@ -1026,26 +1049,9 @@ async fn test_render_outer_headers() -> Result<()> {
|
||||
let chat_id = alice.create_chat_id(bob).await;
|
||||
let sent = alice.send_text(chat_id, "Hello!").await;
|
||||
|
||||
let (unencrypted, _encrypted) = sent
|
||||
.payload()
|
||||
.split_once("-----BEGIN PGP MESSAGE-----")
|
||||
.unwrap();
|
||||
let payload = normalized_payload(sent).await;
|
||||
|
||||
// Normalize the parts of the message that vary between runs
|
||||
// (MIME boundary, Date, Message-ID)
|
||||
let boundary = unencrypted
|
||||
.split_once("boundary=\"")
|
||||
.and_then(|(_, rest)| rest.split_once('"'))
|
||||
.map(|(b, _)| b)
|
||||
.unwrap_or_default();
|
||||
let unencrypted = unencrypted.replace(boundary, "BOUNDARY");
|
||||
|
||||
let rfc724_mid = sent.load_from_db().await.rfc724_mid;
|
||||
let unencrypted = unencrypted.replace(&rfc724_mid, "MESSAGE_ID@localhost");
|
||||
|
||||
let unencrypted = regex!(r"Date:[^\r\n]*")
|
||||
.replace(&unencrypted, "Date: DATE")
|
||||
.to_string();
|
||||
let (unencrypted, _encrypted) = payload.split_once("-----BEGIN PGP MESSAGE-----").unwrap();
|
||||
|
||||
let expected = r#"From: <alice@example.org>
|
||||
Date: DATE
|
||||
@@ -1054,8 +1060,8 @@ MIME-Version: 1.0
|
||||
To: "hidden-recipients": ;
|
||||
Subject: [...]
|
||||
Chat-Version: 1.0
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="BOUNDARY"
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="BOUNDARY"
|
||||
|
||||
|
||||
--BOUNDARY
|
||||
@@ -1081,3 +1087,140 @@ expected (debug print): {expected:?}"
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_unencrypted_msg_basic() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
let sent = alice.send_text(chat.id, "Hello!").await;
|
||||
let unencrypted = normalized_payload(sent).await;
|
||||
|
||||
let expected = r#"From: <alice@example.org>
|
||||
Message-ID: <MESSAGE_ID@localhost>
|
||||
MIME-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
|
||||
keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
|
||||
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
|
||||
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
|
||||
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
|
||||
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
|
||||
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
|
||||
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
|
||||
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
|
||||
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
|
||||
3jsMAg==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Date: DATE
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello!"#
|
||||
.replace("\n", "\r\n");
|
||||
assert_eq!(
|
||||
unencrypted, expected,
|
||||
"---------------- Actual: ----------------
|
||||
{unencrypted}
|
||||
-----------------------------------------
|
||||
actual (debug print): {unencrypted:?}
|
||||
expected (debug print): {expected:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_unencrypted_msg_with_attachment() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text("Hello!".to_string());
|
||||
msg.set_file_from_bytes(alice, "foo.bar", b"content", None)?;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let unencrypted = normalized_payload(sent).await;
|
||||
|
||||
let expected = r#"From: <alice@example.org>
|
||||
Message-ID: <MESSAGE_ID@localhost>
|
||||
MIME-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
|
||||
keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
|
||||
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
|
||||
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
|
||||
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
|
||||
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
|
||||
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
|
||||
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
|
||||
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
|
||||
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
|
||||
3jsMAg==
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="BOUNDARY"
|
||||
Date: DATE
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
|
||||
|
||||
--BOUNDARY
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello!
|
||||
--BOUNDARY
|
||||
Content-Type: application/octet-stream
|
||||
Content-Disposition: attachment; filename="foo.bar"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
Y29udGVudA==
|
||||
|
||||
--BOUNDARY--
|
||||
"#
|
||||
.replace("\n", "\r\n");
|
||||
assert_eq!(
|
||||
unencrypted, expected,
|
||||
"---------------- Actual: ----------------
|
||||
{unencrypted}
|
||||
-----------------------------------------
|
||||
actual (debug print): {unencrypted:?}
|
||||
expected (debug print): {expected:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize the parts of the message that vary between runs
|
||||
/// (MIME boundary, Date, Message-ID)
|
||||
async fn normalized_payload(sent: SentMessage<'_>) -> String {
|
||||
let rfc724_mid = sent.load_from_db().await.rfc724_mid;
|
||||
|
||||
let mut payload = sent.payload;
|
||||
|
||||
if let Some(boundary) = payload
|
||||
.split_once("boundary=\"")
|
||||
.and_then(|(_, rest)| rest.split_once('"'))
|
||||
.map(|(b, _)| b)
|
||||
{
|
||||
payload = payload.replace(boundary, "BOUNDARY");
|
||||
}
|
||||
|
||||
payload = payload.replace(&rfc724_mid, "MESSAGE_ID@localhost");
|
||||
|
||||
payload = regex!(r"Date:[^\r\n]*")
|
||||
.replace(&payload, "Date: DATE")
|
||||
.to_string();
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
+18
-22
@@ -294,11 +294,7 @@ impl MimeMessage {
|
||||
&mut wants_mdn,
|
||||
&mail,
|
||||
);
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| is_hidden(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
);
|
||||
headers_removed.extend(headers.extract_if(|k, _v| is_hidden(k)).map(|(k, _v)| k));
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -935,9 +931,10 @@ impl MimeMessage {
|
||||
// See if an MDN is requested from the other side
|
||||
if self.decryption_error.is_none()
|
||||
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
|
||||
&& self.wants_mdn
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
&& self.wants_mdn
|
||||
&& self.was_encrypted() // Do not send MDNs for unencrypted messages.
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
{
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
@@ -951,7 +948,7 @@ impl MimeMessage {
|
||||
typ: Viewtype::Text,
|
||||
..Default::default()
|
||||
};
|
||||
if self.wants_mdn && self.incoming {
|
||||
if self.wants_mdn && self.was_encrypted() && self.incoming {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
if let Some(ref subject) = self.get_subject()
|
||||
@@ -1751,7 +1748,7 @@ impl MimeMessage {
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| has_header_protection || is_protected(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
.map(|(k, _v)| k),
|
||||
);
|
||||
|
||||
if has_header_protection {
|
||||
@@ -2540,19 +2537,18 @@ async fn handle_ndn(
|
||||
for msg_id in msg_ids {
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.chat_id).await?;
|
||||
if chat.typ == constants::Chattype::OutBroadcast {
|
||||
continue;
|
||||
if chat.typ == constants::Chattype::Single {
|
||||
let aggregated_error = message
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|err| format!("{err}\n\n{err_msg}"));
|
||||
set_msg_failed(
|
||||
context,
|
||||
&mut message,
|
||||
aggregated_error.as_ref().unwrap_or(err_msg),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let aggregated_error = message
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|err| format!("{err}\n\n{err_msg}"));
|
||||
set_msg_failed(
|
||||
context,
|
||||
&mut message,
|
||||
aggregated_error.as_ref().unwrap_or(err_msg),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+4
-2
@@ -410,7 +410,8 @@ impl ProxyConfig {
|
||||
};
|
||||
let tcp_stream =
|
||||
crate::net::connect_tcp(context, &hostname, http_config.port, load_cache)
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to connect to HTTP proxy")?;
|
||||
let auth = if let Some((username, password)) = &http_config.user_password {
|
||||
Some((username.as_str(), password.as_str()))
|
||||
} else {
|
||||
@@ -429,7 +430,8 @@ impl ProxyConfig {
|
||||
|
||||
let tcp_stream =
|
||||
crate::net::connect_tcp(context, &hostname, https_config.port, load_cache)
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to connect to HTTPS proxy")?;
|
||||
let use_sni = true;
|
||||
let tls_stream = wrap_rustls(
|
||||
&hostname,
|
||||
|
||||
@@ -46,7 +46,7 @@ use crate::mimeparser::SystemMessage;
|
||||
|
||||
/// The length of an ed25519 `PublicKey`, in bytes.
|
||||
const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
const PUBLIC_KEY_STUB: &[u8] = b"static_string";
|
||||
|
||||
/// Store Iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -86,14 +86,14 @@ async fn test_can_communicate() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -107,14 +107,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -149,14 +149,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice 2".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice 2".as_bytes() {
|
||||
if data == b"bob -> alice 2" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -314,14 +314,14 @@ async fn test_can_reconnect() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -373,14 +373,14 @@ async fn test_can_reconnect() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
|
||||
+1
-1
@@ -401,7 +401,7 @@ pub fn merge_openpgp_certificates(
|
||||
// such as Alice's key in `test-data/key/alice-secret.asc`.
|
||||
let best_user: Option<SignedUser> = old_users
|
||||
.into_iter()
|
||||
.chain(new_users.clone())
|
||||
.chain(new_users)
|
||||
.filter_map(|SignedUser { id, signatures }| {
|
||||
// Select the best signature for each User ID.
|
||||
// If User ID has no valid signatures, it is filtered out.
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::LazyLock;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::*;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
use crate::{
|
||||
config::Config,
|
||||
decrypt,
|
||||
@@ -19,6 +20,9 @@ async fn decrypt_bytes(
|
||||
auth_tokens_for_decryption: &[String],
|
||||
) -> Result<pgp::composed::Message<'static>> {
|
||||
let t = &TestContext::new().await;
|
||||
add_pseudo_transport(t, "alice@example.org")
|
||||
.await
|
||||
.expect("Failed to add pseudo transport");
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.expect("Failed to configure address");
|
||||
@@ -380,7 +384,7 @@ fn test_merge_openpgp_certificates() {
|
||||
|
||||
// Cannot merge certificates with different primary key.
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob, alice).is_err());
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
|
||||
+6
-223
@@ -13,6 +13,7 @@ use anyhow::{Result, ensure};
|
||||
use crate::chat::{ChatId, send_msg};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -78,7 +79,11 @@ async fn update_pinned_state_in_db(
|
||||
(new_pinned_state, msg.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
context.emit_event(EventType::PinnedMessagesChanged {
|
||||
chat_id: msg.chat_id,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -140,226 +145,4 @@ pub(crate) async fn handle_pinned_state_from_wire(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{ChatItem, add_info_msg, create_broadcast, get_chat_msgs};
|
||||
use crate::config::Config;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{TestContextManager, sync};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pinned_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await; // Alice's second device
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice creates all chat types upfront, with Bob as member if possible
|
||||
let single_chat_id = alice.create_chat(bob).await.id;
|
||||
let group_chat_id = alice.create_group_with_members("Group", &[bob]).await;
|
||||
let broadcast_chat_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(broadcast_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let self_chat_id = alice.get_self_chat().await.id;
|
||||
sync(alice, alice2).await;
|
||||
|
||||
for alice_chat_id in [
|
||||
single_chat_id,
|
||||
group_chat_id,
|
||||
broadcast_chat_id,
|
||||
self_chat_id,
|
||||
] {
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Alice sends message "Foo" and pins it
|
||||
let sent1 = alice.send_text(alice_chat_id, "Foo").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(!msg1.is_pinned());
|
||||
|
||||
set_pinned_state(alice, msg1.id, true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert!(sent1.load_from_db().await.is_pinned());
|
||||
|
||||
let info_msg = sent2.load_from_db().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(alice).await?.is_none()); // contact not needed, tapping shall jump to message
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
// Pinning an info message does not work
|
||||
assert!(set_pinned_state(alice, info_msg.id, true).await.is_err());
|
||||
|
||||
// Unpin the initially pinned message.
|
||||
// Before, send another message "Bar". To test, no visible info message is added this time,
|
||||
let sent3 = alice.send_text(alice_chat_id, "Bar").await;
|
||||
|
||||
set_pinned_state(alice, msg1.id, false).await?;
|
||||
let sent4 = alice.pop_sent_msg().await;
|
||||
assert!(!sent1.load_from_db().await.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let msg3 = sent3.load_from_db().await;
|
||||
assert!(!msg3.is_info());
|
||||
assert!(!msg3.is_pinned());
|
||||
assert_eq!(alice.get_last_msg_id_in(msg3.chat_id).await, msg3.id); // last message is still "Bar", not an info message
|
||||
|
||||
if alice_chat_id != self_chat_id {
|
||||
// Bob receives message "Foo"
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
assert!(!msg1.is_pinned());
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Bob receives info message to pin "Foo"
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert!(Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
let info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(bob).await?.is_none());
|
||||
|
||||
// Bob receives message "Bar" and hidden message to unpin message "Foo"
|
||||
bob.recv_msg(&sent3).await;
|
||||
bob.recv_msg_trash(&sent4).await;
|
||||
assert!(!Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
// Alice's second device receives all four messages and ends up in the same state
|
||||
let msg1 = alice2.recv_msg(&sent1).await;
|
||||
alice2.recv_msg(&sent2).await;
|
||||
assert!(Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
alice2.recv_msg(&sent3).await;
|
||||
alice2.recv_msg_trash(&sent4).await;
|
||||
assert!(!Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(alice2, alice2.get_last_msg_id_in(msg1.chat_id).await)
|
||||
.await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_pinned_messages_order() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
let boilerplate_msg_count = get_chat_msgs(alice, chat_id).await?.len();
|
||||
|
||||
// create three messages, sent1 and sent2 have different timestamp, sent2 and sent3 may differ by ID only
|
||||
let sent1 = alice.send_text(chat_id, "1").await;
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
let sent2 = alice.send_text(chat_id, "2").await;
|
||||
let sent3 = alice.send_text(chat_id, "3").await;
|
||||
|
||||
// get_chat_msgs() start with the oldest message
|
||||
let chat_msgs = get_chat_msgs(alice, chat_id).await?;
|
||||
let msg_ids: Vec<_> = chat_msgs
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
ChatItem::Message { msg_id } => Some(msg_id),
|
||||
ChatItem::DayMarker { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
&msg_ids[boilerplate_msg_count..],
|
||||
&[
|
||||
sent1.sender_msg_id,
|
||||
sent2.sender_msg_id,
|
||||
sent3.sender_msg_id
|
||||
]
|
||||
);
|
||||
|
||||
// get_pinned_messages() has the same order, also starting with the oldest message
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
// order of pinning does not affect the order of pinned messages.
|
||||
// this is to keep scrolling direction of chat bubbles and pinned banner scrollbar in sync,
|
||||
// and not jumping wildly around.
|
||||
// this is also what most other messengers are doing.
|
||||
set_pinned_state(alice, sent1.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, false).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 0);
|
||||
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_pinned_state_from_wire() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
|
||||
let sent1 = alice.send_text(chat_id, "pinnable").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(is_pinnable(&msg1));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg1, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// For not-pinnable messages, handle_pinned_state_from_wire() logs a warning and returns "ok".
|
||||
// otherwise if there is an incompatibility in which messages are treated as "pinnable",
|
||||
// this error will bubble up and user will get a device message saying "please report a bug".
|
||||
let msg2_id = add_info_msg(alice, chat_id, "not pinnable").await?;
|
||||
let msg2 = Message::load_from_db(alice, msg2_id).await?;
|
||||
assert!(!is_pinnable(&msg2));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg2, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
alice.assert_warn("Message is not pinnable").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod pinned_messages_tests;
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
use super::*;
|
||||
use crate::chat::{ChatItem, add_info_msg, create_broadcast, get_chat_msgs};
|
||||
use crate::config::Config;
|
||||
use crate::ephemeral;
|
||||
use crate::message;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{TestContext, TestContextManager, sync};
|
||||
use crate::tools::{SystemTime, time};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Waits for a PinnedMessagesChanged for a given `chat_id`.
|
||||
///
|
||||
/// Panics if event arrives for the wrong `chat_id`.
|
||||
async fn expect_pinned_message_event(context: &TestContext, chat_id: ChatId) {
|
||||
let EventType::PinnedMessagesChanged {
|
||||
chat_id: event_chat_id,
|
||||
} = context
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::PinnedMessagesChanged { .. }))
|
||||
.await
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
assert_eq!(event_chat_id, chat_id);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pinned_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await; // Alice's second device
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice creates all chat types upfront, with Bob as member if possible
|
||||
let single_chat_id = alice.create_chat(bob).await.id;
|
||||
let group_chat_id = alice.create_group_with_members("Group", &[bob]).await;
|
||||
let broadcast_chat_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(broadcast_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let self_chat_id = alice.get_self_chat().await.id;
|
||||
sync(alice, alice2).await;
|
||||
|
||||
for alice_chat_id in [
|
||||
single_chat_id,
|
||||
group_chat_id,
|
||||
broadcast_chat_id,
|
||||
self_chat_id,
|
||||
] {
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Alice sends message "Foo" and pins it
|
||||
let sent1 = alice.send_text(alice_chat_id, "Foo").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(!msg1.is_pinned());
|
||||
|
||||
set_pinned_state(alice, msg1.id, true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
expect_pinned_message_event(alice, msg1.chat_id).await;
|
||||
assert!(sent1.load_from_db().await.is_pinned());
|
||||
|
||||
let info_msg = sent2.load_from_db().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(alice).await?.is_none()); // contact not needed, tapping shall jump to message
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
// Pinning an info message does not work
|
||||
assert!(set_pinned_state(alice, info_msg.id, true).await.is_err());
|
||||
|
||||
// Unpin the initially pinned message.
|
||||
// Before, send another message "Bar". To test, no visible info message is added this time,
|
||||
let sent3 = alice.send_text(alice_chat_id, "Bar").await;
|
||||
|
||||
set_pinned_state(alice, msg1.id, false).await?;
|
||||
let sent4 = alice.pop_sent_msg().await;
|
||||
assert!(!sent1.load_from_db().await.is_pinned());
|
||||
expect_pinned_message_event(alice, msg1.chat_id).await;
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let msg3 = sent3.load_from_db().await;
|
||||
assert!(!msg3.is_info());
|
||||
assert!(!msg3.is_pinned());
|
||||
assert_eq!(alice.get_last_msg_id_in(msg3.chat_id).await, msg3.id); // last message is still "Bar", not an info message
|
||||
|
||||
if alice_chat_id != self_chat_id {
|
||||
// Bob receives message "Foo"
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
assert!(!msg1.is_pinned());
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
// Bob receives info message to pin "Foo"
|
||||
bob.recv_msg(&sent2).await;
|
||||
expect_pinned_message_event(bob, msg1.chat_id).await;
|
||||
assert!(Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
assert_eq!(pinned[0], msg1.id);
|
||||
|
||||
let info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(info_msg.is_info());
|
||||
assert!(!info_msg.hidden);
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(bob).await?.is_none());
|
||||
|
||||
// Bob receives message "Bar" and hidden message to unpin message "Foo"
|
||||
bob.recv_msg(&sent3).await;
|
||||
bob.recv_msg_trash(&sent4).await;
|
||||
expect_pinned_message_event(bob, msg1.chat_id).await;
|
||||
assert!(!Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
// Alice's second device receives all four messages and ends up in the same state
|
||||
let msg1 = alice2.recv_msg(&sent1).await;
|
||||
alice2.recv_msg(&sent2).await;
|
||||
expect_pinned_message_event(alice2, msg1.chat_id).await;
|
||||
assert!(Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
alice2.recv_msg(&sent3).await;
|
||||
alice2.recv_msg_trash(&sent4).await;
|
||||
expect_pinned_message_event(alice2, msg1.chat_id).await;
|
||||
assert!(!Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(alice2, alice2.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_pinned_messages_order() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
let boilerplate_msg_count = get_chat_msgs(alice, chat_id).await?.len();
|
||||
|
||||
// create three messages, sent1 and sent2 have different timestamp, sent2 and sent3 may differ by ID only
|
||||
let sent1 = alice.send_text(chat_id, "1").await;
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
let sent2 = alice.send_text(chat_id, "2").await;
|
||||
let sent3 = alice.send_text(chat_id, "3").await;
|
||||
|
||||
// get_chat_msgs() start with the oldest message
|
||||
let chat_msgs = get_chat_msgs(alice, chat_id).await?;
|
||||
let msg_ids: Vec<_> = chat_msgs
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
ChatItem::Message { msg_id } => Some(msg_id),
|
||||
ChatItem::DayMarker { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
&msg_ids[boilerplate_msg_count..],
|
||||
&[
|
||||
sent1.sender_msg_id,
|
||||
sent2.sender_msg_id,
|
||||
sent3.sender_msg_id
|
||||
]
|
||||
);
|
||||
|
||||
// get_pinned_messages() has the same order, also starting with the oldest message
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
// order of pinning does not affect the order of pinned messages.
|
||||
// this is to keep scrolling direction of chat bubbles and pinned banner scrollbar in sync,
|
||||
// and not jumping wildly around.
|
||||
// this is also what most other messengers are doing.
|
||||
set_pinned_state(alice, sent1.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, false).await?;
|
||||
set_pinned_state(alice, sent3.sender_msg_id, false).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 0);
|
||||
|
||||
set_pinned_state(alice, sent3.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent2.sender_msg_id, true).await?;
|
||||
set_pinned_state(alice, sent1.sender_msg_id, true).await?;
|
||||
let pinned = get_pinned_messages(alice, chat_id).await?;
|
||||
assert_eq!(pinned.len(), 3);
|
||||
assert_eq!(pinned[0], sent1.sender_msg_id);
|
||||
assert_eq!(pinned[1], sent2.sender_msg_id);
|
||||
assert_eq!(pinned[2], sent3.sender_msg_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_pinned_state_from_wire() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice.get_self_chat().await.id;
|
||||
|
||||
let sent1 = alice.send_text(chat_id, "pinnable").await;
|
||||
let msg1 = sent1.load_from_db().await;
|
||||
assert!(is_pinnable(&msg1));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg1, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// For not-pinnable messages, handle_pinned_state_from_wire() logs a warning and returns "ok".
|
||||
// otherwise if there is an incompatibility in which messages are treated as "pinnable",
|
||||
// this error will bubble up and user will get a device message saying "please report a bug".
|
||||
let msg2_id = add_info_msg(alice, chat_id, "not pinnable").await?;
|
||||
let msg2 = Message::load_from_db(alice, msg2_id).await?;
|
||||
assert!(!is_pinnable(&msg2));
|
||||
assert!(
|
||||
handle_pinned_state_from_wire(alice, &msg2, true)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
alice.assert_warn("Message is not pinnable").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that disappearing pinned message expires and emits `PinnedMessagesChanged` event.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_pinned_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = alice.create_chat_id(bob).await;
|
||||
|
||||
// Alice sends ephemeral timer in single chat with Bob.
|
||||
alice_chat_id
|
||||
.set_ephemeral_timer(alice, ephemeral::Timer::from_u32(60))
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Alice sends "Hello!" message to Bob.
|
||||
let bob_msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
|
||||
// Bob reads the message, so the timer starts.
|
||||
message::markseen_msgs(bob, vec![bob_msg.id]).await?;
|
||||
|
||||
// Bob pins "Hello!" message received from Alice.
|
||||
set_pinned_state(bob, bob_msg.id, true).await?;
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
|
||||
// Wait until the message expires.
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
ephemeral::delete_expired_messages(bob, time()).await?;
|
||||
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `PinnedMessagesChanged` event is emitted when pinned message is deleted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_pinned_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let bob_msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
|
||||
// Bob pins "Hello!" message received from Alice.
|
||||
set_pinned_state(bob, bob_msg.id, true).await?;
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert_eq!(pinned.len(), 1);
|
||||
|
||||
message::delete_msgs(bob, &[bob_msg.id]).await?;
|
||||
expect_pinned_message_event(bob, bob_chat_id).await;
|
||||
let pinned = get_pinned_messages(bob, bob_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+5
-9
@@ -1,6 +1,6 @@
|
||||
//! Handle plain text together with some attributes.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use regex::regex;
|
||||
|
||||
use crate::simplify::remove_message_footer;
|
||||
|
||||
@@ -25,12 +25,8 @@ impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
let linkify_mail_re = regex!(r"\b([\w.\-+]+@[\w.\-]+)\b");
|
||||
let linkify_url_re = regex!(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)");
|
||||
|
||||
let lines: Vec<&str> = self.text.lines().collect();
|
||||
let (lines, _footer) = remove_message_footer(&lines);
|
||||
@@ -52,12 +48,12 @@ impl PlainText {
|
||||
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
|
||||
let line = line.to_string().replace('\r', "");
|
||||
|
||||
let mut line = LINKIFY_MAIL_RE
|
||||
let mut line = linkify_mail_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
line = LINKIFY_URL_RE
|
||||
line = linkify_url_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
mod dclogin_scheme;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
pub(crate) use dclogin_scheme::login_param_from_login_qr;
|
||||
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
|
||||
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
||||
use regex::regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::autorelay::login_param_from_host;
|
||||
use crate::config::Config;
|
||||
use crate::configure::MAX_RELAYS;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::key::Fingerprint;
|
||||
@@ -133,18 +134,6 @@ pub enum Qr {
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// Scanned fingerprint does not match the last seen fingerprint.
|
||||
FprMismatch {
|
||||
/// Contact ID.
|
||||
contact_id: Option<ContactId>,
|
||||
},
|
||||
|
||||
/// The scanned QR code contains a fingerprint but no e-mail address.
|
||||
FprWithoutAddr {
|
||||
/// Key fingerprint.
|
||||
fingerprint: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to create an account on the given domain.
|
||||
Account {
|
||||
/// Server domain name.
|
||||
@@ -500,7 +489,8 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
addrs.push(normalize_address(primary_addr)?);
|
||||
};
|
||||
if let Some(secondary_addrs_raw) = param.get("r") {
|
||||
for secondary_address in secondary_addrs_raw.split(',') {
|
||||
let max_secondary = MAX_RELAYS.saturating_sub(addrs.len());
|
||||
for secondary_address in secondary_addrs_raw.split(',').take(max_secondary) {
|
||||
addrs.push(normalize_address(secondary_address)?)
|
||||
}
|
||||
}
|
||||
@@ -651,24 +641,21 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
is_v3,
|
||||
})
|
||||
}
|
||||
} else if let Some(addr) = addrs.first() {
|
||||
let fingerprint = fingerprint.hex();
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup_ext(context, "", addr, &fingerprint, Origin::UnhandledQrScan)
|
||||
.await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
if contact.public_key(context).await?.is_some() {
|
||||
Ok(Qr::FprOk { contact_id })
|
||||
} else {
|
||||
Ok(Qr::FprMismatch {
|
||||
contact_id: Some(contact_id),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(Qr::FprWithoutAddr {
|
||||
fingerprint: fingerprint.human_readable(),
|
||||
})
|
||||
let fingerprint = fingerprint.hex();
|
||||
let contact_id: Option<ContactId> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts WHERE fingerprint=?",
|
||||
(fingerprint,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(contact_id) = contact_id else {
|
||||
bail!("Contact matching the fingerprint is not found");
|
||||
};
|
||||
|
||||
Ok(Qr::FprOk { contact_id })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,7 +764,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
|
||||
let addr = server_config.addr();
|
||||
let host = addr.host().to_string();
|
||||
let host = addr.host();
|
||||
let port = addr.port();
|
||||
Ok(Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
@@ -838,7 +825,8 @@ pub(crate) async fn login_param_from_account_qr(
|
||||
.context("Invalid DCACCOUNT scheme")?;
|
||||
|
||||
if !payload.starts_with(HTTPS_SCHEME) {
|
||||
let param = login_param_from_host(payload);
|
||||
let mark_as_autorelay = false;
|
||||
let param = login_param_from_host(payload, mark_as_autorelay);
|
||||
return Ok(param);
|
||||
}
|
||||
|
||||
@@ -1083,16 +1071,11 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
}
|
||||
|
||||
static VCARD_NAME_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
|
||||
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
|
||||
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let name = VCARD_NAME_RE
|
||||
let name = regex!(r"(?m)^N:([^;]*);([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
@@ -1102,7 +1085,10 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
|
||||
let addr = if let Some(cap) = regex!(r"(?m)^EMAIL([^:\n]*):([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| caps.get(2))
|
||||
{
|
||||
normalize_address(cap.as_str().trim())?
|
||||
} else {
|
||||
bail!("Bad e-mail address");
|
||||
@@ -1136,7 +1122,7 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
|
||||
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
|
||||
|
||||
Ok(new_addr.to_string())
|
||||
Ok(new_addr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -120,7 +120,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
};
|
||||
|
||||
Ok(Qr::Login {
|
||||
address: addr.to_owned(),
|
||||
address: addr,
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
|
||||
+52
-38
@@ -305,12 +305,10 @@ async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
||||
let ctx = TestContext::new_alice().await;
|
||||
|
||||
// Token cannot contain "/"
|
||||
let qr = check_qr(
|
||||
assert!(check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
|
||||
).await?;
|
||||
|
||||
assert!(matches!(qr, Qr::FprMismatch { .. }));
|
||||
).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -361,6 +359,23 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// A bad invite code must not be able to steer us onto arbitrarily many relays.
|
||||
let relays: Vec<String> = (0..20).map(|i| format!("cli%40r{i}.example.org")).collect();
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!(
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&r={}&i=TbnwJ6lSvD5&s=0ejvbdFSQxB",
|
||||
relays.join(",")
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Qr::AskVerifyContact { addrs, .. } = qr {
|
||||
assert_eq!(addrs.len(), MAX_RELAYS);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -373,16 +388,17 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
let alice_contact = bob.add_or_lookup_contact(alice).await;
|
||||
let alice_contact_id = alice_contact.id;
|
||||
|
||||
let qr = check_qr(
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||
assert_ne!(contact_id.unwrap(), alice_contact_id);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
// OPENPGP4FPR may have an address,
|
||||
// but it is not used anymore since key contacts are introduced.
|
||||
// We lookup the contact only by fingerprint and ignore the address.
|
||||
assert!(
|
||||
check_qr(
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let qr = check_qr(
|
||||
bob,
|
||||
@@ -398,46 +414,44 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
assert!(
|
||||
check_qr(
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
||||
)
|
||||
.await?,
|
||||
Qr::FprMismatch { .. }
|
||||
));
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests OPENPGP4FPR QR codes without an email address.
|
||||
///
|
||||
/// Email address in OPENPGP4FPR QR codes was an extension
|
||||
/// used before switch to identifying contacts by fingerprint.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_without_addr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
assert!(
|
||||
check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
assert!(
|
||||
check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
||||
|
||||
+1
-2
@@ -165,7 +165,7 @@ async fn set_msg_id_reaction(
|
||||
.await?;
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)
|
||||
{
|
||||
chat.param
|
||||
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
|
||||
@@ -1032,7 +1032,6 @@ Content-Disposition: reaction\n\
|
||||
assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
|
||||
assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
|
||||
assert!(summary.prefix.is_none());
|
||||
assert!(summary.thumbnail_path.is_none());
|
||||
assert_summary(&alice, "BOB reacted 👍 to \"Party?\"").await;
|
||||
|
||||
// Alice reacts to own message as well
|
||||
|
||||
+23
-17
@@ -483,7 +483,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
let trash = || async {
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid, false).await?];
|
||||
Ok(Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::Undefined,
|
||||
@@ -668,14 +668,15 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
match res {
|
||||
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
|
||||
let msg_id = insert_tombstone(context, rfc724_mid).await?;
|
||||
let needs_delete_job = res == securejoin::HandshakeMessage::Done;
|
||||
let msg_id = insert_tombstone(context, rfc724_mid, needs_delete_job).await?;
|
||||
received_msg = Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::InSeen,
|
||||
hidden: false,
|
||||
sort_timestamp: mime_parser.timestamp_sent,
|
||||
msg_ids: vec![msg_id],
|
||||
needs_delete_job: res == securejoin::HandshakeMessage::Done,
|
||||
needs_delete_job,
|
||||
});
|
||||
}
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
@@ -2107,7 +2108,8 @@ async fn add_parts(
|
||||
let hidden = part.is_reaction;
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
let is_incoming_fresh = mime_parser.incoming && !seen;
|
||||
let is_incoming_fresh =
|
||||
mime_parser.incoming && !seen && chat_id_blocked == Blocked::Not;
|
||||
set_msg_reaction(
|
||||
context,
|
||||
mime_in_reply_to,
|
||||
@@ -2310,7 +2312,7 @@ INSERT INTO msgs
|
||||
// This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)
|
||||
{
|
||||
// write the last subject even if empty -
|
||||
// otherwise a reply may get an outdated subject.
|
||||
@@ -2391,6 +2393,7 @@ async fn handle_edit_delete(
|
||||
}
|
||||
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
let mut pinned_messages_changed_chat_ids = BTreeSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
@@ -2399,7 +2402,7 @@ async fn handle_edit_delete(
|
||||
let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
|
||||
insert_tombstone(context, rfc724_mid).await?;
|
||||
insert_tombstone(context, rfc724_mid, false).await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -2415,8 +2418,17 @@ async fn handle_edit_delete(
|
||||
message::delete_msg_locally(context, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
if msg.is_pinned() {
|
||||
pinned_messages_changed_chat_ids.insert(msg.chat_id);
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
message::delete_msgs_locally_done(
|
||||
context,
|
||||
&msg_ids,
|
||||
modified_chat_ids,
|
||||
pinned_messages_changed_chat_ids,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3403,7 +3415,7 @@ async fn apply_chat_name_avatar_and_description_changes(
|
||||
&& is_from_in_chat
|
||||
&& chat
|
||||
.param
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)
|
||||
{
|
||||
info!(context, "Group-avatar change for {}.", chat.id);
|
||||
match avatar_action {
|
||||
@@ -3732,10 +3744,9 @@ async fn apply_out_broadcast_changes(
|
||||
if from_id == ContactId::SELF {
|
||||
let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
|
||||
if let Some(added_id) = added_id {
|
||||
if chat::is_contact_in_chat(context, chat.id, added_id).await? {
|
||||
info!(context, "No-op broadcast addition (TRASH)");
|
||||
better_msg.get_or_insert("".to_string());
|
||||
} else {
|
||||
info!(context, "Broadcast addition (TRASH)");
|
||||
better_msg.get_or_insert("".to_string());
|
||||
if !chat::is_contact_in_chat(context, chat.id, added_id).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
@@ -3743,11 +3754,6 @@ async fn apply_out_broadcast_changes(
|
||||
&[added_id],
|
||||
)
|
||||
.await?;
|
||||
let msg =
|
||||
stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
|
||||
.await;
|
||||
better_msg.get_or_insert(msg);
|
||||
added_removed_id = Some(added_id);
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -416,7 +416,9 @@ async fn test_escaped_from() {
|
||||
);
|
||||
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.text, "hello");
|
||||
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||
|
||||
// MDN request from unencrypted message is ignored.
|
||||
assert!(msg.param.get_int(Param::WantsMdn).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -726,6 +728,7 @@ async fn test_resend_after_ndn() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// an NDN in a group does not make the whole message as failed
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -758,7 +761,7 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?;
|
||||
assert!(matches!(
|
||||
@@ -766,9 +769,6 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
ChatItem::Message { msg_id } if msg_id == msg.id
|
||||
));
|
||||
|
||||
t.assert_warn("Delivery Status Notification (Failure)")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2725,19 +2725,7 @@ async fn test_read_receipts_dont_create_chats() -> Result<()> {
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// Bob sends a read receipt.
|
||||
let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn(
|
||||
&bob,
|
||||
received_msg.from_id,
|
||||
received_msg.rfc724_mid,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
let rendered_mdn = mdn_mimefactory.render(&bob).await?;
|
||||
let mdn_body = rendered_mdn.message;
|
||||
|
||||
// Alice receives the read receipt.
|
||||
receive_imf(&alice, mdn_body.as_bytes(), false).await?;
|
||||
alice.recv_mdn(&bob, &received_msg).await?;
|
||||
|
||||
// Chat should not pop up in the chatlist.
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
@@ -2760,19 +2748,7 @@ async fn test_read_receipts_dont_unmark_bots() -> Result<()> {
|
||||
.await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
|
||||
// Bob sends a read receipt.
|
||||
let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn(
|
||||
bob,
|
||||
received_msg.from_id,
|
||||
received_msg.rfc724_mid,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
let rendered_mdn = mdn_mimefactory.render(bob).await?;
|
||||
let mdn_body = rendered_mdn.message;
|
||||
|
||||
// Alice receives the read receipt.
|
||||
receive_imf(alice, mdn_body.as_bytes(), false).await?;
|
||||
alice.recv_mdn(bob, &received_msg).await?;
|
||||
let msg = alice.get_last_msg_in(alice_chat.id).await;
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
let ab_contact = alice.add_or_lookup_contact(bob).await;
|
||||
@@ -2981,6 +2957,7 @@ async fn test_weird_and_duplicated_filenames() -> Result<()> {
|
||||
"a.tar.gz",
|
||||
"a.a..a.a.a.a.tar.gz",
|
||||
"a. tar.tar.gz",
|
||||
"very long filename with emoji \u{1F600} very long file name with emoji \u{1FAE0} very long file name with emoji.tar.gz",
|
||||
] {
|
||||
let attachment = alice.blobdir.join(filename_sent);
|
||||
let content = "File content of tar.gz archive".to_string();
|
||||
@@ -3364,6 +3341,35 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blocked_contact_sends_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let bob_msg_id = tcm.send_recv_accept(alice, bob, "Hi!").await.id;
|
||||
|
||||
let chat = alice.get_chat(bob).await;
|
||||
chat.id.block(alice).await?;
|
||||
|
||||
crate::reaction::send_reaction(bob, bob_msg_id, "👍").await?;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_hidden(&sent).await;
|
||||
alice.emit_event(EventType::Test);
|
||||
|
||||
while let Some(ev) = alice.evtracker.recv().await {
|
||||
match ev.typ {
|
||||
EventType::IncomingReaction { .. } => {
|
||||
panic!("Alice is not supposed to receive a notification, since she blocked Bob")
|
||||
}
|
||||
EventType::Test => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing_undecryptable() -> Result<()> {
|
||||
let alice = &TestContext::new().await;
|
||||
@@ -5763,7 +5769,7 @@ pub(crate) async fn first_row_in_smtp_queue(context: &TestContext) -> (MsgId, St
|
||||
let queued_mail = context
|
||||
.sql
|
||||
.transaction_ext(query_only, |transaction| {
|
||||
smtp::load_queued_mail(transaction, rowid)
|
||||
smtp::queue::load_queued_mail(transaction, rowid)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user