mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
Compare commits
1 Commits
v2.37.0
...
hoc/stats-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b334603e27 |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
104
.github/workflows/deltachat-rpc-server.yml
vendored
104
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -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 }}
|
||||
|
||||
10
.github/workflows/jsonrpc-client-npm-package.yml
vendored
10
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/jsonrpc.yml
vendored
2
.github/workflows/jsonrpc.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
4
.github/workflows/repl.yml
vendored
4
.github/workflows/repl.yml
vendored
@@ -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"
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -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
|
||||
|
||||
233
CHANGELOG.md
233
CHANGELOG.md
@@ -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
174
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
9
STYLE.md
9
STYLE.md
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.37.0"
|
||||
version = "2.29.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.37.0"
|
||||
"version": "2.29.0"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"] = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')")
|
||||
@@ -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())
|
||||
|
||||
20
deltachat-rpc-client/tests/test_rpc_virtual.py
Normal file
20
deltachat-rpc-client/tests/test_rpc_virtual.py
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.37.0"
|
||||
"version": "2.29.0"
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
13
deny.toml
13
deny.toml
@@ -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" },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-01-08
|
||||
2025-12-01
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
118
src/calls.rs
118
src/calls.rs
@@ -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;
|
||||
|
||||
175
src/chat.rs
175
src/chat.rs
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
137
src/config.rs
137
src/config.rs
@@ -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;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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'.');
|
||||
|
||||
104
src/contact.rs
104
src/contact.rs
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
14
src/e2ee.rs
14
src/e2ee.rs
@@ -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?;
|
||||
|
||||
|
||||
@@ -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:?}"
|
||||
),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
173
src/imap.rs
173
src/imap.rs
@@ -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)?;
|
||||
|
||||
@@ -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?,
|
||||
|
||||
29
src/imex.rs
29
src/imex.rs
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)]
|
||||
|
||||
69
src/pgp.rs
69
src/pgp.rs
@@ -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?;
|
||||
|
||||
|
||||
10
src/qr.rs
10
src/qr.rs
@@ -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)?;
|
||||
|
||||
56
src/quota.rs
56
src/quota.rs
@@ -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("a.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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} · {not_connected}</li>");
|
||||
continue;
|
||||
};
|
||||
if let Some(quota) = &*quota {
|
||||
match "a.recent {
|
||||
Err(e) => {
|
||||
let error_escaped = escaper::encode_minimal(&e.to_string());
|
||||
ret += &format!("<li>{domain_escaped} · {error_escaped}</li>");
|
||||
}
|
||||
Ok(quota) => {
|
||||
if quota.is_empty() {
|
||||
ret += &format!(
|
||||
"<li>{domain_escaped} · 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} · ");
|
||||
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>";
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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!(
|
||||
|
||||
10
src/stats.rs
10
src/stats.rs
@@ -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?,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
66
src/sync.rs
66
src/sync.rs
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
29
src/tools.rs
29
src/tools.rs
@@ -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]
|
||||
|
||||
224
src/transport.rs
224
src/transport.rs
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user