mirror of
https://github.com/chatmail/core.git
synced 2026-04-27 02:16:29 +03:00
Compare commits
2 Commits
link2xt/se
...
iequidoo/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e283d34449 | ||
|
|
65823fcfc6 |
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.95.0
|
||||
RUST_VERSION: 1.93.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.88.0
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- run: rustup override set $RUST_VERSION
|
||||
shell: bash
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
@@ -59,9 +59,9 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb
|
||||
- uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918
|
||||
with:
|
||||
arguments: --workspace --all-features --locked
|
||||
arguments: --all-features --workspace
|
||||
command: check
|
||||
command-arguments: "-Dwarnings"
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: Rustdoc
|
||||
run: cargo doc --document-private-items --no-deps
|
||||
|
||||
@@ -134,10 +134,10 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754
|
||||
uses: taiki-e/install-action@69e777b377e4ec209ddad9426ae3e0c1008b0ef3
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -168,13 +168,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Build C library
|
||||
run: cargo build -p deltachat_ffi
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
@@ -194,13 +194,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Build deltachat-rpc-server
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
|
||||
32
.github/workflows/deltachat-rpc-server.yml
vendored
32
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -34,13 +34,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
path: result/bin/deltachat-rpc-server
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
path: result/*.whl
|
||||
@@ -82,13 +82,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
@@ -106,13 +106,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
path: result/*.whl
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
@@ -157,13 +157,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
@@ -181,13 +181,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
path: result/*.whl
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
@@ -382,7 +382,7 @@ jobs:
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
@@ -496,7 +496,7 @@ jobs:
|
||||
ls -lah
|
||||
|
||||
- name: Upload to artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-npm-package
|
||||
path: deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
4
.github/workflows/dependabot.yml
vendored
4
.github/workflows/dependabot.yml
vendored
@@ -10,11 +10,11 @@ permissions:
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v3.0.0
|
||||
uses: dependabot/fetch-metadata@v2.4.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
|
||||
23
.github/workflows/dev-version.yml
vendored
23
.github/workflows/dev-version.yml
vendored
@@ -1,23 +0,0 @@
|
||||
# Check that PRs are made against the -dev version.
|
||||
#
|
||||
# If this fails, push commit to update the version to -dev to main.
|
||||
|
||||
name: Check for -dev version
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_dev_version:
|
||||
name: Check that current version ends with -dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run version-checking script
|
||||
run: scripts/check-dev-version.py
|
||||
2
.github/workflows/jsonrpc.yml
vendored
2
.github/workflows/jsonrpc.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: npm install
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -105,5 +105,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
working-directory: deltachat-rpc-client
|
||||
run: python3 -m build
|
||||
- name: Store the distribution packages
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: deltachat-rpc-client/dist/
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
4
.github/workflows/repl.yml
vendored
4
.github/workflows/repl.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: repl.exe
|
||||
path: "result/bin/deltachat-repl.exe"
|
||||
|
||||
57
.github/workflows/upload-docs.yml
vendored
57
.github/workflows/upload-docs.yml
vendored
@@ -1,18 +1,16 @@
|
||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, py.delta.chat and cffi.delta.chat
|
||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- build_jsonrpc_docs_ci
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build-rs:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: rs.delta.chat
|
||||
url: https://rs.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -25,15 +23,12 @@ jobs:
|
||||
- name: Upload to rs.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.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:/var/www/html/rs.delta.chat/"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
||||
|
||||
build-python:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: py.delta.chat
|
||||
url: https://py.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -41,21 +36,18 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.CODESPEAK_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:/var/www/html/py.delta.chat"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
|
||||
|
||||
build-c:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: c.delta.chat
|
||||
url: https://c.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -63,22 +55,18 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.CODESPEAK_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:/var/www/html/c.delta.chat"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||
|
||||
build-ts:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: js.jsonrpc.delta.chat
|
||||
url: https://js.jsonrpc.delta.chat/
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
@@ -102,27 +90,6 @@ jobs:
|
||||
- name: Upload to js.jsonrpc.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.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/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
|
||||
environment:
|
||||
name: cffi.delta.chat
|
||||
url: https://cffi.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
run: |
|
||||
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:/var/www/html/cffi.delta.chat/"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
|
||||
|
||||
31
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
31
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat_ffi` crate documentation
|
||||
# and upload it to <https://cffi.delta.chat/>
|
||||
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.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.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
||||
19
.github/workflows/zizmor-scan.yml
vendored
19
.github/workflows/zizmor-scan.yml
vendored
@@ -6,21 +6,26 @@ on:
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor
|
||||
name: zizmor latest via PyPI
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
6
.github/zizmor.yml
vendored
6
.github/zizmor.yml
vendored
@@ -1,6 +0,0 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
policies:
|
||||
actions/*: ref-pin
|
||||
dependabot/*: ref-pin
|
||||
488
CHANGELOG.md
488
CHANGELOG.md
@@ -1,478 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.49.0] - 2026-04-13
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Flipped Exif orientations ([#8057](https://github.com/chatmail/core/pull/8057)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Determine whether a message is an own message by looking at signature. multiple devices can temporarly have different sets of self addresses, and still need to properly recognize incoming versus outgoing messages. Disclaimer: some LLM tooling was initially involved but i went over everything by hand, and also addressed review comments..
|
||||
- Mark a message as delivered only after it has been fully sent out ([#8062](https://github.com/chatmail/core/pull/8062)).
|
||||
- Do not create 1:1 chat on second device when scanning a QR code.
|
||||
- Do not URL-encode proxy hostnames.
|
||||
- Assign webxdc updates from post-message to webxdc instance.
|
||||
- Let search also return hidden contacts if search value is an email address.
|
||||
- Add missing `extern "C"` to `dc_array_is_independent`.
|
||||
- Make start messages stick to the top of the chat.
|
||||
- For bots, wait with emitting IncomingMsg until the Post-Msg arrived ([#8104](https://github.com/chatmail/core/pull/8104)).
|
||||
- Trash message about group name change from non-member.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove `dc_msg_force_plaintext`.
|
||||
- @deltachat/stdio-rpc-server: also export a class.
|
||||
|
||||
### CI
|
||||
|
||||
- Make sure `-dev` version suffix is not forgotten after release.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document that events are broadcasted to all event emitters.
|
||||
- Fix broken link for i-d "Common PGP/MIME Message Mangling".
|
||||
|
||||
### Refactor
|
||||
|
||||
- ignore ForcePlaintext in saved messages chat.
|
||||
- @deltachat/stdio-rpc-server: make `getRPCServerPath` and `startDeltaChat` synchronous.
|
||||
- @deltachat/stdio-rpc-server: remove `await` from README example.
|
||||
- less nested `remove_contact_from_chat`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add test for `tweak_sort_timestamp()`.
|
||||
- Test that messages are only marked as delivered after being fully sent out ([#8077](https://github.com/chatmail/core/pull/8077)).
|
||||
- Fix flaky `test_no_old_msg_is_fresh`: Wait for incoming message before sending outgoing one.
|
||||
- Use TestContextManager in `test_keep_member_list_if_possibly_nomember`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: bump chrono from 0.4.43 to 0.4.44.
|
||||
- cargo: bump tracing-subscriber from 0.3.22 to 0.3.23.
|
||||
- cargo: bump tempfile from 3.26.0 to 3.27.0.
|
||||
- cargo: bump pin-project from 1.1.10 to 1.1.11.
|
||||
- cargo: bump tokio from 1.49.0 to 1.50.0.
|
||||
- cargo: bump libc from 0.2.182 to 0.2.183.
|
||||
- cargo: bump quote from 1.0.44 to 1.0.45.
|
||||
- cargo: bump image from 0.25.9 to 0.25.10.
|
||||
- cargo: bump proptest from 1.10.0 to 1.11.0.
|
||||
- deps: bump dependabot/fetch-metadata from 2.4.0 to 3.0.0.
|
||||
- bump version to 2.49.0-dev.
|
||||
|
||||
## [2.48.0] - 2026-03-30
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix reordering problems in multi-relay setups by not sorting received messages below the last seen one.
|
||||
- Always sort "Messages are end-to-end encrypted" notice to the beginning.
|
||||
- Make Message-ID of pre-messages stable across resends ([#8007](https://github.com/chatmail/core/pull/8007)).
|
||||
- Delete `imap_markseen` entries not corresponding to any `imap` rows.
|
||||
- Cleanup `imap` and `imap_sync` records without transport in housekeeping.
|
||||
- When receiving MDN, mark all preceding messages as noticed, even having same timestamp ([#7928](https://github.com/chatmail/core/pull/7928)).
|
||||
- Remove migration 108 preventing upgrades from core 1.86.0 to the latest version.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Improve IMAP loop logs.
|
||||
- Add decryption error to the device message about outgoing message decryption failure.
|
||||
- Log received message sort timestamp.
|
||||
|
||||
### Performance
|
||||
|
||||
- Move sorting outside of SQL query in `store_seen_flags_on_imap`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC API `markfresh_chat()`.
|
||||
- ffi: Correctly declare `dc_event_channel_new()` as having no params ([#7831](https://github.com/chatmail/core/pull/7831)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `wal_checkpoint_mutex`, lock `write_mutex` before getting sql connection instead.
|
||||
- Replace async `RwLock` with sync `RwLock` for stock strings.
|
||||
- Cleanup remaining Autocrypt Setup Message processing in `mimeparser`.
|
||||
- SecureJoin: do not check for self address in forwarding protection.
|
||||
- Fix clippy warnings.
|
||||
|
||||
### CI
|
||||
|
||||
- Update {c,py}.delta.chat website deployments.
|
||||
- Use environments for {rs,cffi,js.jsonrpc}.delta.chat deployments.
|
||||
- Fix https://docs.zizmor.sh/audits/#bot-conditions.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add SQL performance tips to STYLE.md.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove `test_old_message_5`.
|
||||
- Do not rely on loading newest chat in `load_imf_email()`.
|
||||
- Use `load_imf_email()` more.
|
||||
- The message is sorted correctly in the chat even if it arrives late.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: update rustls-webpki to 0.103.10.
|
||||
|
||||
## [2.47.0] - 2026-03-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't fall into infinite loop if the folder is missing ([#8021](https://github.com/chatmail/core/pull/8021)).
|
||||
- Delete `available_post_msgs` row if the message is already downloaded.
|
||||
- Delete `available_post_msgs` row if there is no corresponding IMAP entry.
|
||||
- Make newlines work in chat descriptions ([#8012](https://github.com/chatmail/core/pull/8012)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- use SEIPDv2 if all recipients support it.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add shadowsocks spec to standards.md.
|
||||
- Document Header Confidentiality Policy.
|
||||
- `deltachat_rpc_client`: make sphinx documentation display method parameters.
|
||||
- Remove `draft/aeap-mvp.md` which is superseded by key-contacts and multi-relay.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove code to send messages without intended recipient fingerprint.
|
||||
|
||||
### Tests
|
||||
|
||||
- Make `add_or_lookup_contact_id_no_key` public.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: bump sdp from 0.10.0 to 0.17.1.
|
||||
- Add RUSTSEC-2026-0049 exception to deny.toml.
|
||||
|
||||
## [2.46.0] - 2026-03-19
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove functions for sending and receiving Autocrypt Setup Message.
|
||||
- Add `list_transports_ex()` and `set_transport_unpublished()` functions.
|
||||
- Add API `dc_markfresh_chat` to mark messages as "fresh".
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- add `IncomingCallAccepted.from_this_device`.
|
||||
- decode `dcaccount://` URLs and error out on empty URLs early.
|
||||
- enable anonymous OpenPGP key IDs.
|
||||
- tls: do not verify TLS certificates for hostnames starting with `_`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Mark call message as seen when accepting/declining a call ([#7842](https://github.com/chatmail/core/pull/7842)).
|
||||
- do not send MDNs for hidden messages.
|
||||
- call sync_all() instead of sync_data() when writing accounts.toml.
|
||||
- fsync() the rename() of accounts.toml.
|
||||
- count recipients by Intended Recipient Fingerprints.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.0 to 0.5.2.
|
||||
- cargo: bump astral-tokio-tar from 0.5.6 to 0.6.0.
|
||||
- deps: bump actions/upload-artifact from 6 to 7.
|
||||
- cargo: bump blake3 from 1.8.2 to 1.8.3.
|
||||
- add constant_time_eq 0.3.1 to deny.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- use re-exported rustls::pki_types.
|
||||
- import tokio_rustls::rustls.
|
||||
- Move transport_tests to their own file.
|
||||
|
||||
### Tests
|
||||
|
||||
- Shift time even more in flaky test_sync_broadcast_and_send_message.
|
||||
- test markfresh_chat()
|
||||
|
||||
## [2.45.0] - 2026-03-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- JSON-RPC: add `createQrSvg` ([#7949](https://github.com/chatmail/core/pull/7949)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not read own public key from the database.
|
||||
- Securejoin v3, encrypt all securejoin messages ([#7754](https://github.com/chatmail/core/pull/7754)).
|
||||
- Domain separation between securejoin auth tokens and broadcast channel secrets ([#7981](https://github.com/chatmail/core/pull/7981)).
|
||||
- Merge OpenPGP certificates and distribute relays in them.
|
||||
- Advertise SEIPDv2 feature for new keys.
|
||||
- Don't depend on cleartext `Chat-Version`, `In-Reply-To`, and `References` headers for `prefetch_should_download` ([#7932](https://github.com/chatmail/core/pull/7932)).
|
||||
- Don't send unencrypted `In-Reply-To` and `References` headers ([#7935](https://github.com/chatmail/core/pull/7935)).
|
||||
- Don't send unencrypted `Auto-Submitted` header ([#7938](https://github.com/chatmail/core/pull/7938)).
|
||||
- Remove QR code tokens sync compatibility code.
|
||||
- Mutex to prevent fetching from multiple IMAP servers at the same time.
|
||||
- Add support to gif stickers ([#7941](https://github.com/chatmail/core/pull/7941))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix the deadlock by adding a mutex around `wal_checkpoint()`.
|
||||
- Do not run more than one housekeeping at a time.
|
||||
- ffi: don't steal Arc in `dc_jsonrpc_init` ([#7962](https://github.com/chatmail/core/pull/7962)).
|
||||
- Handle the case that the user starts a securejoin, and then deletes the contact ([#7883](https://github.com/chatmail/core/pull/7883)).
|
||||
- Do not trash pre-message if it is received twice.
|
||||
- Set `is_chatmail` during initial configuration.
|
||||
- vCard: Improve property value escaping ([#7931](https://github.com/chatmail/core/pull/7931)).
|
||||
- Percent-decode the address in `dclogin://` URLs.
|
||||
- Make broadcast owner and subscriber hidden contacts for each other ([#7856](https://github.com/chatmail/core/pull/7856)).
|
||||
- Set proper placeholder texts for system messages ([#7953](https://github.com/chatmail/core/pull/7953)).
|
||||
- Add "member added" messages to `OutBroadcast` when executing `SetPgpContacts` sync message ([#7952](https://github.com/chatmail/core/pull/7952)).
|
||||
- Correct channel system messages ([#7959](https://github.com/chatmail/core/pull/7959)).
|
||||
- Drop messages encrypted with the wrong symmetric secret ([#7963](https://github.com/chatmail/core/pull/7963)).
|
||||
- Fix debug assert message incorrectly talking about past members in the current member branch.
|
||||
- Update device chats at the end of configuration.
|
||||
- `deltachat_rpc_client`: make `@futuremethod` decorator keep method metadata.
|
||||
- Use the correct chat description stock string again ([#7939](https://github.com/chatmail/core/pull/7939)).
|
||||
- Use correct string for encryption info.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.94.0.
|
||||
- Allow non-hash references for `actions/*` and `dependabot/*`.
|
||||
- update zizmor workflow to use zizmorcore/zizmor-action.
|
||||
|
||||
### Documentation
|
||||
|
||||
- update `store_self_keypair()` documentation.
|
||||
- Fix documentation for membership change stock strings ([#7944](https://github.com/chatmail/core/pull/7944)).
|
||||
- use correct define for 'description changed' info message.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Un-resultify `KeyPair::new()`.
|
||||
- Remove `KeyPair` type.
|
||||
- pgp: do not use legacy key ID except for IssuerKeyId subpacket.
|
||||
- `use super::*` in qr::dclogin_scheme.
|
||||
- Move WAL checkpointing into `sql::pool` submodule.
|
||||
- Order self addresses by addition timestamp.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove arbitrary timeouts from `test_4_lowlevel.py`.
|
||||
- Fix flaky `test_qr_securejoin_broadcast` ([#7937](https://github.com/chatmail/core/pull/7937)).
|
||||
- Work around `test_sync_broadcast_and_send_message` flakiness.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.44.0-dev.
|
||||
- cargo: bump futures from 0.3.31 to 0.3.32.
|
||||
- cargo: bump quick-xml from 0.39.0 to 0.39.2.
|
||||
- cargo: bump criterion from 0.8.1 to 0.8.2.
|
||||
- cargo: bump tempfile from 3.24.0 to 3.25.0.
|
||||
- cargo: bump async-imap from 0.11.1 to 0.11.2.
|
||||
- cargo: bump regex from 1.12.2 to 1.12.3.
|
||||
- cargo: bump hyper-util from 0.1.19 to 0.1.20.
|
||||
- cargo: bump anyhow from 1.0.100 to 1.0.102.
|
||||
- cargo: bump syn from 2.0.114 to 2.0.117.
|
||||
- cargo: bump proptest from 1.9.0 to 1.10.0.
|
||||
- cargo: bump strum from 0.27.2 to 0.28.0.
|
||||
- cargo: bump strum_macros from 0.27.2 to 0.28.0.
|
||||
- cargo: bump quinn-proto from 0.11.9 to 0.11.14.
|
||||
|
||||
## [2.44.0] - 2026-02-27
|
||||
|
||||
### Build system
|
||||
|
||||
- git-cliff: do not capitalize the first letter of commit message.
|
||||
|
||||
### Documentation
|
||||
|
||||
- RELEASE.md: add section about dealing with antivirus false positives.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- improve logging of connection failures.
|
||||
- add backup versions to the importing error message.
|
||||
- add context to message loading failures.
|
||||
- Add 📱 to all webxdc summaries ([#7790](https://github.com/chatmail/core/pull/7790)).
|
||||
- Send webxdc name instead of raw file name in pre-messages. Display it in summary ([#7790](https://github.com/chatmail/core/pull/7790)).
|
||||
- rpc: add startup health-check and propagate server errors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- imex: do not call `set_config` before running SQL migrations ([#7851](https://github.com/chatmail/core/pull/7851)).
|
||||
- add missing group description strings to cffi.
|
||||
- chat-description-changed text in old clients ([#7870](https://github.com/chatmail/core/pull/7870)).
|
||||
- add cffi type for "Description changed" info message.
|
||||
- If there was no chat description, and it's set to be an empty string, don't send out a "chat description changed" message ([#7879](https://github.com/chatmail/core/pull/7879)).
|
||||
- Make clicking on broadcast member-added messages work always ([#7882](https://github.com/chatmail/core/pull/7882)).
|
||||
- tolerate empty existing directory in Accounts::new() ([#7886](https://github.com/chatmail/core/pull/7886)).
|
||||
- If importing a backup fails, delete the partially-imported profile ([#7885](https://github.com/chatmail/core/pull/7885)).
|
||||
- Don't generate new timestamp for re-sent messages ([#7889](https://github.com/chatmail/core/pull/7889)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: update async-native-tls from 0.5.0 to 0.6.0.
|
||||
- add dev-version bump instructions to RELEASE.md (bumping to 2.44.0-dev).
|
||||
- deps: bump cachix/install-nix-action from 31.9.0 to 31.9.1.
|
||||
|
||||
### Performance
|
||||
|
||||
- batched event reception.
|
||||
|
||||
### Refactor
|
||||
|
||||
- enable clippy::arithmetic_side_effects lint.
|
||||
- imex: check for overflow when adding blob size.
|
||||
- http: saturating addition to calculate cache expiration timestamp.
|
||||
- Move migrations to the end of the file ([#7895](https://github.com/chatmail/core/pull/7895)).
|
||||
- do not chain Autocrypt key verification to parsing.
|
||||
|
||||
### Tests
|
||||
|
||||
- fail fast when CHATMAIL_DOMAIN is unset.
|
||||
|
||||
## [2.43.0] - 2026-02-17
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Group and broadcast channel descriptions ([#7829](https://github.com/chatmail/core/pull/7829)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Assign iroh gossip topic to pre-message when post-message is received.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update fast-socks5 to version 1.0.
|
||||
- cargo: Update keccak from 0.1.5 to 0.1.6.
|
||||
- deps: Bump astral-sh/setup-uv from 7.1.6 to 7.3.0.
|
||||
|
||||
### Performance
|
||||
|
||||
- Use recv_direct() instead of recv() on the event channel.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Enable `clippy::manual_is_variant_and`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix flaky `test_transport_synchronization` ([#7850](https://github.com/chatmail/core/pull/7850)).
|
||||
|
||||
## [2.42.0] - 2026-02-10
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set `mvbox_move` to '0' explicitly for existing chatmail profiles.
|
||||
It's needed to prevent device message about deprecated `mvbox_move` option from appearing in chatmail profiles.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not scan not watched folders.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update rPGP from 0.18.0 to 0.19.0.
|
||||
- cargo: Bump quick-xml from 0.38.4 to 0.39.0.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove test_dont_show_emails.
|
||||
|
||||
### Other
|
||||
|
||||
- Fix typo in CHANGELOG for marknoticed_all_chats.
|
||||
|
||||
## [2.41.0] - 2026-02-06
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not require `ShowEmails` to be set to `All` for adding second relay.
|
||||
- Use different strings for audio and video calls.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't set download state to Failure if message is available on another Session's transport ([#7684](https://github.com/chatmail/core/pull/7684)).
|
||||
- Make use of call stock strings.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump `time` from 0.3.37 to 0.3.47.
|
||||
|
||||
## [2.40.0] - 2026-02-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Receive_imf: Log reasoning for chat assignment.
|
||||
- Use more fitting encryption info message.
|
||||
- Send Intended Recipient Fingerprint subpackets.
|
||||
- Trash messages with intended recipient fingerprints, but w/o our one included.
|
||||
- Do not collect email addresses from messages after configuration.
|
||||
- Add device message about legacy `mvbox_move`.
|
||||
- Never create IMAP folders.
|
||||
- Make summary for pre-messages look like summary for fully downloaded messages ([#7775](https://github.com/chatmail/core/pull/7775)).
|
||||
- Don't call `BlobObject::create_and_deduplicate()` when forwarding message to the same account.
|
||||
- Allow clients to specify whether a call has video initially or not ([#7740](https://github.com/chatmail/core/pull/7740)).
|
||||
- Do not load more than one own key from the keychain.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Cross-account forwarding of a message which `has_html()` ([#7791](https://github.com/chatmail/core/pull/7791)).
|
||||
- Make self-contact a key-contact even if key isn't generated yet.
|
||||
- `apply_group_changes()`: Check whether From is key-contact.
|
||||
- Don't add SELF to unencrypted chat created from encrypted message ([#7661](https://github.com/chatmail/core/pull/7661)).
|
||||
- Don't upscale images and test that image resolution isn't changed unnecessarily ([#7769](https://github.com/chatmail/core/pull/7769)).
|
||||
- Restart i/o when there are new transports in a sync message ([#7640](https://github.com/chatmail/core/pull/7640)).
|
||||
- `add_or_lookup_key_contacts*()`: Advance fingerprint_iter on invalid address.
|
||||
- `receive_imf`: Look up key contact by intended recipient fingerprint ([#7661](https://github.com/chatmail/core/pull/7661)).
|
||||
- Remove `Config::DeleteToTrash` and `Config::ConfiguredTrashFolder`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc(python): Process events forever by default.
|
||||
|
||||
### CI
|
||||
|
||||
- Make scripts/deny.sh test the locked version of dependencies.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unneeded dbg! statements ([#7776](https://github.com/chatmail/core/pull/7776)).
|
||||
- Remove unused Context.is_inbox().
|
||||
- Rename lookup_key_contacts_by_address_list() to lookup_key_contacts_fallback_to_chat().
|
||||
- Mark `ProviderOptions` as `non_exhaustive`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update provider database.
|
||||
- cargo: Update `bytes` from 1.11.0 to 1.11.1.
|
||||
- cargo: Bump tokio from 1.48.0 to 1.49.0.
|
||||
- cargo: Bump tokio-util from 0.7.17 to 0.7.18.
|
||||
- cargo: Bump libc from 0.2.178 to 0.2.180.
|
||||
- cargo: Bump quote from 1.0.42 to 1.0.44.
|
||||
- cargo: Bump syn from 2.0.111 to 2.0.114.
|
||||
- cargo: Bump human-panic from 2.0.4 to 2.0.6.
|
||||
- cargo: Bump chrono from 0.4.42 to 0.4.43.
|
||||
- cargo: Bump data-encoding from 2.9.0 to 2.10.0.
|
||||
- cargo: Bump colorutils-rs from 0.7.5 to 0.7.6.
|
||||
- Update provider database.
|
||||
- cargo: Bump thiserror from 2.0.17 to 2.0.18.
|
||||
- deps: Bump EmbarkStudios/cargo-deny-action from 2.0.14 to 2.0.15.
|
||||
- Remove RUSTSEC-2026-0002 exception from deny.toml.
|
||||
- cargo: Bump tokio-stream from 0.1.17 to 0.1.18.
|
||||
- cargo: Bump toml from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0.
|
||||
- cargo: Bump serde_json from 1.0.148 to 1.0.149.
|
||||
- cargo: Bump uuid from 1.19.0 to 1.20.0.
|
||||
- cargo: Bump rustls-pki-types from 1.13.2 to 1.14.0.
|
||||
- cargo: Bump tracing-subscriber from 0.3.20 to 0.3.22.
|
||||
|
||||
### Tests
|
||||
|
||||
- 2nd device receives message via new primary transport.
|
||||
- Make `test_dont_move_sync_msgs` less flaky.
|
||||
- Encrypted incoming message goes to encrypted 1:1 chat even if references messages in ad-hoc group.
|
||||
- Message in blocked chat arrives as InSeen.
|
||||
- Set `mvbox_move` to 0 for test rust accounts.
|
||||
|
||||
## [2.39.0] - 2026-01-23
|
||||
|
||||
### CI
|
||||
@@ -501,7 +28,7 @@
|
||||
- [**breaking**] Jsonrpc: remove `contacts` from `FullChat`. To migrate load contacts on demand via `get_contacts_by_ids` using `FullChat.contactIds` ([#7282](https://github.com/chatmail/core/pull/7282)).
|
||||
- jsonrpc: Add run_until parameter for bots ([#7688](https://github.com/chatmail/core/pull/7688)).
|
||||
- rust, jsonrpc: Add `get_message_read_receipt_count` method ([#7732](https://github.com/chatmail/core/pull/7732)).
|
||||
- rust and jsonrpc: Marknoticed_all_chats method to mark all chats as noticed, including muted ones. ([#7709](https://github.com/chatmail/core/pull/7709)).
|
||||
- rust and jsonrpc: Marknoticed_all_chats method to mark all chats as notices, including muted ones. ([#7709](https://github.com/chatmail/core/pull/7709)).
|
||||
- Public re-export of Connectivity ([#7737](https://github.com/chatmail/core/pull/7737)).
|
||||
|
||||
### Documentation
|
||||
@@ -733,7 +260,7 @@ that failed to be published for 2.31.0 due to not configured "trusted publishers
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- `lookup_or_create_adhoc_group()`: Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
|
||||
- Lookup_or_create_adhoc_group(): Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
|
||||
|
||||
## [2.31.0] - 2025-12-04
|
||||
|
||||
@@ -8092,14 +7619,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.36.0]: https://github.com/chatmail/core/compare/v2.35.0..v2.36.0
|
||||
[2.37.0]: https://github.com/chatmail/core/compare/v2.36.0..v2.37.0
|
||||
[2.38.0]: https://github.com/chatmail/core/compare/v2.37.0..v2.38.0
|
||||
[2.39.0]: https://github.com/chatmail/core/compare/v2.38.0..v2.39.0
|
||||
[2.40.0]: https://github.com/chatmail/core/compare/v2.39.0..v2.40.0
|
||||
[2.41.0]: https://github.com/chatmail/core/compare/v2.40.0..v2.41.0
|
||||
[2.42.0]: https://github.com/chatmail/core/compare/v2.41.0..v2.42.0
|
||||
[2.43.0]: https://github.com/chatmail/core/compare/v2.42.0..v2.43.0
|
||||
[2.44.0]: https://github.com/chatmail/core/compare/v2.43.0..v2.44.0
|
||||
[2.45.0]: https://github.com/chatmail/core/compare/v2.44.0..v2.45.0
|
||||
[2.46.0]: https://github.com/chatmail/core/compare/v2.45.0..v2.46.0
|
||||
[2.47.0]: https://github.com/chatmail/core/compare/v2.46.0..v2.47.0
|
||||
[2.48.0]: https://github.com/chatmail/core/compare/v2.47.0..v2.48.0
|
||||
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
|
||||
|
||||
619
Cargo.lock
generated
619
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
29
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.50.0-dev"
|
||||
version = "2.39.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
@@ -45,7 +45,7 @@ anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
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-native-tls = { version = "0.5", 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"] }
|
||||
base64 = { workspace = true }
|
||||
@@ -56,7 +56,7 @@ chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
colorutils-rs = { version = "0.7.5", default-features = false }
|
||||
data-encoding = "2.9.0"
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "1"
|
||||
fast-socks5 = "0.10"
|
||||
fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
@@ -78,16 +78,17 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.19.0", default-features = false }
|
||||
pgp = { version = "0.18.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rustls-pki-types = "1.12.0"
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.17.1"
|
||||
sdp = "0.10.0"
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -95,15 +96,15 @@ sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.15.1"
|
||||
strum = "0.28"
|
||||
strum_macros = "0.28"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
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 }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6", default-features = false }
|
||||
astral-tokio-tar = { version = "0.5.6", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
@@ -181,11 +182,11 @@ harness = false
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.44", default-features = false }
|
||||
chrono = { version = "0.4.42", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.32"
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.6.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
@@ -193,15 +194,15 @@ mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.9"
|
||||
regex = "1.12"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.37"
|
||||
sanitize-filename = "0.6"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.27.0"
|
||||
tempfile = "3.24.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.18"
|
||||
tokio-util = "0.7.17"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
|
||||
38
RELEASE.md
38
RELEASE.md
@@ -22,44 +22,6 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
9. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||
|
||||
10. Update the version to the next development version:
|
||||
`scripts/set_core_version.py 1.117.0-dev`.
|
||||
|
||||
11. Commit and push the change:
|
||||
`git commit -m "chore: bump version to 1.117.0-dev" && git push origin main`.
|
||||
|
||||
12. Once the binaries are generated and [published](https://github.com/chatmail/core/releases),
|
||||
check Windows binaries for false positive detections at [VirusTotal].
|
||||
Either upload the binaries directly or submit a direct link to the artifact.
|
||||
You can use [old browsers interface](https://www.virustotal.com/old-browsers/)
|
||||
if there are problems with using the default website.
|
||||
If you submit a direct link and get to the page saying
|
||||
"No security vendors flagged this URL as malicious",
|
||||
it does not mean that the file itself is not detected.
|
||||
You need to go to the "details" tab
|
||||
and click on the SHA-256 hash in the "Body SHA-256" section.
|
||||
If any false positive is detected,
|
||||
open an issue to track removing it.
|
||||
See <https://github.com/chatmail/core/issues/7847>
|
||||
for an example of false positive detection issue.
|
||||
If there is a false positive "Microsoft" detection,
|
||||
mark the issue as a blocker.
|
||||
|
||||
[VirusTotal]: https://www.virustotal.com/
|
||||
|
||||
## Dealing with antivirus false positives
|
||||
|
||||
If Windows release is incorrectly detected by some antivirus, submit requests to remove detection.
|
||||
|
||||
"Microsoft" antivirus is built in Windows and will break user setups so removing its detection should be highest priority.
|
||||
To submit false positive to Microsoft, go to <https://www.microsoft.com/en-us/wdsi/filesubmission> and select "Submit file as a ... Software developer" option.
|
||||
|
||||
False positive contacts for other vendors can be found at <https://docs.virustotal.com/docs/false-positive-contacts>.
|
||||
Not all of them may be up to date, so check the links below first.
|
||||
Previously we successfully used the following contacts:
|
||||
- [ESET-NOD32](mailto:samples@eset.com)
|
||||
- [Symantec](https://symsubmit.symantec.com/)
|
||||
|
||||
## Dealing with failed releases
|
||||
|
||||
Once you make a GitHub release,
|
||||
|
||||
6
STYLE.md
6
STYLE.md
@@ -68,12 +68,6 @@ keyword doesn't help here.
|
||||
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
|
||||
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
|
||||
|
||||
When changing complex SQL queries, test them on a new database with `EXPLAIN QUERY PLAN`
|
||||
to make sure that indexes are used and large tables are not going to be scanned.
|
||||
Never run `ANALYZE` on the databases,
|
||||
this makes query planner unpredictable
|
||||
and may make performance significantly worse: <https://github.com/chatmail/core/issues/6585>
|
||||
|
||||
## Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
|
||||
@@ -8,47 +8,43 @@
|
||||
//! cargo bench --bench decrypting --features="internals"
|
||||
//! ```
|
||||
//!
|
||||
//! or, if you want to only run e.g. the 'Decrypt and parse a symmetrically encrypted message' benchmark:
|
||||
//! or, if you want to only run e.g. the 'Decrypt a symmetrically encrypted message' benchmark:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse a symmetrically encrypted message'
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt a symmetrically encrypted message'
|
||||
//! ```
|
||||
//!
|
||||
//! You can also pass a substring:
|
||||
//! You can also pass a substring.
|
||||
//! So, you can run all 'Decrypt and parse' benchmarks with:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'symmetrically'
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse'
|
||||
//! ```
|
||||
//!
|
||||
//! Symmetric decryption has to try out all known secrets,
|
||||
//! You can benchmark this by adapting the `NUM_SECRETS` variable.
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::internals_for_benches::create_broadcast_secret;
|
||||
use deltachat::internals_for_benches::create_dummy_keypair;
|
||||
use deltachat::internals_for_benches::save_broadcast_secret;
|
||||
use deltachat::securejoin::get_securejoin_qr;
|
||||
use deltachat::{
|
||||
Events, chat::ChatId, config::Config, context::Context, internals_for_benches::key_from_asc,
|
||||
internals_for_benches::parse_and_get_text, internals_for_benches::store_self_keypair,
|
||||
Events,
|
||||
chat::ChatId,
|
||||
config::Config,
|
||||
context::Context,
|
||||
internals_for_benches::key_from_asc,
|
||||
internals_for_benches::parse_and_get_text,
|
||||
internals_for_benches::store_self_keypair,
|
||||
pgp::{KeyPair, SeipdVersion, decrypt, pk_encrypt, symm_encrypt_message},
|
||||
stock_str::StockStrings,
|
||||
};
|
||||
use rand::{Rng, rng};
|
||||
use tempfile::tempdir;
|
||||
|
||||
static NUM_BROADCAST_SECRETS: LazyLock<usize> = LazyLock::new(|| {
|
||||
std::env::var("NUM_BROADCAST_SECRETS")
|
||||
.unwrap_or("500".to_string())
|
||||
.parse()
|
||||
.unwrap()
|
||||
});
|
||||
static NUM_AUTH_TOKENS: LazyLock<usize> = LazyLock::new(|| {
|
||||
std::env::var("NUM_AUTH_TOKENS")
|
||||
.unwrap_or("5000".to_string())
|
||||
.parse()
|
||||
.unwrap()
|
||||
});
|
||||
const NUM_SECRETS: usize = 500;
|
||||
|
||||
async fn create_context() -> Context {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -62,7 +58,9 @@ async fn create_context() -> Context {
|
||||
.await
|
||||
.unwrap();
|
||||
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||
store_self_keypair(&context, &secret)
|
||||
let public = secret.signed_public_key();
|
||||
let key_pair = KeyPair { public, secret };
|
||||
store_self_keypair(&context, &key_pair)
|
||||
.await
|
||||
.expect("Failed to save key");
|
||||
|
||||
@@ -72,6 +70,66 @@ async fn create_context() -> Context {
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Decrypt");
|
||||
|
||||
// ===========================================================================================
|
||||
// Benchmarks for decryption only, without any other parsing
|
||||
// ===========================================================================================
|
||||
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function("Decrypt a symmetrically encrypted message", |b| {
|
||||
let plain = generate_plaintext();
|
||||
let secrets = generate_secrets();
|
||||
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let secret = secrets[NUM_SECRETS / 2].clone();
|
||||
symm_encrypt_message(
|
||||
plain.clone(),
|
||||
create_dummy_keypair("alice@example.org").unwrap().secret,
|
||||
black_box(&secret),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
let mut msg =
|
||||
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
|
||||
let decrypted = msg.as_data_vec().unwrap();
|
||||
|
||||
assert_eq!(black_box(decrypted), plain);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("Decrypt a public-key encrypted message", |b| {
|
||||
let plain = generate_plaintext();
|
||||
let key_pair = create_dummy_keypair("alice@example.org").unwrap();
|
||||
let secrets = generate_secrets();
|
||||
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
pk_encrypt(
|
||||
plain.clone(),
|
||||
vec![black_box(key_pair.public.clone())],
|
||||
key_pair.secret.clone(),
|
||||
true,
|
||||
true,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
let mut msg = decrypt(
|
||||
encrypted.clone().into_bytes(),
|
||||
std::slice::from_ref(&key_pair.secret),
|
||||
black_box(&secrets),
|
||||
)
|
||||
.unwrap();
|
||||
let decrypted = msg.as_data_vec().unwrap();
|
||||
|
||||
assert_eq!(black_box(decrypted), plain);
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================================
|
||||
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
|
||||
// ===========================================================================================
|
||||
@@ -81,7 +139,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
|
||||
// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
|
||||
// Put it into the middle of our secrets:
|
||||
secrets[*NUM_BROADCAST_SECRETS / 2] = "secret".to_string();
|
||||
secrets[NUM_SECRETS / 2] = "secret".to_string();
|
||||
|
||||
let context = rt.block_on(async {
|
||||
let context = create_context().await;
|
||||
@@ -90,10 +148,6 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
for _i in 0..*NUM_AUTH_TOKENS {
|
||||
get_securejoin_qr(&context, None).await.unwrap();
|
||||
}
|
||||
println!("NUM_AUTH_TOKENS={}", *NUM_AUTH_TOKENS);
|
||||
context
|
||||
});
|
||||
|
||||
@@ -107,7 +161,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(black_box(text), "Symmetrically encrypted message");
|
||||
assert_eq!(text, "Symmetrically encrypted message");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -122,7 +176,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(black_box(text), "hi");
|
||||
assert_eq!(text, "hi");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -131,12 +185,17 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn generate_secrets() -> Vec<String> {
|
||||
let secrets: Vec<String> = (0..*NUM_BROADCAST_SECRETS)
|
||||
let secrets: Vec<String> = (0..NUM_SECRETS)
|
||||
.map(|_| create_broadcast_secret())
|
||||
.collect();
|
||||
println!("NUM_BROADCAST_SECRETS={}", *NUM_BROADCAST_SECRETS);
|
||||
secrets
|
||||
}
|
||||
|
||||
fn generate_plaintext() -> Vec<u8> {
|
||||
let mut plain: Vec<u8> = vec![0; 500];
|
||||
rng().fill(&mut plain[..]);
|
||||
plain
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -66,7 +66,7 @@ body = """
|
||||
{% for commit in commits %}
|
||||
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||
{{ commit.message }}.\
|
||||
{{ commit.message | upper_first }}.\
|
||||
{% if commit.footers is defined %}\
|
||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||
|
||||
@@ -36,45 +36,6 @@ impl VcardContact {
|
||||
}
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||
s
|
||||
// backslash must be first!
|
||||
.replace(r"\", r"\\")
|
||||
.replace(',', r"\,")
|
||||
.replace(';', r"\;")
|
||||
.replace('\n', r"\n")
|
||||
}
|
||||
|
||||
fn unescape(s: &str) -> String {
|
||||
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||
let mut out = String::new();
|
||||
|
||||
let mut chars = s.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\\' {
|
||||
if let Some(next) = chars.next() {
|
||||
match next {
|
||||
'\\' | ',' | ';' => out.push(next),
|
||||
'n' | 'N' => out.push('\n'),
|
||||
_ => {
|
||||
// Invalid escape sequence (keep unchanged)
|
||||
out.push('\\');
|
||||
out.push(next);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Invalid escape sequence (keep unchanged)
|
||||
out.push('\\');
|
||||
}
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Returns a vCard containing given contacts.
|
||||
///
|
||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||
@@ -85,6 +46,10 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
s.replace(',', "\\,")
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||
@@ -159,7 +124,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||
let (params, value) = vcard_property_raw(line, property)?;
|
||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||
Some((params, unescape(value)))
|
||||
Some((params, value.replace("\\,", ",")))
|
||||
}
|
||||
fn base64_key(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property_raw(line, "key")?;
|
||||
|
||||
@@ -91,7 +91,7 @@ fn test_make_and_parse_vcard() {
|
||||
authname: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
biography: Some("Hi,\nI'm Alice; and this is a backslash: \\".to_string()),
|
||||
biography: Some("Hi, I'm Alice".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
@@ -110,7 +110,7 @@ fn test_make_and_parse_vcard() {
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||
NOTE:Hi\\,\\nI'm Alice\\; and this is a backslash: \\\\\r\n\
|
||||
NOTE:Hi\\, I'm Alice\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
@@ -276,14 +276,3 @@ END:VCARD",
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_value_escape_unescape() {
|
||||
let original = "Text, with; chars and a \\ and a newline\nand a literal newline \\n";
|
||||
let expected_escaped = r"Text\, with\; chars and a \\ and a newline\nand a literal newline \\n";
|
||||
|
||||
let escaped = escape(original);
|
||||
assert_eq!(escaped, expected_escaped);
|
||||
let unescaped = unescape(&escaped);
|
||||
assert_eq!(original, unescaped);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.50.0-dev"
|
||||
version = "2.39.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -364,14 +364,18 @@ uint32_t dc_get_id (dc_context_t* context);
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
*
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per context.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* may or may not be available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
||||
|
||||
@@ -390,9 +394,27 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
/**
|
||||
* Configure the context. The configuration is handled by key=value pairs as:
|
||||
*
|
||||
* - `configured_addr` = Email address in use.
|
||||
* - `addr` = Email address to use for configuration.
|
||||
* If dc_configure() fails this is not the email address actually in use.
|
||||
* Use `configured_addr` to find out the email address actually in use.
|
||||
* - `configured_addr` = Email address actually in use.
|
||||
* Unless for testing, do not set this value using dc_set_config().
|
||||
* Instead, set `addr` and call dc_configure().
|
||||
* - `mail_server` = IMAP-server, guessed if left out
|
||||
* - `mail_user` = IMAP-username, guessed if left out
|
||||
* - `mail_pw` = IMAP-password (always needed)
|
||||
* - `mail_port` = IMAP-port, guessed if left out
|
||||
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `send_server` = SMTP-server, guessed if left out
|
||||
* - `send_user` = SMTP-user, guessed if left out
|
||||
* - `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 certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
|
||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
|
||||
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
|
||||
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||
@@ -408,6 +430,20 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1=send a copy of outgoing messages to self (default).
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `mvbox_move` = 1=detect chat messages,
|
||||
* move them to the `DeltaChat` folder,
|
||||
* and watch the `DeltaChat` folder for updates (default),
|
||||
* 0=do not move chat-messages
|
||||
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
|
||||
* `DeltaChat` folder. Messages will still be fetched from the
|
||||
* spam folder.
|
||||
* 0=watch all folders normally (default)
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only,
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts (default).
|
||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
@@ -416,7 +452,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
||||
* 1=delete messages directly after receiving from server, mvbox is skipped.
|
||||
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
|
||||
* "Saved messages" are deleted from the server as well as emails, the UI should clearly point that out.
|
||||
* "Saved messages" are deleted from the server as well as
|
||||
* e-mails matching the `show_emails` settings above, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||
@@ -488,27 +525,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1 = Contacts (default, does not include contact requests),
|
||||
* 2 = Nobody (calls never result in a notification).
|
||||
*
|
||||
* Also, there are configs that are only needed
|
||||
* if you want to use the deprecated dc_configure() API, such as:
|
||||
*
|
||||
* - `addr` = Email address to use for configuration.
|
||||
* If dc_configure() fails this is not the email address actually in use.
|
||||
* Use `configured_addr` to find out the email address actually in use.
|
||||
* - `mail_server` = IMAP-server, guessed if left out
|
||||
* - `mail_user` = IMAP-username, guessed if left out
|
||||
* - `mail_pw` = IMAP-password (always needed)
|
||||
* - `mail_port` = IMAP-port, guessed if left out
|
||||
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `send_server` = SMTP-server, guessed if left out
|
||||
* - `send_user` = SMTP-user, guessed if left out
|
||||
* - `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)
|
||||
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -534,6 +550,9 @@ int dc_set_config (dc_context_t* context, const char*
|
||||
* 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.
|
||||
* - `quota_exceeding` = 0: quota is unknown or in normal range;
|
||||
* >=80: quota is about to exceed, the value is the concrete percentage,
|
||||
* a device message is added when that happens, however, that value may still be interesting for bots.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object. For querying system values, this can be NULL.
|
||||
@@ -692,12 +711,6 @@ int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
/**
|
||||
* Configure a context.
|
||||
*
|
||||
* This way of configuring a context is deprecated,
|
||||
* and does not allow to configure multiple transports.
|
||||
* If you can, use the JSON-RPC API (../deltachat-jsonrpc/src/api.rs)
|
||||
* `add_or_update_transport()`/`addOrUpdateTransport()` instead.
|
||||
*
|
||||
* During configuration IO must not be started,
|
||||
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
|
||||
* If the context is already configured,
|
||||
@@ -1229,12 +1242,9 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
* 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.
|
||||
* This allows the recipient's client to adjust incoming call UX.
|
||||
* A call can be upgraded to include video later.
|
||||
* @return ID of the system message announcing the call.
|
||||
*/
|
||||
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info, int has_video);
|
||||
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1387,6 +1397,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
#define DC_GCM_ADDDAYMARKER 0x01
|
||||
#define DC_GCM_INFO_ONLY 0x02
|
||||
|
||||
|
||||
/**
|
||||
@@ -1407,6 +1418,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
||||
* @param marker1before Deprecated, set this to 0.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
*/
|
||||
@@ -1470,6 +1482,7 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch
|
||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||
* @param seconds Count messages older than the given number of seconds.
|
||||
* @return Number of messages that are older than the given number of seconds.
|
||||
* This includes e-mails downloaded due to the `show_emails` option.
|
||||
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
*/
|
||||
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
||||
@@ -1553,7 +1566,7 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
* (read receipts aren't sent for noticed messages).
|
||||
*
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
* See also dc_markseen_msgs() and dc_markfresh_chat().
|
||||
* See also dc_markseen_msgs().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
@@ -1562,29 +1575,6 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
void dc_marknoticed_chat (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Mark the last incoming message in chat as _fresh_.
|
||||
*
|
||||
* UI can use this to offer a "mark unread" option,
|
||||
* so that already noticed chats (see dc_marknoticed_chat()) get a badge counter again.
|
||||
*
|
||||
* dc_get_fresh_msg_cnt() and dc_get_fresh_msgs() usually is increased by one afterwards.
|
||||
*
|
||||
* #DC_EVENT_MSGS_CHANGED is fired as usual,
|
||||
* however, #DC_EVENT_INCOMING_MSG is _not_ fired again.
|
||||
* This is to not add complexity to incoming messages code,
|
||||
* e.g. UI usually does not add notifications for manually unread chats.
|
||||
* If the UI wants to update system badge counters,
|
||||
* they should do so directly after calling dc_markfresh_chat().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID of which the last incoming message should be marked as fresh.
|
||||
* If the chat does not have incoming messages, nothing happens.
|
||||
*/
|
||||
void dc_markfresh_chat (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a given chat or any chat.
|
||||
* Typically used to show a gallery.
|
||||
@@ -1861,16 +1851,15 @@ int dc_remove_contact_from_chat (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
/**
|
||||
* Set the name of a group or broadcast channel.
|
||||
* Set group name.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param chat_id The chat ID to set the name for. Must be a group chat or broadcast channel.
|
||||
* @param chat_id The chat ID to set the name for. Must be a group chat.
|
||||
* @param name New name of the group.
|
||||
* @param context The context object.
|
||||
* @return 1=success, 0=error
|
||||
@@ -1897,11 +1886,10 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
|
||||
int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32_t timer);
|
||||
|
||||
/**
|
||||
* Set group or broadcast channel profile image.
|
||||
* Set group profile image.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* or if this is a brodacast channel,
|
||||
* all members are informed by a special status message that is sent automatically by this function.
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
@@ -1909,7 +1897,7 @@ int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to set the image for. Must be a group chat or broadcast channel.
|
||||
* @param chat_id The chat ID to set the image for.
|
||||
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
* If you pass NULL here, the group image is deleted (for promoted groups, all members are informed about
|
||||
@@ -2478,6 +2466,76 @@ void dc_imex (dc_context_t* context, int what, c
|
||||
char* dc_imex_has_backup (dc_context_t* context, const char* dir);
|
||||
|
||||
|
||||
/**
|
||||
* Initiate Autocrypt Setup Transfer.
|
||||
* Before starting the setup transfer with this function, the user should be asked:
|
||||
*
|
||||
* ~~~
|
||||
* "An 'Autocrypt Setup Message' securely shares your end-to-end setup with other Autocrypt-compliant apps.
|
||||
* The setup will be encrypted by a setup code which is displayed here and must be typed on the other device.
|
||||
* ~~~
|
||||
*
|
||||
* After that, this function should be called to send the Autocrypt Setup Message.
|
||||
* The function creates the setup message and adds it to outgoing message queue.
|
||||
* The message is sent asynchronously.
|
||||
*
|
||||
* The required setup code is returned in the following format:
|
||||
*
|
||||
* ~~~
|
||||
* 1234-1234-1234-1234-1234-1234-1234-1234-1234
|
||||
* ~~~
|
||||
*
|
||||
* The setup code should be shown to the user then:
|
||||
*
|
||||
* ~~~
|
||||
* "Your key has been sent to yourself. Switch to the other device and
|
||||
* open the setup message. You should be prompted for a setup code. Type
|
||||
* the following digits into the prompt:
|
||||
*
|
||||
* 1234 - 1234 - 1234 -
|
||||
* 1234 - 1234 - 1234 -
|
||||
* 1234 - 1234 - 1234
|
||||
*
|
||||
* Once you're done, your other device will be ready to use Autocrypt."
|
||||
* ~~~
|
||||
*
|
||||
* On the _other device_ you will call dc_continue_key_transfer() then
|
||||
* for setup messages identified by dc_msg_is_setupmessage().
|
||||
*
|
||||
* For more details about the Autocrypt setup process, please refer to
|
||||
* https://autocrypt.org/en/latest/level1.html#autocrypt-setup-message
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return The setup code. Must be released using dc_str_unref() after usage.
|
||||
* On errors, e.g. if the message could not be sent, NULL is returned.
|
||||
*/
|
||||
char* dc_initiate_key_transfer (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Continue the Autocrypt Key Transfer on another device.
|
||||
*
|
||||
* If you have started the key transfer on another device using dc_initiate_key_transfer()
|
||||
* and you've detected a setup message with dc_msg_is_setupmessage(), you should prompt the
|
||||
* user for the setup code and call this function then.
|
||||
*
|
||||
* You can use dc_msg_get_setupcodebegin() to give the user a hint about the code (useful if the user
|
||||
* has created several messages and should not enter the wrong code).
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the setup message to decrypt.
|
||||
* @param setup_code The setup code entered by the user. This is the same setup code as returned from
|
||||
* dc_initiate_key_transfer() on the other device.
|
||||
* There is no need to format the string correctly, the function will remove all spaces and other characters and
|
||||
* insert the `-` characters at the correct places.
|
||||
* @return 1=key successfully decrypted and imported; both devices will use the same key now;
|
||||
* 0=key transfer failed e.g. due to a bad setup code.
|
||||
*/
|
||||
int dc_continue_key_transfer (dc_context_t* context, uint32_t msg_id, const char* setup_code);
|
||||
|
||||
|
||||
/**
|
||||
* Signal an ongoing process to stop.
|
||||
*
|
||||
@@ -2815,6 +2873,19 @@ int dc_set_location (dc_context_t* context, double latit
|
||||
dc_array_t* dc_get_locations (dc_context_t* context, uint32_t chat_id, uint32_t contact_id, int64_t timestamp_begin, int64_t timestamp_end);
|
||||
|
||||
|
||||
/**
|
||||
* Delete all locations on the current device.
|
||||
* Locations already sent cannot be deleted.
|
||||
*
|
||||
* Typically results in the event #DC_EVENT_LOCATION_CHANGED
|
||||
* with contact_id set to 0.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
*/
|
||||
void dc_delete_all_locations (dc_context_t* context);
|
||||
|
||||
|
||||
// misc
|
||||
|
||||
/**
|
||||
@@ -3294,14 +3365,18 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
|
||||
@@ -4217,8 +4292,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* true if the Webxdc should get internet access;
|
||||
* this is the case i.e. for experimental maps integration.
|
||||
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
* - is_app_sender: Define if the local user is the one who initially shared the webxdc application in the chat.
|
||||
* - is_broadcast: Define if the app runs in a broadcasting context.
|
||||
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
|
||||
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
|
||||
@@ -4534,7 +4607,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
* - 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
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
@@ -4581,10 +4653,7 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_GROUP_IMAGE_CHANGED 3
|
||||
#define DC_INFO_MEMBER_ADDED_TO_GROUP 4
|
||||
#define DC_INFO_MEMBER_REMOVED_FROM_GROUP 5
|
||||
|
||||
// Deprecated as of 2026-03-16, not used for new messages.
|
||||
#define DC_INFO_AUTOCRYPT_SETUP_MESSAGE 6
|
||||
|
||||
#define DC_INFO_SECURE_JOIN_MESSAGE 7
|
||||
#define DC_INFO_LOCATIONSTREAMING_ENABLED 8
|
||||
#define DC_INFO_LOCATION_ONLY 9
|
||||
@@ -4593,7 +4662,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
#define DC_INFO_GROUP_DESCRIPTION_CHANGED 70
|
||||
|
||||
|
||||
/**
|
||||
@@ -4614,6 +4682,40 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the message is an Autocrypt Setup Message.
|
||||
*
|
||||
* Setup messages should be shown in an unique way e.g. using a different text color.
|
||||
* On a click or another action, the user should be prompted for the setup code
|
||||
* which is forwarded to dc_continue_key_transfer() then.
|
||||
*
|
||||
* Setup message are typically generated by dc_initiate_key_transfer() on another device.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return 1=message is a setup message, 0=no setup message.
|
||||
* For setup messages, dc_msg_get_viewtype() returns #DC_MSG_FILE.
|
||||
*/
|
||||
int dc_msg_is_setupmessage (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get the first characters of the setup code.
|
||||
*
|
||||
* Typically, this is used to pre-fill the first entry field of the setup code.
|
||||
* If the user has several setup messages, he can be sure typing in the correct digits.
|
||||
*
|
||||
* To check, if a message is a setup message, use dc_msg_is_setupmessage().
|
||||
* To decrypt a secret key from a setup message, use dc_continue_key_transfer().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Typically the first two digits of the setup code or an empty string if unknown.
|
||||
* NULL is never returned. Must be released using dc_str_unref() when done.
|
||||
*/
|
||||
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the error status of the message.
|
||||
* If there is no error associated with the message, NULL is returned.
|
||||
@@ -4950,6 +5052,17 @@ 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);
|
||||
|
||||
|
||||
/**
|
||||
* Force the message to be sent in plain text.
|
||||
*
|
||||
* This API is for bots, there is no need to expose it in the UI.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
*/
|
||||
void dc_msg_force_plaintext (dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
*
|
||||
@@ -5787,7 +5900,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* These constants configure TLS certificate checks for IMAP and SMTP connections.
|
||||
*
|
||||
* These constants are set via dc_set_config()
|
||||
* using key "imap_certificate_checks".
|
||||
* using keys "imap_certificate_checks" and "smtp_certificate_checks".
|
||||
*
|
||||
* @addtogroup DC_CERTCK
|
||||
* @{
|
||||
@@ -5917,7 +6030,7 @@ char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const ch
|
||||
* @memberof dc_event_channel_t
|
||||
* @return An event channel wrapper object (dc_event_channel_t).
|
||||
*/
|
||||
dc_event_channel_t* dc_event_channel_new(void);
|
||||
dc_event_channel_t* dc_event_channel_new();
|
||||
|
||||
/**
|
||||
* Release/free the events channel structure.
|
||||
@@ -5937,14 +6050,21 @@ void dc_event_channel_unref(dc_event_channel_t* event_channel);
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
*
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* @memberof dc_event_channel_t
|
||||
* @param The event channel.
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager / event channel.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);
|
||||
|
||||
@@ -6189,7 +6309,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
*
|
||||
* However, for ongoing processes (e.g. dc_configure())
|
||||
* or for functions that are expected to fail
|
||||
* or for functions that are expected to fail (e.g. dc_continue_key_transfer())
|
||||
* it might be better to delay showing these events until the function has really
|
||||
* failed (returned false). It should be sufficient to report only the _last_ error
|
||||
* in a message box then.
|
||||
@@ -6400,7 +6520,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Location of one or more contact has changed.
|
||||
*
|
||||
* @param data1 (int) contact_id of the contact for which the location has changed.
|
||||
* If the locations of several contacts have been changed, this parameter is set to 0.
|
||||
* If the locations of several contacts have been changed,
|
||||
* e.g. after calling dc_delete_all_locations(), this parameter is set to 0.
|
||||
* @param data2 0
|
||||
*/
|
||||
#define DC_EVENT_LOCATION_CHANGED 2035
|
||||
@@ -6627,7 +6748,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
* @param data2 (int) 1 if the call was accepted from this device (process).
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
|
||||
|
||||
@@ -6656,8 +6776,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* UI should update the list.
|
||||
*
|
||||
* The event is emitted when the transports are modified on another device
|
||||
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`,
|
||||
* `set_transport_unpublished` or `set_config(configured_addr)`.
|
||||
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`
|
||||
* or `set_config(configured_addr)`.
|
||||
*/
|
||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||
|
||||
@@ -6671,6 +6791,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_CONFIGURE_PROGRESS || (e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("show_emails")
|
||||
*/
|
||||
#define DC_SHOW_EMAILS_OFF 0
|
||||
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
|
||||
#define DC_SHOW_EMAILS_ALL 2
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("media_quality")
|
||||
*/
|
||||
@@ -7005,7 +7133,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in message summary text for notifications and chatlist.
|
||||
#define DC_STR_FORWARDED 97
|
||||
|
||||
/// @deprecated 2026-04-25
|
||||
/// "Quota exceeding, already %1$s%% used."
|
||||
///
|
||||
/// Used as device message text.
|
||||
///
|
||||
/// `%1$s` will be replaced by the percentage used
|
||||
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
|
||||
|
||||
/// "Multi Device Synchronization"
|
||||
@@ -7357,7 +7489,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
///
|
||||
/// Used in info-messages, UI may add smth. as "Tap to learn more."
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
@@ -7409,6 +7541,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
|
||||
#define DC_STR_DONATION_REQUEST 193
|
||||
|
||||
/// "Outgoing call"
|
||||
#define DC_STR_OUTGOING_CALL 194
|
||||
|
||||
/// "Incoming call"
|
||||
#define DC_STR_INCOMING_CALL 195
|
||||
|
||||
/// "Declined call"
|
||||
#define DC_STR_DECLINED_CALL 196
|
||||
|
||||
@@ -7440,19 +7578,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` and `%2$s` will both be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_STARTED 203
|
||||
|
||||
/// "Channel name changed from %1$s to %2$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the old channel name.
|
||||
/// `%2$s` will be replaced by the new channel name.
|
||||
#define DC_STR_CHANNEL_NAME_CHANGED 204
|
||||
|
||||
/// "Channel image changed."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_CHANNEL_IMAGE_CHANGED 205
|
||||
|
||||
/// "The attachment contains anonymous usage statistics, which help us improve Delta Chat. Thank you!"
|
||||
///
|
||||
/// Used as the message body for statistics sent out.
|
||||
@@ -7473,29 +7598,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as the first info messages in newly created classic email threads.
|
||||
#define DC_STR_CHAT_UNENCRYPTED_EXPLANATON 230
|
||||
|
||||
/// "Outgoing audio call"
|
||||
#define DC_STR_OUTGOING_AUDIO_CALL 232
|
||||
|
||||
/// "Outgoing video call"
|
||||
#define DC_STR_OUTGOING_VIDEO_CALL 233
|
||||
|
||||
/// "Incoming audio call"
|
||||
#define DC_STR_INCOMING_AUDIO_CALL 234
|
||||
|
||||
/// "Incoming video call"
|
||||
#define DC_STR_INCOMING_VIDEO_CALL 235
|
||||
|
||||
/// "You changed the chat description."
|
||||
#define DC_STR_GROUP_DESCRIPTION_CHANGED_BY_YOU 240
|
||||
|
||||
/// "Chat description changed by %1$s."
|
||||
#define DC_STR_GROUP_DESCRIPTION_CHANGED_BY_OTHER 241
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
///
|
||||
/// Used when creating text for the "Encryption Info" dialogs.
|
||||
#define DC_STR_MESSAGES_ARE_E2EE 242
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,6 @@ use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Write;
|
||||
use std::future::Future;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::ptr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
@@ -60,6 +59,7 @@ use self::string::*;
|
||||
// - finally, this behaviour matches the old core-c API and UIs already depend on it
|
||||
|
||||
const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||
const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
|
||||
// dc_context_t
|
||||
|
||||
@@ -305,17 +305,20 @@ pub unsafe extern "C" fn dc_set_stock_translation(
|
||||
let msg = to_string_lossy(stock_msg);
|
||||
let ctx = &*context;
|
||||
|
||||
match StockMessage::from_u32(stock_id)
|
||||
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(id) => ctx
|
||||
.set_stock_translation(id, msg)
|
||||
.context("set_stock_translation failed")
|
||||
block_on(async move {
|
||||
match StockMessage::from_u32(stock_id)
|
||||
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => 0,
|
||||
}
|
||||
{
|
||||
Ok(id) => ctx
|
||||
.set_stock_translation(id, msg)
|
||||
.await
|
||||
.context("set_stock_translation failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -676,6 +679,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
@@ -698,9 +702,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device, ..
|
||||
} => *from_this_device as libc::c_int,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
@@ -1180,7 +1181,6 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
place_call_info: *const libc::c_char,
|
||||
has_video: bool,
|
||||
) -> u32 {
|
||||
if context.is_null() || chat_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_place_outgoing_call()");
|
||||
@@ -1190,7 +1190,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let place_call_info = to_string_lossy(place_call_info);
|
||||
|
||||
block_on(ctx.place_outgoing_call(chat_id, place_call_info, has_video))
|
||||
block_on(ctx.place_outgoing_call(chat_id, place_call_info))
|
||||
.context("Failed to place call")
|
||||
.log_err(ctx)
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
@@ -1337,13 +1337,17 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let info_only = (flags & DC_GCM_INFO_ONLY) != 0;
|
||||
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_msgs_ex(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
||||
@@ -1515,23 +1519,6 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_markfresh_chat(context: *mut dc_context_t, chat_id: u32) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_markfresh_chat()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::markfresh_chat(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.context("Failed markfresh chat")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(())
|
||||
})
|
||||
}
|
||||
|
||||
fn from_prim<S, T>(s: S) -> Option<T>
|
||||
where
|
||||
T: FromPrimitive,
|
||||
@@ -2438,6 +2425,45 @@ pub unsafe extern "C" fn dc_imex_has_backup(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_initiate_key_transfer()");
|
||||
return ptr::null_mut(); // NULL explicitly defined as "error"
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(imex::initiate_key_transfer(ctx))
|
||||
.context("dc_initiate_key_transfer()")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(res) => res.strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_continue_key_transfer(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
setup_code: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || msg_id <= constants::DC_MSG_ID_LAST_SPECIAL || setup_code.is_null() {
|
||||
eprintln!("ignoring careless call to dc_continue_key_transfer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(imex::continue_key_transfer(
|
||||
ctx,
|
||||
MsgId::new(msg_id),
|
||||
&to_string_lossy(setup_code),
|
||||
))
|
||||
.context("dc_continue_key_transfer")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_stop_ongoing_process(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
@@ -2541,7 +2567,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::send_to_chat(
|
||||
block_on(location::send_locations_to_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
seconds as i64,
|
||||
@@ -2561,14 +2587,14 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
if chat_id == 0 {
|
||||
block_on(location::is_sending(ctx))
|
||||
.unwrap_or_log_default(ctx, "Failed is_sending_locations()") as libc::c_int
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
block_on(location::is_sending_to_chat(ctx, ChatId::new(chat_id)))
|
||||
.unwrap_or_log_default(ctx, "Failed is_sending_locations_to_chat()")
|
||||
as libc::c_int
|
||||
}
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
|
||||
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2584,9 +2610,12 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::set(ctx, latitude, longitude, accuracy))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
}) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2621,6 +2650,23 @@ pub unsafe extern "C" fn dc_get_locations(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_delete_all_locations()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
location::delete_all(ctx)
|
||||
.await
|
||||
.context("Failed to delete locations")
|
||||
.log_err(ctx)
|
||||
.ok()
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
||||
if payload.is_null() {
|
||||
@@ -2801,7 +2847,7 @@ pub unsafe extern "C" fn dc_array_search_id(
|
||||
// Returns 1 if location belongs to the track of the user,
|
||||
// 0 if location was reported independently.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_is_independent(
|
||||
pub unsafe fn dc_array_is_independent(
|
||||
array: *const dc_array_t,
|
||||
index: libc::size_t,
|
||||
) -> libc::c_int {
|
||||
@@ -3740,6 +3786,16 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc
|
||||
ffi_msg.message.get_webxdc_href().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_is_setupmessage()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.is_setupmessage().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
@@ -3750,6 +3806,20 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
ffi_msg.message.has_html().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_setupcodebegin()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(ffi_msg.message.get_setupcodebegin(ctx))
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::c_char) {
|
||||
if msg.is_null() {
|
||||
@@ -4029,6 +4099,16 @@ pub unsafe extern "C" fn dc_msg_get_saved_msg_id(msg: *const dc_msg_t) -> u32 {
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_force_plaintext()");
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_msg.message.force_plaintext();
|
||||
}
|
||||
|
||||
// dc_contact_t
|
||||
|
||||
/// FFI struct for [dc_contact_t]
|
||||
@@ -5069,10 +5149,10 @@ pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let account_manager = ManuallyDrop::new(Arc::from_raw(account_manager));
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(Arc::clone(
|
||||
&account_manager,
|
||||
)));
|
||||
let account_manager = Arc::from_raw(account_manager);
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||
account_manager.clone(),
|
||||
));
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.50.0-dev"
|
||||
version = "2.39.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -11,8 +11,8 @@ use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
||||
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
get_chat_msgs_ex, marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat, Chat,
|
||||
ChatId, ChatItem, MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
@@ -31,7 +31,7 @@ use deltachat::peer_channels::{
|
||||
};
|
||||
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::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
@@ -68,7 +68,6 @@ use self::types::{
|
||||
},
|
||||
};
|
||||
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)]
|
||||
@@ -195,16 +194,6 @@ impl CommandApi {
|
||||
.context("event channel is closed")
|
||||
}
|
||||
|
||||
/// Waits for at least one event and return a batch of events.
|
||||
async fn get_next_event_batch(&self) -> Vec<Event> {
|
||||
self.event_emitter
|
||||
.recv_batch()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|event| event.into())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Account Management
|
||||
// ---------------------------------------------
|
||||
@@ -473,7 +462,9 @@ impl CommandApi {
|
||||
let accounts = self.accounts.read().await;
|
||||
for (stock_id, stock_message) in strings {
|
||||
if let Some(stock_id) = StockMessage::from_u32(stock_id) {
|
||||
accounts.set_stock_translation(stock_id, stock_message)?;
|
||||
accounts
|
||||
.set_stock_translation(stock_id, stock_message)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -527,7 +518,6 @@ 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,
|
||||
@@ -553,23 +543,7 @@ 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 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()
|
||||
@@ -587,26 +561,6 @@ impl CommandApi {
|
||||
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?;
|
||||
@@ -678,7 +632,7 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
|
||||
}
|
||||
|
||||
/// (deprecated) Gets messages to be processed by the bot and returns their IDs.
|
||||
/// Gets messages to be processed by the bot and returns their IDs.
|
||||
///
|
||||
/// Only messages with database ID higher than `last_msg_id` config value
|
||||
/// are returned. After processing the messages, the bot should
|
||||
@@ -686,13 +640,6 @@ impl CommandApi {
|
||||
/// or manually updating the value to avoid getting already
|
||||
/// processed messages.
|
||||
///
|
||||
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||
/// even if it is not fully downloaded yet.
|
||||
/// The bot needs to wait for the message to be fully downloaded.
|
||||
/// Since this is usually not the desired behavior,
|
||||
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
|
||||
/// event for getting notified about new messages.
|
||||
///
|
||||
/// [`markseen_msgs`]: Self::markseen_msgs
|
||||
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -705,7 +652,7 @@ impl CommandApi {
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// (deprecated) Waits for messages to be processed by the bot and returns their IDs.
|
||||
/// Waits for messages to be processed by the bot and returns their IDs.
|
||||
///
|
||||
/// This function is similar to [`get_next_msgs`],
|
||||
/// but waits for internal new message notification before returning.
|
||||
@@ -716,13 +663,6 @@ impl CommandApi {
|
||||
/// To shutdown the bot, stopping I/O can be used to interrupt
|
||||
/// pending or next `wait_next_msgs` call.
|
||||
///
|
||||
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||
/// even if it is not fully downloaded yet.
|
||||
/// The bot needs to wait for the message to be fully downloaded.
|
||||
/// Since this is usually not the desired behavior,
|
||||
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
|
||||
/// event for getting notified about new messages.
|
||||
///
|
||||
/// [`get_next_msgs`]: Self::get_next_msgs
|
||||
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -749,6 +689,25 @@ impl CommandApi {
|
||||
message::estimate_deletion_cnt(&ctx, from_server, seconds).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// autocrypt
|
||||
// ---------------------------------------------
|
||||
|
||||
async fn initiate_autocrypt_key_transfer(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
deltachat::imex::initiate_key_transfer(&ctx).await
|
||||
}
|
||||
|
||||
async fn continue_autocrypt_key_transfer(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
setup_code: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
deltachat::imex::continue_key_transfer(&ctx, MsgId::new(message_id), &setup_code).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// chat list
|
||||
// ---------------------------------------------
|
||||
@@ -895,8 +854,6 @@ impl CommandApi {
|
||||
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
/// an out-of-band-verification can be joined using `secure_join()`
|
||||
///
|
||||
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
|
||||
///
|
||||
/// chat_id: If set to a group-chat-id,
|
||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||
/// works for protected groups as well as for normal groups.
|
||||
@@ -1111,8 +1068,7 @@ impl CommandApi {
|
||||
/// Set group name.
|
||||
///
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// or if this is a brodacast channel,
|
||||
/// all members are informed by a special status message that is sent automatically by this function.
|
||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
async fn set_chat_name(&self, account_id: u32, chat_id: u32, new_name: String) -> Result<()> {
|
||||
@@ -1120,39 +1076,10 @@ impl CommandApi {
|
||||
chat::set_chat_name(&ctx, ChatId::new(chat_id), &new_name).await
|
||||
}
|
||||
|
||||
/// Set group or broadcast channel description.
|
||||
///
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// or if this is a brodacast channel,
|
||||
/// all members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
///
|
||||
/// See also [`Self::get_chat_description`] / `getChatDescription()`.
|
||||
async fn set_chat_description(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
description: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::set_chat_description(&ctx, ChatId::new(chat_id), &description).await
|
||||
}
|
||||
|
||||
/// Load the chat description from the database.
|
||||
///
|
||||
/// UIs show this in the profile page of the chat,
|
||||
/// it is settable by [`Self::set_chat_description`] / `setChatDescription()`.
|
||||
async fn get_chat_description(&self, account_id: u32, chat_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::get_chat_description(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Set group profile image.
|
||||
///
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// or if this is a brodacast channel,
|
||||
/// all members are informed by a special status message that is sent automatically by this function.
|
||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
///
|
||||
@@ -1263,15 +1190,6 @@ impl CommandApi {
|
||||
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Marks the last incoming message in the chat as _fresh_.
|
||||
///
|
||||
/// UI can use this to offer a "mark unread" option,
|
||||
/// so that already noticed chats get a badge counter again.
|
||||
async fn markfresh_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
markfresh_chat(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Returns the message that is immediately followed by the last seen
|
||||
/// message.
|
||||
/// From the point of view of the user this is effectively
|
||||
@@ -1366,22 +1284,8 @@ impl CommandApi {
|
||||
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
||||
}
|
||||
|
||||
/// Get all message IDs belonging to a chat.
|
||||
/// Returns all messages of a particular chat.
|
||||
///
|
||||
/// The list is already sorted and starts with the oldest message.
|
||||
/// Clients should not try to re-sort the list as this would be an expensive action
|
||||
/// and would result in inconsistencies between clients.
|
||||
/// Note that the messages are not necessarily sorted by their ID or by their displayed timestamp;
|
||||
/// UIs need to handle both the case of descending message IDs
|
||||
/// and of decreasing timestamps.
|
||||
///
|
||||
/// Optionally, 'daymarkers' added to the ID array may help to
|
||||
/// implement virtual lists.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * 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 `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.
|
||||
@@ -1389,14 +1293,17 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
_info_only: bool,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(msg
|
||||
@@ -1428,24 +1335,21 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all messages belonging to a chat.
|
||||
///
|
||||
/// Similar to `get_message_ids` / `getMessageIds`,
|
||||
/// see that function for details.
|
||||
/// The difference is that this function here returns a list of `MessageListItem`,
|
||||
/// which is an enum of a message or a daymarker.
|
||||
async fn get_message_list_items(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
_info_only: bool,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(msg
|
||||
@@ -2036,8 +1940,6 @@ impl CommandApi {
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 60 seconds to avoid deadlocks.
|
||||
///
|
||||
/// @deprecated as of 2026-03; use `create_qr_svg(get_backup_qr())` instead.
|
||||
///
|
||||
/// Returns the QR code rendered as an SVG image.
|
||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -2051,11 +1953,6 @@ impl CommandApi {
|
||||
generate_backup_qr(&ctx, &qr).await
|
||||
}
|
||||
|
||||
/// Renders the given text as a QR code SVG image.
|
||||
async fn create_qr_svg(&self, text: String) -> Result<String> {
|
||||
create_qr_svg(&text)
|
||||
}
|
||||
|
||||
/// Gets a backup from a remote provider.
|
||||
///
|
||||
/// This retrieves the backup from a remote device over the network and imports it into
|
||||
@@ -2120,21 +2017,6 @@ impl CommandApi {
|
||||
// locations
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Sets current location.
|
||||
///
|
||||
/// Returns true if location streaming is currently
|
||||
/// enabled and locations should be updated.
|
||||
///
|
||||
/// Location is represented as latitude and longitude in degrees
|
||||
/// and horizontal accuracy in meters.
|
||||
async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
self.accounts
|
||||
.read()
|
||||
.await
|
||||
.set_location(latitude, longitude, accuracy)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_locations(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2157,39 +2039,6 @@ impl CommandApi {
|
||||
Ok(locations.into_iter().map(|l| l.into()).collect())
|
||||
}
|
||||
|
||||
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
|
||||
///
|
||||
/// Pass 0 as the number of seconds to disable location streaming in the chat.
|
||||
async fn send_locations_to_chat(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
seconds: i64,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
location::send_to_chat(&ctx, chat_id, seconds).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether any chat is sending locations.
|
||||
async fn is_sending_locations(&self, account_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
location::is_sending(&ctx).await
|
||||
}
|
||||
|
||||
/// Returns whether `chat_id` is sending locations.
|
||||
async fn is_sending_locations_to_chat(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
location::is_sending_to_chat(&ctx, chat_id).await
|
||||
}
|
||||
|
||||
/// Stops sending locations to all chats.
|
||||
async fn stop_sending_locations(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_sending_locations().await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// webxdc
|
||||
// ---------------------------------------------
|
||||
@@ -2318,11 +2167,10 @@ impl CommandApi {
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
place_call_info: String,
|
||||
has_video: bool,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_id = ctx
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info, has_video)
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
|
||||
.await?;
|
||||
Ok(msg_id.to_u32())
|
||||
}
|
||||
@@ -2432,16 +2280,19 @@ impl CommandApi {
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(&ctx, Path::new(&sticker_path), None, None)?;
|
||||
|
||||
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||
msg.force_sticker();
|
||||
|
||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||
Ok(message_id.to_u32())
|
||||
}
|
||||
|
||||
/// Sends a reaction to message.
|
||||
/// Send a reaction to message.
|
||||
///
|
||||
/// A reaction is a string that represents an emoji.
|
||||
/// You can call this function again to change the emoji;
|
||||
/// the last sent reaction overrides all previously sent reactions.
|
||||
/// It is possible to remove the reaction by sending an empty string.
|
||||
/// Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
/// single message can be sent multiple times. The last reaction
|
||||
/// received overrides all previously received reactions. It is
|
||||
/// possible to remove all reactions by sending an empty string.
|
||||
async fn send_reaction(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2614,10 +2465,7 @@ impl CommandApi {
|
||||
continue;
|
||||
}
|
||||
let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default();
|
||||
if sticker_name.ends_with(".png")
|
||||
|| sticker_name.ends_with(".webp")
|
||||
|| sticker_name.ends_with(".gif")
|
||||
{
|
||||
if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") {
|
||||
sticker_paths.push(
|
||||
sticker_entry
|
||||
.path()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{call_state, CallState};
|
||||
use deltachat::calls::{call_state, sdp_has_video, CallState};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::MsgId;
|
||||
use serde::Serialize;
|
||||
@@ -15,7 +15,7 @@ pub struct JsonrpcCallInfo {
|
||||
/// even if incoming call event was missed.
|
||||
pub sdp_offer: String,
|
||||
|
||||
/// True if the call is started as a video call.
|
||||
/// True if SDP offer has a video.
|
||||
pub has_video: bool,
|
||||
|
||||
/// Call state.
|
||||
@@ -30,7 +30,7 @@ impl JsonrpcCallInfo {
|
||||
format!("Attempting to get call state of non-call message {msg_id}")
|
||||
})?;
|
||||
let sdp_offer = call_info.place_call_info.clone();
|
||||
let has_video = call_info.has_video_initially();
|
||||
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
|
||||
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
||||
|
||||
Ok(JsonrpcCallInfo {
|
||||
|
||||
@@ -441,8 +441,6 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
/// The call was accepted from this device (process).
|
||||
from_this_device: bool,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
@@ -636,14 +634,9 @@ impl From<CoreEventType> for EventType {
|
||||
place_call_info,
|
||||
has_video,
|
||||
},
|
||||
CoreEventType::IncomingCallAccepted {
|
||||
msg_id,
|
||||
chat_id,
|
||||
from_this_device,
|
||||
} => IncomingCallAccepted {
|
||||
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
from_this_device,
|
||||
},
|
||||
CoreEventType::OutgoingCallAccepted {
|
||||
msg_id,
|
||||
|
||||
@@ -4,16 +4,6 @@ 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`,
|
||||
@@ -33,12 +23,6 @@ pub struct EnteredLoginParam {
|
||||
/// Imap server port.
|
||||
pub imap_port: Option<u16>,
|
||||
|
||||
/// IMAP server folder.
|
||||
///
|
||||
/// Defaults to "INBOX" if not set.
|
||||
/// Should not be an empty string.
|
||||
pub imap_folder: Option<String>,
|
||||
|
||||
/// Imap socket security.
|
||||
pub imap_security: Option<Socket>,
|
||||
|
||||
@@ -72,15 +56,6 @@ pub struct EnteredLoginParam {
|
||||
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 {
|
||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||
let imap_security: Socket = param.imap.security.into();
|
||||
@@ -91,7 +66,6 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
password: param.imap.password,
|
||||
imap_server: param.imap.server.into_option(),
|
||||
imap_port: param.imap.port.into_option(),
|
||||
imap_folder: param.imap.folder.into_option(),
|
||||
imap_security: imap_security.into_option(),
|
||||
imap_user: param.imap.user.into_option(),
|
||||
smtp_server: param.smtp.server.into_option(),
|
||||
@@ -111,15 +85,14 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
||||
Ok(Self {
|
||||
addr: param.addr,
|
||||
imap: dc::EnteredImapLoginParam {
|
||||
imap: dc::EnteredServerLoginParam {
|
||||
server: param.imap_server.unwrap_or_default(),
|
||||
port: param.imap_port.unwrap_or_default(),
|
||||
folder: param.imap_folder.unwrap_or_default(),
|
||||
security: param.imap_security.unwrap_or_default().into(),
|
||||
user: param.imap_user.unwrap_or_default(),
|
||||
password: param.password,
|
||||
},
|
||||
smtp: dc::EnteredSmtpLoginParam {
|
||||
smtp: dc::EnteredServerLoginParam {
|
||||
server: param.smtp_server.unwrap_or_default(),
|
||||
port: param.smtp_port.unwrap_or_default(),
|
||||
security: param.smtp_security.unwrap_or_default().into(),
|
||||
|
||||
@@ -68,6 +68,7 @@ pub struct MessageObject {
|
||||
/// if `show_padlock` is `false`,
|
||||
/// and nothing if it is `true`.
|
||||
show_padlock: bool,
|
||||
is_setupmessage: bool,
|
||||
is_info: bool,
|
||||
is_forwarded: bool,
|
||||
|
||||
@@ -87,6 +88,8 @@ pub struct MessageObject {
|
||||
override_sender_name: Option<String>,
|
||||
sender: ContactObject,
|
||||
|
||||
setup_code_begin: Option<String>,
|
||||
|
||||
file: Option<String>,
|
||||
file_mime: Option<String>,
|
||||
|
||||
@@ -223,6 +226,7 @@ impl MessageObject {
|
||||
|
||||
subject: message.get_subject().to_owned(),
|
||||
show_padlock: message.get_showpadlock(),
|
||||
is_setupmessage: message.is_setupmessage(),
|
||||
is_info: message.is_info(),
|
||||
is_forwarded: message.is_forwarded(),
|
||||
is_bot: message.is_bot(),
|
||||
@@ -239,6 +243,8 @@ impl MessageObject {
|
||||
override_sender_name,
|
||||
sender,
|
||||
|
||||
setup_code_begin: message.get_setupcodebegin(context).await,
|
||||
|
||||
file: match message.get_file(context) {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
@@ -287,6 +293,8 @@ pub enum MessageViewtype {
|
||||
Gif,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
///
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
@@ -380,7 +388,6 @@ impl From<download::DownloadState> for DownloadState {
|
||||
pub enum SystemMessageType {
|
||||
Unknown,
|
||||
GroupNameChanged,
|
||||
GroupDescriptionChanged,
|
||||
GroupImageChanged,
|
||||
MemberAddedToGroup,
|
||||
MemberRemovedFromGroup,
|
||||
@@ -433,7 +440,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
match system_message_type {
|
||||
SystemMessage::Unknown => SystemMessageType::Unknown,
|
||||
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
||||
SystemMessage::GroupDescriptionChanged => SystemMessageType::GroupDescriptionChanged,
|
||||
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
||||
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
||||
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
||||
|
||||
@@ -19,8 +19,6 @@ pub enum QrObject {
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// Ask the user whether to join the group.
|
||||
AskVerifyGroup {
|
||||
@@ -36,8 +34,6 @@ pub enum QrObject {
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// Ask the user whether to join the broadcast channel.
|
||||
AskJoinBroadcast {
|
||||
@@ -58,8 +54,6 @@ pub enum QrObject {
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// Contact fingerprint is verified.
|
||||
///
|
||||
@@ -235,7 +229,6 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
@@ -244,7 +237,6 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
}
|
||||
}
|
||||
Qr::AskVerifyGroup {
|
||||
@@ -254,7 +246,6 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
@@ -265,7 +256,6 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
}
|
||||
}
|
||||
Qr::AskJoinBroadcast {
|
||||
@@ -275,7 +265,6 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
authcode,
|
||||
invitenumber,
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
@@ -286,7 +275,6 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
authcode,
|
||||
invitenumber,
|
||||
is_v3,
|
||||
}
|
||||
}
|
||||
Qr::FprOk { contact_id } => {
|
||||
|
||||
@@ -24,8 +24,6 @@ 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.
|
||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||
/// Unique reactions and their count, sorted in descending order.
|
||||
reactions: Vec<JsonrpcReaction>,
|
||||
@@ -33,16 +31,27 @@ pub struct JsonrpcReactions {
|
||||
|
||||
impl From<Reactions> for JsonrpcReactions {
|
||||
fn from(reactions: Reactions) -> Self {
|
||||
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
|
||||
.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 mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||
|
||||
for contact_id in reactions.contacts() {
|
||||
let reaction = reactions.get(contact_id);
|
||||
if reaction.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let emojis: Vec<String> = reaction
|
||||
.emojis()
|
||||
.into_iter()
|
||||
.map(|emoji| emoji.to_owned())
|
||||
.collect();
|
||||
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
||||
}
|
||||
|
||||
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||
|
||||
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)
|
||||
let is_from_self = if let Some(self_reactions) = self_reactions {
|
||||
self_reactions.contains(&emoji)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
@@ -37,10 +37,6 @@ pub struct WebxdcMessageInfo {
|
||||
internet_access: bool,
|
||||
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
self_addr: String,
|
||||
/// Define if the local user is the one who initially shared the webxdc application in the chat.
|
||||
is_app_sender: bool,
|
||||
/// Define if the app runs in a broadcasting context.
|
||||
is_broadcast: bool,
|
||||
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
||||
send_update_interval: usize,
|
||||
@@ -64,8 +60,6 @@ impl WebxdcMessageInfo {
|
||||
request_integration: _,
|
||||
internet_access,
|
||||
self_addr,
|
||||
is_app_sender,
|
||||
is_broadcast,
|
||||
send_update_interval,
|
||||
send_update_max_size,
|
||||
} = message.get_webxdc_info(context).await?;
|
||||
@@ -78,8 +72,6 @@ impl WebxdcMessageInfo {
|
||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||
internet_access,
|
||||
self_addr,
|
||||
is_app_sender,
|
||||
is_broadcast,
|
||||
send_update_interval,
|
||||
send_update_max_size,
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ mod tests {
|
||||
assert_eq!(result, response.to_owned());
|
||||
}
|
||||
{
|
||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":""}]}"#;
|
||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||
session.handle_incoming(request).await;
|
||||
let result = receiver.recv().await?;
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.50.0-dev"
|
||||
"version": "2.39.0"
|
||||
}
|
||||
|
||||
@@ -53,19 +53,18 @@ export class BaseDeltaChat<
|
||||
*/
|
||||
async eventLoop(): Promise<void> {
|
||||
while (true) {
|
||||
for (const event of await this.rpc.getNextEventBatch()) {
|
||||
//@ts-ignore
|
||||
this.emit(event.event.kind, event.contextId, event.event);
|
||||
this.emit("ALL", event.contextId, event.event);
|
||||
const event = await this.rpc.getNextEvent();
|
||||
//@ts-ignore
|
||||
this.emit(event.event.kind, event.contextId, event.event);
|
||||
this.emit("ALL", event.contextId, event.event);
|
||||
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.kind,
|
||||
//@ts-ignore
|
||||
event.event as any,
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||
}
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.kind,
|
||||
//@ts-ignore
|
||||
event.event as any,
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.50.0-dev"
|
||||
version = "2.39.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -302,6 +302,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
// TODO: reuse commands definition in main.rs.
|
||||
"imex" => println!(
|
||||
"====================Import/Export commands==\n\
|
||||
initiate-key-transfer\n\
|
||||
get-setupcodebegin <msg-id>\n\
|
||||
continue-key-transfer <msg-id> <setup-code>\n\
|
||||
has-backup\n\
|
||||
export-backup\n\
|
||||
import-backup <backup-file>\n\
|
||||
@@ -340,11 +343,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
groupdescription <description>\n\
|
||||
groupimage <image>\n\
|
||||
chatinfo\n\
|
||||
sendlocations <seconds>\n\
|
||||
setlocation <lat> <lng>\n\
|
||||
dellocations\n\
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
send-sync <text>\n\
|
||||
@@ -404,6 +407,34 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
============================================="
|
||||
),
|
||||
},
|
||||
"initiate-key-transfer" => match initiate_key_transfer(&context).await {
|
||||
Ok(setup_code) => {
|
||||
println!("Setup code for the transferred setup message: {setup_code}",)
|
||||
}
|
||||
Err(err) => bail!("Failed to generate setup code: {err}"),
|
||||
},
|
||||
"get-setupcodebegin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let msg_id: MsgId = MsgId::new(arg1.parse()?);
|
||||
let msg = Message::load_from_db(&context, msg_id).await?;
|
||||
if msg.is_setupmessage() {
|
||||
let setupcodebegin = msg.get_setupcodebegin(&context).await;
|
||||
println!(
|
||||
"The setup code for setup message {} starts with: {}",
|
||||
msg_id,
|
||||
setupcodebegin.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
bail!("{msg_id} is no setup message.",);
|
||||
}
|
||||
}
|
||||
"continue-key-transfer" => {
|
||||
ensure!(
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <setup-code> expected"
|
||||
);
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
}
|
||||
@@ -573,7 +604,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending(&context).await? {
|
||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
@@ -622,6 +653,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
&context,
|
||||
sel_chat.get_id(),
|
||||
chat::MessageListOptions {
|
||||
info_only: false,
|
||||
add_daymarker: true,
|
||||
},
|
||||
)
|
||||
@@ -738,13 +770,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!("Chat name set");
|
||||
}
|
||||
"groupdescription" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <description> missing.");
|
||||
chat::set_chat_description(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||
|
||||
println!("Chat description set");
|
||||
}
|
||||
"groupimage" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
||||
@@ -780,7 +805,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!(
|
||||
"Location streaming: {}",
|
||||
location::is_sending_to_chat(&context, sel_chat.as_ref().unwrap().get_id()).await?,
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
"getlocations" => {
|
||||
@@ -820,7 +849,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "No timeout given.");
|
||||
|
||||
let seconds = arg1.parse()?;
|
||||
location::send_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), seconds).await?;
|
||||
location::send_locations_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
seconds,
|
||||
)
|
||||
.await?;
|
||||
println!(
|
||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -842,6 +876,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Success, streaming can be stopped.");
|
||||
}
|
||||
}
|
||||
"dellocations" => {
|
||||
location::delete_all(&context).await?;
|
||||
}
|
||||
"send" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No message text given.");
|
||||
|
||||
@@ -149,7 +149,10 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
const IMEX_COMMANDS: [&str; 13] = [
|
||||
"initiate-key-transfer",
|
||||
"get-setupcodebegin",
|
||||
"continue-key-transfer",
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -189,11 +192,11 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"addmember",
|
||||
"removemember",
|
||||
"groupname",
|
||||
"groupdescription",
|
||||
"groupimage",
|
||||
"chatinfo",
|
||||
"sendlocations",
|
||||
"setlocation",
|
||||
"dellocations",
|
||||
"getlocations",
|
||||
"send",
|
||||
"send-sync",
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
|
||||
and provides asynchronous interface to it.
|
||||
`rpc.start()` performs a health-check RPC call to verify the server
|
||||
started successfully and will raise an error if startup fails
|
||||
(e.g. if the accounts directory could not be used).
|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.50.0-dev"
|
||||
version = "2.39.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -8,7 +8,7 @@ from .const import EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
from .rpc import JsonRpcError, Rpc
|
||||
from .rpc import Rpc
|
||||
|
||||
__all__ = [
|
||||
"Account",
|
||||
@@ -19,7 +19,6 @@ __all__ = [
|
||||
"Contact",
|
||||
"DeltaChat",
|
||||
"EventType",
|
||||
"JsonRpcError",
|
||||
"Message",
|
||||
"SpecialContactId",
|
||||
"Rpc",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import argparse
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -190,6 +189,9 @@ class futuremethod: # noqa: N801
|
||||
self._func = func
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
def future(*args):
|
||||
generator = self._func(instance, *args)
|
||||
res = next(generator)
|
||||
@@ -202,7 +204,6 @@ class futuremethod: # noqa: N801
|
||||
|
||||
return f
|
||||
|
||||
@functools.wraps(self._func)
|
||||
def wrapper(*args):
|
||||
f = future(*args)
|
||||
return f()
|
||||
|
||||
@@ -405,15 +405,7 @@ class Account:
|
||||
|
||||
@futuremethod
|
||||
def wait_next_messages(self) -> list[Message]:
|
||||
"""(deprecated) Wait for new messages and return a list of them. Meant for bots.
|
||||
|
||||
Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||
even if it is not fully downloaded yet.
|
||||
The bot needs to wait for the message to be fully downloaded.
|
||||
Since this is usually not the desired behavior,
|
||||
bots should instead use the `EventType.INCOMING_MSG`
|
||||
event for getting notified about new messages.
|
||||
"""
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
@@ -491,11 +483,11 @@ class Account:
|
||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||
|
||||
def initiate_autocrypt_key_transfer(self) -> None:
|
||||
"""Send Autocrypt Setup Message."""
|
||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||
|
||||
def ice_servers(self) -> list:
|
||||
"""Return ICE servers for WebRTC configuration."""
|
||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
||||
return json.loads(ice_servers_json)
|
||||
|
||||
def is_sending_locations(self) -> bool:
|
||||
"""Return True if sending locations to any chat."""
|
||||
return self._rpc.is_sending_locations(self.id)
|
||||
|
||||
@@ -206,9 +206,9 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, add_daymarker: bool = False) -> list[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
"""Get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, False, add_daymarker)
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
def get_fresh_message_count(self) -> int:
|
||||
@@ -219,10 +219,6 @@ class Chat:
|
||||
"""Mark all messages in this chat as noticed."""
|
||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
|
||||
def mark_fresh(self) -> None:
|
||||
"""Mark the last incoming message in the chat as fresh."""
|
||||
self._rpc.markfresh_chat(self.account.id, self.id)
|
||||
|
||||
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||
"""Add contacts to this group."""
|
||||
from .account import Account
|
||||
@@ -277,16 +273,6 @@ class Chat:
|
||||
"""Remove profile image of this chat."""
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
|
||||
def send_locations(self, seconds) -> None:
|
||||
"""Enable location streaming in the chat for the given number of seconds.
|
||||
|
||||
Pass 0 to disable location streaming."""
|
||||
self._rpc.send_locations_to_chat(self.account.id, self.id, seconds)
|
||||
|
||||
def is_sending_locations(self) -> bool:
|
||||
"""Return True if sending locations to this chat."""
|
||||
return self._rpc.is_sending_locations_to_chat(self.account.id, self.id)
|
||||
|
||||
def get_locations(
|
||||
self,
|
||||
contact: Optional[Contact] = None,
|
||||
@@ -317,7 +303,7 @@ class Chat:
|
||||
f.flush()
|
||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||
|
||||
def place_outgoing_call(self, place_call_info: str, has_video_initially: bool) -> Message:
|
||||
def place_outgoing_call(self, place_call_info: str) -> Message:
|
||||
"""Starts an outgoing call."""
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info, has_video_initially)
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -104,7 +104,7 @@ class Client:
|
||||
|
||||
def _process_events(
|
||||
self,
|
||||
until_func: Callable[[AttrDict], bool] = _forever,
|
||||
until_func: Callable[[AttrDict], bool],
|
||||
until_event: EventType = False,
|
||||
) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True,
|
||||
|
||||
@@ -59,11 +59,3 @@ class DeltaChat:
|
||||
def set_translations(self, translations: dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
def set_location(self, latitude, longitude, accuracy) -> bool:
|
||||
"""Set location, return True if location streaming should continue."""
|
||||
return self.rpc.set_location(latitude, longitude, accuracy)
|
||||
|
||||
def stop_sending_locations(self) -> None:
|
||||
"""Stop sending locations to all chats."""
|
||||
return self.rpc.stop_sending_locations()
|
||||
|
||||
@@ -25,14 +25,7 @@ class Message:
|
||||
return self.account._rpc
|
||||
|
||||
def send_reaction(self, *reaction: str) -> "Message":
|
||||
"""
|
||||
Sends a reaction to message.
|
||||
|
||||
A reaction is a string that represents an emoji.
|
||||
You can call this function again to change the emoji;
|
||||
the last sent reaction overrides all previously sent reactions.
|
||||
It is possible to remove the reaction by sending an empty string.
|
||||
"""
|
||||
"""Send a reaction to this message."""
|
||||
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -79,6 +72,14 @@ class Message:
|
||||
"""Return True if the message exists."""
|
||||
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
"""Continue the Autocrypt Setup Message key transfer.
|
||||
|
||||
This function can be called on received Autocrypt Setup Message
|
||||
to import the key encrypted with the provided setup code.
|
||||
"""
|
||||
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
||||
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
|
||||
@@ -54,13 +54,13 @@ class ACFactory:
|
||||
|
||||
def get_credentials(self) -> (str, str):
|
||||
"""Generate new credentials for chatmail account."""
|
||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
return f"{username}@{domain}", f"{username}${username}"
|
||||
|
||||
def get_account_qr(self):
|
||||
"""Return "dcaccount:" QR code for testing chatmail relay."""
|
||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
return f"dcaccount:{domain}"
|
||||
|
||||
@futuremethod
|
||||
|
||||
@@ -54,12 +54,7 @@ class RpcMethod:
|
||||
class Rpc:
|
||||
"""RPC client."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
accounts_dir: Optional[str] = None,
|
||||
rpc_server_path="deltachat-rpc-server",
|
||||
**kwargs,
|
||||
):
|
||||
def __init__(self, accounts_dir: Optional[str] = None, rpc_server_path="deltachat-rpc-server", **kwargs):
|
||||
"""Initialize RPC client.
|
||||
|
||||
The 'kwargs' arguments will be passed to subprocess.Popen().
|
||||
@@ -84,15 +79,8 @@ class Rpc:
|
||||
self.events_thread: Thread
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start RPC server subprocess and wait for successful initialization.
|
||||
|
||||
This method blocks until the RPC server responds to an initial
|
||||
health-check RPC call (get_system_info).
|
||||
If the server fails to start
|
||||
(e.g., due to an invalid accounts directory),
|
||||
a JsonRpcError is raised.
|
||||
"""
|
||||
popen_kwargs = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE, "stderr": subprocess.PIPE}
|
||||
"""Start RPC server subprocess."""
|
||||
popen_kwargs = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE}
|
||||
if sys.version_info >= (3, 11):
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
popen_kwargs["process_group"] = 0
|
||||
@@ -102,7 +90,6 @@ class Rpc:
|
||||
|
||||
popen_kwargs.update(self._kwargs)
|
||||
self.process = subprocess.Popen(self.rpc_server_path, **popen_kwargs)
|
||||
|
||||
self.id_iterator = itertools.count(start=1)
|
||||
self.event_queues = {}
|
||||
self.request_results = {}
|
||||
@@ -115,22 +102,6 @@ class Rpc:
|
||||
self.events_thread = Thread(target=self.events_loop)
|
||||
self.events_thread.start()
|
||||
|
||||
# Perform a health-check RPC call to ensure the server started
|
||||
# successfully and the accounts directory is usable.
|
||||
try:
|
||||
system_info = self.get_system_info()
|
||||
except (JsonRpcError, Exception) as e:
|
||||
# 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()
|
||||
if stderr:
|
||||
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
|
||||
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
|
||||
logging.info(
|
||||
"RPC server ready. Core version: %s",
|
||||
system_info.get("deltachat_core_version", "unknown"),
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
@@ -161,10 +132,6 @@ class Rpc:
|
||||
except Exception:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
finally:
|
||||
# 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."""
|
||||
@@ -173,6 +140,7 @@ class Rpc:
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data)
|
||||
self.process.stdin.flush()
|
||||
|
||||
except Exception:
|
||||
# Log an exception if the writer loop dies.
|
||||
logging.exception("Exception in the writer loop")
|
||||
@@ -186,15 +154,15 @@ class Rpc:
|
||||
def events_loop(self) -> None:
|
||||
"""Request new events and distributes them between queues."""
|
||||
try:
|
||||
while events := self.get_next_event_batch():
|
||||
for event in events:
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
payload = event["event"]
|
||||
logging.debug("account_id=%d got an event %s", account_id, payload)
|
||||
queue.put(payload)
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
event = event["event"]
|
||||
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||
queue.put(event)
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
|
||||
@@ -10,15 +10,15 @@ def test_calls(acfactory) -> None:
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info, has_video_initially=True)
|
||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
|
||||
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == place_call_info
|
||||
assert incoming_call_event.has_video
|
||||
assert not incoming_call_event.has_video # Cannot be parsed as SDP, so false by default
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert incoming_call_message.get_call_info().state.kind == "Alerting"
|
||||
assert incoming_call_message.get_call_info().has_video
|
||||
assert not incoming_call_message.get_call_info().has_video
|
||||
|
||||
incoming_call_message.accept_incoming_call(accept_call_info)
|
||||
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
|
||||
@@ -41,38 +41,46 @@ def test_video_call(acfactory) -> None:
|
||||
#
|
||||
# `s=` cannot be empty according to RFC 3264,
|
||||
# so it is more clear as `s=-`.
|
||||
place_call_info = """v=0\r
|
||||
o=alice 2890844526 2890844526 IN IP6 2001:db8::3\r
|
||||
s=-\r
|
||||
c=IN IP6 2001:db8::3\r
|
||||
t=0 0\r
|
||||
a=group:BUNDLE foo bar\r
|
||||
\r
|
||||
m=audio 10000 RTP/AVP 0 8 97\r
|
||||
b=AS:200\r
|
||||
a=mid:foo\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:0 PCMU/8000\r
|
||||
a=rtpmap:8 PCMA/8000\r
|
||||
a=rtpmap:97 iLBC/8000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
\r
|
||||
m=video 10002 RTP/AVP 31 32\r
|
||||
b=AS:1000\r
|
||||
a=mid:bar\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:31 H261/90000\r
|
||||
a=rtpmap:32 MPV/90000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
"""
|
||||
|
||||
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")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
alice_chat_bob.place_outgoing_call(place_call_info)
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == "offer"
|
||||
assert incoming_call_event.place_call_info == place_call_info
|
||||
assert incoming_call_event.has_video
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
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")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=False)
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == "offer"
|
||||
assert not incoming_call_event.has_video
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert not incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
def test_ice_servers(acfactory) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
@@ -84,7 +92,7 @@ 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)
|
||||
alice_chat_bob.place_outgoing_call("offer")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
# Notification for "Hello!" message should arrive
|
||||
@@ -111,7 +119,7 @@ def test_who_can_call_me_nobody(acfactory) -> None:
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
alice_chat_bob.place_outgoing_call("offer")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
# Notification for "Hello!" message should arrive
|
||||
@@ -136,7 +144,7 @@ def test_who_can_call_me_everybody(acfactory) -> None:
|
||||
bob.set_config("who_can_call_me", "0")
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
alice_chat_bob.place_outgoing_call("offer")
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
|
||||
@@ -1,26 +1,322 @@
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client import Contact, EventType, Message
|
||||
|
||||
|
||||
def test_moved_markseen(acfactory, direct_imap, log):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.get_online_account()
|
||||
def test_move_works(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.bring_online()
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
chat.send_text("message1")
|
||||
|
||||
# Message is moved to the movebox
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
|
||||
# Message is downloaded
|
||||
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "message1"
|
||||
|
||||
|
||||
def test_move_avoids_loop(acfactory, direct_imap):
|
||||
"""Test that the message is only moved from INBOX to DeltaChat.
|
||||
|
||||
This is to avoid busy loop if moved message reappears in the Inbox
|
||||
or some scanned folder later.
|
||||
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
|
||||
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
|
||||
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.bring_online()
|
||||
|
||||
# Create INBOX.DeltaChat folder and make sure
|
||||
# it is detected by full folder scan.
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("INBOX.DeltaChat")
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
|
||||
while True:
|
||||
event = ac2.wait_for_event()
|
||||
# Wait until the end of folder scan.
|
||||
if event.kind == EventType.INFO and "Found folders:" in event.msg:
|
||||
break
|
||||
|
||||
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1_chat.send_text("Message 1")
|
||||
|
||||
# Message is moved to the DeltaChat folder and downloaded.
|
||||
ac2_msg1 = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert ac2_msg1.text == "Message 1"
|
||||
|
||||
# Move the message to the INBOX.DeltaChat again.
|
||||
# We assume that test server uses "." as the delimiter.
|
||||
ac2_direct_imap.select_folder("DeltaChat")
|
||||
ac2_direct_imap.conn.move(["*"], "INBOX.DeltaChat")
|
||||
|
||||
ac1_chat.send_text("Message 2")
|
||||
ac2_msg2 = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Stop and start I/O to trigger folder scan.
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
while True:
|
||||
event = ac2.wait_for_event()
|
||||
# Wait until the end of folder scan.
|
||||
if event.kind == EventType.INFO and "Found folders:" in event.msg:
|
||||
break
|
||||
|
||||
# Check that Message 1 is still in the INBOX.DeltaChat folder
|
||||
# and Message 2 is in the DeltaChat folder.
|
||||
ac2_direct_imap.select_folder("INBOX")
|
||||
assert len(ac2_direct_imap.get_all_messages()) == 0
|
||||
ac2_direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac2_direct_imap.get_all_messages()) == 1
|
||||
ac2_direct_imap.select_folder("INBOX.DeltaChat")
|
||||
assert len(ac2_direct_imap.get_all_messages()) == 1
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2.bring_online()
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
assert ac2.is_configured()
|
||||
|
||||
log.section("ac2: creating DeltaChat folder")
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
msg1.wait_until_delivered()
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
logging.info("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_delete_deltachat_folder(acfactory, direct_imap):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.new_configured_account()
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.conn.folder.delete("DeltaChat")
|
||||
assert "DeltaChat" not in ac1_direct_imap.list_folders()
|
||||
|
||||
# Wait until new folder is created and UIDVALIDITY is updated.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.INFO and "transport 1: UID validity for folder DeltaChat changed from " in event.msg:
|
||||
break
|
||||
|
||||
ac2 = acfactory.get_online_account()
|
||||
ac2.create_chat(ac1).send_text("hello")
|
||||
msg = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
|
||||
assert "DeltaChat" in ac1_direct_imap.list_folders()
|
||||
|
||||
|
||||
def test_dont_show_emails(acfactory, direct_imap, log):
|
||||
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
|
||||
So: If it's outgoing AND there is no Received header, then ignore the email.
|
||||
|
||||
If the draft email is sent out and received later (i.e. it's in "Inbox"), it must be shown.
|
||||
|
||||
Also, test that unknown emails in the Spam folder are not shown."""
|
||||
ac1 = acfactory.new_configured_account()
|
||||
ac1.stop_io()
|
||||
ac1.set_config("show_emails", "2")
|
||||
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder("Drafts")
|
||||
ac1_direct_imap.create_folder("Spam")
|
||||
ac1_direct_imap.create_folder("Junk")
|
||||
|
||||
# Learn UID validity for all folders.
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.start_io()
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
ac1.stop_io()
|
||||
|
||||
ac1_direct_imap.append(
|
||||
"Drafts",
|
||||
"""
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts received later
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org, unkwnown.add@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message2@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: delta<address: inbox@nhroy.com>
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message99@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: alice@example.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message3@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Actually interesting message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Junk",
|
||||
"""
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Junk
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
log.section("All prepared, now let DC find the message")
|
||||
ac1.start_io()
|
||||
|
||||
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
|
||||
fresh_msgs = list(ac1.get_fresh_messages())
|
||||
msg = fresh_msgs[0].get_snapshot()
|
||||
chat_msgs = msg.chat.get_messages()
|
||||
assert len(chat_msgs) == 1
|
||||
assert msg.text == "subj – Actually interesting message in Spam"
|
||||
|
||||
assert not any("unknown.address" in c.get_full_snapshot().name for c in ac1.get_chatlist())
|
||||
ac1_direct_imap.select_folder("Spam")
|
||||
assert ac1_direct_imap.get_uid_by_message_id("spam.message@junk.org")
|
||||
|
||||
ac1.stop_io()
|
||||
log.section("'Send out' the draft by moving it to Inbox, and wait for DC to display it this time")
|
||||
ac1_direct_imap.select_folder("Drafts")
|
||||
uid = ac1_direct_imap.get_uid_by_message_id("aepiors@example.org")
|
||||
ac1_direct_imap.conn.move(uid, "Inbox")
|
||||
|
||||
ac1.start_io()
|
||||
event = ac1.wait_for_event(EventType.MSGS_CHANGED)
|
||||
msg2 = Message(ac1, event.msg_id).get_snapshot()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts received later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
|
||||
def test_move_works_on_self_sent(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# Enable movebox and wait until it is created.
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
chat.send_text("message1")
|
||||
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
chat.send_text("message2")
|
||||
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
chat.send_text("message3")
|
||||
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
|
||||
|
||||
def test_moved_markseen(acfactory, direct_imap):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
||||
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
|
||||
ac2.bring_online()
|
||||
|
||||
ac2.stop_io()
|
||||
@@ -30,7 +326,6 @@ def test_moved_markseen(acfactory, direct_imap, log):
|
||||
idle2.wait_for_new_message()
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
log.section("ac2: moving message into DeltaChat folder")
|
||||
ac2_direct_imap.conn.move(["*"], "DeltaChat")
|
||||
ac2_direct_imap.select_folder("DeltaChat")
|
||||
assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1
|
||||
@@ -54,11 +349,15 @@ def test_moved_markseen(acfactory, 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(acfactory, direct_imap):
|
||||
@pytest.mark.parametrize("mvbox_move", [True, False])
|
||||
def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
for ac in ac1, ac2:
|
||||
ac.set_config("delete_server_after", "0")
|
||||
if mvbox_move:
|
||||
ac.set_config("mvbox_move", "1")
|
||||
ac.bring_online()
|
||||
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
@@ -67,7 +366,10 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
msg = ac2.wait_for_incoming_msg()
|
||||
msg.mark_seen()
|
||||
|
||||
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
if mvbox_move:
|
||||
rex = re.compile("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
else:
|
||||
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
for ac in ac1, ac2:
|
||||
while True:
|
||||
@@ -75,11 +377,12 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
if event.kind == EventType.INFO and rex.search(event.msg):
|
||||
break
|
||||
|
||||
folder = "mvbox" if mvbox_move else "inbox"
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
|
||||
ac1_direct_imap.select_folder("INBOX")
|
||||
ac2_direct_imap.select_folder("INBOX")
|
||||
ac1_direct_imap.select_config_folder(folder)
|
||||
ac2_direct_imap.select_config_folder(folder)
|
||||
|
||||
# Check that the mdn is marked as seen
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
@@ -87,12 +390,120 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
|
||||
|
||||
def test_mvbox_and_trash(acfactory, direct_imap, log):
|
||||
log.section("ac1: start with mvbox")
|
||||
ac1 = acfactory.get_online_account()
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
log.section("ac2: start without a mvbox")
|
||||
ac2 = acfactory.get_online_account()
|
||||
|
||||
log.section("ac1: create trash")
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder("Trash")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.stop_io()
|
||||
ac1.start_io()
|
||||
|
||||
log.section("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2.wait_for_incoming_msg().get_snapshot().text == "message1"
|
||||
|
||||
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
|
||||
while ac1.get_config("configured_trash_folder") != "Trash":
|
||||
ac1.wait_for_event(EventType.CONNECTIVITY_CHANGED)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("folder", "move", "expected_destination"),
|
||||
[
|
||||
(
|
||||
"xyz",
|
||||
False,
|
||||
"xyz",
|
||||
), # Test that emails aren't found in a random folder
|
||||
(
|
||||
"xyz",
|
||||
True,
|
||||
"xyz",
|
||||
), # ...emails are found in a random folder and downloaded without moving
|
||||
(
|
||||
"Spam",
|
||||
False,
|
||||
"INBOX",
|
||||
), # ...emails are moved from the spam folder to the Inbox
|
||||
],
|
||||
)
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(acfactory, log, direct_imap, folder, move, expected_destination):
|
||||
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
|
||||
variant = folder + "-" + str(move) + "-" + expected_destination
|
||||
log.section("Testing variant " + variant)
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("delete_server_after", "0")
|
||||
if move:
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
ac1.stop_io()
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder(folder)
|
||||
|
||||
# Wait until each folder was selected once and we are IDLEing:
|
||||
ac1.start_io()
|
||||
ac1.bring_online()
|
||||
|
||||
ac1.stop_io()
|
||||
assert folder in ac1_direct_imap.list_folders()
|
||||
|
||||
log.section("Send a message from ac2 to ac1 and manually move it to `folder`")
|
||||
ac1_direct_imap.select_config_folder("inbox")
|
||||
with ac1_direct_imap.idle() as idle1:
|
||||
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
|
||||
idle1.wait_for_new_message()
|
||||
ac1_direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
|
||||
|
||||
log.section("start_io() and see if DeltaChat finds the message (" + variant + ")")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.start_io()
|
||||
chat = ac1.create_chat(ac2)
|
||||
n_msgs = 1 # "Messages are end-to-end encrypted."
|
||||
if folder == "Spam":
|
||||
msg = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
n_msgs += 1
|
||||
else:
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
assert len(chat.get_messages()) == n_msgs
|
||||
|
||||
# The message has reached its destination.
|
||||
ac1_direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1_direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
ac1_direct_imap.select_folder(folder)
|
||||
assert len(ac1_direct_imap.get_all_messages()) == 0
|
||||
|
||||
|
||||
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
|
||||
# Trash wasn't configured initially, it can't be configured later, let's check this.
|
||||
log.section("Creating trash folder")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("Trash")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0")
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
|
||||
log.section("Check that Trash can be configured initially as well")
|
||||
ac3 = ac2.clone()
|
||||
ac3.bring_online()
|
||||
assert ac3.get_config("configured_trash_folder")
|
||||
ac3.stop_io()
|
||||
|
||||
ac2.start_io()
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -109,15 +520,17 @@ def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
assert msg.text in texts
|
||||
if text != "second":
|
||||
to_delete.append(msg)
|
||||
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
|
||||
# check the configuration.
|
||||
assert ac2.get_config("configured_trash_folder") == "Trash"
|
||||
|
||||
log.section("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
ac2.delete_messages(to_delete)
|
||||
|
||||
log.section("ac2: test that only one message is left")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
while 1:
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
ac2_direct_imap.select_config_folder("inbox")
|
||||
nr_msgs = len(ac2_direct_imap.get_all_messages())
|
||||
assert nr_msgs > 0
|
||||
|
||||
@@ -24,13 +24,6 @@ def path_to_webxdc(request):
|
||||
return str(p)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def path_to_large_webxdc(request):
|
||||
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/realtime-check.xdc")
|
||||
assert p.exists()
|
||||
return str(p)
|
||||
|
||||
|
||||
def log(msg):
|
||||
logging.info(msg)
|
||||
|
||||
@@ -234,29 +227,3 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
|
||||
|
||||
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 = 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)
|
||||
|
||||
# Receive pre-message.
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
|
||||
# Receive post-message.
|
||||
ac2_webxdc_msg = ac2.wait_for_msg(EventType.MSGS_CHANGED)
|
||||
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
|
||||
49
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
49
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def wait_for_autocrypt_setup_message(account):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
|
||||
msg_id = event.msg_id
|
||||
msg = account.get_message_by_id(msg_id)
|
||||
if msg.get_snapshot().is_setupmessage:
|
||||
return msg
|
||||
|
||||
|
||||
def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Test that entering wrong code returns an error.
|
||||
with pytest.raises(JsonRpcError):
|
||||
msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
|
||||
|
||||
def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
# Send the first Autocrypt Setup Message and ignore it.
|
||||
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Send the second Autocrypt Setup Message and import it.
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
@@ -1,32 +0,0 @@
|
||||
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.
|
||||
acfactory.new_configured_account()
|
||||
assert not dc.set_location(3.0, 4.0, 0.1)
|
||||
|
||||
|
||||
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)
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
|
||||
# Test starting and stopping location streaming in a chat.
|
||||
alice_chat_bob.send_locations(3600)
|
||||
assert alice.is_sending_locations()
|
||||
assert alice_chat_bob.is_sending_locations()
|
||||
alice_chat_bob.send_locations(0)
|
||||
assert not alice.is_sending_locations()
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
|
||||
# Test stop_sending_locations() for all accounts and chats.
|
||||
alice_chat_bob.send_locations(3600)
|
||||
assert alice.is_sending_locations()
|
||||
assert alice_chat_bob.is_sending_locations()
|
||||
dc.stop_sending_locations()
|
||||
assert not alice.is_sending_locations()
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
@@ -1,7 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -9,6 +8,12 @@ def test_add_second_address(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
# When the first transport is created,
|
||||
# mvbox_move and only_fetch_mvbox should be disabled.
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("only_fetch_mvbox") == "0"
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 2
|
||||
@@ -26,6 +31,45 @@ def test_add_second_address(acfactory) -> None:
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
# Enabling mvbox_move or only_fetch_mvbox
|
||||
# is not allowed when multi-transport is enabled.
|
||||
for option in ["mvbox_move", "only_fetch_mvbox"]:
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.set_config(option, "1")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
|
||||
def test_no_second_transport_with_mvbox(acfactory, key) -> None:
|
||||
"""Test that second transport cannot be configured if mvbox is used."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("only_fetch_mvbox") == "0"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config(key, "1")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_no_second_transport_without_classic_emails(acfactory) -> None:
|
||||
"""Test that second transport cannot be configured if classic emails are not fetched."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_change_address(acfactory) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
@@ -76,50 +120,47 @@ def test_change_address(acfactory) -> None:
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice.set_config("download_limit", "1")
|
||||
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
|
||||
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
|
||||
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
|
||||
Disabling mvbox_move is required to be able to setup a second transport.
|
||||
"""
|
||||
account = acfactory.get_unconfigured_account()
|
||||
|
||||
account.set_config("fix_is_chatmail", "1")
|
||||
account.set_config("is_chatmail", is_chatmail)
|
||||
|
||||
# The default value when the setting is unset is "1".
|
||||
# This is not changed for compatibility with old databases
|
||||
# imported from backups.
|
||||
assert account.get_config("mvbox_move") == "1"
|
||||
|
||||
alice.stop_io()
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
alice.start_io()
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
alice.create_chat(bob)
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
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
|
||||
chat_id = snapshot.chat_id
|
||||
# Actually the message isn't available yet. Wait somehow for the post-message to arrive.
|
||||
chat_bob_alice.send_message("Now you can download my previous message")
|
||||
alice.wait_for_incoming_msg()
|
||||
alice._rpc.download_full_message(alice.id, msg.id)
|
||||
for dstate in [DownloadState.IN_PROGRESS, DownloadState.DONE]:
|
||||
event = alice.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert event.chat_id == chat_id
|
||||
assert event.msg_id == msg.id
|
||||
assert msg.get_snapshot().download_state == dstate
|
||||
# Once the first transport is set up,
|
||||
# mvbox_move is disabled.
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("is_chatmail") == is_chatmail
|
||||
|
||||
|
||||
def test_reconfigure_transport(acfactory) -> None:
|
||||
"""Test that reconfiguring the transport works."""
|
||||
"""Test that reconfiguring the transport works
|
||||
even if settings not supported for multi-transport
|
||||
like mvbox_move are enabled."""
|
||||
account = acfactory.get_online_account()
|
||||
account.set_config("mvbox_move", "1")
|
||||
|
||||
[transport] = account.list_transports()
|
||||
account.add_or_update_transport(transport)
|
||||
|
||||
# Reconfiguring the transport should not reset
|
||||
# the settings as if when configuring the first transport.
|
||||
assert account.get_config("mvbox_move") == "1"
|
||||
|
||||
|
||||
def test_transport_synchronization(acfactory, log) -> None:
|
||||
"""Test synchronization of transports between devices."""
|
||||
|
||||
def wait_for_io_started(ac):
|
||||
while True:
|
||||
ev = ac.wait_for_event(EventType.INFO)
|
||||
if "scheduler is running" in ev.msg:
|
||||
return
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
@@ -128,13 +169,11 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -144,15 +183,11 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
ac1_clone.delete_transport(transport2["addr"])
|
||||
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1)
|
||||
[transport1, transport3] = ac1.list_transports()
|
||||
|
||||
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
|
||||
@@ -161,7 +196,6 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1_clone)
|
||||
[transport3] = ac1_clone.list_transports()
|
||||
assert transport3["addr"] == addr3
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
@@ -175,7 +209,7 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
|
||||
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 = acfactory.get_online_account()
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
@@ -195,15 +229,6 @@ def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new primary transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
ac1_chat.send_text("Hello!")
|
||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||
bob_chat = bob.get_chat_by_id(bob_chat_id)
|
||||
bob_chat.accept()
|
||||
bob_chat.send_text("hello back")
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
||||
|
||||
|
||||
def test_recognize_self_address(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@@ -243,10 +268,11 @@ def test_transport_limit(acfactory) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory) -> None:
|
||||
def test_message_info_imap_urls(acfactory, log) -> None:
|
||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
log.section("Alice adds ac1 clone removes second transport")
|
||||
qr = acfactory.get_account_qr()
|
||||
for i in range(3):
|
||||
alice.add_transport_from_qr(qr)
|
||||
@@ -254,6 +280,9 @@ def test_message_info_imap_urls(acfactory) -> None:
|
||||
for _ in range(i + 1):
|
||||
alice.bring_online()
|
||||
|
||||
new_alice_addr = alice.list_transports()[2]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
|
||||
# Enable multi-device mode so messages are not deleted immediately.
|
||||
alice.set_config("bcc_self", "1")
|
||||
|
||||
@@ -261,53 +290,12 @@ def test_message_info_imap_urls(acfactory) -> None:
|
||||
# This is where he will send the message.
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
# Alice switches to another transport and removes the rest of the transports.
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
removed_addrs = []
|
||||
for transport in alice.list_transports():
|
||||
if transport["addr"] != new_alice_addr:
|
||||
alice.delete_transport(transport["addr"])
|
||||
removed_addrs.append(transport["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
# Alice changes the transport again.
|
||||
alice.set_config("configured_addr", alice.list_transports()[3]["addr"])
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
msg_info = msg.get_info()
|
||||
assert new_alice_addr in msg_info
|
||||
for removed_addr in removed_addrs:
|
||||
assert removed_addr not in msg_info
|
||||
assert f"{new_alice_addr}/INBOX" in msg_info
|
||||
|
||||
|
||||
def test_remove_primary_transport(acfactory, log) -> None:
|
||||
"""Test that after removing the primary relay, Alice can still receive messages."""
|
||||
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()
|
||||
assert msg1.text == "Hello!"
|
||||
|
||||
log.section("Alice removes the primary relay")
|
||||
alice.delete_transport(transport1["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
bob_chat.send_text("Hello again!")
|
||||
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.create_chat(bob)
|
||||
for alice_transport in alice.list_transports():
|
||||
addr = alice_transport["addr"]
|
||||
assert (addr == new_alice_addr) == (addr in msg.get_info())
|
||||
|
||||
@@ -167,16 +167,11 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
if inviter_side:
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
assert member_added_msg.info_contact_id == contact_snapshot.id
|
||||
else:
|
||||
if chat_msgs[0].get_snapshot().text == "You joined the channel.":
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
else:
|
||||
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
hello_msg = chat_msgs.pop(0).get_snapshot()
|
||||
|
||||
@@ -5,7 +5,6 @@ import logging
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -13,7 +12,7 @@ import pytest
|
||||
from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError, Rpc
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_system_info(rpc) -> None:
|
||||
@@ -273,9 +272,6 @@ def test_chat(acfactory) -> None:
|
||||
assert group.get_messages()
|
||||
group.get_fresh_message_count()
|
||||
group.mark_noticed()
|
||||
assert group.get_fresh_message_count() == 0
|
||||
group.mark_fresh()
|
||||
assert group.get_fresh_message_count() > 0
|
||||
assert group.get_contacts()
|
||||
assert group.get_past_contacts() == []
|
||||
group.remove_contact(alice_contact_bob)
|
||||
@@ -391,29 +387,22 @@ def test_dont_move_sync_msgs(acfactory, direct_imap):
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.set_config("fix_is_chatmail", "1")
|
||||
ac1.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac1.start_io()
|
||||
ac1.bring_online()
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
|
||||
# Sync messages may also be sent during configuration.
|
||||
ac1.wait_for_event(EventType.MSG_DELIVERED)
|
||||
ac1_direct_imap.select_folder("Inbox")
|
||||
while True:
|
||||
if len(ac1_direct_imap.get_all_messages()) == 1:
|
||||
break
|
||||
time.sleep(1)
|
||||
# Sync messages may also be sent during configuration.
|
||||
inbox_msg_cnt = len(ac1_direct_imap.get_all_messages())
|
||||
|
||||
ac1.set_config("displayname", "Alice")
|
||||
ac1.wait_for_event(EventType.MSG_DELIVERED)
|
||||
ac1.set_config("displayname", "Bob")
|
||||
ac1.wait_for_event(EventType.MSG_DELIVERED)
|
||||
ac1_direct_imap.select_folder("Inbox")
|
||||
assert len(ac1_direct_imap.get_all_messages()) == inbox_msg_cnt + 2
|
||||
|
||||
# Message may not be delivered to IMAP immediately
|
||||
# after sending over SMTP,
|
||||
# retry until they are delivered to IMAP.
|
||||
while True:
|
||||
if len(ac1_direct_imap.get_all_messages()) == 3:
|
||||
break
|
||||
time.sleep(1)
|
||||
ac1_direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac1_direct_imap.get_all_messages()) == 0
|
||||
|
||||
|
||||
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
@@ -668,24 +657,6 @@ def test_openrpc_command_line() -> None:
|
||||
assert "methods" in openrpc
|
||||
|
||||
|
||||
def test_early_failure(tmp_path) -> None:
|
||||
"""Test that Rpc.start() raises on invalid accounts directories."""
|
||||
# A file instead of a directory.
|
||||
file_path = tmp_path / "not_a_dir"
|
||||
file_path.write_text("I am a file, not a directory")
|
||||
rpc = Rpc(accounts_dir=str(file_path))
|
||||
with pytest.raises(JsonRpcError, match="(?i)directory"):
|
||||
rpc.start()
|
||||
|
||||
# A non-empty directory that is not a deltachat accounts directory.
|
||||
non_dc_dir = tmp_path / "invalid_dir"
|
||||
non_dc_dir.mkdir()
|
||||
(non_dc_dir / "some_file").write_text("content")
|
||||
rpc = Rpc(accounts_dir=str(non_dc_dir))
|
||||
with pytest.raises(JsonRpcError, match="invalid_dir"):
|
||||
rpc.start()
|
||||
|
||||
|
||||
def test_provider_info(rpc) -> None:
|
||||
account_id = rpc.add_account()
|
||||
|
||||
@@ -1047,7 +1018,6 @@ def test_no_old_msg_is_fresh(acfactory):
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
ac1_clone.wait_for_incoming_msg_event()
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
|
||||
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
|
||||
@@ -18,8 +18,6 @@ def test_webxdc(acfactory) -> None:
|
||||
"sourceCodeUrl": None,
|
||||
"summary": None,
|
||||
"selfAddr": webxdc_info["selfAddr"],
|
||||
"isAppSender": False,
|
||||
"isBroadcast": False,
|
||||
"sendUpdateInterval": 1000,
|
||||
"sendUpdateMaxSize": 18874368,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.50.0-dev"
|
||||
version = "2.39.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -18,7 +18,7 @@ import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
import { C } from "@deltachat/jsonrpc-client";
|
||||
|
||||
async function main() {
|
||||
const dc = startDeltaChat("deltachat-data");
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close()
|
||||
}
|
||||
|
||||
11
deltachat-rpc-server/npm-package/index.d.ts
vendored
11
deltachat-rpc-server/npm-package/index.d.ts
vendored
@@ -15,7 +15,7 @@ export interface SearchOptions {
|
||||
*/
|
||||
export function getRPCServerPath(
|
||||
options?: Partial<SearchOptions>
|
||||
): string;
|
||||
): Promise<string>;
|
||||
|
||||
|
||||
|
||||
@@ -33,15 +33,8 @@ export interface StartOptions {
|
||||
* @param directory directory for accounts folder
|
||||
* @param options
|
||||
*/
|
||||
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): DeltaChatOverJsonRpcServer
|
||||
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
|
||||
|
||||
export class DeltaChatOverJsonRpc extends StdioDeltaChat {
|
||||
constructor(
|
||||
directory: string,
|
||||
options?: Partial<SearchOptions & StartOptions>
|
||||
);
|
||||
readonly pathToServerBinary: string;
|
||||
}
|
||||
|
||||
export namespace FnTypes {
|
||||
export type getRPCServerPath = typeof getRPCServerPath
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//@ts-check
|
||||
import { spawn } from "node:child_process";
|
||||
import { statSync } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import process from "node:process";
|
||||
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
|
||||
@@ -36,7 +36,7 @@ function findRPCServerInNodeModules() {
|
||||
}
|
||||
|
||||
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
||||
export function getRPCServerPath(options = {}) {
|
||||
export async function getRPCServerPath(options = {}) {
|
||||
const { takeVersionFromPATH, disableEnvPath } = {
|
||||
takeVersionFromPATH: false,
|
||||
disableEnvPath: false,
|
||||
@@ -45,7 +45,7 @@ export function getRPCServerPath(options = {}) {
|
||||
// 1. check if it is set as env var
|
||||
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
|
||||
try {
|
||||
if (!statSync(process.env[ENV_VAR_NAME]).isFile()) {
|
||||
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
|
||||
throw new Error(
|
||||
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
|
||||
);
|
||||
@@ -68,49 +68,41 @@ export function getRPCServerPath(options = {}) {
|
||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||
|
||||
/** @type {import("./index").FnTypes.startDeltaChat} */
|
||||
export function startDeltaChat(directory, options = {}) {
|
||||
return new DeltaChatOverJsonRpc(directory, options);
|
||||
}
|
||||
|
||||
export class DeltaChatOverJsonRpc extends StdioDeltaChat {
|
||||
/**
|
||||
*
|
||||
* @param {string} directory
|
||||
* @param {Partial<import("./index").SearchOptions & import("./index").StartOptions>} options
|
||||
*/
|
||||
constructor(directory, options = {}) {
|
||||
const pathToServerBinary = getRPCServerPath(options);
|
||||
const server = spawn(pathToServerBinary, {
|
||||
env: {
|
||||
RUST_LOG: process.env.RUST_LOG,
|
||||
DC_ACCOUNTS_PATH: directory,
|
||||
},
|
||||
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
throw new Error(
|
||||
FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err)
|
||||
);
|
||||
});
|
||||
let shouldClose = false;
|
||||
|
||||
server.on("exit", () => {
|
||||
if (shouldClose) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Server quit");
|
||||
});
|
||||
|
||||
super(server.stdin, server.stdout, true);
|
||||
|
||||
this.close = () => {
|
||||
shouldClose = true;
|
||||
if (!server.kill()) {
|
||||
console.log("server termination failed");
|
||||
}
|
||||
};
|
||||
|
||||
this.pathToServerBinary = pathToServerBinary;
|
||||
}
|
||||
export async function startDeltaChat(directory, options = {}) {
|
||||
const pathToServerBinary = await getRPCServerPath(options);
|
||||
const server = spawn(pathToServerBinary, {
|
||||
env: {
|
||||
RUST_LOG: process.env.RUST_LOG,
|
||||
DC_ACCOUNTS_PATH: directory,
|
||||
},
|
||||
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
|
||||
});
|
||||
let shouldClose = false;
|
||||
|
||||
server.on("exit", () => {
|
||||
if (shouldClose) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Server quit");
|
||||
});
|
||||
|
||||
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
|
||||
//@ts-expect-error
|
||||
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
|
||||
|
||||
dc.close = () => {
|
||||
shouldClose = true;
|
||||
if (!server.kill()) {
|
||||
console.log("server termination failed");
|
||||
}
|
||||
};
|
||||
|
||||
//@ts-expect-error
|
||||
dc.pathToServerBinary = pathToServerBinary;
|
||||
|
||||
return dc;
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.50.0-dev"
|
||||
"version": "2.39.0"
|
||||
}
|
||||
|
||||
22
deny.toml
22
deny.toml
@@ -18,22 +18,10 @@ ignore = [
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134",
|
||||
|
||||
# rustls-webpki v0.102.8
|
||||
# We cannot upgrade to >=0.103.10 because
|
||||
# it is a transitive dependency of iroh 0.35.0
|
||||
# which depends on ^0.102.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0098>
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
|
||||
"RUSTSEC-2026-0049",
|
||||
"RUSTSEC-2026-0098",
|
||||
"RUSTSEC-2026-0099",
|
||||
|
||||
# Panic in CRL signature checks.
|
||||
# We do not check CRL and cannot update rustls-webpki 0.102.8
|
||||
# which is a dependency of iroh 0.35.0.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0104>
|
||||
"RUSTSEC-2026-0104"
|
||||
# Old versions of "lru" are transitive dependencies of iroh 0.35.0.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0002>
|
||||
# <https://github.com/chatmail/core/issues/7692>
|
||||
"RUSTSEC-2026-0002",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -44,7 +32,6 @@ ignore = [
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "constant_time_eq", version = "0.3.1" },
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
@@ -59,7 +46,6 @@ skip = [
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "rustls-webpki", version = "0.102.8" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
|
||||
127
draft/aeap-mvp.md
Normal file
127
draft/aeap-mvp.md
Normal file
@@ -0,0 +1,127 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
|
||||
- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
|
||||
- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
AND there is a `Chat-Version` header\
|
||||
AND the message is signed correctly
|
||||
AND the From address is (also) in the encrypted (and therefore signed) headers <sup>[[1]](#myfootnote1)</sup>\
|
||||
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
|
||||
|
||||
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
|
||||
|
||||
<a name="myfootnote1">[1]</a>: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
|
||||
|
||||
<details>
|
||||
<summary>More details about this</summary>
|
||||
Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
|
||||
|
||||
Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
|
||||
|
||||
Possible mitigations:
|
||||
- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
|
||||
- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
|
||||
|
||||
Note that usually a mail is signed by a key that has a UID matching the from address.
|
||||
|
||||
That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
|
||||
|
||||
https://autocrypt.org/level1.html#openpgp-based-key-data says:
|
||||
> The content of the user id packet is only decorative
|
||||
|
||||
</details>
|
||||
|
||||
### Notes:
|
||||
|
||||
- We treat protected and non-protected chats the same
|
||||
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more people know what old addresses you had).
|
||||
- As soon as we encrypt read receipts, sending a read receipt will be enough to tell a lot of people that you transitioned
|
||||
- AEAP will make the problem of inconsistent group state worse, both because it doesn't work if the message is unencrypted (even if the design allowed it, it would be problematic security-wise) and because some chat partners may have gotten the transition and some not. We should do something against this at some point in the future, like asking the user whether they want to add/remove the members to restore consistent group state.
|
||||
|
||||
#### Downsides of this design:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easy to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- Faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
|
||||
|
||||
### Alternatives and old discussions/plans:
|
||||
|
||||
- Change the contact instead of rewriting the group member lists. This seems to call for more trouble since we will end up with multiple contacts having the same email address.
|
||||
|
||||
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the necessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
|
||||
|
||||
- The condition for a transition temporarily was:
|
||||
|
||||
> When receiving a message: If we are going to assign a message to a chat, but the sender is not a member of this chat\
|
||||
> AND the signing key is the same as the direct (non-gossiped) key of one of the chat members\
|
||||
> AND ...
|
||||
|
||||
However, this would mean that in 1:1 messages can't trigger a transition, since we don't assign private messages to the parent chat, but always to the 1:1 chat with the sender.
|
||||
|
||||
<details>
|
||||
<summary>Some previous state of the discussion, which temporarily lived in an issue description</summary>
|
||||
Summarizing the discussions from https://github.com/deltachat/deltachat-core-rust/pull/2896, mostly quoting @hpk42:
|
||||
|
||||
1. (DONE) At the time of configure we push the current primary to become a secondary.
|
||||
|
||||
2. When a message is sent out to a chat, and the message is encrypted, and we have secondary addresses, then we
|
||||
a) add a protected "AEAP-Replacement" header that contains all secondary addresses
|
||||
b) if any of the secondary addresses is in the chat's member list, we remove it and leave a system message that we did so
|
||||
3. When an encrypted message with a replacement header is received, replace the e-mail address of all secondary contacts (if they exist) with the new primary and drop a sysmessage in all chats the secondary is member off. This might (in edge cases) result in chats that have two or more contacts with the same e-mail address. We might ignore this for a first release and just log a warning. Let's maybe not get hung up on this case before everything else works.
|
||||
|
||||
Notes:
|
||||
- for now we will send out aeap replacement headers forever, there is no termination condition other than lack of secondary addresses. I think that's fine for now. Later on we might introduce options to remove secondary addresses but i wouldn't do this for a first release/PR.
|
||||
- the design is resilient against changing e-mail providers from A to B to C and then back to A, with partially updated chats and diverging views from recipients/contacts on this transition. In the end, you will have a primary and some secondaries, and when you start sending out messages everybody will eventually synchronize when they receive the current state of primaries/secondaries.
|
||||
- of course on incoming message for need to check for each stated secondary address in the replacement header that it uses the same signature as the signature we verified as valid with the incoming message **--> Also we have to somehow make sure that the signing key was not just gossiped from some random other person in some group.**
|
||||
- there are no extra flags/columns in the database needed (i hope)
|
||||
|
||||
#### Downsides of the chosen approach:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
- There will be multiple contacts with the same address in the database. We will have to do something against this at some point.
|
||||
|
||||
The most obvious alternative would be to create a new contact with the new address and replace the old contact in the groups.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
|
||||
|
||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||
|
||||
_end of the previous state of the discussion_
|
||||
|
||||
</details>
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
|
||||
Notes during implementing
|
||||
========================
|
||||
|
||||
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.50.0-dev"
|
||||
version = "2.39.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -553,6 +553,17 @@ class Account:
|
||||
def imex(self, path: str, imex_cmd: int, passphrase: Optional[str] = None) -> None:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), as_dc_charpointer(passphrase))
|
||||
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
has been successfully sent to our own e-mail address ("self-sent message").
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
"""
|
||||
self.check_is_configured()
|
||||
res = lib.dc_initiate_key_transfer(self._dc_context)
|
||||
if res == ffi.NULL:
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
"""get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
|
||||
@@ -167,6 +167,14 @@ class Message:
|
||||
"""return True if this message is a system/info message."""
|
||||
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||
|
||||
def is_setup_message(self):
|
||||
"""return True if this message is a setup message."""
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self) -> str:
|
||||
"""return the first characters of a setup code in a setup message."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
def is_encrypted(self):
|
||||
"""return True if this message was encrypted."""
|
||||
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
|
||||
@@ -190,6 +198,12 @@ class Message:
|
||||
"""Get a message summary as a single line of text. Typically used for notifications."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_summarytext(self._dc_msg, width))
|
||||
|
||||
def continue_key_transfer(self, setup_code):
|
||||
"""extract key and use it as primary key for this account."""
|
||||
res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code))
|
||||
if res == 0:
|
||||
raise ValueError("Importing the key from Autocrypt Setup Message failed")
|
||||
|
||||
@props.with_doc
|
||||
def time_sent(self):
|
||||
"""UTC time when the message was sent.
|
||||
@@ -254,6 +268,10 @@ class Message:
|
||||
"""Quote setter."""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message."""
|
||||
|
||||
@@ -433,6 +433,7 @@ class ACFactory:
|
||||
if self.pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
assert "addr" in configdict and "mail_pw" in configdict
|
||||
return configdict
|
||||
@@ -504,6 +505,7 @@ class ACFactory:
|
||||
"addr": cloned_from.get_config("addr"),
|
||||
"mail_pw": cloned_from.get_config("mail_pw"),
|
||||
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
|
||||
"smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
|
||||
}
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
@@ -520,6 +522,7 @@ class ACFactory:
|
||||
ac = self.get_unconfigured_account()
|
||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
configdict.setdefault("delete_server_after", 0)
|
||||
ac.update_config(configdict)
|
||||
|
||||
@@ -288,7 +288,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
chat2.accept()
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
lp.sec("ac1: receiving message")
|
||||
|
||||
@@ -220,6 +220,21 @@ def test_webxdc_huge_update(acfactory, data, lp):
|
||||
assert update["payload"] == payload
|
||||
|
||||
|
||||
def test_enable_mvbox_move(acfactory, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("ac2: start without mvbox thread")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac2: configuring mvbox")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
|
||||
def test_forward_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -335,7 +350,7 @@ def test_long_group_name(acfactory, lp):
|
||||
|
||||
|
||||
def test_send_self_message(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account(bcc_self=True)
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, bcc_self=True)
|
||||
acfactory.bring_accounts_online()
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
@@ -494,7 +509,7 @@ def test_reply_privately(acfactory):
|
||||
|
||||
|
||||
def test_mdn_asymmetric(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
@@ -523,14 +538,20 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
lp.sec("ac1: waiting for incoming activity")
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_receive_encrypt(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -932,6 +953,7 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
|
||||
def test_connectivity(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
|
||||
@@ -52,19 +52,19 @@ class TestOfflineAccountBasic:
|
||||
|
||||
def test_set_config_int_conversion(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.set_config("bcc_self", False)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
ac1.set_config("bcc_self", True)
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
ac1.set_config("bcc_self", 0)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
ac1.set_config("bcc_self", 1)
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
ac1.set_config("mvbox_move", False)
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
ac1.set_config("mvbox_move", True)
|
||||
assert ac1.get_config("mvbox_move") == "1"
|
||||
ac1.set_config("mvbox_move", 0)
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
ac1.set_config("mvbox_move", 1)
|
||||
assert ac1.get_config("mvbox_move") == "1"
|
||||
|
||||
def test_update_config(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.update_config({"bcc_self": True})
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
ac1.update_config({"mvbox_move": False})
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -558,12 +558,6 @@ class TestOfflineChat:
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="We didn't find a way to correctly reset an account after a failed import attempt "
|
||||
"while simultaneously making sure "
|
||||
"that the password of an encrypted account survives a failed import attempt. "
|
||||
"Since passphrases are not really supported anymore, we decided to just disable the test.",
|
||||
)
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
Test that account passphrase isn't lost if backup failed to be imported.
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_dc_close_events(acfactory):
|
||||
register_global_plugin(ShutdownPlugin())
|
||||
assert hasattr(ac1, "_dc_context")
|
||||
ac1.shutdown()
|
||||
shutdowns.get()
|
||||
shutdowns.get(timeout=2)
|
||||
|
||||
|
||||
def test_wrong_db(tmp_path):
|
||||
@@ -221,7 +221,7 @@ def test_logged_ac_process_ffi_failure(acfactory):
|
||||
|
||||
# cause any event eg contact added/changed
|
||||
ac1.create_contact("something@example.org")
|
||||
res = cap.get()
|
||||
res = cap.get(timeout=10)
|
||||
assert "ac_process_ffi_event" in res
|
||||
assert "ZeroDivisionError" in res
|
||||
assert "Traceback" in res
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-04-13
|
||||
2026-01-23
|
||||
@@ -48,3 +48,19 @@ the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type
|
||||
|
||||
This will **rsync** your current checkout to the remote build machine
|
||||
(no need to commit before) and then run either rust or python tests.
|
||||
|
||||
# coredeps Dockerfile
|
||||
|
||||
`coredeps/Dockerfile` specifies an image that contains all
|
||||
of Delta Chat's core dependencies. It is used to
|
||||
build python wheels (binary packages for Python).
|
||||
|
||||
You can build the docker images yourself locally
|
||||
to avoid the relatively large download:
|
||||
|
||||
cd scripts # where all CI things are
|
||||
docker build -t deltachat/coredeps coredeps
|
||||
|
||||
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
|
||||
apt-get install qemu binfmt-support qemu-user-static
|
||||
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Script to check that current version ends with -dev.
|
||||
# Meant to be run in CI to check that PRs are made against the -dev version.
|
||||
# If the version is not -dev, it was forgotten to be updated
|
||||
# after making a release.
|
||||
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
with Path("Cargo.toml").open("rb") as fp:
|
||||
cargo_toml = tomllib.load(fp)
|
||||
version = cargo_toml["package"]["version"]
|
||||
if not version.endswith("-dev"):
|
||||
print(f"Current version {version} does not end with -dev", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
scripts/concourse/README.md
Normal file
26
scripts/concourse/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Concourse CI pipeline
|
||||
|
||||
`docs_wheels.yml` is a pipeline for [Concourse CI](https://concourse-ci.org/)
|
||||
that builds C documentation, Python documentation, Python wheels for `x86_64`
|
||||
and `aarch64` and Python source packages, and uploads them.
|
||||
|
||||
To setup the pipeline run
|
||||
```
|
||||
fly -t <your-target> set-pipeline -c docs_wheels.yml -p docs_wheels -l secret.yml
|
||||
```
|
||||
where `secret.yml` contains the following secrets:
|
||||
```
|
||||
c.delta.chat:
|
||||
private_key: |
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
devpi:
|
||||
login: dc
|
||||
password: ...
|
||||
```
|
||||
|
||||
Secrets can be read from the password manager:
|
||||
```
|
||||
fly -t b1 set-pipeline -c docs_wheels.yml -p docs_wheels -l <(pass show delta/b1.delta.chat/secret.yml)
|
||||
```
|
||||
305
scripts/concourse/docs_wheels.yml
Normal file
305
scripts/concourse/docs_wheels.yml
Normal file
@@ -0,0 +1,305 @@
|
||||
resources:
|
||||
- name: deltachat-core-rust
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/chatmail/core.git
|
||||
|
||||
- name: deltachat-core-rust-release
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/chatmail/core.git
|
||||
tag_filter: "v*"
|
||||
|
||||
jobs:
|
||||
- name: python-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_x86_64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
# Binary wheels
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload x86_64 wheels and source packages
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*manylinux201*
|
||||
|
||||
- name: python-aarch64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_aarch64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload aarch64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*manylinux201*
|
||||
|
||||
- name: python-musl-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_x86_64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload musl x86_64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*musllinux_1_1_x86_64*
|
||||
|
||||
- name: python-musl-aarch64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_aarch64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload musl aarch64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*musllinux_1_1_aarch64*
|
||||
9
scripts/coredeps/Dockerfile
Normal file
9
scripts/coredeps/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
ARG BASEIMAGE=quay.io/pypa/manylinux2014_x86_64
|
||||
#ARG BASEIMAGE=quay.io/pypa/musllinux_1_1_x86_64
|
||||
#ARG BASEIMAGE=quay.io/pypa/manylinux2014_aarch64
|
||||
|
||||
FROM $BASEIMAGE
|
||||
RUN pipx install tox
|
||||
COPY install-rust.sh /scripts/
|
||||
RUN /scripts/install-rust.sh
|
||||
RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi
|
||||
20
scripts/coredeps/install-rust.sh
Executable file
20
scripts/coredeps/install-rust.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install Rust
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.93.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$ARCH-unknown-linux-$LIBC"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
|
||||
@@ -3,4 +3,4 @@
|
||||
# Update package cache without changing the lockfile.
|
||||
cargo update --dry-run
|
||||
|
||||
cargo deny --workspace --all-features --locked check -D warnings
|
||||
cargo deny --workspace --all-features check -D warnings
|
||||
|
||||
@@ -100,7 +100,7 @@ def main():
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
if not newversion.endswith("-dev"):
|
||||
if "alpha" not in newversion:
|
||||
found = False
|
||||
for line in Path("CHANGELOG.md").open():
|
||||
if line == f"## [{newversion}] - {today}\n":
|
||||
|
||||
@@ -6,11 +6,11 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=ad097ee40579c884e7757de2d3bb0a51f481a32a
|
||||
REV=d041136c19a48b493823b46d472f12b9ee94ae80
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
git clone --filter=blob:none https://github.com/chatmail/provider-db.git "$TMP"
|
||||
git clone --filter=blob:none https://github.com/deltachat/provider-db.git "$TMP"
|
||||
cd "$TMP"
|
||||
git checkout "$REV"
|
||||
DATE=$(git show -s --format=%cs)
|
||||
|
||||
21
spec.md
21
spec.md
@@ -39,24 +39,9 @@ Messages SHOULD be encrypted by the
|
||||
[Autocrypt](https://autocrypt.org/level1.html) standard;
|
||||
`prefer-encrypt=mutual` MAY be set by default.
|
||||
|
||||
Meta data SHOULD be encrypted
|
||||
by the [Header Protection](https://www.rfc-editor.org/rfc/rfc9788.html) standard
|
||||
with the following [Header Confidentiality Policy](https://www.rfc-editor.org/rfc/rfc9788.html#name-header-confidentiality-poli):
|
||||
```
|
||||
hcp_chat(name, val_in) → val_out:
|
||||
if lower(name) is 'from':
|
||||
assert that val_in is an RFC 5322 mailbox
|
||||
return the RFC 5322 addr-spec part of val_in
|
||||
else if lower(name) is 'to':
|
||||
return '"hidden-recipients": ;'
|
||||
else if lower(name) is 'date':
|
||||
return the UTC form of a random date within the last 7 days
|
||||
else if lower(name) is 'subject':
|
||||
return '[...]'
|
||||
else if lower(name) is in ['message-id', 'chat-is-post-message']:
|
||||
return val_in
|
||||
return null
|
||||
```
|
||||
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
||||
by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||
|
||||
|
||||
# Outgoing messages
|
||||
|
||||
|
||||
116
src/accounts.rs
116
src/accounts.rs
@@ -8,7 +8,6 @@ use std::sync::Arc;
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use futures::FutureExt as _;
|
||||
use futures::future;
|
||||
use futures_lite::FutureExt as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
@@ -23,7 +22,6 @@ use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::location;
|
||||
use crate::log::warn;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
@@ -59,8 +57,8 @@ pub struct Accounts {
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
if writable {
|
||||
Self::ensure_accounts_dir(&dir).await?;
|
||||
if writable && !dir.exists() {
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
let events = Events::new();
|
||||
Accounts::open(events, dir, writable).await
|
||||
@@ -69,9 +67,10 @@ impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
/// Uses an existing events channel.
|
||||
pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
|
||||
if writable {
|
||||
Self::ensure_accounts_dir(&dir).await?;
|
||||
if writable && !dir.exists() {
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(events, dir, writable).await
|
||||
}
|
||||
|
||||
@@ -83,20 +82,14 @@ impl Accounts {
|
||||
0
|
||||
}
|
||||
|
||||
/// Ensures the accounts directory and config file exist.
|
||||
/// Creates them if the directory doesn't exist, or if it exists but is empty.
|
||||
/// Errors if the directory exists with files but no config.
|
||||
async fn ensure_accounts_dir(dir: &Path) -> Result<()> {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("Failed to create folder")?;
|
||||
Config::new(dir).await?;
|
||||
} else if !dir.join(CONFIG_NAME).exists() {
|
||||
let mut rd = fs::read_dir(dir).await?;
|
||||
ensure!(rd.next_entry().await?.is_none(), "{dir:?} is not empty");
|
||||
Config::new(dir).await?;
|
||||
}
|
||||
/// Creates a new default structure.
|
||||
async fn create(dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
|
||||
Config::new(dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -538,38 +531,6 @@ impl Accounts {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets location for all accounts.
|
||||
///
|
||||
/// Returns true if location should still be streamed.
|
||||
pub async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
let continue_streaming = future::try_join_all(self.accounts.iter().map(
|
||||
|(account_id, account)| async move {
|
||||
location::set(account, latitude, longitude, accuracy)
|
||||
.await
|
||||
.with_context(|| format!("Failed to set location for account {account_id}"))
|
||||
},
|
||||
))
|
||||
.await?
|
||||
.into_iter()
|
||||
.any(|continue_streaming| continue_streaming);
|
||||
Ok(continue_streaming)
|
||||
}
|
||||
|
||||
/// Stops sending locations to all chats.
|
||||
pub async fn stop_sending_locations(&self) -> Result<()> {
|
||||
future::try_join_all(
|
||||
self.accounts
|
||||
.iter()
|
||||
.map(|(account_id, account)| async move {
|
||||
location::stop_sending(account).await.with_context(|| {
|
||||
format!("Failed to stop sending locations for account {account_id}")
|
||||
})
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration file name.
|
||||
@@ -625,7 +586,6 @@ impl Config {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
|
||||
let lockfile = dir.join(LOCKFILE_NAME);
|
||||
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
|
||||
@@ -720,27 +680,13 @@ impl Config {
|
||||
file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
|
||||
.await
|
||||
.context("failed to write a tmp config")?;
|
||||
|
||||
// We use `sync_all()` and not `sync_data()` here.
|
||||
// This translates to `fsync()` instead of `fdatasync()`.
|
||||
// `fdatasync()` may be insufficient for newely created files
|
||||
// and may not even synchronize the file size on some operating systems,
|
||||
// resulting in a truncated file.
|
||||
file.sync_all()
|
||||
file.sync_data()
|
||||
.await
|
||||
.context("failed to sync a tmp config")?;
|
||||
drop(file);
|
||||
fs::rename(&tmp_path, &self.file)
|
||||
.await
|
||||
.context("failed to rename config")?;
|
||||
// Sync the rename().
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let parent = self.file.parent().context("No parent directory")?;
|
||||
let parent_file = fs::File::open(parent).await?;
|
||||
parent_file.sync_all().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -806,7 +752,6 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Creates a new account in the account manager directory.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn new_account(&mut self) -> Result<AccountConfig> {
|
||||
let id = {
|
||||
let id = self.inner.next_id;
|
||||
@@ -896,7 +841,6 @@ impl Config {
|
||||
///
|
||||
/// Without this workaround removing account may fail on Windows with an error
|
||||
/// "The process cannot access the file because it is being used by another process. (os error 32)".
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
@@ -968,26 +912,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_new_empty_existing_dir() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
// A non-empty directory without accounts.toml should fail.
|
||||
fs::create_dir_all(&p).await.unwrap();
|
||||
fs::write(p.join("stray_file.txt"), b"hello").await.unwrap();
|
||||
assert!(Accounts::new(p.clone(), true).await.is_err());
|
||||
|
||||
// Clean up to an empty directory.
|
||||
fs::remove_file(p.join("stray_file.txt")).await.unwrap();
|
||||
|
||||
// An empty directory without accounts.toml should succeed.
|
||||
let mut accounts = Accounts::new(p.clone(), true).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_new_open_conflict() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -1283,11 +1207,13 @@ mod tests {
|
||||
let account1 = accounts.get_account(1).context("failed to get account 1")?;
|
||||
let account2 = accounts.get_account(2).context("failed to get account 2")?;
|
||||
|
||||
assert_eq!(stock_str::no_messages(&account1), "No messages.");
|
||||
assert_eq!(stock_str::no_messages(&account2), "No messages.");
|
||||
account1.set_stock_translation(StockMessage::NoMessages, "foobar".to_string())?;
|
||||
assert_eq!(stock_str::no_messages(&account1), "foobar");
|
||||
assert_eq!(stock_str::no_messages(&account2), "foobar");
|
||||
assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
|
||||
assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
|
||||
account1
|
||||
.set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
|
||||
.await?;
|
||||
assert_eq!(stock_str::no_messages(&account1).await, "foobar");
|
||||
assert_eq!(stock_str::no_messages(&account2).await, "foobar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,11 +47,11 @@ pub struct Aheader {
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
|
||||
/// Whether `_verified` attribute is present.
|
||||
///
|
||||
/// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
/// header that is used to tell that the sender
|
||||
/// marked this key as verified.
|
||||
// Whether `_verified` attribute is present.
|
||||
//
|
||||
// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
// header that is used to tell that the sender
|
||||
// marked this key as verified.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ impl fmt::Display for Aheader {
|
||||
let keydata = self.public_key.to_base64().chars().enumerate().fold(
|
||||
String::new(),
|
||||
|mut res, (i, c)| {
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
if i % 78 == 78 - "keydata=".len() {
|
||||
res.push(' ')
|
||||
}
|
||||
@@ -108,11 +107,13 @@ impl FromStr for Aheader {
|
||||
.remove("keydata")
|
||||
.context("keydata attribute is not found")
|
||||
.and_then(|raw| {
|
||||
SignedPublicKey::from_base64(&raw).context("Autocrypt key cannot be decoded")
|
||||
SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
|
||||
})
|
||||
.and_then(|key| {
|
||||
key.verify()
|
||||
.and(Ok(key))
|
||||
.context("autocrypt key cannot be verified")
|
||||
})?;
|
||||
public_key
|
||||
.verify_bindings()
|
||||
.context("Autocrypt key cannot be verified")?;
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
.remove("prefer-encrypt")
|
||||
|
||||
561
src/authres.rs
Normal file
561
src/authres.rs
Normal file
@@ -0,0 +1,561 @@
|
||||
//! Parsing and handling of the Authentication-Results header.
|
||||
//! See the comment on [`handle_authres`] for more.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
|
||||
/// `authres` is short for the Authentication-Results header, defined in
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
|
||||
/// about whether DKIM and SPF passed.
|
||||
///
|
||||
/// To mitigate From forgery, we remember for each sending domain whether it is known
|
||||
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
|
||||
/// we don't allow changing the autocrypt key.
|
||||
///
|
||||
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
|
||||
pub(crate) async fn handle_authres(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
) -> Result<DkimResults> {
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
|
||||
}
|
||||
};
|
||||
|
||||
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
|
||||
update_authservid_candidates(context, &authres).await?;
|
||||
compute_dkim_results(context, authres).await
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DkimResults {
|
||||
/// Whether DKIM passed for this particular e-mail.
|
||||
pub dkim_passed: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for DkimResults {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "DKIM Results: Passed={}", self.dkim_passed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type AuthservId = String;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DkimResult {
|
||||
/// The header explicitly said that DKIM passed
|
||||
Passed,
|
||||
/// The header explicitly said that DKIM failed
|
||||
Failed,
|
||||
/// The header didn't say anything about DKIM; this might mean that it wasn't
|
||||
/// checked, but it might also mean that it failed. This is because some providers
|
||||
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
|
||||
/// Authentication-Results if there was no DKIM.
|
||||
Nothing,
|
||||
}
|
||||
|
||||
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
|
||||
|
||||
fn parse_authres_headers(
|
||||
headers: &mailparse::headers::Headers<'_>,
|
||||
from_domain: &str,
|
||||
) -> ParsedAuthresHeaders {
|
||||
let mut res = Vec::new();
|
||||
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
|
||||
let header_value = remove_comments(&header_value);
|
||||
|
||||
if let Some(mut authserv_id) = header_value.split(';').next() {
|
||||
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
|
||||
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
|
||||
// because there is whitespace in the first identifier before the ';'.
|
||||
// Authentication-Results-parsing still works securely because they remove incoming
|
||||
// Authentication-Results headers.
|
||||
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
|
||||
// with providers not implementing the RFC correctly, someone can trick us
|
||||
// into thinking that an incoming email is DKIM-correct, anyway.
|
||||
// The most important thing here is that we have some valid `authserv_id`.
|
||||
authserv_id = "invalidAuthservId";
|
||||
}
|
||||
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
|
||||
res.push((authserv_id.to_string(), dkim_passed));
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// The headers can contain comments that look like this:
|
||||
/// ```text
|
||||
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
|
||||
/// ```
|
||||
fn remove_comments(header: &str) -> Cow<'_, str> {
|
||||
// In Pomsky, this is:
|
||||
// "(" Codepoint* lazy ")"
|
||||
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
|
||||
static RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
|
||||
|
||||
RE.replace_all(header, " ")
|
||||
}
|
||||
|
||||
/// Parses a single Authentication-Results header, like:
|
||||
///
|
||||
/// ```text
|
||||
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
/// ```
|
||||
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
|
||||
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
|
||||
// Check that the character right before `dkim=` is a space or a tab
|
||||
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
|
||||
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
|
||||
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
|
||||
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
|
||||
if let Some(&"pass") = dkim_parts.first() {
|
||||
// DKIM headers contain a header.d or header.i field
|
||||
// that says which domain signed. We have to check ourselves
|
||||
// that this is the same domain as in the From header.
|
||||
let header_d: &str = &format!("header.d={}", &from_domain);
|
||||
let header_i: &str = &format!("header.i=@{}", &from_domain);
|
||||
|
||||
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
|
||||
// We have found a `dkim=pass` header!
|
||||
return DkimResult::Passed;
|
||||
}
|
||||
} else {
|
||||
// dkim=fail, dkim=none, ...
|
||||
return DkimResult::Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DkimResult::Nothing
|
||||
}
|
||||
|
||||
/// ## About authserv-ids
|
||||
///
|
||||
/// After having checked DKIM, our email server adds an Authentication-Results header.
|
||||
///
|
||||
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
|
||||
/// in order to make us think that DKIM was correct in their From-forged email.
|
||||
///
|
||||
/// In order to prevent this, each email server adds its authserv-id to the
|
||||
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
|
||||
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
|
||||
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
|
||||
///
|
||||
/// We need to somehow find out the authserv-id(s) of our email server, so that
|
||||
/// we can use the Authentication-Results with the right authserv-id.
|
||||
///
|
||||
/// ## What this function does
|
||||
///
|
||||
/// When receiving an email, this function is called and updates the candidates for
|
||||
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
|
||||
///
|
||||
/// Usually, every incoming email has Authentication-Results with our server's
|
||||
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
|
||||
/// authserv-ids for our server's authserv-id is a good guess for our server's
|
||||
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
|
||||
/// changed and start over with the new authserv-ids.
|
||||
///
|
||||
/// See [`handle_authres`].
|
||||
async fn update_authservid_candidates(
|
||||
context: &Context,
|
||||
authres: &ParsedAuthresHeaders,
|
||||
) -> Result<()> {
|
||||
let mut new_ids: BTreeSet<&str> = authres
|
||||
.iter()
|
||||
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
|
||||
.collect();
|
||||
if new_ids.is_empty() {
|
||||
// The incoming message doesn't contain any authentication results, maybe it's a
|
||||
// self-sent or a mailer-daemon message
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let old_ids = parse_authservid_candidates_config(&old_config);
|
||||
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
|
||||
if !intersection.is_empty() {
|
||||
new_ids = intersection;
|
||||
}
|
||||
// If there were no AuthservIdCandidates previously, just start with
|
||||
// the ones from the incoming email
|
||||
|
||||
if old_ids != new_ids {
|
||||
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
||||
context
|
||||
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
|
||||
/// and whether a keychange should be allowed.
|
||||
///
|
||||
/// We track in the `sending_domains` table whether we get positive Authentication-Results
|
||||
/// for mails from a contact (meaning that their provider properly authenticates against
|
||||
/// our provider).
|
||||
///
|
||||
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
|
||||
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
|
||||
async fn compute_dkim_results(
|
||||
context: &Context,
|
||||
mut authres: ParsedAuthresHeaders,
|
||||
) -> Result<DkimResults> {
|
||||
let mut dkim_passed = false;
|
||||
|
||||
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let ids = parse_authservid_candidates_config(&ids_config);
|
||||
|
||||
// Remove all foreign authentication results
|
||||
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
|
||||
|
||||
if authres.is_empty() {
|
||||
// If the authentication results are empty, then our provider doesn't add them
|
||||
// and an attacker could just add their own Authentication-Results, making us
|
||||
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
|
||||
dkim_passed = true;
|
||||
} else {
|
||||
for (_authserv_id, current_dkim_passed) in authres {
|
||||
match current_dkim_passed {
|
||||
DkimResult::Passed => {
|
||||
dkim_passed = true;
|
||||
break;
|
||||
}
|
||||
DkimResult::Failed => {
|
||||
dkim_passed = false;
|
||||
break;
|
||||
}
|
||||
DkimResult::Nothing => {
|
||||
// Continue looking for an Authentication-Results header
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DkimResults { dkim_passed })
|
||||
}
|
||||
|
||||
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
|
||||
config
|
||||
.as_deref()
|
||||
.map(|c| c.split_whitespace().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
use crate::mimeparser;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
|
||||
#[test]
|
||||
fn test_remove_comments() {
|
||||
let header = "Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
remove_comments(&header),
|
||||
"Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass header.d=riseup.net;"
|
||||
);
|
||||
|
||||
let header = ") aaa (".to_string();
|
||||
assert_eq!(remove_comments(&header), ") aaa (");
|
||||
|
||||
let header = "((something weird) no comment".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment");
|
||||
|
||||
let header = "🎉(🎉(🎉))🎉(".to_string();
|
||||
assert_eq!(remove_comments(&header), "🎉 )🎉(");
|
||||
|
||||
// Comments are allowed to include whitespace
|
||||
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment ");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_authentication_results() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr("alice@gmx.net").await;
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Passed),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Nothing),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
|
||||
|
||||
// Weird Authentication-Results from Outlook without an authserv-id
|
||||
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
|
||||
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
|
||||
header.d=hotmail.com;dmarc=pass action=none
|
||||
header.from=hotmail.com;compauth=pass reason=100";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
|
||||
// At this point, the most important thing to test is that there are no
|
||||
// authserv-ids with whitespace in them.
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Failed),
|
||||
("gmx.net".to_string(), DkimResult::Passed)
|
||||
]
|
||||
);
|
||||
|
||||
// ';' in comments
|
||||
let bytes = b"Authentication-Results: mx1.riseup.net;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
|
||||
dkim-atps=neutral";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = br#"Authentication-Results: box.hispanilandia.net;
|
||||
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
|
||||
dkim-atps=neutral
|
||||
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
|
||||
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Failed),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_authservid_candidates() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx3.messagingengine.com");
|
||||
|
||||
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
// A message without any Authentication-Results headers shouldn't remove all
|
||||
// candidates since it could be a mailer-daemon message or so
|
||||
update_authservid_candidates_test(&t, &[]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
|
||||
.await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calls update_authservid_candidates(), meant for using in a test.
|
||||
///
|
||||
/// update_authservid_candidates() only looks at the keys of its
|
||||
/// `authentication_results` parameter. So, this function takes `incoming_ids`
|
||||
/// and adds some AuthenticationResults to get the HashMap we need.
|
||||
async fn update_authservid_candidates_test(context: &Context, incoming_ids: &[&str]) {
|
||||
let v = incoming_ids
|
||||
.iter()
|
||||
.map(|id| (id.to_string(), DkimResult::Passed))
|
||||
.collect();
|
||||
update_authservid_candidates(context, &v).await.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_realworld_authentication_results() -> Result<()> {
|
||||
let mut test_failed = false;
|
||||
|
||||
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
for entry in dir {
|
||||
if !entry.file_type().await.unwrap().is_dir() {
|
||||
continue;
|
||||
}
|
||||
let self_addr = entry.file_name().into_string().unwrap();
|
||||
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
|
||||
let authres_parsing_works = [
|
||||
"ik.me",
|
||||
"web.de",
|
||||
"posteo.de",
|
||||
"gmail.com",
|
||||
"hotmail.com",
|
||||
"mail.ru",
|
||||
"aol.com",
|
||||
"yahoo.com",
|
||||
"icloud.com",
|
||||
"fastmail.com",
|
||||
"mail.de",
|
||||
"outlook.com",
|
||||
"gmx.de",
|
||||
"testrun.org",
|
||||
]
|
||||
.contains(&self_domain.as_str());
|
||||
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr(&self_addr).await;
|
||||
if !authres_parsing_works {
|
||||
println!("========= Receiving as {} =========", &self_addr);
|
||||
}
|
||||
|
||||
// Simulate receiving all emails once, so that we have the correct authserv-ids
|
||||
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
|
||||
|
||||
// The ordering in which the emails are received can matter;
|
||||
// the test _should_ pass for every ordering.
|
||||
dir.sort_by_key(|d| d.file_name());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
bytes.clear();
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from).await?;
|
||||
let from_domain = EmailAddress::new(from).unwrap().domain;
|
||||
|
||||
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
|
||||
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
|
||||
// These are (fictional) forged emails where the attacker added a fake
|
||||
// Authentication-Results before sending the email
|
||||
&& from != "forged-authres-added@example.com"
|
||||
// Other forged emails
|
||||
&& !from.starts_with("forged");
|
||||
|
||||
if res.dkim_passed != expected_result {
|
||||
if authres_parsing_works {
|
||||
println!(
|
||||
"!!!!!! FAILURE Receiving {:?} wrong result: !!!!!!",
|
||||
entry.path(),
|
||||
);
|
||||
test_failed = true;
|
||||
}
|
||||
println!("From {}: {}", from_domain, res.dkim_passed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!test_failed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_authres() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
|
||||
// return an Err because this would prevent the message from being added
|
||||
// to the database and downloaded again and again
|
||||
let bytes = b"From: invalid@from.com
|
||||
Authentication-Results: dkim=";
|
||||
let mail = mailparse::parse_mail(bytes).unwrap();
|
||||
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Bob knows his server's authserv-id
|
||||
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
|
||||
.await?;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(rcvd.error.is_none());
|
||||
|
||||
// Do the same without the mailing list header, this time the failed
|
||||
// authres isn't ignored
|
||||
let mut sent = alice
|
||||
.send_text(alice_bob_chat.id, "hellooo without mailing list")
|
||||
.await;
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
|
||||
// The message info should contain a warning:
|
||||
assert!(
|
||||
rcvd.id
|
||||
.get_info(&bob)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("DKIM Results: Passed=false")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
77
src/blob.rs
77
src/blob.rs
@@ -1,6 +1,5 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use std::cmp::max;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -10,8 +9,8 @@ use anyhow::{Context as _, Result, ensure, format_err};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::ImageReader;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::{fs, task};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
@@ -256,7 +255,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
/// Recode image to avatar size.
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||
let (max_wh, max_bytes) =
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
@@ -273,7 +272,7 @@ impl<'a> BlobObject<'a> {
|
||||
let is_avatar = true;
|
||||
self.check_or_recode_to_size(
|
||||
context, None, // The name of an avatar doesn't matter
|
||||
viewtype, max_wh, max_bytes, is_avatar,
|
||||
viewtype, img_wh, max_bytes, is_avatar,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -284,13 +283,17 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if it's a correct
|
||||
/// image, `*viewtype` is set to [`Viewtype::Image`].
|
||||
///
|
||||
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
|
||||
/// image is a true sticker assuming that it must have at least one fully transparent corner,
|
||||
/// otherwise `*viewtype` is set to [`Viewtype::Image`].
|
||||
pub async fn check_or_recode_image(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
) -> Result<String> {
|
||||
let (max_wh, max_bytes) =
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
@@ -301,15 +304,13 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let is_avatar = false;
|
||||
self.check_or_recode_to_size(context, name, viewtype, max_wh, max_bytes, is_avatar)
|
||||
self.check_or_recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
|
||||
}
|
||||
|
||||
/// Checks or recodes the image so that it fits into limits on width/height and/or byte size.
|
||||
/// Checks or recodes the image so that it fits into limits on width/height and byte size.
|
||||
///
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `max_wh` and proceeds
|
||||
/// with the result (even if `max_bytes` is still exceeded).
|
||||
///
|
||||
/// If `is_avatar`, the resolution will be reduced in a loop until the image fits `max_bytes`.
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
|
||||
/// with the result without rechecking.
|
||||
///
|
||||
/// This modifies the blob object in-place.
|
||||
///
|
||||
@@ -317,13 +318,12 @@ impl<'a> BlobObject<'a> {
|
||||
/// then the updated user-visible filename will be returned;
|
||||
/// this may be necessary because the format may be changed to JPG,
|
||||
/// i.e. "image.png" -> "image.jpg".
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn check_or_recode_to_size(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
max_wh: u32,
|
||||
mut img_wh: u32,
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
) -> Result<String> {
|
||||
@@ -358,10 +358,7 @@ impl<'a> BlobObject<'a> {
|
||||
return Ok(name);
|
||||
}
|
||||
let mut img = imgreader.decode().context("image decode failure")?;
|
||||
let orientation = exif
|
||||
.as_ref()
|
||||
.map(|exif| exif_orientation(exif, context))
|
||||
.unwrap_or(Orientation::NoTransforms);
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
|
||||
if *vt == Viewtype::Sticker {
|
||||
@@ -380,16 +377,15 @@ impl<'a> BlobObject<'a> {
|
||||
return Ok(name);
|
||||
}
|
||||
}
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
||||
// target_wh target-resolution for resizing the image.
|
||||
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
|
||||
let mut target_wh = if exceeds_wh {
|
||||
max_wh
|
||||
} else {
|
||||
max(img.width(), img.height())
|
||||
img = match orientation {
|
||||
Some(90) => img.rotate90(),
|
||||
Some(180) => img.rotate180(),
|
||||
Some(270) => img.rotate270(),
|
||||
_ => img,
|
||||
};
|
||||
|
||||
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
@@ -442,9 +438,9 @@ impl<'a> BlobObject<'a> {
|
||||
// usually has less pixels by cropping, UI that needs to wait anyways,
|
||||
// and also benefits from slightly better (5%) encoding of Triangle-filtered images.
|
||||
let new_img = if is_avatar {
|
||||
img.resize(target_wh, target_wh, image::imageops::FilterType::Triangle)
|
||||
img.resize(img_wh, img_wh, image::imageops::FilterType::Triangle)
|
||||
} else {
|
||||
img.thumbnail(target_wh, target_wh)
|
||||
img.thumbnail(img_wh, img_wh)
|
||||
};
|
||||
|
||||
if encoded_img_exceeds_bytes(
|
||||
@@ -455,19 +451,19 @@ impl<'a> BlobObject<'a> {
|
||||
&mut encoded,
|
||||
)? && is_avatar
|
||||
{
|
||||
if target_wh < 20 {
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
));
|
||||
}
|
||||
|
||||
target_wh = target_wh * 7 / 8;
|
||||
img_wh = img_wh * 2 / 3;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px).",
|
||||
encoded.len(),
|
||||
target_wh
|
||||
img_wh
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -544,17 +540,18 @@ fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
|
||||
Ok((len, exif))
|
||||
}
|
||||
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation {
|
||||
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)
|
||||
&& let Some(val) = orientation.value.get_uint(0)
|
||||
&& let Ok(val) = TryInto::<u8>::try_into(val)
|
||||
{
|
||||
return Orientation::from_exif(val).unwrap_or({
|
||||
warn!(context, "Exif orientation value ignored: {val:?}.");
|
||||
Orientation::NoTransforms
|
||||
});
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
||||
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
// we only use rotation, in practise, flipping is not used.
|
||||
match orientation.value.get_uint(0) {
|
||||
Some(3) => return 180,
|
||||
Some(6) => return 90,
|
||||
Some(8) => return 270,
|
||||
other => warn!(context, "Exif orientation value ignored: {other:?}."),
|
||||
}
|
||||
}
|
||||
Orientation::NoTransforms
|
||||
0
|
||||
}
|
||||
|
||||
/// All files in the blobdir.
|
||||
|
||||
@@ -305,7 +305,7 @@ async fn test_recode_image_2() {
|
||||
has_exif: true,
|
||||
original_width: 2000,
|
||||
original_height: 1800,
|
||||
orientation: Some(Orientation::Rotate270),
|
||||
orientation: 270,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
@@ -336,28 +336,6 @@ async fn test_recode_image_2() {
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_vflipped() {
|
||||
let bytes = include_bytes!("../../test-data/image/rectangle200x180-vflipped.jpg");
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 200,
|
||||
original_height: 180,
|
||||
orientation: Some(Orientation::FlipVertical),
|
||||
compressed_width: 200,
|
||||
compressed_height: 180,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_bad_exif() {
|
||||
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
|
||||
@@ -445,6 +423,7 @@ async fn test_recode_image_balanced_png() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
@@ -452,7 +431,6 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
res_viewtype: Some(Viewtype::Sticker),
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
@@ -552,7 +530,7 @@ struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) has_exif: bool,
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: Option<Orientation>,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) res_viewtype: Option<Viewtype>,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
@@ -568,7 +546,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
let has_exif = self.has_exif;
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation.unwrap_or(Orientation::NoTransforms);
|
||||
let orientation = self.orientation;
|
||||
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
@@ -734,6 +712,8 @@ async fn test_send_gif_as_sticker() -> Result<()> {
|
||||
let chat = alice.get_self_chat().await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
|
||||
// extension.
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
Ok(())
|
||||
}
|
||||
@@ -818,56 +798,3 @@ async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that an image that already fits into the width limit,
|
||||
/// but not the bytes limit,
|
||||
/// is compressed without changing the resolution.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_without_downscaling() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
|
||||
let image = include_bytes!("../../test-data/image/screenshot120x120.jpg");
|
||||
const { assert!(120 < constants::WORSE_AVATAR_SIZE) };
|
||||
|
||||
for is_avatar in [true, false] {
|
||||
let mut blob =
|
||||
BlobObject::create_and_deduplicate_from_bytes(t, image, "image.jpg").unwrap();
|
||||
let image_path = blob.to_abs_path();
|
||||
check_image_size(&image_path, 120, 120);
|
||||
|
||||
assert!(
|
||||
fs::metadata(&image_path).await.unwrap().len() > constants::WORSE_AVATAR_BYTES as u64
|
||||
);
|
||||
|
||||
// Repeat the check, because a second call to `check_or_recode_to_size()`
|
||||
// is not supposed to change anything:
|
||||
let mut imgs = vec![];
|
||||
for _ in 0..2 {
|
||||
let mut viewtype = Viewtype::Image;
|
||||
let new_name = blob.check_or_recode_to_size(
|
||||
t,
|
||||
Some("image.jpg".to_string()),
|
||||
&mut viewtype,
|
||||
constants::WORSE_AVATAR_SIZE,
|
||||
constants::WORSE_AVATAR_BYTES,
|
||||
is_avatar,
|
||||
)?;
|
||||
let image_path = blob.to_abs_path();
|
||||
assert_eq!(new_name, "image.jpg"); // The name shall not have changed
|
||||
assert_eq!(viewtype, Viewtype::Image); // The viewtype shall not have changed
|
||||
let img = check_image_size(&image_path, 120, 120); // The resolution shall not have changed
|
||||
imgs.push(img);
|
||||
|
||||
let new_image_bytes = fs::metadata(&image_path).await.unwrap().len();
|
||||
assert!(
|
||||
new_image_bytes < constants::WORSE_AVATAR_BYTES as u64,
|
||||
"The new image size, {new_image_bytes}, should be lower than {}, is_avatar={is_avatar}",
|
||||
constants::WORSE_AVATAR_BYTES
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(imgs[0], imgs[1]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
87
src/calls.rs
87
src/calls.rs
@@ -11,16 +11,17 @@ use crate::context::{Context, WeakContext};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype, markseen_msgs};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{normalize_text, time};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use num_traits::FromPrimitive;
|
||||
use sdp::SessionDescription;
|
||||
use serde::Serialize;
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::task;
|
||||
@@ -79,7 +80,6 @@ impl CallInfo {
|
||||
}
|
||||
|
||||
fn remaining_ring_seconds(&self) -> i64 {
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time();
|
||||
remaining_seconds.clamp(0, RINGING_SECONDS)
|
||||
}
|
||||
@@ -104,12 +104,10 @@ impl CallInfo {
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
let incoming_call_str = stock_str::incoming_call(context, self.has_video_initially());
|
||||
self.update_text(context, &format!("{incoming_call_str}\n{duration}"))
|
||||
self.update_text(context, &format!("Incoming call\n{duration}"))
|
||||
.await?;
|
||||
} else {
|
||||
let outgoing_call_str = stock_str::outgoing_call(context, self.has_video_initially());
|
||||
self.update_text(context, &format!("{outgoing_call_str}\n{duration}"))
|
||||
self.update_text(context, &format!("Outgoing call\n{duration}"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -128,14 +126,6 @@ impl CallInfo {
|
||||
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
|
||||
}
|
||||
|
||||
/// Returns true if the call is started as a video call.
|
||||
pub fn has_video_initially(&self) -> bool {
|
||||
self.msg
|
||||
.param
|
||||
.get_bool(Param::WebrtcHasVideoInitially)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns true if the call is missed
|
||||
/// because the caller canceled it
|
||||
/// explicitly before ringing stopped.
|
||||
@@ -174,7 +164,6 @@ impl CallInfo {
|
||||
}
|
||||
|
||||
/// Returns call duration in seconds.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn duration_seconds(&self) -> i64 {
|
||||
if let (Some(start), Some(end)) = (
|
||||
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
|
||||
@@ -196,7 +185,6 @@ impl Context {
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
place_call_info: String,
|
||||
has_video_initially: bool,
|
||||
) -> Result<MsgId> {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
@@ -205,15 +193,12 @@ impl Context {
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
let outgoing_call_str = stock_str::outgoing_call(self, has_video_initially);
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Call,
|
||||
text: outgoing_call_str,
|
||||
text: "Outgoing call".into(),
|
||||
..Default::default()
|
||||
};
|
||||
call.param.set(Param::WebrtcRoom, &place_call_info);
|
||||
call.param
|
||||
.set_int(Param::WebrtcHasVideoInitially, has_video_initially.into());
|
||||
call.id = send_msg(self, chat_id, &mut call).await?;
|
||||
|
||||
let wait = RINGING_SECONDS;
|
||||
@@ -247,7 +232,6 @@ impl Context {
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).await?;
|
||||
}
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
|
||||
// send an acceptance message around: to the caller as well as to the other devices of the callee
|
||||
let mut msg = Message {
|
||||
@@ -264,7 +248,6 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: true,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
@@ -283,13 +266,10 @@ impl Context {
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_ended(self).await?;
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
let declined_call_str = stock_str::declined_call(self);
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self);
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
@@ -331,12 +311,10 @@ impl Context {
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
let missed_call_str = stock_str::missed_call(&context);
|
||||
call.update_text(&context, &missed_call_str).await?;
|
||||
call.update_text(&context, "Missed call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
let canceled_call_str = stock_str::canceled_call(&context);
|
||||
call.update_text(&context, &canceled_call_str).await?;
|
||||
call.update_text(&context, "Canceled call").await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
@@ -361,14 +339,18 @@ impl Context {
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
let missed_call_str = stock_str::missed_call(self);
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
call.update_text(self, "Missed call").await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
} else {
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(self, call.has_video_initially());
|
||||
call.update_text(self, &incoming_call_str).await?;
|
||||
call.update_text(self, "Incoming call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
|
||||
let has_video = match sdp_has_video(&call.place_call_info) {
|
||||
Ok(has_video) => has_video,
|
||||
Err(err) => {
|
||||
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
|
||||
false
|
||||
}
|
||||
};
|
||||
let can_call_me = match who_can_call_me(self).await? {
|
||||
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
@@ -395,7 +377,7 @@ impl Context {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video: call.has_video_initially(),
|
||||
has_video,
|
||||
});
|
||||
}
|
||||
let wait = call.remaining_ring_seconds();
|
||||
@@ -407,8 +389,7 @@ impl Context {
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let outgoing_call_str = stock_str::outgoing_call(self, call.has_video_initially());
|
||||
call.update_text(self, &outgoing_call_str).await?;
|
||||
call.update_text(self, "Outgoing call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
} else {
|
||||
@@ -430,7 +411,6 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: false,
|
||||
});
|
||||
} else {
|
||||
let accept_call_info = mime_message
|
||||
@@ -459,23 +439,19 @@ impl Context {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_ended(self).await?;
|
||||
let declined_call_str = stock_str::declined_call(self);
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
let missed_call_str = stock_str::missed_call(self);
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
call.update_text(self, "Missed call").await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_canceled(self).await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self);
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
let declined_call_str = stock_str::declined_call(self);
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -531,6 +507,19 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if SDP offer has a video.
|
||||
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
|
||||
let mut cursor = Cursor::new(sdp);
|
||||
let session_description =
|
||||
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
|
||||
for media_description in &session_description.media_descriptions {
|
||||
if media_description.media_name.media == "video" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// State of the call for display in the message bubble.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CallState {
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::*;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
@@ -26,6 +25,13 @@ async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()>
|
||||
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
|
||||
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
|
||||
|
||||
/// 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=-`.
|
||||
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
|
||||
|
||||
async fn setup_call() -> Result<CallSetup> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
@@ -46,7 +52,7 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
bob2.create_chat(&alice).await;
|
||||
|
||||
let test_msg_id = alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent1.sender_msg_id, test_msg_id);
|
||||
@@ -62,8 +68,7 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
assert!(!info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(info.has_video_initially(), true);
|
||||
assert_text(t, m.id, "Outgoing video call").await?;
|
||||
assert_text(t, m.id, "Outgoing call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
@@ -84,8 +89,7 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
assert!(info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(info.has_video_initially(), true);
|
||||
assert_text(t, m.id, "Incoming video call").await?;
|
||||
assert_text(t, m.id, "Incoming call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
@@ -116,28 +120,9 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
// Bob accepts the incoming call
|
||||
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
|
||||
.await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob
|
||||
@@ -149,17 +134,9 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
|
||||
|
||||
bob2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: false,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let info = bob2
|
||||
.load_call_by_id(bob2_call.id)
|
||||
@@ -170,7 +147,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
|
||||
// Alice receives the acceptance message
|
||||
alice.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call").await?;
|
||||
let ev = alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
@@ -192,7 +169,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
|
||||
|
||||
alice2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
@@ -228,21 +205,10 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
|
||||
// Bob has accepted the call and also ends it
|
||||
bob.end_call(bob_call.id).await?;
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -253,7 +219,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -264,7 +230,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
|
||||
// Alice receives the ending message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -275,7 +241,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -305,7 +271,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
|
||||
// Bob has accepted the call but Alice ends it
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -317,7 +283,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -329,7 +295,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -339,7 +305,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -367,18 +333,8 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob has accepted Alice before, but does not want to talk with Alice
|
||||
bob_call.chat_id.accept(&bob).await?;
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Declined call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -419,35 +375,6 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_callee_sees_contact_request_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
// Bob can't end_call() because the contact request isn't accepted, but he can mark the call as
|
||||
// seen.
|
||||
markseen_msgs(bob, vec![bob_call.id]).await?;
|
||||
assert_eq!(bob_call.id.get_state(bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN only to self so that an unaccepted contact can't know anything.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, ContactId::SELF)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
@@ -498,7 +425,7 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Test that message summary says it is a missed call.
|
||||
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
|
||||
let summary = bob_call_msg.get_summary(&bob, None).await?;
|
||||
assert_eq!(summary.text, "🎥 Missed call");
|
||||
assert_eq!(summary.text, "📞 Missed call");
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
@@ -598,6 +525,13 @@ async fn test_update_call_text() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sdp_has_video() {
|
||||
assert!(sdp_has_video("foobar").is_err());
|
||||
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
|
||||
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
|
||||
}
|
||||
|
||||
/// Tests that calls are forwarded as text messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_call() -> Result<()> {
|
||||
@@ -608,7 +542,7 @@ async fn test_forward_call() -> Result<()> {
|
||||
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let alice_msg_id = alice
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string(), true)
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
|
||||
.await
|
||||
.context("Failed to place a call")?;
|
||||
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
|
||||
772
src/chat.rs
772
src/chat.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -142,7 +142,7 @@ impl Chatlist {
|
||||
AND c.blocked!=1
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
@@ -168,7 +168,7 @@ impl Chatlist {
|
||||
AND c.blocked!=1
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft,),
|
||||
process_row,
|
||||
)
|
||||
@@ -204,7 +204,7 @@ impl Chatlist {
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?3
|
||||
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
process_row,
|
||||
)
|
||||
@@ -253,7 +253,7 @@ impl Chatlist {
|
||||
AND NOT c.archived=?
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(
|
||||
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
|
||||
Chattype::Group, ContactId::SELF,
|
||||
@@ -279,7 +279,7 @@ impl Chatlist {
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND NOT c.archived=?
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
@@ -417,7 +417,7 @@ impl Chatlist {
|
||||
Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
|
||||
} else {
|
||||
Ok(Summary {
|
||||
text: stock_str::no_messages(context),
|
||||
text: stock_str::no_messages(context).await,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -648,6 +648,7 @@ mod tests {
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
t.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
|
||||
.await
|
||||
@@ -655,6 +656,7 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
t.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
|
||||
.await
|
||||
|
||||
@@ -7,7 +7,6 @@ use colorutils_rs::{Oklch, Rgb, TransferFunction};
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Converts an identifier to Hue angle.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn str_to_angle(s: &str) -> f32 {
|
||||
let bytes = s.as_bytes();
|
||||
let result = Sha1::digest(bytes);
|
||||
@@ -20,7 +19,6 @@ fn str_to_angle(s: &str) -> f32 {
|
||||
///
|
||||
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
|
||||
/// most significant bits corresponding to the red color.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
|
||||
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
|
||||
}
|
||||
|
||||
264
src/config.rs
264
src/config.rs
@@ -19,7 +19,7 @@ use crate::log::LogExt;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::Provider;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{get_abs_path, time};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
|
||||
use crate::{constants, stats};
|
||||
|
||||
@@ -42,85 +42,50 @@ use crate::{constants, stats};
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Config {
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredAddr, [`crate::login_param::EnteredLoginParam`],
|
||||
/// or add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Email address, used in the `From:` field.
|
||||
Addr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server hostname.
|
||||
MailServer,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server username.
|
||||
MailUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server password.
|
||||
MailPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server port.
|
||||
MailPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// IMAP server security (e.g. TLS, STARTTLS).
|
||||
MailSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// How to check TLS certificates.
|
||||
///
|
||||
/// "IMAP" in the name is for compatibility,
|
||||
/// this actually applies to both IMAP and SMTP connections.
|
||||
ImapCertificateChecks,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server hostname.
|
||||
SendServer,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server username.
|
||||
SendUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server password.
|
||||
SendPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server port.
|
||||
SendPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// SMTP server security (e.g. TLS, STARTTLS).
|
||||
SendSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
/// Deprecated option for backwards compatibility.
|
||||
///
|
||||
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
|
||||
SmtpCertificateChecks,
|
||||
|
||||
/// Whether to use OAuth 2.
|
||||
///
|
||||
/// Historically contained other bitflags, which are now deprecated.
|
||||
@@ -190,10 +155,31 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
|
||||
/// ones are moved there anyway.
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxMove,
|
||||
|
||||
/// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
|
||||
///
|
||||
/// This will not entirely disable other folders, e.g. the spam folder will also still
|
||||
/// be watched for new messages.
|
||||
#[strum(props(default = "0"))]
|
||||
OnlyFetchMvbox,
|
||||
|
||||
/// Whether to show classic emails or only chat messages.
|
||||
#[strum(props(default = "2"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
/// Quality of the media files to send.
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
/// If set to "1", then existing messages are considered to be already fetched.
|
||||
/// This flag is reset after successful configuration.
|
||||
#[strum(props(default = "1"))]
|
||||
FetchedExistingMsgs,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
@@ -213,50 +199,39 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
/// Move messages to the Trash folder instead of marking them "\Deleted". Overrides
|
||||
/// `ProviderOptions::delete_to_trash`.
|
||||
DeleteToTrash,
|
||||
|
||||
/// The primary email address.
|
||||
ConfiguredAddr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// List of configured IMAP servers as a JSON array.
|
||||
ConfiguredImapServers,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server hostname.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailServer,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server port.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server security (e.g. TLS, STARTTLS).
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server username.
|
||||
///
|
||||
/// This is set if user has configured username manually.
|
||||
ConfiguredMailUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured IMAP server password.
|
||||
ConfiguredMailPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured TLS certificate checks.
|
||||
/// This option is saved on successful configuration
|
||||
/// and should not be modified manually.
|
||||
@@ -265,68 +240,55 @@ pub enum Config {
|
||||
/// but has "IMAP" in the name for backwards compatibility.
|
||||
ConfiguredImapCertificateChecks,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// List of configured SMTP servers as a JSON array.
|
||||
ConfiguredSmtpServers,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server hostname.
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendServer,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server port.
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendPort,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server security (e.g. TLS, STARTTLS).
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server username.
|
||||
///
|
||||
/// This is set if user has configured username manually.
|
||||
ConfiguredSendUser,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Configured SMTP server password.
|
||||
ConfiguredSendPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
/// Deprecated, stored for backwards compatibility.
|
||||
///
|
||||
/// ConfiguredImapCertificateChecks is actually used.
|
||||
ConfiguredSmtpCertificateChecks,
|
||||
|
||||
/// Whether OAuth 2 is used with configured provider.
|
||||
ConfiguredServerFlags,
|
||||
|
||||
/// Configured folder for incoming messages.
|
||||
ConfiguredInboxFolder,
|
||||
|
||||
/// Configured folder for chat messages.
|
||||
ConfiguredMvboxFolder,
|
||||
|
||||
/// Configured "Trash" folder.
|
||||
ConfiguredTrashFolder,
|
||||
|
||||
/// Unix timestamp of the last successful configuration.
|
||||
ConfiguredTimestamp,
|
||||
|
||||
/// ID of the configured provider from the provider database.
|
||||
ConfiguredProvider,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use [`Context::is_configured()`] instead.
|
||||
///
|
||||
/// True if account is configured.
|
||||
Configured,
|
||||
|
||||
@@ -367,12 +329,21 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// If a warning about exceeding quota was shown recently,
|
||||
/// this is the percentage of quota at the time the warning was given.
|
||||
/// Unset, when quota falls below minimal warning threshold again.
|
||||
QuotaExceeding,
|
||||
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
|
||||
LastCantDecryptOutgoingMsgs,
|
||||
|
||||
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
|
||||
/// Whether to avoid using IMAP IDLE even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing "fake idle".
|
||||
@@ -407,6 +378,12 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
/// Space-separated list of all the authserv-ids which we believe
|
||||
/// may be the one of our email server.
|
||||
///
|
||||
/// See `crate::authres::update_authservid_candidates`.
|
||||
AuthservIdCandidates,
|
||||
|
||||
/// Make all outgoing messages with Autocrypt header "multipart/signed".
|
||||
SignUnencrypted,
|
||||
|
||||
@@ -504,13 +481,21 @@ impl Config {
|
||||
pub(crate) fn is_synced(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus,
|
||||
Self::Displayname
|
||||
| Self::MdnsEnabled
|
||||
| Self::MvboxMove
|
||||
| Self::ShowEmails
|
||||
| Self::Selfavatar
|
||||
| Self::Selfstatus,
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(self, Config::ConfiguredAddr)
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,7 +607,15 @@ impl Context {
|
||||
.get_config(key)
|
||||
.await?
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.is_some_and(|x| x != 0))
|
||||
.map(|x| x != 0)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns true if movebox ("DeltaChat" folder) should be watched.
|
||||
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::MvboxMove).await?
|
||||
|| self.get_config_bool(Config::OnlyFetchMvbox).await?
|
||||
|| !self.get_config_bool(Config::IsChatmail).await?)
|
||||
}
|
||||
|
||||
/// Returns true if sync messages should be sent.
|
||||
@@ -706,6 +699,9 @@ impl Context {
|
||||
| Config::ProxyEnabled
|
||||
| Config::BccSelf
|
||||
| Config::MdnsEnabled
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::DeleteToTrash
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
@@ -728,6 +724,16 @@ impl Context {
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let n_transports = self.count_transports().await?;
|
||||
if n_transports > 1
|
||||
&& matches!(
|
||||
key,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails
|
||||
)
|
||||
{
|
||||
bail!("Cannot reconfigure {key} when multiple transports are configured");
|
||||
}
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self).await?,
|
||||
_ => Default::default(),
|
||||
@@ -806,6 +812,12 @@ impl Context {
|
||||
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
|
||||
.await?;
|
||||
}
|
||||
Config::MvboxMove => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
self.sql
|
||||
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
|
||||
.await?;
|
||||
}
|
||||
Config::ConfiguredAddr => {
|
||||
let Some(addr) = value else {
|
||||
bail!("Cannot unset configured_addr");
|
||||
@@ -839,22 +851,6 @@ impl Context {
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
// Update the timestamp for the primary transport
|
||||
// so it becomes the first in `get_all_self_addrs()` list
|
||||
// and the list of relays distributed in the public key.
|
||||
// This ensures that messages will be sent
|
||||
// to the primary relay by the contacts
|
||||
// and will be fetched in background_fetch()
|
||||
// which only fetches from the primary transport.
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE transports SET add_timestamp=?, is_published=1 WHERE addr=?",
|
||||
(time(), addr),
|
||||
)
|
||||
.context(
|
||||
"Failed to update add_timestamp for the new primary transport",
|
||||
)?;
|
||||
|
||||
// Clean up SMTP and IMAP APPEND queue.
|
||||
//
|
||||
// The messages in the queue have a different
|
||||
@@ -945,10 +941,15 @@ impl Context {
|
||||
/// Returns `false` if no addresses are configured.
|
||||
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
Ok(self
|
||||
.get_all_self_addrs()
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
.any(|a| addr_cmp(addr, a))
|
||||
|| self
|
||||
.get_secondary_self_addrs()
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
}
|
||||
|
||||
/// Sets `primary_new` as the new primary self address and saves the old
|
||||
@@ -966,51 +967,20 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all self addresses, newest first.
|
||||
/// Returns the primary self address followed by all secondary ones.
|
||||
pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports ORDER BY add_timestamp DESC, id DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter();
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter();
|
||||
|
||||
Ok(primary_addrs.chain(secondary_addrs).collect())
|
||||
}
|
||||
|
||||
/// Returns all published self addresses, newest first.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports WHERE is_published=1 ORDER BY add_timestamp DESC, id DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all published secondary self addresses.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports
|
||||
WHERE is_published
|
||||
AND addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')
|
||||
ORDER BY add_timestamp DESC, id DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
/// Returns all secondary self addresses.
|
||||
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Returns the primary self address.
|
||||
|
||||
@@ -196,6 +196,13 @@ async fn test_sync() -> Result<()> {
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
|
||||
|
||||
for key in [Config::ShowEmails, Config::MvboxMove] {
|
||||
let val = alice0.get_config_bool(key).await?;
|
||||
alice0.set_config_bool(key, !val).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(alice1.get_config_bool(key).await?, !val);
|
||||
}
|
||||
|
||||
// `Config::SyncMsgs` mustn't be synced.
|
||||
alice0.set_config_bool(Config::SyncMsgs, false).await?;
|
||||
alice0.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
@@ -239,7 +246,8 @@ async fn test_sync() -> Result<()> {
|
||||
alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.is_some_and(|path| path.ends_with(".png"))
|
||||
.filter(|path| path.ends_with(".png"))
|
||||
.is_some()
|
||||
);
|
||||
alice0.set_config(Config::Selfavatar, None).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
@@ -297,14 +305,16 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.is_some_and(|path| path.ends_with(".jpg"))
|
||||
.filter(|path| path.ends_with(".jpg"))
|
||||
.is_some()
|
||||
);
|
||||
sync(alice1, alice0).await;
|
||||
assert!(
|
||||
alice0
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.is_some_and(|path| path.ends_with(".jpg"))
|
||||
.filter(|path| path.ends_with(".jpg"))
|
||||
.is_some()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
147
src/configure.rs
147
src/configure.rs
@@ -23,13 +23,13 @@ use percent_encoding::utf8_percent_encode;
|
||||
use server_params::{ServerParams, expand_param_vector};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::{self, Config};
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::warn;
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{EnteredCertificateChecks, TransportListEntry};
|
||||
use crate::message::Message;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
@@ -76,7 +76,7 @@ impl Context {
|
||||
/// Deprecated since 2025-02; use `add_transport_from_qr()`
|
||||
/// or `add_or_update_transport()` instead.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
let mut param = EnteredLoginParam::load_legacy(self).await?;
|
||||
let mut param = EnteredLoginParam::load(self).await?;
|
||||
|
||||
self.add_transport_inner(&mut param).await
|
||||
}
|
||||
@@ -110,7 +110,6 @@ impl Context {
|
||||
/// 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.
|
||||
pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
let result = self.add_transport_inner(param).await;
|
||||
@@ -146,11 +145,11 @@ impl Context {
|
||||
if let Err(err) = res.as_ref() {
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
|
||||
progress!(self, 0, Some(error_msg.clone()));
|
||||
bail!(error_msg);
|
||||
} else {
|
||||
param.save_legacy(self).await?;
|
||||
param.save(self).await?;
|
||||
progress!(self, 1000);
|
||||
}
|
||||
|
||||
@@ -189,22 +188,14 @@ impl Context {
|
||||
/// 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.
|
||||
pub async fn list_transports(&self) -> Result<Vec<TransportListEntry>> {
|
||||
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
|
||||
let transports = self
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT entered_param, is_published FROM transports",
|
||||
(),
|
||||
|row| {
|
||||
let param: String = row.get(0)?;
|
||||
let param: EnteredLoginParam = serde_json::from_str(¶m)?;
|
||||
let is_published: bool = row.get(1)?;
|
||||
Ok(TransportListEntry {
|
||||
param,
|
||||
is_unpublished: !is_published,
|
||||
})
|
||||
},
|
||||
)
|
||||
.query_map_vec("SELECT entered_param FROM transports", (), |row| {
|
||||
let entered_param: String = row.get(0)?;
|
||||
let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
|
||||
Ok(transport)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(transports)
|
||||
@@ -243,6 +234,11 @@ impl Context {
|
||||
Ok((id, add_timestamp))
|
||||
},
|
||||
)?;
|
||||
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
|
||||
transaction.execute(
|
||||
"DELETE FROM imap_sync WHERE transport_id=?",
|
||||
(transport_id,),
|
||||
)?;
|
||||
|
||||
// Removal timestamp should not be lower than addition timestamp
|
||||
// to be accepted by other devices when synced.
|
||||
@@ -261,49 +257,10 @@ impl Context {
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
self.quota.write().await.remove(&removed_transport_id);
|
||||
self.restart_io_if_running().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub async fn set_transport_unpublished(&self, addr: &str, unpublished: bool) -> Result<()> {
|
||||
self.sql
|
||||
.transaction(|trans| {
|
||||
let primary_addr: String = trans
|
||||
.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| row.get(0),
|
||||
)
|
||||
.context("Select primary address")?;
|
||||
if primary_addr == addr && unpublished {
|
||||
bail!("Can't set primary relay as unpublished");
|
||||
}
|
||||
// We need to update the timestamp so that the key's timestamp changes
|
||||
// and is recognized as newer by our peers
|
||||
trans
|
||||
.execute(
|
||||
"UPDATE transports SET is_published=?, add_timestamp=? WHERE addr=? AND is_published!=?1",
|
||||
(!unpublished, time(), addr),
|
||||
)
|
||||
.context("Update transports")?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
@@ -316,16 +273,36 @@ impl Context {
|
||||
(¶m.addr,),
|
||||
)
|
||||
.await?
|
||||
&& self
|
||||
{
|
||||
// Should be checked before `MvboxMove` because the latter makes no sense in presense of
|
||||
// `OnlyFetchMvbox` and even grayed out in the UIs in this case.
|
||||
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
|
||||
bail!(
|
||||
"To use additional relays, set the legacy option \"Settings / Advanced / Show Classic Emails\" to \"All\"."
|
||||
);
|
||||
}
|
||||
|
||||
if self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM transports", ())
|
||||
.await?
|
||||
>= MAX_TRANSPORT_RELAYS
|
||||
{
|
||||
bail!(
|
||||
"You have reached the maximum number of relays ({}).",
|
||||
MAX_TRANSPORT_RELAYS
|
||||
)
|
||||
{
|
||||
bail!(
|
||||
"You have reached the maximum number of relays ({}).",
|
||||
MAX_TRANSPORT_RELAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let provider = match configure(self, param).await {
|
||||
@@ -538,7 +515,6 @@ async fn get_configured_param(
|
||||
.collect(),
|
||||
imap_user: param.imap.user.clone(),
|
||||
imap_password: param.imap.password.clone(),
|
||||
imap_folder: Some(param.imap.folder.clone()).filter(|folder| !folder.is_empty()),
|
||||
smtp: servers
|
||||
.iter()
|
||||
.filter_map(|params| {
|
||||
@@ -578,6 +554,9 @@ async fn get_configured_param(
|
||||
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
let configured_param = get_configured_param(ctx, param).await?;
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
|
||||
@@ -616,11 +595,11 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||
let configuring = true;
|
||||
let imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(imap_session) => imap_session,
|
||||
Err(err) => {
|
||||
bail!("{}", nicer_configuration_error(ctx, format!("{err:#}")));
|
||||
}
|
||||
if let Err(err) = imap.connect(ctx, configuring).await {
|
||||
bail!(
|
||||
"{}",
|
||||
nicer_configuration_error(ctx, format!("{err:#}")).await
|
||||
);
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
@@ -631,17 +610,11 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
progress!(ctx, 900);
|
||||
|
||||
let is_configured = ctx.is_configured().await?;
|
||||
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
if imap_session.is_chatmail() {
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
|
||||
} else if !is_configured {
|
||||
// Reset the setting that may have been set
|
||||
// during failed configuration.
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
|
||||
}
|
||||
if !is_configured {
|
||||
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
|
||||
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
|
||||
}
|
||||
|
||||
drop(imap_session);
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
@@ -658,12 +631,12 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
progress!(ctx, 920);
|
||||
|
||||
ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
|
||||
.await?;
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
ctx.update_device_chats()
|
||||
.await
|
||||
.context("Failed to update device chats")?;
|
||||
update_device_chats_handle.await??;
|
||||
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.emit_event(EventType::AccountsItemChanged);
|
||||
@@ -756,7 +729,7 @@ async fn get_autoconfig(
|
||||
None
|
||||
}
|
||||
|
||||
fn nicer_configuration_error(context: &Context, e: String) -> String {
|
||||
async fn nicer_configuration_error(context: &Context, e: String) -> String {
|
||||
if e.to_lowercase().contains("could not resolve")
|
||||
|| e.to_lowercase().contains("connection attempts")
|
||||
|| e.to_lowercase()
|
||||
@@ -765,7 +738,7 @@ fn nicer_configuration_error(context: &Context, e: String) -> String {
|
||||
|| e.to_lowercase()
|
||||
.contains("failed to lookup address information")
|
||||
{
|
||||
return stock_str::error_no_network(context);
|
||||
return stock_str::error_no_network(context).await;
|
||||
}
|
||||
|
||||
e
|
||||
@@ -794,7 +767,7 @@ pub enum Error {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::login_param::EnteredImapLoginParam;
|
||||
use crate::login_param::EnteredServerLoginParam;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -813,7 +786,7 @@ mod tests {
|
||||
let entered_param = EnteredLoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
|
||||
imap: EnteredImapLoginParam {
|
||||
imap: EnteredServerLoginParam {
|
||||
user: "alice@example.net".to_string(),
|
||||
password: "foobar".to_string(),
|
||||
..Default::default()
|
||||
|
||||
@@ -36,6 +36,17 @@ pub enum Blocked {
|
||||
Request = 2,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum ShowEmails {
|
||||
Off = 0,
|
||||
AcceptedContacts = 1,
|
||||
#[default] // also change Config.ShowEmails props(default) on changes
|
||||
All = 2,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
@@ -188,7 +199,7 @@ pub const WORSE_IMAGE_BYTES: usize = 130_000;
|
||||
// max. width/height and bytes of an avatar
|
||||
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;
|
||||
pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000;
|
||||
pub(crate) const WORSE_AVATAR_SIZE: u32 = 256;
|
||||
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
|
||||
pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k.
|
||||
|
||||
// max. width/height of images scaled down because of being too huge
|
||||
@@ -199,6 +210,11 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
|
||||
/// usage by UIs.
|
||||
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
|
||||
|
||||
// Key for the folder configuration version (see below).
|
||||
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
|
||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 5;
|
||||
|
||||
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
|
||||
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
|
||||
// `max_smtp_rcpt_to` in the provider db.
|
||||
@@ -218,6 +234,19 @@ pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
|
||||
// Newer Delta Chats will remove the prefix as needed.
|
||||
pub(crate) const EDITED_PREFIX: &str = "✏️";
|
||||
|
||||
// Strings needed to render the Autocrypt Setup Message.
|
||||
// Left untranslated as not being supported/recommended workflow and as translations would require deep knowledge.
|
||||
pub(crate) const ASM_SUBJECT: &str = "Autocrypt Setup Message";
|
||||
pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
|
||||
used to transfer your end-to-end setup between clients.
|
||||
|
||||
To decrypt and use your setup, \
|
||||
open the message in an Autocrypt-compliant client \
|
||||
and enter the setup code presented on the generating device.
|
||||
|
||||
If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \
|
||||
use \"Settings / Add Second Device\" instead.";
|
||||
|
||||
/// Period between `sql::housekeeping()` runs.
|
||||
pub(crate) const HOUSEKEEPING_PERIOD: i64 = 24 * 60 * 60;
|
||||
|
||||
@@ -233,9 +262,6 @@ Here is what to do:
|
||||
|
||||
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
|
||||
|
||||
/// How many recent messages should be re-sent to a new broadcast member.
|
||||
pub(crate) const N_MSGS_TO_NEW_BROADCAST_MEMBER: usize = 10;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -251,6 +277,18 @@ mod tests {
|
||||
assert_eq!(Chattype::OutBroadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_showemails_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(ShowEmails::All, ShowEmails::default());
|
||||
assert_eq!(ShowEmails::Off, ShowEmails::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
ShowEmails::AcceptedContacts,
|
||||
ShowEmails::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(ShowEmails::All, ShowEmails::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocked_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user