Compare commits

..

1 Commits

Author SHA1 Message Date
iequidoo
974e32dd76 fix: Don't decrease member add/remove timestamps if they aren't far away in the future
We shouldn't decrease `add_timestamp` and `remove_timestamp` in the `chats_contacts` table normally,
even if remote changes arrive reordered. This particularly makes sense for ad-hoc groups (see
`chat::update_chat_contacts_table()` in `apply_group_changes()`) and in case if we join an encrypted
group which we were a member of before (see `chat::add_to_chat_contacts_table()` call).

Still, limit already stored timestamps in case local clock was in the future and is set back
now. But our clock may be slow, so limit stored timestamps with a remote timestamp if it's
bigger.

NB: `receive_imf::update_chats_contacts_timestamps()` already only increases timestamps, but it's
used only for handling of the "Chat-Group-Member-Timestamps" header, i.e. for encrypted groups.
2026-06-07 21:45:47 -03:00
267 changed files with 15729 additions and 16543 deletions

View File

@@ -15,6 +15,6 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
interval: "weekly"
cooldown:
default-days: 7

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.98.1
RUST_VERSION: 1.95.0
# Minimum Supported Rust Version
MSRV: 1.89.0
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -40,7 +40,7 @@ jobs:
- run: rustup override set $RUST_VERSION
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -58,16 +58,30 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25
- uses: EmbarkStudios/cargo-deny-action@a531616d8ce3b9177443e48a1159bc945a099823
with:
arguments: --workspace --all-features --locked
command: check
command-arguments: "-Dwarnings"
provider_database:
name: Check provider database
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Install rustfmt
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
- name: Check provider database
run: scripts/update-provider-database.sh
docs:
name: Rust doc comments
runs-on: ubuntu-latest
@@ -75,12 +89,12 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -114,7 +128,7 @@ jobs:
shell: bash
if: matrix.rust == 'latest'
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -126,13 +140,13 @@ jobs:
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Install nextest
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
uses: taiki-e/install-action@60ae4ce63c7aeb6e96d7f572c1ec7fafbb17ca80
with:
tool: nextest
@@ -157,13 +171,13 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -186,13 +200,13 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -212,7 +226,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -238,7 +252,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -288,7 +302,7 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -300,7 +314,7 @@ jobs:
path: target/debug
- name: Install python
uses: actions/setup-python@v7.0.0
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
@@ -342,13 +356,13 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Install python
uses: actions/setup-python@v7.0.0
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}

View File

@@ -30,11 +30,11 @@ jobs:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -54,11 +54,11 @@ jobs:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
@@ -78,11 +78,11 @@ jobs:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -102,11 +102,11 @@ jobs:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
@@ -127,7 +127,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -153,11 +153,11 @@ jobs:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -177,11 +177,11 @@ jobs:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
@@ -204,11 +204,11 @@ jobs:
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v7
@@ -370,14 +370,6 @@ jobs:
- name: List artifacts
run: ls -l dist/
- name: Check that the wheel metadata reads back
run: |
echo 'You can check a local `nix build` on non-Mac machines against the following checksums.'
sha256sum dist/*.whl
mkdir tagcheck
cp dist/*.whl tagcheck/
nix run --inputs-from . nixpkgs#python3Packages.wheel -- tags tagcheck/*.whl
- name: Upload binaries to the GitHub release
if: github.event_name == 'release'
env:
@@ -390,7 +382,7 @@ jobs:
- name: Publish deltachat-rpc-server to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
@@ -405,11 +397,11 @@ jobs:
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: actions/setup-python@v7.0.0
- uses: actions/setup-python@v6
with:
python-version: "3.11"
@@ -521,12 +513,15 @@ jobs:
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: 24
node-version: 20
registry-url: "https://registry.npmjs.org"
package-manager-cache: false # never use caching in release builds
# 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'

View File

@@ -14,7 +14,7 @@ jobs:
name: Check that current version ends with -dev
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false

View File

@@ -17,29 +17,31 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
# Configure Node.js for publishing.
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: 24
node-version: 20
registry-url: "https://registry.npmjs.org"
package-manager-cache: false # never use caching in release builds
# 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-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
- name: Package
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: |
npm run build
npm pack .
- name: Publish
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public

View File

@@ -16,30 +16,30 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Use Node.js 24
uses: actions/setup-node@v7
- name: Use Node.js 18.x
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 18.x
- name: Add Rust cache
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: npm install
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm install
- name: Build TypeScript, run Rust tests, generate bindings
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm run build
- name: Run integration tests
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
- name: Run linter
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm run prettier:check

View File

@@ -5,13 +5,11 @@ on:
paths:
- flake.nix
- flake.lock
- nix/**
- .github/workflows/nix.yml
push:
paths:
- flake.nix
- flake.lock
- nix/**
- .github/workflows/nix.yml
branches:
- main
@@ -23,12 +21,12 @@ jobs:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- run: nix fmt flake.nix nix/ -- --check
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix fmt flake.nix -- --check
build:
name: nix build
@@ -81,11 +79,11 @@ jobs:
#- deltachat-rpc-server-x86_64-android
#- deltachat-rpc-server-x86-android
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -102,9 +100,9 @@ jobs:
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
# - deltachat-rpc-server-aarch64-darwin
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix build .#${{ matrix.installable }}

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -47,4 +47,4 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b

View File

@@ -14,11 +14,11 @@ jobs:
name: Build REPL example
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary

View File

@@ -15,7 +15,7 @@ jobs:
url: https://rs.delta.chat/
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -27,7 +27,7 @@ jobs:
mkdir -p "$HOME/.ssh"
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
build-python:
runs-on: ubuntu-latest
@@ -36,12 +36,12 @@ jobs:
url: https://py.delta.chat/
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -49,7 +49,7 @@ jobs:
mkdir -p "$HOME/.ssh"
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
build-c:
runs-on: ubuntu-latest
@@ -58,12 +58,12 @@ jobs:
url: https://c.delta.chat/
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
@@ -71,7 +71,7 @@ jobs:
mkdir -p "$HOME/.ssh"
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
build-ts:
runs-on: ubuntu-latest
@@ -81,18 +81,18 @@ jobs:
defaults:
run:
working-directory: ./deltachat-jsonrpc-bindings/typescript
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@v6
with:
node-version: 24
node-version: '18'
- name: npm install
run: npm install
- name: npm run build
@@ -104,7 +104,7 @@ jobs:
mkdir -p "$HOME/.ssh"
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc-bindings/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
build-cffi:
runs-on: ubuntu-latest
@@ -113,7 +113,7 @@ jobs:
url: https://cffi.delta.chat/
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -125,4 +125,4 @@ jobs:
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"

View File

@@ -18,9 +18,9 @@ jobs:
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6

View File

@@ -1,493 +1,5 @@
# Changelog
## [2.60.0] - 2026-09-11
### API-Changes
- [**breaking**] remove a relay immediately instead of unpublishing it.
- `set_transport_unpublished()` is removed: UIs call `delete_transport()` when the user removes a relay.
- `list_transports_ex()` and the `TransportListEntry` type are removed: use `list_transports()`.
- `delete_transport()` no longer refuses to remove the primary transport: it refuses only to remove the last one and re-elects the sending transport as needed.
- `TransportsModified` is now also emitted on the device modifying the transports, not only on devices applying the synced change.
- [**breaking**] do not load webxdc icon if it has too large dimensions.
- `get_webxdc_blob()` may fail to load `icon.png` or `icon.jpg` if image dimensions are too large.
Fixing the issue discovered by https://github.com/Sergei768
- Generate JSON-RPC headers at build time ([#8350](https://github.com/chatmail/core/pull/8350)).
- Generate Qt JSON-RPC bindings ([#8330](https://github.com/chatmail/core/pull/8330)).
### Features / Changes
- Introduce keyupdate messages informing contacts about relay changes.
- Remove `Final-Recipient` from MDNs (and keyupdates).
- Carry all published relay addresses in securejoin links ([#8591](https://github.com/chatmail/core/pull/8591)).
- Use display name for contacts in encryption info ([#8609](https://github.com/chatmail/core/pull/8609)).
- Do not create device messages for IMAP authentication errors.
- Delete avatars referred to by parameters of special contacts.
- Import `Autocrypt-Gossip` keys without checking the addresses.
- Ignore `Chat-Disposition-Notification-To` value.
- Increase `sys.msgsize_max_recommended` to match chatmail relay message size limit.
### Fixes
- Do not try to load profile image from param for self.
- Send legacy securejoin key requests as `multipart/mixed` so they are not rejected by chatmail relays.
- rpc: avoid hang when requests race a dying rpc-server.
- Take `timestamp_rcvd` into account in `estimate_deletion_cnt`.
- Reliably complete configuration with progress=1000 or progress=0.
- Make `create_send_msg_jobs` actually return row IDs.
- Don't notify of missed call from blocked user.
- Trash MDNs that reference no message early.
- Return no relay address for key-contacts without an address.
- Do not emit events in `set_profile_image()` if contact avatar is unchanged.
- Remove `Original-Recipient` field from MDNs.
- ffi: support custom allocators in event string getters.
- Start checking column documentation in CI and add comment for `transports.add_timestamp`.
- Sanitize `version_string` we got from the wire ([#8582](https://github.com/chatmail/core/pull/8582))
- RUSTSEC-2026-0258 ([#8603](https://github.com/chatmail/core/pull/8603)).
### Build system
- Use `--locked` in `scripts/clippy.sh`.
- Produce correct wheel metadata.
### Documentation
- Always suggest using `--locked` with "cargo install".
- JSON-RPC: clarify when `reactions` is `None`.
- Fix async-imap and async-smtp URLs in README.md ([#8637](https://github.com/chatmail/core/pull/8637)).
- Update the timeout value in `DC_EVENT_CALL_ENDED` description.
### Refactor
- Don't store email address in location KML. ([#8615](https://github.com/chatmail/core/pull/8615)).
- Turn `DC_CHAT_ID_*` into `ChatId::*` associated constants.
- Turn `DC_MSG_ID_*` into `MsgId::*` associated constants.
- Don't include email addresses in export filenames ([#8626](https://github.com/chatmail/core/pull/8626)).
- Make `create_send_msg_jobs()` private.
- Rename `automatic_relay_management` to autorelay.
- Extract shared pieces for non-chat messages.
- Remove unused functions from the tools module.
- Remove the code to set own avatar in `set_profile_image()`.
- Move pgp tests to submodule.
- Split `flake.nix` into multiple files.
- Use `&[..]` instead of `&Vec<..>`.
### Tests
- [**breaking**] rename rpc fixtures to disambiguate from ffi fixtures.
- Test `dc_send_msg_sync()`.
- Print which error/warning was expected if it does not arrive.
### CI
- Update Rust to 1.98.1.
### Miscellaneous Tasks
- Add script to show the sizes of futures (async Rust) ([#8536](https://github.com/chatmail/core/pull/8536)).
- deps: bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2.
- deps: bump swatinem/rust-cache from 2.9.1 to 2.9.2.
- deps: bump pypa/gh-action-pypi-publish from 1.14.1 to 1.14.2.
- deps: bump taiki-e/install-action from 2.85.1 to 2.86.7.
- cargo: bump futures from 0.3.33 to 0.3.34.
- cargo: bump thiserror from 2.0.19 to 2.0.20.
- cargo: bump syn from 3.0.3 to 3.0.4.
- cargo: bump log from 0.4.33 to 0.4.34.
- cargo: bump mail-builder from 0.4.4 to 0.5.0.
- cargo: bump blake3 from 1.8.5 to 1.8.7.
- cargo: bump http-body-util from 0.1.3 to 0.1.5.
- cargo: bump uuid from 1.20.0 to 1.25.0.
- cargo: bump data-encoding from 2.11.0 to 2.11.1.
- bump chacha20 0.10.1 to 0.10.2.
## [2.59.0] - 2026-08-14
### API-Changes
- [**breaking**] Remove deprecated `dc_chat_is_protected()`.
- Deprecate `dc_chat_get_info_json()` ([#8580](https://github.com/chatmail/core/pull/8580))
- New `get_app_version()` JSON-RPC API to get information about available updates.
### Features / Changes
- Add stock strings for being added/removed from group ([#8562](https://github.com/chatmail/core/pull/8562)).
- Client version information ([#8557](https://github.com/chatmail/core/pull/8557)).
- Remove hidden headers.
- Stop creating info messages for old broadcast lists.
### Fixes
- Filtered reactions are info, not error in device chat.
- Send MDNs to self even if MDNs are disabled.
- Send HTTP requests in origin not absolute form.
### Documentation
- json-rpc: improve `reactions_by_contact` doc.
- Do not refer to `is_chat_protected()`.
- Do not talk about verified chats in securejoin QR-scanning functions.
- Add SQL schema documentation.
### Miscellaneous Tasks
- Fix nightly clippy warnings.
- cargo: bump astral-tokio-tar from 0.6.3 to 0.6.4.
- cargo: bump bytes from 1.12.0 to 1.12.1.
- FFI: don't swallow but log errors in three places.
### Refactor
- Remove `MessengerMessage`.
- Stop setting chats.protected column explicitly.
- Merge `msg_group_left_local` into `msg_del_member_local` ([#8575](https://github.com/chatmail/core/pull/8575)).
- Rename `_ex()` -> `_ext()`.
- mimefactory: add Encryption enum.
### Tests
- Fix flakyness of iroh tests by sending "forever" so that late swarm-joins still make the test work.
- Move iroh tests into separate module.
- Provide complete test isolation by not re-using account addresses.
- Avoid another source of random failures with `direct_imap` failing to connect on first try.
- Remove all cache-related logic in the FFI pytest plugin.
- Add a CI-failing check that documented sql schema matches real one.
- Abort early if DNS to chatmail domain does not work and nicer pytest startup header.
- Load test data through the `data` fixture.
- Allow to run the test suite against underscore-domain relays.
## [2.58.0] - 2026-08-10
### API-Changes
- [**breaking**] remove getPushState() and core's internal tracking of it
- [**breaking**] remove `dc_chatlist_get_context()`, because it was easy to misuse and likely led to crashes ([#8503](https://github.com/chatmail/core/pull/8503))
- instead, store reference-counted Context in `dc_msg_t`, `dc_contact_t` and `dc_chatlist_t`
- add "pinned messages" API.
### Build system
- update all crates to Rust 2024 edition.
### CI
- update github actions monthly instead of weekly.
### Documentation
- clarify `ChatId::do_set_draft()` docs.
- add missing slash to ConnectionSecurity::Starttls doc comment.
### Features / Changes
- send Autocrypt pgp key in MDNs occassionally and when relaylist changes.
- reduce unncessary gossipping of keys in group chats.
- stop requiring XDELTAPUSH capability for push notifications.
- prepare basic multi-relay onboarding ([#8444](https://github.com/chatmail/core/pull/8444))
- collect ICE servers from all relays.
- send messages to 5 relays instead of the newest 3 ones.
- allow to send reactions in broadcast channels ([#8450](https://github.com/chatmail/core/pull/8450)).
- allow only default reactions in channels broadcast ([#8545](https://github.com/chatmail/core/pull/8545)).
- resend pinned state in broadcast channels ([#8549](https://github.com/chatmail/core/pull/8549)).
### Fixes
- **The primary transport is not synchronized between devices anymore.**
- Don't warn about correct EXIF orientation values. ([#8483](https://github.com/chatmail/core/pull/8483)).
- deltachat-rpc-client: don't depend on execnet for importing pytest plugin, remove deprecated "py" usage.
- send MDNs to all authentic relays of a contact, not just whatever `get_addr()` returns..
- mark `as_path()` function unsafe.
- python: create event emitter when EventThread is initialized.
- Don't download pre-message again if it is known already ([#8488](https://github.com/chatmail/core/pull/8488)).
- recognize self addresses in various places (instead of just the "primary").
- fix multi relay connectivity view ([#8550](https://github.com/chatmail/core/pull/8550)).
- ensure same-second primary transport change propagates correctly.
- invalidate `configured_addr` cache before sending transport sync message.
- prevent transport de-synchronization because of early fetch cancellation.
- improve connectivity HTML if quota info has an error.
### Miscellaneous Tasks
- bump version to 2.58.0-dev.
- deps: bump actions/setup-python from 6 to 6.3.0.
- deps: bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0.
- cargo: bump futures from 0.3.32 to 0.3.33.
- cargo: bump tokio from 1.52.3 to 1.53.0.
- cargo: bump regex from 1.12.4 to 1.13.1.
- disable "large futures" lint again.
- cargo: bump tokio-util from 0.7.18 to 0.7.19.
- deps: bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1.
- deps: bump taiki-e/install-action from 2.83.4 to 2.85.1.
- cargo: bump `serde_json` from 1.0.150 to 1.0.151.
- deps: bump pypa/gh-action-pypi-publish from 1.14.0 to 1.14.1.
- deps: bump actions/setup-python from 6.3.0 to 7.0.0.
- cargo: introduce syn 3 dependency.
- cargo: bump anyhow from 1.0.103 to 1.0.104.
- cargo: bump serde from 1.0.228 to 1.0.229.
- cargo: bump thiserror from 2.0.18 to 2.0.19.
- cargo: bump libc from 0.2.186 to 0.2.189.
### Performance
- Box::pin iroh::endpoint::Builder::bind in order to reduce memory usage.
### Refactor
- use the new regex! macro.
- Remove FolderMeaning and `target_folder` ([#8456](https://github.com/chatmail/core/pull/8456)).
- Unify naming of direct/single/1:1/normal chats ([#8442](https://github.com/chatmail/core/pull/8442)).
- un-nest `prepare_msg_blob`.
- do not clean `imap_send` table on transport change.
- mark enabled ephemeral timer duration as NonZero.
- reduce the scope of unsafe in `dc_context_unref()`.
- mimefactory: separate rendering of message payload and sendable message.
### Tests
- fix flaky `test_markseen_message_and_mdn` test.
- fix flaky `test_no_markseen_in_team_profile` ([#8500](https://github.com/chatmail/core/pull/8500)).
- Add `test_bcc_self`.
- Add test for unencrypted headers ([#8538](https://github.com/chatmail/core/pull/8538)).
- Assert log warnings and errors ([#8457](https://github.com/chatmail/core/pull/8457)).
## [2.57.0] - 2026-07-25
### API-Changes
- [**breaking**] remove heartbeat push notifications.
- [**breaking**] remove provider-db handling and provider lookup APIs.
- provider lookup APIs were removed from CFFI and JSON-RPC.
also removes offline provider database code and generated provider data,
provider-specific fields in configure/transport paths, and REPL providerinfo.
### Documentation
- remove oauth2 from standards.
### Features / Changes
- accept messages from key contacts with forged From address.
- enable TLS certificate compression.
- read SMTP recipient limit from relay IMAP metadata.
### Fixes
- fixup CI failures.
- never merge outer To headers if standard header protection is used.
- Re-add oauth2 to serialized structs ([#8464](https://github.com/chatmail/core/pull/8464)).
- migrate transports configured on 2.56 to also have a oauth:false flag.
### Miscellaneous Tasks
- bump version to 2.57.0-dev.
- deps: bump actions/setup-node from 6 to 7.
- deps: bump cachix/install-nix-action from 31.10.6 to 31.11.0.
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.20 to 2.1.1.
- deps: bump taiki-e/install-action from 2.82.10 to 2.83.4.
- cargo: bump quinn-proto from 0.11.14 to 0.11.16.
## [2.56.0] - 2026-07-21
### API-Changes
- [**breaking**] remove all oauth support and drop DC_LP_AUTH flags.
- removed oauth2 module, dc_get_oauth2_url FFI function, DC_LP_AUTH flags and configured/serverflags, and the oauth2 parameter/field from SMTP/IMAP clients, JSON-RPC interfaces, and CLI tools.
also contains regenerated provider data after dropping oauth in the update script.
### Features / Changes
- do not set backup_time in exported databases.
### Fixes
- revert 207c2e6e4c1bec43204c3b8a46fcbbff67d54b3f because some users reported problems with it.
### Miscellaneous Tasks
- bump version to 2.56.0-dev.
## [2.55.0] - 2026-07-20
Minor release to fix CI because releasing 2.54.0 failed.
### CI
- Update Node version to 24.
## [2.54.0] - 2026-07-20
### API-Changes
- [**breaking**] Deprecate `is_chatmail`.
- UIs should not behave differently for chatmail relays than for classical email servers; most usages of `is_chatmail` can be replaced by `force_encryption`.
- [**breaking**] `delete_transport()` must not be used by UIs anymore. Instead, `set_transport_unpublished()` must be called when a user clicks on "Remove".
- [**breaking**] `list_transports()` doesn't return unpublished relays anymore.
- UIs should use `list_transports()` rather than `list_transports_ex()`, because unpublished transports count as removed from the user point of view, and should not be shown in the relay list anymore.
- deltachat-rpc-client: add `Account.set_transport_unpublished()`.
- Add `MsgReadCountChanged` event.
### Features / Changes
- Implement support for populating and maintaining a list of default relays ([#8341](https://github.com/chatmail/core/pull/8341)).
- Remove hidden relays automatically ([#8402](https://github.com/chatmail/core/pull/8402)).
- Automatically remove oldest unpublished relay in order to make space when the user wants to add more; don't allow more than 5 relays overall ([#8428](https://github.com/chatmail/core/pull/8428)).
- Add silent group changes messages as InNoticed, not InSeen.
- Remove `?emailaddress` argument from autoconfig URL that is not using a dedicated domain.
- Remove `imap::Session::sync_seen_flags()` ([#7742](https://github.com/chatmail/core/pull/7742)).
- Use CAPABILITY response code if IMAP LOGIN command returns it.
- Increase max idle timeout for iroh backup receiver to 60 seconds.
### Fixes
- Request MDNs for resent channel messages.
- Make pre-messages w/o text want MDNs ([#8004](https://github.com/chatmail/core/pull/8004)).
- Make truncated edited messages have HTML for receivers ([#8249](https://github.com/chatmail/core/pull/8249)).
- Un-escape message footer marks in full messages (`get_html`) ([#8427](https://github.com/chatmail/core/pull/8427)).
- Hide synced chat if we only know its visibility ([#8343](https://github.com/chatmail/core/pull/8343)).
- Tombstone MDN before sending it ([#8252](https://github.com/chatmail/core/pull/8252)).
- Recreate `imap_markseen` with `PRIMARY KEY` constraint.
- Rerun the full securejoin protocol if the address was outdated ([#8358](https://github.com/chatmail/core/pull/8358)).
- Return early from `receive_imf` to not tombstone Iroh-Node-Addr message if webxdc instance isn't found ([#8372](https://github.com/chatmail/core/pull/8372)).
- Replace `last_added_location_id` with `last_added_location_timestamp`.
- Do not put locations into pre-messages.
- RUSTSEC-2026-0204 ([#8403](https://github.com/chatmail/core/pull/8403)).
- Ensure public key signatures are not in the past compared to the public key.
- Do not bubble up errors in IMAP candidate loop.
- Do not log errors if full message is not available on any transport.
- Apply reactions that arrived before the message at later time ([#8415](https://github.com/chatmail/core/pull/8415)).
### Performance
- Add timestamp to `msgs_index7` and speed up `Chatlist::try_load()` ([#7848](https://github.com/chatmail/core/pull/7848)).
### CI
- Update Rust to 1.97.1.
- rrsync prepends the restricted upload path, we need to leave it out ([#8405](https://github.com/chatmail/core/pull/8405)).
### Documentation
- Update STYLE.md: macros should be used only when necessary ([#8410](https://github.com/chatmail/core/pull/8410)).
- `create_group_chat_unencrypted()` may lead to chat split on the first device.
### Refactor
- Deprecate unused `SkipAutocrypt` param.
- Remove commented out `RenderedEmail.envelope`.
- Remove the ability to send messages with non-standard header protection.
- Make `crate::pgp::symm_encrypt_message` non-async.
- Move `ensure_secret_key_exists` into key.rs.
- Improve comment ([#8366](https://github.com/chatmail/core/pull/8366)).
- Remove `set_modseq()` function.
- Remove unnecessary reference in format string.
- Label the loop iterating over the candidates.
- Remove `GROUP BY c.id` from chatlist queries.
### Tests
- securejoin: Check that "vc-{,request-}pubkey" messages don't contain displayname.
### Miscellaneous Tasks
- bump version to 2.54.0-dev.
- deps: bump taiki-e/install-action from 2.81.1 to 2.81.8.
- deps: bump taiki-e/install-action from 2.81.8 to 2.81.11.
- update rPGP from 0.19.0 to 0.20.0.
- update astral-tokio-tar from 0.6.2 to 0.6.3.
- deps: bump anyhow to 1.0.103.
- deps: bump actions/checkout from 6 to 7.
- cargo: bump syn from 2.0.117 to 2.0.118.
- cargo: bump quote from 1.0.45 to 1.0.46.
- cargo: bump bytes from 1.11.1 to 1.12.0.
- cargo: bump regex from 1.12.3 to 1.12.4.
- cargo: bump log from 0.4.31 to 0.4.33.
- cargo: bump hyper from 1.9.0 to 1.10.1.
- deps: bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7.
- cargo: bump chrono from 0.4.44 to 0.4.45.
- update quick-xml to 0.41.0.
- cargo: bump brotli from 8.0.2 to 8.0.4.
- cargo: bump smallvec from 1.15.1 to 1.15.2.
- deps: bump taiki-e/install-action from 2.81.11 to 2.82.6.
- update yanked spin@0.9.8 and spin@0.10.0.
- deps: bump taiki-e/install-action from 2.82.6 to 2.82.10.
- update async-imap to 0.11.3.
## [2.53.0] - 2026-06-15
### Features / Changes
- Make quality of images sent in chats more consistent between images with different aspect ratio.
- `MsgId::get_html`: Make only one db query.
- Do not log the recipient list for sent messages.
### Fixes
- Do not trash pre-messages without text but with a webxdc update.
- Don't send or process webxdc status updates in pre-messages.
- Ignore SecureJoin messages from blocked contacts ([#8295](https://github.com/chatmail/core/pull/8295)).
- Do not abort IMAP connection if setting the push token fails.
### Documentation
- STYLE.md: Require to list columns explicitly in `INSERT` statements.
### Build system
- nix: switch to the "master" branch for naersk.
- flake.nix: Use hostPlatform.rust.rustcTarget instead of hardcoding it.
### Miscellaneous Tasks
- Bump version to 2.52.0-dev.
- deps: bump taiki-e/install-action from 2.79.10 to 2.81.1.
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.19 to 2.0.20.
- Bump version to 2.53.0-dev.
### Refactor
- Move the definition of the `target_wh`-variable.
- Remove timesmearing.
### Tests
- Print multiline chat descriptions with debug formatter.
- `exec_securejoin_qr_multi_device()`: Make inviter devices receive each other messages.
- Fixup the tests after removing timesmearing.
- Remove timeout from `pop_sent_msg_ex()`.
## [2.52.0] - 2026-06-09
### Fixes
- Update the channel title after joining if the QR code included a wrong title ([#8260](https://github.com/chatmail/core/pull/8260)).
- Don't send removal message to contact that hasn't been a chat member ([#8298](https://github.com/chatmail/core/pull/8298)).
### Features / Changes
- Add cryptography-related statistics (`number_of_transports`, `key_version`, `key_algorithm`, `pubkey_size`, `number_of_keys`) ([#8293](https://github.com/chatmail/core/pull/8293), [#8297](https://github.com/chatmail/core/pull/8297)).
- Add IMAP folder to `Context::get_info()` ([#8285](https://github.com/chatmail/core/pull/8285)).
### Miscellaneous Tasks
- Update preloaded DNS cache.
- Use default aws-lc-rs cryptography provider for rustls.
- Add exception for unmaintained proc-macro-error2 to deny.toml.
- cargo: bump `pin-project` from 1.1.11 to 1.1.13.
- cargo: bump `tokio` from 1.52.1 to 1.52.3.
- cargo: bump `log` from 0.4.29 to 0.4.30.
- cargo: bump `serde_json` from 1.0.149 to 1.0.150.
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.18 to 2.0.19.
- deps: bump taiki-e/install-action from 2.79.2 to 2.79.10.
### Build system
- nix: fix windows cross-compilation by adding pthreads includes.
### Refactor
- Remove support for building "source" packages for deltachat-rpc-server.
## [2.51.0] - 2026-05-29
### Features / Changes
@@ -8785,12 +8297,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
[2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0
[2.52.0]: https://github.com/chatmail/core/compare/v2.51.0..v2.52.0
[2.53.0]: https://github.com/chatmail/core/compare/v2.52.0..v2.53.0
[2.54.0]: https://github.com/chatmail/core/compare/v2.53.0..v2.54.0
[2.55.0]: https://github.com/chatmail/core/compare/v2.54.0..v2.55.0
[2.56.0]: https://github.com/chatmail/core/compare/v2.55.0..v2.56.0
[2.57.0]: https://github.com/chatmail/core/compare/v2.56.0..v2.57.0
[2.58.0]: https://github.com/chatmail/core/compare/v2.57.0..v2.58.0
[2.59.0]: https://github.com/chatmail/core/compare/v2.58.0..v2.59.0
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0

View File

@@ -2,59 +2,45 @@ cmake_minimum_required(VERSION 3.16)
project(deltachat LANGUAGES C)
include(GNUInstallDirs)
option(WITH_JSONRPC_BINDINGS "Generate jsonrpc bindings" OFF)
find_program(CARGO cargo)
if(APPLE)
set(DYNAMIC_EXT "dylib")
set(DYNAMIC_EXT "dylib")
elseif(UNIX)
set(DYNAMIC_EXT "so")
set(DYNAMIC_EXT "so")
else()
set(DYNAMIC_EXT "dll")
set(DYNAMIC_EXT "dll")
endif()
if(DEFINED ENV{CARGO_BUILD_TARGET})
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/$ENV{CARGO_BUILD_TARGET}/release")
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
else()
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/release")
set(ARCH_DIR "./")
endif()
if(WITH_JSONRPC_BINDINGS)
set(JSONRPC_ARGS --package deltachat-jsonrpc-bindings)
endif()
add_custom_target(
lib_deltachat
ALL
COMMAND
${CMAKE_COMMAND} -E env
CARGO_TARGET_DIR="${CMAKE_BINARY_DIR}/target"
PREFIX="${CMAKE_INSTALL_PREFIX}"
LIBDIR="${CMAKE_INSTALL_FULL_LIBDIR}"
INCLUDEDIR="${CMAKE_INSTALL_FULL_INCLUDEDIR}"
${CARGO} build --release --package deltachat_ffi ${JSONRPC_ARGS}
WORKING_DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}"
add_custom_command(
OUTPUT
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
install(FILES "${CARGO_OUT_DIR}/libdeltachat.a" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(FILES "${CARGO_OUT_DIR}/libdeltachat.${DYNAMIC_EXT}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(FILES "${CARGO_OUT_DIR}/pkgconfig/deltachat.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
add_custom_target(
lib_deltachat
ALL
DEPENDS
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
)
if(WITH_JSONRPC_BINDINGS)
install(
FILES
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/types.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/client.hpp"
DESTINATION
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc/generated"
)
install(
FILES
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp"
DESTINATION
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc"
)
endif()
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

579
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.61.0-dev"
version = "2.51.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.89"
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
@@ -70,7 +70,7 @@ iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.5.0", default-features = false }
mail-builder = { version = "0.4.4", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.17"
@@ -78,10 +78,10 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.20.0", features = ["draft-pqc"], default-features = false }
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.41", features = ["escape-html"] }
quick-xml = { version = "0.39", features = ["escape-html"] }
rand-old = { package = "rand", version = "0.8" }
rand = { workspace = true }
regex = { workspace = true }
@@ -89,6 +89,7 @@ rusqlite = { workspace = true, features = ["sqlcipher"] }
sanitize-filename = { workspace = true }
sdp = "0.17.1"
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
@@ -100,9 +101,9 @@ tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6.3", default-features = false }
astral-tokio-tar = { version = "0.6.2", default-features = false }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
@@ -129,7 +130,6 @@ members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc",
"deltachat-jsonrpc-bindings",
"deltachat-rpc-server",
"deltachat-ratelimit",
"deltachat-repl",
@@ -203,7 +203,7 @@ thiserror = "2"
tokio = "1"
tokio-util = "0.7.18"
tracing-subscriber = "0.3"
yerpc = "0.7"
yerpc = "0.6.4"
[features]
default = ["vendored"]

View File

@@ -21,8 +21,8 @@ The following protocols are handled without requiring API users to know much abo
- secure TLS setup with DNS caching and shadowsocks/proxy support
- robust [SMTP](https://github.com/chatmail/async-smtp)
and [IMAP](https://github.com/chatmail/async-imap) handling
- robust [SMTP](https://github.com/chatmail/async-imap)
and [IMAP](https://github.com/chatmail/async-smtp) handling
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
and [MIME building](https://github.com/stalwartlabs/mail-builder).
@@ -167,7 +167,7 @@ $ cargo test -- --ignored
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
```sh
$ cargo install --locked cargo-bolero
$ cargo install cargo-bolero
```
Run fuzzing tests with

View File

@@ -59,13 +59,6 @@ If column is already declared without `NOT NULL`, use `IFNULL` function to provi
Use `HAVING COUNT(*) > 0` clause
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
List columns explicitly in `INSERT` statements:
```
INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0);
```
Otherwise if a new column with default value is added in a future DB version, an upgraded DB can't
be used with the old code, e.g. after transferring a DB from a device running a newer version.
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
an older version. Also don't change the column type, consider adding a new column with another name
@@ -181,10 +174,3 @@ Calling `into()`, `try_into()` or `parse()`
creates an indirection,
which is hard to follow for people who are not familiar with Rust,
or who are not using rust-analyzer.
## Use macros only when really needed
Macros can be hard to read for people unfamiliar with Rust,
and can have surprising effects like evaluating arguments multiple times.
Therefore, macros should only be used when really needed;
using functions is usually better.

View File

@@ -1,7 +1,7 @@
[package]
name = "deltachat-contact-tools"
version = "0.0.0" # No semver-stable versioning
edition = "2024"
edition = "2021"
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
license = "MPL-2.0"

View File

@@ -29,16 +29,17 @@
use std::fmt;
use std::ops::Deref;
use std::sync::LazyLock;
use anyhow::Result;
use anyhow::bail;
use regex::regex;
use anyhow::Result;
use regex::Regex;
mod vcard;
pub use vcard::{VcardContact, make_vcard, parse_vcard};
pub use vcard::{make_vcard, parse_vcard, VcardContact};
/// Valid contact address.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct ContactAddress(String);
impl Deref for ContactAddress {
@@ -87,7 +88,9 @@ impl rusqlite::types::ToSql for ContactAddress {
/// - Removes special characters from the name, see [`sanitize_name()`]
/// - Removes the name if it is equal to the address by setting it to ""
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
let (name, addr) = if let Some(captures) = regex!("(.*)<(.*)>").captures(addr.as_ref()) {
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
captures.get(1).map_or("", |m| m.as_str())
@@ -104,7 +107,7 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
let mut name = sanitize_name(name);
// If the 'display name' is just the address, remove it:
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)".
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
// If the display name is empty, DC will just show the address when it needs a display name.
if name == addr {
name = "".to_string();

View File

@@ -1,8 +1,10 @@
use std::sync::LazyLock;
use anyhow::Context as _;
use anyhow::Result;
use chrono::DateTime;
use chrono::NaiveDateTime;
use regex::regex;
use regex::Regex;
use crate::sanitize_name_and_addr;
@@ -208,7 +210,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
}
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
let unfolded_lines = regex!("\r?\n[\t ]").replace_all(vcard, "");
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
let mut lines = unfolded_lines.lines().peekable();
let mut contacts = Vec::new();

View File

@@ -220,10 +220,7 @@ END:VCARD
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(
contacts[0].profile_image.as_deref().unwrap(),
"/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="
);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
}
}
@@ -247,10 +244,7 @@ END:VCARD",
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice Wonderland");
assert_eq!(
contacts[0].key.as_ref().unwrap(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image, None);
}
@@ -278,15 +272,9 @@ END:VCARD",
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice");
assert_eq!(
contacts[0].key.as_ref().unwrap(),
"xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="
);
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
assert!(contacts[0].timestamp.is_err());
assert_eq!(
contacts[0].profile_image.as_ref().unwrap(),
"/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z"
);
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
}
#[test]

View File

@@ -1,8 +1,9 @@
[package]
name = "deltachat_ffi"
version = "2.61.0-dev"
version = "2.51.0-dev"
description = "Deltachat FFI"
edition = "2024"
edition = "2018"
readme = "README.md"
license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]

View File

@@ -19,6 +19,7 @@ typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t;
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;
@@ -453,9 +454,15 @@ char* dc_get_blobdir (const dc_context_t* context);
* always auto-downloaded.
* 0 = no limit (default).
* Changes affect future messages only.
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
* This is an experimental option not compatible to other MUAs
* and older Delta Chat versions.
* 1 = enable.
* 0 = disable (default).
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
* In contrast to `dc_set_chat_mute_duration()`,
@@ -493,6 +500,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `send_pw` = SMTP-password, guessed if left out
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `imap_certificate_checks` = how to check IMAP and SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
@@ -517,10 +525,9 @@ int dc_set_config (dc_context_t* context, const char*
*
* - `sys.version` = get the version string e.g. as `1.2.3` or as `1.2.3special4`.
* - `sys.msgsize_max_recommended` = maximal recommended attachment size in bytes.
* All possible overheads are already subtracted and this value can be used
* e.g. for direct comparison with the size of a file the user wants to attach.
* If an attachment is larger than this value, the message is sent anyway,
* but email servers are likely to reject the message when receiving it or before trying to send.
* All possible overheads are already subtracted and this value can be used e.g. for direct comparison
* with the size of a file the user wants to attach. If an attachment is larger than this value,
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
* - `sys.config_keys` = get a space-separated list of all config-keys available.
* The config-keys are the keys that can be passed to the parameter `key` of this function.
*
@@ -580,7 +587,36 @@ int dc_set_config_from_qr (dc_context_t* context, const char* qr);
char* dc_get_info (const dc_context_t* context);
/**
* Get URL that can be used to initiate an OAuth2 authorization.
*
* If an OAuth2 authorization is possible for a given e-mail address,
* this function returns the URL that should be opened in a browser.
*
* If the user authorizes access,
* the given redirect_uri is called by the provider.
* It's up to the UI to handle this call.
*
* The provider will attach some parameters to the URL,
* most important the parameter `code` that should be set as the `mail_pw`.
* With `server_flags` set to #DC_LP_AUTH_OAUTH2,
* dc_configure() can be called as usual afterwards.
*
* @memberof dc_context_t
* @param context The context object.
* @param addr E-mail address the user has entered.
* In case the user selects a different e-mail address during
* authorization, this is corrected in dc_configure()
* @param redirect_uri URL that will get `code` that is used as `mail_pw` then.
* Not all URLs are allowed here, however, the following should work:
* `chat.delta:/PATH`, `http://localhost:PORT/PATH`,
* `https://localhost:PORT/PATH`, `urn:ietf:wg:oauth:2.0:oob`
* (the latter just displays the code the user can copy+paste then)
* @return URL that can be opened in the browser to start OAuth2.
* Returned strings must be released using dc_str_unref().
* If OAuth2 is not possible for the given e-mail address, NULL is returned.
*/
char* dc_get_oauth2_url (dc_context_t* context, const char* addr, const char* redirect_uri);
#define DC_CONNECTIVITY_NOT_CONNECTED 1000
@@ -592,10 +628,13 @@ char* dc_get_info (const dc_context_t* context);
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (4000): Show e.g. the string "Connected" or a green dot
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
@@ -627,6 +666,24 @@ int dc_get_connectivity (dc_context_t* context);
char* dc_get_connectivity_html (dc_context_t* context);
#define DC_PUSH_NOT_CONNECTED 0
#define DC_PUSH_HEARTBEAT 1
#define DC_PUSH_CONNECTED 2
/**
* Get the current push notification state.
* One of:
* - DC_PUSH_NOT_CONNECTED
* - DC_PUSH_HEARTBEAT
* - DC_PUSH_CONNECTED
*
* @memberof dc_context_t
* @param context The context object.
* @return Push notification state.
*/
int dc_get_push_state (dc_context_t* context);
// connect
/**
@@ -653,7 +710,7 @@ char* dc_get_connectivity_html (dc_context_t* context);
* to get the full configuration from well-known URLs.
*
* - If _more_ options as `mail_server`, `mail_port`, `send_server`,
* `send_port` or `send_user` are specified,
* `send_port`, `send_user` or `server_flags` are specified,
* **autoconfigure/autodiscover is skipped**.
*
* While dc_configure() returns immediately,
@@ -688,21 +745,6 @@ char* dc_get_connectivity_html (dc_context_t* context);
void dc_configure (dc_context_t* context);
/**
* Add fake transport that cannot be used to connect.
*
* Used for offline tests only.
*
* To add a transport, use JSON-RPC calls `add_or_update_transport`
* and `add_transport_from_qr` instead.
*
* @memberof dc_context_t
* @param context The context object.
* @param addr The email address of the new transport.
*/
void dc_add_pseudo_transport (dc_context_t* context, const char *addr);
/**
* Check if the context is already configured.
*
@@ -876,14 +918,14 @@ uint32_t dc_create_chat_by_contact_id (dc_context_t* context, uint32_t co
/**
* Check, if there is a single chat with a given contact.
* Check, if there is a normal chat with a given contact.
* To get the chat messages, use dc_get_chat_msgs().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param contact_id The contact ID to check.
* @return If there is a single chat with the given contact_id, this chat_id is
* returned. If there is no single chat with the contact_id, the function
* @return If there is a normal chat with the given contact_id, this chat_id is
* returned. If there is no normal chat with the contact_id, the function
* returns 0.
*/
uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t contact_id);
@@ -1180,7 +1222,7 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to place a call for.
* This needs to be a single chat.
* This needs to be a one-to-one chat.
* @param place_call_info any data that other devices receive
* in #DC_EVENT_INCOMING_CALL.
* @param has_video Whether the call has video initially.
@@ -1586,7 +1628,7 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
* - 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
* be unexpected as (1) deleting a single chat also does not prevent new mails
* be unexpected as (1) deleting a normal chat also does not prevent new mails
* from arriving, (2) leaving a group requires sending a message to
* all group members - especially for groups not used for a longer time, this is
* really unexpected when deletion results in contacting all members again,
@@ -1604,7 +1646,7 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
/**
* Block a chat.
*
* Blocking single chats blocks the corresponding contact. Blocking
* Blocking 1:1 chats blocks the corresponding contact. Blocking
* mailing lists creates a pseudo-contact in the list of blocked
* contacts, so blocked mailing lists can be discovered and unblocked
* the same way as the contacts. Blocking group chats deletes the
@@ -1633,7 +1675,7 @@ void dc_accept_chat (dc_context_t* context, uint32_t ch
/**
* Get the contact IDs belonging to a chat.
*
* - for single chats, the function always returns exactly one contact,
* - for normal chats, the function always returns exactly one contact,
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
*
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
@@ -1784,6 +1826,8 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* @memberof dc_context_t
@@ -2069,7 +2113,7 @@ int dc_resend_msgs (dc_context_t* context, const uint3
* The concrete action depends on the type of the chat and on the users settings
* (dc_msgs_presented() may be a better name therefore, but well. :)
*
* - For single chats, the IMAP state is updated, MDN is sent
* - For normal chats, the IMAP state is updated, MDN is sent
* (if dc_set_config()-options `mdns_enabled` is set)
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
*
@@ -2282,7 +2326,7 @@ void dc_block_contact (dc_context_t* context, uint32_t co
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*
* @memberof dc_context_t
* @param context The context object.
@@ -2449,7 +2493,7 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
void dc_stop_ongoing_process (dc_context_t* context);
// securejoin
// out-of-band verification
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
@@ -2484,7 +2528,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* The QR code state is returned in dc_lot_t::state as:
*
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID:
* ask whether to start chatting with the contact;
* ask whether to verify the contact;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
@@ -2493,7 +2537,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
* contact fingerprint matches,
* contact fingerprint verified,
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id().
*
@@ -2569,24 +2613,23 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer a SecureJoin invitation.
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns
* DC_QR_ASK_VERIFYCONTACT, DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
* the SecureJoin protocol can be started using dc_join_securejoin()
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* The returned text will also work as a normal https:-link,
* so that the QR code is useful also without Delta Chat being installed
* or can be passed to contacts through other channels.
*
* To reset invitations, pass the link to dc_set_config_from_qr().
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
* the SecureJoin QR code for the group is returned.
* If set to 0, the setup contact QR code is returned.
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://securejoin.delta.chat/
* for details about both protocols.
* @return The text that should go to the QR code,
@@ -2612,7 +2655,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_t chat_id);
/**
* Continue the SecureJoin protocol
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with dc_get_securejoin_qr().
* This function is typically called when dc_check_qr() returns
* lot.state=DC_QR_ASK_VERIFYCONTACT, lot.state=DC_QR_ASK_VERIFYGROUP or lot.state=DC_QR_ASK_VERIFYBROADCAST
@@ -2632,6 +2675,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
* to dc_check_qr().
* @return The chat ID of the joined chat, the UI may redirect to the this chat.
* On errors, 0 is returned, however, most errors will happen during handshake later on.
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
*/
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
@@ -3193,33 +3237,19 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
/**
* Perform a background fetch for all accounts in parallel with a timeout.
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
*
* For an account with IO stopped, the scheduler is paused
* and every transport is fetched concurrently on a dedicated connection.
* The account is done as soon as one transport received messages, the others stop.
* Only one batch of messages is fetched per transport this way,
* so a larger backlog is left to the next call or to started IO.
* dc_accounts_background_fetch() was created for the iOS Background fetch.
*
* For an account with IO running, IMAP IDLE is interrupted on every transport
* and the account is done once every transport is.
*
* The call never waits for outgoing messages and never triggers sending them itself.
* Received messages may still queue replies, securejoin handshakes for example,
* which go out only while IO is running.
*
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end,
* also on timeout, when another background fetch is already running
* and when the call is ignored because the timeout is too small,
* so it is safe to wait for the event whenever `accounts` is not NULL.
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
* even in case of timeout, unless the function fails and returns 0.
* Process all events until you get this one and you can safely return to the background
* without forgetting to create a generic notification if no message was fetched.
* The event carries no data identifying the call it belongs to,
* so it marks your own call only if no concurrent background fetch is happening.
* without forgetting to create notifications caused by timing race conditions.
*
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @param timeout The timeout in seconds
* @return Return 0 if the call was ignored because `accounts` is NULL or the timeout is too small, 1 otherwise.
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
*/
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
@@ -3575,9 +3605,17 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
/**
* Get info summary for a chat, in JSON format.
* Helper function to get the associated context object.
*
* @deprecated 2026-08-13, use dedicated dc_chat_get_*() getters or jsonrpc
* @memberof dc_chatlist_t
* @param chatlist The chatlist object to empty.
* @return The context object associated with the chatlist. NULL if none or on errors.
*/
dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
/**
* Get info summary for a chat, in JSON format.
*
* The returned JSON string has the following key/values:
*
@@ -3589,6 +3627,7 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
* last-message-state: @ref DC_STATE constant
* last-message-date:
* avatar-path: path-to-blobfile
* is_verified: yes/no
* @return a UTF8-encoded JSON string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
*/
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
@@ -3658,7 +3697,7 @@ char* dc_chat_get_mailinglist_addr (const dc_chat_t* chat);
/**
* Get name of a chat. For single chats, this is the name of the contact.
* Get name of a chat. For one-to-one chats, this is the name of the contact.
* For group chats, this is the name given e.g. to dc_create_group_chat() or
* received by a group-creation message.
*
@@ -3675,7 +3714,7 @@ char* dc_chat_get_name (const dc_chat_t* chat);
* Get the chat's profile image.
* For groups, this is the image set by any group member
* using dc_set_chat_profile_image().
* For single chats, this is the image set by each remote user on their own
* For normal chats, this is the image set by each remote user on their own
* using dc_set_config(context, "selfavatar", image).
*
* @memberof dc_chat_t
@@ -3689,7 +3728,7 @@ char* dc_chat_get_profile_image (const dc_chat_t* chat);
/**
* Get a color for the chat.
* For single chats, the color is calculated from the contact's e-mail address.
* For 1:1 chats, the color is calculated from the contact's e-mail address.
* Otherwise, the chat name is used.
* The color can be used for an fallback avatar with white initials
* as well as for headlines in bubbles of group chats.
@@ -3756,7 +3795,7 @@ int dc_chat_is_unpromoted (const dc_chat_t* chat);
/**
* Check if a chat is a self talk. Self talks are single chats with
* Check if a chat is a self talk. Self talks are normal chats with
* the only contact DC_CONTACT_ID_SELF.
*
* @memberof dc_chat_t
@@ -3797,12 +3836,23 @@ int dc_chat_is_device_talk (const dc_chat_t* chat);
int dc_chat_can_send (const dc_chat_t* chat);
/**
* Deprecated, always returns 0.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Always 0.
* @deprecated 2025-09-09
*/
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Check if the chat is encrypted.
*
* Single chats with key-contacts and group chats with key-contacts
* 1:1 chats with key-contacts and group chats with key-contacts
* are encrypted.
* Single chats with emails contacts and ad-hoc groups
* 1:1 chats with emails contacts and ad-hoc groups
* created for email threads are not encrypted.
*
* @memberof dc_chat_t
@@ -4336,16 +4386,16 @@ char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_c
* display name, or NULL.
*
* If this returns non-NULL, put a `~` before the override-sender-name and show the
* override-sender-name and the sender's avatar even in single chats.
* override-sender-name and the sender's avatar even in 1:1 chats.
*
* In mailing lists, sender display name and sender address do not always belong together.
* In this case, this function gives you the name that should actually be shown over the message.
*
* Also, sometimes, we need to indicate a different sender in single chats:
* Also, sometimes, we need to indicate a different sender in 1:1 chats:
* Suppose that our user writes an e-mail to support@delta.chat, which forwards to
* Bob <bob@delta.chat>, and Bob replies.
*
* Then, Bob's reply is shown in our single chat with support@delta.chat and the override-sender-name is
* Then, Bob's reply is shown in our 1:1 chat with support@delta.chat and the override-sender-name is
* set to `Bob`. The UI should show the sender name as `~Bob` and show the avatar, just
* as in group messages. If the user then taps on the avatar, they can see that this message
* comes from bob@delta.chat.
@@ -4479,7 +4529,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
* - DC_INFO_GROUP_DESCRIPTION_CHANGED (70) - Info-message "Description changed", UI should open the profile with the description
* - DC_INFO_MESSAGE_PINNED (71) - Message pinned, UI should scroll to the pinned message returned by dc_msg_get_parent()
*
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
@@ -4539,7 +4588,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_CHAT_E2EE 50
#define DC_INFO_GROUP_DESCRIPTION_CHANGED 70
#define DC_INFO_MESSAGE_PINNED 71
/**
@@ -4857,8 +4905,6 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
* Used for Webxdc-info-messages
* to jump to the corresponding instance that created the info message.
*
* For Pinned-info-messages, this refers to the pinned message.
*
* For quotes, please use the more specialized
* dc_msg_get_quoted_text() and dc_msg_get_quoted_msg().
*
@@ -4898,20 +4944,6 @@ uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
*/
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
/**
* Check if the message is pinned.
*
* Pinned messages should be marked by a pin needle in the UI.
* To pin messages or get all pinned messages, use jsonrpc's "setPinnedMessageState" and "getPinnedMessages".
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is pinned, 0=message not pinned.
*/
int dc_msg_is_pinned (const dc_msg_t* msg);
/**
* @class dc_contact_t
*
@@ -4927,6 +4959,7 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
* By default, these names are equal,
* but functions working with contact names
* (e.g. dc_contact_get_name(), dc_contact_get_display_name(),
* dc_contact_get_name_n_addr(),
* dc_create_contact() or dc_add_address_book())
* only affect the given-name.
*/
@@ -4976,7 +5009,7 @@ char* dc_contact_get_addr (const dc_contact_t* contact);
* The function does not return the contact name as received from the network.
*
* This name is typically used in a form where the user can edit the name of a contact.
* To get a fine name to display in lists etc., use dc_contact_get_display_name().
* To get a fine name to display in lists etc., use dc_contact_get_display_name() or dc_contact_get_name_n_addr().
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5031,6 +5064,23 @@ char* dc_contact_get_display_name (const dc_contact_t* contact);
#define dc_contact_get_first_name dc_contact_get_display_name
/**
* Get a summary of name and address.
*
* The returned string is either "Name (email@domain.com)" or just
* "email@domain.com" if the name is unset.
*
* The summary is typically used when asking the user something about the contact.
* The attached e-mail address makes the question unique, e.g. "Chat with Alan Miller (am@uniquedomain.com)?"
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return A summary string, must be released using dc_str_unref().
* Never returns NULL.
*/
char* dc_contact_get_name_n_addr (const dc_contact_t* contact);
/**
* Get the contact's profile image.
* This is the image set by each remote user on their own
@@ -5110,6 +5160,19 @@ int dc_contact_was_seen_recently (const dc_contact_t* contact);
int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if the contact
* can be added to protected chats.
*
* See dc_contact_get_verifier_id() for a guidance how to display these information.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions.
*/
int dc_contact_is_verified (dc_contact_t* contact);
/**
* Returns whether contact is a bot.
*
@@ -5134,6 +5197,128 @@ int dc_contact_is_bot (dc_contact_t* contact);
int dc_contact_is_key_contact (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
*
* As verifier may be unknown,
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
*
* UI should display the information in the contact's profile as follows:
*
* - If dc_contact_get_verifier_id() != 0,
* display text "Introduced by ..."
* with the name of the contact
* formatted by dc_contact_get_name().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
* display "Introduced" prefixed by a green checkmark.
*
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
* display nothing
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
uint32_t dc_contact_get_verifier_id (dc_contact_t* contact);
/**
* @class dc_provider_t
*
* Opaque object containing information about one single e-mail provider.
*/
/**
* Create a provider struct for the given e-mail address by local lookup.
*
* Lookup is done from the local database by extracting the domain from the e-mail address.
* Therefore the provider for custom domains cannot be identified.
*
* @memberof dc_provider_t
* @param context The context object.
* @param email The user's e-mail address to extract the provider info form.
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
/**
* Create a provider struct for the given e-mail address by local lookup.
*
* DNS lookup is not used anymore and this function is deprecated.
*
* @memberof dc_provider_t
* @param context The context object.
* @param email The user's e-mail address to extract the provider info form.
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
*/
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
/**
* URL of the overview page.
*
* This URL allows linking to the providers page on providers.delta.chat.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string with a fully-qualified URL,
* if there is no such URL, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_overview_page (const dc_provider_t* provider);
/**
* Get hints to be shown to the user on the login screen.
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
* the UI may want to highlight the hint.
*
* Moreover, the UI should display a "More information" link
* that forwards to the URL returned by dc_provider_get_overview_page().
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string with the hint to show to the user, may contain multiple lines,
* if there is no such hint, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
/**
* Whether DC works with this provider.
*
* Can be one of #DC_PROVIDER_STATUS_OK,
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return The status as a constant number.
*/
int dc_provider_get_status (const dc_provider_t* provider);
/**
* Free the provider info struct.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
*/
void dc_provider_unref (dc_provider_t* provider);
/**
* @class dc_lot_t
*
@@ -5447,7 +5632,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CHAT_TYPE_UNDEFINED 0
/**
* A single chat with a single contact.
* A one-to-one chat with a single contact.
*
* dc_get_chat_contacts() contains one record for the user.
* DC_CONTACT_ID_SELF is added _only_ for a self talk.
@@ -5548,6 +5733,41 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @}
*/
/**
* @defgroup DC_LP DC_LP
*
* Flags for configuring IMAP and SMTP servers.
* These flags are optional
* and may be set together with the username, password etc.
* via dc_set_config() using the key "server_flags".
*
* @addtogroup DC_LP
* @{
*/
/**
* Force OAuth2 authorization. This flag does not skip automatic configuration.
* Before calling dc_configure() with DC_LP_AUTH_OAUTH2 set,
* the user has to confirm access at the URL returned by dc_get_oauth2_url().
*/
#define DC_LP_AUTH_OAUTH2 0x2
/**
* Force NORMAL authorization, this is the default.
* If this flag is set, automatic configuration is skipped.
*/
#define DC_LP_AUTH_NORMAL 0x4
/**
* @}
*/
#define DC_LP_AUTH_FLAGS (DC_LP_AUTH_OAUTH2|DC_LP_AUTH_NORMAL) // if none of these flags are set, the default is chosen
/**
* @defgroup DC_CERTCK DC_CERTCK
*
@@ -5635,7 +5855,6 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
* - getAccountFileSize()
* - importVcard(), parseVcard(), makeVcard()
* - sendWebxdcRealtimeData, sendWebxdcRealtimeAdvertisement(), leaveWebxdcRealtime()
* - setPinnedMessageState(), getPinnedMessages()
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
@@ -6122,15 +6341,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_MSG_DELETED 2016
/**
* Like @ref DC_EVENT_MSG_READ, but also fires on subsequent MDNs,
* if there are multiple receivers, i.e. in groups and channels.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
*/
#define DC_EVENT_MSG_READ_COUNT_CHANGED 2018
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
@@ -6164,7 +6374,7 @@ void dc_event_unref(dc_event_t* event);
/**
* Contact(s) created, renamed, blocked or deleted.
* Contact(s) created, renamed, verified, blocked or deleted.
*
* @param data1 (int) contact_id of the changed contact or 0 on batch-changes or deletion.
* @param data2 0
@@ -6236,7 +6446,8 @@ void dc_event_unref(dc_event_t* event);
*
* @param data1 (int) The ID of the inviting contact.
* @param data2 (int) The progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
* 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
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
@@ -6319,18 +6530,11 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
/**
* Tells that a call to dc_accounts_background_fetch() is done:
* the fetch completed, timed out, was stopped or was not started.
*
* For the call that started the fetch, this event acts as a marker:
* when you reach it, all events emitted during the fetch were processed.
* A call made while another background fetch is running gets the event immediately,
* and the running fetch keeps emitting events until its own marker.
*
* The event carries no data identifying the call it belongs to,
* so it marks your own call only if no concurrent background fetch is happening.
* Your own call has finished when dc_accounts_background_fetch() returns.
* Tells that the Background fetch was completed (or timed out).
*
* This event acts as a marker, when you reach this event you can be sure
* that all events emitted during the background fetch were processed.
*
* This event is only emitted by the account manager
*/
@@ -6426,7 +6630,7 @@ void dc_event_unref(dc_event_t* event);
/**
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
* Moreover, the event is sent when the call was not accepted within two minutes.
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
*
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
@@ -6438,10 +6642,9 @@ void dc_event_unref(dc_event_t* event);
* Transport relay added/deleted or default has changed.
* UI should update the list.
*
* The event is emitted on the device modifying the transports
* as well as on other devices applying the synced change,
* for the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`,
* `delete_transport` or `set_config(configured_addr)`.
* 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`,
* `set_transport_unpublished` or `set_config(configured_addr)`.
*/
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
@@ -6462,6 +6665,61 @@ void dc_event_unref(dc_event_t* event);
#define DC_MEDIA_QUALITY_WORSE 1
/**
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
*
* These constants are used as return values for dc_provider_get_status().
*
* @addtogroup DC_PROVIDER_STATUS
* @{
*/
/**
* Provider works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the e-mail address.
*
* - There is no need for the user to do any special things
* (enable IMAP or so) in the provider's web interface or at other places.
* - There is no need for the user to enter advanced settings;
* server, port etc. are known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_OK 1
/**
* Provider works, but there are preparations needed.
*
* - The user has to do some special things as "Enable IMAP in the web interface",
* what exactly, is described in the string returned by dc_provider_get_before_login_hints()
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
* - There is no need for the user to enter advanced settings;
* server, port etc. should be known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_PREPARATION 2
/**
* Provider is not working.
* This provider status is returned for providers
* that are known to not work with Delta Chat.
* The UI should block logging in with this provider.
*
* More information about that is typically provided
* in the string returned by dc_provider_get_before_login_hints()
* and in the page linked by dc_provider_get_overview_page().
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_BROKEN 3
/**
* @}
*/
/**
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
*
@@ -6626,12 +6884,21 @@ void dc_event_unref(dc_event_t* event);
/// Used to build the string returned by dc_get_contact_encrinfo().
#define DC_STR_FINGERPRINTS 30
/// "%1$s verified"
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the verified contact
#define DC_STR_CONTACT_VERIFIED 35
/// "Archived chats"
///
/// Used as the name for the corresponding chatlist entry.
#define DC_STR_ARCHIVEDCHATS 40
/// @deprecated 2026-08-24
/// "Cannot login as %1$s."
///
/// Used in error strings.
/// - %1$s will be replaced by the failing login name
#define DC_STR_CANNOT_LOGIN 60
/// "Location streaming enabled."
@@ -6676,7 +6943,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message from %1$s"
///
/// Used in subjects of outgoing messages in single chats.
/// Used in subjects of outgoing messages in one-to-one chats.
/// - %1$s will be replaced by the name of the sender,
/// this is the dc_set_config()-option `displayname` or `addr`
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
@@ -6804,7 +7071,7 @@ void dc_event_unref(dc_event_t* event);
///
/// Added as an info-message directly after scanning a QR code for joining a group.
/// May be followed by the info-messages
/// #DC_STR_SECURE_JOIN_REPLIES and #DC_STR_MSGADDMEMBER.
/// #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.
#define DC_STR_SECURE_JOIN_STARTED 117
@@ -6813,13 +7080,15 @@ void dc_event_unref(dc_event_t* event);
///
/// Info-message on scanning a QR code for joining a group.
/// Added after #DC_STR_SECURE_JOIN_STARTED.
/// If the handshake allows to skip a step and go for #DC_STR_CONTACT_VERIFIED directly,
/// this info-message is skipped.
///
/// `%1$s` will be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_REPLIES 118
/// "Scan to chat with %1$s"
///
/// Subtitle for the invite qrcode svg image generated by the core.
/// Subtitle for verification qrcode svg image generated by the core.
///
/// `%1$s` will be replaced by name of the inviter.
#define DC_STR_SETUP_CONTACT_QR_DESC 119
@@ -7114,30 +7383,6 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by name of the removed contact.
#define DC_STR_REMOVE_MEMBER 178
/// "You were removed by %1$s."
///
/// `%1$s` will be replaced by name of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_REMOVE_YOU_BY 179
/// "You were added by %1$s."
///
/// `%1$s` will be replaced by name of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_ADD_YOU_BY 180
/// "You were removed."
///
/// Used in status messages.
#define DC_STR_REMOVE_YOU 181
/// "You were added."
///
/// Used in status messages.
#define DC_STR_ADD_YOU 182
/// "Establishing connection, please wait…"
///
/// Used as info message.
@@ -7233,15 +7478,6 @@ void dc_event_unref(dc_event_t* event);
/// Used when creating text for the "Encryption Info" dialogs.
#define DC_STR_MESSAGES_ARE_E2EE 242
/// "You pinned a message."
#define DC_STR_MESSAGE_PINNED_BY_YOU 243
/// "Message pinned by %1$s."
#define DC_STR_MESSAGE_PINNED_BY_OTHER 244
/// @deprecated 2026-08-31
#define DC_STR_PHASING_OUT 245
/**
* @}
*/

View File

@@ -1,4 +1,5 @@
use crate::chat::ChatItem;
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::contact::ContactId;
use crate::location::Location;
use crate::message::MsgId;
@@ -20,7 +21,7 @@ impl dc_array_t {
Self::ContactIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
},
Self::Locations(array) => array[index].location_id,
Self::Uint(array) => array[index],

File diff suppressed because it is too large Load Diff

View File

@@ -17,15 +17,13 @@ use std::ptr;
/// }
/// ```
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
unsafe {
let ret: *mut libc::c_char = if !s.is_null() {
libc::strdup(s)
} else {
libc::calloc(1, 1) as *mut libc::c_char
};
assert!(!ret.is_null());
ret
}
let ret: *mut libc::c_char = if !s.is_null() {
libc::strdup(s)
} else {
libc::calloc(1, 1) as *mut libc::c_char
};
assert!(!ret.is_null());
ret
}
/// Error type for the [OsStrExt] trait
@@ -166,40 +164,34 @@ pub(crate) trait Strdup {
/// This function will panic when the original string contains an
/// interior null byte as this can not be represented in raw C
/// strings.
fn strdup(&self) -> *mut libc::c_char;
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl Strdup for str {
fn strdup(&self) -> *mut libc::c_char {
unsafe {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
}
impl Strdup for String {
fn strdup(&self) -> *mut libc::c_char {
unsafe fn strdup(&self) -> *mut libc::c_char {
let s: &str = self;
s.strdup()
}
}
impl Strdup for std::path::Path {
fn strdup(&self) -> *mut libc::c_char {
unsafe {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
dc_strdup(tmp.as_ptr())
}
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
dc_strdup(tmp.as_ptr())
}
}
impl Strdup for [u8] {
fn strdup(&self) -> *mut libc::c_char {
unsafe {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
}
@@ -217,15 +209,15 @@ pub(crate) trait OptStrdup {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [Strdup::strdup] for details.
fn strdup(&self) -> *mut libc::c_char;
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrdup for Option<T> {
fn strdup(&self) -> *mut libc::c_char {
unsafe fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::new_lossy(s.as_ref());
unsafe { dc_strdup(tmp.as_ptr()) }
dc_strdup(tmp.as_ptr())
}
None => ptr::null_mut(),
}
@@ -263,18 +255,20 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
///
/// [Path]: std::path::Path
#[cfg(not(target_os = "windows"))]
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
use std::os::unix::ffi::OsStrExt;
let c_str = unsafe { std::ffi::CStr::from_ptr(s) }.to_bytes();
let os_str = std::ffi::OsStr::from_bytes(c_str);
std::path::Path::new(os_str)
unsafe {
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
let os_str = std::ffi::OsStr::from_bytes(c_str);
std::path::Path::new(os_str)
}
}
// as_path() implementation for windows, documented above.
#[cfg(target_os = "windows")]
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
unsafe { as_path_unicode(s) }
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
as_path_unicode(s)
}
// Implementation for as_path() on Windows.
@@ -282,7 +276,7 @@ pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path
// Having this as a separate function means it can be tested on unix
// too.
#[allow(dead_code)]
unsafe fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
let cstr = unsafe { CStr::from_ptr(s) };
@@ -370,20 +364,14 @@ mod tests {
fn test_as_path() {
let some_path = CString::new("/some/path").unwrap();
let ptr = some_path.as_ptr();
assert_eq!(
unsafe { as_path(ptr) },
std::ffi::OsString::from("/some/path")
)
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
}
#[test]
fn test_as_path_unicode_fn() {
let some_path = CString::new("/some/path").unwrap();
let ptr = some_path.as_ptr();
assert_eq!(
unsafe { as_path_unicode(ptr) },
std::ffi::OsString::from("/some/path")
);
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
}
#[test]

View File

@@ -1,16 +0,0 @@
[package]
name = "deltachat-jsonrpc-bindings"
version = "2.61.0-dev"
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/chatmail/core"
[build-dependencies]
deltachat-jsonrpc = { workspace = true }
[dependencies]
[features]
default = ["vendored"]
vendored = ["deltachat-jsonrpc/vendored"]

View File

@@ -1,7 +0,0 @@
use deltachat_jsonrpc::api::{write_qt_bindings, write_ts_bindings};
use std::path::Path;
fn main() {
write_ts_bindings(Path::new("typescript/generated"));
write_qt_bindings(Path::new("qt/generated"), "deltachat");
}

View File

@@ -1 +0,0 @@
generated

View File

@@ -1,111 +0,0 @@
#pragma once
#include "deltachat.h"
#include "generated/client.hpp"
#include "generated/types.hpp"
#include <cstdint>
#include <mutex>
#include <thread>
namespace deltachat {
class CffiTransport : public Transport {
using CompletionHandler = Transport::CompletionHandler;
public:
explicit CffiTransport(dc_accounts_t *accounts)
: jsonrpc_(dc_jsonrpc_init(accounts)) {
if (!jsonrpc_)
std::abort();
thread_ = std::thread([this] { run(); });
}
~CffiTransport() override {
done_ = true;
// Unblock dc_jsonrpc_next_response by sending a dummy request
if (jsonrpc_)
dc_jsonrpc_request(
jsonrpc_,
"{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"get_system_info\"}");
if (thread_.joinable())
thread_.join();
std::lock_guard lk(mu_);
for (auto &[id, cb] : pending_) {
cb(Result<QJsonValue>::error(-32060, "Transport destructed"));
}
pending_.clear();
if (jsonrpc_)
dc_jsonrpc_unref(jsonrpc_);
}
virtual void send(const QString method, const QJsonValue params,
CompletionHandler onCompleted) override {
uint32_t id = next_id_++;
QJsonObject envelope{
{"jsonrpc", "2.0"},
{"id", static_cast<qint64>(id)},
{"method", method},
{"params", params},
};
{
std::lock_guard lk(mu_);
pending_[id] = std::move(onCompleted);
}
QByteArray json = QJsonDocument(envelope).toJson(QJsonDocument::Compact);
dc_jsonrpc_request(jsonrpc_, json.constData());
}
private:
void run() {
while (!done_) {
char *raw_json = dc_jsonrpc_next_response(jsonrpc_);
if (!raw_json) {
break;
}
QByteArray json{raw_json};
dc_str_unref(raw_json);
if (done_)
break;
QJsonObject obj = QJsonDocument::fromJson(json).object();
if (!obj["id"].isDouble()) {
qCritical() << "No valid rpc id in" << QString{json};
continue;
}
uint32_t id = static_cast<uint32_t>(obj["id"].toInt());
CompletionHandler cb;
{
std::lock_guard<std::mutex> lk(mu_);
if (auto nh = pending_.extract(id)) {
cb = std::move(nh.mapped());
} else {
qCritical() << "Could not map response" << QString{json};
continue;
}
}
cb(parseResult(obj));
}
}
private:
dc_jsonrpc_instance_t *jsonrpc_;
std::thread thread_;
std::mutex mu_;
std::atomic<uint32_t> next_id_{1};
std::atomic<bool> done_{false};
std::unordered_map<uint32_t, CompletionHandler> pending_;
};
class CffiDeltaChat : public RawClient {
public:
explicit CffiDeltaChat(dc_accounts_t *accounts)
: RawClient(std::make_unique<CffiTransport>(accounts)) {}
};
} // namespace deltachat
Q_DECLARE_METATYPE(deltachat::CffiDeltaChat *)

View File

@@ -1 +0,0 @@

View File

@@ -1,8 +1,8 @@
[package]
name = "deltachat-jsonrpc"
version = "2.61.0-dev"
version = "2.51.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2024"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/chatmail/core"

View File

@@ -5,30 +5,31 @@ use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, Result, anyhow, bail, ensure};
use deltachat::EventEmitter;
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, Chat, ChatId, ChatItem, MessageListOptions, add_contact_to_chat, forward_msgs,
forward_msgs_2ctx, get_chat_media, get_chat_msgs, get_chat_msgs_ext, markfresh_chat,
marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat,
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::{Config, get_all_ui_config_keys};
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
use deltachat::config::{get_all_ui_config_keys, Config};
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::{
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ext, get_existing_msg_ids,
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs,
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
@@ -36,9 +37,10 @@ use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{Mutex, RwLock, watch};
use tokio::sync::{watch, Mutex, RwLock};
use types::login_param::EnteredLoginParam;
use yerpc::rpc;
@@ -52,6 +54,8 @@ use types::contact::{ContactObject, VcardContact};
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::notify_state::JsonrpcNotifyState;
use types::provider_info::ProviderInfo;
use types::reactions::JsonrpcReactions;
use types::webxdc::WebxdcMessageInfo;
@@ -63,8 +67,8 @@ use self::types::{
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use crate::api::types::appversions::JsonrpcAppSource;
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::login_param::TransportListEntry;
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
#[derive(Debug)]
@@ -153,7 +157,7 @@ impl CommandApi {
}
}
#[rpc(all_positional)]
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
@@ -278,26 +282,9 @@ impl CommandApi {
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// For an account with IO stopped, the scheduler is paused
/// and every transport is fetched concurrently on a dedicated connection.
/// The account is done as soon as one transport received messages, the others stop.
/// Only one batch of messages is fetched per transport this way,
/// so a larger backlog is left to the next call or to started IO.
///
/// For an account with IO running, IMAP IDLE is interrupted on every transport
/// and the account is done once every transport is.
///
/// The call never waits for outgoing messages and never triggers sending them itself.
/// Received messages may still queue replies, securejoin handshakes for example,
/// which go out only while IO is running.
/// Use `is_sending_finished()` to tell whether the outgoing queue is empty.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout,
/// and immediately if another background fetch is already running.
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create a generic notification if no message was fetched.
/// The event carries no data identifying the call it belongs to,
/// so it marks your own call only if no concurrent background fetch is happening.
/// without forgetting to create notifications caused by timing race conditions.
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
let future = {
let lock = self.accounts.read().await;
@@ -308,11 +295,6 @@ impl CommandApi {
Ok(())
}
/// Stops an ongoing `background_fetch()` call, making it return early
/// without waiting for the remaining transports or for the timeout.
///
/// The `AccountsBackgroundFetchDone` event is emitted as usual.
/// Does nothing if no background fetch is running.
async fn stop_background_fetch(&self) -> Result<()> {
self.accounts.read().await.stop_background_fetch();
Ok(())
@@ -348,6 +330,12 @@ impl CommandApi {
}
}
/// Get the current push notification state.
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.push_state().await.into())
}
/// Get the combined filesize of an account in bytes
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
let ctx = self.get_context(account_id).await?;
@@ -357,6 +345,21 @@ impl CommandApi {
Ok(dbfile + total_size)
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
async fn get_provider_info(
&self,
_account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
Ok(ProviderInfo::from_dc_type(provider_info))
}
/// Checks if the context is already configured.
async fn is_configured(&self, account_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
@@ -524,6 +527,7 @@ impl CommandApi {
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
async fn add_or_update_transport(
&self,
account_id: u32,
@@ -548,8 +552,24 @@ impl CommandApi {
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::delete_transport()] to remove a transport.
/// and [Self::delete_transport()] to delete a transport.
/// Use [Self::list_transports_ex()] to additionally query
/// whether the transports are marked as 'unpublished'.
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
let ctx = self.get_context(account_id).await?;
let res = ctx
.list_transports()
.await?
.into_iter()
.map(|t| t.param.into())
.collect();
Ok(res)
}
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
async fn list_transports_ex(&self, account_id: u32) -> Result<Vec<TransportListEntry>> {
let ctx = self.get_context(account_id).await?;
let res = ctx
.list_transports()
@@ -560,17 +580,33 @@ impl CommandApi {
Ok(res)
}
/// Removes a transport.
/// UIs should call this function when the user removes a relay.
///
/// The last transport cannot be removed.
/// If the removed transport was the one used for sending,
/// another one is chosen automatically.
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.delete_transport(&addr).await
}
/// Change whether the transport is unpublished.
///
/// Unpublished transports are not advertised to contacts,
/// and self-sent messages are not sent there,
/// so that we don't cause extra messages to the corresponding inbox,
/// but can still receive messages from contacts who don't know our new transport addresses yet.
///
/// The default is false, but when the user updates from a version that didn't have this flag,
/// existing secondary transports are set to unpublished,
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
async fn set_transport_unpublished(
&self,
account_id: u32,
addr: String,
unpublished: bool,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.set_transport_unpublished(&addr, unpublished).await
}
/// Signal an ongoing process to stop.
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -823,7 +859,7 @@ impl CommandApi {
/// - 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
/// be unexpected as (1) deleting a single chat also does not prevent new mails
/// be unexpected as (1) deleting a normal chat also does not prevent new mails
/// from arriving, (2) leaving a group requires sending a message to
/// all group members - especially for groups not used for a longer time, this is
/// really unexpected when deletion results in contacting all members again,
@@ -847,8 +883,6 @@ impl CommandApi {
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
///
/// To reset invitations, pass the link to `set_config_from_qr()`.
///
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
/// If `chat_id` is unset, setup contact QR code is returned.
async fn get_chat_securejoin_qr_code(
@@ -862,19 +896,20 @@ impl CommandApi {
Ok(qr)
}
/// Get QR code (text and SVG) that will offer a SecureJoin invitation.
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
///
/// The scanning device will pass the scanned content to `checkQr()` then;
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
/// the securejoin protocol can be started using `secure_join()`
/// an out-of-band-verification can be joined using `secure_join()`
///
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
///
/// chat_id: If set to a group-chat-id,
/// the SecureJoin QR code for the group is returned.
/// If not set, the setup contact QR code is returned.
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://securejoin.delta.chat/ for details about both protocols.
///
/// return format: `[code, svg]`
@@ -890,7 +925,7 @@ impl CommandApi {
Ok((qr, svg))
}
/// Continue the SecureJoin protocol
/// Continue a Setup-Contact or Verified-Group-Invite protocol
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
/// This function is typically called when `check_qr()` returns
/// type=AskVerifyContact or type=AskVerifyGroup.
@@ -908,6 +943,7 @@ impl CommandApi {
/// to `check_qr()`.
///
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
/// A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
///
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
@@ -973,6 +1009,8 @@ impl CommandApi {
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// If the group has group protection enabled, only verified contacts can be added to the group.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
async fn add_contact_to_chat(
&self,
@@ -986,7 +1024,7 @@ impl CommandApi {
/// Get the contact IDs belonging to a chat.
///
/// - for single chats, the function always returns exactly one contact,
/// - for normal chats, the function always returns exactly one contact,
/// DC_CONTACT_ID_SELF is returned only for SELF-chats.
///
/// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
@@ -1043,9 +1081,7 @@ impl CommandApi {
/// Create a new unencrypted group chat.
///
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
/// address-contacts. NB: Chats with similar names and the same members are merged on other
/// devices, but usually users don't create such chats and look up the existing one instead, so
/// chat split on the first device is acceptable.
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_unencrypted(&ctx, &name)
@@ -1314,7 +1350,7 @@ impl CommandApi {
/// The concrete action depends on the type of the chat and on the users settings
/// (dc_msgs_presented() may be a better name therefore, but well. :)
///
/// - For single chats, the IMAP state is updated, MDN is sent
/// - For normal chats, the IMAP state is updated, MDN is sent
/// (if set_config()-options `mdns_enabled` is set)
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
///
@@ -1352,7 +1388,7 @@ impl CommandApi {
///
/// * chat_id The chat ID of which the messages IDs should be queried.
/// * _info_only: Deprecated, pass `false` here.
/// * `add_daymarker` - If `true`, add day markers as `MsgId::DAYMARKER` to the result,
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
/// corresponding (following) day in the local timezone.
async fn get_message_ids(
@@ -1363,7 +1399,7 @@ impl CommandApi {
add_daymarker: bool,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ext(
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions { add_daymarker },
@@ -1374,7 +1410,7 @@ impl CommandApi {
.map(|chat_item| -> u32 {
match chat_item {
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
deltachat::chat::ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
}
})
.collect())
@@ -1412,7 +1448,7 @@ impl CommandApi {
add_daymarker: bool,
) -> Result<Vec<JsonrpcMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ext(
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions { add_daymarker },
@@ -1478,32 +1514,12 @@ impl CommandApi {
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
}
/// Sets the "pinned" state for a message.
async fn set_pinned_message_state(
&self,
account_id: u32,
message_id: u32,
pinned_state: bool,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
deltachat::pinned_messages::set_pinned_state(&ctx, MsgId::new(message_id), pinned_state)
.await
}
/// Returns all pinned messages of a chat.
async fn get_pinned_messages(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids =
deltachat::pinned_messages::get_pinned_messages(&ctx, ChatId::new(chat_id)).await?;
Ok(msg_ids.into_iter().map(|id| id.to_u32()).collect())
}
/// Delete messages. The messages are deleted on the current device and
/// on the IMAP server.
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs_ext(&ctx, &msgs, false).await
delete_msgs_ex(&ctx, &msgs, false).await
}
/// Delete messages. The messages are deleted on the current device,
@@ -1511,7 +1527,7 @@ impl CommandApi {
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs_ext(&ctx, &msgs, true).await
delete_msgs_ex(&ctx, &msgs, true).await
}
/// Get an informational text for a single message. The text is multiline and may
@@ -1794,7 +1810,7 @@ impl CommandApi {
/// Get encryption info for a contact.
/// Get a multi-line encryption info, containing your fingerprint and the
/// fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
/// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
async fn get_contact_encryption_info(
&self,
account_id: u32,
@@ -1876,7 +1892,7 @@ impl CommandApi {
// chat
// ---------------------------------------------
/// Returns the [`ChatId`] for the single chat with `contact_id` if it exists.
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
///
/// If it does not exist, `None` is returned.
async fn get_chat_id_by_contact_id(
@@ -2059,20 +2075,15 @@ impl CommandApi {
Ok(())
}
/// Waits until all transports are idle or failed and no background work is left.
/// Never returns unless I/O is started. Must ONLY be used by tests.
async fn wait_for_all_work_done(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.wait_for_all_work_done().await;
Ok(())
}
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
/// One of:
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (4000): Show e.g. the string "Connected" or a green dot
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
///
/// We don't use exact values but ranges here so that we can split up
/// states into multiple states in the future.
///
/// Meant as a rough overview that can be shown
/// e.g. in the title of the main screen.
@@ -2256,9 +2267,6 @@ impl CommandApi {
/// Get blob encoded as base64 from a webxdc message
///
/// path is the path of the file within webxdc archive
///
/// If the file is `icon.png` or `icon.jpg`,
/// loading it may fail if dimensions are unexpectedly large.
async fn get_webxdc_blob(
&self,
account_id: u32,
@@ -2269,7 +2277,7 @@ impl CommandApi {
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
let blob = message.get_webxdc_blob(&ctx, &path).await?;
use base64::{Engine as _, engine::general_purpose};
use base64::{engine::general_purpose, Engine as _};
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
@@ -2439,7 +2447,6 @@ impl CommandApi {
}
/// Returns reactions to the message.
/// `None` when there are no reactions.
async fn get_message_reactions(
&self,
account_id: u32,
@@ -2771,48 +2778,6 @@ impl CommandApi {
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
}
}
/// Get version information of a specific client and source
/// across all configured accounts and transports.
///
/// Returns the source with the highest `version_integer`.
/// If no matching version information is available at all, `None` is returned.
///
/// UIs shall call the function after a reasonable time after app start,
/// when most relays have reported the information they have, say 30 seconds.
/// After that, once a day.
/// (it is accepted if by the simple approach an update message is delayed.
/// an event was considered, but that seemed more complex for few benefit:
/// as we do not know if "late" relays will report "better" versions,
/// also there we would work with timeouts etc.)
///
/// If the reported `version_integer` is larger than the running app version,
/// the UI shall report to the user, that an update is available,
/// and, if possible, offer a direct update by the given URL.
///
/// Security note: consumers need to verify themselves
/// that downloaded app files are valid before installing them.
async fn get_app_version(
&self,
client_id: String,
source_id: String,
) -> Result<Option<JsonrpcAppSource>> {
let accounts = self.accounts.read().await;
Ok(
deltachat::appversions::get_app_version(&accounts, &client_id, &source_id)
.await?
.map(JsonrpcAppSource::from_core_type),
)
}
/// Returns true if all accounts have empty outgoing message queue.
///
/// This API is intended to be used by UIs
/// to request that operating system does not put the application in background
/// while there are still outgoing messages that are not sent out.
async fn is_sending_finished(&self) -> Result<bool> {
self.accounts.read().await.is_sending_finished().await
}
}
// Helper functions (to prevent code duplication)

View File

@@ -1,31 +0,0 @@
use deltachat::appversions::AppSource;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
/// Version information of a single source of a client, eg. "gplay" or "fdroid".
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "AppSource", rename_all = "camelCase")]
pub struct JsonrpcAppSource {
/// Always increasing version number.
pub version_integer: u32,
/// Version string that should be shown to the user.
/// UI must not linkify the string
/// as it may be interpreted like a phone number or an IP address.
pub version_string: String,
/// Where to download that version.
/// Security note: consumers need to verify themselves
/// that downloaded app files are valid before installing them.
pub download_url: String,
}
impl JsonrpcAppSource {
pub fn from_core_type(source: AppSource) -> Self {
JsonrpcAppSource {
version_integer: source.version_integer,
version_string: source.version_string,
download_url: source.download_url,
}
}
}

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use deltachat::calls::{CallState, call_state};
use deltachat::calls::{call_state, CallState};
use deltachat::context::Context;
use deltachat::message::MsgId;
use serde::Serialize;

View File

@@ -1,7 +1,7 @@
use std::time::{Duration, SystemTime};
use anyhow::{Context as _, Result, bail};
use deltachat::chat::{self, ChatVisibility, get_chat_contacts, get_past_chat_contacts};
use anyhow::{bail, Context as _, Result};
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};

View File

@@ -4,7 +4,7 @@ use deltachat::chatlist::get_last_message_for_chat;
use deltachat::constants::*;
use deltachat::contact::Contact;
use deltachat::{
chat::{ChatVisibility, get_chat_contacts},
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
};
use num_traits::cast::ToPrimitive;

View File

@@ -17,6 +17,7 @@ pub struct ContactObject {
id: u32,
name: String,
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
/// Is the contact a key contact.
@@ -30,6 +31,37 @@ pub struct ContactObject {
/// e.g. if we just scanned the fingerprint from a QR code.
e2ee_avail: bool,
/// True if the contact
/// can be added to protected chats
/// because SELF and contact have verified their fingerprints in both directions.
///
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
is_verified: bool,
/// The contact ID that verified a contact.
///
/// As verifier may be unknown,
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
///
/// UI should display the information in the contact's profile as follows:
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name of the contact.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,
/// display "Introduced" prefixed by a green checkmark.
///
/// - if `verifierId` == 0 and `isVerified` == 0,
/// display nothing
///
/// This contains the contact ID of the verifier.
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
/// If it is None/Null, we don't have verifier information or
/// the contact is not verified.
verifier_id: Option<u32>,
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: bool,
@@ -47,6 +79,14 @@ impl ContactObject {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let is_verified = contact.is_verified(context).await?;
let verifier_id = contact
.get_verifier_id(context)
.await?
.flatten()
.map(|contact_id| contact_id.to_u32());
Ok(ContactObject {
address: contact.get_addr().to_owned(),
color: color_int_to_hex_string(contact.get_color()),
@@ -56,9 +96,12 @@ impl ContactObject {
id: contact.id.to_u32(),
name: contact.get_name().to_owned(),
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_key_contact: contact.is_key_contact(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
is_bot: contact.is_bot(),

View File

@@ -203,17 +203,6 @@ pub enum EventType {
msg_id: u32,
},
/// Like [`EventType::MsgRead`], but also fires on subsequent MDNs,
/// if there are multiple receivers, i.e. in groups and channels.
#[serde(rename_all = "camelCase")]
MsgReadCountChanged {
/// ID of the chat which the message belongs to.
chat_id: u32,
/// ID of the message that was read.
msg_id: u32,
},
/// A single message was deleted.
///
/// This event means that the message will no longer appear in the messagelist.
@@ -337,7 +326,8 @@ pub enum EventType {
contact_id: u32,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
/// 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,
},
@@ -393,15 +383,11 @@ pub enum EventType {
msg_id: u32,
},
/// Tells that a background fetch call is done:
/// the fetch completed, timed out, was stopped or was not started.
/// Tells that the Background fetch was completed (or timed out).
/// This event acts as a marker, when you reach this event you can be sure
/// that all events emitted during the background fetch were processed.
///
/// For the call that started the fetch, this event acts as a marker:
/// all events emitted during the fetch were processed once it is reached.
/// A call made while another background fetch is running gets the event immediately,
/// and the running fetch keeps emitting events until its own marker.
///
/// This event is only emitted by the account manager.
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
/// Inform that set of chats or the order of the chats in the chatlist has changed.
///
@@ -481,9 +467,9 @@ pub enum EventType {
///
/// UI should update the list.
///
/// The event is emitted on the device modifying
/// the transports as well as on other devices
/// applying the synced change.
/// This event is emitted when transport
/// synchronization messages arrives,
/// but not when the UI modifies the transport list by itself.
TransportsModified,
}
@@ -560,10 +546,6 @@ impl From<CoreEventType> for EventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::MsgReadCountChanged { chat_id, msg_id } => MsgReadCountChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),

View File

@@ -16,7 +16,7 @@ pub struct HttpResponse {
impl From<CoreHttpResponse> for HttpResponse {
fn from(response: CoreHttpResponse) -> Self {
use base64::{Engine as _, engine::general_purpose};
use base64::{engine::general_purpose, Engine as _};
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
let mimetype = response.mimetype;
let encoding = response.encoding;

View File

@@ -4,6 +4,16 @@ use serde::Deserialize;
use serde::Serialize;
use yerpc::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TransportListEntry {
/// The login data entered by the user.
pub param: EnteredLoginParam,
/// Whether this transport is set to 'unpublished'.
/// See `set_transport_unpublished` / `setTransportUnpublished` for details.
pub is_unpublished: bool,
}
/// Login parameters entered by the user.
///
/// Usually it will be enough to only set `addr` and `password`,
@@ -56,6 +66,19 @@ pub struct EnteredLoginParam {
/// invalid hostnames.
/// Default: Automatic
pub certificate_checks: Option<EnteredCertificateChecks>,
/// If true, login via OAUTH2 (not recommended anymore).
/// Default: false
pub oauth2: Option<bool>,
}
impl From<dc::TransportListEntry> for TransportListEntry {
fn from(transport: dc::TransportListEntry) -> Self {
TransportListEntry {
param: transport.param.into(),
is_unpublished: transport.is_unpublished,
}
}
}
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
@@ -77,6 +100,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
smtp_user: param.smtp.user.into_option(),
smtp_password: param.smtp.password.into_option(),
certificate_checks: certificate_checks.into_option(),
oauth2: param.oauth2.into_option(),
}
}
}
@@ -103,7 +127,7 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
password: param.smtp_password.unwrap_or_default(),
},
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
oauth2: false,
oauth2: param.oauth2.unwrap_or_default(),
})
}
}
@@ -150,8 +174,9 @@ impl From<Socket> for dc::Socket {
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum EnteredCertificateChecks {
/// `Automatic` means strict certificate checks,
/// unless a legacy-domain override disables them.
/// `Automatic` means that provider database setting should be taken.
/// If there is no provider database setting for certificate checks,
/// check certificates strictly.
#[default]
Automatic,

View File

@@ -103,9 +103,6 @@ pub struct MessageObject {
saved_message_id: Option<u32>,
is_pinned: bool,
/// `None` when there are no reactions.
reactions: Option<JsonrpcReactions>,
vcard_contact: Option<VcardContact>,
@@ -266,7 +263,6 @@ impl MessageObject {
.await?
.map(|id| id.to_u32()),
is_pinned: message.is_pinned(),
reactions,
vcard_contact: vcard_contacts.first().cloned(),
@@ -394,11 +390,11 @@ pub enum SystemMessageType {
LocationOnly,
InvalidUnencryptedMail,
/// Single chats info message telling that SecureJoin has started and the user should wait for it
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
/// to complete.
SecurejoinWait,
/// Single chats info message telling that SecureJoin is still running, but the user may already
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
/// send messages.
SecurejoinWaitTimeout,
@@ -429,8 +425,6 @@ pub enum SystemMessageType {
CallAccepted,
CallEnded,
MessagePinned,
MessageUnpinned,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -460,8 +454,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
SystemMessage::CallEnded => SystemMessageType::CallEnded,
SystemMessage::MessagePinned => SystemMessageType::MessagePinned,
SystemMessage::MessageUnpinned => SystemMessageType::MessageUnpinned,
}
}
}
@@ -734,9 +726,9 @@ impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
fn from(value: deltachat::ephemeral::Timer) -> Self {
match value {
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
deltachat::ephemeral::Timer::Enabled { duration } => EphemeralTimer::Enabled {
duration: duration.get(),
},
deltachat::ephemeral::Timer::Enabled { duration } => {
EphemeralTimer::Enabled { duration }
}
}
}
}

View File

@@ -1,5 +1,4 @@
pub mod account;
pub mod appversions;
pub mod calls;
pub mod chat;
pub mod chat_list;
@@ -9,6 +8,8 @@ pub mod http;
pub mod location;
pub mod login_param;
pub mod message;
pub mod notify_state;
pub mod provider_info;
pub mod qr;
pub mod reactions;
pub mod webxdc;

View File

@@ -0,0 +1,26 @@
use deltachat::push::NotifyState;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "NotifyState")]
pub enum JsonrpcNotifyState {
/// Not subscribed to push notifications.
NotConnected,
/// Subscribed to heartbeat push notifications.
Heartbeat,
/// Subscribed to push notifications for new messages.
Connected,
}
impl From<NotifyState> for JsonrpcNotifyState {
fn from(state: NotifyState) -> Self {
match state {
NotifyState::NotConnected => Self::NotConnected,
NotifyState::Heartbeat => Self::Heartbeat,
NotifyState::Connected => Self::Connected,
}
}
}

View File

@@ -0,0 +1,25 @@
use deltachat::provider::Provider;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
/// Unique ID, corresponding to provider database filename.
pub id: String,
pub before_login_hint: String,
pub overview_page: String,
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
}
impl ProviderInfo {
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
provider.map(|p| ProviderInfo {
id: p.id.to_owned(),
before_login_hint: p.before_login_hint.to_owned(),
overview_page: p.overview_page.to_owned(),
status: p.status.to_u32().unwrap(),
})
}
}

View File

@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "kind")]
pub enum QrObject {
/// Ask the user whether to start chatting with the contact.
/// Ask the user whether to verify the contact.
///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact {
@@ -61,7 +61,7 @@ pub enum QrObject {
/// Whether the inviter supports the new Securejoin v3 protocol
is_v3: bool,
},
/// Contact fingerprint matches.
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk {
@@ -236,7 +236,6 @@ impl From<Qr> for QrObject {
invitenumber,
authcode,
is_v3,
..
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable();
@@ -256,7 +255,6 @@ impl From<Qr> for QrObject {
invitenumber,
authcode,
is_v3,
..
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable();
@@ -278,7 +276,6 @@ impl From<Qr> for QrObject {
authcode,
invitenumber,
is_v3,
..
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable();

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use deltachat::contact::ContactId;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -23,11 +24,8 @@ pub struct JsonrpcReaction {
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JsonrpcReactions {
/// Map from a contact to it's reaction to message.
///
/// There is only a single reaction per contact,
/// but this contains a list of reactions for historical reasons.
///
/// For channels subscribers, this map is empty or contains `ContactId::SELF` only.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count, sorted in descending order.
reactions: Vec<JsonrpcReaction>,
@@ -36,24 +34,30 @@ pub struct JsonrpcReactions {
impl From<Reactions> for JsonrpcReactions {
fn from(reactions: Reactions) -> Self {
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
.by_contact
.iter()
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
.collect();
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
let reactions = reactions
.frequencies
.into_iter()
.map(|entry| JsonrpcReaction {
emoji: entry.reaction.as_str().to_string(),
count: entry.count,
is_from_self: entry.is_from_self,
})
.collect();
let mut reactions_v = Vec::new();
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
let is_from_self = if let Some(self_reaction) = self_reaction {
self_reaction.contains(&emoji)
} else {
false
};
let reaction = JsonrpcReaction {
emoji,
count,
is_from_self,
};
reactions_v.push(reaction)
}
JsonrpcReactions {
reactions_by_contact,
reactions,
reactions: reactions_v,
}
}
}

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.61.0-dev"
"version": "2.51.0-dev"
}

View File

@@ -44,6 +44,7 @@ const constants = data
key.startsWith("DC_CERTCK_") ||
key.startsWith("DC_SOCKET_") ||
key.startsWith("DC_LP_AUTH_") ||
key.startsWith("DC_PUSH_") ||
key.startsWith("DC_TEXT1_") ||
key.startsWith("DC_CHAT_TYPE")
);

View File

@@ -148,6 +148,23 @@ describe("online tests", function () {
expect(message2.text).equal("super secret message");
expect(message2.showPadlock).equal(true);
});
it("get provider info for example.com", async () => {
const acc = await dc.rpc.addAccount();
const info = await dc.rpc.getProviderInfo(acc, "example.com");
expect(info).to.be.not.null;
expect(info?.overviewPage).to.equal(
"https://providers.delta.chat/example-com",
);
expect(info?.status).to.equal(3);
});
it("get provider info - domain and email should give same result", async () => {
const acc = await dc.rpc.addAccount();
const info_domain = await dc.rpc.getProviderInfo(acc, "example.com");
const info_email = await dc.rpc.getProviderInfo(acc, "hi@example.com");
expect(info_email).to.deep.equal(info_domain);
});
});
async function waitForEvent<T extends DcEvent["kind"]>(

View File

@@ -2,7 +2,7 @@
name = "ratelimit"
version = "1.0.0"
description = "Token bucket implementation"
edition = "2024"
edition = "2021"
license = "MPL-2.0"
[dependencies]

View File

@@ -1,8 +1,8 @@
[package]
name = "deltachat-repl"
version = "2.61.0-dev"
version = "2.51.0-dev"
license = "MPL-2.0"
edition = "2024"
edition = "2021"
repository = "https://github.com/chatmail/core"
[dependencies]

View File

@@ -5,10 +5,9 @@ use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Result, bail, ensure};
use anyhow::{bail, ensure, Result};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
@@ -25,6 +24,7 @@ use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
use deltachat::{config, provider};
use tokio::fs;
/// Reset database tables.
@@ -228,7 +228,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id == MsgId::DAYMARKER {
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
println!(
"--------------------------------------------------------------------------------"
);
@@ -259,13 +259,19 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
let verified_str = if contact.is_verified(context).await? {
""
} else {
""
};
let line = format!(
"{} <{}>",
"{}{} <{}>",
if !name.is_empty() {
name
} else {
"<name unset>"
},
verified_str,
if !addr.is_empty() { addr } else { "addr unset" }
);
@@ -313,6 +319,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
info\n\
set <configuration-key> [<value>]\n\
get <configuration-key>\n\
oauth2\n\
configure\n\
connect\n\
disconnect\n\
@@ -389,6 +396,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
joinqr <qr-content>\n\
setqr <qr-content>\n\
createqrsvg <qr-content>\n\
providerinfo <addr>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
clear -- clear screen\n\
@@ -610,7 +618,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let sel_chat = sel_chat.as_ref().unwrap();
let time_start = std::time::SystemTime::now();
let msglist = chat::get_chat_msgs_ext(
let msglist = chat::get_chat_msgs_ex(
&context,
sel_chat.get_id(),
chat::MessageListOptions {
@@ -624,7 +632,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.into_iter()
.map(|x| match x {
ChatItem::Message { msg_id } => msg_id,
ChatItem::DayMarker { .. } => MsgId::DAYMARKER,
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
})
.collect();
@@ -1113,11 +1121,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let contact_id = ContactId::new(arg1.parse()?);
let contact = Contact::get_by_id(&context, contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
let name_n_addr = contact.get_name_n_addr();
let mut res = format!(
"Contact info for: {name} ({addr}):\nIcon: {}\n",
"Contact info for: {}:\nIcon: {}\n",
name_n_addr,
match contact.get_profile_image(&context).await? {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
@@ -1197,14 +1205,33 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
fs::write(&file, svg).await?;
println!("{file:#?} written.");
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {arg1} found.");
}
}
}
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
let Ok(buf) = read_file(&context, Path::new(arg1)).await else {
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
let (width, height) = get_filemeta(&buf)?;
println!("width={width}, height={height}");
} else {
bail!("Command failed.");
};
let (width, height) = get_filemeta(&buf)?;
println!("width={width}, height={height}");
}
}
"estimatedeletion" => {
ensure!(!arg1.is_empty(), "Argument <seconds> missing");

View File

@@ -9,12 +9,14 @@ extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use anyhow::{Error, bail};
use deltachat::EventType;
use anyhow::{bail, Error};
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::EventType;
use log::{error, info, warn};
use nu_ansi_term::Color;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
@@ -160,10 +162,11 @@ const IMEX_COMMANDS: [&str; 10] = [
"stop",
];
const DB_COMMANDS: [&str; 10] = [
const DB_COMMANDS: [&str; 11] = [
"info",
"set",
"get",
"oauth2",
"configure",
"connect",
"disconnect",
@@ -173,7 +176,7 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 38] = [
const CHAT_COMMANDS: [&str; 39] = [
"listchats",
"listarchived",
"start-realtime",
@@ -182,6 +185,7 @@ const CHAT_COMMANDS: [&str; 38] = [
"createchat",
"creategroup",
"createbroadcast",
"createprotected",
"addmember",
"removemember",
"groupname",
@@ -236,7 +240,7 @@ const CONTACT_COMMANDS: [&str; 9] = [
"import-vcard",
"make-vcard",
];
const MISC_COMMANDS: [&str; 13] = [
const MISC_COMMANDS: [&str; 14] = [
"getqr",
"getqrsvg",
"getbadqr",
@@ -244,6 +248,7 @@ const MISC_COMMANDS: [&str; 13] = [
"joinqr",
"setqr",
"createqrsvg",
"providerinfo",
"fileinfo",
"estimatedeletion",
"clear",
@@ -265,11 +270,10 @@ impl Hinter for DcHelper {
&CONTACT_COMMANDS[..],
&MISC_COMMANDS[..],
] {
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos]))
&& *entry != line
&& *entry != &line[..pos]
{
return Some(entry[pos..].to_owned());
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos])) {
if *entry != line && *entry != &line[..pos] {
return Some(entry[pos..].to_owned());
}
}
}
}
@@ -421,6 +425,19 @@ async fn handle_cmd(
"configure" => {
ctx.configure().await?;
}
"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 {
println!("OAuth2 not available for {addr}.");
}
} else {
println!("oauth2: set addr first.");
}
}
"clear" => {
println!("\n\n\n");
print!("\x1b[1;1H\x1b[2J");

View File

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

View File

@@ -157,7 +157,7 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
"""
# You removed member a@b.
# You added member a@b.
# You were removed by a@b.
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
@@ -36,15 +35,6 @@ class Account:
if event_type is None or next_event.kind == event_type:
return next_event
def wait_for_realtime_data(self, msg_id: int) -> bytes:
"""Wait for the next realtime data received for the given webxdc message and return it."""
logging.info(f"account {self.id}: waiting for realtime data for msg {msg_id}")
while True:
event = self.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
if event.msg_id == msg_id:
logging.info(f"account {self.id}: got realtime data for msg {msg_id}: {event.data[:20]}")
return bytes(event.data)
def clear_all_events(self):
"""Remove all queued-up events for a given account.
@@ -150,10 +140,9 @@ class Account:
return transports
def bring_online(self):
"""Start I/O, wait until all transports became IDLE and drop the events seen so far."""
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
self._rpc.wait_for_all_work_done(self.id)
self.clear_all_events()
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
@@ -193,7 +182,7 @@ class Account:
return [Contact(self, contact_id) for contact_id in contact_ids]
def create_chat(self, account: "Account") -> Chat:
"""Create a single chat with another account."""
"""Create a 1:1 chat with another account."""
return self.create_contact(account).create_chat()
def get_device_chat(self) -> Chat:
@@ -229,7 +218,7 @@ class Account:
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
"""Return single chat for a contact if it exists."""
"""Return 1:1 chat for a contact if it exists."""
if isinstance(contact, Contact):
assert contact.account == self
contact_id = contact.id
@@ -272,7 +261,7 @@ class Account:
return Contact(self, SpecialContactId.SELF)
@property
def device_contact(self) -> Contact:
def device_contact(self) -> Chat:
"""Account's device contact."""
return Contact(self, SpecialContactId.DEVICE)
@@ -360,7 +349,7 @@ class Account:
return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat:
"""Continue the SecureJoin protocol started on another device.
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
The function returns immediately and the handshake runs in background, sending
and receiving several messages.

View File

@@ -252,7 +252,7 @@ class Chat:
def get_contacts(self) -> list[Contact]:
"""Get the contacts belonging to this chat.
For single chats self-address is not included.
For single/direct chats self-address is not included.
"""
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]

View File

@@ -54,7 +54,6 @@ class EventType(str, Enum):
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead"
MSG_READ_COUNT_CHANGED = "MsgReadCountChanged"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
CHAT_DELETED = "ChatDeleted"
@@ -70,7 +69,6 @@ class EventType(str, Enum):
SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
ACCOUNTS_BACKGROUND_FETCH_DONE = "AccountsBackgroundFetchDone"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
ACCOUNTS_CHANGED = "AccountsChanged"
@@ -98,7 +96,7 @@ class ChatType(str, Enum):
"""Chat type."""
SINGLE = "Single"
"""Single chat (a chat with a with a single contact)"""
"""1:1 chat, i.e. a direct chat with a single contact"""
GROUP = "Group"
@@ -232,6 +230,14 @@ class KeyGenType(IntEnum):
RSA4096 = 3
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
"""Authorization flags."""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
"""Media quality setting."""
@@ -247,6 +253,14 @@ class ProviderStatus(IntEnum):
BROKEN = 3
class PushNotifyState(IntEnum):
"""Push notifications state."""
NOT_CONNECTED = 0
HEARTBEAT = 1
CONNECTED = 2
class ShowEmails(IntEnum):
"""Show emails mode."""

View File

@@ -55,7 +55,7 @@ class Contact:
return snapshot
def create_chat(self) -> "Chat":
"""Create or get an existing single chat for this contact."""
"""Create or get an existing 1:1 chat for this contact."""
from .chat import Chat
return Chat(

View File

@@ -48,13 +48,6 @@ class DeltaChat:
"""Stop ongoing background fetch."""
self.rpc.stop_background_fetch()
def wait_for_event(self, event_type=None) -> AttrDict:
"""Wait until the next account manager event and return it."""
while True:
next_event = AttrDict(self.rpc.wait_for_event(0))
if event_type is None or next_event.kind == event_type:
return next_event
def maybe_network(self) -> None:
"""Indicate that the network conditions might have changed."""
self.rpc.maybe_network()
@@ -74,7 +67,3 @@ class DeltaChat:
def stop_sending_locations(self) -> None:
"""Stop sending locations to all chats."""
return self.rpc.stop_sending_locations()
def is_sending_finished(self) -> bool:
"""Return true if sending queues of all accounts are empty."""
return self.rpc.is_sending_finished()

View File

@@ -121,7 +121,7 @@ class Message:
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
def send_webxdc_realtime_data(self, data):
def send_webxdc_realtime_data(self, data) -> None:
"""Send data to the realtime channel."""
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))

View File

@@ -7,13 +7,12 @@ import os
import pathlib
import platform
import random
import socket
import subprocess
import sys
import time
import urllib.parse
from typing import Iterator, Optional
from typing import AsyncGenerator, Optional
import execnet
import py
import pytest
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
@@ -27,42 +26,19 @@ Currently this is "Messages are end-to-end encrypted."
"""
def pytest_configure(config):
# Run only in the xdist controller, before the workers exist.
if not hasattr(config, "workerinput"):
domain = os.environ.get("CHATMAIL_DOMAIN")
if domain:
check_chatmail_domain_and_warmup_dns_cache(domain)
def check_chatmail_domain_and_warmup_dns_cache(domain):
for i in range(6):
try:
socket.getaddrinfo(domain, 443)
return
except socket.gaierror as e:
error = e
logging.warning(f"DNS resolution of {domain} failed (attempt {i}): {e}")
time.sleep(10)
pytest.exit(f"cannot resolve chatmail relay domain {domain}: {error}")
def pytest_report_header():
headers = [f"CHATMAIL_DOMAIN: {os.environ.get('CHATMAIL_DOMAIN')}"]
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()
headers.append(f"RPC-SERVER: {fn} [{version}]")
break
return f"deltachat-rpc-server: {fn} [{version}]"
return headers
return None
class RPCAccountFactory:
class ACFactory:
"""Test account factory."""
def __init__(self, deltachat: DeltaChat) -> None:
@@ -76,7 +52,7 @@ class RPCAccountFactory:
"""Create a new unconfigured bot."""
return Bot(self.get_unconfigured_account())
def get_credentials(self) -> tuple[str, str]:
def get_credentials(self) -> (str, str):
"""Generate new credentials for chatmail account."""
domain = os.environ["CHATMAIL_DOMAIN"]
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
@@ -128,7 +104,7 @@ class RPCAccountFactory:
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
"""Create a new single chat between ac1 and ac2 accepted on both sides.
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
Returned chat is a chat with ac2 from ac1 point of view.
"""
@@ -175,7 +151,7 @@ class RPCAccountFactory:
@pytest.fixture
def rpc(tmp_path) -> Iterator[Rpc]:
def rpc(tmp_path) -> AsyncGenerator:
"""RPC client fixture."""
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
@@ -189,20 +165,20 @@ def dc(rpc) -> DeltaChat:
@pytest.fixture
def acf(dc) -> RPCAccountFactory:
def acfactory(dc) -> AsyncGenerator:
"""Return account factory fixture."""
return RPCAccountFactory(dc)
return ACFactory(dc)
@pytest.fixture
def rpcdata():
def data():
"""Test data."""
class Data:
def __init__(self) -> None:
for path in pathlib.Path(__file__).parents:
datadir = path / "test-data"
if datadir.is_dir():
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join("test-data")
if datadir.isdir():
self.path = datadir
return
raise Exception("Data path cannot be found")
@@ -292,33 +268,26 @@ def get_core_python_env(tmp_path_factory):
@pytest.fixture
def alice_and_remote_bob(tmp_path, acf, get_core_python_env):
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`.
"""
from execnet import makegateway
def factory(core_version):
python, rpc_server_path = get_core_python_env(core_version)
gw = makegateway(f"popen//python={python}")
gw = execnet.makegateway(f"popen//python={python}")
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
channel = gw.remote_exec(remote_bob_loop)
# old cores need "ic=3" to accept
# the self-signed cert of an underscore domain
addr, password = acf.get_credentials()
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
if os.environ["CHATMAIL_DOMAIN"].startswith("_"):
dclogin_qr += "&ic=3"
cm = os.environ.get("CHATMAIL_DOMAIN")
# trigger getting an online account on bob's side
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
channel.send((accounts_dir, str(rpc_server_path), cm))
# meanwhile get a local alice account
alice = acf.get_online_account()
alice = acfactory.get_online_account()
channel.send(alice.self_contact.make_vcard())
# wait for bob to have started
@@ -347,8 +316,10 @@ def remote_bob_loop(channel):
import os
from deltachat_rpc_client import DeltaChat, Rpc
from deltachat_rpc_client.pytestplugin import ACFactory
accounts_dir, rpc_server_path, dclogin_qr = channel.receive()
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
@@ -359,13 +330,8 @@ def remote_bob_loop(channel):
with rpc:
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
# RPCAccountFactory would configure from a "dcaccount" QR,
# which old cores cannot use on underscore domains
bob = dc.add_account()
bob.add_transport_from_qr(dclogin_qr)
bob.bring_online()
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}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import contextlib
import itertools
import json
import logging
@@ -39,15 +38,8 @@ class RpcMethod:
"params": args,
"id": request_id,
}
queue: Queue = Queue()
# Register before testing for shutdown, so that either the reader loop
# finds this request while draining, or the test below catches it here.
# Testing first would race with the reader loop finishing in between.
self.rpc.request_results[request_id] = queue
if self.rpc.request_queue_closed:
self.rpc._fail_request(request_id)
else:
self.rpc.request_queue.put(request)
self.rpc.request_results[request_id] = queue = Queue()
self.rpc.request_queue.put(request)
def rpc_future():
"""Wait for the request to receive a result."""
@@ -86,10 +78,6 @@ class Rpc:
# Map from request ID to a Queue which provides a single result
self.request_results: dict[int, Queue]
self.request_queue: Queue[Any]
# Emulates `request_queue.shutdown(immediate=False)`, which needs Python 3.13:
# https://github.com/python/cpython/blob/v3.13.0/Lib/queue.py#L236-L257
# Note that `request_queue_closed` is set by the reader loop.
self.request_queue_closed: bool
self.closing: bool
self.reader_thread: Thread
self.writer_thread: Thread
@@ -119,7 +107,6 @@ class Rpc:
self.event_queues = {}
self.request_results = {}
self.request_queue = Queue()
self.request_queue_closed = False
self.closing = False
self.reader_thread = Thread(target=self.reader_loop)
self.reader_thread.start()
@@ -136,8 +123,6 @@ class Rpc:
# The reader_loop already saw EOF on stdout, so the process
# has exited and stderr is available.
stderr = self.process.stderr.read().decode(errors="replace").strip()
self.closing = True
self._shutdown_loops()
if stderr:
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
@@ -150,31 +135,11 @@ class Rpc:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
self.stop_io_for_all_accounts()
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
# otherwise it might exit through an "RPC server closed" error instead.
self.events_thread.join()
self._shutdown_loops()
def _shutdown_loops(self) -> None:
"""Close the server pipe and wait for the loop threads to finish.
The writer blocks on an empty request queue,
so it needs the sentinel to notice the shutdown.
"""
with contextlib.suppress(BrokenPipeError):
# An exited server may leave data unflushed,
# which close() would try to write out again.
self.process.stdin.close()
self.request_queue.put(None)
self.process.stdin.close()
self.reader_thread.join()
self.request_queue.put(None)
self.writer_thread.join()
self.events_thread.join()
def _fail_request(self, request_id: int) -> None:
"""Answer a registered request with an error, unless it was answered already."""
queue = self.request_results.pop(request_id, None)
if queue is not None:
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
def __enter__(self):
self.start()
@@ -197,11 +162,9 @@ class Rpc:
# Log an exception if the reader loop dies.
logging.exception("Exception in the reader loop")
finally:
# Shut the request queue first, so that requests registered from now
# on are failed by their caller, then answer the pending ones here.
self.request_queue_closed = True
for request_id in list(self.request_results):
self._fail_request(request_id)
# Unblock any pending requests when the server closes stdout.
for _request_id, queue in self.request_results.items():
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
def writer_loop(self) -> None:
"""Writer loop ensuring only a single thread writes requests."""

View File

@@ -22,10 +22,8 @@ ALL = "1:*"
class DirectImap:
"""Internal Python-level IMAP handling."""
def __init__(self, account: Account, addr=None, password=None) -> None:
def __init__(self, account: Account) -> None:
self.account = account
self.addr = addr or account.get_config("addr")
self.password = password or account.get_config("mail_pw")
self.logid = account.get_config("displayname") or id(account)
self._idling = False
self.connect()
@@ -35,15 +33,11 @@ class DirectImap:
host = self.account.get_config("configured_mail_server")
port = 993
user = self.addr
user = self.account.get_config("addr")
host = user.rsplit("@")[-1]
pw = self.password
pw = self.account.get_config("mail_pw")
ssl_context = ssl.create_default_context()
if host.startswith("_"):
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
self.conn.login(user, pw)
self.select_folder("INBOX")
@@ -171,7 +165,7 @@ class DirectImap:
self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)), mark_seen=False)]
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]
@@ -180,6 +174,9 @@ class DirectImap:
class IdleManager:
def __init__(self, direct_imap) -> None:
self.direct_imap = direct_imap
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> list[bytes]:

View File

@@ -5,16 +5,16 @@ from typing import TYPE_CHECKING
from deltachat_rpc_client import EventType
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
from deltachat_rpc_client.pytestplugin import ACFactory
def test_event_on_configuration(acf: RPCAccountFactory) -> None:
def test_event_on_configuration(acfactory: ACFactory) -> None:
"""
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
"""
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account.add_or_update_transport.future({"addr": addr, "password": password})

View File

@@ -1,8 +1,8 @@
from deltachat_rpc_client import EventType, Message
def test_calls(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_calls(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
place_call_info = "offer"
accept_call_info = "answer"
@@ -35,14 +35,14 @@ def test_calls(acf) -> None:
assert incoming_call_message.get_call_info().state.kind == "Completed"
def test_video_call(acf) -> None:
def test_video_call(acfactory) -> None:
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
# with `s= ` replaced with `s=-`.
#
# `s=` cannot be empty according to RFC 3264,
# so it is more clear as `s=-`.
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -57,8 +57,8 @@ def test_video_call(acf) -> None:
assert incoming_call_message.get_call_info().has_video
def test_audio_call(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_audio_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -73,15 +73,15 @@ def test_audio_call(acf) -> None:
assert not incoming_call_message.get_call_info().has_video
def test_ice_servers(acf) -> None:
alice = acf.get_online_account()
def test_ice_servers(acfactory) -> None:
alice = acfactory.get_online_account()
ice_servers = alice.ice_servers()
assert len(ice_servers) == 1
def test_no_contact_request_call(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_no_contact_request_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
@@ -101,8 +101,8 @@ def test_no_contact_request_call(acf) -> None:
break
def test_who_can_call_me_nobody(acf) -> None:
alice, bob = acf.get_online_accounts(2)
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")
@@ -128,9 +128,9 @@ def test_who_can_call_me_nobody(acf) -> None:
break
def test_who_can_call_me_everybody(acf) -> None:
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 = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (0)
bob.set_config("who_can_call_me", "0")

View File

@@ -2,10 +2,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from deltachat_rpc_client import EventType, const
from deltachat_rpc_client import Account, EventType, const
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
from deltachat_rpc_client.pytestplugin import ACFactory
def wait_for_chatlist_and_specific_item(account, chat_id):
@@ -40,11 +40,11 @@ def wait_for_chatlist(account):
break
def test_delivery_status(acf: RPCAccountFactory) -> None:
def test_delivery_status(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes (delivered, read)
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -82,11 +82,11 @@ def test_delivery_status(acf: RPCAccountFactory) -> None:
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
def test_delivery_status_failed(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes failed
"""
(alice,) = acf.get_online_accounts(1)
(alice,) = acfactory.get_online_accounts(1)
alice.set_config("force_encryption", "0")
invalid_contact = alice.create_contact("example@example.com", "invalid address")
@@ -110,12 +110,12 @@ def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
def test_download_on_demand(acfactory: ACFactory) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -128,7 +128,7 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
file=rpcdata.get_path("image/screenshot.jpg"),
file="../test-data/image/screenshot.jpg",
)
message = alice.wait_for_incoming_msg()
@@ -144,8 +144,8 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
wait_for_chatlist_specific_item(alice, chat_id)
def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
alice, bob = acf.get_online_accounts(2)
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -161,12 +161,12 @@ def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
return [alice, alice_second_device, bob, alice_chat_bob]
def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> None:
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
"""
Test that chatlist changed events are emitted for the second device
when the message is marked as read on the first device
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
bob.create_chat(alice)
@@ -191,11 +191,11 @@ def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> None:
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
def test_multidevice_sync_chat(acf: RPCAccountFactory) -> None:
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
"""
Test multidevice sync: syncing chat visibility and muting across multiple devices
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.archive()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)

View File

@@ -1,5 +1,4 @@
import subprocess
import time
import pytest
@@ -17,7 +16,7 @@ def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
def test_qr_setup_contact(acfactory, 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)
@@ -25,48 +24,28 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
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.e2ee_avail
assert alice_contact_bob_snapshot.is_verified
remote_eval("bob.wait_for_securejoin_joiner_success()")
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
# Test that Bob can also scan a QR code
# of Alice for which the key is not known yet.
# For the test above Bob already knew the key from a vCard.
alice2 = acf.get_online_account()
alice2 = acfactory.get_online_account()
qr_code = alice2.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
remote_eval("bob.wait_for_securejoin_joiner_success()")
alice2.wait_for_securejoin_inviter_success()
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core, with multiple transports."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
relay_qr = acf.get_account_qr()
alice.add_transport_from_qr(relay_qr)
alice.add_transport_from_qr(relay_qr)
qr_code = alice.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success()
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.e2ee_avail
remote_eval("bob.wait_for_securejoin_joiner_success()")
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
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.23.0")
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
@@ -74,149 +53,14 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
assert msg.get_snapshot().text == "hello"
def test_second_device(acf, alice_and_remote_bob) -> None:
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.23.0")
_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 = acf.get_unconfigured_account()
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')")
@pytest.mark.parametrize("replace_relay", [False, True], ids=["add", "replace"])
def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, replace_relay):
"""Test 2.48 Bob learns a relay change of Alice from a keyupdate, and is shown nothing."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.48.0")
def bob_sees():
return remote_eval(
"{'chats': len(bob.get_chatlist()),"
" 'fresh': len(bob._rpc.get_fresh_msgs(bob.id)),"
" 'contacts': len(bob.get_contacts()),"
" 'alice_chat': bob._rpc.get_chat_id_by_contact_id(bob.id, bob_contact_alice.id) or 0}",
)
# Keyupdates go to contacts who plausibly hold our key:
# an accepted chat alone is not enough, a message must have flowed.
alice_chat = alice_contact_bob.create_chat()
alice.set_config("keyupdate_debounce", "1")
old_addr = alice.get_config("configured_addr")
alice_chat.send_text("hi")
assert remote_eval("bob.wait_for_incoming_msg().get_snapshot().text") == "hi"
before = bob_sees()
# Certificate merging keeps the newest direct key signature,
# and signature timestamps have one-second resolution:
# without waiting, the re-signed key can tie with the copy Bob holds, keeping his.
time.sleep(2)
alice.add_transport_from_qr(acf.get_account_qr())
(new_addr,) = [t["addr"] for t in alice.list_transports() if t["addr"] != old_addr]
if replace_relay:
alice.delete_transport(old_addr)
alice.bring_online()
# The 2.48 core has no encryption enforcement, but the keyupdate MDN without
# referenced message keeps it invisible; merging happens before the trashing.
for _ in range(60):
if new_addr in remote_eval("bob_contact_alice.get_encryption_info()"):
break
time.sleep(1)
else:
pytest.fail("Bob never received the keyupdate")
# It also leaves no trace: no chat with Alice, no message anywhere,
# and no address-contact for the address it was sent from.
assert bob_sees() == before
if replace_relay:
remote_eval("bob_contact_alice.create_chat().send_text('hello after replacement')")
assert alice.wait_for_incoming_msg().get_snapshot().text == "hello after replacement"
class LocalSide:
"""Alice on the core under test."""
def __init__(self, account, peer_contact):
self.account = account
self.peer_contact = peer_contact
self.chat = None
def make_qr(self, invite):
if invite == "group":
self.chat = self.account.create_group("Group")
elif invite == "broadcast":
self.chat = self.account.create_broadcast("Channel")
return self.chat.get_qr_code() if self.chat else self.account.get_qr_code()
def join(self, qr):
self.account.secure_join(qr)
def wait_inviter(self):
self.account.wait_for_securejoin_inviter_success()
def wait_joiner(self):
self.account.wait_for_securejoin_joiner_success()
def send_text(self, text):
chat = self.chat or self.peer_contact.create_chat()
chat.send_text(text)
def next_text(self):
return self.account.wait_for_incoming_msg().get_snapshot().text
class RemoteSide:
"""Bob on the other core, driven through remote_eval."""
def __init__(self, remote_eval):
self.remote_eval = remote_eval
self.chat = None
def make_qr(self, invite):
if invite == "contact":
return self.remote_eval("bob.get_qr_code()")
create = {"group": "bob.create_group('Group')", "broadcast": "bob.create_broadcast('Channel')"}[invite]
self.remote_eval(f"locals().update(chat={create})")
self.chat = "chat"
return self.remote_eval("chat.get_qr_code()")
def join(self, qr):
self.remote_eval(f"bob.secure_join({qr!r})")
def wait_inviter(self):
self.remote_eval("bob.wait_for_securejoin_inviter_success()")
def wait_joiner(self):
self.remote_eval("bob.wait_for_securejoin_joiner_success()")
def send_text(self, text):
chat = self.chat or "bob_contact_alice.create_chat()"
self.remote_eval(f"{chat}.send_text({text!r})")
def next_text(self):
return self.remote_eval("bob.wait_for_incoming_msg().get_snapshot().text")
@pytest.mark.parametrize("version", ["2.48.0"])
@pytest.mark.parametrize("invite", ["contact", "group", "broadcast"])
@pytest.mark.parametrize("remote_invites", [False, True], ids=["local-invites", "remote-invites"])
def test_securejoin_invite(alice_and_remote_bob, version, invite, remote_invites):
"""Every invite link type works with either core as the inviter."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
local = LocalSide(alice, alice_contact_bob)
remote = RemoteSide(remote_eval)
inviter, joiner = (remote, local) if remote_invites else (local, remote)
joiner.join(inviter.make_qr(invite))
joiner.wait_joiner()
inviter.wait_inviter()
# Group and broadcast joins add an info message first.
if invite != "contact":
joiner.next_text()
inviter.send_text("hello")
assert joiner.next_text() == "hello"

View File

@@ -5,12 +5,12 @@ from imap_tools import AND, U
from deltachat_rpc_client import EventType
def test_moved_markseen(acf, direct_imap, log):
def test_moved_markseen(acfactory, direct_imap, log):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acf.get_online_account()
ac1 = acfactory.get_online_account()
addr, password = acf.get_credentials()
ac2 = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.bring_online()
@@ -57,28 +57,24 @@ def test_moved_markseen(acf, direct_imap, log):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
def test_markseen_message_and_mdn(acf, direct_imap):
ac1, ac2 = acf.get_online_accounts(2)
def test_markseen_message_and_mdn(acfactory, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
# Make sure that messages are not immediately auto-deleted on the server:
ac1.set_config("bcc_self", "1")
ac2.set_config("bcc_self", "1")
acf.get_accepted_chat(ac1, ac2).send_text("hi")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2.wait_for_incoming_msg()
msg.mark_seen()
rex = re.compile("Marked messages ([0-9,:]+) in folder INBOX as seen.")
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
# Each profile flags two messages but the logged UID set
# covers a varying number of them, so just count UIDs mentioned.
# We are not processing UID ranges, here we just care for two UIDs.
for ac in ac1, ac2:
uids = set()
while len(uids) < 2:
while True:
event = ac.wait_for_event()
if event.kind == EventType.INFO and (match := rex.search(event.msg)):
uids.update(re.split("[,:]", match.group(1)))
if event.kind == EventType.INFO and rex.search(event.msg):
break
ac1_direct_imap = direct_imap(ac1)
ac2_direct_imap = direct_imap(ac2)
@@ -91,8 +87,8 @@ def test_markseen_message_and_mdn(acf, direct_imap):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
def test_trash_multiple_messages(acf, direct_imap, log):
ac1, ac2 = acf.get_online_accounts(2)
def test_trash_multiple_messages(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.stop_io()
# Make sure that messages are not immediately auto-deleted on the server:
@@ -101,7 +97,7 @@ def test_trash_multiple_messages(acf, direct_imap, log):
ac2.set_config("sync_msgs", "0")
ac2.start_io()
chat12 = acf.get_accepted_chat(ac1, ac2)
chat12 = acfactory.get_accepted_chat(ac1, ac2)
log.section("ac1: sending 3 messages")
texts = ["first", "second", "third"]

View File

@@ -7,25 +7,15 @@ If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import itertools
import logging
import os
import threading
from contextlib import contextmanager
import time
import pytest
from deltachat_rpc_client import EventType
# Relays on underscore domains advertise themselves as iroh relay
# but serve a self-signed certificate that iroh's TLS stack rejects.
# Skipping instead of xfailing keeps the run fast:
# these tests only fail after waiting for realtime connections to time out.
pytestmark = pytest.mark.skipif(
os.environ.get("CHATMAIL_DOMAIN", "").startswith("_"),
reason="iroh does not accept the self-signed certificate of an underscore domain",
)
@pytest.fixture
def path_to_webxdc(request):
@@ -45,11 +35,7 @@ def log(msg):
logging.info(msg)
# payload used to probe/establish realtime connectivity, filtered out by tests
SETUP_DATA = b"realtime-setup"
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
assert ac1.get_config("webxdc_realtime_enabled") == "1"
assert ac2.get_config("webxdc_realtime_enabled") == "1"
ac1_ac2_chat = ac1.create_chat(ac2)
@@ -66,44 +52,45 @@ def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
if wait:
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg)])
return ac1_webxdc_msg, ac2_webxdc_msg
@contextmanager
def send_realtime_data_forever(msgs, data=None):
stop = threading.Event()
data = data or [SETUP_DATA] * len(msgs)
def setup_thread_send_realtime_data(msg, data):
def thread_run():
for _i in range(10):
msg.send_webxdc_realtime_data(data)
time.sleep(1)
def thread_run(msg, payload):
for i in itertools.count():
msg.send_webxdc_realtime_data(payload(i) if callable(payload) else payload)
if stop.wait(1):
return
for msg_payload in zip(msgs, data, strict=True):
threading.Thread(target=thread_run, args=msg_payload, daemon=True).start()
try:
yield
finally:
stop.set()
threading.Thread(target=thread_run, daemon=True).start()
def wait_realtime_connected(msg_pairs):
with send_realtime_data_forever([sender for sender, _ in msg_pairs]):
for _, receiver in msg_pairs:
receiver.account.wait_for_realtime_data(receiver.id)
def wait_receive_realtime_data(msg_data_list):
account = msg_data_list[0][0].account
msg_data_list = msg_data_list[:]
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
while msg_data_list:
event = account.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
for i, (msg, data) in enumerate(msg_data_list):
if msg.id == event.msg_id:
assert list(data) == event.data
log(f"msg {msg.id}: got correct realtime data {data}")
del msg_data_list[i]
break
def test_realtime_sequentially(acf, path_to_webxdc):
def test_realtime_sequentially(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acf.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
@@ -111,7 +98,7 @@ def test_realtime_sequentially(acf, path_to_webxdc):
# send iroh announcements sequentially
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
acf.send_message(from_account=ac1, to_account=ac2, text="ping1")
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("waiting for incoming message on ac2")
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
@@ -119,7 +106,7 @@ def test_realtime_sequentially(acf, path_to_webxdc):
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
acf.send_message(from_account=ac2, to_account=ac1, text="ping2")
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
log("waiting for incoming message on ac1")
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
@@ -130,27 +117,50 @@ def test_realtime_sequentially(acf, path_to_webxdc):
data = os.urandom(128000)
ac1_webxdc_msg.send_webxdc_realtime_data(data)
assert ac2.wait_for_realtime_data(ac2_webxdc_msg.id) == data
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == list(data)
break
def test_realtime_simultaneously(acf, path_to_webxdc):
def test_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acf.get_online_accounts(2)
setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
def test_two_parallel_realtime_simultaneously(acf, path_to_webxdc):
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acf.get_online_accounts(2)
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg), (ac2_webxdc_msg, ac1_webxdc_msg)])
wait_realtime_connected([(ac1_webxdc_msg2, ac2_webxdc_msg2), (ac2_webxdc_msg2, ac1_webxdc_msg2)])
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
def test_no_duplicate_messages(acf, path_to_webxdc):
def test_no_duplicate_messages(acfactory, path_to_webxdc):
"""Test that messages are received only once."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
@@ -164,29 +174,50 @@ def test_no_duplicate_messages(acf, path_to_webxdc):
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
with send_realtime_data_forever([ac1_webxdc_msg], data=[lambda i: str(i).encode()]):
n = int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode())
assert int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode()) > n
def thread_run():
for i in range(10):
data = str(i).encode()
ac1_webxdc_msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
n = int(bytes(event.data).decode())
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
assert int(bytes(event.data).decode()) > n
def test_no_reordering(acf, path_to_webxdc):
def test_no_reordering(acfactory, path_to_webxdc):
"""Test that sending a lot of realtime messages does not result in reordering."""
ac1, ac2 = acf.get_online_accounts(2)
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
for i in range(200):
ac1_webxdc_msg.send_webxdc_realtime_data([i])
for i in range(200):
# lingering SETUP_DATA payloads from the wait_realtime_connected() barrier may still arrive
while (data := ac2.wait_for_realtime_data(ac2_webxdc_msg.id)) == SETUP_DATA:
pass
assert data == bytes([i]), "Reordering detected"
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
if event.data[0] == i:
break
pytest.fail("Reordering detected")
def test_advertisement_after_chatting(acf, path_to_webxdc):
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
"""Test that realtime advertisement is assigned to the correct message after chatting."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
@@ -205,14 +236,17 @@ def test_advertisement_after_chatting(acf, path_to_webxdc):
assert event.msg_id == ac1_webxdc_msg.id
def test_realtime_large_webxdc(acf, path_to_large_webxdc):
def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
"""Tests initializing realtime channel on a large webxdc.
This is a regression test for a bug that existed in version 2.42.0.
Large webxdc is split into pre- and post- message,
and this previously resulted in failure to initialize realtime.
"""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac2.create_chat(ac1)
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc)

View File

@@ -1,15 +1,15 @@
def test_set_location(dc, acf) -> None:
def test_set_location(dc, acfactory) -> None:
# Try setting location without any accounts.
assert not dc.set_location(1.0, 2.0, 0.1)
# Create one account that does not stream,
# set location.
acf.new_configured_account()
acfactory.new_configured_account()
assert not dc.set_location(3.0, 4.0, 0.1)
def test_send_locations_to_chat(dc, acf):
alice, bob = acf.get_online_accounts(2)
def test_send_locations_to_chat(dc, acfactory):
alice, bob = acfactory.get_online_accounts(2)
assert not alice.is_sending_locations()
alice_chat_bob = alice.create_chat(bob)

View File

@@ -4,8 +4,8 @@ from deltachat_rpc_client import EventType
from deltachat_rpc_client.const import MessageState
def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
ac = acf.get_online_account()
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
ac = acfactory.get_online_account()
# Initially after getting online
# the setting bcc_self is set to 0 because there is only one device
@@ -29,22 +29,27 @@ def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
assert ac.get_config("bcc_self") == "1"
def test_one_account_send_bcc_setting(acf, log, direct_imap):
ac1, ac2 = acf.get_online_accounts(2)
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
log.section("send out message without bcc to ourselves")
ac1.set_config("bcc_self", "0")
chat = ac1.create_chat(ac2)
self_addr = ac1.get_config("addr")
other_addr = ac2.get_config("addr")
msg_out = chat.send_text("message1")
assert not msg_out.get_snapshot().is_forwarded
# wait for send out (no BCC)
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert ac1.get_config("bcc_self") == "0"
assert self_addr not in ev.msg
assert other_addr in ev.msg
log.section("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
@@ -52,16 +57,20 @@ def test_one_account_send_bcc_setting(acf, log, direct_imap):
msg_out = chat.send_text("message2")
# wait for send out (BCC)
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert ac1.get_config("bcc_self") == "1"
# Second client receives only the second message, but not the first.
# Second client receives only second message, but not the first.
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
# now make sure we are sending message to ourselves too
assert self_addr in ev.msg
assert self_addr in ev.msg
# BCC-self messages are marked as seen by the sender device.
while True:
event = ac1.wait_for_event()
@@ -75,9 +84,9 @@ def test_one_account_send_bcc_setting(acf, log, direct_imap):
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_multidevice_sync_seen(acf, log):
def test_multidevice_sync_seen(acfactory, log):
"""Test that message marked as seen on one device is marked as seen on another."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
@@ -127,34 +136,3 @@ def test_multidevice_sync_seen(acf, log):
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_info()
def test_multidevice_sync_seen_mdns_off(acf, log):
"""Test that MDNs to self are sent even if MDNs are disabled."""
ac1, ac2 = acf.get_online_accounts(2)
ac1.set_config("mdns_enabled", "0")
ac1_clone = ac1.clone()
ac1_clone.bring_online()
assert ac1.get_config("bcc_self") == "1"
assert ac1.get_config("mdns_enabled") == "0"
assert ac1_clone.get_config("bcc_self") == "1"
assert ac1_clone.get_config("mdns_enabled") == "0"
ac1.create_chat(ac2)
ac1_clone_chat = ac1_clone.create_chat(ac2)
ac2_chat = ac2.create_chat(ac1)
log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
ac2_chat.send_text("Hi")
ac1_message = ac1.wait_for_incoming_msg()
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
ac1_message.mark_seen()
assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
log.section("ac1 clone detects that message is marked as seen")
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
assert ev.chat_id == ac1_clone_chat.id
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN

View File

@@ -1,6 +1,3 @@
import time
import urllib.parse
import pytest
from deltachat_rpc_client import EventType
@@ -8,27 +5,11 @@ from deltachat_rpc_client.const import ChatType, DownloadState
from deltachat_rpc_client.rpc import JsonRpcError
def alice_with_two_transports_and_bob(acf):
alice, bob = acf.get_online_accounts(2)
alice.add_transport_from_qr(acf.get_account_qr())
alice.bring_online()
return alice, alice.create_chat(bob), bob.create_chat(alice)
def messages_with_text(chat, text):
return [msg for msg in chat.get_messages() if msg.get_snapshot().text == text]
def wait_for_imap_message(imap):
while not imap.get_all_messages():
time.sleep(1)
def test_add_second_address(acf) -> None:
account = acf.new_configured_account()
def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 2
@@ -37,24 +18,20 @@ def test_add_second_address(acf) -> None:
first_addr = account.list_transports()[0]["addr"]
second_addr = account.list_transports()[1]["addr"]
third_addr = account.list_transports()[2]["addr"]
assert account.get_config("configured_addr") == first_addr
account.delete_transport(first_addr)
assert len(account.list_transports()) == 2
assert account.get_config("configured_addr") != first_addr
# Cannot delete the first address.
with pytest.raises(JsonRpcError):
account.delete_transport(first_addr)
account.delete_transport(second_addr)
assert len(account.list_transports()) == 1
with pytest.raises(JsonRpcError):
account.delete_transport(third_addr)
assert len(account.list_transports()) == 2
def test_change_address(acf) -> None:
"""Test Alice configuring a second transport and removing the first one."""
alice, bob = acf.get_online_accounts(2)
def test_change_address(acfactory) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob.create_chat(alice)
alice_chat_bob = alice.create_chat(bob)
@@ -64,18 +41,28 @@ def test_change_address(acf) -> None:
sender_addr1 = msg1.sender.get_snapshot().address
alice.stop_io()
old_alice_addr = alice.list_transports()[0]["addr"]
old_alice_addr = alice.get_config("configured_addr")
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr in alice_vcard
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
with pytest.raises(JsonRpcError):
# Cannot use the address that is not
# configured for any transport.
alice.set_config("configured_addr", bob_addr)
alice.delete_transport(old_alice_addr)
# Load old address so it is cached.
assert alice.get_config("configured_addr") == old_alice_addr
alice.set_config("configured_addr", new_alice_addr)
# Make sure that setting `configured_addr` invalidated the cache.
assert alice.get_config("configured_addr") == new_alice_addr
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr not in alice_vcard
assert new_alice_addr in alice_vcard
with pytest.raises(JsonRpcError):
alice.delete_transport(new_alice_addr)
alice.start_io()
alice_chat_bob.send_text("Hello again!")
@@ -89,37 +76,18 @@ def test_change_address(acf) -> None:
assert sender_addr2 == new_alice_addr
def test_remove_transport_keep_messages(acf) -> None:
"""Test that deleting current sending transport keeps queued messages."""
alice, bob = acf.get_online_accounts(2)
qr = acf.get_account_qr()
alice.add_transport_from_qr(qr)
alice.stop_io()
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello!")
new_alice_addr = alice.list_transports()[1]["addr"]
alice.delete_transport(alice.list_transports()[0]["addr"])
alice.start_io()
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
assert bob_msg.sender.get_snapshot().address == new_alice_addr
def test_download_on_demand(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
def test_download_on_demand(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice.set_config("download_limit", "1")
alice.stop_io()
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
alice.start_io()
alice.create_chat(bob)
chat_bob_alice = bob.create_chat(alice)
chat_bob_alice.send_message(file=rpcdata.get_path("image/screenshot.jpg"))
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
msg = alice.wait_for_incoming_msg()
snapshot = msg.get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
@@ -135,15 +103,15 @@ def test_download_on_demand(acf, rpcdata) -> None:
assert msg.get_snapshot().download_state == dstate
def test_reconfigure_transport(acf) -> None:
def test_reconfigure_transport(acfactory) -> None:
"""Test that reconfiguring the transport works."""
account = acf.get_online_account()
account = acfactory.get_online_account()
[transport] = account.list_transports()
account.add_or_update_transport(transport)
def test_transport_synchronization(acf, log) -> None:
def test_transport_synchronization(acfactory, log) -> None:
"""Test synchronization of transports between devices."""
def wait_for_io_started(ac):
@@ -152,24 +120,22 @@ def test_transport_synchronization(acf, log) -> None:
if "scheduler is running" in ev.msg:
return
def wait_transports(ac, n):
while len(ac.list_transports()) != n:
ac.wait_for_event(EventType.TRANSPORTS_MODIFIED)
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
ac1.add_transport_from_qr(qr)
wait_transports(ac1_clone, 2)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1_clone)
assert len(ac1.list_transports()) == 2
assert len(ac1_clone.list_transports()) == 2
ac1_clone.add_transport_from_qr(qr)
wait_transports(ac1, 3)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1)
assert len(ac1.list_transports()) == 3
assert len(ac1_clone.list_transports()) == 3
log.section("ac1 clone removes second transport")
@@ -177,17 +143,24 @@ def test_transport_synchronization(acf, log) -> None:
addr3 = transport3["addr"]
ac1_clone.delete_transport(transport2["addr"])
wait_transports(ac1, 2)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1)
[transport1, transport3] = ac1.list_transports()
log.section("ac1 changes the sending transport")
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport3["addr"])
# One event for updated `add_timestamp` of the new primary transport,
# one event for the `configured_addr` update.
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
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"])
wait_transports(ac1_clone, 1)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1_clone)
[transport3] = ac1_clone.list_transports()
assert transport3["addr"] == addr3
@@ -200,16 +173,15 @@ def test_transport_synchronization(acf, log) -> None:
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
def test_transport_sync_new_as_primary(acf, log) -> None:
"""Test that a transport promoted on one device is usable on other devices."""
ac1, bob = acf.get_online_accounts(2)
def test_transport_sync_new_as_primary(acfactory, log) -> None:
"""Test synchronization of new transport as primary between devices."""
ac1, bob = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
ac1.add_transport_from_qr(qr)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
ac1_transports = ac1.list_transports()
assert len(ac1_transports) == 2
[transport1, transport2] = ac1_transports
@@ -220,7 +192,10 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport2["addr"])
log.section("ac1_clone receives a message via the new transport")
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
log.section("ac1_clone receives a message via the new primary transport")
ac1_chat = ac1.create_chat(bob)
ac1_chat.send_text("Hello!")
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
@@ -230,12 +205,12 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
def test_recognize_self_address(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_recognize_self_address(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_chat = bob.create_chat(alice)
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
@@ -246,10 +221,10 @@ def test_recognize_self_address(acf) -> None:
assert msg.chat == alice.create_chat(bob)
def test_transport_limit(acf) -> None:
def test_transport_limit(acfactory) -> None:
"""Test transports limit."""
account = acf.get_online_account()
qr = acf.get_account_qr()
account = acfactory.get_online_account()
qr = acfactory.get_account_qr()
limit = 5
@@ -262,22 +237,22 @@ def test_transport_limit(acf) -> None:
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)
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
def test_message_info_imap_urls(acf) -> None:
def test_message_info_imap_urls(acfactory) -> None:
"""Test that message info contains IMAP URLs of where the message was received."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
qr = acf.get_account_qr()
for _ in range(3):
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.
alice.bring_online()
for _ in range(i + 1):
alice.bring_online()
# Enable multi-device mode so messages are not deleted immediately.
alice.set_config("bcc_self", "1")
@@ -307,12 +282,20 @@ def test_message_info_imap_urls(acf) -> None:
assert f"{new_alice_addr}/INBOX" in msg_info
def test_remove_primary_transport(acf, log) -> None:
def test_remove_primary_transport(acfactory, log) -> None:
"""Test that after removing the primary relay, Alice can still receive messages."""
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
alice, bob = acfactory.get_online_accounts(2)
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
alice.bring_online()
bob_chat = bob.create_chat(alice)
alice.create_chat(bob)
log.section("Alice sets up second transport")
[transport1, transport2] = alice.list_transports()
alice.set_config("configured_addr", transport2["addr"])
bob_chat.send_text("Hello!")
msg1 = alice.wait_for_incoming_msg().get_snapshot()
@@ -320,7 +303,6 @@ def test_remove_primary_transport(acf, log) -> None:
log.section("Alice removes the primary relay")
alice.delete_transport(transport1["addr"])
assert alice.get_config("configured_addr") == transport2["addr"]
alice.stop_io()
alice.start_io()
@@ -328,75 +310,4 @@ def test_remove_primary_transport(acf, log) -> None:
msg2 = alice.wait_for_incoming_msg().get_snapshot()
assert msg2.text == "Hello again!"
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
assert msg2.chat == alice_chat
def test_qr_works_after_removing_primary_transport(acf, log) -> None:
log.section("Alice setups an account and adds two additional relays")
alice = acf.new_configured_account()
relay_qr = acf.get_account_qr()
alice.add_transport_from_qr(relay_qr)
alice.add_transport_from_qr(relay_qr)
first_addr = alice.list_transports()[0]["addr"]
second_addr = alice.list_transports()[1]["addr"]
third_addr = alice.list_transports()[2]["addr"]
log.section("Alice creates a QR code")
chat_qr = alice.get_qr_code()
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
assert f"&a={first_addr}" in chat_qr_unquoted
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted
log.section("Alice removes first and second transport")
alice.set_config("configured_addr", third_addr)
alice.delete_transport(first_addr)
alice.delete_transport(second_addr)
log.section("Bob scans the QR code, which still works")
alice.bring_online()
bob = acf.get_online_account()
bob.secure_join(chat_qr)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
def test_background_fetch_from_second_transport(acf, direct_imap, dc):
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
[transport1, transport2] = alice.list_transports()
assert alice.get_config("configured_addr") == transport1["addr"]
alice.stop_io()
bob_chat.send_text("hello")
imap1 = direct_imap(alice, transport1["addr"], transport1["password"])
wait_for_imap_message(direct_imap(alice, transport2["addr"], transport2["password"]))
wait_for_imap_message(imap1)
# Leave the message on the second transport only.
imap1.delete("1:*")
dc.background_fetch(300)
assert len(messages_with_text(alice_chat, "hello")) == 1
def test_background_fetch_no_duplicates(acf, direct_imap, dc):
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
alice.stop_io()
bob_chat.send_text("hello")
for transport in alice.list_transports():
wait_for_imap_message(direct_imap(alice, transport["addr"], transport["password"]))
dc.background_fetch(300)
assert len(messages_with_text(alice_chat, "hello")) == 1
def test_multitransport_mdn(acf):
"""Test sending an MDN right after configuring two transports."""
alice, bob = acf.get_online_accounts(2)
alice.add_transport_from_qr(acf.get_account_qr())
alice.bring_online()
alice.create_chat(bob)
bob_msg = bob.create_chat(alice).send_text("Hello!")
alice.wait_for_incoming_msg().mark_seen()
assert bob.wait_for_event(EventType.MSG_READ).msg_id == bob_msg.id
assert msg2.chat == alice.create_chat(bob)

View File

@@ -4,29 +4,48 @@ import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
from deltachat_rpc_client.const import ChatType
from deltachat_rpc_client.rpc import JsonRpcError
def test_qr_setup_contact(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.e2ee_avail
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.e2ee_avail
assert bob_contact_alice_snapshot.is_verified
# Test that if Bob imports a key,
# backwards verification is not lost
# because default key is not changed.
logging.info("Bob 2 is created")
bob2 = acfactory.new_configured_account()
bob2.export_self_keys(tmp_path)
logging.info("Bob tries to import a key")
# Importing a second key is not allowed.
with pytest.raises(JsonRpcError):
bob.import_self_keys(tmp_path)
assert bob.get_config("key_id") == "1"
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
def test_qr_setup_contact_svg(acf) -> None:
alice = acf.new_configured_account()
def test_qr_setup_contact_svg(acfactory) -> None:
alice = acfactory.new_configured_account()
_, _, domain = alice.get_config("addr").rpartition("@")
_qr_code, svg = alice.get_qr_code_svg()
@@ -40,8 +59,8 @@ def test_qr_setup_contact_svg(acf) -> None:
assert "Alice" in svg
def test_qr_securejoin(acf):
alice, bob, fiona = acf.get_online_accounts(3)
def test_qr_securejoin(acfactory):
alice, bob, fiona = acfactory.get_online_accounts(3)
# Setup second device for Alice
# to test observing securejoin protocol.
@@ -62,24 +81,26 @@ def test_qr_securejoin(acf):
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.e2ee_avail
assert alice_contact_bob_snapshot.is_verified
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.e2ee_avail
assert bob_contact_alice_snapshot.is_verified
# Start second Alice device.
# Alice observes the securejoin protocol on the second device.
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.create_contact(bob)
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.e2ee_avail
assert alice2_contact_bob_snapshot.is_verified
# The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("Fiona joins the group via alice2")
@@ -90,8 +111,8 @@ def test_qr_securejoin(acf):
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_qr_securejoin_broadcast(acf, all_devices_online):
alice, bob, fiona = acf.get_online_accounts(3)
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
alice, bob, fiona = acfactory.get_online_accounts(3)
alice2 = alice.clone()
bob2 = bob.clone()
@@ -130,9 +151,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
assert snapshot2.chat_id == chat.id
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
# Check that the chat partner's key is known.
# Check that the chat partner is verified.
contact_snapshot = contact.get_snapshot()
assert contact_snapshot.e2ee_avail
assert contact_snapshot.is_verified
chat = get_broadcast(ac)
chat_msgs = chat.get_messages()
@@ -231,9 +252,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
def test_qr_securejoin_contact_request(acf) -> None:
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -257,8 +278,8 @@ def test_qr_securejoin_contact_request(acf) -> None:
assert bob_chat_alice.get_basic_snapshot().is_contact_request
def test_qr_readreceipt(acf) -> None:
alice, bob, charlie = acf.get_online_accounts(3)
def test_qr_readreceipt(acfactory) -> None:
alice, bob, charlie = acfactory.get_online_accounts(3)
logging.info("Bob and Charlie setup contact with Alice")
qr_code = alice.get_qr_code()
@@ -314,24 +335,24 @@ def test_qr_readreceipt(acf) -> None:
assert not bob.get_chat_by_contact(bob_contact_charlie)
def test_setup_contact_resetup(acf) -> None:
def test_setup_contact_resetup(acfactory) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acf.resetup_account(alice)
alice = acfactory.resetup_account(alice)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
def test_group_member_added_recovery(acf) -> None:
"""Tests group recovery after a member resets its key."""
ac1, ac2, ac3 = acf.get_online_accounts(3)
def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates a group")
chat = ac1.create_group("Group")
@@ -341,7 +362,11 @@ def test_group_member_added_recovery(acf) -> None:
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 joins the group")
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.create_contact(ac2)
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
@@ -349,9 +374,9 @@ def test_group_member_added_recovery(acf) -> None:
ac3_contact_ac2_old = ac3.create_contact(ac2)
logging.info("ac2 logs in on a new device")
ac2 = acf.resetup_account(ac2)
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 scans ac3's QR code again")
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -391,20 +416,28 @@ def test_group_member_added_recovery(acf) -> None:
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.create_contact(ac2)
ac1_contact_ac3 = ac1.create_contact(ac3)
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/chatmail/core/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: set up contact with ac2")
logging.info("ac3: verify with ac2")
qr_code = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
# in order for ac2 to have pending bobstate with a group
# we first create a fully joined group, and then start
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create a group that ac2 fully joins")
@@ -413,7 +446,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
# ensure ac1 can write and ac2 receives messages in the chat
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
@@ -426,11 +459,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect the ac3/ac2 setup-contact started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().e2ee_avail
assert ac2.create_contact(ac3).get_snapshot().e2ee_avail
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().is_verified
assert ac2.create_contact(ac3).get_snapshot().is_verified
logging.info("ac3: create a group VG with ac2")
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created")
vg.add_contact(ac3.create_contact(ac2))
@@ -451,17 +484,17 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
return
def test_qr_new_group_unblocked(acf):
def test_qr_new_group_unblocked(acfactory):
"""Regression test for a bug introduced in core v1.113.0.
ac2 scans a group QR code created by ac1.
This results in creation of a blocked single chat with ac1 on ac2,
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining")
qr_code = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
@@ -480,13 +513,13 @@ def test_qr_new_group_unblocked(acf):
@pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow(acf):
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
addr, password = acf.get_credentials()
addr, password = acfactory.get_credentials()
logging.info("ac1: create group QR, ac2 scans and joins")
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello")
qr_code = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
@@ -522,15 +555,65 @@ def test_aeap_flow(acf):
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
def test_securejoin_after_contact_resetup(acf) -> None:
"""
Regression test for a bug that prevented joining a group with a QR code
if the group already contains a contact with the same address as the inviter,
but different key fingerprint while a securejoin with that contact is still pending.
"""
ac1, ac2, ac3 = acf.get_online_accounts(3)
def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
# ac3 creates a group with ac1.
# Bob verifies Alice.
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
# Bob verifies Carol.
qr_code = carol.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
bob_contact_alice = bob.create_contact(alice, "Alice")
bob_contact_carol = bob.create_contact(carol, "Carol")
carol_contact_alice = carol.create_contact(alice, "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Autocrypt group")
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Group propagates verification using Autocrypt-Gossip header.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Securejoin group")
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Securejoin group"
assert snapshot.show_padlock
# Securejoin propagates verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acfactory) -> None:
"""
Regression test for a bug that prevented joining verified group with a QR code
if the group is already created and contains
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Group")
# ac1 joins ac3 group.
@@ -540,27 +623,31 @@ def test_securejoin_after_contact_resetup(acf) -> None:
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 sets up contact with ac1
# ac2 verifies ac1
qr_code = ac1.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 is verified for ac2.
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert ac2_contact_ac1.get_snapshot().e2ee_avail
assert ac2_contact_ac1.get_snapshot().is_verified
# ac1 resetups the account.
ac1 = acf.resetup_account(ac1)
ac1 = acfactory.resetup_account(ac1)
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()
# Scanning a QR code creates a group with the inviter, here ac1.
# Normally the securejoin protocol
# would complete and "Member added" would arrive,
# but ac1 is offline so it never finishes.
# Scanning a QR code results in creating an unprotected group with an inviter.
# In this case inviter is ac1 which has an inconsistent key state.
# Normally inviter becomes verified as a result of Securejoin protocol
# and then the group chat becomes verified when "Member added" is received,
# but in this case ac1 is offline and this Securejoin process will never finish.
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
ac2.secure_join(ac1_qr_code)
@@ -577,13 +664,16 @@ def test_securejoin_after_contact_resetup(acf) -> None:
ac2_chat = snapshot.chat
assert len(ac2_chat.get_contacts()) == 3
# ac1 is still "not verified" for ac2 due to inconsistent state.
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acf):
alice, bob = acf.get_online_accounts(2)
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group")
logging.info("Bob joins the group")
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
@@ -592,7 +682,7 @@ def test_withdraw_securejoin_qr(acf):
alice.clear_all_events()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
@@ -614,25 +704,3 @@ def test_withdraw_securejoin_qr(acf):
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
):
break
def test_qr_scan_updates_new_relay_address(acf):
alice, bob = acf.get_online_accounts(2)
bob_alice_chat = bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
for ac in [alice, bob]:
old_addr = ac.get_config("configured_addr")
ac.add_transport_from_qr(acf.get_account_qr())
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
ac.delete_transport(old_addr)
bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
bob_alice_chat.send_text("hi")
snapshot = alice.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "hi"

View File

@@ -48,8 +48,8 @@ def test_email_address_validity(rpc) -> None:
assert not rpc.check_email_validity(addr)
def test_acf(acf) -> None:
account = acf.new_configured_account()
def test_acfactory(acfactory) -> None:
account = acfactory.new_configured_account()
while True:
event = account.wait_for_event()
if event.kind == EventType.CONFIGURE_PROGRESS:
@@ -61,9 +61,9 @@ def test_acf(acf) -> None:
logging.info("Successful configuration")
def test_configure_starttls(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
def test_configure_starttls(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -75,10 +75,10 @@ def test_configure_starttls(acf) -> None:
assert account.is_configured()
def test_lowercase_address(acf) -> None:
addr, password = acf.get_credentials()
def test_lowercase_address(acfactory) -> None:
addr, password = acfactory.get_credentials()
addr_upper = addr.upper()
account = acf.get_unconfigured_account()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr_upper,
@@ -103,9 +103,9 @@ def test_lowercase_address(acf) -> None:
assert addr_upper not in param
def test_configure_ip(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
def test_configure_ip(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
with pytest.raises(JsonRpcError):
@@ -119,10 +119,10 @@ def test_configure_ip(acf) -> None:
)
def test_configure_alternative_port(acf) -> None:
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -134,9 +134,9 @@ def test_configure_alternative_port(acf) -> None:
assert account.is_configured()
def test_list_transports(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
def test_list_transports(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -152,8 +152,8 @@ def test_list_transports(acf) -> None:
assert params["imapUser"] == addr
def test_account(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -221,32 +221,8 @@ def test_account(acf) -> None:
alice.stop_io()
def test_mark_fresh_vs_self_mdn(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob.set_config("bcc_self", "1")
alice_contact_bob = alice.create_contact(bob)
alice_chat = alice_contact_bob.create_chat()
alice_chat.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
chat_id = event.chat_id
msg_id = event.msg_id
bob_chat = bob.get_chat_by_id(chat_id)
message = bob.get_message_by_id(msg_id)
bob_chat.accept()
bob.mark_seen_messages([message])
bob_chat.mark_fresh()
assert bob_chat.get_fresh_message_count() == 1
alice.wait_for_event(EventType.MSG_READ)
alice_chat.send_text("You've read 'Hello!'")
bob.wait_for_incoming_msg_event()
assert bob_chat.get_fresh_message_count() == 2
def test_chat(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -275,7 +251,7 @@ def test_chat(acf) -> None:
bob_chat_alice.unpin()
bob_chat_alice.archive()
bob_chat_alice.unarchive()
with pytest.raises(JsonRpcError): # can't set name for single chats
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
bob_chat_alice.set_name("test")
bob_chat_alice.set_ephemeral_timer(300)
bob_chat_alice.get_encryption_info()
@@ -315,8 +291,8 @@ def test_chat(acf) -> None:
group.get_locations()
def test_contact(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -332,8 +308,8 @@ def test_contact(acf) -> None:
alice_contact_bob.create_chat()
def test_message(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -363,8 +339,8 @@ def test_message(acf) -> None:
assert reactions == snapshot.reactions
def test_receive_imf_failure(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_receive_imf_failure(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -380,7 +356,7 @@ def test_receive_imf_failure(acf) -> None:
snapshot.text == "❌ Failed to receive a message:"
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
f" Core version {version}."
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/"
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
)
# The failed message doesn't break the IMAP loop.
@@ -392,8 +368,8 @@ def test_receive_imf_failure(acf) -> None:
assert snapshot.error is None
def test_selfavatar_sync(acf, rpcdata, log) -> None:
alice = acf.get_online_account()
def test_selfavatar_sync(acfactory, data, log) -> None:
alice = acfactory.get_online_account()
log.section("Alice adds a second device")
alice2 = alice.clone()
@@ -402,7 +378,7 @@ def test_selfavatar_sync(acf, rpcdata, log) -> None:
alice2.start_io()
log.section("First device changes avatar")
image = rpcdata.get_path("image/avatar1000x1000.jpg")
image = data.get_path("image/avatar1000x1000.jpg")
alice.set_config("selfavatar", image)
avatar_config = alice.get_config("selfavatar")
avatar_hash = os.path.basename(avatar_config)
@@ -417,10 +393,11 @@ def test_selfavatar_sync(acf, rpcdata, log) -> None:
assert avatar_config != avatar_config2
def test_dont_move_sync_msgs(acf, direct_imap):
addr, password = acf.get_credentials()
ac1 = acf.get_unconfigured_account()
def test_dont_move_sync_msgs(acfactory, direct_imap):
addr, password = acfactory.get_credentials()
ac1 = acfactory.get_unconfigured_account()
ac1.set_config("bcc_self", "1")
ac1.set_config("fix_is_chatmail", "1")
ac1.add_or_update_transport({"addr": addr, "password": password})
ac1.start_io()
ac1_direct_imap = direct_imap(ac1)
@@ -447,8 +424,8 @@ def test_dont_move_sync_msgs(acf, direct_imap):
time.sleep(1)
def test_reaction_seen_on_another_dev(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_reaction_seen_on_another_dev(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
@@ -473,38 +450,9 @@ def test_reaction_seen_on_another_dev(acf) -> None:
assert chat_id == alice2_chat_bob.id
def test_2nd_device_events_when_msgs_are_seen(acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
# Get an accepted chat, otherwise alice2 won't be notified about the 2nd message.
chat_alice2 = alice2.create_chat(bob)
chat_id_alice2 = chat_alice2.get_basic_snapshot().id
chat_bob_alice = bob.create_chat(alice)
chat_bob_alice.send_text("Hello!")
msg_alice = alice.wait_for_incoming_msg()
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
chat_bob_alice.send_text("What's new?")
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
chat_alice2 = alice2.get_chat_by_id(chat_id_alice2)
assert chat_alice2.get_fresh_message_count() == 2
msg_alice.mark_seen()
assert alice2.wait_for_msgs_changed_event().chat_id == chat_id_alice2
assert chat_alice2.get_fresh_message_count() == 1
msg_id = alice.wait_for_msgs_changed_event().msg_id
msg = alice.get_message_by_id(msg_id)
msg.mark_seen()
assert alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id == chat_id_alice2
assert chat_alice2.get_fresh_message_count() == 0
def test_is_bot(acf) -> None:
def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -518,18 +466,18 @@ def test_is_bot(acf) -> None:
assert snapshot.is_bot
def test_bot(acf) -> None:
def test_bot(acfactory) -> None:
mock = MagicMock()
user = (acf.get_online_accounts(1))[0]
bot = acf.new_configured_bot()
bot2 = acf.new_configured_bot()
user = (acfactory.get_online_accounts(1))[0]
bot = acfactory.new_configured_bot()
bot2 = acfactory.new_configured_bot()
assert bot.is_configured()
assert bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
event = acf.process_message(from_account=user, to_client=bot, text="Hello!")
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id)
@@ -542,28 +490,28 @@ def test_bot(acf) -> None:
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
event = acf.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
event = acf.process_message(from_account=user, to_client=bot, text="hello!")
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
acf.process_message(from_account=bot2.account, to_client=bot, text="hello")
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
acf.process_message(from_account=user, to_client=bot, text="hey!")
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook)
mock.hook.reset_mock()
acf.process_message(from_account=user, to_client=bot, text="hello")
event = acf.process_message(from_account=user, to_client=bot, text="/help")
acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)
def test_wait_next_messages(acf) -> None:
alice = acf.get_online_account()
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.get_online_account()
# Create a bot account so it does not receive device messages in the beginning.
addr, password = acf.get_credentials()
bot = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
bot.set_config("bot", "1")
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
@@ -589,19 +537,19 @@ def test_wait_next_messages(acf) -> None:
assert snapshot.text == "Hello!"
def test_import_export_backup(acf, tmp_path) -> None:
alice = acf.new_configured_account()
def test_import_export_backup(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acf.get_unconfigured_account()
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
assert alice2.manager.get_system_info()
def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
(ac1, some1) = acf.get_online_accounts(2)
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")
@@ -609,7 +557,7 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
chat1.send_text("msg1")
assert len(ac1.get_contacts()) == 1
original_image_path = rpcdata.get_path("image/avatar64x64.png")
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
@@ -660,7 +608,7 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
ac1.start_io()
log.section("get fresh empty account")
ac2 = acf.get_unconfigured_account()
ac2 = acfactory.get_unconfigured_account()
log.section("import backup and check it's proper")
ac2.import_backup(files_written[0])
@@ -697,8 +645,8 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
assert len(list(backupdir.glob("*.tar"))) == 2
def test_import_export_keys(acf, tmp_path) -> None:
alice, bob = acf.get_online_accounts(2)
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
@@ -710,7 +658,7 @@ def test_import_export_keys(acf, tmp_path) -> None:
alice_keys_path = tmp_path / "alice_keys"
alice_keys_path.mkdir()
alice.export_self_keys(alice_keys_path)
alice = acf.resetup_account(alice)
alice = acfactory.resetup_account(alice)
alice.import_self_keys(alice_keys_path)
snapshot.chat.accept()
@@ -745,14 +693,31 @@ def test_early_failure(tmp_path) -> None:
with pytest.raises(JsonRpcError, match="invalid_dir"):
rpc.start()
# Requests issued after the server exited must fail immediately
# instead of waiting forever for the finished reader loop.
with pytest.raises(JsonRpcError, match="RPC server closed"):
rpc.get_system_info()
def test_provider_info(rpc) -> None:
account_id = rpc.add_account()
provider_info = rpc.get_provider_info(account_id, "example.org")
assert provider_info["id"] == "example.com"
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
assert provider_info is None
# Test MX record resolution.
# This previously resulted in Gmail provider
# because MX record pointed to google.com domain,
# but MX record resolution has been removed.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
# Disable MX record resolution.
rpc.set_config(account_id, "proxy_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
def test_mdn_doesnt_break_autocrypt(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -782,10 +747,10 @@ def test_mdn_doesnt_break_autocrypt(acf) -> None:
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
download_limit = 300000
alice, *others = acf.get_online_accounts(n_accounts)
alice, *others = acfactory.get_online_accounts(n_accounts)
bob = others[0]
alice_group = alice.create_group("test group")
@@ -821,10 +786,10 @@ def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
assert snapshot.chat == bob_group
def test_download_small_msg_first(acf, tmp_path):
def test_download_small_msg_first(acfactory, tmp_path):
download_limit = 70000
alice, bob0 = acf.get_online_accounts(2)
alice, bob0 = acfactory.get_online_accounts(2)
bob1 = bob0.clone()
bob1.set_config("download_limit", str(download_limit))
@@ -845,14 +810,14 @@ def test_download_small_msg_first(acf, tmp_path):
@pytest.mark.parametrize("delete_chat", [False, True])
def test_delete_available_msg(acf, tmp_path, direct_imap, delete_chat):
def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
"""
Tests `DownloadState.AVAILABLE` message deletion on the receiver side.
Also tests pre- and post-message deletion on the sender side.
"""
# Min. UI setting as of v2.35
download_limit = 163840
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
bob.set_config("download_limit", str(download_limit))
# Avoid immediate deletion from the server
alice.set_config("bcc_self", "1")
@@ -895,8 +860,8 @@ def test_delete_available_msg(acf, tmp_path, direct_imap, delete_chat):
break
def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
alice, bob = acf.get_online_accounts(2)
def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
alice, bob = acfactory.get_online_accounts(2)
# Avoid immediate deletion from the server
bob.set_config("bcc_self", "1")
@@ -931,8 +896,8 @@ def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
break
def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
alice, bob = acf.get_online_accounts(2)
def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
alice, bob = acfactory.get_online_accounts(2)
chat_alice = alice.create_chat(bob)
path = tmp_path / "large"
@@ -960,12 +925,12 @@ def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
break
def test_markseen_contact_request(acf):
def test_markseen_contact_request(acfactory):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
# Bob sets up a second device.
bob2 = bob.clone()
@@ -984,11 +949,11 @@ def test_markseen_contact_request(acf):
@pytest.mark.parametrize("team_profile", [True, False])
def test_no_markseen_in_team_profile(team_profile, acf):
def test_no_markseen_in_team_profile(team_profile, acfactory):
"""
Test that seen status is synchronized iff `team_profile` isn't set.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
if team_profile:
bob.set_config("team_profile", "1")
@@ -1007,11 +972,6 @@ def test_no_markseen_in_team_profile(team_profile, acf):
message.mark_seen()
# The MDN is queued in `smtp_mdns`, which is drained only after the regular
# `smtp` queue, so "Outgoing message" would otherwise overtake it on the wire.
# Wait for the read receipt to reach Alice before queueing "Outgoing message".
alice.wait_for_event(EventType.MSG_READ)
# Send a message and wait until it arrives
# in order to wait until Bob2 gets the markseen message.
# This also tests that outgoing messages
@@ -1029,11 +989,11 @@ def test_no_markseen_in_team_profile(team_profile, acf):
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_read_receipt(acf):
def test_read_receipt(acfactory):
"""
Test sending a read receipt and ensure it is attributed to the correct contact.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_contact_bob = alice.create_contact(bob)
@@ -1052,15 +1012,15 @@ def test_read_receipt(acf):
assert read_receipt_cnt == 1
def test_get_http_response(acf):
alice = acf.new_configured_account()
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
assert http_response["mimetype"] == "text/html"
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
def test_configured_imap_certificate_checks(acf):
alice = acf.new_configured_account()
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
@@ -1079,8 +1039,8 @@ def test_configured_imap_certificate_checks(acf):
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
def test_no_old_msg_is_fresh(acf):
ac1, ac2 = acf.get_online_accounts(2)
def test_no_old_msg_is_fresh(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.start_io()
@@ -1107,9 +1067,9 @@ def test_no_old_msg_is_fresh(acf):
assert len(list(ac1.get_fresh_messages())) == 0
def test_rename_synchronization(acf):
def test_rename_synchronization(acfactory):
"""Test synchronization of contact renaming."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.bring_online()
@@ -1124,9 +1084,9 @@ def test_rename_synchronization(acf):
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
def test_rename_group(acf):
def test_rename_group(acfactory):
"""Test renaming the group."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_group = alice.create_group("Test group")
alice_contact_bob = alice.create_contact(bob)
@@ -1139,7 +1099,6 @@ def test_rename_group(acf):
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
for name in ["Baz", "Foo bar", "Xyzzy"]:
time.sleep(1)
alice_group.set_name(name)
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
@@ -1155,8 +1114,8 @@ def test_get_all_accounts_deadlock(rpc):
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_leave_broadcast(acf, all_devices_online):
alice, bob = acf.get_online_accounts(2)
def test_leave_broadcast(acfactory, all_devices_online):
alice, bob = acfactory.get_online_accounts(2)
bob2 = bob.clone()
@@ -1256,8 +1215,8 @@ def test_leave_broadcast(acf, all_devices_online):
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
def test_leave_and_delete_group(acf, log):
alice, bob = acf.get_online_accounts(2)
def test_leave_and_delete_group(acfactory, log):
alice, bob = acfactory.get_online_accounts(2)
log.section("Alice creates a group")
alice_chat = alice.create_group("Group")
@@ -1280,12 +1239,12 @@ def test_leave_and_delete_group(acf, log):
alice.wait_for_event(EventType.CHAT_MODIFIED)
def test_immediate_autodelete(acf, direct_imap, log):
def test_immediate_autodelete(acfactory, direct_imap, log):
"""
`bcc_self` is off by default,
so that messages are supposed to be immediately autodeleted
"""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
assert ac1.get_config("bcc_self") == "0"
log.section("ac1: create chat with ac2")
@@ -1316,8 +1275,8 @@ def test_immediate_autodelete(acf, direct_imap, log):
assert ev.msg_id == sent_msg.id
def test_background_fetch(acf, dc):
ac1, ac2 = acf.get_online_accounts(2)
def test_background_fetch(acfactory, dc):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.stop_io()
ac1_chat = ac1.create_chat(ac2)
@@ -1353,24 +1312,8 @@ def test_background_fetch(acf, dc):
break
def test_background_fetch_does_not_wait_for_sending(dc, acf):
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice.stop_io()
text = "x" * 200_000
for _ in range(50):
alice_chat_bob.send_text(text)
assert not dc.is_sending_finished()
alice.start_io()
dc.background_fetch(50)
dc.wait_for_event(EventType.ACCOUNTS_BACKGROUND_FETCH_DONE)
assert not dc.is_sending_finished()
def test_message_exists(acf):
ac1, ac2 = acf.get_online_accounts(2)
def test_message_exists(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
message1 = chat.send_text("Hello!")
message2 = chat.send_text("Hello again!")
@@ -1388,7 +1331,7 @@ def test_message_exists(acf):
assert not message2.exists()
def test_synchronize_member_list_on_group_rejoin(acf, log):
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
@@ -1396,7 +1339,7 @@ def test_synchronize_member_list_on_group_rejoin(acf, log):
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 = acf.get_online_accounts(3)
ac1, ac2, ac3 = accounts = acfactory.get_online_accounts(3)
log.section("ac1: creating group chat with 2 other members")
chat = ac1.create_group("title1")
@@ -1432,17 +1375,17 @@ def test_synchronize_member_list_on_group_rejoin(acf, log):
assert msg.get_snapshot().chat.num_contacts() == 2
def test_large_message(acf, rpcdata) -> None:
def test_large_message(acfactory) -> None:
"""
Test sending large message without download limit set,
so it is sent with pre-message but downloaded without user interaction.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_message(
"Hello World, this message is bigger than 5 bytes",
file=rpcdata.get_path("image/screenshot.jpg"),
file="../test-data/image/screenshot.jpg",
)
msg = bob.wait_for_incoming_msg()
@@ -1450,35 +1393,3 @@ def test_large_message(acf, rpcdata) -> None:
assert msg.id == msgs_changed_event.msg_id
snapshot = msg.get_snapshot()
assert snapshot.text == "Hello World, this message is bigger than 5 bytes"
def test_is_sending_finished(dc, acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
bob_chat_alice = bob.create_chat(alice)
assert dc.is_sending_finished()
alice_chat_bob.send_text("Hello!")
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert dc.is_sending_finished()
alice.stop_io()
bob.stop_io()
bob_chat_alice.send_text("Hello back!")
alice_chat_bob.send_text("Hello again!")
assert not dc.is_sending_finished()
alice.start_io()
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert not dc.is_sending_finished()
bob.start_io()
bob.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert dc.is_sending_finished()

View File

@@ -1,132 +0,0 @@
"""Test that docs/schema.sql matches the actual database schema."""
import re
import sqlite3
from pathlib import Path
DOC_PATH = Path(__file__).resolve().parents[2] / "docs" / "schema.sql"
def strip_comments(sql):
return re.sub(r"--[^\n]*", "", sql)
def normalize(stmt):
stmt = re.sub(r"\s+", " ", stmt).strip()
stmt = stmt.replace("CREATE TABLE IF NOT EXISTS ", "CREATE TABLE ")
return re.sub(r'"(\w+)"', r"\1", stmt)
def split_table(body):
items = []
depth = 0
current = ""
for char in body:
if char == "(":
depth += 1
elif char == ")":
depth -= 1
if char == "," and depth == 0:
items.append(current.strip())
current = ""
else:
current += char
if current.strip():
items.append(current.strip())
return items
def parse_schema(sql):
objects = {}
for raw_stmt in strip_comments(sql).split(";"):
stmt = normalize(raw_stmt)
if not stmt:
continue
match = re.match(r"CREATE TABLE (\w+) ?\((.*)\)( STRICT)?$", stmt)
if match:
name, body, strict = match.groups()
objects[f"table {name}"] = {
"items": sorted(split_table(body)),
"strict": bool(strict),
}
continue
match = re.match(r"CREATE (?:UNIQUE )?INDEX (\w+)", stmt)
if match:
objects[f"index {match.group(1)}"] = {"sql": stmt}
continue
objects[stmt[:60]] = {"sql": stmt}
return objects
def format_diff(documented, real):
if "items" in documented and "items" in real:
lines = []
# disregards order
for item in sorted(set(documented["items"]) - set(real["items"])):
lines.append(f" documented but not in the database: {item}")
for item in sorted(set(real["items"]) - set(documented["items"])):
lines.append(f" in the database but not documented: {item}")
if documented["strict"] != real["strict"]:
lines.append(f" STRICT: documented={documented['strict']} actual={real['strict']}")
return "\n".join(lines)
return f" documented: {documented}\n actual: {real}"
def read_database_schema(dbfile):
with sqlite3.connect(f"file:{dbfile}?mode=ro", uri=True) as conn:
rows = conn.execute(
"SELECT sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'",
).fetchall()
return ";\n".join(row[0] for row in rows)
# Tables where every column carries a comment in docs/schema.sql.
# Opt-in: document a table's columns, then add it here to lock it in.
FULLY_DOCUMENTED_TABLES = {
"contacts",
"imap_markseen",
"multi_device_sync",
"transports",
}
def undocumented_columns(sql, tables):
result = []
table = None
documented = False
for line in sql.splitlines():
code, _, comment = line.strip().partition("--")
code = code.strip()
if code.startswith("CREATE TABLE "):
name = code.removeprefix("CREATE TABLE ").partition("(")[0].strip()
table = name if name in tables else None
assert not table or code.endswith("("), f"{code}: want one column per line"
elif code.startswith(")"):
table = None
elif table and code and not re.match(r"(UNIQUE|PRIMARY|FOREIGN|CHECK)\b", code):
column = re.match(r"(\w+) (?!INTEGER PRIMARY KEY)", code)
if column and not documented and not comment:
result.append(f"{table}.{column.group(1)}")
documented = bool(comment) and not code
return result
def test_documented_tables_stay_documented():
missing = undocumented_columns(DOC_PATH.read_text(), FULLY_DOCUMENTED_TABLES)
assert not missing, "columns without a comment in docs/schema.sql:\n" + "\n".join(missing)
def test_documented_schema_matches_database(acf):
account = acf.get_unconfigured_account()
real = parse_schema(read_database_schema(account.get_info()["database_dir"]))
documented = parse_schema(DOC_PATH.read_text())
problems = []
for name in sorted(real.keys() - documented.keys()):
problems.append(f"{name} exists in the database but is not documented")
for name in sorted(documented.keys() - real.keys()):
problems.append(f"{name} is documented but does not exist in the database")
for name in sorted(documented.keys() & real.keys()):
if documented[name] != real[name]:
problems.append(f"{name} differs:\n{format_diff(documented[name], real[name])}")
assert not problems, "documented schema deviates from the database:\n" + "\n".join(problems)

View File

@@ -1,5 +1,5 @@
def test_vcard(acf) -> None:
alice, bob, fiona = acf.get_online_accounts(3)
def test_vcard(acfactory) -> None:
alice, bob, fiona = acfactory.get_online_accounts(3)
bob.create_chat(alice)
alice_contact_bob = alice.create_contact(bob, "Bob")

View File

@@ -1,9 +1,9 @@
def test_webxdc(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
def test_webxdc(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
event = bob.wait_for_incoming_msg_event()
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
@@ -21,7 +21,7 @@ def test_webxdc(acf, rpcdata) -> None:
"isAppSender": False,
"isBroadcast": False,
"sendUpdateInterval": 1000,
"sendUpdateMaxSize": 2**20 * (30 - 1) * 3 // 4,
"sendUpdateMaxSize": 18874368,
}
status_updates = message.get_webxdc_status_updates()
@@ -43,12 +43,12 @@ def test_webxdc(acf, rpcdata) -> None:
]
def test_webxdc_insert_lots_of_updates(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
for i in range(2000):
message.send_webxdc_status_update({"payload": str(i)}, "description")

View File

@@ -1,8 +1,9 @@
[package]
name = "deltachat-rpc-server"
version = "2.61.0-dev"
version = "2.51.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2024"
edition = "2021"
readme = "README.md"
license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]

View File

@@ -11,7 +11,7 @@ Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`
To install from source run:
```sh
cargo install --locked --git https://github.com/chatmail/core/ deltachat-rpc-server
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
```
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.61.0-dev"
"version": "2.51.0-dev"
}

View File

@@ -56,7 +56,7 @@ for (const { folder_name, package_name } of platform_package_names) {
if (is_local) {
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
`file:${join(expected_cwd, "/../../deltachat-jsonrpc-bindings/typescript")}`;
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
} else {
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
}

View File

@@ -2,7 +2,7 @@
import { ENV_VAR_NAME } from "./const.js";
const cargoInstallCommand =
"cargo install --locked --git https://github.com/chatmail/core deltachat-rpc-server";
"cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
return `deltachat-rpc-server not found:

View File

@@ -6,7 +6,7 @@ use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{anyhow, Context as _, Result};
use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;

View File

@@ -2,7 +2,7 @@
name = "deltachat-time"
version = "1.0.0"
description = "Time-related tools"
edition = "2024"
edition = "2021"
license = "MPL-2.0"
[dependencies]

View File

@@ -1,12 +1,12 @@
[package]
name = "deltachat_derive"
version = "2.0.0"
edition = "2024"
edition = "2018"
license = "MPL-2.0"
[lib]
proc-macro = true
[dependencies]
syn = "3"
syn = "2"
quote = "1"

View File

@@ -14,7 +14,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let q = quote! {
let gen = quote! {
impl rusqlite::types::ToSql for #name {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let num = *self as i64;
@@ -24,7 +24,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
}
}
};
q.into()
gen.into()
}
#[proc_macro_derive(FromSql)]
@@ -32,7 +32,7 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let q = quote! {
let gen = quote! {
impl rusqlite::types::FromSql for #name {
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let inner = rusqlite::types::FromSql::column_result(col)?;
@@ -44,5 +44,5 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
}
}
};
q.into()
gen.into()
}

Some files were not shown because too many files have changed in this diff Show More