Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Laux
907728731e feat: webxdc: fallback to index.html if it exists when loading directory
This makes it easier for developers and users that port webapps or even
vibecode normal websites. Those may rely on the webserver behavior of
loading an index.html when navigating to a directory.

Before this pr this did not work, with this pr we have a better out of
the box experience.

Still we may want to document that links ending in `dir/index.html` are
more reliable than just linking to `dir/` - atleast in other host app
implementations
2026-06-03 19:22:28 +02:00
257 changed files with 13362 additions and 15349 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@6c8f9facfa5047ec02d8485b6bf52b587b7777d1
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@213ccc1a076163c093f914550b94feb90fab916d
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,492 +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))
### 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
@@ -8784,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)

519
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 }
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",
@@ -184,7 +184,6 @@ base64 = "0.22"
chrono = { version = "0.4.44", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat-jsonrpc-bindings = { path = "deltachat-jsonrpc-bindings", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.32"
futures-lite = "2.6.1"
@@ -204,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())

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,8 @@
[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"

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;
@@ -461,10 +462,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
* This is deprecated, UIs should not behave differently
* for chatmail relays and classical email servers.
* Most usages in UIs can be replaced by `force_encryption`.
* - `is_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()`,
@@ -502,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)
@@ -526,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.
*
@@ -589,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
@@ -601,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.
@@ -636,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
/**
@@ -662,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,
@@ -870,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);
@@ -1174,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.
@@ -1580,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,
@@ -1598,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
@@ -1627,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
@@ -2065,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.
*
@@ -2627,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);
@@ -3188,22 +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 from all transports at once and then resumes the scheduler.
* The fetch for an account ends as soon as one of its transports received messages.
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
*
* dc_accounts_background_fetch() was created for the iOS Background fetch.
*
* 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 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);
@@ -3559,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:
*
@@ -3643,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.
*
@@ -3660,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
@@ -3674,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.
@@ -3741,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
@@ -3782,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
@@ -4321,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.
@@ -4464,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.
@@ -4524,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
/**
@@ -4842,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().
*
@@ -4883,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
*
@@ -5180,6 +5227,98 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
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
*
@@ -5493,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.
@@ -5594,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
*
@@ -5681,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().
@@ -6168,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.
@@ -6366,14 +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.
* 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
*/
@@ -6469,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.
*
@@ -6481,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
@@ -6505,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
*
@@ -6680,7 +6895,10 @@ void dc_event_unref(dc_event_t* event);
/// 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."
@@ -6725,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
@@ -7165,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.
@@ -7284,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,8 +282,7 @@ impl CommandApi {
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// 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 notifications caused by timing race conditions.
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
@@ -327,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?;
@@ -336,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?;
@@ -503,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,
@@ -527,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()
@@ -539,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?;
@@ -802,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,
@@ -886,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?;
@@ -966,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
@@ -1023,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)
@@ -1294,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.
///
@@ -1332,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(
@@ -1343,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 },
@@ -1354,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())
@@ -1392,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 },
@@ -1458,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,
@@ -1491,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
@@ -1856,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(
@@ -2039,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.
@@ -2236,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,
@@ -2249,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))
}
@@ -2419,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,
@@ -2751,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

@@ -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.
@@ -394,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.
///
@@ -482,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,
}
@@ -561,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

@@ -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!(
"--------------------------------------------------------------------------------"
);
@@ -319,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\
@@ -395,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\
@@ -616,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 {
@@ -630,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();
@@ -1203,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",
@@ -237,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",
@@ -245,6 +248,7 @@ const MISC_COMMANDS: [&str; 13] = [
"joinqr",
"setqr",
"createqrsvg",
"providerinfo",
"fileinfo",
"estimatedeletion",
"clear",
@@ -266,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());
}
}
}
}
@@ -422,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

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"
@@ -97,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"
@@ -231,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."""
@@ -246,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

@@ -67,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

@@ -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:
@@ -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")

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

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
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) -> [Account, Account, Account]:
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) -> [Account, Account, A
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)
@@ -37,38 +36,16 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
# 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()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
remote_eval("bob.wait_for_securejoin_joiner_success()")
# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
def test_send_and_receive_message(alice_and_remote_bob) -> None:
"""Test other-core Bob profile can send a message to Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.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')")
@@ -76,64 +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"

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,23 +18,18 @@ 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:
def test_change_address(acfactory) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob.create_chat(alice)
@@ -68,7 +44,7 @@ def test_change_address(acf) -> None:
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):
@@ -85,6 +61,8 @@ def test_change_address(acf) -> None:
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!")
@@ -98,18 +76,18 @@ def test_change_address(acf) -> None:
assert sender_addr2 == 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
@@ -125,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):
@@ -142,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")
@@ -167,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
@@ -190,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
@@ -209,12 +191,11 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport2["addr"])
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
log.section("ac1_clone receives a message via the new transport")
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
@@ -224,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"]
@@ -240,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
@@ -256,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")
@@ -301,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()
@@ -314,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()
@@ -322,64 +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
assert msg2.chat == alice.create_chat(bob)

View File

@@ -7,8 +7,8 @@ from deltachat_rpc_client.const import ChatType
from deltachat_rpc_client.rpc import JsonRpcError
def test_qr_setup_contact(acf, tmp_path) -> 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)
@@ -31,7 +31,7 @@ def test_qr_setup_contact(acf, tmp_path) -> None:
# backwards verification is not lost
# because default key is not changed.
logging.info("Bob 2 is created")
bob2 = acf.new_configured_account()
bob2 = acfactory.new_configured_account()
bob2.export_self_keys(tmp_path)
logging.info("Bob tries to import a key")
@@ -44,8 +44,8 @@ def test_qr_setup_contact(acf, tmp_path) -> None:
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()
@@ -59,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.
@@ -87,7 +87,7 @@ def test_qr_securejoin(acf):
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)
@@ -111,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()
@@ -252,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()
@@ -278,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()
@@ -335,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_verified_group_member_added_recovery(acf) -> None:
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 = acf.get_online_accounts(3)
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates a group")
chat = ac1.create_group("Group")
@@ -374,7 +374,7 @@ def test_verified_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 reverifies with ac3")
qr_code = ac3.get_qr_code()
@@ -425,11 +425,11 @@ def test_verified_group_member_added_recovery(acf) -> None:
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: verify with ac2")
qr_code = ac2.get_qr_code()
@@ -484,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 verified group QR code created by ac1.
This results in creation of a blocked single chat with ac1 on ac2,
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)
@@ -513,11 +513,11 @@ def test_qr_new_group_unblocked(acf):
@pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow_verified(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 verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello")
@@ -555,8 +555,8 @@ def test_aeap_flow_verified(acf):
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
def test_gossip_verification(acf) -> None:
alice, bob, carol = acf.get_online_accounts(3)
def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
# Bob verifies Alice.
qr_code = alice.get_qr_code()
@@ -605,13 +605,13 @@ def test_gossip_verification(acf) -> None:
assert not carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acf) -> None:
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 = acf.get_online_accounts(3)
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Group")
@@ -623,7 +623,7 @@ def test_securejoin_after_contact_resetup(acf) -> None:
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 verifies ac1
@@ -636,7 +636,7 @@ def test_securejoin_after_contact_resetup(acf) -> None:
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
@@ -668,8 +668,8 @@ def test_securejoin_after_contact_resetup(acf) -> None:
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")
@@ -682,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()
@@ -704,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,9 +393,9 @@ 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})
@@ -448,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()
@@ -474,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()
@@ -519,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)
@@ -543,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()
@@ -590,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")
@@ -610,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
@@ -661,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])
@@ -698,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!")
@@ -711,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()
@@ -746,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")
@@ -783,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")
@@ -822,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))
@@ -846,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")
@@ -896,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")
@@ -932,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"
@@ -961,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()
@@ -985,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")
@@ -1008,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
@@ -1030,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)
@@ -1053,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
@@ -1080,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()
@@ -1108,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()
@@ -1125,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)
@@ -1140,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)
@@ -1156,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()
@@ -1257,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")
@@ -1281,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")
@@ -1317,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)
@@ -1354,8 +1312,8 @@ def test_background_fetch(acf, dc):
break
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!")
@@ -1373,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
@@ -1381,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")
@@ -1417,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()
@@ -1435,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,8 @@
[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"

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()
}

View File

@@ -18,11 +18,6 @@ ignore = [
# this should be fixed by upgrading to iroh 1.0 once it is released.
"RUSTSEC-2025-0134",
# Unmaintained proc-macro-error2
# Transitive dependency of typescript-type-def 0.5.13.
# <https://rustsec.org/advisories/RUSTSEC-2026-0173>
"RUSTSEC-2026-0173",
# rustls-webpki v0.102.8
# We cannot upgrade to >=0.103.10 because
# it is a transitive dependency of iroh 0.35.0
@@ -70,7 +65,6 @@ skip = [
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "0.2.12" },
{ name = "getrandom", version = "0.3.3" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "hybrid-array", version = "0.2.3" },
@@ -81,18 +75,14 @@ skip = [
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "r-efi", version = "5.2.0" },
{ name = "rustix", version = "0.38.44" },
{ name = "rustls-webpki", version = "0.102.8" },
{ name = "serdect", version = "0.2.0" },
{ name = "snafu-derive", version = "0.8.5" },
{ name = "snafu", version = "0.8.5" },
{ name = "socket2", version = "0.5.9" },
{ name = "spin", version = "0.9.8" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },
{ name = "syn", version = "2.0.118" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },

View File

@@ -1,783 +0,0 @@
-- Commented SQLite database schema.
--
-- This file should only be used for documentation,
-- do not run this SQL e.g. to create databases.
-- This is because we want to be 100% sure
-- that new users and users who run the migrations
-- get the same database schema.
--
-- This is a dump of the database schema using `sqlite3 dc.db .schema`,
-- formatted, reordered and commented afterwards.
-- Raw dump of the database schema does not have comments
-- for deprecated columns and columns added by migrations.
CREATE TABLE config (
id INTEGER PRIMARY KEY,
keyname TEXT UNIQUE,
value TEXT NOT NULL
);
CREATE INDEX config_index1 ON config (keyname);
CREATE TABLE contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Name of the contact as set by the user.
name TEXT DEFAULT '',
-- Normalized name for search.
name_normalized TEXT,
-- Email address last seen as the From address for this contact.
-- For address-contacts that have empty "fingerprint" this should never change.
-- For key-contacts this address may change when a new signed message is received.
--
-- This is the address messages are sent to unless the key
-- advertises a different set of addresses, in which case
-- this address should be ignored
-- (and not appended to the list of addresses advertised in the key).
addr TEXT DEFAULT '' COLLATE NOCASE,
-- The origin or source of the contact,
-- e.g. whether the contact was added from some chat
-- or via SecureJoin.
origin INTEGER DEFAULT 0,
-- True if the contact is blocked.
-- Unlike the chat, contact is either blocked or not,
-- there is no third "contact request" state.
blocked INTEGER DEFAULT 0,
-- Timestamp of the last time any message was received from this contact.
last_seen INTEGER DEFAULT 0,
-- Key-value parameters.
param TEXT DEFAULT '',
-- Name of the contact as sent by the contact itself.
authname TEXT DEFAULT '',
-- Timestamp of the last time we have sent our avatar to this contact.
-- Used to decide whether to send the avatar.
-- Normally avatars are resent after 14 days.
-- This column is reset to 0 when avatar is changed.
selfavatar_sent INTEGER DEFAULT 0,
-- Last seen message signature from this contact, also known as bio in the UI.
status TEXT DEFAULT '',
-- True if the contact is a bot.
is_bot INTEGER NOT NULL DEFAULT 0,
-- OpenPGP key fingerprint for "key-contacts",
-- empty string for "address-contacts".
fingerprint TEXT NOT NULL DEFAULT '',
-- ID of the contact that has "introduced" us to this contact
-- by sharing the key with a verified attribute or in a "verified" chat.
verifier INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);
CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint);
CREATE TABLE chats (
-- Chat ID 0 should never be used as it is used as a sentinel value in some APIs.
--
-- Chat IDs 1 to 9, including 9, are reserved.
-- The first proper chat gets ID 10, but may not exist if it is deleted.
--
-- Chat ID 3 is the trash chat and this chat ID is assigned to deleted messages
-- to create "tombstones".
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Chat type, e.g. 100 for single chat, 120 for a group etc.
-- Check the Chattype enumeration in the code for concrete values.
type INTEGER DEFAULT 0,
name TEXT DEFAULT '',
-- Normalized name for search.
name_normalized TEXT,
-- 0 for visible chat, 1 for hidden and 2 for contact request.
-- 1 does not necessarily mean that the contact is blocked.
blocked INTEGER DEFAULT 0,
-- Chat-Group-ID header for encrypted groups and channels.
-- Empty for unencrypted groups and single chats.
grpid TEXT DEFAULT '',
-- Key-value parameters.
param TEXT DEFAULT '',
-- Chat visibility.
-- 0 for normal, 1 for archived and 2 for pinned chats.
archived INTEGER DEFAULT 0,
locations_send_begin INTEGER DEFAULT 0,
locations_send_until INTEGER DEFAULT 0,
locations_last_sent INTEGER DEFAULT 0,
-- Time when the chat was created.
-- Used for sorting in the chatlist when the chat has no messages.
created_timestamp INTEGER DEFAULT 0,
-- 0 if the chat is not muted.
-- -1 if the chat is muted forever.
-- Otherwise the timestamp until which the chat is muted.
muted_until INTEGER DEFAULT 0,
-- Disappearing messages timer.
-- 0 means the timer is disabled.
ephemeral_timer INTEGER,
-- Deprecated, but still used to send Chat-Verified headers
-- for existing protected chats.
-- All new chats are created as "not protected".
protected INTEGER DEFAULT 0,
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table
-- Unused columns, drafts are now tracked as separate
-- messages with a special msgs.state value.
draft_timestamp INTEGER DEFAULT 0,
draft_txt TEXT DEFAULT ''
);
CREATE INDEX chats_index1 ON chats (grpid);
CREATE INDEX chats_index2 ON chats (archived);
CREATE INDEX chats_index3 ON chats (locations_send_until);
CREATE INDEX chats_index4 ON chats (name);
CREATE TABLE chats_descriptions (
chat_id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL DEFAULT ''
) STRICT;
-- Chat member lists.
-- Saved messages has only self.
-- Single chats have only the contact, but not self.
-- Groups have self if we are part of the group.
CREATE TABLE chats_contacts (
chat_id INTEGER,
contact_id INTEGER,
add_timestamp NOT NULL DEFAULT 0,
remove_timestamp NOT NULL DEFAULT 0,
UNIQUE(chat_id, contact_id)
);
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
-- This table contains "message bubbles" that are normally visible
-- and "tombstones" that are put into the trash chat
-- or have a "hidden" column set to the true value.
--
-- Tombstones are used to avoid downloading and processing
-- the same messages twice, e.g. when the message is deleted
-- but another copy of it arrives later.
CREATE TABLE msgs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- The messages may be split into pre- and post-message.
-- Pre-messages have a Chat-Post-Message-ID header
-- which contains the Message-ID of the post-message.
-- Post-messages have a Chat-Is-Post-Message header.
--
-- For outgoing messages that are split into pre-message
-- and post-message, rfc724_mid contains the Message-ID
-- of the post-message, and pre_rfc724_mid
-- contains the Message-ID of the pre-message.
--
-- For incoming messages, when a pre-message arrives,
-- rfc724_mid is set to the Message-ID of the post-message,
-- and pre_rfc724_mid is set to the Message-ID of the arrived pre-messsage.
-- For other messages rfc724_mid is taken from the Message-ID
-- and pre_rfc724_mid is set to empty string.
-- Message-ID as defined in RFC 724 (now replaced by RFC 5322)
rfc724_mid TEXT DEFAULT '',
-- Message-ID of the pre-message.
pre_rfc724_mid TEXT DEFAULT '',
-- Chat ID.
--
-- Chat ID 3 is the trash chat, messages with this ID are tombstones.
chat_id INTEGER DEFAULT 0,
-- Contact ID of the message author.
from_id INTEGER DEFAULT 0,
-- Mostly unused Contact ID of the first recipient or 0.
-- For info messages set to ContactID::INFO (2).
--
-- If to_id is set to 2, the message must be displayed
-- as an info message even if from_id is not set to 2.
--
-- Info messages generated by Webxdc status updates
-- have from_id set to the contact ID of the sender
-- and to_id set to 2.
-- from_id is then used to collapse series of updates
-- into a single message (see test_webxdc_info_msg_cleanup_series),
-- while to_id of 2 makes sure the message is displayed
-- as an info message.
to_id INTEGER DEFAULT 0,
-- Message viewtype.
-- 10 is a text message,
-- 20 is an image etc.
type INTEGER DEFAULT 0,
-- Message state,
-- e.g. 10 for fresh incoming messages,
-- 26 for outgoing delivered message,
-- 28 for outgoing message that got a read receipt.
--
-- Messages sent by self, but arriving from a second device
-- are still considered "outgoing".
state INTEGER DEFAULT 0,
-- Size of the attachment for file parts, otherwise 0.
bytes INTEGER DEFAULT 0,
txt TEXT DEFAULT '', -- Message text for display.
txt_normalized TEXT, -- Message text normalized for search.
txt_raw TEXT DEFAULT '', -- deprecated 2025-03-29
-- Key-value parameters.
param TEXT DEFAULT '',
-- For messages bookmarked into the Saved Messages chat,
-- ID of the original message, making it possible to jump to it.
starred INTEGER DEFAULT 0,
-- True if the message is pinned.
pinned INTEGER NOT NULL DEFAULT 0,
timestamp INTEGER DEFAULT 0, -- Timestamp of the message used for sorting.
timestamp_sent INTEGER DEFAULT 0, -- Timestamp of the message as sent in the Date header.
timestamp_rcvd INTEGER DEFAULT 0,
-- True if the message should not be displayed.
-- Most messages that should not be displayed
-- better go to the trash chat (ID 3),
-- but at least reactions are hidden
-- so they still belong to the chat
-- and can be marked as noticed when the chat is opened.
hidden INTEGER DEFAULT 0,
-- mime_headers column actually contains BLOBs, i.e. it may
-- contain non-UTF8 MIME messages. TEXT was a bad choice, but
-- thanks to SQLite 3 being dynamically typed, there is no need to
-- change column type.
mime_headers TEXT,
-- True if mime_headers column is compressed with Brotli.
mime_compressed INTEGER NOT NULL DEFAULT 0,
mime_modified INTEGER DEFAULT 0,
mime_in_reply_to TEXT,
mime_references TEXT,
-- Location ID for POI locations manually placed on the map.
location_id INTEGER DEFAULT 0,
error TEXT DEFAULT '',
-- Timer value in seconds. For incoming messages this
-- timer starts when message is read, so we want to have
-- the value stored here until the timer starts.
ephemeral_timer INTEGER DEFAULT 0,
-- Timestamp indicating when the message should be
-- deleted. It is convenient to store it here because UI
-- needs this value to display how much time is left until
-- the message is deleted.
ephemeral_timestamp INTEGER DEFAULT 0,
subject TEXT DEFAULT '',
-- Download state for the message.
-- 0 for most messages, meaning the message is fully downloaded.
-- Otherwise 10 for the message available for download etc.
download_state INTEGER DEFAULT 0,
-- Information extracted from Received headers
-- as a plain text for debugging.
hop_info TEXT,
-- True if the message is deleted on the server.
-- If this is true and another copy of the message arrives
-- it should be deleted from the server as well.
deleted INTEGER NOT NULL DEFAULT 0,
--
-- Unused columns.
--
-- Always 1 for new messages.
-- Previously:
-- 0 if the message is a non-chat message,
-- 1 if the message is a chat message,
-- 2 if the message is a non-chat reply to a chat message
msgrmsg INTEGER DEFAULT 1,
server_folder TEXT DEFAULT '', -- Deprecated column that was used before "imap" table, replaced by imap.folder
server_uid INTEGER DEFAULT 0, -- Deprecated column that was used before "imap" table, replaced by imap.uid
-- Unused column formely used to mark the messages
-- that should be moved to the dedicated IMAP folder.
-- It was replaced with imap.target, which is also now
-- used only to mark the messages on IMAP for deletion.
move_state INTEGER DEFAULT 1
);
CREATE INDEX msgs_index1 ON msgs (rfc724_mid);
CREATE INDEX msgs_index2 ON msgs (chat_id);
CREATE INDEX msgs_index3 ON msgs (timestamp);
CREATE INDEX msgs_index4 ON msgs (state);
CREATE INDEX msgs_index5 ON msgs (starred);
CREATE INDEX msgs_index6 ON msgs (location_id);
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id, timestamp);
CREATE INDEX msgs_index8 ON msgs (ephemeral_timestamp);
CREATE INDEX msgs_index9 ON msgs (pre_rfc724_mid);
CREATE INDEX msgs_index10 ON msgs (pinned) WHERE pinned=1;
CREATE TABLE leftgrps (
id INTEGER PRIMARY KEY,
grpid TEXT DEFAULT ''
);
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
CREATE TABLE msgs_mdns (
msg_id INTEGER,
contact_id INTEGER,
timestamp_sent INTEGER DEFAULT 0
);
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL DEFAULT 0.0,
longitude REAL DEFAULT 0.0,
accuracy REAL DEFAULT 0.0,
timestamp INTEGER DEFAULT 0,
chat_id INTEGER DEFAULT 0,
from_id INTEGER DEFAULT 0,
-- If true, the location is an independent POI
-- and should not be part of the path.
independent INTEGER DEFAULT 0
);
CREATE INDEX locations_index1 ON locations (from_id);
CREATE INDEX locations_index2 ON locations (timestamp);
CREATE TABLE devmsglabels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT,
msg_id INTEGER DEFAULT 0
);
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
-- Table to store QR code tokens for SecureJoin protocol.
CREATE TABLE tokens (
id INTEGER PRIMARY KEY,
-- Namespace is one of:
-- 0 - unknown
-- 100 - invite number
-- 110 - auth
namespc INTEGER NOT NULL,
foreign_key TEXT DEFAULT '' NOT NULL,
token TEXT NOT NULL UNIQUE,
timestamp INTEGER DEFAULT 0 NOT NULL
) STRICT;
-- State of the scanner of the QR code (Bob) in SecureJoin protocol.
CREATE TABLE bobstate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invite TEXT NOT NULL,
next_step INTEGER NOT NULL,
chat_id INTEGER NOT NULL
);
CREATE TABLE smtp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rfc724_mid TEXT NOT NULL, -- Message-ID
mime TEXT NOT NULL, -- SMTP payload
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
recipients TEXT NOT NULL, -- List of recipients separated by space
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
);
CREATE TABLE smtp_mdns (
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination
rfc724_mid TEXT NOT NULL, -- Message-ID header
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN
);
CREATE TABLE smtp_status_updates (
msg_id INTEGER NOT NULL UNIQUE, -- msg_id of the webxdc instance with pending updates
first_serial INTEGER NOT NULL, -- id in msgs_status_updates
last_serial INTEGER NOT NULL, -- id in msgs_status_updates
descr TEXT NOT NULL -- text to send along with the updates
);
-- Table of "sync items" to be grouped into sync messages
-- and sent to own devices.
CREATE TABLE multi_device_sync (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- JSON of the "sync item".
item TEXT DEFAULT ''
);
CREATE TABLE reactions (
msg_id INTEGER NOT NULL, -- id of the message reacted to
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
PRIMARY KEY(msg_id, contact_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
);
CREATE INDEX reactions_index1 ON reactions (msg_id);
CREATE TABLE pending_reactions (
rfc724_mid TEXT NOT NULL,
contact_id INTEGER NOT NULL,
reaction TEXT NOT NULL,
timestamp INTEGER NOT NULL,
PRIMARY KEY(rfc724_mid, contact_id),
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE
) STRICT;
-- accumulated reactions received from broadcast owner.
-- pairs of reaction and their counts.
-- these pairs are given to the UI (for non-broadcasts, the pairs are calculated from the `reactions` table)
CREATE TABLE broadcasted_reactions (
msg_id INTEGER NOT NULL DEFAULT 0,
reaction TEXT NOT NULL DEFAULT '',
count INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
) STRICT;
CREATE INDEX broadcasted_reactions_index1 ON broadcasted_reactions (msg_id);
-- messages that received reactions from broadcast subscriber to broadcast owner.
-- the broadcast owner will send them, accumulated by chat_id,
-- to all other subscribers every some minutes, and then remove all entries with the chat_id processed.
CREATE TABLE reactions_need_broadcast (
chat_id INTEGER NOT NULL DEFAULT 0,
msg_id INTEGER NOT NULL DEFAULT 0,
UNIQUE (chat_id, msg_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
) STRICT;
CREATE INDEX reactions_need_broadcast_index1 ON reactions_need_broadcast (chat_id);
CREATE TABLE connection_history (
host TEXT NOT NULL, -- server hostname
port INTEGER NOT NULL, -- server port
alpn TEXT NOT NULL, -- ALPN such as smtp or imap
addr TEXT NOT NULL, -- IP address
timestamp INTEGER NOT NULL, -- timestamp of the most recent successful connection
UNIQUE (host, port, alpn, addr)
) STRICT;
CREATE TABLE dns_cache (
hostname TEXT NOT NULL,
address TEXT NOT NULL, -- IPv4 or IPv6 address
timestamp INTEGER NOT NULL,
UNIQUE (hostname, address)
);
CREATE TABLE tls_spki (
host TEXT NOT NULL UNIQUE,
spki_hash TEXT NOT NULL, -- base64 of SPKI SHA-256 hash
timestamp INTEGER NOT NULL -- timestamp of the last time we have seen this key
) STRICT;
CREATE INDEX tls_spki_index_timestamp ON tls_spki (timestamp);
CREATE TABLE http_cache (
url TEXT PRIMARY KEY,
expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
stale INTEGER NOT NULL, -- When the cache entry is considered stale, timestamp in seconds.
blobname TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
) STRICT;
-- Webxdc updates.
CREATE TABLE msgs_status_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
msg_id INTEGER,
update_item TEXT DEFAULT '',
uid TEXT UNIQUE,
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE
);
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
-- Webxdc realtime.
CREATE TABLE iroh_gossip_peers (
msg_id INTEGER not NULL,
topic BLOB NOT NULL,
public_key BLOB NOT NULL,
relay_server TEXT, UNIQUE (topic, public_key),
PRIMARY KEY(topic, public_key)
) STRICT;
--
-- OpenPGP.
--
-- Storage for own private keys.
-- Only one of the keys is used.
--
-- In the past mulitple keys could be imported,
-- so this table can contain multiple keys for existing users.
-- Used key is identified by the "key_id" config value.
-- Other keys must never be used, even for decryption.
CREATE TABLE keypairs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- OpenPGP private key stored as a binary blob.
private_key UNIQUE NOT NULL,
-- Unused public key aka OpenPGP certificate.
-- Stored only for compatibility.
-- OpenPGP certificate is generated at runtime from the private key.
public_key UNIQUE NOT NULL,
--
-- Unused columns.
--
--
-- Columns "addr", "is_default" and "created" were
-- dropped in migration 107
-- but added back for compatibility in migration 110.
addr TEXT DEFAULT '' COLLATE NOCASE,
-- Migrated into "key_id" config value in migration 107.
is_default INTEGER DEFAULT 0,
created INTEGER DEFAULT 0
);
CREATE TABLE public_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key.
public_key BLOB NOT NULL -- Binary key, not ASCII-armored
) STRICT;
CREATE INDEX public_key_index ON public_keys (fingerprint);
CREATE TABLE gossip_timestamp (
chat_id INTEGER NOT NULL,
fingerprint TEXT NOT NULL, -- Upper-case fingerprint of the key.
timestamp INTEGER NOT NULL,
UNIQUE (chat_id, fingerprint)
) STRICT;
CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
-- Timestamps of distributing own key in the Autocrypt header of MDNs.
CREATE TABLE mdn_autocrypt_timestamp (
fingerprint TEXT PRIMARY KEY NOT NULL, -- Upper-case fingerprint of the recipient key.
attached_timestamp INTEGER NOT NULL
) STRICT;
-- Passwords used to derive symmetric keys for broadcast lists aka channels.
CREATE TABLE broadcast_secrets(
chat_id INTEGER PRIMARY KEY NOT NULL,
secret TEXT NOT NULL
) STRICT;
-- Candidate chatmail relays for automatic relay management.
CREATE TABLE relay_candidates(
host TEXT PRIMARY KEY NOT NULL,
last_tried INTEGER NOT NULL DEFAULT 0 -- Timestamp of the last connection attempt.
) STRICT;
CREATE TABLE transports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Email address associated with this transport.
-- It uniquely identifies the transport.
addr TEXT NOT NULL,
-- JSON with the settings entered by the user.
-- The settings are not necessary entered manually,
-- but can be entered by scanning the QR code.
-- These settings are only used during the transport configuration process.
entered_param TEXT NOT NULL,
-- JSON with the settings used to connect to the transport.
-- These settings are derived from entered parameters
-- and possibly autoconfiguration XML fetched over HTTPS.
-- The settings stored here are known to have worked at least once,
-- this is ensured during configuration.
configured_param TEXT NOT NULL,
-- Bumping this forces re-signing the key:
-- the relay-list signature is created at MAX(add_timestamp, remove_timestamp)
-- over this table and `removed_transports`, and contacts keep the newest one.
add_timestamp INTEGER NOT NULL DEFAULT 0,
-- Unused since migration 165, which removed unpublished transports.
is_published INTEGER DEFAULT 1 NOT NULL,
-- Time when the transport was last used to receive a message.
last_rcvd_timestamp INTEGER NOT NULL DEFAULT 0,
UNIQUE(addr)
);
CREATE TABLE removed_transports (
addr TEXT NOT NULL,
remove_timestamp INTEGER NOT NULL,
UNIQUE(addr)
) STRICT;
CREATE TABLE imap (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
rfc724_mid TEXT NOT NULL, -- Message-ID header
folder TEXT NOT NULL, -- IMAP folder
target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
uid INTEGER NOT NULL, -- UID
uidvalidity INTEGER NOT NULL,
UNIQUE (transport_id, folder, uid, uidvalidity)
) STRICT;
CREATE INDEX imap_folder ON imap(transport_id, folder);
CREATE INDEX imap_rfc724_mid ON imap(transport_id, rfc724_mid);
CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid);
CREATE TABLE imap_markseen (
id INTEGER PRIMARY KEY NOT NULL,
FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
);
CREATE TABLE imap_sync (
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
folder TEXT NOT NULL,
uidvalidity INTEGER NOT NULL DEFAULT 0,
uid_next INTEGER NOT NULL DEFAULT 0,
modseq INTEGER NOT NULL DEFAULT 0,
UNIQUE (transport_id, folder)
) STRICT;
CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
CREATE TABLE download (
rfc724_mid TEXT PRIMARY KEY,
msg_id INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE available_post_msgs (
rfc724_mid TEXT PRIMARY KEY
) STRICT;
--
-- Statistics.
--
CREATE TABLE stats_securejoin_sources(
source INTEGER PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE stats_securejoin_uipaths(
uipath INTEGER PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE stats_securejoin_invites(
already_existed INTEGER NOT NULL,
already_verified INTEGER NOT NULL,
type TEXT NOT NULL
) STRICT;
CREATE TABLE stats_msgs(
chattype INTEGER PRIMARY KEY,
verified INTEGER NOT NULL DEFAULT 0,
unverified_encrypted INTEGER NOT NULL DEFAULT 0,
unencrypted INTEGER NOT NULL DEFAULT 0,
only_to_self INTEGER NOT NULL DEFAULT 0,
last_counted_msg_id INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE stats_sending_enabled_events(timestamp INTEGER NOT NULL) STRICT;
CREATE TABLE stats_sending_disabled_events(timestamp INTEGER NOT NULL) STRICT;
-- Deprecated and unused tables.
--
-- We don't immediately drop unused tables,
-- because we want users to be able to downgrade.
-- Deprecated table for Autocrypt peer states.
-- It is replaced by the new "public_keys" table in migration 132
-- that introduced "key contacts" which have a fixed fingerprint.
CREATE TABLE acpeerstates (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
last_seen INTEGER DEFAULT 0,
last_seen_autocrypt INTEGER DEFAULT 0,
public_key,
prefer_encrypted INTEGER DEFAULT 0,
gossip_timestamp INTEGER DEFAULT 0,
gossip_key,
public_key_fingerprint TEXT DEFAULT '',
gossip_key_fingerprint TEXT DEFAULT '',
verified_key,
verified_key_fingerprint TEXT DEFAULT '',
verifier TEXT DEFAULT '',
secondary_verified_key,
secondary_verified_key_fingerprint TEXT DEFAULT '',
secondary_verifier TEXT DEFAULT '',
backward_verified_key_id -- What we think the contact has as our verified key
INTEGER,
UNIQUE (addr) -- Only one peerstate per address
);
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
-- Deprecated table previously used to upload sync messages using IMAP APPEND.
-- Sync messages are sent over SMTP now as they should be sent to multiple transports.
CREATE TABLE imap_send (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mime TEXT NOT NULL, -- Message content
msg_id INTEGER NOT NULL, -- ID of the message in the `msgs` table
attempts INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
);
-- Deprecated table used for a job system.
CREATE TABLE jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
added_timestamp INTEGER,
desired_timestamp INTEGER DEFAULT 0,
action INTEGER,
foreign_id INTEGER,
param TEXT DEFAULT '',
thread INTEGER DEFAULT 0,
tries INTEGER DEFAULT 0
);
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
-- Backup of the keypairs left after migration 107.
-- Not used by any code, it was only for a recovery
-- in case migration 107 goes wrong.
CREATE TABLE old_keypairs (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
is_default INTEGER DEFAULT 0,
private_key,
public_key,
created INTEGER DEFAULT 0
);
-- Unused table, previously introduced for AEAP mechanism
-- which is replaced by key contacts.
CREATE TABLE sending_domains(
domain TEXT PRIMARY KEY,
dkim_works INTEGER DEFAULT 0
);

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