Compare commits

..

1 Commits

Author SHA1 Message Date
Hocuri
b334603e27 [WIP] For testing, send statistics once every minute 2025-12-04 17:49:17 +01:00
93 changed files with 1306 additions and 3243 deletions

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.92.0
RUST_VERSION: 1.91.0
# Minimum Supported Rust Version
MSRV: 1.88.0
@@ -40,7 +40,7 @@ jobs:
- run: rustup override set $RUST_VERSION
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@v2
- name: Run rustfmt
run: cargo fmt --all -- --check
- name: Run clippy
@@ -59,7 +59,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918
- uses: EmbarkStudios/cargo-deny-action@v2
with:
arguments: --all-features --workspace
command: check
@@ -91,7 +91,7 @@ jobs:
show-progress: false
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@v2
- name: Rustdoc
run: cargo doc --document-private-items --no-deps
@@ -134,10 +134,10 @@ jobs:
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@v2
- name: Install nextest
uses: taiki-e/install-action@69e777b377e4ec209ddad9426ae3e0c1008b0ef3
uses: taiki-e/install-action@v2
with:
tool: nextest
@@ -168,13 +168,13 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@v2
- name: Build C library
run: cargo build -p deltachat_ffi
- name: Upload C library
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
@@ -194,13 +194,13 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@v2
- name: Build deltachat-rpc-server
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
@@ -243,7 +243,7 @@ jobs:
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: ubuntu-latest-libdeltachat.a
path: target/debug
@@ -293,7 +293,7 @@ jobs:
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
@@ -355,7 +355,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -34,13 +34,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
- name: Upload binary
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux
path: result/bin/deltachat-rpc-server
@@ -58,13 +58,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
- name: Upload wheel
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
path: result/*.whl
@@ -82,13 +82,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}
path: result/bin/deltachat-rpc-server.exe
@@ -106,13 +106,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
- name: Upload wheel
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
path: result/*.whl
@@ -139,7 +139,7 @@ jobs:
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
@@ -157,13 +157,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
- name: Upload binary
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android
path: result/bin/deltachat-rpc-server
@@ -181,13 +181,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
- name: Upload wheel
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
path: result/*.whl
@@ -208,124 +208,124 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux aarch64 wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-linux-wheel
path: deltachat-rpc-server-aarch64-linux-wheel.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv7l wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux-wheel
path: deltachat-rpc-server-armv7l-linux-wheel.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux armv6l wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux-wheel
path: deltachat-rpc-server-armv6l-linux-wheel.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux i686 wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux-wheel
path: deltachat-rpc-server-i686-linux-wheel.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Linux x86_64 wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux-wheel
path: deltachat-rpc-server-x86_64-linux-wheel.d
- name: Download Win32 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win32 wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32-wheel
path: deltachat-rpc-server-win32-wheel.d
- name: Download Win64 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download Win64 wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64-wheel
path: deltachat-rpc-server-win64-wheel.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android wheel for arm64-v8a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-arm64-v8a-android-wheel
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: Download Android wheel for armeabi-v7a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android-wheel
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
@@ -382,15 +382,12 @@ jobs:
- name: Publish deltachat-rpc-server to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
uses: pypa/gh-action-pypi-publish@release/v1
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
needs: ["build_linux", "build_windows", "build_macos"]
runs-on: "ubuntu-latest"
environment:
name: npm-stdio-rpc-server
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
permissions:
id-token: write
@@ -406,67 +403,67 @@ jobs:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -496,7 +493,7 @@ jobs:
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
@@ -518,14 +515,11 @@ jobs:
node-version: 20
registry-url: "https://registry.npmjs.org"
# Ensure npm 11.5.1 or later is installed.
# It is needed for <https://docs.npmjs.com/trusted-publishers>
- name: Update npm
run: npm install -g npm@latest
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
if: github.event_name == 'release'
working-directory: deltachat-rpc-server/npm-package
run: |
ls -lah platform_package
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,9 +10,6 @@ jobs:
pack-module:
name: "Publish @deltachat/jsonrpc-client"
runs-on: ubuntu-latest
environment:
name: npm-jsonrpc-client
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
permissions:
id-token: write
contents: read
@@ -27,11 +24,6 @@ jobs:
node-version: 20
registry-url: "https://registry.npmjs.org"
# Ensure npm 11.5.1 or later is installed.
# It is needed for <https://docs.npmjs.com/trusted-publishers>
- name: Update npm
run: npm install -g npm@latest
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
@@ -45,3 +37,5 @@ jobs:
- name: Publish
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -25,7 +25,7 @@ jobs:
with:
node-version: 18.x
- name: Add Rust cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: Swatinem/rust-cache@v2
- name: npm install
working-directory: deltachat-jsonrpc/typescript
run: npm install

View File

@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- run: nix fmt flake.nix -- --check
build:
@@ -84,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -105,5 +105,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- run: nix build .#${{ matrix.installable }}

View File

@@ -23,7 +23,7 @@ jobs:
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
@@ -42,9 +42,9 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v7
uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -18,11 +18,11 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: repl.exe
path: "result/bin/deltachat-repl.exe"

View File

@@ -36,7 +36,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -55,7 +55,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat

View File

@@ -19,7 +19,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif

View File

@@ -1,230 +1,5 @@
# Changelog
## [2.37.0] - 2026-01-08
### API-Changes
- JSON-RPC API `get_all_ui_config_keys` to get all "ui.*" config keys ([#7579](https://github.com/chatmail/core/pull/7579)).
- Add `who_can_call_me` config option.
- cffi api to create account manager with existing events channel to see events emitted during startup. `dc_event_channel_new`, `dc_event_channel_unref`, `dc_event_channel_get_event_emitter` and `dc_accounts_new_with_event_channel` ([#7609](https://github.com/chatmail/core/pull/7609)).
### Features / Changes
- Config option to skip seen synchronization ([#7694](https://github.com/chatmail/core/pull/7694)).
- More text instead of sender in channel summary.
### Fixes
- Do not rely on Secure-Join header to detect {vc,vg}-request.
### Documentation
- Update instructions to UI where to display the address.
### Miscellaneous Tasks
- cargo: bump rsa from 0.9.9 to 0.9.10.
- Update lru 0.12.3 to 0.12.5 and add RUSTSEC-2026-0002 exception.
### Refactor
- ffi: Replace implicit drop in cffi with explicit `drop(Arc::from_raw(var))` ([#7664](https://github.com/chatmail/core/pull/7664)).
### Tests
- Regression test for vc-request encrypted by the server.
- Test that channel summary does not have sender name.
## [2.36.0] - 2026-01-03
### CI
- Pin GitHub Action references.
### API-Changes
- Add transports event to FFI.
### Features / Changes
- Add core version to `receive_imf` failure message.
- Connectivity view: quota for all transports ([#7630](https://github.com/chatmail/core/pull/7630)).
- Send sync messages over SMTP and do not move them to mvbox.
### Fixes
- When accepting group, add members with `Origin::IncomingTo` and sort them down in the contact list (7592).
- Update fallback welcome message.
- `inner_configure`: Check Config::OnlyFetchMvbox before MvboxMove for multi-transport ([#7637](https://github.com/chatmail/core/pull/7637)).
- Reset options not available for chatmail on chatmail profiles.
- Don't send webxdc notification for `notify: "*"` when chat is muted ([#7658](https://github.com/chatmail/core/pull/7658)).
### Documentation
- `delete_chat()`: don't lie that messages aren't deleted from server.
- Remove references to removed `sentbox_watch` config.
- Update documentation for `TransportsModified` event.
### Tests
- Contact list after accepting group with unknown contacts ([#7592](https://github.com/chatmail/core/pull/7592)).
- Port test_import_export_online_all to JSON-RPC ([#7411](https://github.com/chatmail/core/pull/7411)).
### Refactor
- Turn `DC_VERSION_STR` into `&str`.
- ffi: Remove one pointer indirection for `dc_accounts_t`.
### Miscellaneous Tasks
- deps: Bump actions/download-artifact from 6 to 7.
- deps: Bump actions/upload-artifact from 5 to 6.
- deps: Bump astral-sh/setup-uv from 7.1.4 to 7.1.6.
- deps: Bump cachix/install-nix-action from 31.8.4 to 31.9.0.
- cargo: Bump serde_json from 1.0.145 to 1.0.147.
- cargo: Bump uuid from 1.18.1 to 1.19.0.
- cargo: Bump toml from 0.9.8 to 0.9.10+spec-1.1.0.
- cargo: Bump tempfile from 3.23.0 to 3.24.0.
- cargo: Bump libc from 0.2.177 to 0.2.178.
- cargo: Bump tracing from 0.1.41 to 0.1.44.
- cargo: Bump hyper-util from 0.1.18 to 0.1.19.
- cargo: Bump log from 0.4.28 to 0.4.29.
- cargo: Bump rustls-pki-types from 1.13.0 to 1.13.2.
- cargo: Bump criterion from 0.7.0 to 0.8.1.
## [2.35.0] - 2025-12-16
### API-Changes
- Add blob dir size to storage info ([#7605](https://github.com/chatmail/core/pull/7605)).
### Features / Changes
- Use `turn.delta.chat` as fallback TURN server ([#7382](https://github.com/chatmail/core/pull/7382)).
- Add ip addresses of known public chatmail relays from https://chatmail.at/relays to DNS cache ([#7607](https://github.com/chatmail/core/pull/7607)).
- Improve error messages on adding relays.
- Add transport addresses to IMAP URLs in message info.
- `lookup_host_with_cache()`: Don't return empty address list ([#7596](https://github.com/chatmail/core/pull/7596)).
### Fixes
- `get_chat_msgs_ex()`: Don't match on "S=" (Cmd) in param payload.
- Remove `SecurejoinWait` info message when received Alice's key ([#7585](https://github.com/chatmail/core/pull/7585)).
- Do not set normalized name for existing chats and contacts in a migration.
- Remove now redundant "used_account_settings" and "entered_account_settings" from `Context.get_info()` ([#7587](https://github.com/chatmail/core/pull/7587)).
- Don't use fallback servers if got TURN servers from IMAP METADATA.
- Use fallback ICE servers if server can't IMAP METADATA ([#7382](https://github.com/chatmail/core/pull/7382)).
- Add explicit limit for adding relays (5 at the moment) ([#7611](https://github.com/chatmail/core/pull/7611)).
- Take `transport_id` into account when using `imap` table.
### CI
- Update Rust to 1.92.0.
### Miscellaneous Tasks
- Apply Rust 1.92.0 clippy suggestions.
### Other
- Log entered login params and actual used params on configuration failure ([#7610](https://github.com/chatmail/core/pull/7610)).
## [2.34.0] - 2025-12-11
### API-Changes
- rpc-client: Accept `Account` for `Chat.{add,remove}_contact()`.
- rpc-client: Add `Chat.num_contacts()`.
- Forwarding messages to another profile ([#7491](https://github.com/chatmail/core/pull/7491)).
### Features / Changes
- Double ringing time to 120 seconds.
- Better logging for failing securejoin messages ([#7593](https://github.com/chatmail/core/pull/7593)).
- Add multi-transport information to `Context.get_info` ([#7583](https://github.com/chatmail/core/pull/7583))
### Fixes
- Multi-transport: all transports were shown as "inbox" in connectivity view, now they are shown by their hostname ([#7582](https://github.com/chatmail/core/pull/7582)).
- Multi-transport: Synchronize primary transport immediately after changing it.
- Use u64 instead of usize to calculate storage usage.
- Use u64 to represent the number of bytes in backup files.
- Use u64 to count the number of bytes sent/received over the network.
- Use logging macros instead of emitting event directly, so that it is also logged by tracing ([#7459](https://github.com/chatmail/core/pull/7459)).
- Let securejoin succeed even if the chat was deleted in the meantime ([#7594](https://github.com/chatmail/core/pull/7594)).
### Miscellaneous Tasks
- Add RUSTSEC-2025-0134 exception to deny.toml.
### Refactor
- Use u16 instead of usize to represent progress bar.
- Remove EncryptHelper.prefer_encrypt.
- Add params when forwarding message instead of removing unneeded ones.
### Tests
- Port test_synchronize_member_list_on_group_rejoin to JSON-RPC.
- Test setting up second device between core versions.
## [2.33.0] - 2025-12-05
### Features / Changes
- Case-insensitive search for non-ASCII chat and contact names ([#7477](https://github.com/chatmail/core/pull/7477)).
### Fixes
- Recognize all transport addresses as own addresses.
## [2.32.0] - 2025-12-04
Version bump to trigger publishing of npm prebuilds
that failed to be published for 2.31.0 due to not configured "trusted publishers".
### Features / Changes
- Lookup_or_create_adhoc_group(): Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
## [2.31.0] - 2025-12-04
### CI
- Update npm before publishing packages.
### Features / Changes
- Use v2 SEIPD when sending messages to self.
## [2.30.0] - 2025-12-04
### Features / Changes
- Disable SNI for STARTTLS ([#7499](https://github.com/chatmail/core/pull/7499)).
- Introduce cross-core testing along with improvements to test frameworking.
- Synchronize transports via sync messages.
### Fixes
- Fix shutdown shortly after call.
### API-Changes
- Add `TransportsModified` event (for tests).
### CI
- Use "trusted publishing" for NPM packages.
### Miscellaneous Tasks
- deps: Bump actions/checkout from 5 to 6.
- cargo: Bump syn from 2.0.110 to 2.0.111.
- deps: Bump astral-sh/setup-uv from 7.1.3 to 7.1.4.
- cargo: Bump sdp from 0.8.0 to 0.10.0.
- Remove two outdated todo comments ([#7550](https://github.com/chatmail/core/pull/7550)).
## [2.29.0] - 2025-12-01
### API-Changes
@@ -7535,11 +7310,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0
[2.29.0]: https://github.com/chatmail/core/compare/v2.28.0..v2.29.0
[2.30.0]: https://github.com/chatmail/core/compare/v2.29.0..v2.30.0
[2.31.0]: https://github.com/chatmail/core/compare/v2.30.0..v2.31.0
[2.32.0]: https://github.com/chatmail/core/compare/v2.31.0..v2.32.0
[2.33.0]: https://github.com/chatmail/core/compare/v2.32.0..v2.33.0
[2.34.0]: https://github.com/chatmail/core/compare/v2.33.0..v2.34.0
[2.35.0]: https://github.com/chatmail/core/compare/v2.34.0..v2.35.0
[2.36.0]: https://github.com/chatmail/core/compare/v2.35.0..v2.36.0
[2.37.0]: https://github.com/chatmail/core/compare/v2.36.0..v2.37.0

174
Cargo.lock generated
View File

@@ -62,6 +62,18 @@ dependencies = [
"aes",
]
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -86,15 +98,6 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -1039,11 +1042,10 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.8.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
@@ -1052,7 +1054,6 @@ dependencies = [
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
@@ -1065,9 +1066,9 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.8.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
dependencies = [
"cast",
"itertools",
@@ -1303,7 +1304,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.37.0"
version = "2.29.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1387,7 +1388,6 @@ dependencies = [
"tracing",
"url",
"uuid",
"walkdir",
"webpki-roots",
]
@@ -1413,7 +1413,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.37.0"
version = "2.29.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1429,12 +1429,13 @@ dependencies = [
"tempfile",
"tokio",
"typescript-type-def",
"walkdir",
"yerpc",
]
[[package]]
name = "deltachat-repl"
version = "2.37.0"
version = "2.29.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1450,7 +1451,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.37.0"
version = "2.29.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1479,7 +1480,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.37.0"
version = "2.29.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1996,7 +1997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix 1.1.3",
"rustix 1.0.5",
"windows-sys 0.59.0",
]
@@ -2366,14 +2367,22 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashbrown"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@@ -2383,7 +2392,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown",
"hashbrown 0.15.4",
]
[[package]]
@@ -2639,9 +2648,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.19"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
dependencies = [
"bytes",
"futures-channel",
@@ -2900,7 +2909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.4",
]
[[package]]
@@ -3164,7 +3173,7 @@ dependencies = [
"iroh-metrics",
"iroh-quinn",
"iroh-quinn-proto",
"lru 0.12.5",
"lru 0.12.3",
"n0-future",
"num_enum",
"pin-project",
@@ -3258,9 +3267,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.178"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libm"
@@ -3308,9 +3317,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
[[package]]
name = "litemap"
@@ -3335,9 +3344,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.29"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "loom"
@@ -3354,11 +3363,11 @@ dependencies = [
[[package]]
name = "lru"
version = "0.12.5"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
dependencies = [
"hashbrown",
"hashbrown 0.14.5",
]
[[package]]
@@ -3759,10 +3768,11 @@ dependencies = [
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
@@ -4013,16 +4023,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -5050,9 +5050,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.10"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
@@ -5127,15 +5127,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.1.3"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.61.1",
"linux-raw-sys 0.9.3",
"windows-sys 0.59.0",
]
[[package]]
@@ -5164,9 +5164,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.13.2"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [
"web-time",
"zeroize",
@@ -5422,22 +5422,22 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.148"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "1.0.4"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
dependencies = [
"serde_core",
]
@@ -5947,14 +5947,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.24.0"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.3",
"rustix 1.0.5",
"windows-sys 0.61.1",
]
@@ -6217,14 +6217,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.10+spec-1.1.0"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_datetime 0.7.3",
"toml_parser",
"toml_writer",
"winnow 0.7.13",
@@ -6238,9 +6238,9 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
dependencies = [
"serde_core",
]
@@ -6270,9 +6270,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
dependencies = [
"winnow 0.7.13",
]
@@ -6285,9 +6285,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.6+spec-1.1.0"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "tower"
@@ -6318,9 +6318,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log",
"pin-project-lite",
@@ -6330,9 +6330,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.31"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
@@ -6341,9 +6341,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.36"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
@@ -6542,13 +6542,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.19.0"
version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"serde_core",
"serde",
"wasm-bindgen",
]
@@ -7465,12 +7465,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
[[package]]
name = "zmij"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f"
[[package]]
name = "zune-core"
version = "0.5.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.37.0"
version = "2.29.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
@@ -111,12 +111,11 @@ toml = "0.9"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
walkdir = "2.5.0"
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"] }
criterion = { version = "0.7.0", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -199,7 +198,7 @@ rusqlite = "0.37"
sanitize-filename = "0.6"
serde = "1.0"
serde_json = "1"
tempfile = "3.24.0"
tempfile = "3.23.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.17"

View File

@@ -16,8 +16,7 @@ id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await
.context("CREATE TABLE messages")?;
.await?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
@@ -30,8 +29,7 @@ id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await
.context("CREATE TABLE messages")?;
.await?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
@@ -65,9 +63,6 @@ an older version. Also don't change the column type, consider adding a new colum
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
## Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.

View File

@@ -38,7 +38,7 @@ use deltachat::{
internals_for_benches::key_from_asc,
internals_for_benches::parse_and_get_text,
internals_for_benches::store_self_keypair,
pgp::{KeyPair, SeipdVersion, decrypt, pk_encrypt, symm_encrypt_message},
pgp::{KeyPair, decrypt, pk_encrypt, symm_encrypt_message},
stock_str::StockStrings,
};
use rand::{Rng, rng};
@@ -111,7 +111,6 @@ fn criterion_benchmark(c: &mut Criterion) {
key_pair.secret.clone(),
true,
true,
SeipdVersion::V2,
)
.await
.unwrap()

View File

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

View File

@@ -22,7 +22,6 @@ typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_event_channel dc_event_channel_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
@@ -430,13 +429,16 @@ char* dc_get_blobdir (const dc_context_t* context);
* 1=send a copy of outgoing messages to self (default).
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
* 0=do not watch the `Sent`-folder (default).
* - `mvbox_move` = 1=detect chat messages,
* move them to the `DeltaChat` folder,
* and watch the `DeltaChat` folder for updates (default),
* 0=do not move chat-messages
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
* `DeltaChat` folder. Messages will still be fetched from the
* spam folder.
* spam folder and `sendbox_watch` will also still be respected
* if enabled.
* 0=watch all folders normally (default)
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only,
@@ -517,10 +519,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop.
* 1 = WebXDC realtime API is enabled (default).
* - `who_can_call_me` = Who can cause call notifications.
* 0 = Everybody (except explicitly blocked contacts),
* 1 = Contacts (default, does not include contact requests),
* 2 = Nobody (calls never result in a notification).
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -1613,10 +1611,10 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
*
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
* Messages are deleted from the server in background.
*
* Things that are _not_ done implicitly:
*
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear
* and the user may create the chat again.
* - **Groups are not left** - this would
@@ -3094,7 +3092,7 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
/**
* Create a new account manager.
* The account manager takes a directory
* The account manager takes an directory
* where all context-databases are placed in.
* To add a context to the account manager,
* use dc_accounts_add_account() or dc_accounts_migrate_account().
@@ -3116,35 +3114,6 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
*/
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
/**
* Create a new account manager with an existing events channel,
* which allows you to see events emitted during startup.
*
* The account manager takes a directory
* where all context-databases are placed in.
* To add a context to the account manager,
* use dc_accounts_add_account() or dc_accounts_migrate_account().
* All account information are persisted.
* To remove a context from the account manager,
* use dc_accounts_remove_account().
*
* @memberof dc_accounts_t
* @param dir The directory to create the context-databases in.
* If the directory does not exist,
* dc_accounts_new_with_event_channel() will try to create it.
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
* @param dc_event_channel_t Events Channel to be used for this accounts manager,
* create one with dc_event_channel_new().
* This channel is consumed by this method and can not be used again afterwards,
* so be sure to call `dc_event_channel_get_event_emitter` before.
* @return An account manager object.
* The object must be passed to the other account manager functions
* and must be freed using dc_accounts_unref() after usage.
* On errors, NULL is returned.
*/
dc_accounts_t* dc_accounts_new_with_event_channel(const char* dir, int writable, dc_event_channel_t* events_channel);
/**
* Free an account manager object.
@@ -3385,12 +3354,8 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
* Having more than one event emitter running at the same time on the same account manager
* will result in events randomly delivered to the one or to the other.
*/
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
@@ -5363,8 +5328,8 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
*
* - If dc_contact_get_verifier_id() != 0,
* display text "Introduced by ..."
* with the name of the contact
* formatted by dc_contact_get_name().
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
@@ -6034,62 +5999,6 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
/**
* @class dc_event_channel_t
*
* Opaque object that is used to create an event emitter which can be used log events during startup of an accounts manger.
* Only used for dc_accounts_new_with_event_channel().
* To use it:
* 1. create an events channel with `dc_event_channel_new()`.
* 2. get an event emitter for it with `dc_event_channel_get_event_emitter()`.
* 3. use it to create your account manager with `dc_accounts_new_with_event_channel()`, which consumes the channel.
* 4. free the empty channel wrapper object with `dc_event_channel_unref()`.
*/
/**
* Create a new event channel.
*
* @memberof dc_event_channel_t
* @return An event channel wrapper object (dc_event_channel_t).
*/
dc_event_channel_t* dc_event_channel_new();
/**
* Release/free the events channel structure.
* This function releases the memory of the `dc_event_channel_t` structure.
*
* you can call it after calling dc_accounts_new_with_event_channel,
* which took the events channel out of it already, so this just frees the underlying option.
*
* @memberof dc_event_channel_t
*/
void dc_event_channel_unref(dc_event_channel_t* event_channel);
/**
* Create the event emitter that is used to receive events.
*
* The library will emit various @ref DC_EVENT events, such as "new message", "message read" etc.
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
*
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
*
* @memberof dc_event_channel_t
* @param The event channel.
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager / event channel.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
*/
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);
/**
* @class dc_event_emitter_t
*
@@ -6793,16 +6702,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CALL_ENDED 2580
/**
* Transport relay added/deleted or default has changed.
* UI should update the list.
*
* The event is emitted when the transports are modified on another device
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`
* or `set_config(configured_addr)`.
*/
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
/**
* @}
@@ -7405,6 +7304,12 @@ void dc_event_unref(dc_event_t* event);
/// Used as a headline in the connectivity view.
#define DC_STR_OUTGOING_MESSAGES 104
/// "Storage on %1$s"
///
/// Used as a headline in the connectivity view.
///
/// `%1$s` will be replaced by the domain of the configured e-mail address.
#define DC_STR_STORAGE_ON_DOMAIN 105
/// @deprecated Deprecated 2022-04-16, this string is no longer needed.
#define DC_STR_ONE_MOMENT 106
@@ -7467,7 +7372,8 @@ void dc_event_unref(dc_event_t* event);
/// May be followed by the info-messages
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
///
/// `%1$s` and `%2$s` will be replaced by name of the inviter.
/// `%1$s` will be replaced by name and address of the inviter,
/// `%2$s` will be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_STARTED 117
/// "%1$s replied, waiting for being added to the group…"
@@ -7484,7 +7390,7 @@ void dc_event_unref(dc_event_t* event);
///
/// Subtitle for verification qrcode svg image generated by the core.
///
/// `%1$s` will be replaced by name of the inviter.
/// `%1$s` will be replaced by name and address of the inviter.
#define DC_STR_SETUP_CONTACT_QR_DESC 119
/// "Scan to join %1$s"
@@ -7515,7 +7421,7 @@ void dc_event_unref(dc_event_t* event);
///
/// `%1$s` will be replaced by the old group name.
/// `%2$s` will be replaced by the new group name.
/// `%3$s` will be replaced by name of the contact who did the action.
/// `%3$s` will be replaced by name and address of the contact who did the action.
#define DC_STR_GROUP_NAME_CHANGED_BY_OTHER 125
/// "You changed the group image."
@@ -7523,7 +7429,7 @@ void dc_event_unref(dc_event_t* event);
/// "Group image changed by %1$s."
///
/// `%1$s` will be replaced by name of the contact who did the action.
/// `%1$s` will be replaced by name and address of the contact who did the action.
#define DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER 127
/// "You added member %1$s."
@@ -7535,23 +7441,23 @@ void dc_event_unref(dc_event_t* event);
/// "Member %1$s added by %2$s."
///
/// `%1$s` will be replaced by name of the contact added to the group.
/// `%2$s` will be replaced by name of the contact who did the action.
/// `%1$s` will be replaced by name and address of the contact added to the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_ADD_MEMBER_BY_OTHER 129
/// "You removed member %1$s."
///
/// `%1$s` will be replaced by name of the contact removed from the group.
/// `%1$s` will be replaced by name and address of the contact removed from the group.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_YOU 130
/// "Member %1$s removed by %2$s."
///
/// `%1$s` will be replaced by name of the contact removed from the group.
/// `%2$s` will be replaced by name of the contact who did the action.
/// `%1$s` will be replaced by name and address of the contact removed from the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
@@ -7563,7 +7469,7 @@ void dc_event_unref(dc_event_t* event);
/// "Group left by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_OTHER 133
@@ -7575,7 +7481,7 @@ void dc_event_unref(dc_event_t* event);
/// "Group image deleted by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_IMAGE_DELETED_BY_OTHER 135
@@ -7587,7 +7493,7 @@ void dc_event_unref(dc_event_t* event);
/// "Location streaming enabled by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_LOCATION_ENABLED_BY_OTHER 137
@@ -7599,7 +7505,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is disabled by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER 139
@@ -7614,7 +7520,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to %1$s s by %2$s."
///
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
/// `%2$s` will be replaced by name of the contact.
/// `%2$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER 141
@@ -7626,7 +7532,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 minute by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
/// @deprecated 2025-11-14, this string is no longer needed
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
@@ -7637,7 +7543,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 hour by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER 145
@@ -7649,7 +7555,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 day by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER 147
@@ -7661,7 +7567,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 week by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER 149
@@ -7678,7 +7584,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
/// `%2$s` will be replaced by name of the contact.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER 151
/// "You set message deletion timer to %1$s hours."
@@ -7693,7 +7599,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
/// `%2$s` will be replaced by name of the contact.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER 153
/// "You set message deletion timer to %1$s days."
@@ -7708,7 +7614,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
/// `%2$s` will be replaced by name of the contact.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER 155
/// "You set message deletion timer to %1$s weeks."
@@ -7723,7 +7629,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
/// `%2$s` will be replaced by name of the contact.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You set message deletion timer to 1 year."
@@ -7733,14 +7639,14 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 year by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name of the account.
/// `%1$s` will be replaced by name and address of the account.
#define DC_STR_BACKUP_TRANSFER_QR 162
/// "Account transferred to your second device."

View File

@@ -15,9 +15,10 @@ use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::{Arc, LazyLock, Mutex};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
@@ -558,7 +559,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::IncomingCallAccepted { .. } => 2560,
EventType::OutgoingCallAccepted { .. } => 2570,
EventType::CallEnded { .. } => 2580,
EventType::TransportsModified => 2600,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -593,8 +593,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::TransportsModified => 0,
| EventType::AccountsItemChanged => 0,
EventType::IncomingReaction { contact_id, .. }
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
@@ -682,8 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingCallAccepted { .. }
| EventType::OutgoingCallAccepted { .. }
| EventType::CallEnded { .. }
| EventType::EventChannelOverflow { .. }
| EventType::TransportsModified => 0,
| EventType::EventChannelOverflow { .. } => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
@@ -782,8 +780,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::IncomingCallAccepted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::TransportsModified => ptr::null_mut(),
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
EventType::IncomingCall {
place_call_info, ..
} => {
@@ -4738,13 +4735,33 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
/// `dc_accounts_t` in multiple threads at once.
pub type dc_accounts_t = RwLock<Accounts>;
pub struct AccountsWrapper {
inner: Arc<RwLock<Accounts>>,
}
impl Deref for AccountsWrapper {
type Target = Arc<RwLock<Accounts>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = Arc::new(RwLock::new(accounts));
Self { inner }
}
}
/// Struct representing a list of deltachat accounts.
pub type dc_accounts_t = AccountsWrapper;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new(
dir: *const libc::c_char,
writable: libc::c_int,
) -> *const dc_accounts_t {
) -> *mut dc_accounts_t {
setup_panic!();
if dir.is_null() {
@@ -4755,99 +4772,7 @@ pub unsafe extern "C" fn dc_accounts_new(
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
match accs {
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
Err(err) => {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
eprintln!("failed to create accounts: {err:#}");
ptr::null_mut()
}
}
}
pub type dc_event_channel_t = Mutex<Option<Events>>;
#[no_mangle]
pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t {
Box::into_raw(Box::new(Mutex::new(Some(Events::new()))))
}
/// Release the events channel structure.
///
/// This function releases the memory of the `dc_event_channel_t` structure.
///
/// you can call it after calling dc_accounts_new_with_event_channel,
/// which took the events channel out of it already, so this just frees the underlying option.
#[no_mangle]
pub unsafe extern "C" fn dc_event_channel_unref(event_channel: *mut dc_event_channel_t) {
if event_channel.is_null() {
eprintln!("ignoring careless call to dc_event_channel_unref()");
return;
}
drop(Box::from_raw(event_channel))
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_channel_get_event_emitter(
event_channel: *mut dc_event_channel_t,
) -> *mut dc_event_emitter_t {
if event_channel.is_null() {
eprintln!("ignoring careless call to dc_event_channel_get_event_emitter()");
return ptr::null_mut();
}
let Some(event_channel) = &*(*event_channel)
.lock()
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
else {
eprintln!(
"ignoring careless call to dc_event_channel_get_event_emitter()
-> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel"
);
return ptr::null_mut();
};
let emitter = event_channel.get_emitter();
Box::into_raw(Box::new(emitter))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new_with_event_channel(
dir: *const libc::c_char,
writable: libc::c_int,
event_channel: *mut dc_event_channel_t,
) -> *const dc_accounts_t {
setup_panic!();
if dir.is_null() || event_channel.is_null() {
eprintln!("ignoring careless call to dc_accounts_new_with_event_channel()");
return ptr::null_mut();
}
// consuming channel enforce that you need to get the event emitter
// before initializing the account manager,
// so that you don't miss events/errors during initialisation.
// It also prevents you from using the same channel on multiple account managers.
let Some(event_channel) = (*event_channel)
.lock()
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
.take()
else {
eprintln!(
"ignoring careless call to dc_accounts_new_with_event_channel()
-> channel was already consumed"
);
return ptr::null_mut();
};
let accs = block_on(Accounts::new_with_events(
as_path(dir).into(),
writable != 0,
event_channel,
));
match accs {
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
Err(err) => {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
eprintln!("failed to create accounts: {err:#}");
@@ -4860,17 +4785,17 @@ pub unsafe extern "C" fn dc_accounts_new_with_event_channel(
///
/// This function releases the memory of the `dc_accounts_t` structure.
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_unref(accounts: *const dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_unref(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_unref()");
return;
}
drop(Arc::from_raw(accounts));
let _ = Box::from_raw(accounts);
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_account(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
id: u32,
) -> *mut dc_context_t {
if accounts.is_null() {
@@ -4887,7 +4812,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_selected_account(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
) -> *mut dc_context_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
@@ -4903,7 +4828,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_select_account(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
id: u32,
) -> libc::c_int {
if accounts.is_null() {
@@ -4927,13 +4852,13 @@ pub unsafe extern "C" fn dc_accounts_select_account(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *const dc_accounts_t) -> u32 {
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_account()");
return 0;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
block_on(async move {
let mut accounts = accounts.write().await;
@@ -4948,13 +4873,13 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *const dc_accounts_t)
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *const dc_accounts_t) -> u32 {
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
return 0;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
block_on(async move {
let mut accounts = accounts.write().await;
@@ -4970,7 +4895,7 @@ pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *const dc_acco
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_remove_account(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
id: u32,
) -> libc::c_int {
if accounts.is_null() {
@@ -4978,7 +4903,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
return 0;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
block_on(async move {
let mut accounts = accounts.write().await;
@@ -4996,7 +4921,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_migrate_account(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
dbfile: *const libc::c_char,
) -> u32 {
if accounts.is_null() || dbfile.is_null() {
@@ -5004,7 +4929,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
return 0;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
let dbfile = to_string_lossy(dbfile);
block_on(async move {
@@ -5025,7 +4950,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *const dc_accounts_t) -> *mut dc_array_t {
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *mut dc_array_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_all()");
return ptr::null_mut();
@@ -5039,18 +4964,18 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *const dc_accounts_t) ->
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *const dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_start_io()");
return;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
block_on(async move { accounts.write().await.start_io().await });
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *const dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_stop_io()");
return;
@@ -5061,7 +4986,7 @@ pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *const dc_accounts_t) {
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *const dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_maybe_network()");
return;
@@ -5072,7 +4997,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *const dc_accounts_
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *const dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_maybe_network_lost()");
return;
@@ -5084,7 +5009,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *const dc_acco
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_background_fetch(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
timeout_in_seconds: u64,
) -> libc::c_int {
if accounts.is_null() || timeout_in_seconds <= 2 {
@@ -5103,7 +5028,7 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *const dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_stop_background_fetch()");
return;
@@ -5115,7 +5040,7 @@ pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *const dc_a
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
token: *const libc::c_char,
) {
if accounts.is_null() {
@@ -5138,7 +5063,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *const dc_accounts_t,
accounts: *mut dc_accounts_t,
) -> *mut dc_event_emitter_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
@@ -5158,16 +5083,16 @@ pub struct dc_jsonrpc_instance_t {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *const dc_accounts_t,
account_manager: *mut dc_accounts_t,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let account_manager = Arc::from_raw(account_manager);
let account_manager = &*account_manager;
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
account_manager.clone(),
account_manager.inner.clone(),
));
let (request_handle, receiver) = RpcClient::new();

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.37.0"
version = "2.29.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
@@ -19,6 +19,7 @@ yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
tokio = { workspace = true }
sanitize-filename = { workspace = true }
walkdir = "2.5.0"
base64 = { workspace = true }
[dev-dependencies]

View File

@@ -10,12 +10,11 @@ pub use deltachat::accounts::Accounts;
use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem,
MessageListOptions,
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::{get_all_ui_config_keys, Config};
use deltachat::config::Config;
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
@@ -35,13 +34,14 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
use deltachat::storage_usage::get_storage_usage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use types::login_param::EnteredLoginParam;
use walkdir::WalkDir;
use yerpc::rpc;
pub mod types;
@@ -329,7 +329,13 @@ impl CommandApi {
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
let ctx = self.get_context(account_id).await?;
let dbfile = ctx.get_dbfile().metadata()?.len();
let total_size = get_blobdir_storage_usage(&ctx);
let total_size = WalkDir::new(ctx.get_blobdir())
.max_depth(2)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len());
Ok(dbfile + total_size)
}
@@ -452,12 +458,6 @@ impl CommandApi {
Ok(result)
}
/// Returns all `ui.*` config keys that were set by the UI.
async fn get_all_ui_config_keys(&self, account_id: u32) -> Result<Vec<String>> {
let ctx = self.get_context(account_id).await?;
get_all_ui_config_keys(&ctx).await
}
async fn set_stock_strings(&self, strings: HashMap<u32, String>) -> Result<()> {
let accounts = self.accounts.read().await;
for (stock_id, stock_message) in strings {
@@ -801,11 +801,11 @@ impl CommandApi {
/// Delete a chat.
///
/// Messages are deleted from the device and the chat database entry is deleted.
/// After that, a `MsgsChanged` event is emitted.
/// Messages are deleted from the server in background.
/// After that, the event #DC_EVENT_MSGS_CHANGED is posted.
///
/// Things that are _not done_ implicitly:
///
/// - Messages are **not deleted from the server**.
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
/// and the user may create the chat again.
/// - **Groups are not left** - this would
@@ -2208,27 +2208,6 @@ impl CommandApi {
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
/// Forward messages to a chat in another account.
/// See [`Self::forward_messages`] for more info.
async fn forward_messages_to_account(
&self,
src_account_id: u32,
src_message_ids: Vec<u32>,
dst_account_id: u32,
dst_chat_id: u32,
) -> Result<()> {
let src_ctx = self.get_context(src_account_id).await?;
let dst_ctx = self.get_context(dst_account_id).await?;
let src_message_ids: Vec<MsgId> = src_message_ids.into_iter().map(MsgId::new).collect();
forward_msgs_2ctx(
&src_ctx,
&src_message_ids,
&dst_ctx,
ChatId::new(dst_chat_id),
)
.await
}
/// Resend messages and make information available for newly added chat members.
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
/// Clients that already have the original message can still ignore the resent message as

View File

@@ -47,7 +47,8 @@ pub struct ContactObject {
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name of the contact.
/// with the name and address of the contact
/// formatted by `name_and_addr`/`nameAndAddr`.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,

View File

@@ -271,7 +271,7 @@ pub enum EventType {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: u16,
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
@@ -282,7 +282,7 @@ pub enum EventType {
#[serde(rename_all = "camelCase")]
ImexProgress {
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: u16,
progress: usize,
},
/// A file has been exported. A file has been written by imex().
@@ -313,7 +313,7 @@ pub enum EventType {
chat_id: u32,
/// Progress, always 1000.
progress: u16,
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
@@ -329,7 +329,7 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received
progress: u16,
progress: usize,
},
/// The connectivity to the server changed.
@@ -460,15 +460,6 @@ pub enum EventType {
/// ID of the chat which the message belongs to.
chat_id: u32,
},
/// One or more transports has changed.
///
/// UI should update the list.
///
/// This event is emitted when transport
/// synchronization messages arrives,
/// but not when the UI modifies the transport list by itself.
TransportsModified,
}
impl From<CoreEventType> for EventType {
@@ -651,8 +642,6 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
CoreEventType::TransportsModified => TransportsModified,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.37.0"
"version": "2.29.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.37.0"
version = "2.29.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -430,12 +430,12 @@ async fn handle_cmd(
}
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
if let Some(oauth2_url) =
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
{
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
} else {
let oauth2_url =
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
if oauth2_url.is_none() {
println!("OAuth2 not available for {}.", &addr);
} else {
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap());
}
} else {
println!("oauth2: set addr first.");

View File

@@ -30,15 +30,6 @@ $ pip install .
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
## Activating current checkout of deltachat-rpc-client and -server for development
Go to root repository directory and run:
```
$ scripts/make-rpc-testenv.sh
$ source venv/bin/activate
```
## Using in REPL
Setup a development environment:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.37.0"
version = "2.29.0"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [

View File

@@ -219,12 +219,10 @@ class Chat:
"""Mark all messages in this chat as noticed."""
self._rpc.marknoticed_chat(self.account.id, self.id)
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group."""
from .account import Account
for cnt in contact:
if isinstance(cnt, (str, Account)):
if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
@@ -232,12 +230,10 @@ class Chat:
contact_id = cnt
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
def remove_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
from .account import Account
for cnt in contact:
if isinstance(cnt, (str, Account)):
if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
@@ -253,10 +249,6 @@ class Chat:
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]
def num_contacts(self) -> int:
"""Return number of contacts in this chat."""
return len(self.get_contacts())
def get_past_contacts(self) -> list[Contact]:
"""Get past contacts for this chat."""
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)

View File

@@ -80,7 +80,6 @@ class EventType(str, Enum):
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
TRANSPORTS_MODIFIED = "TransportsModified"
class ChatId(IntEnum):

View File

@@ -3,14 +3,9 @@
from __future__ import annotations
import os
import pathlib
import platform
import random
import subprocess
import sys
from typing import AsyncGenerator, Optional
import execnet
import py
import pytest
@@ -25,18 +20,6 @@ Currently this is "End-to-end encryption available".
"""
def pytest_report_header():
for base in os.get_exec_path():
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
if fn.exists():
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
proc.wait()
version = proc.stderr.read().decode().strip()
return f"deltachat-rpc-server: {fn} [{version}]"
return None
class ACFactory:
"""Test account factory."""
@@ -214,134 +197,3 @@ def log():
print(" " + msg)
return Printer()
#
# support for testing against different deltachat-rpc-server/clients
# installed into a temporary virtualenv and connected via 'execnet' channels
#
def find_path(venv, name):
is_windows = platform.system() == "Windows"
bin = venv / ("bin" if not is_windows else "Scripts")
tryadd = [""]
if is_windows:
tryadd += os.environ["PATHEXT"].split(os.pathsep)
for ext in tryadd:
p = bin.joinpath(name + ext)
if p.exists():
return str(p)
return None
@pytest.fixture(scope="session")
def get_core_python_env(tmp_path_factory):
"""Return a factory to create virtualenv environments with rpc server/client packages
installed.
The factory takes a version and returns a (python_path, rpc_server_path) tuple
of the respective binaries in the virtualenv.
"""
envs = {}
def get_versioned_venv(core_version):
venv = envs.get(core_version)
if not venv:
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
subprocess.check_call([sys.executable, "-m", "venv", venv])
python = find_path(venv, "python")
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"]
subprocess.check_call([python, "-m", "pip", "install"] + pkgs)
envs[core_version] = venv
python = find_path(venv, "python")
rpc_server_path = find_path(venv, "deltachat-rpc-server")
print(f"python={python}\nrpc_server={rpc_server_path}")
return python, rpc_server_path
return get_versioned_venv
@pytest.fixture
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
The 'eval' function allows to remote-execute arbitrary expressions
that can use the `bob` online account, and the `bob_contact_alice`.
"""
def factory(core_version):
python, rpc_server_path = get_core_python_env(core_version)
gw = execnet.makegateway(f"popen//python={python}")
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
channel = gw.remote_exec(remote_bob_loop)
cm = os.environ.get("CHATMAIL_DOMAIN")
# trigger getting an online account on bob's side
channel.send((accounts_dir, str(rpc_server_path), cm))
# meanwhile get a local alice account
alice = acfactory.get_online_account()
channel.send(alice.self_contact.make_vcard())
# wait for bob to have started
sysinfo = channel.receive()
assert sysinfo == f"v{core_version}"
bob_vcard = channel.receive()
[alice_contact_bob] = alice.import_vcard(bob_vcard)
def eval(eval_str):
channel.send(eval_str)
return channel.receive()
return alice, alice_contact_bob, eval
return factory
def remote_bob_loop(channel):
# This function executes with versioned
# deltachat-rpc-client/server packages
# installed into the virtualenv.
#
# The "channel" argument is a send/receive pipe
# to the process that runs the corresponding remote_exec(remote_bob_loop)
import os
from deltachat_rpc_client import DeltaChat, Rpc
from deltachat_rpc_client.pytestplugin import ACFactory
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
# older core versions don't support specifying rpc_server_path
# so we can't just pass `rpc_server_path` argument to Rpc constructor
basepath = os.path.dirname(rpc_server_path)
os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]])
rpc = Rpc(accounts_dir=accounts_dir)
with rpc:
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
acfactory = ACFactory(dc)
bob = acfactory.get_online_account()
alice_vcard = channel.receive()
[alice_contact] = bob.import_vcard(alice_vcard)
ns = {"bob": bob, "bob_contact_alice": alice_contact}
channel.send(bob.self_contact.make_vcard())
while 1:
eval_str = channel.receive()
res = eval(eval_str, ns)
try:
channel.send(res)
except Exception:
# some unserializable result
channel.send(None)

View File

@@ -57,7 +57,7 @@ class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, rpc_server_path="deltachat-rpc-server", **kwargs):
"""Initialize RPC client.
The 'kwargs' arguments will be passed to subprocess.Popen().
The given arguments will be passed to subprocess.Popen().
"""
if accounts_dir:
kwargs["env"] = {

View File

@@ -107,48 +107,3 @@ def test_no_contact_request_call(acfactory) -> None:
msg = bob.get_message_by_id(event.msg_id)
if msg.get_snapshot().text == "Hello!":
break
def test_who_can_call_me_nobody(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (2)
bob.set_config("who_can_call_me", "2")
# Bob even accepts Alice in advance so the chat does not appear as contact request.
bob.create_chat(alice)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.send_text("Hello!")
# Notification for "Hello!" message should arrive
# without the call ringing.
while True:
event = bob.wait_for_event()
# There should be no incoming call notification.
assert event.kind != EventType.INCOMING_CALL
if event.kind == EventType.INCOMING_MSG:
msg = bob.get_message_by_id(event.msg_id)
if msg.get_snapshot().text == "Hello!":
break
def test_who_can_call_me_everybody(acfactory) -> None:
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
alice, bob = acfactory.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (0)
bob.set_config("who_can_call_me", "0")
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
incoming_call_message = Message(bob, incoming_call_event.msg_id)
# Even with the call arriving, the chat is still in the contact request mode.
incoming_chat = incoming_call_message.get_snapshot().chat
assert incoming_chat.get_basic_snapshot().is_contact_request

View File

@@ -1,57 +0,0 @@
import subprocess
import pytest
from deltachat_rpc_client import DeltaChat, Rpc
def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
python, rpc_server_path = get_core_python_env("2.24.0")
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"])
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path)
with rpc:
dc = DeltaChat(rpc)
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0"
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
qr_code = alice.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
remote_eval("bob.wait_for_securejoin_joiner_success()")
# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
def test_send_and_receive_message(alice_and_remote_bob) -> None:
"""Test other-core Bob profile can send a message to Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
msg = alice.wait_for_incoming_msg()
assert msg.get_snapshot().text == "hello"
def test_second_device(acfactory, alice_and_remote_bob) -> None:
"""Test setting up current version as a second device for old version."""
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
new_account = acfactory.get_unconfigured_account()
new_account._rpc.get_backup(new_account.id, qr)
remote_eval("locals()['future']()")
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")

View File

@@ -1,6 +1,5 @@
import pytest
from deltachat_rpc_client import EventType
from deltachat_rpc_client.rpc import JsonRpcError
@@ -157,122 +156,3 @@ def test_reconfigure_transport(acfactory) -> None:
# Reconfiguring the transport should not reset
# the settings as if when configuring the first transport.
assert account.get_config("mvbox_move") == "1"
def test_transport_synchronization(acfactory, log) -> None:
"""Test synchronization of transports between devices."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acfactory.get_account_qr()
ac1.add_transport_from_qr(qr)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert len(ac1.list_transports()) == 2
assert len(ac1_clone.list_transports()) == 2
ac1_clone.add_transport_from_qr(qr)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert len(ac1.list_transports()) == 3
assert len(ac1_clone.list_transports()) == 3
log.section("ac1 clone removes second transport")
[transport1, transport2, transport3] = ac1_clone.list_transports()
addr3 = transport3["addr"]
ac1_clone.delete_transport(transport2["addr"])
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport1, transport3] = ac1.list_transports()
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport3["addr"])
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport1, transport3] = ac1_clone.list_transports()
assert ac1_clone.get_config("configured_addr") == addr3
log.section("ac1 removes the first transport")
ac1.delete_transport(transport1["addr"])
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport3] = ac1_clone.list_transports()
assert transport3["addr"] == addr3
assert ac1_clone.get_config("configured_addr") == addr3
ac2_chat = ac2.create_chat(ac1)
ac2_chat.send_text("Hello!")
assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
def test_recognize_self_address(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_chat = bob.create_chat(alice)
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
alice.set_config("configured_addr", new_alice_addr)
bob_chat.send_text("Hello!")
msg = alice.wait_for_incoming_msg().get_snapshot()
assert msg.chat == alice.create_chat(bob)
def test_transport_limit(acfactory) -> None:
"""Test transports limit."""
account = acfactory.get_online_account()
qr = acfactory.get_account_qr()
limit = 5
for _ in range(1, limit):
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == limit
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
second_addr = account.list_transports()[1]["addr"]
account.delete_transport(second_addr)
# test that adding a transport after deleting one works again
account.add_transport_from_qr(qr)
def test_message_info_imap_urls(acfactory, log) -> None:
"""Test that message info contains IMAP URLs of where the message was received."""
alice, bob = acfactory.get_online_accounts(2)
log.section("Alice adds ac1 clone removes second transport")
qr = acfactory.get_account_qr()
for i in range(3):
alice.add_transport_from_qr(qr)
# Wait for all transports to go IDLE after adding each one.
for _ in range(i + 1):
alice.bring_online()
new_alice_addr = alice.list_transports()[2]["addr"]
alice.set_config("configured_addr", new_alice_addr)
# Enable multi-device mode so messages are not deleted immediately.
alice.set_config("bcc_self", "1")
# Bob creates chat, learning about Alice's currently selected transport.
# This is where he will send the message.
bob_chat = bob.create_chat(alice)
# Alice changes the transport again.
alice.set_config("configured_addr", alice.list_transports()[3]["addr"])
bob_chat.send_text("Hello!")
msg = alice.wait_for_incoming_msg()
for alice_transport in alice.list_transports():
addr = alice_transport["addr"]
assert (addr == new_alice_addr) == (addr in msg.get_info())

View File

@@ -0,0 +1,20 @@
import subprocess
import sys
from platform import system # noqa
import pytest
from deltachat_rpc_client import DeltaChat, Rpc
@pytest.mark.skipif("system() == 'Windows'")
def test_install_venv_and_use_other_core(tmp_path):
venv = tmp_path.joinpath("venv1")
subprocess.check_call([sys.executable, "-m", "venv", venv])
python = venv / "bin" / "python"
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"])
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server"))
with rpc:
dc = DeltaChat(rpc)
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.20.0"

View File

@@ -696,6 +696,6 @@ def test_withdraw_securejoin_qr(acfactory):
event = alice.wait_for_event()
if (
event.kind == EventType.WARNING
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
):
break

View File

@@ -90,9 +90,12 @@ def test_lowercase_address(acfactory) -> None:
assert account.get_config("configured_addr") == addr
assert account.list_transports()[0]["addr"] == addr
param = account.get_info()["used_transport_settings"]
assert addr in param
assert addr_upper not in param
for param in [
account.get_info()["used_account_settings"],
account.get_info()["entered_account_settings"],
]:
assert addr in param
assert addr_upper not in param
def test_configure_ip(acfactory) -> None:
@@ -340,11 +343,9 @@ def test_receive_imf_failure(acfactory) -> None:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
version = bob.get_info()["deltachat_core_version"]
assert (
snapshot.text == "❌ Failed to receive a message:"
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
f" Core version {version}."
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
)
@@ -507,103 +508,6 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
assert alice2.manager.get_system_info()
def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
(ac1, some1) = acfactory.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
original_image_path = data.get_path("image/avatar64x64.png")
chat1.send_file(str(original_image_path))
# Add another 100KB file that ensures that the progress is smooth enough
path = tmp_path / "attachment.txt"
with path.open("w") as file:
file.truncate(100000)
chat1.send_file(str(path))
def assert_account_is_proper(ac):
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
assert messages[0 + E2EE_INFO_MSGS].get_snapshot().text == "msg1"
snapshot = messages[1 + E2EE_INFO_MSGS].get_snapshot()
assert snapshot.file_mime == "image/png"
assert os.stat(snapshot.file).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
assert_account_is_proper(ac1)
backupdir = tmp_path / "backup"
backupdir.mkdir()
log.section(f"export all to {backupdir}")
ac1.stop_io()
ac1.export_backup(backupdir)
progress = 0
files_written = []
while True:
event = ac1.wait_for_event()
if event.kind == EventType.IMEX_PROGRESS:
assert event.progress > 0 # Progress 0 indicates error.
assert event.progress < progress + 250
progress = event.progress
if progress == 1000:
break
elif event.kind == EventType.IMEX_FILE_WRITTEN:
files_written.append(event.path)
else:
logging.info(event)
assert len(files_written) == 1
assert os.path.exists(files_written[0])
ac1.start_io()
log.section("get fresh empty account")
ac2 = acfactory.get_unconfigured_account()
log.section("import backup and check it's proper")
ac2.import_backup(files_written[0])
progress = 0
while True:
event = ac2.wait_for_event()
if event.kind == EventType.IMEX_PROGRESS:
assert event.progress > 0 # Progress 0 indicates error.
assert event.progress < progress + 250
progress = event.progress
if progress == 1000:
break
else:
logging.info(event)
assert_account_is_proper(ac1)
assert_account_is_proper(ac2)
log.section(f"Second-time export all to {backupdir}")
ac1.stop_io()
ac1.export_backup(backupdir)
while True:
event = ac1.wait_for_event()
if event.kind == EventType.IMEX_PROGRESS:
assert event.progress > 0
if event.progress == 1000:
break
elif event.kind == EventType.IMEX_FILE_WRITTEN:
files_written.append(event.path)
else:
logging.info(event)
assert len(files_written) == 2
assert os.path.exists(files_written[1])
assert files_written[1] != files_written[0]
assert len(list(backupdir.glob("*.tar"))) == 2
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
@@ -829,7 +733,7 @@ def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
# Certificate checks should be configured (not None)
assert "cert_strict" in alice.get_info().used_transport_settings
assert "cert_strict" in alice.get_info().used_account_settings
# "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
@@ -842,7 +746,7 @@ def test_configured_imap_certificate_checks(acfactory):
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
assert "cert_old_automatic" not in alice.get_info().used_account_settings
def test_no_old_msg_is_fresh(acfactory):
@@ -1108,47 +1012,3 @@ def test_message_exists(acfactory):
ac1.remove()
assert not message1.exists()
assert not message2.exists()
def test_synchronize_member_list_on_group_rejoin(acfactory, log):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
Then it removes ac2, removes ac3 and adds ac2 back.
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
log.section("setting up accounts, accepted with each other")
ac1, ac2, ac3 = accounts = acfactory.get_online_accounts(3)
log.section("ac1: creating group chat with 2 other members")
chat = ac1.create_group("title1")
chat.add_contact(ac2)
chat.add_contact(ac3)
log.section("ac1: send message to new group chat")
msg = chat.send_text("hello")
assert chat.num_contacts() == 3
log.section("checking that the chat arrived correctly")
for ac in accounts[1:]:
msg = ac.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
assert msg.chat.num_contacts() == 3
msg.chat.accept()
log.section("ac1: removing ac2")
chat.remove_contact(ac2)
log.section("ac2: wait for a message about removal from the chat")
ac2.wait_for_incoming_msg()
log.section("ac1: removing ac3")
chat.remove_contact(ac3)
log.section("ac1: adding ac2 back")
chat.add_contact(ac2)
log.section("ac2: check that ac3 is removed")
msg = ac2.wait_for_incoming_msg()
assert chat.num_contacts() == 2
assert msg.get_snapshot().chat.num_contacts() == 2

View File

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

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.37.0"
"version": "2.29.0"
}

View File

@@ -43,7 +43,7 @@ async fn main_impl() -> Result<()> {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
eprintln!("{DC_VERSION_STR}");
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
} else if first_arg.to_str() == Some("--openrpc") {
if let Some(arg) = args.next() {

View File

@@ -12,16 +12,6 @@ ignore = [
# Unmaintained paste
"RUSTSEC-2024-0436",
# Unmaintained rustls-pemfile
# It is a transitive dependency of iroh 0.35.0,
# this should be fixed by upgrading to iroh 1.0 once it is released.
"RUSTSEC-2025-0134",
# Old versions of "lru" are transitive dependencies of iroh 0.35.0.
# <https://rustsec.org/advisories/RUSTSEC-2026-0002>
# <https://github.com/chatmail/core/issues/7692>
"RUSTSEC-2026-0002",
]
[bans]
@@ -36,10 +26,11 @@ skip = [
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "0.2.12" },
{ name = "hashbrown", version = "0.14.5" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "lru", version = "0.12.5" },
{ name = "lru", version = "0.12.3" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nom", version = "7.1.3" },
{ name = "rand_chacha", version = "0.3.1" },

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.37.0"
version = "2.29.0"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"

View File

@@ -1,3 +1,4 @@
import sys
import time
import deltachat as dc
@@ -62,6 +63,56 @@ class TestGroupStressTests:
# Message should be encrypted because keys of other members are gossiped
assert msg.is_encrypted()
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
Then it removes ac2, removes ac3 and adds ac2 back.
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
lp.sec("setting up accounts, accepted with each other")
accounts = acfactory.get_online_accounts(3)
acfactory.introduce_each_other(accounts)
ac1, ac2, ac3 = accounts
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title1", contacts=[ac2, ac3])
assert not chat.is_promoted()
lp.sec("ac1: send message to new group chat")
msg = chat.send_text("hello")
assert chat.is_promoted() and msg.is_encrypted()
assert chat.num_contacts() == 3
lp.sec("checking that the chat arrived correctly")
for ac in accounts[1:]:
msg = ac._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
print("chat is", msg.chat)
assert msg.chat.num_contacts() == 3
lp.sec("ac1: removing ac2")
chat.remove_contact(ac2)
lp.sec("ac2: wait for a message about removal from the chat")
msg = ac2._evtracker.wait_next_incoming_message()
lp.sec("ac1: removing ac3")
chat.remove_contact(ac3)
lp.sec("ac1: adding ac2 back")
# Group is promoted, message is sent automatically
assert chat.is_promoted()
chat.add_contact(ac2)
lp.sec("ac2: check that ac3 is removed")
msg = ac2._evtracker.wait_next_incoming_message()
assert chat.num_contacts() == 2
assert msg.chat.num_contacts() == 2
acfactory.dump_imap_summary(sys.stdout)
def test_qr_verified_group_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)

View File

@@ -9,6 +9,7 @@ from imap_tools import AND
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
from deltachat.testplugin import E2EE_INFO_MSGS
@@ -268,23 +269,22 @@ def test_enable_mvbox_move(acfactory, lp):
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
def test_dont_move_sync_msgs(acfactory):
def test_move_sync_msgs(acfactory):
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
acfactory.bring_accounts_online()
ac1.direct_imap.select_folder("Inbox")
ac1.direct_imap.select_folder("DeltaChat")
# Sync messages may also be sent during the configuration.
inbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
mvbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
ac1.set_config("displayname", "Alice")
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
ac1.set_config("displayname", "Bob")
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
ac1.direct_imap.select_folder("Inbox")
assert len(ac1.direct_imap.get_all_messages()) == inbox_msg_cnt + 2
ac1.direct_imap.select_folder("DeltaChat")
assert len(ac1.direct_imap.get_all_messages()) == 0
ac1.direct_imap.select_folder("DeltaChat")
assert len(ac1.direct_imap.get_all_messages()) == mvbox_msg_cnt + 2
def test_forward_messages(acfactory, lp):
@@ -826,6 +826,86 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
def test_import_export_online_all(acfactory, tmp_path, data, lp):
(ac1, some1) = acfactory.get_online_accounts(2)
lp.sec("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
original_image_path = data.get_path("d.png")
chat1.send_image(original_image_path)
# Add another 100KB file that ensures that the progress is smooth enough
path = tmp_path / "attachment.txt"
with path.open("w") as file:
file.truncate(100000)
chat1.send_file(str(path))
def assert_account_is_proper(ac):
contacts = ac.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
assert_account_is_proper(ac1)
backupdir = tmp_path / "backup"
backupdir.mkdir()
lp.sec(f"export all to {backupdir}")
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
ac1.stop_io()
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
# check progress events for export
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
assert imex_tracker.wait_progress(250, progress_upper_limit=499)
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
paths = imex_tracker.wait_finish()
assert len(paths) == 1
path = paths[0]
assert os.path.exists(path)
ac1.start_io()
lp.sec("get fresh empty account")
ac2 = acfactory.get_unconfigured_account()
lp.sec("get latest backup file")
path2 = ac2.get_latest_backupfile(str(backupdir))
assert path2 == path
lp.sec("import backup and check it's proper")
with ac2.temp_plugin(ImexTracker()) as imex_tracker:
ac2.import_all(path)
# check progress events for import
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
assert imex_tracker.wait_progress(1000)
assert_account_is_proper(ac1)
assert_account_is_proper(ac2)
lp.sec(f"Second-time export all to {backupdir}")
ac1.stop_io()
path2 = ac1.export_all(str(backupdir))
assert os.path.exists(path2)
assert path2 != path
assert ac2.get_latest_backupfile(str(backupdir)) == path2
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification
@@ -1215,17 +1295,16 @@ def test_configure_error_msgs_invalid_server(acfactory):
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if ev.data1 == 0:
break
err_lower = ev.data2.lower()
# Can't connect so it probably should say something about "internet"
# again, should not repeat itself
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
# in configure.rs returned false because the error message was changed
# (i.e. did not contain "could not resolve" anymore)
assert (err_lower.count("internet") + err_lower.count("network")) == 1
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
# Should mention that it can't connect:
assert err_lower.count("connect") == 1
assert ev.data2.count("connect") == 1
# The users do not know what "configuration" is
assert "configuration" not in err_lower
assert "configuration" not in ev.data2.lower()
def test_status(acfactory):

View File

@@ -1 +1 @@
2026-01-08
2025-12-01

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.92.0
RUST_VERSION=1.91.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -60,18 +60,8 @@ impl Accounts {
if writable && !dir.exists() {
Accounts::create(&dir).await?;
}
let events = Events::new();
Accounts::open(events, dir, writable).await
}
/// Loads or creates an accounts folder at the given `dir`.
/// Uses an existing events channel.
pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
if writable && !dir.exists() {
Accounts::create(&dir).await?;
}
Accounts::open(events, dir, writable).await
Accounts::open(dir, writable).await
}
/// Get the ID used to log events.
@@ -95,14 +85,14 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{config_file:?} does not exist");
let config = Config::from_file(config_file, writable).await?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
let accounts = config
@@ -387,11 +377,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 {
@@ -407,11 +392,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].
@@ -449,11 +429,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,

View File

@@ -4,21 +4,18 @@
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
use crate::chat::ChatIdBlocked;
use crate::chat::{Chat, ChatId, send_msg};
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::contact::ContactId;
use crate::context::{Context, WeakContext};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::log::warn;
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::net::dns::lookup_host_with_cache;
use crate::param::Param;
use crate::tools::{normalize_text, time};
use crate::tools::time;
use anyhow::{Context as _, Result, ensure};
use deltachat_derive::{FromSql, ToSql};
use num_traits::FromPrimitive;
use sdp::SessionDescription;
use serde::Serialize;
use std::io::Cursor;
@@ -36,7 +33,7 @@ use tokio::time::sleep;
///
/// For the caller, this means they should also not wait longer,
/// as the callee won't start the call afterwards.
const RINGING_SECONDS: i64 = 120;
const RINGING_SECONDS: i64 = 60;
// For persisting parameters in the call, we use Param::Arg*
@@ -89,7 +86,7 @@ impl CallInfo {
.sql
.execute(
"UPDATE msgs SET txt=?, txt_normalized=? WHERE id=?",
(text, normalize_text(text), self.msg.id),
(text, message::normalize_text(text), self.msg.id),
)
.await?;
Ok(())
@@ -202,9 +199,8 @@ impl Context {
call.id = send_msg(self, chat_id, &mut call).await?;
let wait = RINGING_SECONDS;
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
context,
self.clone(),
wait.try_into()?,
call.id,
));
@@ -295,12 +291,11 @@ impl Context {
}
async fn emit_end_call_if_unaccepted(
context: WeakContext,
context: Context,
wait: u64,
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let context = context.upgrade()?;
let Some(mut call) = context.load_call_by_id(call_id).await? else {
warn!(
context,
@@ -351,39 +346,30 @@ impl Context {
false
}
};
let can_call_me = match who_can_call_me(self).await? {
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_some_and(|chat_id_blocked| {
match chat_id_blocked.blocked {
Blocked::Not => true,
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
false
}
}
}),
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
WhoCanCallMe::Nobody => false,
};
if can_call_me {
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
if let Some(chat_id_blocked) =
ChatIdBlocked::lookup_by_contact(self, from_id).await?
{
match chat_id_blocked.blocked {
Blocked::Not => {
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
}
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
}
}
}
let wait = call.remaining_ring_seconds();
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
context,
self.clone(),
wait.try_into()?,
call.msg.id,
));
@@ -674,7 +660,9 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
// because of bandwidth costs:
// <https://github.com/jselbie/stunserver/issues/50>
// We use nine.testrun.org for a default STUN server.
let hostname = "nine.testrun.org";
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
@@ -682,27 +670,14 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
.into_iter()
.map(|addr| format!("stun:{addr}"))
.collect();
let stun_server = IceServer {
let ice_server = IceServer {
urls,
username: None,
credential: None,
};
let hostname = "turn.delta.chat";
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
.await?
.into_iter()
.map(|addr| format!("turn:{addr}"))
.collect();
let turn_server = IceServer {
urls,
username: Some("public".to_string()),
credential: Some("o4tR7yG4rG2slhXqRUf9zgmHz".to_string()),
};
let json = serde_json::to_string(&[stun_server, turn_server])?;
let json = serde_json::to_string(&[ice_server])?;
Ok(json)
}
@@ -723,32 +698,5 @@ pub async fn ice_servers(context: &Context) -> Result<String> {
}
}
/// "Who can call me" config options.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum WhoCanCallMe {
/// Everybody can call me if they are not blocked.
///
/// This includes contact requests.
Everybody = 0,
/// Every contact who is not blocked and not a contact request, can call.
#[default]
Contacts = 1,
/// Nobody can call me.
Nobody = 2,
}
/// Returns currently configuration of the "who can call me" option.
async fn who_can_call_me(context: &Context) -> Result<WhoCanCallMe> {
let who_can_call_me =
WhoCanCallMe::from_i32(context.get_config_int(Config::WhoCanCallMe).await?)
.unwrap_or_default();
Ok(who_can_call_me)
}
#[cfg(test)]
mod calls_tests;

View File

@@ -45,7 +45,7 @@ use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_secret, create_id,
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
gm2local_offset, smeared_time, time, truncate_msg_text,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{chatlist_events, imap};
@@ -286,11 +286,10 @@ impl ChatId {
let timestamp = cmp::min(timestamp, smeared_time(context));
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, name_normalized, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, 0, ?)",
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, 0, ?);",
(
chattype,
&grpname,
normalize_text(&grpname),
grpid,
create_blocked,
timestamp,
@@ -432,18 +431,14 @@ impl ChatId {
match chat.typ {
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
// User has "created a chat" with all these contacts.
//
// Previously accepting a chat literally created a chat because unaccepted chats
// went to "contact requests" list rather than normal chatlist.
// But for groups we use lower origin because users don't always check all members
// before accepting a chat and may not want to have the group members mixed with
// existing contacts. `IncomingTo` fits here by its definition.
let origin = match chat.typ {
Chattype::Group => Origin::IncomingTo,
_ => Origin::CreateChat,
};
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != ContactId::SELF {
ContactId::scaleup_origin(context, &[contact_id], origin).await?;
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
.await?;
}
}
}
@@ -600,10 +595,6 @@ impl ChatId {
}
/// Deletes a chat.
///
/// Messages are deleted from the device and the chat database entry is deleted.
/// After that, a `MsgsChanged` event is emitted.
/// Messages are deleted from the server in background.
pub async fn delete(self, context: &Context) -> Result<()> {
self.delete_ex(context, Sync).await
}
@@ -662,7 +653,7 @@ impl ChatId {
context
.set_config_internal(Config::LastHousekeeping, None)
.await?;
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -791,7 +782,7 @@ impl ChatId {
time(),
msg.viewtype,
&msg.text,
normalize_text(&msg.text),
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
@@ -832,7 +823,7 @@ impl ChatId {
msg.viewtype,
MessageState::OutDraft,
&msg.text,
normalize_text(&msg.text),
message::normalize_text(&msg.text),
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
@@ -1928,7 +1919,7 @@ impl Chat {
msg.viewtype,
msg.state,
msg_text,
normalize_text(&msg_text),
message::normalize_text(&msg_text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -1979,7 +1970,7 @@ impl Chat {
msg.viewtype,
msg.state,
msg_text,
normalize_text(&msg_text),
message::normalize_text(&msg_text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -2115,7 +2106,7 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
context
.add_sync_item(SyncData::AlterChat { id, action })
.await?;
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -2283,8 +2274,8 @@ async fn update_special_chat_name(
context
.sql
.execute(
"UPDATE chats SET name=?, name_normalized=? WHERE id=? AND name!=?",
(&name, normalize_text(&name), chat_id, &name),
"UPDATE chats SET name=? WHERE id=? AND name!=?",
(&name, chat_id, &name),
)
.await?;
}
@@ -2397,12 +2388,11 @@ impl ChatIdBlocked {
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
(type, name, name_normalized, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?, ?)",
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(
Chattype::Single,
&chat_name,
normalize_text(&chat_name),
chat_name,
params.to_string(),
create_blocked as u8,
smeared_time,
@@ -2736,7 +2726,7 @@ async fn prepare_send_msg(
Ok(row_ids)
}
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
/// Constructs jobs for sending a message and inserts them into the appropriate table.
///
/// Updates the message `GuaranteeE2ee` parameter and persists it
/// in the database depending on whether the message
@@ -2860,27 +2850,30 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
t.execute(
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
(),
)?;
}
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
let row_id = t.execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
VALUES (?1, ?2, ?3, ?4)",
(
&rendered_msg.rfc724_mid,
recipients_chunk,
&rendered_msg.message,
msg.id,
),
t.execute(
"INSERT INTO imap_send (mime, msg_id) VALUES (?, ?)",
(&rendered_msg.message, msg.id),
)?;
row_ids.push(row_id.try_into()?);
} else {
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
let row_id = t.execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
VALUES (?1, ?2, ?3, ?4)",
(
&rendered_msg.rfc724_mid,
recipients_chunk,
&rendered_msg.message,
msg.id,
),
)?;
row_ids.push(row_id.try_into()?);
}
}
Ok(row_ids)
};
@@ -2951,7 +2944,7 @@ pub(crate) async fn save_text_edit_to_db(
"UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
(
new_text,
normalize_text(new_text),
message::normalize_text(new_text),
original_msg.param.to_string(),
original_msg.id,
),
@@ -3095,7 +3088,7 @@ pub async fn get_chat_msgs_ex(
WHERE m.chat_id=?
AND m.hidden=0
AND (
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
m.param GLOB \"*S=*\"
OR m.from_id == ?
OR m.to_id == ?
);",
@@ -3440,15 +3433,9 @@ pub(crate) async fn create_group_ex(
.sql
.insert(
"INSERT INTO chats
(type, name, name_normalized, grpid, param, created_timestamp)
VALUES(?, ?, ?, ?, \'U=1\', ?)",
(
Chattype::Group,
&chat_name,
normalize_text(&chat_name),
&grpid,
timestamp,
),
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(Chattype::Group, &chat_name, &grpid, timestamp),
)
.await?;
@@ -3532,15 +3519,9 @@ pub(crate) async fn create_out_broadcast_ex(
t.execute(
"INSERT INTO chats
(type, name, name_normalized, grpid, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(
Chattype::OutBroadcast,
&chat_name,
normalize_text(&chat_name),
&grpid,
timestamp,
),
(type, name, grpid, created_timestamp)
VALUES(?, ?, ?, ?);",
(Chattype::OutBroadcast, &chat_name, &grpid, timestamp),
)?;
let chat_id = ChatId::new(t.last_insert_rowid().try_into()?);
@@ -3842,7 +3823,7 @@ pub(crate) async fn add_contact_to_chat_ex(
.log_err(context)
.is_ok()
{
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
}
}
context.emit_event(EventType::ChatModified(chat_id));
@@ -4113,8 +4094,8 @@ async fn rename_ex(
context
.sql
.execute(
"UPDATE chats SET name=?, name_normalized=? WHERE id=?",
(&new_name, normalize_text(&new_name), chat_id),
"UPDATE chats SET name=? WHERE id=?;",
(new_name.to_string(), chat_id),
)
.await?;
if chat.is_promoted()
@@ -4208,16 +4189,6 @@ pub async fn set_chat_profile_image(
/// Forwards multiple messages to a chat.
pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Result<()> {
forward_msgs_2ctx(context, msg_ids, context, chat_id).await
}
/// Forwards multiple messages to a chat in another context.
pub async fn forward_msgs_2ctx(
ctx_src: &Context,
msg_ids: &[MsgId],
ctx_dst: &Context,
chat_id: ChatId,
) -> Result<()> {
ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward");
ensure!(!chat_id.is_special(), "can not forward to special chat");
@@ -4225,16 +4196,16 @@ pub async fn forward_msgs_2ctx(
let mut curr_timestamp: i64;
chat_id
.unarchive_if_not_muted(ctx_dst, MessageState::Undefined)
.unarchive_if_not_muted(context, MessageState::Undefined)
.await?;
let mut chat = Chat::load_from_db(ctx_dst, chat_id).await?;
if let Some(reason) = chat.why_cant_send(ctx_dst).await? {
let mut chat = Chat::load_from_db(context, chat_id).await?;
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {chat_id}: {reason}");
}
curr_timestamp = create_smeared_timestamps(ctx_dst, msg_ids.len());
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids {
let ts: i64 = ctx_src
let ts: i64 = context
.sql
.query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
.await?
@@ -4244,14 +4215,11 @@ pub async fn forward_msgs_2ctx(
msgs.sort_unstable();
for (_, id) in msgs {
let src_msg_id: MsgId = id;
let mut msg = Message::load_from_db(ctx_src, src_msg_id).await?;
let mut msg = Message::load_from_db(context, src_msg_id).await?;
if msg.state == MessageState::OutDraft {
bail!("cannot forward drafts.");
}
let mut param = msg.param;
msg.param = Params::new();
if msg.get_viewtype() != Viewtype::Sticker {
msg.param
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
@@ -4261,16 +4229,17 @@ pub async fn forward_msgs_2ctx(
msg.viewtype = Viewtype::Text;
}
let param = &mut param;
msg.param.steal(param, Param::File);
msg.param.steal(param, Param::Filename);
msg.param.steal(param, Param::Width);
msg.param.steal(param, Param::Height);
msg.param.steal(param, Param::Duration);
msg.param.steal(param, Param::MimeType);
msg.param.steal(param, Param::ProtectQuote);
msg.param.steal(param, Param::Quote);
msg.param.steal(param, Param::Summary1);
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.param.remove(Param::Cmd);
msg.param.remove(Param::OverrideSenderDisplayname);
msg.param.remove(Param::WebxdcDocument);
msg.param.remove(Param::WebxdcDocumentTimestamp);
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
msg.param.remove(Param::IsEdited);
msg.param.remove(Param::WebrtcRoom);
msg.param.remove(Param::WebrtcAccepted);
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimefactory
@@ -4279,16 +4248,16 @@ pub async fn forward_msgs_2ctx(
msg.state = MessageState::OutPending;
msg.rfc724_mid = create_outgoing_rfc724_mid();
msg.timestamp_sort = curr_timestamp;
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
chat.prepare_msg_raw(context, &mut msg, None).await?;
curr_timestamp += 1;
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
ctx_dst.scheduler.interrupt_smtp().await;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
}
created_msgs.push(msg.id);
}
for msg_id in created_msgs {
ctx_dst.emit_msgs_changed(chat_id, msg_id);
context.emit_msgs_changed(chat_id, msg_id);
}
Ok(())
}
@@ -4316,7 +4285,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
})
.await?;
}
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -4560,7 +4529,7 @@ pub async fn add_device_msg_with_importance(
msg.viewtype,
state,
&msg.text,
normalize_text(&msg.text),
message::normalize_text(&msg.text),
msg.param.to_string(),
rfc724_mid,
),
@@ -4699,7 +4668,7 @@ pub(crate) async fn add_info_msg_with_cmd(
Viewtype::Text,
MessageState::InNoticed,
text,
normalize_text(text),
message::normalize_text(text),
rfc724_mid,
ephemeral_timer,
param.to_string(),
@@ -4741,7 +4710,7 @@ pub(crate) async fn update_msg_text_and_timestamp(
.sql
.execute(
"UPDATE msgs SET txt=?, txt_normalized=?, timestamp=? WHERE id=?;",
(text, normalize_text(text), timestamp, msg_id),
(text, message::normalize_text(text), timestamp, msg_id),
)
.await?;
context.emit_msgs_changed(chat_id, msg_id);

View File

@@ -5240,44 +5240,6 @@ async fn test_send_delete_request_no_encryption() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_msgs_2ctx() -> 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 alice_sent = alice.send_text(alice_chat.id, "hi").await;
let bob_alice_msg = bob.recv_msg(&alice_sent).await;
let bob_chat_id = bob_alice_msg.chat_id;
bob_chat_id.accept(bob).await?;
let bob_text = "Hi, did you know we're using the same device so i have access to your profile?";
let bob_sent = bob.send_text(bob_chat_id, bob_text).await;
alice.recv_msg(&bob_sent).await;
let alice_chat_len = alice_chat.id.get_msg_cnt(alice).await?;
forward_msgs_2ctx(
bob,
&[bob_alice_msg.id, bob_sent.sender_msg_id],
alice,
alice_chat.id,
)
.await?;
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, alice_chat_len + 2);
let msg = alice.get_last_msg().await;
assert!(msg.is_forwarded());
assert_eq!(msg.text, bob_text);
assert_eq!(msg.from_id, ContactId::SELF);
let sent = alice.pop_sent_msg().await;
let msg = bob.recv_msg(&sent).await;
assert!(msg.is_forwarded());
assert_eq!(msg.text, bob_text);
assert_eq!(msg.from_id, bob_alice_msg.from_id);
Ok(())
}
/// Tests that in multi-device setup
/// second device learns the key of a contact
/// via Autocrypt-Gossip in 1:1 chats.

View File

@@ -185,7 +185,7 @@ impl Chatlist {
warn!(context, "Cannot update special chat names: {err:#}.")
}
let str_like_cmd = format!("%{}%", query.to_lowercase());
let str_like_cmd = format!("%{query}%");
context
.sql
.query_map_vec(
@@ -201,7 +201,7 @@ impl Chatlist {
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1
AND IFNULL(c.name_normalized,c.name) LIKE ?3
AND c.name LIKE ?3
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -396,6 +396,8 @@ impl Chatlist {
if lastmsg.from_id == ContactId::SELF {
None
} else if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
|| chat.typ == Chattype::Mailinglist
|| chat.is_self_talk()
{
@@ -469,11 +471,10 @@ mod tests {
use super::*;
use crate::chat::save_msgs;
use crate::chat::{
add_contact_to_chat, create_broadcast, create_group, get_chat_contacts,
remove_contact_from_chat, send_text_msg, set_chat_name,
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
send_text_msg,
};
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
@@ -481,7 +482,7 @@ mod tests {
use std::time::Duration;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() -> Result<()> {
async fn test_try_load() {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let chat_id1 = create_group(bob, "a chat").await.unwrap();
@@ -551,15 +552,6 @@ mod tests {
.await
.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = create_group(bob, "Δ-chat").await.unwrap();
let chats = Chatlist::try_load(bob, 0, Some("δ"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.ids[0].0, chat_id);
set_chat_name(bob, chat_id, "abcδe").await?;
let chats = Chatlist::try_load(bob, 0, Some("Δ"), None).await?;
assert_eq!(chats.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -805,32 +797,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_summary_prefix_for_channel() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chat_id = create_broadcast(&alice, "alice's channel".to_string()).await?;
let qr = get_securejoin_qr(&alice, Some(alice_chat_id)).await?;
tcm.exec_securejoin_qr(&bob, &alice, &qr).await;
send_text_msg(&alice, alice_chat_id, "hi".into()).await?;
let sent1 = alice.pop_sent_msg().await;
let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
let summary = chatlist.get_summary(&alice, 0, None).await?;
assert!(summary.prefix.is_none());
assert_eq!(summary.text, "hi");
bob.recv_msg(&sent1).await;
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
let summary = chatlist.get_summary(&bob, 0, None).await?;
assert!(summary.prefix.is_none());
assert_eq!(summary.text, "hi");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;

View File

@@ -13,6 +13,7 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::configure::EnteredLoginParam;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
@@ -20,7 +21,7 @@ use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::Provider;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
use crate::transport::ConfiguredLoginParam;
use crate::{constants, stats};
/// The available configuration keys.
@@ -203,7 +204,7 @@ pub enum Config {
/// `ProviderOptions::delete_to_trash`.
DeleteToTrash,
/// The primary email address.
/// The primary email address. Also see `SecondaryAddrs`.
ConfiguredAddr,
/// List of configured IMAP servers as a JSON array.
@@ -305,6 +306,10 @@ pub enum Config {
/// Meant to help profile owner to differ between profiles with similar names.
PrivateTag,
/// All secondary self addresses separated by spaces
/// (`addr1@example.org addr2@example.org addr3@example.org`)
SecondaryAddrs,
/// Read-only core version string.
#[strum(serialize = "sys.version")]
SysVersion,
@@ -446,16 +451,6 @@ pub enum Config {
/// Protected Email".
#[strum(props(default = "1"))]
StdHeaderProtectionComposing,
/// Who can call me.
///
/// The options are from the `WhoCanCallMe` enum.
#[strum(props(default = "1"))]
WhoCanCallMe,
/// Experimental option denoting that the current profile is shared between multiple team members.
/// For now, the only effect of this option is that seen flags are not synchronized.
TeamProfile,
}
impl Config {
@@ -514,7 +509,7 @@ impl Context {
.into_owned()
})
}
Config::SysVersion => Some(constants::DC_VERSION_STR.to_string()),
Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key.as_ref()).await?,
@@ -615,6 +610,12 @@ impl Context {
&& !self.get_config_bool(Config::Bot).await?)
}
/// Returns whether sync messages should be uploaded to the mvbox.
pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| !self.get_config_bool(Config::IsChatmail).await?)
}
/// Returns whether MDNs should be requested.
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
match self.get_config_bool_opt(Config::MdnsEnabled).await? {
@@ -818,43 +819,42 @@ impl Context {
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,),
)?;
// Clean up SMTP and IMAP APPEND queue.
//
// The messages in the queue have a different
// From address so we cannot send them over
// the new SMTP transport.
transaction.execute("DELETE FROM smtp", ())?;
transaction.execute("DELETE FROM imap_send", ())?;
Ok(())
})
.await?;
send_sync_transports(self).await?;
self.sql.uncache_raw_config("configured_addr").await;
ConfiguredLoginParam::from_json(&format!(
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
))?
.save_to_transports_table(self, &EnteredLoginParam::default())
.await?;
}
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,),
)?;
// Clean up SMTP and IMAP APPEND queue.
//
// The messages in the queue have a different
// From address so we cannot send them over
// the new SMTP transport.
transaction.execute("DELETE FROM smtp", ())?;
transaction.execute("DELETE FROM imap_send", ())?;
Ok(())
})
.await?;
self.sql.uncache_raw_config("configured_addr").await;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
@@ -884,7 +884,7 @@ impl Context {
{
return Ok(());
}
self.scheduler.interrupt_smtp().await;
self.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -948,7 +948,17 @@ impl Context {
/// This should only be used by test code and during configure.
#[cfg(test)] // AEAP is disabled, but there are still tests for it
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
self.quota.write().await.clear();
self.quota.write().await.take();
// add old primary address (if exists) to secondary addresses
let mut secondary_addrs = self.get_all_self_addrs().await?;
// never store a primary address also as a secondary
secondary_addrs.retain(|a| !addr_cmp(a, primary_new));
self.set_config_internal(
Config::SecondaryAddrs,
Some(secondary_addrs.join(" ").as_str()),
)
.await?;
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
@@ -967,10 +977,14 @@ impl Context {
/// Returns all secondary self addresses.
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
let addr: String = row.get(0)?;
Ok(addr)
}).await
let secondary_addrs = self
.get_config(Config::SecondaryAddrs)
.await?
.unwrap_or_default();
Ok(secondary_addrs
.split_ascii_whitespace()
.map(|s| s.to_string())
.collect())
}
/// Returns the primary self address.
@@ -993,18 +1007,5 @@ fn get_config_keys_string() -> String {
format!(" {keys} ")
}
/// Returns all `ui.*` config keys that were set by the UI.
pub async fn get_all_ui_config_keys(context: &Context) -> Result<Vec<String>> {
let ui_keys = context
.sql
.query_map_vec(
"SELECT keyname FROM config WHERE keyname GLOB 'ui.*' ORDER BY config.id",
(),
|row| Ok(row.get::<_, String>(0)?),
)
.await?;
Ok(ui_keys)
}
#[cfg(test)]
mod config_tests;

View File

@@ -81,37 +81,6 @@ async fn test_ui_config() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_all_ui_config_keys() -> Result<()> {
let t = TestContext::new().await;
t.set_ui_config("ui.android.screen_security", Some("safe"))
.await?;
t.set_ui_config("ui.lastchatid", Some("231")).await?;
t.set_ui_config(
"ui.desktop.webxdcBounds.528490",
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
)
.await?;
t.set_ui_config(
"ui.desktop.webxdcBounds.556543",
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
)
.await?;
assert_eq!(
get_all_ui_config_keys(&t).await?,
vec![
"ui.android.screen_security",
"ui.lastchatid",
"ui.desktop.webxdcBounds.528490",
"ui.desktop.webxdcBounds.556543"
]
);
Ok(())
}
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bool() -> Result<()> {
@@ -125,6 +94,59 @@ async fn test_set_config_bool() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_addrs() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(alice.is_self_addr("alice@example.org").await?);
assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
assert!(!alice.is_self_addr("alice@alice.com").await?);
// Test adding the same primary address
alice.set_primary_self_addr("alice@example.org").await?;
alice.set_primary_self_addr("Alice@Example.Org").await?;
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
// Test adding a new (primary) self address
// The address is trimmed during configure by `LoginParam::from_database()`,
// so `set_primary_self_addr()` doesn't have to trim it.
alice.set_primary_self_addr("Alice@alice.com").await?;
assert!(alice.is_self_addr("aliCe@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["Alice@alice.com", "Alice@Example.Org"]
);
// Check that the entry is not duplicated
alice.set_primary_self_addr("alice@alice.com").await?;
alice.set_primary_self_addr("alice@alice.com").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.com", "Alice@Example.Org"]
);
// Test switching back
alice.set_primary_self_addr("alice@example.org").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@example.org", "alice@alice.com"]
);
// Test setting a new primary self address, the previous self address
// should be kept as a secondary self address
alice.set_primary_self_addr("alice@alice.xyz").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
);
assert!(alice.is_self_addr("alice@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert!(alice.is_self_addr("Alice@alice.xyz").await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdns_default_behaviour() -> Result<()> {
let t = &TestContext::new_alice().await;
@@ -268,7 +290,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
let status = "Sent via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_msg().await;
alice0.pop_sent_sync_msg().await;
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
@@ -292,7 +314,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_msg().await;
alice0.pop_sent_sync_msg().await;
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;

View File

@@ -40,15 +40,11 @@ use crate::sync::Sync::*;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, send_sync_transports,
ConnectionCandidate,
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
/// Maximum number of relays
/// see <https://github.com/chatmail/core/issues/7608>
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
assert!(
@@ -209,9 +205,7 @@ impl Context {
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
let now = time();
let removed_transport_id = self
.sql
self.sql
.transaction(|transaction| {
let primary_addr = transaction.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
@@ -225,13 +219,12 @@ impl Context {
if primary_addr == addr {
bail!("Cannot delete primary transport");
}
let (transport_id, add_timestamp) = transaction.query_row(
"DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
let transport_id = transaction.query_row(
"DELETE FROM transports WHERE addr=? RETURNING id",
(addr,),
|row| {
let id: u32 = row.get(0)?;
let add_timestamp: i64 = row.get(1)?;
Ok((id, add_timestamp))
Ok(id)
},
)?;
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
@@ -240,24 +233,9 @@ impl Context {
(transport_id,),
)?;
// Removal timestamp should not be lower than addition timestamp
// to be accepted by other devices when synced.
let remove_timestamp = std::cmp::max(now, add_timestamp);
transaction.execute(
"INSERT INTO removed_transports (addr, remove_timestamp)
VALUES (?, ?)
ON CONFLICT (addr)
DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
(addr, remove_timestamp),
)?;
Ok(transport_id)
Ok(())
})
.await?;
send_sync_transports(self).await?;
self.quota.write().await.remove(&removed_transport_id);
Ok(())
}
@@ -274,53 +252,18 @@ impl Context {
)
.await?
{
// Should be checked before `MvboxMove` because the latter makes no sense in presense of
// `OnlyFetchMvbox` and even grayed out in the UIs in this case.
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
bail!(
"To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
);
}
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
bail!(
"To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
);
bail!("Cannot use multi-transport with mvbox_move enabled.");
}
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
bail!("Cannot use multi-transport with only_fetch_mvbox enabled.");
}
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
bail!(
"To use additional relays, set the legacy option \"Settings / Advanced / Show Classic Emails\" to \"All\"."
);
}
if self
.sql
.count("SELECT COUNT(*) FROM transports", ())
.await?
>= MAX_TRANSPORT_RELAYS
{
bail!(
"You have reached the maximum number of relays ({}).",
MAX_TRANSPORT_RELAYS
)
bail!("Cannot use multi-transport with disabled fetching of classic emails.");
}
}
let provider = match configure(self, param).await {
Err(error) => {
// Log entered and actual params
let configured_param = get_configured_param(self, param).await;
warn!(
self,
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
param.to_string(),
configured_param
.map(|param| param.to_string())
.unwrap_or("error".to_owned())
);
return Err(error);
}
Ok(provider) => provider,
};
let provider = configure(self, param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, provider).await?;
@@ -609,8 +552,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 900);
let is_configured = ctx.is_configured().await?;
if !is_configured {
if !ctx.is_configured().await? {
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
}
@@ -621,10 +563,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
let provider = configured_param.provider;
configured_param
.clone()
.save_to_transports_table(ctx, param, time())
.save_to_transports_table(ctx, param)
.await?;
send_sync_transports(ctx).await?;
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;

View File

@@ -2,13 +2,16 @@
#![allow(missing_docs)]
use std::sync::LazyLock;
use deltachat_derive::{FromSql, ToSql};
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: &str = env!("CARGO_PKG_VERSION");
pub static DC_VERSION_STR: LazyLock<String> =
LazyLock::new(|| env!("CARGO_PKG_VERSION").to_string());
/// Set of characters to percent-encode in email addresses and names.
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');

View File

@@ -36,7 +36,7 @@ use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -115,23 +115,9 @@ impl ContactId {
let row = context
.sql
.transaction(|transaction| {
let authname;
let name_or_authname = if !name.is_empty() {
name
} else {
authname = transaction.query_row(
"SELECT authname FROM contacts WHERE id=?",
(self,),
|row| {
let authname: String = row.get(0)?;
Ok(authname)
},
)?;
&authname
};
let is_changed = transaction.execute(
"UPDATE contacts SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1",
(name, normalize_text(name_or_authname), self),
"UPDATE contacts SET name=?1 WHERE id=?2 AND name!=?1",
(name, self),
)? > 0;
if is_changed {
update_chat_names(context, transaction, self)?;
@@ -981,22 +967,11 @@ impl Contact {
} else {
row_name
};
let new_authname = if update_authname {
name.to_string()
} else {
row_authname
};
transaction.execute(
"UPDATE contacts SET name=?, name_normalized=?, addr=?, origin=?, authname=? WHERE id=?",
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
(
&new_name,
normalize_text(
if !new_name.is_empty() {
&new_name
} else {
&new_authname
}),
new_name,
if update_addr {
addr.to_string()
} else {
@@ -1007,7 +982,11 @@ impl Contact {
} else {
row_origin
},
&new_authname,
if update_authname {
name.to_string()
} else {
row_authname
},
row_id,
),
)?;
@@ -1019,18 +998,18 @@ impl Contact {
sth_modified = Modifier::Modified;
}
} else {
let update_name = manual;
let update_authname = !manual;
transaction.execute(
"
INSERT INTO contacts (name, name_normalized, addr, fingerprint, origin, authname)
VALUES (?, ?, ?, ?, ?, ?)
",
"INSERT INTO contacts (name, addr, fingerprint, origin, authname)
VALUES (?, ?, ?, ?, ?);",
(
if manual { &name } else { "" },
normalize_text(&name),
if update_name { &name } else { "" },
&addr,
fingerprint,
origin,
if manual { "" } else { &name },
if update_authname { &name } else { "" },
),
)?;
@@ -1133,26 +1112,23 @@ VALUES (?, ?, ?, ?, ?, ?)
Origin::IncomingReplyTo
};
if query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or("").to_lowercase());
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
.sql
.query_map(
"
SELECT c.id, c.addr FROM contacts c
WHERE c.id>?
AND (c.fingerprint='')=?
AND c.origin>=?
AND c.blocked=0
AND (IFNULL(c.name_normalized,IIF(c.name='',c.authname,c.name)) LIKE ? OR c.addr LIKE ?)
ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
",
"SELECT c.id, c.addr FROM contacts c
WHERE c.id>?
AND (c.fingerprint='')=?
AND c.origin>=? \
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
ORDER BY c.last_seen DESC, c.id DESC;",
(
ContactId::LAST_SPECIAL,
flag_address,
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
Origin::CreateChat,
),
|row| {
let id: ContactId = row.get(0)?;
@@ -1202,13 +1178,8 @@ ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
AND (fingerprint='')=?
AND origin>=?
AND blocked=0
ORDER BY origin>=? DESC, last_seen DESC, id DESC",
(
ContactId::LAST_SPECIAL,
flag_address,
minimal_origin,
Origin::CreateChat,
),
ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL, flag_address, minimal_origin),
|row| {
let id: ContactId = row.get(0)?;
let addr: String = row.get(1)?;
@@ -1278,18 +1249,8 @@ ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
};
// Always do an update in case the blocking is reset or name is changed.
transaction.execute(
"
UPDATE contacts
SET name=?, name_normalized=IIF(?1='',name_normalized,?), origin=?, blocked=1, fingerprint=?
WHERE addr=?
",
(
&name,
normalize_text(&name),
Origin::MailinglistAddress,
fingerprint,
&grpid,
),
"UPDATE contacts SET name=?, origin=?, blocked=1, fingerprint=? WHERE addr=?",
(&name, Origin::MailinglistAddress, fingerprint, &grpid),
)?;
}
Ok(())
@@ -1658,7 +1619,8 @@ WHERE addr=?
///
/// If this returns Some(_),
/// display green checkmark in the profile and "Introduced by ..." line
/// with the name of the contact.
/// with the name and address of the contact
/// formatted by [Self::get_name_n_addr].
///
/// If this returns `Some(None)`, then the contact is verified,
/// but it's unclear by whom.
@@ -1763,8 +1725,8 @@ fn update_chat_names(
};
let count = transaction.execute(
"UPDATE chats SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1",
(&chat_name, normalize_text(&chat_name), chat_id),
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
(chat_name, chat_id),
)?;
if count > 0 {

View File

@@ -60,16 +60,16 @@ async fn test_get_contacts() -> Result<()> {
let context = tcm.bob().await;
let alice = tcm.alice().await;
alice
.set_config(Config::Displayname, Some("MyNameIsΔ"))
.set_config(Config::Displayname, Some("MyName"))
.await?;
// Alice is not in the contacts yet.
let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&context.ctx, 0, Some("MyNameIsΔ")).await?;
let contacts = Contact::get_all(&context.ctx, 0, Some("MyName")).await?;
assert_eq!(contacts.len(), 0);
let claire_id = Contact::create(&context, "Δ-someone", "claire@example.org").await?;
let claire_id = Contact::create(&context, "someone", "claire@example.org").await?;
let dave_id = Contact::create(&context, "", "dave@example.org").await?;
let id = context.add_or_lookup_contact_id(&alice).await;
@@ -77,8 +77,8 @@ async fn test_get_contacts() -> Result<()> {
let contact = Contact::get_by_id(&context, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "MyNameIsΔ");
assert_eq!(contact.get_display_name(), "MyNameIsΔ");
assert_eq!(contact.get_authname(), "MyName");
assert_eq!(contact.get_display_name(), "MyName");
// Search by name.
let contacts = Contact::get_all(&context, 0, Some("myname")).await?;
@@ -93,12 +93,12 @@ async fn test_get_contacts() -> Result<()> {
let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?;
assert_eq!(contacts.len(), 0);
// Set Alice name manually.
id.set_name(&context, "Δ-someone").await?;
// Set Alice name to "someone" manually.
id.set_name(&context, "someone").await?;
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "Δ-someone");
assert_eq!(contact.get_authname(), "MyNameIsΔ");
assert_eq!(contact.get_display_name(), "Δ-someone");
assert_eq!(contact.get_name(), "someone");
assert_eq!(contact.get_authname(), "MyName");
assert_eq!(contact.get_display_name(), "someone");
// Not searchable by authname, because it is not displayed.
let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
@@ -108,9 +108,7 @@ async fn test_get_contacts() -> Result<()> {
info!(&context, "add_self={add_self}");
// Search key-contacts by display name (same as manually set name).
let contacts = Contact::get_all(&context.ctx, add_self, Some("Δ-someone")).await?;
assert_eq!(contacts, vec![id]);
let contacts = Contact::get_all(&context.ctx, add_self, Some("δ-someon")).await?;
let contacts = Contact::get_all(&context.ctx, add_self, Some("someone")).await?;
assert_eq!(contacts, vec![id]);
// Get all key-contacts.
@@ -122,7 +120,7 @@ async fn test_get_contacts() -> Result<()> {
}
// Search address-contacts by display name.
let contacts = Contact::get_all(&context, constants::DC_GCL_ADDRESS, Some("Δ-someone")).await?;
let contacts = Contact::get_all(&context, constants::DC_GCL_ADDRESS, Some("someone")).await?;
assert_eq!(contacts, vec![claire_id]);
// Get all address-contacts. Newer contacts go first.
@@ -136,16 +134,6 @@ async fn test_get_contacts() -> Result<()> {
.await?;
assert_eq!(contacts, vec![dave_id, claire_id, ContactId::SELF]);
// Reset the user-provided name for Alice.
id.set_name(&context, "").await?;
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "MyNameIsΔ");
assert_eq!(contact.get_display_name(), "MyNameIsΔ");
let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&context, 0, Some("δ")).await?;
assert_eq!(contacts.len(), 1);
Ok(())
}

View File

@@ -5,7 +5,7 @@ use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock, Weak};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use anyhow::{Context as _, Result, bail, ensure};
@@ -23,6 +23,7 @@ use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::self_fingerprint;
use crate::log::warn;
use crate::logged_debug_assert;
use crate::login_param::EnteredLoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::net::tls::TlsSessionStore;
use crate::peer_channels::Iroh;
@@ -200,25 +201,6 @@ impl Deref for Context {
}
}
/// A weak reference to a [`Context`]
///
/// Can be used to obtain a [`Context`]. An existing weak reference does not prevent the corresponding [`Context`] from being dropped.
#[derive(Clone, Debug)]
pub(crate) struct WeakContext {
inner: Weak<InnerContext>,
}
impl WeakContext {
/// Returns the [`Context`] if it is still available.
pub(crate) fn upgrade(&self) -> Result<Context> {
let inner = self
.inner
.upgrade()
.ok_or_else(|| anyhow::anyhow!("Inner struct has been dropped"))?;
Ok(Context { inner })
}
}
/// Actual context, expensive to clone.
#[derive(Debug)]
pub struct InnerContext {
@@ -243,9 +225,9 @@ pub struct InnerContext {
pub(crate) scheduler: SchedulerState,
pub(crate) ratelimit: RwLock<Ratelimit>,
/// Recently loaded quota information for each trasnport, if any.
/// If quota was never tried to load, then the transport doesn't have an entry in the BTreeMap.
pub(crate) quota: RwLock<BTreeMap<u32, QuotaInfo>>,
/// Recently loaded quota information, if any.
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
/// Notify about new messages.
///
@@ -351,7 +333,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
#[cfg(not(debug_assertions))]
res.insert("debug_assertions", "Off".to_string());
res.insert("deltachat_core_version", format!("v{DC_VERSION_STR}"));
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("num_cpus", num_cpus::get().to_string());
@@ -403,13 +385,6 @@ impl Context {
Ok(context)
}
/// Returns a weak reference to this [`Context`].
pub(crate) fn get_weak_context(&self) -> WeakContext {
WeakContext {
inner: Arc::downgrade(&self.inner),
}
}
/// Opens the database with the given passphrase.
/// NB: Db encryption is deprecated, so `passphrase` should be empty normally. See
/// [`ContextBuilder::with_password()`] for reasoning.
@@ -479,7 +454,7 @@ impl Context {
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3.
quota: RwLock::new(BTreeMap::new()),
quota: RwLock::new(None),
new_msgs_notify,
server_id: RwLock::new(None),
metadata: RwLock::new(None),
@@ -614,13 +589,8 @@ impl Context {
}
// Update quota (to send warning if full) - but only check it once in a while.
// note: For now this only checks quota of primary transport,
// because background check only checks primary transport at the moment
if self
.quota_needs_update(
session.transport_id(),
DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT,
)
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
.await
&& let Err(err) = self.update_recent_quota(&mut session).await
{
@@ -820,17 +790,12 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let l = EnteredLoginParam::load(self).await?;
let l2 = ConfiguredLoginParam::load(self).await?.map_or_else(
|| "Not configured".to_string(),
|(_transport_id, param)| param.to_string(),
);
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
.await?
.into_iter()
.map(|(transport_id, param)| format!("{transport_id}: {param}"))
.collect();
let all_transports = if all_transports.is_empty() {
"Not configured".to_string()
} else {
all_transports.join(",")
};
let chats = get_chat_cnt(self).await?;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
let request_msgs = message::get_request_msg_cnt(self).await;
@@ -909,7 +874,8 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("proxy_enabled", proxy_enabled.to_string());
res.insert("used_transport_settings", all_transports);
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2);
if let Some(server_id) = &*self.server_id.read().await {
res.insert("imap_server_id", format!("{server_id:?}"));
@@ -954,10 +920,6 @@ impl Context {
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert(
"who_can_call_me",
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
);
res.insert(
"download_limit",
self.get_config_int(Config::DownloadLimit)
@@ -1106,10 +1068,6 @@ impl Context {
.await?
.unwrap_or_default(),
);
res.insert(
"team_profile",
self.get_config_bool(Config::TeamProfile).await?.to_string(),
);
let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed));
@@ -1347,5 +1305,10 @@ impl Context {
}
}
/// Returns core version as a string.
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
}
#[cfg(test)]
mod context_tests;

View File

@@ -153,15 +153,11 @@ pub(crate) async fn download_msg(
return Ok(());
};
let transport_id = session.transport_id();
let row = context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap
WHERE rfc724_mid=?
AND transport_id=?
AND target!=''",
(&msg.rfc724_mid, transport_id),
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;

View File

@@ -8,27 +8,33 @@ use mail_builder::mime::MimePart;
use crate::aheader::{Aheader, EncryptPreference};
use crate::context::Context;
use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key};
use crate::pgp::{self, SeipdVersion};
use crate::pgp;
#[derive(Debug)]
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
pub addr: String,
pub public_key: SignedPublicKey,
}
impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt = EncryptPreference::Mutual;
let addr = context.get_primary_self_addr().await?;
let public_key = load_self_public_key(context).await?;
Ok(EncryptHelper { addr, public_key })
Ok(EncryptHelper {
prefer_encrypt,
addr,
public_key,
})
}
pub fn get_aheader(&self) -> Aheader {
Aheader {
addr: self.addr.clone(),
public_key: self.public_key.clone(),
prefer_encrypt: EncryptPreference::Mutual,
prefer_encrypt: self.prefer_encrypt,
verified: false,
}
}
@@ -41,7 +47,6 @@ impl EncryptHelper {
mail_to_encrypt: MimePart<'static>,
compress: bool,
anonymous_recipients: bool,
seipd_version: SeipdVersion,
) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
@@ -55,7 +60,6 @@ impl EncryptHelper {
sign_key,
compress,
anonymous_recipients,
seipd_version,
)
.await?;

View File

@@ -30,10 +30,9 @@ pub(crate) async fn emit_chatlist_item_changed_for_contact_chat(
match ChatId::lookup_by_contact(context, contact_id).await {
Ok(Some(chat_id)) => self::emit_chatlist_item_changed(context, chat_id),
Ok(None) => {}
Err(error) => error!(
context,
Err(error) => context.emit_event(EventType::Error(format!(
"failed to find chat id for contact for chatlist event: {error:?}"
),
))),
}
}

View File

@@ -243,7 +243,7 @@ pub enum EventType {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: u16,
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
@@ -253,7 +253,7 @@ pub enum EventType {
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
ImexProgress(u16),
ImexProgress(usize),
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
@@ -280,7 +280,7 @@ pub enum EventType {
chat_type: Chattype,
/// Progress, always 1000.
progress: u16,
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
@@ -295,7 +295,7 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received
progress: u16,
progress: usize,
},
/// The connectivity to the server changed.
@@ -417,15 +417,6 @@ pub enum EventType {
chat_id: ChatId,
},
/// One or more transports has changed.
///
/// UI should update the list.
///
/// This event is emitted when transport
/// synchronization messages arrives,
/// but not when the UI modifies the transport list by itself.
TransportsModified,
/// Event for using in tests, e.g. as a fence between normally generated events.
#[cfg(test)]
Test,

View File

@@ -27,7 +27,7 @@ use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{self, Blocked, DC_VERSION_STR, ShowEmails};
use crate::constants::{self, Blocked, Chattype, ShowEmails};
use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::events::EventType;
@@ -123,7 +123,7 @@ struct OAuth2 {
access_token: String,
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub(crate) struct ServerMetadata {
/// IMAP METADATA `/shared/comment` as defined in
/// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1>.
@@ -916,7 +916,7 @@ impl Session {
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
for (uid, (rfc724_mid, target)) in &msgs {
// This may detect previously undetected moved
// messages, so we update server_folder too.
@@ -1054,16 +1054,14 @@ impl Session {
///
/// 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<()> {
let transport_id = self.transport_id();
let rows = context
.sql
.query_map_vec(
"SELECT id, uid, target FROM imap
WHERE folder = ?
AND transport_id = ?
AND target != folder
ORDER BY target, uid",
(folder, transport_id),
WHERE folder = ?
AND target != folder
ORDER BY target, uid",
(folder,),
|row| {
let rowid: i64 = row.get(0)?;
let uid: u32 = row.get(1)?;
@@ -1110,13 +1108,54 @@ impl Session {
Ok(())
}
/// Uploads sync messages from the `imap_send` table with `\Seen` flag set.
pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
context.send_sync_msg().await?;
while let Some((id, mime, msg_id, attempts)) = context
.sql
.query_row_optional(
"SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
(),
|row| {
let id: i64 = row.get(0)?;
let mime: String = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
let attempts: i64 = row.get(3)?;
Ok((id, mime, msg_id, attempts))
},
)
.await
.context("Failed to SELECT from imap_send")?
{
let res = self
.append(folder, Some("(\\Seen)"), None, mime)
.await
.with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
.log_err(context);
if res.is_ok() {
msg_id.set_delivered(context).await?;
}
const MAX_ATTEMPTS: i64 = 2;
if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
context
.sql
.execute("DELETE FROM imap_send WHERE id=?", (id,))
.await
.context("Failed to delete from imap_send")?;
} else {
context
.sql
.execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
.await
.context("Failed to update imap_send.attempts")?;
res?;
}
}
Ok(())
}
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
if context.get_config_bool(Config::TeamProfile).await? {
info!(context, "Team profile, skipping seen flag synchronization.");
return Ok(());
}
let rows = context
.sql
.query_map_vec(
@@ -1185,11 +1224,6 @@ impl Session {
return Ok(());
}
if context.get_config_bool(Config::TeamProfile).await? {
info!(context, "Team profile, skipping seen flag synchronization.");
return Ok(());
}
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
@@ -1243,10 +1277,10 @@ impl Session {
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
if is_seen
&& let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
.await
.with_context(|| {
format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
format!("failed to update seen status for msg {folder}/{uid}")
})?
{
updated_chat_ids.insert(chat_id);
@@ -1466,7 +1500,7 @@ impl Session {
warn!(context, "receive_imf error: {err:#}.");
let text = format!(
"❌ Failed to receive a message: {err:#}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/.",
"❌ Failed to receive a message: {err:#}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
);
let mut msg = Message::new_text(text);
add_device_msg(context, None, Some(&mut msg)).await?;
@@ -1512,17 +1546,17 @@ impl Session {
Ok(())
}
/// Retrieves server metadata if it is supported, otherwise uses fallback one.
/// Retrieves server metadata if it is supported.
///
/// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
/// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
/// metadata.
pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
let mut lock = context.metadata.write().await;
pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
if !self.can_metadata() {
*lock = Some(Default::default());
return Ok(());
}
let mut lock = context.metadata.write().await;
if let Some(ref mut old_metadata) = *lock {
let now = time();
@@ -1531,33 +1565,31 @@ impl Session {
return Ok(());
}
info!(context, "ICE servers expired, requesting new credentials.");
let mailbox = "";
let options = "";
let metadata = self
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
.await?;
let mut got_turn_server = false;
if self.can_metadata() {
info!(context, "ICE servers expired, requesting new credentials.");
let mailbox = "";
let options = "";
let metadata = self
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
.await?;
for m in metadata {
if m.entry == "/shared/vendor/deltachat/turn"
&& let Some(value) = m.value
{
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = true;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
for m in metadata {
if m.entry == "/shared/vendor/deltachat/turn"
&& let Some(value) = m.value
{
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = false;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
}
}
}
if !got_turn_server {
info!(context, "Will use fallback ICE servers.");
// Set expiration timestamp 7 days in the future so we don't request it again.
old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
@@ -2077,6 +2109,17 @@ async fn needs_move_to_mvbox(
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
if !context.get_config_bool(Config::IsChatmail).await?
&& has_chat_version
&& headers
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
.is_some()
&& let Some(from) = mimeparser::get_from(headers)
&& context.is_self_addr(&from.addr).await?
{
return Ok(true);
}
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
@@ -2212,6 +2255,21 @@ pub(crate) fn create_message_id() -> String {
format!("{}{}", GENERATED_PREFIX, create_id())
}
/// Returns chat by prefetched headers.
async fn prefetch_get_chat(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<chat::Chat>> {
let parent = get_prefetch_parent_message(context, headers).await?;
if let Some(parent) = &parent {
return Ok(Some(
chat::Chat::load_from_db(context, parent.get_chat_id()).await?,
));
}
Ok(None)
}
/// Determines whether the message should be downloaded based on prefetched headers.
pub(crate) async fn prefetch_should_download(
context: &Context,
@@ -2230,6 +2288,15 @@ pub(crate) async fn prefetch_should_download(
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
// the further process).
if let Some(chat) = prefetch_get_chat(context, headers).await?
&& chat.typ == Chattype::Group
&& !chat.id.is_special()
{
// This might be a group command, like removing a group member.
// We really need to fetch this to avoid inconsistent group state.
return Ok(true);
}
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
@@ -2290,7 +2357,6 @@ pub(crate) async fn prefetch_should_download(
/// Returns updated chat ID if any message was marked as seen.
async fn mark_seen_by_uid(
context: &Context,
transport_id: u32,
folder: &str,
uid_validity: u32,
uid: u32,
@@ -2301,13 +2367,12 @@ async fn mark_seen_by_uid(
"SELECT id, chat_id FROM msgs
WHERE id > 9 AND rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE transport_id=?
AND folder=?
AND uidvalidity=?
AND uid=?
WHERE folder=?1
AND uidvalidity=?2
AND uid=?3
LIMIT 1
)",
(transport_id, &folder, uid_validity, uid),
(&folder, uid_validity, uid),
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;

View File

@@ -1,6 +1,5 @@
use super::*;
use crate::test_utils::TestContext;
use crate::transport::add_pseudo_transport;
#[test]
fn test_get_folder_meaning_by_name() {
@@ -272,14 +271,12 @@ async fn test_get_imap_search_command() -> Result<()> {
r#"FROM "alice@example.org""#
);
add_pseudo_transport(&t, "alice@another.com").await?;
t.ctx.set_primary_self_addr("alice@another.com").await?;
assert_eq!(
get_imap_self_sent_search_command(&t.ctx).await?,
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
);
add_pseudo_transport(&t, "alice@third.com").await?;
t.ctx.set_primary_self_addr("alice@third.com").await?;
assert_eq!(
get_imap_self_sent_search_command(&t.ctx).await?,

View File

@@ -25,8 +25,7 @@ use crate::pgp;
use crate::qr::DCBACKUP_VERSION;
use crate::sql;
use crate::tools::{
TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, usize_to_u64,
write_file,
TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file,
};
mod key_transfer;
@@ -264,14 +263,14 @@ struct ProgressReader<R> {
inner: R,
/// Number of bytes successfully read from the internal reader.
read: u64,
read: usize,
/// Total size of the backup .tar file expected to be read from the reader.
/// Used to calculate the progress.
file_size: u64,
file_size: usize,
/// Last progress emitted to avoid emitting the same progress value twice.
last_progress: u16,
last_progress: usize,
/// Context for emitting progress events.
context: Context,
@@ -282,7 +281,7 @@ impl<R> ProgressReader<R> {
Self {
inner: r,
read: 0,
file_size,
file_size: file_size as usize,
last_progress: 1,
context,
}
@@ -302,11 +301,9 @@ where
let before = buf.filled().len();
let res = this.inner.poll_read(cx, buf);
if let std::task::Poll::Ready(Ok(())) = res {
*this.read = this
.read
.saturating_add(usize_to_u64(buf.filled().len() - before));
*this.read = this.read.saturating_add(buf.filled().len() - before);
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999) as u16;
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999);
if progress > *this.last_progress {
this.context.emit_event(EventType::ImexProgress(progress));
*this.last_progress = progress;
@@ -493,14 +490,14 @@ struct ProgressWriter<W> {
inner: W,
/// Number of bytes successfully written into the internal writer.
written: u64,
written: usize,
/// Total size of the backup .tar file expected to be written into the writer.
/// Used to calculate the progress.
file_size: u64,
file_size: usize,
/// Last progress emitted to avoid emitting the same progress value twice.
last_progress: u16,
last_progress: usize,
/// Context for emitting progress events.
context: Context,
@@ -511,7 +508,7 @@ impl<W> ProgressWriter<W> {
Self {
inner: w,
written: 0,
file_size,
file_size: file_size as usize,
last_progress: 1,
context,
}
@@ -530,9 +527,9 @@ where
let this = self.project();
let res = this.inner.poll_write(cx, buf);
if let std::task::Poll::Ready(Ok(written)) = res {
*this.written = this.written.saturating_add(usize_to_u64(written));
*this.written = this.written.saturating_add(written);
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999) as u16;
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999);
if progress > *this.last_progress {
this.context.emit_event(EventType::ImexProgress(progress));
*this.last_progress = progress;

View File

@@ -15,17 +15,16 @@ use pin_project::pin_project;
use crate::events::{Event, EventType, Events};
use crate::net::session::SessionStream;
use crate::tools::usize_to_u64;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
#[derive(Debug)]
struct Metrics {
/// Total number of bytes read.
pub total_read: u64,
pub total_read: usize,
/// Total number of bytes written.
pub total_written: u64,
pub total_written: usize,
}
impl Metrics {
@@ -92,11 +91,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),
@@ -104,7 +98,7 @@ impl<S: SessionStream> AsyncRead for LoggingStream<S> {
}
let n = old_remaining - buf.remaining();
this.metrics.total_read = this.metrics.total_read.saturating_add(usize_to_u64(n));
this.metrics.total_read = this.metrics.total_read.saturating_add(n);
res
}
@@ -119,7 +113,7 @@ impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
let this = self.project();
let res = this.inner.poll_write(cx, buf);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(usize_to_u64(n));
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
}
res
}
@@ -146,7 +140,7 @@ impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
let this = self.project();
let res = this.inner.poll_write_vectored(cx, bufs);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(usize_to_u64(n));
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
}
res
}

View File

@@ -171,17 +171,12 @@ impl MsgId {
context
.sql
.query_map_vec(
"SELECT transports.addr, imap.folder, imap.uid
FROM imap
LEFT JOIN transports
ON transports.id = imap.transport_id
WHERE imap.rfc724_mid=?",
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
(rfc724_mid,),
|row| {
let addr: String = row.get(0)?;
let folder: String = row.get(1)?;
let uid: u32 = row.get(2)?;
Ok(format!("<{addr}/{folder}/;UID={uid}>"))
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok(format!("</{folder}/;UID={uid}>"))
},
)
.await
@@ -850,10 +845,11 @@ impl Message {
let contact = if self.from_id != ContactId::SELF {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
Some(Contact::get_by_id(context, self.from_id).await?)
}
Chattype::Single | Chattype::OutBroadcast | Chattype::InBroadcast => None,
Chattype::Group
| Chattype::OutBroadcast
| Chattype::InBroadcast
| Chattype::Mailinglist => Some(Contact::get_by_id(context, self.from_id).await?),
Chattype::Single => None,
}
} else {
None
@@ -1716,7 +1712,6 @@ pub async fn delete_msgs_ex(
msgs: deleted_rfc724_mid,
})
.await?;
context.scheduler.interrupt_smtp().await;
}
for &msg_id in msg_ids {
@@ -2253,5 +2248,14 @@ impl Viewtype {
}
}
/// Returns text for storing in the `msgs.txt_normalized` column (to make case-insensitive search
/// possible for non-ASCII messages).
pub(crate) fn normalize_text(text: &str) -> Option<String> {
if text.is_ascii() {
return None;
};
Some(text.to_lowercase()).filter(|t| t != text)
}
#[cfg(test)]
mod message_tests;

View File

@@ -32,7 +32,6 @@ use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{SystemMessage, is_hidden};
use crate::param::Param;
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
use crate::pgp::SeipdVersion;
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use crate::tools::{
@@ -1259,17 +1258,6 @@ impl MimeFactory {
} else {
// Asymmetric encryption
let seipd_version = if encryption_pubkeys.is_empty() {
// If message is sent only to self,
// use v2 SEIPD.
SeipdVersion::V2
} else {
// If message is sent to others,
// they may not support v2 SEIPD yet,
// so use v1 SEIPD.
SeipdVersion::V1
};
// Encrypt to self unconditionally,
// even for a single-device setup.
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
@@ -1283,7 +1271,6 @@ impl MimeFactory {
message,
compress,
anonymous_recipients,
seipd_version,
)
.await?
};

View File

@@ -40,7 +40,7 @@
//! used for successful connection timestamp of
//! retrieving them from in-memory cache is used.
use anyhow::{Context as _, Result, ensure};
use anyhow::{Context as _, Result};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
@@ -506,6 +506,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"mail.nubo.coop",
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
),
(
"mehl.cloud",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
),
(
"mx.freenet.de",
vec![
@@ -676,72 +680,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 164)),
],
),
// Known public chatmail relays from https://chatmail.at/relays
(
"mehl.cloud",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
),
(
"mailchat.pl",
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 144, 137))],
),
(
"chatmail.woodpeckersnest.space",
vec![IpAddr::V4(Ipv4Addr::new(85, 215, 162, 146))],
),
(
"chatmail.culturanerd.it",
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 94, 165))],
),
(
"chatmail.hackea.org",
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 11, 85))],
),
(
"chika.aangat.lahat.computer",
vec![IpAddr::V4(Ipv4Addr::new(71, 19, 150, 113))],
),
(
"tarpit.fun",
vec![IpAddr::V4(Ipv4Addr::new(152, 53, 86, 246))],
),
(
"d.gaufr.es",
vec![IpAddr::V4(Ipv4Addr::new(51, 77, 140, 91))],
),
(
"chtml.ca",
vec![IpAddr::V4(Ipv4Addr::new(51, 222, 156, 177))],
),
(
"chatmail.au",
vec![IpAddr::V4(Ipv4Addr::new(45, 124, 54, 79))],
),
(
"sombras.chat",
vec![IpAddr::V4(Ipv4Addr::new(82, 25, 70, 154))],
),
(
"e2ee.wang",
vec![IpAddr::V4(Ipv4Addr::new(139, 84, 233, 161))],
),
(
"chat.privittytech.com",
vec![IpAddr::V4(Ipv4Addr::new(35, 154, 144, 0))],
),
("e2ee.im", vec![IpAddr::V4(Ipv4Addr::new(45, 137, 99, 57))]),
(
"chatmail.email",
vec![IpAddr::V4(Ipv4Addr::new(57, 128, 220, 120))],
),
(
"danneskjold.de",
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 216, 132))],
),
(
"darkrun.dev",
vec![IpAddr::V4(Ipv4Addr::new(72, 11, 149, 146))],
),
])
});
@@ -850,7 +788,7 @@ pub(crate) async fn lookup_host_with_cache(
}
};
let addrs = if load_cache {
if load_cache {
let mut cache = lookup_cache(context, hostname, port, alpn, now).await?;
if let Some(ips) = DNS_PRELOAD.get(hostname) {
for ip in ips {
@@ -861,15 +799,10 @@ pub(crate) async fn lookup_host_with_cache(
}
}
merge_with_cache(resolved_addrs, cache)
Ok(merge_with_cache(resolved_addrs, cache))
} else {
resolved_addrs
};
ensure!(
!addrs.is_empty(),
"Could not find DNS resolutions for {hostname}:{port}. Check server hostname and your network"
);
Ok(addrs)
Ok(resolved_addrs)
}
}
/// Merges results received from DNS with cached results.

View File

@@ -433,14 +433,6 @@ impl Params {
self.set(key, format!("{value}"));
self
}
pub fn steal(&mut self, src: &mut Self, key: Param) -> &mut Self {
let val = src.inner.remove(&key);
if let Some(val) = val {
self.inner.insert(key, val);
}
self
}
}
#[cfg(test)]

View File

@@ -160,20 +160,6 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey
.find(|subkey| subkey.is_encryption_key())
}
/// Version of SEIPD packet to use.
///
/// See
/// <https://www.rfc-editor.org/rfc/rfc9580#name-avoiding-ciphertext-malleab>
/// for the discussion on when v2 SEIPD should be used.
#[derive(Debug)]
pub enum SeipdVersion {
/// Use v1 SEIPD, for compatibility.
V1,
/// Use v2 SEIPD when we know that v2 SEIPD is supported.
V2,
}
/// Encrypts `plain` text using `public_keys_for_encryption`
/// and signs it using `private_key_for_signing`.
pub async fn pk_encrypt(
@@ -182,7 +168,6 @@ pub async fn pk_encrypt(
private_key_for_signing: SignedSecretKey,
compress: bool,
anonymous_recipients: bool,
seipd_version: SeipdVersion,
) -> Result<String> {
Handle::current()
.spawn_blocking(move || {
@@ -193,49 +178,21 @@ pub async fn pk_encrypt(
.filter_map(select_pk_for_encryption);
let msg = MessageBuilder::from_bytes("", plain);
let encoded_msg = match seipd_version {
SeipdVersion::V1 => {
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
for pkey in pkeys {
if anonymous_recipients {
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
} else {
msg.encrypt_to_key(&mut rng, &pkey)?;
}
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
msg.to_armored_string(&mut rng, Default::default())?
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
for pkey in pkeys {
if anonymous_recipients {
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
} else {
msg.encrypt_to_key(&mut rng, &pkey)?;
}
SeipdVersion::V2 => {
let mut msg = msg.seipd_v2(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
AeadAlgorithm::Ocb,
ChunkSize::C8KiB,
);
}
for pkey in pkeys {
if anonymous_recipients {
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
} else {
msg.encrypt_to_key(&mut rng, &pkey)?;
}
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
msg.to_armored_string(&mut rng, Default::default())?
}
};
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
Ok(encoded_msg)
})
@@ -590,7 +547,6 @@ mod tests {
KEYS.alice_secret.clone(),
compress,
anonymous_recipients,
SeipdVersion::V2,
)
.await
.unwrap()
@@ -760,7 +716,6 @@ mod tests {
KEYS.alice_secret.clone(),
true,
true,
SeipdVersion::V2,
)
.await?;

View File

@@ -16,6 +16,7 @@ use serde::Deserialize;
use crate::config::Config;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::key::Fingerprint;
use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam};
use crate::net::http::post_empty;
@@ -823,10 +824,9 @@ pub(crate) async fn login_param_from_account_qr(
match serde_json::from_str::<CreateAccountErrorResponse>(&response_text) {
Ok(error) => Err(anyhow!(error.reason)),
Err(parse_error) => {
error!(
context,
context.emit_event(EventType::Error(format!(
"Cannot create account, server response could not be parsed:\n{parse_error:#}\nraw response:\n{response_text}"
);
)));
bail!("Cannot create account, unexpected server response:\n{response_text:?}")
}
}
@@ -904,7 +904,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
.await?;
token::save(context, token::Namespace::Auth, None, &authcode, timestamp).await?;
context.sync_qr_code_tokens(None).await?;
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
}
Qr::ReviveVerifyGroup {
invitenumber,
@@ -936,7 +936,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
)
.await?;
context.sync_qr_code_tokens(Some(&grpid)).await?;
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
}
Qr::Login { address, options } => {
let mut param = login_param_from_login_qr(&address, options)?;

View File

@@ -107,10 +107,10 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
impl Context {
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
/// called.
pub(crate) async fn quota_needs_update(&self, transport_id: u32, ratelimit_secs: u64) -> bool {
pub(crate) async fn quota_needs_update(&self, ratelimit_secs: u64) -> bool {
let quota = self.quota.read().await;
quota
.get(&transport_id)
.as_ref()
.filter(|quota| time_elapsed(&quota.modified) < Duration::from_secs(ratelimit_secs))
.is_none()
}
@@ -155,13 +155,10 @@ impl Context {
}
}
self.quota.write().await.insert(
session.transport_id(),
QuotaInfo {
recent: quota,
modified: tools::Time::now(),
},
);
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
modified: tools::Time::now(),
});
self.emit_event(EventType::ConnectivityChanged);
Ok(())
@@ -206,42 +203,27 @@ mod tests {
let mut tcm = TestContextManager::new();
let t = &tcm.unconfigured().await;
const TIMEOUT: u64 = 60;
assert!(t.quota_needs_update(0, TIMEOUT).await);
assert!(t.quota_needs_update(TIMEOUT).await);
*t.quota.write().await = {
let mut map = BTreeMap::new();
map.insert(
0,
QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
},
);
map
};
assert!(t.quota_needs_update(0, TIMEOUT).await);
*t.quota.write().await = Some(QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
});
assert!(t.quota_needs_update(TIMEOUT).await);
*t.quota.write().await = {
let mut map = BTreeMap::new();
map.insert(
0,
QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now(),
},
);
map
};
assert!(!t.quota_needs_update(0, TIMEOUT).await);
*t.quota.write().await = Some(QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now(),
});
assert!(!t.quota_needs_update(TIMEOUT).await);
t.evtracker.clear_events();
t.set_primary_self_addr("new@addr").await?;
assert!(t.quota.read().await.is_empty());
assert!(t.quota.read().await.is_none());
t.evtracker
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
.await;
assert!(t.quota_needs_update(0, TIMEOUT).await);
assert!(t.quota_needs_update(TIMEOUT).await);
Ok(())
}
}

View File

@@ -38,16 +38,12 @@ use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
use crate::reaction::{Reaction, set_msg_reaction};
use crate::rusqlite::OptionalExtension;
use crate::securejoin::{
self, get_secure_join_step, handle_securejoin_handshake, observe_securejoin_on_other_device,
};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::simplify;
use crate::stats::STATISTICS_BOT_EMAIL;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{
self, buf_compress, normalize_text, remove_subject_prefix, validate_broadcast_secret,
};
use crate::tools::{self, buf_compress, remove_subject_prefix, validate_broadcast_secret};
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
/// This is the struct that is returned after receiving one email (aka MIME message).
@@ -675,26 +671,16 @@ pub(crate) async fn receive_imf_inner(
.await?;
let received_msg;
if let Some(_step) = get_secure_join_step(&mime_parser) {
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
let res = if mime_parser.incoming {
handle_securejoin_handshake(context, &mut mime_parser, from_id)
.await
.with_context(|| {
format!(
"Error in Secure-Join '{}' message handling",
mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
)
})?
.context("error in Secure-Join message handling")?
} else if let Some(to_id) = to_ids.first().copied().flatten() {
// handshake may mark contacts as verified and must be processed before chats are created
observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await
.with_context(|| {
format!(
"Error in Secure-Join '{}' watching",
mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
)
})?
.context("error in Secure-Join watching")?
} else {
securejoin::HandshakeMessage::Propagate
};
@@ -841,41 +827,6 @@ pub(crate) async fn receive_imf_inner(
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
// Receiving encrypted message from self updates primary transport.
let from_addr = &mime_parser.from.addr;
let transport_changed = context
.sql
.transaction(|transaction| {
let transport_exists = transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(from_addr,),
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)?;
let transport_changed = if transport_exists {
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(from_addr,),
)? > 0
} else {
warn!(
context,
"Received sync message from unknown address {from_addr:?}."
);
false
};
Ok(transport_changed)
})
.await?;
if transport_changed {
info!(context, "Primary transport changed to {from_addr:?}.");
context.sql.uncache_raw_config("configured_addr").await;
}
context
.execute_sync_items(sync_items, mime_parser.timestamp_sent)
.await;
@@ -2108,7 +2059,7 @@ RETURNING id
if trash { MessageState::Undefined } else { state },
if trash { MessengerMessage::No } else { is_dc_message },
if trash || hidden { "" } else { msg },
if trash || hidden { None } else { normalize_text(msg) },
if trash || hidden { None } else { message::normalize_text(msg) },
if trash || hidden { "" } else { &subject },
if trash {
"".to_string()
@@ -2520,11 +2471,10 @@ async fn lookup_or_create_adhoc_group(
id INTEGER PRIMARY KEY
) STRICT",
(),
)
.context("CREATE TEMP TABLE temp.contacts")?;
)?;
let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
for &id in &contact_ids {
stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
stmt.execute((id,))?;
}
let val = t
.query_row(
@@ -2546,10 +2496,8 @@ async fn lookup_or_create_adhoc_group(
Ok((id, blocked))
},
)
.optional()
.context("Select chat with matching name and members")?;
t.execute("DROP TABLE temp.contacts", ())
.context("DROP TABLE temp.contacts")?;
.optional()?;
t.execute("DROP TABLE temp.contacts", ())?;
Ok(val)
};
let query_only = true;
@@ -3105,10 +3053,7 @@ async fn apply_chat_name_and_avatar_changes(
info!(context, "Updating grpname for chat {}.", chat.id);
context
.sql
.execute(
"UPDATE chats SET name=?, name_normalized=? WHERE id=?",
(grpname, normalize_text(grpname), chat.id),
)
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
.await?;
*send_event_chat_modified = true;
}
@@ -3397,10 +3342,7 @@ async fn apply_mailinglist_changes(
info!(context, "Updating listname for chat {chat_id}.");
context
.sql
.execute(
"UPDATE chats SET name=?, name_normalized=? WHERE id=?",
(&new_name, normalize_text(&new_name), chat_id),
)
.execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}

View File

@@ -3852,38 +3852,6 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_contacts_goto_bottom() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id = create_group(alice, "Testing contact list").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
send_text_msg(alice, alice_chat_id, "hello".to_string()).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
assert_eq!(Contact::get_all(bob, 0, None).await?.len(), 0);
bob_chat_id.accept(bob).await?;
let contacts = Contact::get_all(bob, 0, None).await?;
assert_eq!(contacts.len(), 2);
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
assert_eq!(contacts[1], bob_fiona_id);
ChatId::create_for_contact(bob, bob_fiona_id).await?;
let contacts = Contact::get_all(bob, 0, None).await?;
assert_eq!(contacts.len(), 2);
assert_eq!(contacts[0], bob_fiona_id);
Ok(())
}
/// Test for the bug when remote group membership changes from outdated messages overrode local
/// ones. Especially that was a problem when a message is sent offline so that it doesn't
/// incorporate recent group membership changes.

View File

@@ -325,8 +325,6 @@ impl Drop for IoPausedGuard {
#[derive(Debug)]
struct SchedBox {
/// Hostname of used chatmail/email relay
host: String,
meaning: FolderMeaning,
conn_state: ImapConnectionState,
@@ -481,7 +479,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
}
// Update quota no more than once a minute.
if ctx.quota_needs_update(session.transport_id(), 60).await
if ctx.quota_needs_update(60).await
&& let Err(err) = ctx.update_recent_quota(&mut session).await
{
warn!(ctx, "Failed to update quota: {:#}.", err);
@@ -538,9 +536,9 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
.await
.context("Failed to download messages")?;
session
.update_metadata(ctx)
.fetch_metadata(ctx)
.await
.context("update_metadata")?;
.context("Failed to fetch metadata")?;
session
.register_token(ctx)
.await
@@ -572,6 +570,28 @@ async fn fetch_idle(
};
if folder_config == Config::ConfiguredInboxFolder {
let mvbox;
let syncbox = match ctx.should_move_sync_msgs().await? {
false => &watch_folder,
true => {
mvbox = ctx.get_config(Config::ConfiguredMvboxFolder).await?;
mvbox.as_deref().unwrap_or(&watch_folder)
}
};
if ctx
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default()
== connection.addr
{
session
.send_sync_msgs(ctx, syncbox)
.await
.context("fetch_idle: send_sync_msgs")
.log_err(ctx)
.ok();
}
session
.store_seen_flags_on_imap(ctx)
.await
@@ -861,14 +881,7 @@ impl Scheduler {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
};
let host = configured_login_param
.addr
.split("@")
.last()
.context("address has no host")?
.to_owned();
let inbox = SchedBox {
host: host.clone(),
meaning: FolderMeaning::Inbox,
conn_state,
handle,
@@ -884,7 +897,6 @@ impl Scheduler {
let meaning = FolderMeaning::Mvbox;
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
oboxes.push(SchedBox {
host,
meaning,
conn_state,
handle,

View File

@@ -373,13 +373,7 @@ impl Context {
InnerSchedulerState::Started(ref sched) => (
sched
.boxes()
.map(|b| {
(
b.host.clone(),
b.meaning,
b.conn_state.state.connectivity.clone(),
)
})
.map(|b| (b.meaning, b.conn_state.state.connectivity.clone()))
.collect::<Vec<_>>(),
sched.smtp.state.connectivity.clone(),
),
@@ -402,7 +396,7 @@ impl Context {
let watched_folders = get_watched_folder_configs(self).await?;
let incoming_messages = stock_str::incoming_messages(self).await;
ret += &format!("<h3>{incoming_messages}</h3><ul>");
for (host, folder, state) in &folders_states {
for (folder, state) in &folders_states {
let mut folder_added = false;
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
@@ -413,11 +407,7 @@ impl Context {
ret += "<li>";
ret += &*detailed.to_icon();
ret += " <b>";
if folder == &FolderMeaning::Inbox {
ret += &*escaper::encode_minimal(host);
} else {
ret += &*escaper::encode_minimal(&foldername);
}
ret += &*escaper::encode_minimal(&foldername);
ret += ":</b> ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "</li>";
@@ -462,41 +452,21 @@ impl Context {
// [======67%===== ]
// =============================================================================================
ret += "<h3>Message Buffers</h3>";
let transports = self
.sql
.query_map_vec("SELECT id, addr FROM transports", (), |row| {
let transport_id: u32 = row.get(0)?;
let addr: String = row.get(1)?;
Ok((transport_id, addr))
})
.await?;
let domain =
&deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
.domain;
let storage_on_domain =
escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await);
ret += &format!("<h3>{storage_on_domain}</h3><ul>");
let quota = self.quota.read().await;
ret += "<ul>";
for (transport_id, transport_addr) in transports {
let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
.map_or(transport_addr, |email| email.domain);
let domain_escaped = escaper::encode_minimal(domain);
let Some(quota) = quota.get(&transport_id) else {
let not_connected = stock_str::not_connected(self).await;
ret += &format!("<li>{domain_escaped} &middot; {not_connected}</li>");
continue;
};
if let Some(quota) = &*quota {
match &quota.recent {
Err(e) => {
let error_escaped = escaper::encode_minimal(&e.to_string());
ret += &format!("<li>{domain_escaped} &middot; {error_escaped}</li>");
}
Ok(quota) => {
if quota.is_empty() {
ret += &format!(
"<li>{domain_escaped} &middot; Warning: {domain_escaped} claims to support quota but gives no information</li>"
);
} else {
if !quota.is_empty() {
for (root_name, resources) in quota {
use async_imap::types::QuotaResourceName::*;
for resource in resources {
ret += &format!("<li>{domain_escaped} &middot; ");
ret += "<li>";
// root name is empty eg. for gmail and redundant eg. for riseup.
// therefore, use it only if there are really several roots.
@@ -559,9 +529,21 @@ impl Context {
ret += "</li>";
}
}
} else {
let domain_escaped = escaper::encode_minimal(domain);
ret += &format!(
"<li>Warning: {domain_escaped} claims to support quota but gives no information</li>"
);
}
}
Err(e) => {
let error_escaped = escaper::encode_minimal(&e.to_string());
ret += &format!("<li>{error_escaped}</li>");
}
}
} else {
let not_connected = stock_str::not_connected(self).await;
ret += &format!("<li>{not_connected}</li>");
}
ret += "</ul>";

View File

@@ -160,7 +160,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
context
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
.await?;
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
}
let chat_name = chat.get_name();
@@ -192,7 +192,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
.replace("%20", "+");
if sync_token {
context.sync_qr_code_tokens(None).await?;
context.scheduler.interrupt_smtp().await;
context.scheduler.interrupt_inbox().await;
}
format!(
"https://i.delta.chat/#{fingerprint}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}",
@@ -343,62 +343,6 @@ pub(crate) enum HandshakeMessage {
Propagate,
}
/// Step of Secure-Join protocol.
#[derive(Debug, Display, PartialEq, Eq)]
pub(crate) enum SecureJoinStep {
/// vc-request or vg-request
Request { invitenumber: String },
/// vc-auth-required or vg-auth-required
AuthRequired,
/// vc-request-with-auth or vg-request-with-auth
RequestWithAuth,
/// vc-contact-confirm
ContactConfirm,
/// vg-member-added
MemberAdded,
/// Deprecated step such as `vg-member-added-received` or `vc-contact-confirm-received`.
Deprecated,
/// Unknown step.
Unknown { step: String },
}
/// Parses message headers to find out which Secure-Join step the message represents.
///
/// Returns `None` if the message is not a Secure-Join message.
pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJoinStep> {
if let Some(invitenumber) = mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
// We do not care about presence of `Secure-Join: vc-request` or `Secure-Join: vg-request` header.
// This allows us to always treat `Secure-Join` header as protected and ignore it
// in the unencrypted part even though it is sent there for backwards compatibility.
Some(SecureJoinStep::Request {
invitenumber: invitenumber.to_string(),
})
} else if let Some(step) = mime_message.get_header(HeaderDef::SecureJoin) {
match step {
"vg-auth-required" | "vc-auth-required" => Some(SecureJoinStep::AuthRequired),
"vg-request-with-auth" | "vc-request-with-auth" => {
Some(SecureJoinStep::RequestWithAuth)
}
"vc-contact-confirm" => Some(SecureJoinStep::ContactConfirm),
"vg-member-added" => Some(SecureJoinStep::MemberAdded),
"vg-member-added-received" | "vc-contact-confirm-received" => {
Some(SecureJoinStep::Deprecated)
}
step => Some(SecureJoinStep::Unknown {
step: step.to_string(),
}),
}
} else {
None
}
}
/// Handle incoming secure-join handshake.
///
/// This function will update the securejoin state in the database as the protocol
@@ -418,8 +362,9 @@ pub(crate) async fn handle_securejoin_handshake(
if contact_id.is_special() {
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
let step = mime_message
.get_header(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
info!(context, "Received secure-join message {step:?}.");
@@ -438,7 +383,7 @@ pub(crate) async fn handle_securejoin_handshake(
// will improve security (completely unrelated to the securejoin protocol)
// and is something we want to do in the future:
// https://www.rfc-editor.org/rfc/rfc9580.html#name-surreptitious-forwarding
if !matches!(step, SecureJoinStep::Request { .. }) {
if !matches!(step, "vg-request" | "vc-request") {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for (addr, key) in &mime_message.gossiped_keys {
@@ -458,7 +403,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
match step {
SecureJoinStep::Request { ref invitenumber } => {
"vg-request" | "vc-request" => {
/*=======================================================
==== Alice - the inviter side ====
==== Step 3 in "Setup verified contact" protocol ====
@@ -468,6 +413,13 @@ pub(crate) async fn handle_securejoin_handshake(
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
let invitenumber = match mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing)");
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
warn!(context, "Secure-join denied (bad invitenumber).");
return Ok(HandshakeMessage::Ignore);
@@ -484,29 +436,24 @@ pub(crate) async fn handle_securejoin_handshake(
)
.await?;
let prefix = mime_message
.get_header(HeaderDef::SecureJoin)
.and_then(|step| step.get(..2))
.unwrap_or("vc");
// Alice -> Bob
send_alice_handshake_msg(
context,
autocrypt_contact_id,
&format!("{prefix}-auth-required"),
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
)
.await
.context("failed sending auth-required handshake message")?;
Ok(HandshakeMessage::Done)
}
SecureJoinStep::AuthRequired => {
"vg-auth-required" | "vc-auth-required" => {
/*========================================================
==== Bob - the joiner's side =====
==== Step 4 in "Setup verified contact" protocol =====
========================================================*/
bob::handle_auth_required(context, mime_message).await
}
SecureJoinStep::RequestWithAuth => {
"vg-request-with-auth" | "vc-request-with-auth" => {
/*==========================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
@@ -617,14 +564,14 @@ pub(crate) async fn handle_securejoin_handshake(
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
SecureJoinStep::ContactConfirm => {
"vc-contact-confirm" => {
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id,
progress: JoinerProgress::Succeeded.into_u16(),
progress: JoinerProgress::Succeeded.to_usize(),
});
Ok(HandshakeMessage::Ignore)
}
SecureJoinStep::MemberAdded => {
"vg-member-added" => {
let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
else {
warn!(
@@ -643,16 +590,17 @@ pub(crate) async fn handle_securejoin_handshake(
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id,
progress: JoinerProgress::Succeeded.into_u16(),
progress: JoinerProgress::Succeeded.to_usize(),
});
Ok(HandshakeMessage::Propagate)
}
SecureJoinStep::Deprecated => {
"vg-member-added-received" | "vc-contact-confirm-received" => {
// Deprecated steps, delete them immediately.
Ok(HandshakeMessage::Done)
}
SecureJoinStep::Unknown { ref step } => {
warn!(context, "Invalid SecureJoin step: {step:?}.");
_ => {
warn!(context, "invalid step: {}", step);
Ok(HandshakeMessage::Ignore)
}
}
@@ -683,20 +631,17 @@ pub(crate) async fn observe_securejoin_on_other_device(
if contact_id.is_special() {
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
let step = mime_message
.get_header(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
info!(context, "Observing secure-join message {step:?}.");
match step {
SecureJoinStep::Request { .. }
| SecureJoinStep::AuthRequired
| SecureJoinStep::Deprecated
| SecureJoinStep::Unknown { .. } => {
return Ok(HandshakeMessage::Ignore);
}
SecureJoinStep::RequestWithAuth
| SecureJoinStep::MemberAdded
| SecureJoinStep::ContactConfirm => {}
}
if !matches!(
step,
"vg-request-with-auth" | "vc-request-with-auth" | "vg-member-added" | "vc-contact-confirm"
) {
return Ok(HandshakeMessage::Ignore);
};
if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
warn!(
@@ -728,10 +673,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
if matches!(
step,
SecureJoinStep::MemberAdded | SecureJoinStep::ContactConfirm
) {
if step == "vg-member-added" || step == "vc-contact-confirm" {
let chat_type = if mime_message
.get_header(HeaderDef::ChatGroupMemberAdded)
.is_none()
@@ -754,14 +696,14 @@ pub(crate) async fn observe_securejoin_on_other_device(
inviter_progress(context, contact_id, chat_id, chat_type)?;
}
if matches!(step, SecureJoinStep::RequestWithAuth) {
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
// This actually reflects what happens on the first device (which does the secure
// join) and causes a subsequent "vg-member-added" message to create an unblocked
// verified group.
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
}
if matches!(step, SecureJoinStep::MemberAdded) {
if step == "vg-member-added" {
Ok(HandshakeMessage::Propagate)
} else {
Ok(HandshakeMessage::Ignore)

View File

@@ -5,16 +5,14 @@ use anyhow::{Context as _, Result};
use super::HandshakeMessage;
use super::qrinvite::QrInvite;
use crate::chat::{self, ChatId, is_contact_in_chat};
use crate::chatlist_events;
use crate::constants::{Blocked, Chattype};
use crate::contact::Origin;
use crate::context::Context;
use crate::events::EventType;
use crate::key::self_fingerprint;
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::param::Param;
use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint};
use crate::stock_str;
use crate::sync::Sync::*;
@@ -45,21 +43,31 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
// hidden, if a user starts sending messages in it it will be unhidden in
// receive_imf.
let private_chat_id = private_chat_id(context, &invite).await?;
let hidden = match invite {
QrInvite::Contact { .. } => Blocked::Not,
QrInvite::Group { .. } => Blocked::Yes,
QrInvite::Broadcast { .. } => Blocked::Yes,
};
// The 1:1 chat with the inviter
let private_chat_id =
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
context.emit_event(EventType::ContactsChanged(None));
let has_key = context
.sql
.exists(
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
(invite.fingerprint().hex(),),
)
.await?;
// Now start the protocol and initialise the state.
{
let has_key = context
.sql
.exists(
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
(invite.fingerprint().hex(),),
)
.await?;
// `joining_chat_id` is `Some` if group chat
// already exists and we are in the chat.
let joining_chat_id = match invite {
@@ -86,7 +94,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// Even if Alice is not verified, we don't send anything.
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::Succeeded.into_u16(),
progress: JoinerProgress::Succeeded.to_usize(),
});
return Ok(joining_chat_id);
} else if has_key
@@ -105,7 +113,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::RequestWithAuthSent.into_u16(),
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
});
} else {
send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
@@ -144,22 +152,20 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
Ok(joining_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it is
// used to send the handshake messages.
if !has_key {
chat::add_info_msg_with_cmd(
context,
private_chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
None,
time(),
None,
None,
None,
)
.await?;
}
// For setup-contact the BobState already ensured the 1:1 chat exists because it
// uses it to send the handshake messages.
chat::add_info_msg_with_cmd(
context,
private_chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
None,
time(),
None,
None,
None,
)
.await?;
Ok(private_chat_id)
}
}
@@ -169,9 +175,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
///
/// Returns the ID of the newly inserted entry.
async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
// The `chat_id` isn't actually needed anymore,
// but we still save it;
// can be removed as a future improvement.
context
.sql
.insert(
@@ -181,38 +184,6 @@ async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatI
.await
}
async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> {
if let Some((msg_id, param)) = context
.sql
.query_row_optional(
"
SELECT id, param FROM msgs
WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0)
AND chat_id=? AND hidden=0
LIMIT 1
",
(chat_id, chat_id),
|row| {
let id: MsgId = row.get(0)?;
let param: String = row.get(1)?;
let param: Params = param.parse().unwrap_or_default();
Ok((id, param))
},
)
.await?
&& param.get_cmd() == SystemMessage::SecurejoinWait
{
let on_server = false;
msg_id.trash(context, on_server).await?;
context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
context.emit_msgs_changed_without_msg_id(chat_id);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
}
Ok(())
}
/// Handles `vc-auth-required` and `vg-auth-required` handshake messages.
///
/// # Bob - the joiner's side
@@ -224,10 +195,11 @@ pub(super) async fn handle_auth_required(
// Load all Bob states that expect `vc-auth-required` or `vg-auth-required`.
let bob_states = context
.sql
.query_map_vec("SELECT id, invite FROM bobstate", (), |row| {
.query_map_vec("SELECT id, invite, chat_id FROM bobstate", (), |row| {
let row_id: i64 = row.get(0)?;
let invite: QrInvite = row.get(1)?;
Ok((row_id, invite))
let chat_id: ChatId = row.get(2)?;
Ok((row_id, invite, chat_id))
})
.await?;
@@ -237,7 +209,7 @@ pub(super) async fn handle_auth_required(
);
let mut auth_sent = false;
for (bobstate_row_id, invite) in bob_states {
for (bobstate_row_id, invite, chat_id) in bob_states {
if !encrypted_and_signed(context, message, invite.fingerprint()) {
continue;
}
@@ -248,12 +220,6 @@ pub(super) async fn handle_auth_required(
}
info!(context, "Fingerprint verified.",);
let chat_id = private_chat_id(context, &invite).await?;
delete_securejoin_wait_msg(context, chat_id)
.await
.context("delete_securejoin_wait_msg")
.log_err(context)
.ok();
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
context
.sql
@@ -274,7 +240,7 @@ pub(super) async fn handle_auth_required(
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::RequestWithAuthSent.into_u16(),
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
});
auth_sent = true;
@@ -382,22 +348,6 @@ impl BobHandshakeMsg {
}
}
/// Returns the 1:1 chat with the inviter.
///
/// This is the chat in which securejoin messages are sent.
/// The 1:1 chat will be created if it does not yet exist.
async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
let hidden = match invite {
QrInvite::Contact { .. } => Blocked::Not,
QrInvite::Group { .. } => Blocked::Yes,
QrInvite::Broadcast { .. } => Blocked::Yes,
};
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
}
/// Returns the [`ChatId`] of the chat being joined.
///
/// This is the chat in which you want to notify the user as well.
@@ -456,7 +406,8 @@ pub(crate) enum JoinerProgress {
}
impl JoinerProgress {
pub(crate) fn into_u16(self) -> u16 {
#[expect(clippy::wrong_self_convention)]
pub(crate) fn to_usize(self) -> usize {
match self {
JoinerProgress::RequestWithAuthSent => 400,
JoinerProgress::Succeeded => 1000,

View File

@@ -6,10 +6,8 @@ use super::*;
use crate::chat::{CantSendReason, add_contact_to_chat, remove_contact_from_chat};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::key::self_fingerprint;
use crate::mimeparser::{GossipedKey, SystemMessage};
use crate::qr::Qr;
use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2e_encrypted};
use crate::test_utils::{
@@ -102,17 +100,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
bob_chat.why_cant_send(&bob).await.unwrap(),
Some(CantSendReason::MissingKey)
);
// Check Bob's info messages.
let msg_cnt = 2;
let mut i = 0..msg_cnt;
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
let sent = bob.pop_sent_msg().await;
assert!(!sent.payload.contains("Bob Examplenet"));
@@ -256,7 +243,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
.unwrap();
match case {
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
_ => assert_eq!(contact_alice.get_authname(), ""),
_ => assert_eq!(contact_alice.get_authname(), "Alice Exampleorg"),
};
// Check Alice sent the right message to Bob.
@@ -285,10 +272,15 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert!(contact_alice.get_name().is_empty());
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
// The `SecurejoinWait` info message has been removed, but the e2ee notice remains.
let msg = get_chat_msg(&bob, bob_chat.get_id(), 0, 1).await;
// Check Bob got expected info messages in his 1:1 chat.
let msg_cnt = 2;
let mut i = 0..msg_cnt;
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1021,7 +1013,7 @@ async fn test_expired_synced_auth_token() -> Result<()> {
let qr = get_securejoin_qr(alice2, None).await?;
alice2.send_sync_msg().await.unwrap();
let sync_msg = alice2.pop_sent_msg().await;
let sync_msg = alice2.pop_sent_sync_msg().await;
// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
@@ -1225,148 +1217,3 @@ async fn test_qr_no_implicit_inviter_addition() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_user_deletes_chat_before_securejoin_completes() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let qr = get_securejoin_qr(alice, None).await?;
let bob_chat_id = join_securejoin(bob, &qr).await?;
let bob_alice_chat = bob.get_chat(alice).await;
// It's not possible yet to send to the chat, because Bob doesn't have Alice's key:
assert_eq!(bob_alice_chat.can_send(bob).await?, false);
assert_eq!(bob_alice_chat.id, bob_chat_id);
let request = bob.pop_sent_msg().await;
bob_chat_id.delete(bob).await?;
alice.recv_msg_trash(&request).await;
let auth_required = alice.pop_sent_msg().await;
bob.recv_msg_trash(&auth_required).await;
// The chat with Alice should be recreated,
// and it should be sendable now:
assert!(bob.get_chat(alice).await.can_send(bob).await?);
Ok(())
}
/// Tests that vc-request is processed even if the server
/// encrypts incoming unencrypted messages with OpenPGP.
///
/// For an example of software that encrypts incoming messages
/// with OpenPGP, see <https://lacre.io/>.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_vc_request_encrypted_at_rest() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let qr = get_securejoin_qr(alice, None).await?;
let Qr::AskVerifyContact { invitenumber, .. } = check_qr(bob, &qr).await? else {
panic!("Failed to parse QR code");
};
// Encrypted payload is this message encrypted to Alice's key:
/*
Content-Type: multipart/mixed;
boundary="1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b"
--1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b
Content-Type: text/plain; charset="utf-8"
Message-ID: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
Content-Transfer-Encoding: 7bit
Secure-Join: vc-request
--1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b--
*/
let payload = format!("To: <alice@example.org>
Subject: [...]
Date: Tue, 6 Jan 2026 08:20:47 +0000
References: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
Chat-Version: 1.0
Autocrypt: addr=bob@example.net; prefer-encrypt=mutual; keydata=xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjA
zbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJe
DvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1Jz
dKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBam
e1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS
5uZXQ+wsCOBBMBCAA4BQJpXW0wFiEEzMtaqfbhFByUMWXx2xixjLz3BIcCGwMCHgAECwkIBwYVCAkK
CwIDFgIBAScCGQEACgkQ2xixjLz3BIcexwgAsOauz2xZ6QhVBQNX3Hg8OoYTn60w6b4XUOpI4UtqAT
bAmPr5VPhCUa5vzI19QCSekLZIVxUSl6C+7YF/mU8yJldKPgq1Kpr8tbnISNbUDpvuGz7NQdK0TEwx
RkPDw+ilqY7v2Rhhg0Sogi23Yrbyqvs72oorDjia6dBA7VLixpW1O9h/MfQ6WiXJcHSavoLqk2hFW1
y/AsVmArGiuT+kzRJWhTzojZLjQLHTKQii/AOQ3MhkJycyayI/igSEDWWwCcuqL8SF+PUJWGyU5PxU
2hTHDBsVItLLsqseM3WrwJRkBBdsdsR8+moHVyXJV9Gfl/4UFQCgltdEwWISgNTbAc7ATQReMMdXAQ
gAogeBLbIjaeJII3W2pxsu+SEusQkJVykbGYDtqyXV+XBZisY4GE0kTawK5mqSh+rDqquCxDgYWBRT
nGZwEKohnj2NG75pjfyVPhYMUdJt7+Ya1oiFvZlgrrOj1btjevq53yFtQolMN+X2oS8mlf9jSzIyPC
eDxJk1N1gxaAATg3ByAyB1Td0wDdFPp48ni8qzfyGZeYicvJlx74YnOaja2lnI/y+I9LsmmqiOgI8H
cbmH1od5qSnVjhcpBoTEA15YLIEkSE3C00Q5USlDS3EVg/IOu3FXnLl7v0hQ/jXyv88eycfpSfFcbM
Hot9VtJ4TIPIoSX7DQ+uU2SXJKiZNkVQARAQABwsB2BBgBCAAgBQJpXW0wAhsMFiEEzMtaqfbhFByU
MWXx2xixjLz3BIcACgkQ2xixjLz3BIcxvwf9G33yLtvekpNGeRbWkjRXSa083k/4CZpkF0Fb5led1O
hjRjw9KQdZj68qn1vVrDxygnYdHwZWy8WbwEkK+l3gyJ2v2D/Wsj+sHjQ1YMtPnx+EsRFWjKsolYfN
gJZ+IfMTasip8uJNxN5LIvI/svn43mzgeLUudddekmr/2eekhXfYz5covje3yf+u4rYDWQ78TLpJL5
bxTwiiUyV1ZIO1G2F4RNjv8wZHsGD3DasQck+OCV7jQNuyojcmq6J22lXN0clisI51mHzwkLAnI+Yh
jRj3nqwVSCxsmgfejap53JGHEL0CEy6KvepLqjFf8BiK4kwRBmI7/YWOyY2xBWdTew==
Secure-Join: vc-request
Secure-Join-Invitenumber: {invitenumber}
MIME-Version: 1.0
From: <bob@example.net>
Message-ID: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
Content-Type: multipart/encrypted;
protocol=\"application/pgp-encrypted\";
boundary=\"1767687657/562150/131174/example.org\"
This is a MIME-encapsulated message.
--1767687657/562150/131174/example.org
Content-Type: application/pgp-encrypted
Content-Disposition: attachment
Version: 1
--1767687657/562150/131174/example.org
Content-Type: application/octet-stream
Content-Disposition: inline; filename=\"msg.asc\"
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdAGKP3GqsznPOYBXOo8FYf5x8r0ZaJZUGkXIR+Oi32H1Mw
LdVEu0jcXmZ1fGZ7LLPtvfwllB2W9Bhiy/lCZZs+c98VYzSWaT0DHmp23WvTsBZn
0sDeAS0n2I6xPwYhUjqL8G1a9xdyvZRgHYUXhiARrXzx7G+aX8/KFjk6e2OZxiVy
PSHKh61T9wPYzPMlToPsI0GjXAzOgI9I3XQ8xPqihiUpnU6BSg+Tj26m9ZqdJe8v
aVAHxAu7aqz7eljp2H336Idgfvf1lXdA6gMA8HhHI729Ws/Mc8lcgefM9kzZWEIN
FOBYmf4JCdTghESpEJ5i+pF28ChqZ3+8lGzNmT66c2SQRTclwxFa4SS19D4mxkHq
iOMqj7LZ8R6IhnNVRD1V4mVapc7vFVZy8ViyZlmhJF0TBR/TEXlit6vtSkDKFiR9
Tb5zuFQVAwhun3i1tG3/Ii1ReflZNnteVlL1J1XnptOJ70vVUeO3qRqkbkiH8Mdm
Asvy24WKkz9jmEgdt0Hj8VUHtcOL5xmtsr0ZUCLUriylpzDh8E+0LTl2/DOgH+mI
D/gFLCzFHjI0jfXyLTGCYOPYAnvqJukLBT/X7JbCAI/62aq0K28Lp6beJhFs+bIz
gU6dGXsFMe/RpRHrIAkMAaM5xkxMDRuRJDxiUdS/X+Y8
=QrdS
-----END PGP MESSAGE-----
--1767687657/562150/131174/example.org--
");
// Alice receives vc-request, but it is encrypted
// by the server.
let received = receive_imf(alice, payload.as_bytes(), false)
.await?
.unwrap();
assert_eq!(received.chat_id, DC_CHAT_ID_TRASH);
// Test that Alice sends vc-auth-required after processing vc-request.
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-auth-required"
);
Ok(())
}

View File

@@ -495,7 +495,6 @@ async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) -> Result<()> {
let ratelimited = if context.ratelimit.read().await.can_send() {
// add status updates and sync messages to end of sending queue
context.send_sync_msg().await?;
context.flush_status_updates().await?;
false
} else {

View File

@@ -1011,8 +1011,6 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
inc_and_check(&mut migration_version, 119)?;
if dbversion < migration_version {
// This table is deprecated sinc 2025-12-25.
// Sync messages are again sent over SMTP.
sql.execute_migration(
"CREATE TABLE imap_send (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1441,64 +1439,6 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
.await?;
}
inc_and_check(&mut migration_version, 142)?;
if dbversion < migration_version {
sql.execute_migration(
"ALTER TABLE transports
ADD COLUMN add_timestamp INTEGER NOT NULL DEFAULT 0;
CREATE TABLE removed_transports (
addr TEXT NOT NULL,
remove_timestamp INTEGER NOT NULL,
UNIQUE(addr)
) STRICT;",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 143)?;
if dbversion < migration_version {
sql.execute_migration(
"
ALTER TABLE chats ADD COLUMN name_normalized TEXT;
ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 144)?;
if dbversion < migration_version {
sql.execute_migration_transaction(
|transaction| {
let is_chatmail = transaction
.query_row(
"SELECT value FROM config WHERE keyname='is_chatmail'",
(),
|row| {
let value: String = row.get(0)?;
Ok(value)
},
)
.optional()?
.as_deref()
== Some("1");
if is_chatmail {
transaction.execute_batch(
"DELETE FROM config WHERE keyname='only_fetch_mvbox';
DELETE FROM config WHERE keyname='show_emails';
UPDATE config SET value='0' WHERE keyname='mvbox_move'",
)?;
}
Ok(())
},
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -160,7 +160,9 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
"#,
)?)).await?;
t.sql.run_migrations(&t).await?;
STOP_MIGRATIONS_AT
.scope(133, t.sql.run_migrations(&t))
.await?;
// Hidden address-contact can't be looked up.
assert!(

View File

@@ -14,9 +14,9 @@ use serde::Serialize;
use crate::chat::{self, ChatId, MuteDuration};
use crate::config::Config;
use crate::constants::{Chattype, DC_VERSION_STR};
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId, Origin, import_vcard, mark_contact_id_as_verified};
use crate::context::Context;
use crate::context::{Context, get_version_str};
use crate::key::load_self_public_keyring;
use crate::log::LogExt;
use crate::message::{Message, Viewtype};
@@ -25,8 +25,8 @@ use crate::tools::{create_id, time};
pub(crate) const STATISTICS_BOT_EMAIL: &str = "self_reporting@testrun.org";
const STATISTICS_BOT_VCARD: &str = include_str!("../assets/statistics-bot.vcf");
const SENDING_INTERVAL_SECONDS: i64 = 3600 * 24 * 7; // 1 week
// const SENDING_INTERVAL_SECONDS: i64 = 60; // 1 minute (for testing)
// const SENDING_INTERVAL_SECONDS: i64 = 3600 * 24 * 7; // 1 week
const SENDING_INTERVAL_SECONDS: i64 = 60; // 1 minute (for testing)
const MESSAGE_STATS_UPDATE_INTERVAL_SECONDS: i64 = 4 * 60; // 4 minutes (less than the lowest ephemeral messages timeout)
#[derive(Serialize)]
@@ -356,7 +356,7 @@ async fn get_stats(context: &Context) -> Result<String> {
get_timestamps(context, "stats_sending_disabled_events").await?;
let stats = Statistics {
core_version: DC_VERSION_STR.to_string(),
core_version: get_version_str().to_string(),
key_create_timestamps,
stats_id: stats_id(context).await?,
is_chatmail: context.is_chatmail().await?,

View File

@@ -46,7 +46,7 @@ async fn test_maybe_send_stats() -> Result<()> {
r.get("contact_stats").unwrap(),
&serde_json::Value::Array(vec![])
);
assert_eq!(r.get("core_version").unwrap(), DC_VERSION_STR);
assert_eq!(r.get("core_version").unwrap(), get_version_str());
assert_eq!(maybe_send_stats(alice).await?, None);

View File

@@ -109,15 +109,15 @@ pub enum StockMessage {
))]
DeviceMessagesHint = 70,
#[strum(props(fallback = "Get in contact!\n\n\
🙌 Tap \"QR code\" on the main screen of both devices. \
Choose \"Scan QR Code\" on one device, and point it at the other\n\n\
🌍 If not in the same room, \
scan via video call or share an invite link from \"Scan QR code\"\n\n\
Then: Enjoy your decentralized messenger experience. \
In contrast to other popular apps, \
without central control or tracking or selling you, \
friends, colleagues or family out to large organizations."))]
#[strum(props(fallback = "Welcome to Delta Chat! \
Delta Chat looks and feels like other popular messenger apps, \
but does not involve centralized control, \
tracking or selling you, friends, colleagues or family out to large organizations.\n\n\
Technically, Delta Chat is an email application with a modern chat interface. \
Email in a new dress if you will 👻\n\n\
Use Delta Chat with anyone out of billions of people: just use their e-mail address. \
Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Message from %1$s"))]
@@ -1144,6 +1144,14 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String {
translated(context, StockMessage::OutgoingMessages).await
}
/// Stock string: `Storage on %1$s`.
/// `%1$s` will be replaced by the domain of the configured email-address.
pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
translated(context, StockMessage::StorageOnDomain)
.await
.replace1(domain)
}
/// Stock string: `Not connected`.
pub(crate) async fn not_connected(context: &Context) -> String {
translated(context, StockMessage::NotConnected).await

View File

@@ -2,28 +2,23 @@
use crate::{context::Context, message::MsgId};
use anyhow::Result;
use humansize::{BINARY, format_size};
use walkdir::WalkDir;
/// Storage Usage Report
/// Useful for debugging space usage problems in the deltachat database.
#[derive(Debug)]
pub struct StorageUsage {
/// Total database size, subtract this from the backup size to estimate size of all blobs
pub db_size: u64,
pub db_size: usize,
/// size and row count of the 10 biggest tables
pub largest_tables: Vec<(String, u64, Option<u64>)>,
pub largest_tables: Vec<(String, usize, Option<usize>)>,
/// count and total size of status updates
/// for the 10 webxdc apps with the most size usage in status updates
pub largest_webxdc_data: Vec<(MsgId, u64, u64)>,
/// Total size of all files in the blobdir
pub blobdir_size: u64,
pub largest_webxdc_data: Vec<(MsgId, usize, usize)>,
}
impl std::fmt::Display for StorageUsage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Storage Usage:")?;
let blobdir_size = format_size(self.blobdir_size, BINARY);
writeln!(f, "[Blob Directory Size]: {blobdir_size}")?;
let human_db_size = format_size(self.db_size, BINARY);
writeln!(f, "[Database Size]: {human_db_size}")?;
writeln!(f, "[Largest Tables]:")?;
@@ -51,16 +46,12 @@ impl std::fmt::Display for StorageUsage {
/// Get storage usage information for the Context's database
pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
let context_clone = ctx.clone();
let blobdir_size =
tokio::task::spawn_blocking(move || get_blobdir_storage_usage(&context_clone));
let page_size: u64 = ctx
let page_size: usize = ctx
.sql
.query_get_value("PRAGMA page_size", ())
.await?
.unwrap_or_default();
let page_count: u64 = ctx
let page_count: usize = ctx
.sql
.query_get_value("PRAGMA page_count", ())
.await?
@@ -77,7 +68,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
(),
|row| {
let name: String = row.get(0)?;
let size: u64 = row.get(1)?;
let size: usize = row.get(1)?;
Ok((name, size, None))
},
)
@@ -85,7 +76,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
for row in &mut largest_tables {
let name = &row.0;
let row_count: Result<Option<u64>> = ctx
let row_count: Result<Option<usize>> = ctx
.sql
// SECURITY: the table name comes from the db, not from the user
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
@@ -102,31 +93,17 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
(),
|row| {
let msg_id: MsgId = row.get(0)?;
let size: u64 = row.get(1)?;
let count: u64 = row.get(2)?;
let size: usize = row.get(1)?;
let count: usize = row.get(2)?;
Ok((msg_id, size, count))
},
)
.await?;
let blobdir_size = blobdir_size.await?;
Ok(StorageUsage {
db_size: page_size * page_count,
largest_tables,
largest_webxdc_data,
blobdir_size,
})
}
/// Returns storage usage of the blob directory
pub fn get_blobdir_storage_usage(ctx: &Context) -> u64 {
WalkDir::new(ctx.get_blobdir())
.max_depth(2)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len())
}

View File

@@ -98,13 +98,14 @@ impl Summary {
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == ContactId::SELF {
if msg.is_info() || msg.viewtype == Viewtype::Call || chat.typ == Chattype::OutBroadcast
{
if msg.is_info() || msg.viewtype == Viewtype::Call {
None
} else {
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
}
} else if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
|| chat.typ == Chattype::Mailinglist
|| chat.is_self_talk()
{

View File

@@ -9,15 +9,14 @@ use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::ContactId;
use crate::context::Context;
use crate::log::{LogExt as _, warn};
use crate::login_param::EnteredLoginParam;
use crate::log::LogExt;
use crate::log::warn;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
use crate::token::Namespace;
use crate::tools::time;
use crate::transport::{ConfiguredLoginParamJson, sync_transports};
use crate::{message, stock_str, token};
use std::collections::HashSet;
@@ -53,29 +52,6 @@ pub(crate) struct QrTokenData {
pub(crate) grpid: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct TransportData {
/// Configured login parameters.
pub(crate) configured: ConfiguredLoginParamJson,
/// Login parameters entered by the user.
///
/// They can be used to reconfigure the transport.
pub(crate) entered: EnteredLoginParam,
/// Timestamp of when the transport was last time (re)configured.
pub(crate) timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct RemovedTransportData {
/// Address of the removed transport.
pub(crate) addr: String,
/// Timestamp of when the transport was removed.
pub(crate) timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum SyncData {
AddQrToken(QrTokenData),
@@ -95,28 +71,6 @@ pub(crate) enum SyncData {
DeleteMessages {
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
},
/// Update transport configuration.
///
/// This message contains a list of all added transports
/// together with their addition timestamp,
/// and all removed transports together with
/// the removal timestamp.
///
/// In case of a tie, addition and removal timestamps
/// being the same, removal wins.
/// It is more likely that transport is added
/// and then removed within a second,
/// but unlikely the other way round
/// as adding new transport takes time
/// to run configuration.
Transports {
/// Active transports.
transports: Vec<TransportData>,
/// Removed transports with the timestamp of removal.
removed_transports: Vec<RemovedTransportData>,
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -175,7 +129,7 @@ impl Context {
/// Adds most recent qr-code tokens for the given group or self-contact to the list of items to
/// be synced. If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
/// The caller should call `SchedulerState::interrupt_smtp()` on its own to trigger sending.
/// The caller should call `SchedulerState::interrupt_inbox()` on its own to trigger sending.
pub(crate) async fn sync_qr_code_tokens(&self, grpid: Option<&str>) -> Result<()> {
if !self.should_send_sync_msgs().await? {
return Ok(());
@@ -208,7 +162,7 @@ impl Context {
grpid: None,
}))
.await?;
self.scheduler.interrupt_smtp().await;
self.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -320,10 +274,6 @@ impl Context {
SyncData::Config { key, val } => self.sync_config(key, val).await,
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
SyncData::Transports {
transports,
removed_transports,
} => sync_transports(self, transports, removed_transports).await,
},
SyncDataOrUnknown::Unknown(data) => {
warn!(self, "Ignored unknown sync item: {data}.");
@@ -675,7 +625,7 @@ mod tests {
// let alice's other device receive and execute the sync message,
// also here, self-talk should stay hidden
let sent_msg = alice.pop_sent_msg().await;
let sent_msg = alice.pop_sent_sync_msg().await;
let alice2 = TestContext::new_alice().await;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
alice2.recv_msg_trash(&sent_msg).await;
@@ -722,7 +672,7 @@ mod tests {
}))
.await?;
alice1.send_sync_msg().await?.unwrap();
alice1.pop_sent_msg().await
alice1.pop_sent_sync_msg().await
} else {
let chat = alice1.get_self_chat().await;
alice1.send_text(chat.id, "Hi").await
@@ -760,7 +710,7 @@ mod tests {
.set_config(Config::Displayname, Some("Alice Human"))
.await?;
alice.send_sync_msg().await?;
alice.pop_sent_msg().await;
alice.pop_sent_sync_msg().await;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.text, "hi");
@@ -794,7 +744,7 @@ mod tests {
// group is promoted for compatibility (because the group could be created by older Core).
// TODO: assert!(msg_id.is_none());
assert!(msg_id.is_some());
let sent = alice.pop_sent_msg().await;
let sent = alice.pop_sent_sync_msg().await;
let msg = alice.parse_msg(&sent).await;
let mut sync_items = msg.sync_items.unwrap().items;
assert_eq!(sync_items.len(), 1);

View File

@@ -600,7 +600,7 @@ impl TestContext {
self.ctx
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.expect("Failed to configure address");
.unwrap();
if let Some(name) = addr.split('@').next() {
self.set_name(name);
@@ -711,6 +711,46 @@ impl TestContext {
})
}
/// Retrieves a sent sync message from the db.
///
/// This retrieves and removes a sync message which has been scheduled to send from the jobs
/// table. Messages are returned in the order they have been sent.
///
/// Panics if there is no message or on any error.
pub async fn pop_sent_sync_msg(&self) -> SentMessage<'_> {
let (id, msg_id, payload) = self
.ctx
.sql
.query_row(
"SELECT id, msg_id, mime \
FROM imap_send \
ORDER BY id",
(),
|row| {
let rowid: i64 = row.get(0)?;
let msg_id: MsgId = row.get(1)?;
let mime: String = row.get(2)?;
Ok((rowid, msg_id, mime))
},
)
.await
.expect("query_row failed");
self.ctx
.sql
.execute("DELETE FROM imap_send WHERE id=?", (id,))
.await
.expect("failed to remove job");
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("failed to update message state");
SentMessage {
payload,
sender_msg_id: msg_id,
sender_context: &self.ctx,
recipients: self.get_primary_self_addr().await.unwrap(),
}
}
/// Parses a message.
///
/// Parsing a message does not run the entire receive pipeline, but is not without
@@ -856,15 +896,6 @@ impl TestContext {
/// If the contact does not exist yet, a new contact will be created
/// with the correct fingerprint, but without the public key.
pub async fn add_or_lookup_contact_no_key(&self, other: &TestContext) -> Contact {
let contact_id = self.add_or_lookup_contact_id_no_key(other).await;
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
}
/// Returns the [`ContactId`] for the other [`TestContext`], creating it if necessary.
///
/// If the contact does not exist yet, a new contact will be created
/// with the correct fingerprint, but without the public key.
async fn add_or_lookup_contact_id_no_key(&self, other: &TestContext) -> ContactId {
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
let addr = ContactAddress::new(&primary_self_addr).unwrap();
let fingerprint = self_fingerprint(other).await.unwrap();
@@ -873,7 +904,7 @@ impl TestContext {
Contact::add_or_lookup_ex(self, "", &addr, fingerprint, Origin::MailinglistAddress)
.await
.expect("add_or_lookup");
contact_id
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
}
/// Returns 1:1 [`Chat`] with another account address-contact.
@@ -904,7 +935,7 @@ impl TestContext {
/// so may create a key-contact with a fingerprint
/// but without the key.
pub async fn get_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_contact_id_no_key(other).await;
let contact = self.add_or_lookup_contact_id(other).await;
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact)
.await
@@ -1434,14 +1465,13 @@ impl EventTracker {
event_matcher: F,
) -> Option<EventType> {
ctx.emit_event(EventType::Test);
let mut found_event = None;
loop {
let event = self.recv().await.unwrap();
if let EventType::Test = event.typ {
return found_event;
}
if event_matcher(&event.typ) {
found_event = Some(event.typ);
return Some(event.typ);
}
if let EventType::Test = event.typ {
return None;
}
}
}
@@ -1511,7 +1541,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
/// alice0's side that implies sending a sync message.
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
alice0.send_sync_msg().await.unwrap();
let sync_msg = alice0.pop_sent_msg().await;
let sync_msg = alice0.pop_sent_sync_msg().await;
alice1.recv_msg_trash(&sync_msg).await;
}

View File

@@ -779,15 +779,6 @@ pub(crate) fn to_lowercase(s: &str) -> Cow<'_, str> {
}
}
/// Returns text for storing in special db columns to make case-insensitive search possible for
/// non-ASCII messages, chat and contact names.
pub(crate) fn normalize_text(text: &str) -> Option<String> {
if text.is_ascii() {
return None;
};
Some(text.to_lowercase()).filter(|t| t != text)
}
/// Increments `*t` and checks that it equals to `expected` after that.
pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
t: &mut T,
@@ -798,26 +789,6 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
Ok(())
}
/// Converts usize to u64 without using `as`.
///
/// This is needed for example to convert in-memory buffer sizes
/// to u64 type used for counting all the bytes written.
///
/// On 32-bit systems it is possible to have files
/// larger than 4 GiB or write more than 4 GiB to network connection,
/// in which case we need a 64-bit total counter,
/// but use 32-bit usize for buffer sizes.
///
/// This can only break if usize has more than 64 bits
/// and this is not the case as of 2025 and is
/// unlikely to change for general purpose computers.
/// See <https://github.com/rust-lang/rust/issues/30495>
/// and <https://users.rust-lang.org/t/cant-convert-usize-to-u64/6243>
/// and <https://github.com/rust-lang/rust/issues/106050>.
pub(crate) fn usize_to_u64(v: usize) -> u64 {
u64::try_from(v).unwrap_or(u64::MAX)
}
/// Returns early with an error if a condition is not satisfied.
/// In non-optimized builds, panics instead if so.
#[macro_export]

View File

@@ -18,12 +18,10 @@ use crate::config::Config;
use crate::configure::server_params::{ServerParams, expand_param_vector};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::events::EventType;
use crate::login_param::EnteredLoginParam;
use crate::net::load_connection_timestamp;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id};
use crate::sql::Sql;
use crate::sync::{RemovedTransportData, SyncData, TransportData};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum ConnectionSecurity {
@@ -192,10 +190,10 @@ pub(crate) struct ConfiguredLoginParam {
pub oauth2: bool,
}
/// JSON representation of ConfiguredLoginParam
/// for the database and sync messages.
/// The representation of ConfiguredLoginParam in the database,
/// saved as Json.
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ConfiguredLoginParamJson {
struct ConfiguredLoginParamJson {
pub addr: String,
pub imap: Vec<ConfiguredServerLoginParam>,
pub imap_user: String,
@@ -559,9 +557,35 @@ impl ConfiguredLoginParam {
self,
context: &Context,
entered_param: &EnteredLoginParam,
timestamp: i64,
) -> Result<()> {
save_transport(context, entered_param, &self.into(), timestamp).await?;
let addr = addr_normalize(&self.addr);
let provider_id = self.provider.map(|provider| provider.id);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
context
.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param)
VALUES (?, ?, ?)
ON CONFLICT (addr)
DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param",
(
self.addr.clone(),
serde_json::to_string(entered_param)?,
self.into_json()?,
),
)
.await?;
if configured_addr.is_none() {
// If there is no transport yet, set the new transport as the primary one
context
.sql
.set_raw_config(Config::ConfiguredProvider.as_ref(), provider_id)
.await?;
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
}
Ok(())
}
@@ -585,7 +609,18 @@ impl ConfiguredLoginParam {
}
pub(crate) fn into_json(self) -> Result<String> {
let json: ConfiguredLoginParamJson = self.into();
let json = ConfiguredLoginParamJson {
addr: self.addr,
imap: self.imap,
imap_user: self.imap_user,
imap_password: self.imap_password,
smtp: self.smtp,
smtp_user: self.smtp_user,
smtp_password: self.smtp_password,
provider_id: self.provider.map(|p| p.id.to_string()),
certificate_checks: self.certificate_checks,
oauth2: self.oauth2,
};
Ok(serde_json::to_string(&json)?)
}
@@ -603,181 +638,12 @@ impl ConfiguredLoginParam {
}
}
impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
fn from(configured_login_param: ConfiguredLoginParam) -> Self {
Self {
addr: configured_login_param.addr,
imap: configured_login_param.imap,
imap_user: configured_login_param.imap_user,
imap_password: configured_login_param.imap_password,
smtp: configured_login_param.smtp,
smtp_user: configured_login_param.smtp_user,
smtp_password: configured_login_param.smtp_password,
provider_id: configured_login_param.provider.map(|p| p.id.to_string()),
certificate_checks: configured_login_param.certificate_checks,
oauth2: configured_login_param.oauth2,
}
}
}
/// Saves transport to the database.
pub(crate) async fn save_transport(
context: &Context,
entered_param: &EnteredLoginParam,
configured: &ConfiguredLoginParamJson,
add_timestamp: i64,
) -> Result<()> {
let addr = addr_normalize(&configured.addr);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
context
.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
VALUES (?, ?, ?, ?)
ON CONFLICT (addr)
DO UPDATE SET entered_param=excluded.entered_param,
configured_param=excluded.configured_param,
add_timestamp=excluded.add_timestamp",
(
&addr,
serde_json::to_string(entered_param)?,
serde_json::to_string(configured)?,
add_timestamp,
),
)
.await?;
if configured_addr.is_none() {
// If there is no transport yet, set the new transport as the primary one
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
}
Ok(())
}
/// Sends a sync message to synchronize transports across devices.
pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
info!(context, "Sending transport synchronization message.");
// Synchronize all transport configurations.
//
// Transport with ID 1 is never synchronized
// because it can only be created during initial configuration.
// This also guarantees that credentials for the first
// transport are never sent in sync messages,
// so this is not worse than when not using multi-transport.
// If transport ID 1 is reconfigured,
// likely because the password has changed,
// user has to reconfigure it manually on all devices.
let transports = context
.sql
.query_map_vec(
"SELECT entered_param, configured_param, add_timestamp
FROM transports WHERE id>1",
(),
|row| {
let entered_json: String = row.get(0)?;
let entered: EnteredLoginParam = serde_json::from_str(&entered_json)?;
let configured_json: String = row.get(1)?;
let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
let timestamp: i64 = row.get(2)?;
Ok(TransportData {
configured,
entered,
timestamp,
})
},
)
.await?;
let removed_transports = context
.sql
.query_map_vec(
"SELECT addr, remove_timestamp FROM removed_transports",
(),
|row| {
let addr: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
Ok(RemovedTransportData { addr, timestamp })
},
)
.await?;
context
.add_sync_item(SyncData::Transports {
transports,
removed_transports,
})
.await?;
context.scheduler.interrupt_smtp().await;
Ok(())
}
/// Process received data for transport synchronization.
pub(crate) async fn sync_transports(
context: &Context,
transports: &[TransportData],
removed_transports: &[RemovedTransportData],
) -> Result<()> {
for TransportData {
configured,
entered,
timestamp,
} in transports
{
save_transport(context, entered, configured, *timestamp).await?;
}
context
.sql
.transaction(|transaction| {
for RemovedTransportData { addr, timestamp } in removed_transports {
transaction.execute(
"DELETE FROM transports
WHERE addr=? AND add_timestamp<=?",
(addr, timestamp),
)?;
transaction.execute(
"INSERT INTO removed_transports (addr, remove_timestamp)
VALUES (?, ?)
ON CONFLICT (addr) DO
UPDATE SET remove_timestamp = excluded.remove_timestamp
WHERE excluded.remove_timestamp > remove_timestamp",
(addr, timestamp),
)?;
}
Ok(())
})
.await?;
context.emit_event(EventType::TransportsModified);
Ok(())
}
/// Adds transport entry to the `transports` table with empty configuration.
pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
context.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
(
addr,
serde_json::to_string(&EnteredLoginParam::default())?,
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log::LogExt as _;
use crate::provider::get_provider_by_id;
use crate::test_utils::TestContext;
use crate::tools::time;
#[test]
fn test_configured_certificate_checks_display() {
@@ -822,7 +688,7 @@ mod tests {
param
.clone()
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
.save_to_transports_table(&t, &EnteredLoginParam::default())
.await?;
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
assert_eq!(
@@ -1040,7 +906,7 @@ mod tests {
certificate_checks: ConfiguredCertificateChecks::Automatic,
oauth2: false,
}
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
.save_to_transports_table(&t, &EnteredLoginParam::default())
.await?;
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();

View File

@@ -411,16 +411,8 @@ impl Context {
&& let Some(notify_list) = status_update_item.notify
{
let self_addr = instance.get_webxdc_self_addr(self).await?;
let notify_text = if let Some(notify_text) = notify_list.get(&self_addr) {
Some(notify_text)
} else if let Some(notify_text) = notify_list.get("*")
&& !Chat::load_from_db(self, instance.chat_id).await?.is_muted()
if let Some(notify_text) = notify_list.get(&self_addr).or_else(|| notify_list.get("*"))
{
Some(notify_text)
} else {
None
};
if let Some(notify_text) = notify_text {
self.emit_event(EventType::IncomingWebxdcNotify {
chat_id: instance.chat_id,
contact_id: from_id,

View File

@@ -5,8 +5,8 @@ use serde_json::json;
use super::*;
use crate::chat::{
ChatId, MuteDuration, add_contact_to_chat, create_broadcast, create_group, forward_msgs,
remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, set_muted,
ChatId, add_contact_to_chat, create_broadcast, create_group, forward_msgs,
remove_contact_from_chat, resend_msgs, send_msg, send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
@@ -2073,74 +2073,6 @@ async fn test_webxdc_notify_all() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_notify_muted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members("grp", &[&bob, &fiona])
.await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
let bob_instance = bob.recv_msg(&sent1).await;
let fiona_instance = fiona.recv_msg(&sent1).await;
set_muted(&bob, bob_instance.chat_id, MuteDuration::Forever).await?;
alice
.send_webxdc_status_update(
alice_instance.id,
"{\"payload\":7,\"info\": \"all\", \"notify\":{\"*\":\"notify all\"} }",
)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let info_msg = alice.get_last_msg().await;
assert_eq!(info_msg.text, "all");
assert!(!has_incoming_webxdc_event(&alice, info_msg, "notify all").await);
bob.recv_msg_trash(&sent2).await;
let info_msg = bob.get_last_msg().await;
assert_eq!(info_msg.text, "all");
assert!(!has_incoming_webxdc_event(&bob, info_msg, "notify all").await);
fiona.recv_msg_trash(&sent2).await;
let info_msg = fiona.get_last_msg().await;
assert_eq!(info_msg.text, "all");
assert!(has_incoming_webxdc_event(&fiona, info_msg, "notify all").await);
alice
.send_webxdc_status_update(
alice_instance.id,
&format!(
"{{\"payload\":8,\"info\": \"reply\", \"notify\":{{\"{}\":\"reply, Bob\",\"{}\":\"reply, Fiona\"}} }}",
bob_instance.get_webxdc_self_addr(&bob).await?,
fiona_instance.get_webxdc_self_addr(&fiona).await?
),
)
.await?;
alice.flush_status_updates().await?;
let sent3 = alice.pop_sent_msg().await;
let info_msg = alice.get_last_msg().await;
assert_eq!(info_msg.text, "reply");
assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await);
bob.recv_msg_trash(&sent3).await;
let info_msg = bob.get_last_msg().await;
assert_eq!(info_msg.text, "reply");
assert!(has_incoming_webxdc_event(&bob, info_msg, "reply, Bob").await);
fiona.recv_msg_trash(&sent3).await;
let info_msg = fiona.get_last_msg().await;
assert_eq!(info_msg.text, "reply");
assert!(has_incoming_webxdc_event(&fiona, info_msg, "reply, Fiona").await);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_notify_bob_and_all() -> Result<()> {
let mut tcm = TestContextManager::new();