mirror of
https://github.com/chatmail/core.git
synced 2026-10-09 14:40:16 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
974e32dd76 |
@@ -15,6 +15,6 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
+29
-15
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.97.1
|
||||
RUST_VERSION: 1.95.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -58,16 +58,30 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25
|
||||
- uses: EmbarkStudios/cargo-deny-action@a531616d8ce3b9177443e48a1159bc945a099823
|
||||
with:
|
||||
arguments: --workspace --all-features --locked
|
||||
command: check
|
||||
command-arguments: "-Dwarnings"
|
||||
|
||||
provider_database:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Install rustfmt
|
||||
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
||||
- name: Check provider database
|
||||
run: scripts/update-provider-database.sh
|
||||
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
runs-on: ubuntu-latest
|
||||
@@ -75,7 +89,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -114,7 +128,7 @@ jobs:
|
||||
shell: bash
|
||||
if: matrix.rust == 'latest'
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -132,7 +146,7 @@ jobs:
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@3d7d7cd5ac7f994c1892ae0c06165095b9139094
|
||||
uses: taiki-e/install-action@60ae4ce63c7aeb6e96d7f572c1ec7fafbb17ca80
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -157,7 +171,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -186,7 +200,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -212,7 +226,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -238,7 +252,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -288,7 +302,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -300,7 +314,7 @@ jobs:
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -342,13 +356,13 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ jobs:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
@@ -78,11 +78,11 @@ jobs:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -102,11 +102,11 @@ jobs:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -153,11 +153,11 @@ jobs:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -177,11 +177,11 @@ jobs:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
@@ -204,11 +204,11 @@ jobs:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
@@ -382,7 +382,7 @@ jobs:
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
@@ -397,11 +397,11 @@ jobs:
|
||||
# Needed to publish the binaries to the release.
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7.0.0
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -513,12 +513,15 @@ jobs:
|
||||
deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
package-manager-cache: false # never use caching in release builds
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||
if: github.event_name == 'release'
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: Check that current version ends with -dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
@@ -17,18 +17,20 @@ jobs:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
package-manager-cache: false # never use caching in release builds
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
|
||||
@@ -16,14 +16,14 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v7
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
name: check flake formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -79,11 +79,11 @@ jobs:
|
||||
#- deltachat-rpc-server-x86_64-android
|
||||
#- deltachat-rpc-server-x86-android
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -100,9 +100,9 @@ jobs:
|
||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
|
||||
@@ -14,11 +14,11 @@ jobs:
|
||||
name: Build REPL example
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
url: https://rs.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
|
||||
|
||||
build-python:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -36,12 +36,12 @@ jobs:
|
||||
url: https://py.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
|
||||
|
||||
build-c:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
url: https://c.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
|
||||
|
||||
build-ts:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -84,15 +84,15 @@ jobs:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
run: npm install
|
||||
- name: npm run build
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
|
||||
|
||||
build-cffi:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
url: https://cffi.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -125,4 +125,4 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
|
||||
-395
@@ -1,392 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.59.0] - 2026-08-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove deprecated `dc_chat_is_protected()`.
|
||||
- Deprecate `dc_chat_get_info_json()` ([#8580](https://github.com/chatmail/core/pull/8580))
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add stock strings for being added/removed from group ([#8562](https://github.com/chatmail/core/pull/8562)).
|
||||
- Client version information ([#8557](https://github.com/chatmail/core/pull/8557)).
|
||||
- Remove hidden headers.
|
||||
- Stop creating info messages for old broadcast lists.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Filtered reactions are info, not error in device chat.
|
||||
- Send MDNs to self even if MDNs are disabled.
|
||||
- Send HTTP requests in origin not absolute form.
|
||||
|
||||
### Documentation
|
||||
|
||||
- json-rpc: improve `reactions_by_contact` doc.
|
||||
- Do not refer to `is_chat_protected()`.
|
||||
- Do not talk about verified chats in securejoin QR-scanning functions.
|
||||
- Add SQL schema documentation.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Fix nightly clippy warnings.
|
||||
- cargo: bump astral-tokio-tar from 0.6.3 to 0.6.4.
|
||||
- cargo: bump bytes from 1.12.0 to 1.12.1.
|
||||
- FFI: don't swallow but log errors in three places.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `MessengerMessage`.
|
||||
- Stop setting chats.protected column explicitly.
|
||||
- Merge `msg_group_left_local` into `msg_del_member_local` ([#8575](https://github.com/chatmail/core/pull/8575)).
|
||||
- Rename `_ex()` -> `_ext()`.
|
||||
- mimefactory: add Encryption enum.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix flakyness of iroh tests by sending "forever" so that late swarm-joins still make the test work.
|
||||
- Move iroh tests into separate module.
|
||||
- Provide complete test isolation by not re-using account addresses.
|
||||
- Avoid another source of random failures with `direct_imap` failing to connect on first try.
|
||||
- Remove all cache-related logic in the FFI pytest plugin.
|
||||
- Add a CI-failing check that documented sql schema matches real one.
|
||||
- Abort early if DNS to chatmail domain does not work and nicer pytest startup header.
|
||||
- Load test data through the `data` fixture.
|
||||
- Allow to run the test suite against underscore-domain relays.
|
||||
|
||||
## [2.58.0] - 2026-08-10
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove getPushState() and core's internal tracking of it
|
||||
- [**breaking**] remove `dc_chatlist_get_context()`, because it was easy to misuse and likely led to crashes ([#8503](https://github.com/chatmail/core/pull/8503))
|
||||
- instead, store reference-counted Context in `dc_msg_t`, `dc_contact_t` and `dc_chatlist_t`
|
||||
- add "pinned messages" API.
|
||||
|
||||
### Build system
|
||||
|
||||
- update all crates to Rust 2024 edition.
|
||||
|
||||
### CI
|
||||
|
||||
- update github actions monthly instead of weekly.
|
||||
|
||||
### Documentation
|
||||
|
||||
- clarify `ChatId::do_set_draft()` docs.
|
||||
- add missing slash to ConnectionSecurity::Starttls doc comment.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- send Autocrypt pgp key in MDNs occassionally and when relaylist changes.
|
||||
- reduce unncessary gossipping of keys in group chats.
|
||||
- stop requiring XDELTAPUSH capability for push notifications.
|
||||
- prepare basic multi-relay onboarding ([#8444](https://github.com/chatmail/core/pull/8444))
|
||||
- collect ICE servers from all relays.
|
||||
- send messages to 5 relays instead of the newest 3 ones.
|
||||
- allow to send reactions in broadcast channels ([#8450](https://github.com/chatmail/core/pull/8450)).
|
||||
- allow only default reactions in channels broadcast ([#8545](https://github.com/chatmail/core/pull/8545)).
|
||||
- resend pinned state in broadcast channels ([#8549](https://github.com/chatmail/core/pull/8549)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- **The primary transport is not synchronized between devices anymore.**
|
||||
- Don't warn about correct EXIF orientation values. ([#8483](https://github.com/chatmail/core/pull/8483)).
|
||||
- deltachat-rpc-client: don't depend on execnet for importing pytest plugin, remove deprecated "py" usage.
|
||||
- send MDNs to all authentic relays of a contact, not just whatever `get_addr()` returns..
|
||||
- mark `as_path()` function unsafe.
|
||||
- python: create event emitter when EventThread is initialized.
|
||||
- Don't download pre-message again if it is known already ([#8488](https://github.com/chatmail/core/pull/8488)).
|
||||
- recognize self addresses in various places (instead of just the "primary").
|
||||
- fix multi relay connectivity view ([#8550](https://github.com/chatmail/core/pull/8550)).
|
||||
- ensure same-second primary transport change propagates correctly.
|
||||
- invalidate `configured_addr` cache before sending transport sync message.
|
||||
- prevent transport de-synchronization because of early fetch cancellation.
|
||||
- improve connectivity HTML if quota info has an error.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.58.0-dev.
|
||||
- deps: bump actions/setup-python from 6 to 6.3.0.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0.
|
||||
- cargo: bump futures from 0.3.32 to 0.3.33.
|
||||
- cargo: bump tokio from 1.52.3 to 1.53.0.
|
||||
- cargo: bump regex from 1.12.4 to 1.13.1.
|
||||
- disable "large futures" lint again.
|
||||
- cargo: bump tokio-util from 0.7.18 to 0.7.19.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1.
|
||||
- deps: bump taiki-e/install-action from 2.83.4 to 2.85.1.
|
||||
- cargo: bump `serde_json` from 1.0.150 to 1.0.151.
|
||||
- deps: bump pypa/gh-action-pypi-publish from 1.14.0 to 1.14.1.
|
||||
- deps: bump actions/setup-python from 6.3.0 to 7.0.0.
|
||||
- cargo: introduce syn 3 dependency.
|
||||
- cargo: bump anyhow from 1.0.103 to 1.0.104.
|
||||
- cargo: bump serde from 1.0.228 to 1.0.229.
|
||||
- cargo: bump thiserror from 2.0.18 to 2.0.19.
|
||||
- cargo: bump libc from 0.2.186 to 0.2.189.
|
||||
|
||||
### Performance
|
||||
|
||||
- Box::pin iroh::endpoint::Builder::bind in order to reduce memory usage.
|
||||
|
||||
### Refactor
|
||||
|
||||
- use the new regex! macro.
|
||||
- Remove FolderMeaning and `target_folder` ([#8456](https://github.com/chatmail/core/pull/8456)).
|
||||
- Unify naming of direct/single/1:1/normal chats ([#8442](https://github.com/chatmail/core/pull/8442)).
|
||||
- un-nest `prepare_msg_blob`.
|
||||
- do not clean `imap_send` table on transport change.
|
||||
- mark enabled ephemeral timer duration as NonZero.
|
||||
- reduce the scope of unsafe in `dc_context_unref()`.
|
||||
- mimefactory: separate rendering of message payload and sendable message.
|
||||
|
||||
### Tests
|
||||
|
||||
- fix flaky `test_markseen_message_and_mdn` test.
|
||||
- fix flaky `test_no_markseen_in_team_profile` ([#8500](https://github.com/chatmail/core/pull/8500)).
|
||||
- Add `test_bcc_self`.
|
||||
- Add test for unencrypted headers ([#8538](https://github.com/chatmail/core/pull/8538)).
|
||||
- Assert log warnings and errors ([#8457](https://github.com/chatmail/core/pull/8457)).
|
||||
|
||||
## [2.57.0] - 2026-07-25
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove heartbeat push notifications.
|
||||
- [**breaking**] remove provider-db handling and provider lookup APIs.
|
||||
- provider lookup APIs were removed from CFFI and JSON-RPC.
|
||||
|
||||
also removes offline provider database code and generated provider data,
|
||||
provider-specific fields in configure/transport paths, and REPL providerinfo.
|
||||
|
||||
### Documentation
|
||||
|
||||
- remove oauth2 from standards.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- accept messages from key contacts with forged From address.
|
||||
- enable TLS certificate compression.
|
||||
- read SMTP recipient limit from relay IMAP metadata.
|
||||
|
||||
### Fixes
|
||||
|
||||
- fixup CI failures.
|
||||
- never merge outer To headers if standard header protection is used.
|
||||
- Re-add oauth2 to serialized structs ([#8464](https://github.com/chatmail/core/pull/8464)).
|
||||
- migrate transports configured on 2.56 to also have a oauth:false flag.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.57.0-dev.
|
||||
- deps: bump actions/setup-node from 6 to 7.
|
||||
- deps: bump cachix/install-nix-action from 31.10.6 to 31.11.0.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.20 to 2.1.1.
|
||||
- deps: bump taiki-e/install-action from 2.82.10 to 2.83.4.
|
||||
- cargo: bump quinn-proto from 0.11.14 to 0.11.16.
|
||||
|
||||
## [2.56.0] - 2026-07-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove all oauth support and drop DC_LP_AUTH flags.
|
||||
- removed oauth2 module, dc_get_oauth2_url FFI function, DC_LP_AUTH flags and configured/serverflags, and the oauth2 parameter/field from SMTP/IMAP clients, JSON-RPC interfaces, and CLI tools.
|
||||
|
||||
also contains regenerated provider data after dropping oauth in the update script.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- do not set backup_time in exported databases.
|
||||
|
||||
### Fixes
|
||||
|
||||
- revert 207c2e6e4c1bec43204c3b8a46fcbbff67d54b3f because some users reported problems with it.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.56.0-dev.
|
||||
|
||||
## [2.55.0] - 2026-07-20
|
||||
|
||||
Minor release to fix CI because releasing 2.54.0 failed.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Node version to 24.
|
||||
|
||||
## [2.54.0] - 2026-07-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Deprecate `is_chatmail`.
|
||||
- UIs should not behave differently for chatmail relays than for classical email servers; most usages of `is_chatmail` can be replaced by `force_encryption`.
|
||||
- [**breaking**] `delete_transport()` must not be used by UIs anymore. Instead, `set_transport_unpublished()` must be called when a user clicks on "Remove".
|
||||
- [**breaking**] `list_transports()` doesn't return unpublished relays anymore.
|
||||
- UIs should use `list_transports()` rather than `list_transports_ex()`, because unpublished transports count as removed from the user point of view, and should not be shown in the relay list anymore.
|
||||
- deltachat-rpc-client: add `Account.set_transport_unpublished()`.
|
||||
- Add `MsgReadCountChanged` event.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Implement support for populating and maintaining a list of default relays ([#8341](https://github.com/chatmail/core/pull/8341)).
|
||||
- Remove hidden relays automatically ([#8402](https://github.com/chatmail/core/pull/8402)).
|
||||
- Automatically remove oldest unpublished relay in order to make space when the user wants to add more; don't allow more than 5 relays overall ([#8428](https://github.com/chatmail/core/pull/8428)).
|
||||
- Add silent group changes messages as InNoticed, not InSeen.
|
||||
- Remove `?emailaddress` argument from autoconfig URL that is not using a dedicated domain.
|
||||
- Remove `imap::Session::sync_seen_flags()` ([#7742](https://github.com/chatmail/core/pull/7742)).
|
||||
- Use CAPABILITY response code if IMAP LOGIN command returns it.
|
||||
- Increase max idle timeout for iroh backup receiver to 60 seconds.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Request MDNs for resent channel messages.
|
||||
- Make pre-messages w/o text want MDNs ([#8004](https://github.com/chatmail/core/pull/8004)).
|
||||
- Make truncated edited messages have HTML for receivers ([#8249](https://github.com/chatmail/core/pull/8249)).
|
||||
- Un-escape message footer marks in full messages (`get_html`) ([#8427](https://github.com/chatmail/core/pull/8427)).
|
||||
- Hide synced chat if we only know its visibility ([#8343](https://github.com/chatmail/core/pull/8343)).
|
||||
- Tombstone MDN before sending it ([#8252](https://github.com/chatmail/core/pull/8252)).
|
||||
- Recreate `imap_markseen` with `PRIMARY KEY` constraint.
|
||||
- Rerun the full securejoin protocol if the address was outdated ([#8358](https://github.com/chatmail/core/pull/8358)).
|
||||
- Return early from `receive_imf` to not tombstone Iroh-Node-Addr message if webxdc instance isn't found ([#8372](https://github.com/chatmail/core/pull/8372)).
|
||||
- Replace `last_added_location_id` with `last_added_location_timestamp`.
|
||||
- Do not put locations into pre-messages.
|
||||
- RUSTSEC-2026-0204 ([#8403](https://github.com/chatmail/core/pull/8403)).
|
||||
- Ensure public key signatures are not in the past compared to the public key.
|
||||
- Do not bubble up errors in IMAP candidate loop.
|
||||
- Do not log errors if full message is not available on any transport.
|
||||
- Apply reactions that arrived before the message at later time ([#8415](https://github.com/chatmail/core/pull/8415)).
|
||||
|
||||
### Performance
|
||||
|
||||
- Add timestamp to `msgs_index7` and speed up `Chatlist::try_load()` ([#7848](https://github.com/chatmail/core/pull/7848)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.97.1.
|
||||
- rrsync prepends the restricted upload path, we need to leave it out ([#8405](https://github.com/chatmail/core/pull/8405)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update STYLE.md: macros should be used only when necessary ([#8410](https://github.com/chatmail/core/pull/8410)).
|
||||
- `create_group_chat_unencrypted()` may lead to chat split on the first device.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Deprecate unused `SkipAutocrypt` param.
|
||||
- Remove commented out `RenderedEmail.envelope`.
|
||||
- Remove the ability to send messages with non-standard header protection.
|
||||
- Make `crate::pgp::symm_encrypt_message` non-async.
|
||||
- Move `ensure_secret_key_exists` into key.rs.
|
||||
- Improve comment ([#8366](https://github.com/chatmail/core/pull/8366)).
|
||||
- Remove `set_modseq()` function.
|
||||
- Remove unnecessary reference in format string.
|
||||
- Label the loop iterating over the candidates.
|
||||
- Remove `GROUP BY c.id` from chatlist queries.
|
||||
|
||||
### Tests
|
||||
|
||||
- securejoin: Check that "vc-{,request-}pubkey" messages don't contain displayname.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.54.0-dev.
|
||||
- deps: bump taiki-e/install-action from 2.81.1 to 2.81.8.
|
||||
- deps: bump taiki-e/install-action from 2.81.8 to 2.81.11.
|
||||
- update rPGP from 0.19.0 to 0.20.0.
|
||||
- update astral-tokio-tar from 0.6.2 to 0.6.3.
|
||||
- deps: bump anyhow to 1.0.103.
|
||||
- deps: bump actions/checkout from 6 to 7.
|
||||
- cargo: bump syn from 2.0.117 to 2.0.118.
|
||||
- cargo: bump quote from 1.0.45 to 1.0.46.
|
||||
- cargo: bump bytes from 1.11.1 to 1.12.0.
|
||||
- cargo: bump regex from 1.12.3 to 1.12.4.
|
||||
- cargo: bump log from 0.4.31 to 0.4.33.
|
||||
- cargo: bump hyper from 1.9.0 to 1.10.1.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7.
|
||||
- cargo: bump chrono from 0.4.44 to 0.4.45.
|
||||
- update quick-xml to 0.41.0.
|
||||
- cargo: bump brotli from 8.0.2 to 8.0.4.
|
||||
- cargo: bump smallvec from 1.15.1 to 1.15.2.
|
||||
- deps: bump taiki-e/install-action from 2.81.11 to 2.82.6.
|
||||
- update yanked spin@0.9.8 and spin@0.10.0.
|
||||
- deps: bump taiki-e/install-action from 2.82.6 to 2.82.10.
|
||||
- update async-imap to 0.11.3.
|
||||
|
||||
## [2.53.0] - 2026-06-15
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Make quality of images sent in chats more consistent between images with different aspect ratio.
|
||||
- `MsgId::get_html`: Make only one db query.
|
||||
- Do not log the recipient list for sent messages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not trash pre-messages without text but with a webxdc update.
|
||||
- Don't send or process webxdc status updates in pre-messages.
|
||||
- Ignore SecureJoin messages from blocked contacts ([#8295](https://github.com/chatmail/core/pull/8295)).
|
||||
- Do not abort IMAP connection if setting the push token fails.
|
||||
|
||||
### Documentation
|
||||
|
||||
- STYLE.md: Require to list columns explicitly in `INSERT` statements.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: switch to the "master" branch for naersk.
|
||||
- flake.nix: Use hostPlatform.rust.rustcTarget instead of hardcoding it.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Bump version to 2.52.0-dev.
|
||||
- deps: bump taiki-e/install-action from 2.79.10 to 2.81.1.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.19 to 2.0.20.
|
||||
- Bump version to 2.53.0-dev.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move the definition of the `target_wh`-variable.
|
||||
- Remove timesmearing.
|
||||
|
||||
### Tests
|
||||
|
||||
- Print multiline chat descriptions with debug formatter.
|
||||
- `exec_securejoin_qr_multi_device()`: Make inviter devices receive each other messages.
|
||||
- Fixup the tests after removing timesmearing.
|
||||
- Remove timeout from `pop_sent_msg_ex()`.
|
||||
|
||||
## [2.52.0] - 2026-06-09
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update the channel title after joining if the QR code included a wrong title ([#8260](https://github.com/chatmail/core/pull/8260)).
|
||||
- Don't send removal message to contact that hasn't been a chat member ([#8298](https://github.com/chatmail/core/pull/8298)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add cryptography-related statistics (`number_of_transports`, `key_version`, `key_algorithm`, `pubkey_size`, `number_of_keys`) ([#8293](https://github.com/chatmail/core/pull/8293), [#8297](https://github.com/chatmail/core/pull/8297)).
|
||||
- Add IMAP folder to `Context::get_info()` ([#8285](https://github.com/chatmail/core/pull/8285)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update preloaded DNS cache.
|
||||
- Use default aws-lc-rs cryptography provider for rustls.
|
||||
- Add exception for unmaintained proc-macro-error2 to deny.toml.
|
||||
- cargo: bump `pin-project` from 1.1.11 to 1.1.13.
|
||||
- cargo: bump `tokio` from 1.52.1 to 1.52.3.
|
||||
- cargo: bump `log` from 0.4.29 to 0.4.30.
|
||||
- cargo: bump `serde_json` from 1.0.149 to 1.0.150.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.18 to 2.0.19.
|
||||
- deps: bump taiki-e/install-action from 2.79.2 to 2.79.10.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: fix windows cross-compilation by adding pthreads includes.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove support for building "source" packages for deltachat-rpc-server.
|
||||
|
||||
## [2.51.0] - 2026-05-29
|
||||
|
||||
### Features / Changes
|
||||
@@ -8684,11 +8297,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
|
||||
[2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0
|
||||
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0
|
||||
[2.52.0]: https://github.com/chatmail/core/compare/v2.51.0..v2.52.0
|
||||
[2.53.0]: https://github.com/chatmail/core/compare/v2.52.0..v2.53.0
|
||||
[2.54.0]: https://github.com/chatmail/core/compare/v2.53.0..v2.54.0
|
||||
[2.55.0]: https://github.com/chatmail/core/compare/v2.54.0..v2.55.0
|
||||
[2.56.0]: https://github.com/chatmail/core/compare/v2.55.0..v2.56.0
|
||||
[2.57.0]: https://github.com/chatmail/core/compare/v2.56.0..v2.57.0
|
||||
[2.58.0]: https://github.com/chatmail/core/compare/v2.57.0..v2.58.0
|
||||
[2.59.0]: https://github.com/chatmail/core/compare/v2.58.0..v2.59.0
|
||||
|
||||
Generated
+254
-277
File diff suppressed because it is too large
Load Diff
+7
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.59.0"
|
||||
version = "2.51.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -78,10 +78,10 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.20.0", features = ["draft-pqc"], default-features = false }
|
||||
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.41", features = ["escape-html"] }
|
||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
@@ -89,6 +89,7 @@ rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.17.1"
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
@@ -100,9 +101,9 @@ tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.3", default-features = false }
|
||||
astral-tokio-tar = { version = "0.6.2", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
|
||||
@@ -59,13 +59,6 @@ If column is already declared without `NOT NULL`, use `IFNULL` function to provi
|
||||
Use `HAVING COUNT(*) > 0` clause
|
||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||
|
||||
List columns explicitly in `INSERT` statements:
|
||||
```
|
||||
INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0);
|
||||
```
|
||||
Otherwise if a new column with default value is added in a future DB version, an upgraded DB can't
|
||||
be used with the old code, e.g. after transferring a DB from a device running a newer version.
|
||||
|
||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||
an older version. Also don't change the column type, consider adding a new column with another name
|
||||
@@ -181,10 +174,3 @@ Calling `into()`, `try_into()` or `parse()`
|
||||
creates an indirection,
|
||||
which is hard to follow for people who are not familiar with Rust,
|
||||
or who are not using rust-analyzer.
|
||||
|
||||
## Use macros only when really needed
|
||||
|
||||
Macros can be hard to read for people unfamiliar with Rust,
|
||||
and can have surprising effects like evaluating arguments multiple times.
|
||||
Therefore, macros should only be used when really needed;
|
||||
using functions is usually better.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.0.0" # No semver-stable versioning
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||
license = "MPL-2.0"
|
||||
|
||||
|
||||
@@ -29,16 +29,17 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use regex::regex;
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
|
||||
mod vcard;
|
||||
pub use vcard::{VcardContact, make_vcard, parse_vcard};
|
||||
pub use vcard::{make_vcard, parse_vcard, VcardContact};
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress {
|
||||
@@ -87,7 +88,9 @@ impl rusqlite::types::ToSql for ContactAddress {
|
||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||
/// - Removes the name if it is equal to the address by setting it to ""
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
let (name, addr) = if let Some(captures) = regex!("(.*)<(.*)>").captures(addr.as_ref()) {
|
||||
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
captures.get(1).map_or("", |m| m.as_str())
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::DateTime;
|
||||
use chrono::NaiveDateTime;
|
||||
use regex::regex;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::sanitize_name_and_addr;
|
||||
|
||||
@@ -208,7 +210,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
let unfolded_lines = regex!("\r?\n[\t ]").replace_all(vcard, "");
|
||||
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
@@ -220,10 +220,7 @@ END:VCARD
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(
|
||||
contacts[0].profile_image.as_deref().unwrap(),
|
||||
"/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="
|
||||
);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,10 +244,7 @@ END:VCARD",
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||
assert_eq!(
|
||||
contacts[0].key.as_ref().unwrap(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
@@ -278,15 +272,9 @@ END:VCARD",
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice");
|
||||
assert_eq!(
|
||||
contacts[0].key.as_ref().unwrap(),
|
||||
"xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="
|
||||
);
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(
|
||||
contacts[0].profile_image.as_ref().unwrap(),
|
||||
"/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z"
|
||||
);
|
||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.59.0"
|
||||
version = "2.51.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2024"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
|
||||
+282
-96
@@ -19,6 +19,7 @@ typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
typedef struct _dc_contact dc_contact_t;
|
||||
typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
typedef struct _dc_event_channel dc_event_channel_t;
|
||||
@@ -461,10 +462,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* This is deprecated, UIs should not behave differently
|
||||
* for chatmail relays and classical email servers.
|
||||
* Most usages in UIs can be replaced by `force_encryption`.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
@@ -502,6 +500,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `send_pw` = SMTP-password, guessed if left out
|
||||
* - `send_port` = SMTP-port, guessed if left out
|
||||
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
|
||||
* - `proxy_enabled` = Proxy enabled. Disabled by default.
|
||||
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
|
||||
* - `imap_certificate_checks` = how to check IMAP and SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
@@ -588,7 +587,36 @@ int dc_set_config_from_qr (dc_context_t* context, const char* qr);
|
||||
char* dc_get_info (const dc_context_t* context);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get URL that can be used to initiate an OAuth2 authorization.
|
||||
*
|
||||
* If an OAuth2 authorization is possible for a given e-mail address,
|
||||
* this function returns the URL that should be opened in a browser.
|
||||
*
|
||||
* If the user authorizes access,
|
||||
* the given redirect_uri is called by the provider.
|
||||
* It's up to the UI to handle this call.
|
||||
*
|
||||
* The provider will attach some parameters to the URL,
|
||||
* most important the parameter `code` that should be set as the `mail_pw`.
|
||||
* With `server_flags` set to #DC_LP_AUTH_OAUTH2,
|
||||
* dc_configure() can be called as usual afterwards.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param addr E-mail address the user has entered.
|
||||
* In case the user selects a different e-mail address during
|
||||
* authorization, this is corrected in dc_configure()
|
||||
* @param redirect_uri URL that will get `code` that is used as `mail_pw` then.
|
||||
* Not all URLs are allowed here, however, the following should work:
|
||||
* `chat.delta:/PATH`, `http://localhost:PORT/PATH`,
|
||||
* `https://localhost:PORT/PATH`, `urn:ietf:wg:oauth:2.0:oob`
|
||||
* (the latter just displays the code the user can copy+paste then)
|
||||
* @return URL that can be opened in the browser to start OAuth2.
|
||||
* Returned strings must be released using dc_str_unref().
|
||||
* If OAuth2 is not possible for the given e-mail address, NULL is returned.
|
||||
*/
|
||||
char* dc_get_oauth2_url (dc_context_t* context, const char* addr, const char* redirect_uri);
|
||||
|
||||
|
||||
#define DC_CONNECTIVITY_NOT_CONNECTED 1000
|
||||
@@ -600,10 +628,13 @@ char* dc_get_info (const dc_context_t* context);
|
||||
/**
|
||||
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
|
||||
* One of:
|
||||
* - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
|
||||
* - DC_CONNECTIVITY_CONNECTING (2000): Show e.g. the string "Connecting…" or a yellow dot
|
||||
* - DC_CONNECTIVITY_WORKING (3000): Show e.g. the string "Getting new messages" or a spinning wheel
|
||||
* - DC_CONNECTIVITY_CONNECTED (4000): Show e.g. the string "Connected" or a green dot
|
||||
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
|
||||
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
|
||||
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
|
||||
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
|
||||
*
|
||||
* We don't use exact values but ranges here so that we can split up
|
||||
* states into multiple states in the future.
|
||||
*
|
||||
* Meant as a rough overview that can be shown
|
||||
* e.g. in the title of the main screen.
|
||||
@@ -635,6 +666,24 @@ int dc_get_connectivity (dc_context_t* context);
|
||||
char* dc_get_connectivity_html (dc_context_t* context);
|
||||
|
||||
|
||||
#define DC_PUSH_NOT_CONNECTED 0
|
||||
#define DC_PUSH_HEARTBEAT 1
|
||||
#define DC_PUSH_CONNECTED 2
|
||||
|
||||
/**
|
||||
* Get the current push notification state.
|
||||
* One of:
|
||||
* - DC_PUSH_NOT_CONNECTED
|
||||
* - DC_PUSH_HEARTBEAT
|
||||
* - DC_PUSH_CONNECTED
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return Push notification state.
|
||||
*/
|
||||
int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
|
||||
// connect
|
||||
|
||||
/**
|
||||
@@ -661,7 +710,7 @@ char* dc_get_connectivity_html (dc_context_t* context);
|
||||
* to get the full configuration from well-known URLs.
|
||||
*
|
||||
* - If _more_ options as `mail_server`, `mail_port`, `send_server`,
|
||||
* `send_port` or `send_user` are specified,
|
||||
* `send_port`, `send_user` or `server_flags` are specified,
|
||||
* **autoconfigure/autodiscover is skipped**.
|
||||
*
|
||||
* While dc_configure() returns immediately,
|
||||
@@ -869,14 +918,14 @@ uint32_t dc_create_chat_by_contact_id (dc_context_t* context, uint32_t co
|
||||
|
||||
|
||||
/**
|
||||
* Check, if there is a single chat with a given contact.
|
||||
* Check, if there is a normal chat with a given contact.
|
||||
* To get the chat messages, use dc_get_chat_msgs().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param contact_id The contact ID to check.
|
||||
* @return If there is a single chat with the given contact_id, this chat_id is
|
||||
* returned. If there is no single chat with the contact_id, the function
|
||||
* @return If there is a normal chat with the given contact_id, this chat_id is
|
||||
* returned. If there is no normal chat with the contact_id, the function
|
||||
* returns 0.
|
||||
*/
|
||||
uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t contact_id);
|
||||
@@ -1173,7 +1222,7 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to place a call for.
|
||||
* This needs to be a single chat.
|
||||
* This needs to be a one-to-one chat.
|
||||
* @param place_call_info any data that other devices receive
|
||||
* in #DC_EVENT_INCOMING_CALL.
|
||||
* @param has_video Whether the call has video initially.
|
||||
@@ -1579,7 +1628,7 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
|
||||
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear
|
||||
* and the user may create the chat again.
|
||||
* - **Groups are not left** - this would
|
||||
* be unexpected as (1) deleting a single chat also does not prevent new mails
|
||||
* be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||
* from arriving, (2) leaving a group requires sending a message to
|
||||
* all group members - especially for groups not used for a longer time, this is
|
||||
* really unexpected when deletion results in contacting all members again,
|
||||
@@ -1597,7 +1646,7 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
|
||||
/**
|
||||
* Block a chat.
|
||||
*
|
||||
* Blocking single chats blocks the corresponding contact. Blocking
|
||||
* Blocking 1:1 chats blocks the corresponding contact. Blocking
|
||||
* mailing lists creates a pseudo-contact in the list of blocked
|
||||
* contacts, so blocked mailing lists can be discovered and unblocked
|
||||
* the same way as the contacts. Blocking group chats deletes the
|
||||
@@ -1626,7 +1675,7 @@ void dc_accept_chat (dc_context_t* context, uint32_t ch
|
||||
/**
|
||||
* Get the contact IDs belonging to a chat.
|
||||
*
|
||||
* - for single chats, the function always returns exactly one contact,
|
||||
* - for normal chats, the function always returns exactly one contact,
|
||||
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||
*
|
||||
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||
@@ -2064,7 +2113,7 @@ int dc_resend_msgs (dc_context_t* context, const uint3
|
||||
* The concrete action depends on the type of the chat and on the users settings
|
||||
* (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||
*
|
||||
* - For single chats, the IMAP state is updated, MDN is sent
|
||||
* - For normal chats, the IMAP state is updated, MDN is sent
|
||||
* (if dc_set_config()-options `mdns_enabled` is set)
|
||||
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
*
|
||||
@@ -2626,6 +2675,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
|
||||
* to dc_check_qr().
|
||||
* @return The chat ID of the joined chat, the UI may redirect to the this chat.
|
||||
* On errors, 0 is returned, however, most errors will happen during handshake later on.
|
||||
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
|
||||
*/
|
||||
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
|
||||
|
||||
@@ -3555,9 +3605,17 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
/**
|
||||
* Get info summary for a chat, in JSON format.
|
||||
* Helper function to get the associated context object.
|
||||
*
|
||||
* @deprecated 2026-08-13, use dedicated dc_chat_get_*() getters or jsonrpc
|
||||
* @memberof dc_chatlist_t
|
||||
* @param chatlist The chatlist object to empty.
|
||||
* @return The context object associated with the chatlist. NULL if none or on errors.
|
||||
*/
|
||||
dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
|
||||
|
||||
|
||||
/**
|
||||
* Get info summary for a chat, in JSON format.
|
||||
*
|
||||
* The returned JSON string has the following key/values:
|
||||
*
|
||||
@@ -3639,7 +3697,7 @@ char* dc_chat_get_mailinglist_addr (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get name of a chat. For single chats, this is the name of the contact.
|
||||
* Get name of a chat. For one-to-one chats, this is the name of the contact.
|
||||
* For group chats, this is the name given e.g. to dc_create_group_chat() or
|
||||
* received by a group-creation message.
|
||||
*
|
||||
@@ -3656,7 +3714,7 @@ char* dc_chat_get_name (const dc_chat_t* chat);
|
||||
* Get the chat's profile image.
|
||||
* For groups, this is the image set by any group member
|
||||
* using dc_set_chat_profile_image().
|
||||
* For single chats, this is the image set by each remote user on their own
|
||||
* For normal chats, this is the image set by each remote user on their own
|
||||
* using dc_set_config(context, "selfavatar", image).
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -3670,7 +3728,7 @@ char* dc_chat_get_profile_image (const dc_chat_t* chat);
|
||||
|
||||
/**
|
||||
* Get a color for the chat.
|
||||
* For single chats, the color is calculated from the contact's e-mail address.
|
||||
* For 1:1 chats, the color is calculated from the contact's e-mail address.
|
||||
* Otherwise, the chat name is used.
|
||||
* The color can be used for an fallback avatar with white initials
|
||||
* as well as for headlines in bubbles of group chats.
|
||||
@@ -3737,7 +3795,7 @@ int dc_chat_is_unpromoted (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a chat is a self talk. Self talks are single chats with
|
||||
* Check if a chat is a self talk. Self talks are normal chats with
|
||||
* the only contact DC_CONTACT_ID_SELF.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -3778,12 +3836,23 @@ int dc_chat_is_device_talk (const dc_chat_t* chat);
|
||||
int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Deprecated, always returns 0.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return Always 0.
|
||||
* @deprecated 2025-09-09
|
||||
*/
|
||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the chat is encrypted.
|
||||
*
|
||||
* Single chats with key-contacts and group chats with key-contacts
|
||||
* 1:1 chats with key-contacts and group chats with key-contacts
|
||||
* are encrypted.
|
||||
* Single chats with emails contacts and ad-hoc groups
|
||||
* 1:1 chats with emails contacts and ad-hoc groups
|
||||
* created for email threads are not encrypted.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -4317,16 +4386,16 @@ char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_c
|
||||
* display name, or NULL.
|
||||
*
|
||||
* If this returns non-NULL, put a `~` before the override-sender-name and show the
|
||||
* override-sender-name and the sender's avatar even in single chats.
|
||||
* override-sender-name and the sender's avatar even in 1:1 chats.
|
||||
*
|
||||
* In mailing lists, sender display name and sender address do not always belong together.
|
||||
* In this case, this function gives you the name that should actually be shown over the message.
|
||||
*
|
||||
* Also, sometimes, we need to indicate a different sender in single chats:
|
||||
* Also, sometimes, we need to indicate a different sender in 1:1 chats:
|
||||
* Suppose that our user writes an e-mail to support@delta.chat, which forwards to
|
||||
* Bob <bob@delta.chat>, and Bob replies.
|
||||
*
|
||||
* Then, Bob's reply is shown in our single chat with support@delta.chat and the override-sender-name is
|
||||
* Then, Bob's reply is shown in our 1:1 chat with support@delta.chat and the override-sender-name is
|
||||
* set to `Bob`. The UI should show the sender name as `~Bob` and show the avatar, just
|
||||
* as in group messages. If the user then taps on the avatar, they can see that this message
|
||||
* comes from bob@delta.chat.
|
||||
@@ -4460,7 +4529,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||
* - DC_INFO_GROUP_DESCRIPTION_CHANGED (70) - Info-message "Description changed", UI should open the profile with the description
|
||||
* - DC_INFO_MESSAGE_PINNED (71) - Message pinned, UI should scroll to the pinned message returned by dc_msg_get_parent()
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
@@ -4520,7 +4588,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
#define DC_INFO_GROUP_DESCRIPTION_CHANGED 70
|
||||
#define DC_INFO_MESSAGE_PINNED 71
|
||||
|
||||
|
||||
/**
|
||||
@@ -4838,8 +4905,6 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
|
||||
* Used for Webxdc-info-messages
|
||||
* to jump to the corresponding instance that created the info message.
|
||||
*
|
||||
* For Pinned-info-messages, this refers to the pinned message.
|
||||
*
|
||||
* For quotes, please use the more specialized
|
||||
* dc_msg_get_quoted_text() and dc_msg_get_quoted_msg().
|
||||
*
|
||||
@@ -4879,20 +4944,6 @@ uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
|
||||
*/
|
||||
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the message is pinned.
|
||||
*
|
||||
* Pinned messages should be marked by a pin needle in the UI.
|
||||
* To pin messages or get all pinned messages, use jsonrpc's "setPinnedMessageState" and "getPinnedMessages".
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return 1=message is pinned, 0=message not pinned.
|
||||
*/
|
||||
int dc_msg_is_pinned (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
*
|
||||
@@ -5176,6 +5227,98 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
uint32_t dc_contact_get_verifier_id (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_provider_t
|
||||
*
|
||||
* Opaque object containing information about one single e-mail provider.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
*
|
||||
* Lookup is done from the local database by extracting the domain from the e-mail address.
|
||||
* Therefore the provider for custom domains cannot be identified.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
* @param email The user's e-mail address to extract the provider info form.
|
||||
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
*
|
||||
* DNS lookup is not used anymore and this function is deprecated.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
* @param email The user's e-mail address to extract the provider info form.
|
||||
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
* URL of the overview page.
|
||||
*
|
||||
* This URL allows linking to the providers page on providers.delta.chat.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string with a fully-qualified URL,
|
||||
* if there is no such URL, an empty string is returned, NULL is never returned.
|
||||
* The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_overview_page (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Get hints to be shown to the user on the login screen.
|
||||
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
|
||||
* the UI may want to highlight the hint.
|
||||
*
|
||||
* Moreover, the UI should display a "More information" link
|
||||
* that forwards to the URL returned by dc_provider_get_overview_page().
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string with the hint to show to the user, may contain multiple lines,
|
||||
* if there is no such hint, an empty string is returned, NULL is never returned.
|
||||
* The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Whether DC works with this provider.
|
||||
*
|
||||
* Can be one of #DC_PROVIDER_STATUS_OK,
|
||||
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return The status as a constant number.
|
||||
*/
|
||||
int dc_provider_get_status (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Free the provider info struct.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
*/
|
||||
void dc_provider_unref (dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_lot_t
|
||||
*
|
||||
@@ -5489,7 +5632,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CHAT_TYPE_UNDEFINED 0
|
||||
|
||||
/**
|
||||
* A single chat with a single contact.
|
||||
* A one-to-one chat with a single contact.
|
||||
*
|
||||
* dc_get_chat_contacts() contains one record for the user.
|
||||
* DC_CONTACT_ID_SELF is added _only_ for a self talk.
|
||||
@@ -5590,6 +5733,41 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_LP DC_LP
|
||||
*
|
||||
* Flags for configuring IMAP and SMTP servers.
|
||||
* These flags are optional
|
||||
* and may be set together with the username, password etc.
|
||||
* via dc_set_config() using the key "server_flags".
|
||||
*
|
||||
* @addtogroup DC_LP
|
||||
* @{
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Force OAuth2 authorization. This flag does not skip automatic configuration.
|
||||
* Before calling dc_configure() with DC_LP_AUTH_OAUTH2 set,
|
||||
* the user has to confirm access at the URL returned by dc_get_oauth2_url().
|
||||
*/
|
||||
#define DC_LP_AUTH_OAUTH2 0x2
|
||||
|
||||
|
||||
/**
|
||||
* Force NORMAL authorization, this is the default.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_AUTH_NORMAL 0x4
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
#define DC_LP_AUTH_FLAGS (DC_LP_AUTH_OAUTH2|DC_LP_AUTH_NORMAL) // if none of these flags are set, the default is chosen
|
||||
|
||||
/**
|
||||
* @defgroup DC_CERTCK DC_CERTCK
|
||||
*
|
||||
@@ -5677,7 +5855,6 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
* - getAccountFileSize()
|
||||
* - importVcard(), parseVcard(), makeVcard()
|
||||
* - sendWebxdcRealtimeData, sendWebxdcRealtimeAdvertisement(), leaveWebxdcRealtime()
|
||||
* - setPinnedMessageState(), getPinnedMessages()
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
@@ -6164,15 +6341,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_MSG_DELETED 2016
|
||||
|
||||
/**
|
||||
* Like @ref DC_EVENT_MSG_READ, but also fires on subsequent MDNs,
|
||||
* if there are multiple receivers, i.e. in groups and channels.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
*/
|
||||
#define DC_EVENT_MSG_READ_COUNT_CHANGED 2018
|
||||
|
||||
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
@@ -6497,6 +6665,61 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_MEDIA_QUALITY_WORSE 1
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
|
||||
*
|
||||
* These constants are used as return values for dc_provider_get_status().
|
||||
*
|
||||
* @addtogroup DC_PROVIDER_STATUS
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provider works out-of-the-box.
|
||||
* This provider status is returned for provider where the login
|
||||
* works by just entering the name or the e-mail address.
|
||||
*
|
||||
* - There is no need for the user to do any special things
|
||||
* (enable IMAP or so) in the provider's web interface or at other places.
|
||||
* - There is no need for the user to enter advanced settings;
|
||||
* server, port etc. are known by the core.
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_OK 1
|
||||
|
||||
/**
|
||||
* Provider works, but there are preparations needed.
|
||||
*
|
||||
* - The user has to do some special things as "Enable IMAP in the web interface",
|
||||
* what exactly, is described in the string returned by dc_provider_get_before_login_hints()
|
||||
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
|
||||
* - There is no need for the user to enter advanced settings;
|
||||
* server, port etc. should be known by the core.
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_PREPARATION 2
|
||||
|
||||
/**
|
||||
* Provider is not working.
|
||||
* This provider status is returned for providers
|
||||
* that are known to not work with Delta Chat.
|
||||
* The UI should block logging in with this provider.
|
||||
*
|
||||
* More information about that is typically provided
|
||||
* in the string returned by dc_provider_get_before_login_hints()
|
||||
* and in the page linked by dc_provider_get_overview_page().
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_BROKEN 3
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
|
||||
*
|
||||
@@ -6720,7 +6943,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message from %1$s"
|
||||
///
|
||||
/// Used in subjects of outgoing messages in single chats.
|
||||
/// Used in subjects of outgoing messages in one-to-one chats.
|
||||
/// - %1$s will be replaced by the name of the sender,
|
||||
/// this is the dc_set_config()-option `displayname` or `addr`
|
||||
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
|
||||
@@ -7160,30 +7383,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by name of the removed contact.
|
||||
#define DC_STR_REMOVE_MEMBER 178
|
||||
|
||||
/// "You were removed by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name of the contact who did the action.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_YOU_BY 179
|
||||
|
||||
/// "You were added by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name of the contact who did the action.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_ADD_YOU_BY 180
|
||||
|
||||
/// "You were removed."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_YOU 181
|
||||
|
||||
/// "You were added."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_ADD_YOU 182
|
||||
|
||||
/// "Establishing connection, please wait…"
|
||||
///
|
||||
/// Used as info message.
|
||||
@@ -7279,19 +7478,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used when creating text for the "Encryption Info" dialogs.
|
||||
#define DC_STR_MESSAGES_ARE_E2EE 242
|
||||
|
||||
/// "You pinned a message."
|
||||
#define DC_STR_MESSAGE_PINNED_BY_YOU 243
|
||||
|
||||
/// "Message pinned by %1$s."
|
||||
#define DC_STR_MESSAGE_PINNED_BY_OTHER 244
|
||||
|
||||
/// "Phasing out"
|
||||
///
|
||||
/// Used in connectivity view to flag unpublished relays.
|
||||
/// This should match the wording used for relay deletion confirmation,
|
||||
/// saying "Before deletion, it will be gradually phased out so your contacts can switch over smoothly"
|
||||
#define DC_STR_PHASING_OUT 245
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
+1155
-937
File diff suppressed because it is too large
Load Diff
+32
-44
@@ -17,15 +17,13 @@ use std::ptr;
|
||||
/// }
|
||||
/// ```
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
}
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
}
|
||||
|
||||
/// Error type for the [OsStrExt] trait
|
||||
@@ -166,40 +164,34 @@ pub(crate) trait Strdup {
|
||||
/// This function will panic when the original string contains an
|
||||
/// interior null byte as this can not be represented in raw C
|
||||
/// strings.
|
||||
fn strdup(&self) -> *mut libc::c_char;
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl Strdup for str {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for String {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let s: &str = self;
|
||||
s.strdup()
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for std::path::Path {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for [u8] {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,15 +209,15 @@ pub(crate) trait OptStrdup {
|
||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||
///
|
||||
/// See [Strdup::strdup] for details.
|
||||
fn strdup(&self) -> *mut libc::c_char;
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
match self {
|
||||
Some(s) => {
|
||||
let tmp = CString::new_lossy(s.as_ref());
|
||||
unsafe { dc_strdup(tmp.as_ptr()) }
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -263,18 +255,20 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let c_str = unsafe { std::ffi::CStr::from_ptr(s) }.to_bytes();
|
||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
||||
std::path::Path::new(os_str)
|
||||
unsafe {
|
||||
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
|
||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
||||
std::path::Path::new(os_str)
|
||||
}
|
||||
}
|
||||
|
||||
// as_path() implementation for windows, documented above.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
unsafe { as_path_unicode(s) }
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
as_path_unicode(s)
|
||||
}
|
||||
|
||||
// Implementation for as_path() on Windows.
|
||||
@@ -282,7 +276,7 @@ pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path
|
||||
// Having this as a separate function means it can be tested on unix
|
||||
// too.
|
||||
#[allow(dead_code)]
|
||||
unsafe fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
|
||||
let cstr = unsafe { CStr::from_ptr(s) };
|
||||
@@ -370,20 +364,14 @@ mod tests {
|
||||
fn test_as_path() {
|
||||
let some_path = CString::new("/some/path").unwrap();
|
||||
let ptr = some_path.as_ptr();
|
||||
assert_eq!(
|
||||
unsafe { as_path(ptr) },
|
||||
std::ffi::OsString::from("/some/path")
|
||||
)
|
||||
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_path_unicode_fn() {
|
||||
let some_path = CString::new("/some/path").unwrap();
|
||||
let ptr = some_path.as_ptr();
|
||||
assert_eq!(
|
||||
unsafe { as_path_unicode(ptr) },
|
||||
std::ffi::OsString::from("/some/path")
|
||||
);
|
||||
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.59.0"
|
||||
version = "2.51.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
|
||||
+63
-107
@@ -5,31 +5,31 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use deltachat::EventEmitter;
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, MessageListOptions, add_contact_to_chat, forward_msgs,
|
||||
forward_msgs_2ctx, get_chat_media, get_chat_msgs, get_chat_msgs_ext, markfresh_chat,
|
||||
marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat,
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
||||
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::{Config, get_all_ui_config_keys};
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::{
|
||||
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ext, get_existing_msg_ids,
|
||||
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs,
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
|
||||
markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
};
|
||||
use deltachat::provider::get_provider_info;
|
||||
use deltachat::qr::{self, Qr};
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
@@ -37,9 +37,10 @@ use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{Mutex, RwLock, watch};
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use yerpc::rpc;
|
||||
|
||||
@@ -53,6 +54,8 @@ use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
use types::notify_state::JsonrpcNotifyState;
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::reactions::JsonrpcReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
@@ -64,8 +67,7 @@ use self::types::{
|
||||
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
use crate::api::types::appversions::JsonrpcAppSource;
|
||||
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::login_param::TransportListEntry;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
@@ -328,6 +330,12 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current push notification state.
|
||||
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.push_state().await.into())
|
||||
}
|
||||
|
||||
/// Get the combined filesize of an account in bytes
|
||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -337,6 +345,21 @@ impl CommandApi {
|
||||
Ok(dbfile + total_size)
|
||||
}
|
||||
|
||||
/// Returns provider for the given domain.
|
||||
///
|
||||
/// This function looks up domain in offline database.
|
||||
///
|
||||
/// For compatibility, email address can be passed to this function
|
||||
/// instead of the domain.
|
||||
async fn get_provider_info(
|
||||
&self,
|
||||
_account_id: u32,
|
||||
email: String,
|
||||
) -> Result<Option<ProviderInfo>> {
|
||||
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
|
||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||
}
|
||||
|
||||
/// Checks if the context is already configured.
|
||||
async fn is_configured(&self, account_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -503,7 +526,7 @@ impl CommandApi {
|
||||
/// - [Self::add_transport_from_qr()] to add a transport
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::set_transport_unpublished()] to remove a transport.
|
||||
/// - [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,
|
||||
@@ -529,31 +552,23 @@ 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::set_transport_unpublished()] to remove a transport.
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
/// Use [Self::list_transports_ex()] to additionally query
|
||||
/// whether the transports are marked as 'unpublished'.
|
||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let res = ctx
|
||||
.list_transports()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|t| !t.is_unpublished)
|
||||
.map(|t| t.param.into())
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Deprecated 2026-06: This is not needed by UI implementations anymore,
|
||||
/// because unpublished relays now count as removed from the user point of view,
|
||||
/// and must not be shown in the list of relays.
|
||||
/// This means that UIs should use `list_transports()` instead of this function.
|
||||
///
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
///
|
||||
/// As opposed to `list_transports()`, this function also returns unpublished transports,
|
||||
/// and for each returned transport it returns the information whether or not is `unpublished`.
|
||||
///
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::set_transport_unpublished()] to change whether a transport is 'published'.
|
||||
/// 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
|
||||
@@ -565,31 +580,23 @@ impl CommandApi {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Immediately deletes a transport, potentially causing messages not to arrive.
|
||||
/// This must ONLY be used by the automated tests.
|
||||
/// UI implementations must use [`Self::set_transport_unpublished`] instead.
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.delete_transport(&addr).await
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
/// UIs should call this function when the user clicks on "Remove".
|
||||
/// Core will keep listening on this transport for some time,
|
||||
/// and automatically remove it once it is no longer needed.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// When more transports are added by [`Self::add_or_update_transport()`] or [`Self::add_transport_from_qr`],
|
||||
/// the least recently needed unpublished transport is automatically removed
|
||||
/// if this is necessary in order to stay below the maximum number of allowed relays.
|
||||
/// Also, unpublished transports that are not used to receive any new messages for a time defined by
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`] are automatically removed.
|
||||
///
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`]: deltachat::sql::UNPUBLISHED_TRANSPORT_KEEP_TIME
|
||||
/// 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,
|
||||
@@ -852,7 +859,7 @@ impl CommandApi {
|
||||
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
||||
/// and the user may create the chat again.
|
||||
/// - **Groups are not left** - this would
|
||||
/// be unexpected as (1) deleting a single chat also does not prevent new mails
|
||||
/// be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||
/// from arriving, (2) leaving a group requires sending a message to
|
||||
/// all group members - especially for groups not used for a longer time, this is
|
||||
/// really unexpected when deletion results in contacting all members again,
|
||||
@@ -936,6 +943,7 @@ impl CommandApi {
|
||||
/// to `check_qr()`.
|
||||
///
|
||||
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
|
||||
/// A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
|
||||
///
|
||||
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1016,7 +1024,7 @@ impl CommandApi {
|
||||
|
||||
/// Get the contact IDs belonging to a chat.
|
||||
///
|
||||
/// - for single chats, the function always returns exactly one contact,
|
||||
/// - for normal chats, the function always returns exactly one contact,
|
||||
/// DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||
///
|
||||
/// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||
@@ -1073,9 +1081,7 @@ impl CommandApi {
|
||||
/// Create a new unencrypted group chat.
|
||||
///
|
||||
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||
/// address-contacts. NB: Chats with similar names and the same members are merged on other
|
||||
/// devices, but usually users don't create such chats and look up the existing one instead, so
|
||||
/// chat split on the first device is acceptable.
|
||||
/// address-contacts.
|
||||
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_group_unencrypted(&ctx, &name)
|
||||
@@ -1344,7 +1350,7 @@ impl CommandApi {
|
||||
/// The concrete action depends on the type of the chat and on the users settings
|
||||
/// (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||
///
|
||||
/// - For single chats, the IMAP state is updated, MDN is sent
|
||||
/// - For normal chats, the IMAP state is updated, MDN is sent
|
||||
/// (if set_config()-options `mdns_enabled` is set)
|
||||
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
///
|
||||
@@ -1393,7 +1399,7 @@ impl CommandApi {
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ext(
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
@@ -1442,7 +1448,7 @@ impl CommandApi {
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ext(
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
@@ -1508,32 +1514,12 @@ impl CommandApi {
|
||||
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Sets the "pinned" state for a message.
|
||||
async fn set_pinned_message_state(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
pinned_state: bool,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
deltachat::pinned_messages::set_pinned_state(&ctx, MsgId::new(message_id), pinned_state)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all pinned messages of a chat.
|
||||
async fn get_pinned_messages(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_ids =
|
||||
deltachat::pinned_messages::get_pinned_messages(&ctx, ChatId::new(chat_id)).await?;
|
||||
Ok(msg_ids.into_iter().map(|id| id.to_u32()).collect())
|
||||
}
|
||||
|
||||
/// Delete messages. The messages are deleted on the current device and
|
||||
/// on the IMAP server.
|
||||
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||
delete_msgs_ext(&ctx, &msgs, false).await
|
||||
delete_msgs_ex(&ctx, &msgs, false).await
|
||||
}
|
||||
|
||||
/// Delete messages. The messages are deleted on the current device,
|
||||
@@ -1541,7 +1527,7 @@ impl CommandApi {
|
||||
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||
delete_msgs_ext(&ctx, &msgs, true).await
|
||||
delete_msgs_ex(&ctx, &msgs, true).await
|
||||
}
|
||||
|
||||
/// Get an informational text for a single message. The text is multiline and may
|
||||
@@ -1906,7 +1892,7 @@ impl CommandApi {
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Returns the [`ChatId`] for the single chat with `contact_id` if it exists.
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
///
|
||||
/// If it does not exist, `None` is returned.
|
||||
async fn get_chat_id_by_contact_id(
|
||||
@@ -2091,10 +2077,13 @@ impl CommandApi {
|
||||
|
||||
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
|
||||
/// One of:
|
||||
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
|
||||
/// - DC_CONNECTIVITY_CONNECTING (2000): Show e.g. the string "Connecting…" or a yellow dot
|
||||
/// - DC_CONNECTIVITY_WORKING (3000): Show e.g. the string "Getting new messages" or a spinning wheel
|
||||
/// - DC_CONNECTIVITY_CONNECTED (4000): Show e.g. the string "Connected" or a green dot
|
||||
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
|
||||
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
|
||||
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
|
||||
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
|
||||
///
|
||||
/// We don't use exact values but ranges here so that we can split up
|
||||
/// states into multiple states in the future.
|
||||
///
|
||||
/// Meant as a rough overview that can be shown
|
||||
/// e.g. in the title of the main screen.
|
||||
@@ -2288,7 +2277,7 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
let blob = message.get_webxdc_blob(&ctx, &path).await?;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||
}
|
||||
|
||||
@@ -2789,39 +2778,6 @@ impl CommandApi {
|
||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get version information of a specific client and source
|
||||
/// across all configured accounts and transports.
|
||||
///
|
||||
/// Returns the source with the highest `version_integer`.
|
||||
/// If no matching version information is available at all, `None` is returned.
|
||||
///
|
||||
/// UIs shall call the function after a reasonable time after app start,
|
||||
/// when most relays have reported the information they have, say 30 seconds.
|
||||
/// After that, once a day.
|
||||
/// (it is accepted if by the simple approach an update message is delayed.
|
||||
/// an event was considered, but that seemed more complex for few benefit:
|
||||
/// as we do not know if "late" relays will report "better" versions,
|
||||
/// also there we would work with timeouts etc.)
|
||||
///
|
||||
/// If the reported `version_integer` is larger than the running app version,
|
||||
/// the UI shall report to the user, that an update is available,
|
||||
/// and, if possible, offer a direct update by the given URL.
|
||||
///
|
||||
/// Security note: consumers need to verify themselves
|
||||
/// that downloaded app files are valid before installing them.
|
||||
async fn get_app_version(
|
||||
&self,
|
||||
client_id: String,
|
||||
source_id: String,
|
||||
) -> Result<Option<JsonrpcAppSource>> {
|
||||
let accounts = self.accounts.read().await;
|
||||
Ok(
|
||||
deltachat::appversions::get_app_version(&accounts, &client_id, &source_id)
|
||||
.await?
|
||||
.map(JsonrpcAppSource::from_core_type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions (to prevent code duplication)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
use deltachat::appversions::AppSource;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
/// Version information of a single source of a client, eg. "gplay" or "fdroid".
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "AppSource", rename_all = "camelCase")]
|
||||
pub struct JsonrpcAppSource {
|
||||
/// Always increasing version number.
|
||||
pub version_integer: u32,
|
||||
|
||||
/// Any version string.
|
||||
pub version_string: String,
|
||||
|
||||
/// Where to download that version.
|
||||
/// Security note: consumers need to verify themselves
|
||||
/// that downloaded app files are valid before installing them.
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
impl JsonrpcAppSource {
|
||||
pub fn from_core_type(source: AppSource) -> Self {
|
||||
JsonrpcAppSource {
|
||||
version_integer: source.version_integer,
|
||||
version_string: source.version_string,
|
||||
download_url: source.download_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{CallState, call_state};
|
||||
use deltachat::calls::{call_state, CallState};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::MsgId;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use deltachat::chat::{self, ChatVisibility, get_chat_contacts, get_past_chat_contacts};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
|
||||
@@ -4,7 +4,7 @@ use deltachat::chatlist::get_last_message_for_chat;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::{
|
||||
chat::{ChatVisibility, get_chat_contacts},
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
|
||||
@@ -203,17 +203,6 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Like [`EventType::MsgRead`], but also fires on subsequent MDNs,
|
||||
/// if there are multiple receivers, i.e. in groups and channels.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgReadCountChanged {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
|
||||
/// ID of the message that was read.
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message was deleted.
|
||||
///
|
||||
/// This event means that the message will no longer appear in the messagelist.
|
||||
@@ -557,10 +546,6 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgReadCountChanged { chat_id, msg_id } => MsgReadCountChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct HttpResponse {
|
||||
|
||||
impl From<CoreHttpResponse> for HttpResponse {
|
||||
fn from(response: CoreHttpResponse) -> Self {
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
||||
let mimetype = response.mimetype;
|
||||
let encoding = response.encoding;
|
||||
|
||||
@@ -66,6 +66,10 @@ pub struct EnteredLoginParam {
|
||||
/// invalid hostnames.
|
||||
/// Default: Automatic
|
||||
pub certificate_checks: Option<EnteredCertificateChecks>,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore).
|
||||
/// Default: false
|
||||
pub oauth2: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<dc::TransportListEntry> for TransportListEntry {
|
||||
@@ -96,6 +100,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
smtp_user: param.smtp.user.into_option(),
|
||||
smtp_password: param.smtp.password.into_option(),
|
||||
certificate_checks: certificate_checks.into_option(),
|
||||
oauth2: param.oauth2.into_option(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +127,7 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||
password: param.smtp_password.unwrap_or_default(),
|
||||
},
|
||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||
oauth2: false,
|
||||
oauth2: param.oauth2.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -169,8 +174,9 @@ impl From<Socket> for dc::Socket {
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EnteredCertificateChecks {
|
||||
/// `Automatic` means strict certificate checks,
|
||||
/// unless a legacy-domain override disables them.
|
||||
/// `Automatic` means that provider database setting should be taken.
|
||||
/// If there is no provider database setting for certificate checks,
|
||||
/// check certificates strictly.
|
||||
#[default]
|
||||
Automatic,
|
||||
|
||||
|
||||
@@ -103,8 +103,6 @@ pub struct MessageObject {
|
||||
|
||||
saved_message_id: Option<u32>,
|
||||
|
||||
is_pinned: bool,
|
||||
|
||||
reactions: Option<JsonrpcReactions>,
|
||||
|
||||
vcard_contact: Option<VcardContact>,
|
||||
@@ -265,7 +263,6 @@ impl MessageObject {
|
||||
.await?
|
||||
.map(|id| id.to_u32()),
|
||||
|
||||
is_pinned: message.is_pinned(),
|
||||
reactions,
|
||||
|
||||
vcard_contact: vcard_contacts.first().cloned(),
|
||||
@@ -393,11 +390,11 @@ pub enum SystemMessageType {
|
||||
LocationOnly,
|
||||
InvalidUnencryptedMail,
|
||||
|
||||
/// Single chats info message telling that SecureJoin has started and the user should wait for it
|
||||
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
|
||||
/// to complete.
|
||||
SecurejoinWait,
|
||||
|
||||
/// Single chats info message telling that SecureJoin is still running, but the user may already
|
||||
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
|
||||
/// send messages.
|
||||
SecurejoinWaitTimeout,
|
||||
|
||||
@@ -428,8 +425,6 @@ pub enum SystemMessageType {
|
||||
|
||||
CallAccepted,
|
||||
CallEnded,
|
||||
MessagePinned,
|
||||
MessageUnpinned,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -459,8 +454,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
||||
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
||||
SystemMessage::MessagePinned => SystemMessageType::MessagePinned,
|
||||
SystemMessage::MessageUnpinned => SystemMessageType::MessageUnpinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,9 +726,9 @@ impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
|
||||
fn from(value: deltachat::ephemeral::Timer) -> Self {
|
||||
match value {
|
||||
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
|
||||
deltachat::ephemeral::Timer::Enabled { duration } => EphemeralTimer::Enabled {
|
||||
duration: duration.get(),
|
||||
},
|
||||
deltachat::ephemeral::Timer::Enabled { duration } => {
|
||||
EphemeralTimer::Enabled { duration }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod account;
|
||||
pub mod appversions;
|
||||
pub mod calls;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
@@ -9,6 +8,8 @@ pub mod http;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
pub mod notify_state;
|
||||
pub mod provider_info;
|
||||
pub mod qr;
|
||||
pub mod reactions;
|
||||
pub mod webxdc;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use deltachat::push::NotifyState;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "NotifyState")]
|
||||
pub enum JsonrpcNotifyState {
|
||||
/// Not subscribed to push notifications.
|
||||
NotConnected,
|
||||
|
||||
/// Subscribed to heartbeat push notifications.
|
||||
Heartbeat,
|
||||
|
||||
/// Subscribed to push notifications for new messages.
|
||||
Connected,
|
||||
}
|
||||
|
||||
impl From<NotifyState> for JsonrpcNotifyState {
|
||||
fn from(state: NotifyState) -> Self {
|
||||
match state {
|
||||
NotifyState::NotConnected => Self::NotConnected,
|
||||
NotifyState::Heartbeat => Self::Heartbeat,
|
||||
NotifyState::Connected => Self::Connected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use deltachat::provider::Provider;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderInfo {
|
||||
/// Unique ID, corresponding to provider database filename.
|
||||
pub id: String,
|
||||
pub before_login_hint: String,
|
||||
pub overview_page: String,
|
||||
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
|
||||
}
|
||||
|
||||
impl ProviderInfo {
|
||||
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
|
||||
provider.map(|p| ProviderInfo {
|
||||
id: p.id.to_owned(),
|
||||
before_login_hint: p.before_login_hint.to_owned(),
|
||||
overview_page: p.overview_page.to_owned(),
|
||||
status: p.status.to_u32().unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,6 @@ impl From<Qr> for QrObject {
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
..
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
@@ -256,7 +255,6 @@ impl From<Qr> for QrObject {
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
..
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
@@ -278,7 +276,6 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
invitenumber,
|
||||
is_v3,
|
||||
..
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use deltachat::contact::ContactId;
|
||||
use deltachat::reaction::Reactions;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
@@ -23,11 +24,8 @@ pub struct JsonrpcReaction {
|
||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||
pub struct JsonrpcReactions {
|
||||
/// Map from a contact to it's reaction to message.
|
||||
///
|
||||
/// There is only a single reaction per contact,
|
||||
/// but this contains a list of reactions for historical reasons.
|
||||
///
|
||||
/// For channels subscribers, this map is empty or contains `ContactId::SELF` only.
|
||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||
/// Unique reactions and their count, sorted in descending order.
|
||||
reactions: Vec<JsonrpcReaction>,
|
||||
@@ -36,24 +34,30 @@ pub struct JsonrpcReactions {
|
||||
impl From<Reactions> for JsonrpcReactions {
|
||||
fn from(reactions: Reactions) -> Self {
|
||||
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
|
||||
.by_contact
|
||||
.iter()
|
||||
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
|
||||
.collect();
|
||||
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||
|
||||
let reactions = reactions
|
||||
.frequencies
|
||||
.into_iter()
|
||||
.map(|entry| JsonrpcReaction {
|
||||
emoji: entry.reaction.as_str().to_string(),
|
||||
count: entry.count,
|
||||
is_from_self: entry.is_from_self,
|
||||
})
|
||||
.collect();
|
||||
let mut reactions_v = Vec::new();
|
||||
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
||||
let is_from_self = if let Some(self_reaction) = self_reaction {
|
||||
self_reaction.contains(&emoji)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let reaction = JsonrpcReaction {
|
||||
emoji,
|
||||
count,
|
||||
is_from_self,
|
||||
};
|
||||
reactions_v.push(reaction)
|
||||
}
|
||||
|
||||
JsonrpcReactions {
|
||||
reactions_by_contact,
|
||||
reactions,
|
||||
reactions: reactions_v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.59.0"
|
||||
"version": "2.51.0-dev"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const constants = data
|
||||
key.startsWith("DC_CERTCK_") ||
|
||||
key.startsWith("DC_SOCKET_") ||
|
||||
key.startsWith("DC_LP_AUTH_") ||
|
||||
key.startsWith("DC_PUSH_") ||
|
||||
key.startsWith("DC_TEXT1_") ||
|
||||
key.startsWith("DC_CHAT_TYPE")
|
||||
);
|
||||
|
||||
@@ -148,6 +148,23 @@ describe("online tests", function () {
|
||||
expect(message2.text).equal("super secret message");
|
||||
expect(message2.showPadlock).equal(true);
|
||||
});
|
||||
|
||||
it("get provider info for example.com", async () => {
|
||||
const acc = await dc.rpc.addAccount();
|
||||
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||
expect(info).to.be.not.null;
|
||||
expect(info?.overviewPage).to.equal(
|
||||
"https://providers.delta.chat/example-com",
|
||||
);
|
||||
expect(info?.status).to.equal(3);
|
||||
});
|
||||
|
||||
it("get provider info - domain and email should give same result", async () => {
|
||||
const acc = await dc.rpc.addAccount();
|
||||
const info_domain = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||
const info_email = await dc.rpc.getProviderInfo(acc, "hi@example.com");
|
||||
expect(info_email).to.deep.equal(info_domain);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEvent<T extends DcEvent["kind"]>(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "ratelimit"
|
||||
version = "1.0.0"
|
||||
description = "Token bucket implementation"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.59.0"
|
||||
version = "2.51.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -5,10 +5,9 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail, ensure};
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
@@ -25,6 +24,7 @@ use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use deltachat::{config, provider};
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
@@ -319,6 +319,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
info\n\
|
||||
set <configuration-key> [<value>]\n\
|
||||
get <configuration-key>\n\
|
||||
oauth2\n\
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
@@ -395,6 +396,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
createqrsvg <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
clear -- clear screen\n\
|
||||
@@ -616,7 +618,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let sel_chat = sel_chat.as_ref().unwrap();
|
||||
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist = chat::get_chat_msgs_ext(
|
||||
let msglist = chat::get_chat_msgs_ex(
|
||||
&context,
|
||||
sel_chat.get_id(),
|
||||
chat::MessageListOptions {
|
||||
@@ -1203,14 +1205,33 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
fs::write(&file, svg).await?;
|
||||
println!("{file:#?} written.");
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
match provider::get_provider_info(arg1) {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {arg1}:");
|
||||
println!("status: {}", info.status as u32);
|
||||
println!("before_login_hint: {}", info.before_login_hint);
|
||||
println!("after_login_hint: {}", info.after_login_hint);
|
||||
println!("overview_page: {}", info.overview_page);
|
||||
for server in info.server.iter() {
|
||||
println!("server: {}:{}", server.hostname, server.port,);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("No information for provider belonging to {arg1} found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
let Ok(buf) = read_file(&context, Path::new(arg1)).await else {
|
||||
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
} else {
|
||||
bail!("Command failed.");
|
||||
};
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
}
|
||||
}
|
||||
"estimatedeletion" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||
|
||||
@@ -9,12 +9,14 @@ extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
|
||||
use anyhow::{Error, bail};
|
||||
use deltachat::EventType;
|
||||
use anyhow::{bail, Error};
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
use deltachat::context::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::EventType;
|
||||
use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
@@ -160,10 +162,11 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 10] = [
|
||||
const DB_COMMANDS: [&str; 11] = [
|
||||
"info",
|
||||
"set",
|
||||
"get",
|
||||
"oauth2",
|
||||
"configure",
|
||||
"connect",
|
||||
"disconnect",
|
||||
@@ -237,7 +240,7 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"import-vcard",
|
||||
"make-vcard",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 13] = [
|
||||
const MISC_COMMANDS: [&str; 14] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
@@ -245,6 +248,7 @@ const MISC_COMMANDS: [&str; 13] = [
|
||||
"joinqr",
|
||||
"setqr",
|
||||
"createqrsvg",
|
||||
"providerinfo",
|
||||
"fileinfo",
|
||||
"estimatedeletion",
|
||||
"clear",
|
||||
@@ -266,11 +270,10 @@ impl Hinter for DcHelper {
|
||||
&CONTACT_COMMANDS[..],
|
||||
&MISC_COMMANDS[..],
|
||||
] {
|
||||
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos]))
|
||||
&& *entry != line
|
||||
&& *entry != &line[..pos]
|
||||
{
|
||||
return Some(entry[pos..].to_owned());
|
||||
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos])) {
|
||||
if *entry != line && *entry != &line[..pos] {
|
||||
return Some(entry[pos..].to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,6 +425,19 @@ async fn handle_cmd(
|
||||
"configure" => {
|
||||
ctx.configure().await?;
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
if let Some(oauth2_url) =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
||||
{
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||
} else {
|
||||
println!("OAuth2 not available for {addr}.");
|
||||
}
|
||||
} else {
|
||||
println!("oauth2: set addr first.");
|
||||
}
|
||||
}
|
||||
"clear" => {
|
||||
println!("\n\n\n");
|
||||
print!("\x1b[1;1H\x1b[2J");
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.59.0"
|
||||
version = "2.51.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -157,7 +157,7 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||
"""
|
||||
# You removed member a@b.
|
||||
# You added member a@b.
|
||||
# You were removed by a@b.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
@@ -36,15 +35,6 @@ class Account:
|
||||
if event_type is None or next_event.kind == event_type:
|
||||
return next_event
|
||||
|
||||
def wait_for_realtime_data(self, msg_id: int) -> bytes:
|
||||
"""Wait for the next realtime data received for the given webxdc message and return it."""
|
||||
logging.info(f"account {self.id}: waiting for realtime data for msg {msg_id}")
|
||||
while True:
|
||||
event = self.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||
if event.msg_id == msg_id:
|
||||
logging.info(f"account {self.id}: got realtime data for msg {msg_id}: {event.data[:20]}")
|
||||
return bytes(event.data)
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Remove all queued-up events for a given account.
|
||||
|
||||
@@ -143,10 +133,6 @@ class Account:
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
def set_transport_unpublished(self, addr: str, unpublished: bool = True):
|
||||
"""Unpublish the transport."""
|
||||
self._rpc.set_transport_unpublished(self.id, addr, unpublished)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
@@ -196,7 +182,7 @@ class Account:
|
||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
"""Create a single chat with another account."""
|
||||
"""Create a 1:1 chat with another account."""
|
||||
return self.create_contact(account).create_chat()
|
||||
|
||||
def get_device_chat(self) -> Chat:
|
||||
@@ -232,7 +218,7 @@ class Account:
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||
"""Return single chat for a contact if it exists."""
|
||||
"""Return 1:1 chat for a contact if it exists."""
|
||||
if isinstance(contact, Contact):
|
||||
assert contact.account == self
|
||||
contact_id = contact.id
|
||||
|
||||
@@ -252,7 +252,7 @@ class Chat:
|
||||
def get_contacts(self) -> list[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single chats self-address is not included.
|
||||
For single/direct chats self-address is not included.
|
||||
"""
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
@@ -54,7 +54,6 @@ class EventType(str, Enum):
|
||||
MSG_DELIVERED = "MsgDelivered"
|
||||
MSG_FAILED = "MsgFailed"
|
||||
MSG_READ = "MsgRead"
|
||||
MSG_READ_COUNT_CHANGED = "MsgReadCountChanged"
|
||||
MSG_DELETED = "MsgDeleted"
|
||||
CHAT_MODIFIED = "ChatModified"
|
||||
CHAT_DELETED = "ChatDeleted"
|
||||
@@ -97,7 +96,7 @@ class ChatType(str, Enum):
|
||||
"""Chat type."""
|
||||
|
||||
SINGLE = "Single"
|
||||
"""Single chat (a chat with a with a single contact)"""
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
|
||||
GROUP = "Group"
|
||||
|
||||
@@ -231,6 +230,14 @@ class KeyGenType(IntEnum):
|
||||
RSA4096 = 3
|
||||
|
||||
|
||||
# "Lp" means "login parameters"
|
||||
class LpAuthFlag(IntEnum):
|
||||
"""Authorization flags."""
|
||||
|
||||
OAUTH2 = 0x2
|
||||
NORMAL = 0x4
|
||||
|
||||
|
||||
class MediaQuality(IntEnum):
|
||||
"""Media quality setting."""
|
||||
|
||||
@@ -246,6 +253,14 @@ class ProviderStatus(IntEnum):
|
||||
BROKEN = 3
|
||||
|
||||
|
||||
class PushNotifyState(IntEnum):
|
||||
"""Push notifications state."""
|
||||
|
||||
NOT_CONNECTED = 0
|
||||
HEARTBEAT = 1
|
||||
CONNECTED = 2
|
||||
|
||||
|
||||
class ShowEmails(IntEnum):
|
||||
"""Show emails mode."""
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class Contact:
|
||||
return snapshot
|
||||
|
||||
def create_chat(self) -> "Chat":
|
||||
"""Create or get an existing single chat for this contact."""
|
||||
"""Create or get an existing 1:1 chat for this contact."""
|
||||
from .chat import Chat
|
||||
|
||||
return Chat(
|
||||
|
||||
@@ -7,13 +7,12 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import random
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import execnet
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
@@ -27,39 +26,16 @@ Currently this is "Messages are end-to-end encrypted."
|
||||
"""
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
# Run only in the xdist controller, before the workers exist.
|
||||
if not hasattr(config, "workerinput"):
|
||||
domain = os.environ.get("CHATMAIL_DOMAIN")
|
||||
if domain:
|
||||
check_chatmail_domain_and_warmup_dns_cache(domain)
|
||||
|
||||
|
||||
def check_chatmail_domain_and_warmup_dns_cache(domain):
|
||||
for i in range(6):
|
||||
try:
|
||||
socket.getaddrinfo(domain, 443)
|
||||
return
|
||||
except socket.gaierror as e:
|
||||
error = e
|
||||
logging.warning(f"DNS resolution of {domain} failed (attempt {i}): {e}")
|
||||
time.sleep(10)
|
||||
|
||||
pytest.exit(f"cannot resolve chatmail relay domain {domain}: {error}")
|
||||
|
||||
|
||||
def pytest_report_header():
|
||||
headers = [f"CHATMAIL_DOMAIN: {os.environ.get('CHATMAIL_DOMAIN')}"]
|
||||
for base in os.get_exec_path():
|
||||
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
|
||||
if fn.exists():
|
||||
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
|
||||
proc.wait()
|
||||
version = proc.stderr.read().decode().strip()
|
||||
headers.append(f"RPC-SERVER: {fn} [{version}]")
|
||||
break
|
||||
return f"deltachat-rpc-server: {fn} [{version}]"
|
||||
|
||||
return headers
|
||||
return None
|
||||
|
||||
|
||||
class ACFactory:
|
||||
@@ -128,7 +104,7 @@ class ACFactory:
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
"""Create a new single chat between ac1 and ac2 accepted on both sides.
|
||||
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
|
||||
|
||||
Returned chat is a chat with ac2 from ac1 point of view.
|
||||
"""
|
||||
@@ -200,9 +176,9 @@ def data():
|
||||
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
for path in pathlib.Path(__file__).parents:
|
||||
datadir = path / "test-data"
|
||||
if datadir.is_dir():
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join("test-data")
|
||||
if datadir.isdir():
|
||||
self.path = datadir
|
||||
return
|
||||
raise Exception("Data path cannot be found")
|
||||
@@ -298,24 +274,17 @@ def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||
The 'eval' function allows to remote-execute arbitrary expressions
|
||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||
"""
|
||||
from execnet import makegateway
|
||||
|
||||
def factory(core_version):
|
||||
python, rpc_server_path = get_core_python_env(core_version)
|
||||
gw = makegateway(f"popen//python={python}")
|
||||
gw = execnet.makegateway(f"popen//python={python}")
|
||||
|
||||
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
||||
channel = gw.remote_exec(remote_bob_loop)
|
||||
|
||||
# old cores need "ic=3" to accept
|
||||
# the self-signed cert of an underscore domain
|
||||
addr, password = acfactory.get_credentials()
|
||||
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
|
||||
if os.environ["CHATMAIL_DOMAIN"].startswith("_"):
|
||||
dclogin_qr += "&ic=3"
|
||||
cm = os.environ.get("CHATMAIL_DOMAIN")
|
||||
|
||||
# trigger getting an online account on bob's side
|
||||
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
|
||||
channel.send((accounts_dir, str(rpc_server_path), cm))
|
||||
|
||||
# meanwhile get a local alice account
|
||||
alice = acfactory.get_online_account()
|
||||
@@ -347,8 +316,10 @@ def remote_bob_loop(channel):
|
||||
import os
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
accounts_dir, rpc_server_path, dclogin_qr = channel.receive()
|
||||
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
|
||||
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
|
||||
|
||||
# older core versions don't support specifying rpc_server_path
|
||||
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
||||
@@ -359,13 +330,8 @@ def remote_bob_loop(channel):
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
||||
|
||||
# ACFactory would configure from a "dcaccount" QR,
|
||||
# which old cores cannot use on underscore domains
|
||||
bob = dc.add_account()
|
||||
bob.add_transport_from_qr(dclogin_qr)
|
||||
bob.bring_online()
|
||||
|
||||
acfactory = ACFactory(dc)
|
||||
bob = acfactory.get_online_account()
|
||||
alice_vcard = channel.receive()
|
||||
[alice_contact] = bob.import_vcard(alice_vcard)
|
||||
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
||||
|
||||
@@ -37,11 +37,7 @@ class DirectImap:
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
if host.startswith("_"):
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
@@ -110,7 +110,7 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory: ACFactory, data) -> None:
|
||||
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test if download on demand emits chatlist update events.
|
||||
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
||||
@@ -128,7 +128,7 @@ def test_download_on_demand(acfactory: ACFactory, data) -> None:
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
file=data.get_path("image/screenshot.jpg"),
|
||||
file="../test-data/image/screenshot.jpg",
|
||||
)
|
||||
|
||||
message = alice.wait_for_incoming_msg()
|
||||
|
||||
@@ -45,7 +45,7 @@ def test_qr_setup_contact(acfactory, alice_and_remote_bob, version) -> None:
|
||||
|
||||
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
"""Test other-core Bob profile can send a message to Alice on current core."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||
|
||||
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
|
||||
def test_second_device(acfactory, alice_and_remote_bob) -> None:
|
||||
"""Test setting up current version as a second device for old version."""
|
||||
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
|
||||
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||
|
||||
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
|
||||
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
|
||||
|
||||
@@ -68,17 +68,13 @@ 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.")
|
||||
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# Each profile flags two messages but the logged UID set
|
||||
# covers a varying number of them, so just count UIDs mentioned.
|
||||
# We are not processing UID ranges, here we just care for two UIDs.
|
||||
for ac in ac1, ac2:
|
||||
uids = set()
|
||||
while len(uids) < 2:
|
||||
while True:
|
||||
event = ac.wait_for_event()
|
||||
if event.kind == EventType.INFO and (match := rex.search(event.msg)):
|
||||
uids.update(re.split("[,:]", match.group(1)))
|
||||
if event.kind == EventType.INFO and rex.search(event.msg):
|
||||
break
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
|
||||
@@ -7,25 +7,15 @@ If you want to debug iroh at rust-trace/log level set
|
||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
# Relays on underscore domains advertise themselves as iroh relay
|
||||
# but serve a self-signed certificate that iroh's TLS stack rejects.
|
||||
# Skipping instead of xfailing keeps the run fast:
|
||||
# these tests only fail after waiting for realtime connections to time out.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("CHATMAIL_DOMAIN", "").startswith("_"),
|
||||
reason="iroh does not accept the self-signed certificate of an underscore domain",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def path_to_webxdc(request):
|
||||
@@ -45,11 +35,7 @@ def log(msg):
|
||||
logging.info(msg)
|
||||
|
||||
|
||||
# payload used to probe/establish realtime connectivity, filtered out by tests
|
||||
SETUP_DATA = b"realtime-setup"
|
||||
|
||||
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
||||
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
@@ -66,39 +52,40 @@ def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
if wait:
|
||||
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg)])
|
||||
|
||||
return ac1_webxdc_msg, ac2_webxdc_msg
|
||||
|
||||
|
||||
@contextmanager
|
||||
def send_realtime_data_forever(msgs, data=None):
|
||||
stop = threading.Event()
|
||||
data = data or [SETUP_DATA] * len(msgs)
|
||||
def setup_thread_send_realtime_data(msg, data):
|
||||
def thread_run():
|
||||
for _i in range(10):
|
||||
msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
def thread_run(msg, payload):
|
||||
for i in itertools.count():
|
||||
msg.send_webxdc_realtime_data(payload(i) if callable(payload) else payload)
|
||||
if stop.wait(1):
|
||||
return
|
||||
|
||||
for msg_payload in zip(msgs, data, strict=True):
|
||||
threading.Thread(target=thread_run, args=msg_payload, daemon=True).start()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
stop.set()
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
|
||||
def wait_realtime_connected(msg_pairs):
|
||||
with send_realtime_data_forever([sender for sender, _ in msg_pairs]):
|
||||
for _, receiver in msg_pairs:
|
||||
receiver.account.wait_for_realtime_data(receiver.id)
|
||||
def wait_receive_realtime_data(msg_data_list):
|
||||
account = msg_data_list[0][0].account
|
||||
msg_data_list = msg_data_list[:]
|
||||
|
||||
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
|
||||
while msg_data_list:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
for i, (msg, data) in enumerate(msg_data_list):
|
||||
if msg.id == event.msg_id:
|
||||
assert list(data) == event.data
|
||||
log(f"msg {msg.id}: got correct realtime data {data}")
|
||||
del msg_data_list[i]
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection sequentially."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
@@ -130,27 +117,50 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
data = os.urandom(128000)
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||
|
||||
assert ac2.wait_for_realtime_data(ac2_webxdc_msg.id) == data
|
||||
log("ac2: waiting for realtime data")
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert event.data == list(data)
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
|
||||
|
||||
|
||||
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
|
||||
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
|
||||
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg), (ac2_webxdc_msg, ac1_webxdc_msg)])
|
||||
wait_realtime_connected([(ac1_webxdc_msg2, ac2_webxdc_msg2), (ac2_webxdc_msg2, ac1_webxdc_msg2)])
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
|
||||
|
||||
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
|
||||
|
||||
|
||||
def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
"""Test that messages are received only once."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
|
||||
@@ -164,29 +174,50 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
with send_realtime_data_forever([ac1_webxdc_msg], data=[lambda i: str(i).encode()]):
|
||||
n = int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode())
|
||||
assert int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode()) > n
|
||||
def thread_run():
|
||||
for i in range(10):
|
||||
data = str(i).encode()
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||
n = int(bytes(event.data).decode())
|
||||
|
||||
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
|
||||
|
||||
def test_no_reordering(acfactory, path_to_webxdc):
|
||||
"""Test that sending a lot of realtime messages does not result in reordering."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
|
||||
|
||||
for i in range(200):
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data([i])
|
||||
|
||||
for i in range(200):
|
||||
# lingering SETUP_DATA payloads from the wait_realtime_connected() barrier may still arrive
|
||||
while (data := ac2.wait_for_realtime_data(ac2_webxdc_msg.id)) == SETUP_DATA:
|
||||
pass
|
||||
assert data == bytes([i]), "Reordering detected"
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
|
||||
if event.data[0] == i:
|
||||
break
|
||||
pytest.fail("Reordering detected")
|
||||
|
||||
|
||||
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
"""Test that realtime advertisement is assigned to the correct message after chatting."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
@@ -213,6 +244,9 @@ def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
|
||||
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)
|
||||
|
||||
@@ -37,14 +37,19 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
log.section("send out message without bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "0")
|
||||
chat = ac1.create_chat(ac2)
|
||||
self_addr = ac1.get_config("addr")
|
||||
other_addr = ac2.get_config("addr")
|
||||
|
||||
msg_out = chat.send_text("message1")
|
||||
assert not msg_out.get_snapshot().is_forwarded
|
||||
|
||||
# wait for send out (no BCC)
|
||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
assert self_addr not in ev.msg
|
||||
assert other_addr in ev.msg
|
||||
|
||||
log.section("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
@@ -52,16 +57,20 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
# wait for send out (BCC)
|
||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only the second message, but not the first.
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.msg
|
||||
assert self_addr in ev.msg
|
||||
|
||||
# BCC-self messages are marked as seen by the sender device.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
@@ -127,34 +136,3 @@ def test_multidevice_sync_seen(acfactory, log):
|
||||
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
|
||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||
assert "Expires: " in ac1_clone_message.get_info()
|
||||
|
||||
|
||||
def test_multidevice_sync_seen_mdns_off(acfactory, log):
|
||||
"""Test that MDNs to self are sent even if MDNs are disabled."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("mdns_enabled", "0")
|
||||
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
assert ac1.get_config("mdns_enabled") == "0"
|
||||
assert ac1_clone.get_config("bcc_self") == "1"
|
||||
assert ac1_clone.get_config("mdns_enabled") == "0"
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
|
||||
log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||
ac2_chat.send_text("Hi")
|
||||
ac1_message = ac1.wait_for_incoming_msg()
|
||||
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
|
||||
|
||||
ac1_message.mark_seen()
|
||||
assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
log.section("ac1 clone detects that message is marked as seen")
|
||||
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
|
||||
assert ev.chat_id == ac1_clone_chat.id
|
||||
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
@@ -76,7 +76,7 @@ def test_change_address(acfactory) -> None:
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory, data) -> None:
|
||||
def test_download_on_demand(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_download_on_demand(acfactory, data) -> None:
|
||||
|
||||
alice.create_chat(bob)
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
chat_bob_alice.send_message(file=data.get_path("image/screenshot.jpg"))
|
||||
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
snapshot = msg.get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
@@ -150,9 +150,12 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
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") == transport1["addr"]
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
log.section("ac1 removes the first transport")
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
@@ -171,7 +174,7 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
|
||||
|
||||
def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
"""Test that a transport promoted on one device is usable on other devices."""
|
||||
"""Test synchronization of new transport as primary between devices."""
|
||||
ac1, bob = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
@@ -190,9 +193,9 @@ def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new transport")
|
||||
log.section("ac1_clone receives a message via the new primary transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
ac1_chat.send_text("Hello!")
|
||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||
@@ -234,21 +237,10 @@ def test_transport_limit(acfactory) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
third_addr = account.list_transports()[2]["addr"]
|
||||
account.delete_transport(second_addr)
|
||||
|
||||
# test that adding a transport after unpublishing one works again
|
||||
account.set_transport_unpublished(second_addr)
|
||||
# test that adding a transport after deleting one works again
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
# UIs are not expected to delete transports directly,
|
||||
# but we still test that adding a transport
|
||||
# after deleting one instead of unpublishing works.
|
||||
account.delete_transport(third_addr)
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory) -> None:
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_qr_securejoin(acfactory):
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
@@ -487,7 +487,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
def test_qr_new_group_unblocked(acfactory):
|
||||
"""Regression test for a bug introduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked single chat with ac1 on ac2,
|
||||
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
ac2 should receive a message and create a contact request for the group.
|
||||
@@ -623,7 +623,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
|
||||
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
@@ -682,7 +682,7 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||
@@ -704,25 +704,3 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
||||
):
|
||||
break
|
||||
|
||||
|
||||
def test_qr_scan_updates_new_relay_address(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_alice_chat = bob.secure_join(alice.get_qr_code())
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
for ac in [alice, bob]:
|
||||
old_addr = ac.get_config("configured_addr")
|
||||
ac.add_transport_from_qr(acfactory.get_account_qr())
|
||||
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
|
||||
ac.delete_transport(old_addr)
|
||||
|
||||
bob.secure_join(alice.get_qr_code())
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
bob_alice_chat.send_text("hi")
|
||||
snapshot = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "hi"
|
||||
|
||||
@@ -221,30 +221,6 @@ def test_account(acfactory) -> None:
|
||||
alice.stop_io()
|
||||
|
||||
|
||||
def test_mark_fresh_vs_self_mdn(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
bob.set_config("bcc_self", "1")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_chat = alice_contact_bob.create_chat()
|
||||
alice_chat.send_text("Hello!")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
|
||||
bob_chat = bob.get_chat_by_id(chat_id)
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
bob_chat.accept()
|
||||
bob.mark_seen_messages([message])
|
||||
bob_chat.mark_fresh()
|
||||
assert bob_chat.get_fresh_message_count() == 1
|
||||
alice.wait_for_event(EventType.MSG_READ)
|
||||
alice_chat.send_text("You've read 'Hello!'")
|
||||
bob.wait_for_incoming_msg_event()
|
||||
assert bob_chat.get_fresh_message_count() == 2
|
||||
|
||||
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -275,7 +251,7 @@ def test_chat(acfactory) -> None:
|
||||
bob_chat_alice.unpin()
|
||||
bob_chat_alice.archive()
|
||||
bob_chat_alice.unarchive()
|
||||
with pytest.raises(JsonRpcError): # can't set name for single chats
|
||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||
bob_chat_alice.set_name("test")
|
||||
bob_chat_alice.set_ephemeral_timer(300)
|
||||
bob_chat_alice.get_encryption_info()
|
||||
@@ -380,7 +356,7 @@ def test_receive_imf_failure(acfactory) -> None:
|
||||
snapshot.text == "❌ Failed to receive a message:"
|
||||
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
|
||||
f" Core version {version}."
|
||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/"
|
||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
|
||||
)
|
||||
|
||||
# The failed message doesn't break the IMAP loop.
|
||||
@@ -474,35 +450,6 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
assert chat_id == alice2_chat_bob.id
|
||||
|
||||
|
||||
def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
# Get an accepted chat, otherwise alice2 won't be notified about the 2nd message.
|
||||
chat_alice2 = alice2.create_chat(bob)
|
||||
chat_id_alice2 = chat_alice2.get_basic_snapshot().id
|
||||
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
chat_bob_alice.send_text("Hello!")
|
||||
msg_alice = alice.wait_for_incoming_msg()
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_bob_alice.send_text("What's new?")
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_alice2 = alice2.get_chat_by_id(chat_id_alice2)
|
||||
assert chat_alice2.get_fresh_message_count() == 2
|
||||
|
||||
msg_alice.mark_seen()
|
||||
assert alice2.wait_for_msgs_changed_event().chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 1
|
||||
|
||||
msg_id = alice.wait_for_msgs_changed_event().msg_id
|
||||
msg = alice.get_message_by_id(msg_id)
|
||||
msg.mark_seen()
|
||||
assert alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 0
|
||||
|
||||
|
||||
def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@@ -747,6 +694,28 @@ def test_early_failure(tmp_path) -> None:
|
||||
rpc.start()
|
||||
|
||||
|
||||
def test_provider_info(rpc) -> None:
|
||||
account_id = rpc.add_account()
|
||||
|
||||
provider_info = rpc.get_provider_info(account_id, "example.org")
|
||||
assert provider_info["id"] == "example.com"
|
||||
|
||||
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
|
||||
assert provider_info is None
|
||||
|
||||
# Test MX record resolution.
|
||||
# This previously resulted in Gmail provider
|
||||
# because MX record pointed to google.com domain,
|
||||
# but MX record resolution has been removed.
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
|
||||
# Disable MX record resolution.
|
||||
rpc.set_config(account_id, "proxy_enabled", "1")
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
|
||||
|
||||
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1003,11 +972,6 @@ def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||
|
||||
message.mark_seen()
|
||||
|
||||
# The MDN is queued in `smtp_mdns`, which is drained only after the regular
|
||||
# `smtp` queue, so "Outgoing message" would otherwise overtake it on the wire.
|
||||
# Wait for the read receipt to reach Alice before queueing "Outgoing message".
|
||||
alice.wait_for_event(EventType.MSG_READ)
|
||||
|
||||
# Send a message and wait until it arrives
|
||||
# in order to wait until Bob2 gets the markseen message.
|
||||
# This also tests that outgoing messages
|
||||
@@ -1135,7 +1099,6 @@ def test_rename_group(acfactory):
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
|
||||
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||
time.sleep(1)
|
||||
alice_group.set_name(name)
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
@@ -1412,7 +1375,7 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
|
||||
assert msg.get_snapshot().chat.num_contacts() == 2
|
||||
|
||||
|
||||
def test_large_message(acfactory, data) -> None:
|
||||
def test_large_message(acfactory) -> None:
|
||||
"""
|
||||
Test sending large message without download limit set,
|
||||
so it is sent with pre-message but downloaded without user interaction.
|
||||
@@ -1422,7 +1385,7 @@ def test_large_message(acfactory, data) -> None:
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
file=data.get_path("image/screenshot.jpg"),
|
||||
file="../test-data/image/screenshot.jpg",
|
||||
)
|
||||
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Test that docs/schema.sql matches the actual database schema."""
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
DOC_PATH = Path(__file__).resolve().parents[2] / "docs" / "schema.sql"
|
||||
|
||||
|
||||
def strip_comments(sql):
|
||||
return re.sub(r"--[^\n]*", "", sql)
|
||||
|
||||
|
||||
def normalize(stmt):
|
||||
stmt = re.sub(r"\s+", " ", stmt).strip()
|
||||
stmt = stmt.replace("CREATE TABLE IF NOT EXISTS ", "CREATE TABLE ")
|
||||
return re.sub(r'"(\w+)"', r"\1", stmt)
|
||||
|
||||
|
||||
def split_table(body):
|
||||
items = []
|
||||
depth = 0
|
||||
current = ""
|
||||
for char in body:
|
||||
if char == "(":
|
||||
depth += 1
|
||||
elif char == ")":
|
||||
depth -= 1
|
||||
if char == "," and depth == 0:
|
||||
items.append(current.strip())
|
||||
current = ""
|
||||
else:
|
||||
current += char
|
||||
if current.strip():
|
||||
items.append(current.strip())
|
||||
return items
|
||||
|
||||
|
||||
def parse_schema(sql):
|
||||
objects = {}
|
||||
for raw_stmt in strip_comments(sql).split(";"):
|
||||
stmt = normalize(raw_stmt)
|
||||
if not stmt:
|
||||
continue
|
||||
match = re.match(r"CREATE TABLE (\w+) ?\((.*)\)( STRICT)?$", stmt)
|
||||
if match:
|
||||
name, body, strict = match.groups()
|
||||
objects[f"table {name}"] = {
|
||||
"items": sorted(split_table(body)),
|
||||
"strict": bool(strict),
|
||||
}
|
||||
continue
|
||||
match = re.match(r"CREATE (?:UNIQUE )?INDEX (\w+)", stmt)
|
||||
if match:
|
||||
objects[f"index {match.group(1)}"] = {"sql": stmt}
|
||||
continue
|
||||
objects[stmt[:60]] = {"sql": stmt}
|
||||
return objects
|
||||
|
||||
|
||||
def format_diff(documented, real):
|
||||
if "items" in documented and "items" in real:
|
||||
lines = []
|
||||
# disregards order
|
||||
for item in sorted(set(documented["items"]) - set(real["items"])):
|
||||
lines.append(f" documented but not in the database: {item}")
|
||||
for item in sorted(set(real["items"]) - set(documented["items"])):
|
||||
lines.append(f" in the database but not documented: {item}")
|
||||
if documented["strict"] != real["strict"]:
|
||||
lines.append(f" STRICT: documented={documented['strict']} actual={real['strict']}")
|
||||
return "\n".join(lines)
|
||||
return f" documented: {documented}\n actual: {real}"
|
||||
|
||||
|
||||
def read_database_schema(dbfile):
|
||||
with sqlite3.connect(f"file:{dbfile}?mode=ro", uri=True) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'",
|
||||
).fetchall()
|
||||
return ";\n".join(row[0] for row in rows)
|
||||
|
||||
|
||||
def test_documented_schema_matches_database(acfactory):
|
||||
account = acfactory.get_unconfigured_account()
|
||||
real = parse_schema(read_database_schema(account.get_info()["database_dir"]))
|
||||
documented = parse_schema(DOC_PATH.read_text())
|
||||
|
||||
problems = []
|
||||
for name in sorted(real.keys() - documented.keys()):
|
||||
problems.append(f"{name} exists in the database but is not documented")
|
||||
for name in sorted(documented.keys() - real.keys()):
|
||||
problems.append(f"{name} is documented but does not exist in the database")
|
||||
for name in sorted(documented.keys() & real.keys()):
|
||||
if documented[name] != real[name]:
|
||||
problems.append(f"{name} differs:\n{format_diff(documented[name], real[name])}")
|
||||
assert not problems, "documented schema deviates from the database:\n" + "\n".join(problems)
|
||||
@@ -1,9 +1,9 @@
|
||||
def test_webxdc(acfactory, data) -> None:
|
||||
def test_webxdc(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
@@ -43,12 +43,12 @@ def test_webxdc(acfactory, data) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_webxdc_insert_lots_of_updates(acfactory, data) -> None:
|
||||
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
message = alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
|
||||
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
for i in range(2000):
|
||||
message.send_webxdc_status_update({"payload": str(i)}, "description")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.59.0"
|
||||
version = "2.51.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.59.0"
|
||||
"version": "2.51.0-dev"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "deltachat-time"
|
||||
version = "1.0.0"
|
||||
description = "Time-related tools"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
edition = "2024"
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "3"
|
||||
syn = "2"
|
||||
quote = "1"
|
||||
|
||||
@@ -14,7 +14,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let q = quote! {
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::ToSql for #name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let num = *self as i64;
|
||||
@@ -24,7 +24,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
};
|
||||
q.into()
|
||||
gen.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(FromSql)]
|
||||
@@ -32,7 +32,7 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let q = quote! {
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::FromSql for #name {
|
||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||
@@ -44,5 +44,5 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
};
|
||||
q.into()
|
||||
gen.into()
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@ ignore = [
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134",
|
||||
|
||||
# Unmaintained proc-macro-error2
|
||||
# Transitive dependency of typescript-type-def 0.5.13.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0173>
|
||||
"RUSTSEC-2026-0173",
|
||||
|
||||
# rustls-webpki v0.102.8
|
||||
# We cannot upgrade to >=0.103.10 because
|
||||
# it is a transitive dependency of iroh 0.35.0
|
||||
@@ -70,7 +65,6 @@ skip = [
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "getrandom", version = "0.3.3" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hybrid-array", version = "0.2.3" },
|
||||
@@ -81,18 +75,14 @@ skip = [
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "r-efi", version = "5.2.0" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "rustls-webpki", version = "0.102.8" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "snafu-derive", version = "0.8.5" },
|
||||
{ name = "snafu", version = "0.8.5" },
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "syn", version = "2.0.118" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_datetime", version = "0.6.11" },
|
||||
|
||||
-783
@@ -1,783 +0,0 @@
|
||||
-- Commented SQLite database schema.
|
||||
--
|
||||
-- This file should only be used for documentation,
|
||||
-- do not run this SQL e.g. to create databases.
|
||||
-- This is because we want to be 100% sure
|
||||
-- that new users and users who run the migrations
|
||||
-- get the same database schema.
|
||||
--
|
||||
-- This is a dump of the database schema using `sqlite3 dc.db .schema`,
|
||||
-- formatted, reordered and commented afterwards.
|
||||
-- Raw dump of the database schema does not have comments
|
||||
-- for deprecated columns and columns added by migrations.
|
||||
|
||||
CREATE TABLE config (
|
||||
id INTEGER PRIMARY KEY,
|
||||
keyname TEXT UNIQUE,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX config_index1 ON config (keyname);
|
||||
|
||||
CREATE TABLE contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- Name of the contact as set by the user.
|
||||
name TEXT DEFAULT '',
|
||||
|
||||
-- Normalized name for search.
|
||||
name_normalized TEXT,
|
||||
|
||||
-- Email address last seen as the From address for this contact.
|
||||
-- For address-contacts that have empty "fingerprint" this should never change.
|
||||
-- For key-contacts this address may change when a new signed message is received.
|
||||
--
|
||||
-- This is the address messages are sent to unless the key
|
||||
-- advertises a different set of addresses, in which case
|
||||
-- this address should be ignored
|
||||
-- (and not appended to the list of addresses advertised in the key).
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
|
||||
-- The origin or source of the contact,
|
||||
-- e.g. whether the contact was added from some chat
|
||||
-- or via SecureJoin.
|
||||
origin INTEGER DEFAULT 0,
|
||||
|
||||
-- True if the contact is blocked.
|
||||
-- Unlike the chat, contact is either blocked or not,
|
||||
-- there is no third "contact request" state.
|
||||
blocked INTEGER DEFAULT 0,
|
||||
|
||||
-- Timestamp of the last time any message was received from this contact.
|
||||
last_seen INTEGER DEFAULT 0,
|
||||
|
||||
-- Key-value parameters.
|
||||
param TEXT DEFAULT '',
|
||||
|
||||
-- Name of the contact as sent by the contact itself.
|
||||
authname TEXT DEFAULT '',
|
||||
|
||||
-- Timestamp of the last time we have sent our avatar to this contact.
|
||||
-- Used to decide whether to send the avatar.
|
||||
-- Normally avatars are resent after 14 days.
|
||||
-- This column is reset to 0 when avatar is changed.
|
||||
selfavatar_sent INTEGER DEFAULT 0,
|
||||
|
||||
-- Last seen message signature from this contact, also known as bio in the UI.
|
||||
status TEXT DEFAULT '',
|
||||
|
||||
-- True if the contact is a bot.
|
||||
is_bot INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- OpenPGP key fingerprint for "key-contacts",
|
||||
-- empty string for "address-contacts".
|
||||
fingerprint TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- ID of the contact that has "introduced" us to this contact
|
||||
-- by sharing the key with a verified attribute or in a "verified" chat.
|
||||
verifier INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
|
||||
CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);
|
||||
CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint);
|
||||
|
||||
CREATE TABLE chats (
|
||||
-- Chat ID 0 should never be used as it is used as a sentinel value in some APIs.
|
||||
--
|
||||
-- Chat IDs 1 to 9, including 9, are reserved.
|
||||
-- The first proper chat gets ID 10, but may not exist if it is deleted.
|
||||
--
|
||||
-- Chat ID 3 is the trash chat and this chat ID is assigned to deleted messages
|
||||
-- to create "tombstones".
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- Chat type, e.g. 100 for single chat, 120 for a group etc.
|
||||
-- Check the Chattype enumeration in the code for concrete values.
|
||||
type INTEGER DEFAULT 0,
|
||||
|
||||
name TEXT DEFAULT '',
|
||||
|
||||
-- Normalized name for search.
|
||||
name_normalized TEXT,
|
||||
|
||||
-- 0 for visible chat, 1 for hidden and 2 for contact request.
|
||||
-- 1 does not necessarily mean that the contact is blocked.
|
||||
blocked INTEGER DEFAULT 0,
|
||||
|
||||
-- Chat-Group-ID header for encrypted groups and channels.
|
||||
-- Empty for unencrypted groups and single chats.
|
||||
grpid TEXT DEFAULT '',
|
||||
|
||||
-- Key-value parameters.
|
||||
param TEXT DEFAULT '',
|
||||
|
||||
-- Chat visibility.
|
||||
-- 0 for normal, 1 for archived and 2 for pinned chats.
|
||||
archived INTEGER DEFAULT 0,
|
||||
|
||||
locations_send_begin INTEGER DEFAULT 0,
|
||||
locations_send_until INTEGER DEFAULT 0,
|
||||
locations_last_sent INTEGER DEFAULT 0,
|
||||
|
||||
-- Time when the chat was created.
|
||||
-- Used for sorting in the chatlist when the chat has no messages.
|
||||
created_timestamp INTEGER DEFAULT 0,
|
||||
|
||||
-- 0 if the chat is not muted.
|
||||
-- -1 if the chat is muted forever.
|
||||
-- Otherwise the timestamp until which the chat is muted.
|
||||
muted_until INTEGER DEFAULT 0,
|
||||
|
||||
-- Disappearing messages timer.
|
||||
-- 0 means the timer is disabled.
|
||||
ephemeral_timer INTEGER,
|
||||
|
||||
-- Deprecated, but still used to send Chat-Verified headers
|
||||
-- for existing protected chats.
|
||||
-- All new chats are created as "not protected".
|
||||
protected INTEGER DEFAULT 0,
|
||||
|
||||
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table
|
||||
|
||||
-- Unused columns, drafts are now tracked as separate
|
||||
-- messages with a special msgs.state value.
|
||||
draft_timestamp INTEGER DEFAULT 0,
|
||||
draft_txt TEXT DEFAULT ''
|
||||
);
|
||||
CREATE INDEX chats_index1 ON chats (grpid);
|
||||
CREATE INDEX chats_index2 ON chats (archived);
|
||||
CREATE INDEX chats_index3 ON chats (locations_send_until);
|
||||
CREATE INDEX chats_index4 ON chats (name);
|
||||
|
||||
CREATE TABLE chats_descriptions (
|
||||
chat_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
description TEXT NOT NULL DEFAULT ''
|
||||
) STRICT;
|
||||
|
||||
-- Chat member lists.
|
||||
-- Saved messages has only self.
|
||||
-- Single chats have only the contact, but not self.
|
||||
-- Groups have self if we are part of the group.
|
||||
CREATE TABLE chats_contacts (
|
||||
chat_id INTEGER,
|
||||
contact_id INTEGER,
|
||||
add_timestamp NOT NULL DEFAULT 0,
|
||||
remove_timestamp NOT NULL DEFAULT 0,
|
||||
UNIQUE(chat_id, contact_id)
|
||||
);
|
||||
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
|
||||
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
|
||||
|
||||
-- This table contains "message bubbles" that are normally visible
|
||||
-- and "tombstones" that are put into the trash chat
|
||||
-- or have a "hidden" column set to the true value.
|
||||
--
|
||||
-- Tombstones are used to avoid downloading and processing
|
||||
-- the same messages twice, e.g. when the message is deleted
|
||||
-- but another copy of it arrives later.
|
||||
CREATE TABLE msgs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- The messages may be split into pre- and post-message.
|
||||
-- Pre-messages have a Chat-Post-Message-ID header
|
||||
-- which contains the Message-ID of the post-message.
|
||||
-- Post-messages have a Chat-Is-Post-Message header.
|
||||
--
|
||||
-- For outgoing messages that are split into pre-message
|
||||
-- and post-message, rfc724_mid contains the Message-ID
|
||||
-- of the post-message, and pre_rfc724_mid
|
||||
-- contains the Message-ID of the pre-message.
|
||||
--
|
||||
-- For incoming messages, when a pre-message arrives,
|
||||
-- rfc724_mid is set to the Message-ID of the post-message,
|
||||
-- and pre_rfc724_mid is set to the Message-ID of the arrived pre-messsage.
|
||||
-- For other messages rfc724_mid is taken from the Message-ID
|
||||
-- and pre_rfc724_mid is set to empty string.
|
||||
|
||||
-- Message-ID as defined in RFC 724 (now replaced by RFC 5322)
|
||||
rfc724_mid TEXT DEFAULT '',
|
||||
-- Message-ID of the pre-message.
|
||||
pre_rfc724_mid TEXT DEFAULT '',
|
||||
|
||||
-- Chat ID.
|
||||
--
|
||||
-- Chat ID 3 is the trash chat, messages with this ID are tombstones.
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
|
||||
-- Contact ID of the message author.
|
||||
from_id INTEGER DEFAULT 0,
|
||||
|
||||
-- Mostly unused Contact ID of the first recipient or 0.
|
||||
-- For info messages set to ContactID::INFO (2).
|
||||
--
|
||||
-- If to_id is set to 2, the message must be displayed
|
||||
-- as an info message even if from_id is not set to 2.
|
||||
--
|
||||
-- Info messages generated by Webxdc status updates
|
||||
-- have from_id set to the contact ID of the sender
|
||||
-- and to_id set to 2.
|
||||
-- from_id is then used to collapse series of updates
|
||||
-- into a single message (see test_webxdc_info_msg_cleanup_series),
|
||||
-- while to_id of 2 makes sure the message is displayed
|
||||
-- as an info message.
|
||||
to_id INTEGER DEFAULT 0,
|
||||
|
||||
-- Message viewtype.
|
||||
-- 10 is a text message,
|
||||
-- 20 is an image etc.
|
||||
type INTEGER DEFAULT 0,
|
||||
|
||||
-- Message state,
|
||||
-- e.g. 10 for fresh incoming messages,
|
||||
-- 26 for outgoing delivered message,
|
||||
-- 28 for outgoing message that got a read receipt.
|
||||
--
|
||||
-- Messages sent by self, but arriving from a second device
|
||||
-- are still considered "outgoing".
|
||||
state INTEGER DEFAULT 0,
|
||||
|
||||
-- Size of the attachment for file parts, otherwise 0.
|
||||
bytes INTEGER DEFAULT 0,
|
||||
|
||||
txt TEXT DEFAULT '', -- Message text for display.
|
||||
txt_normalized TEXT, -- Message text normalized for search.
|
||||
txt_raw TEXT DEFAULT '', -- deprecated 2025-03-29
|
||||
|
||||
-- Key-value parameters.
|
||||
param TEXT DEFAULT '',
|
||||
|
||||
-- For messages bookmarked into the Saved Messages chat,
|
||||
-- ID of the original message, making it possible to jump to it.
|
||||
starred INTEGER DEFAULT 0,
|
||||
|
||||
-- True if the message is pinned.
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
timestamp INTEGER DEFAULT 0, -- Timestamp of the message used for sorting.
|
||||
timestamp_sent INTEGER DEFAULT 0, -- Timestamp of the message as sent in the Date header.
|
||||
timestamp_rcvd INTEGER DEFAULT 0,
|
||||
|
||||
-- True if the message should not be displayed.
|
||||
-- Most messages that should not be displayed
|
||||
-- better go to the trash chat (ID 3),
|
||||
-- but at least reactions are hidden
|
||||
-- so they still belong to the chat
|
||||
-- and can be marked as noticed when the chat is opened.
|
||||
hidden INTEGER DEFAULT 0,
|
||||
|
||||
-- mime_headers column actually contains BLOBs, i.e. it may
|
||||
-- contain non-UTF8 MIME messages. TEXT was a bad choice, but
|
||||
-- thanks to SQLite 3 being dynamically typed, there is no need to
|
||||
-- change column type.
|
||||
mime_headers TEXT,
|
||||
|
||||
-- True if mime_headers column is compressed with Brotli.
|
||||
mime_compressed INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
mime_modified INTEGER DEFAULT 0,
|
||||
|
||||
mime_in_reply_to TEXT,
|
||||
mime_references TEXT,
|
||||
|
||||
-- Location ID for POI locations manually placed on the map.
|
||||
location_id INTEGER DEFAULT 0,
|
||||
|
||||
error TEXT DEFAULT '',
|
||||
|
||||
-- Timer value in seconds. For incoming messages this
|
||||
-- timer starts when message is read, so we want to have
|
||||
-- the value stored here until the timer starts.
|
||||
ephemeral_timer INTEGER DEFAULT 0,
|
||||
|
||||
-- Timestamp indicating when the message should be
|
||||
-- deleted. It is convenient to store it here because UI
|
||||
-- needs this value to display how much time is left until
|
||||
-- the message is deleted.
|
||||
ephemeral_timestamp INTEGER DEFAULT 0,
|
||||
|
||||
subject TEXT DEFAULT '',
|
||||
|
||||
-- Download state for the message.
|
||||
-- 0 for most messages, meaning the message is fully downloaded.
|
||||
-- Otherwise 10 for the message available for download etc.
|
||||
download_state INTEGER DEFAULT 0,
|
||||
|
||||
-- Information extracted from Received headers
|
||||
-- as a plain text for debugging.
|
||||
hop_info TEXT,
|
||||
|
||||
-- True if the message is deleted on the server.
|
||||
-- If this is true and another copy of the message arrives
|
||||
-- it should be deleted from the server as well.
|
||||
deleted INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
--
|
||||
-- Unused columns.
|
||||
--
|
||||
|
||||
-- Always 1 for new messages.
|
||||
-- Previously:
|
||||
-- 0 if the message is a non-chat message,
|
||||
-- 1 if the message is a chat message,
|
||||
-- 2 if the message is a non-chat reply to a chat message
|
||||
msgrmsg INTEGER DEFAULT 1,
|
||||
|
||||
server_folder TEXT DEFAULT '', -- Deprecated column that was used before "imap" table, replaced by imap.folder
|
||||
server_uid INTEGER DEFAULT 0, -- Deprecated column that was used before "imap" table, replaced by imap.uid
|
||||
|
||||
-- Unused column formely used to mark the messages
|
||||
-- that should be moved to the dedicated IMAP folder.
|
||||
-- It was replaced with imap.target, which is also now
|
||||
-- used only to mark the messages on IMAP for deletion.
|
||||
move_state INTEGER DEFAULT 1
|
||||
);
|
||||
CREATE INDEX msgs_index1 ON msgs (rfc724_mid);
|
||||
CREATE INDEX msgs_index2 ON msgs (chat_id);
|
||||
CREATE INDEX msgs_index3 ON msgs (timestamp);
|
||||
CREATE INDEX msgs_index4 ON msgs (state);
|
||||
CREATE INDEX msgs_index5 ON msgs (starred);
|
||||
CREATE INDEX msgs_index6 ON msgs (location_id);
|
||||
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id, timestamp);
|
||||
CREATE INDEX msgs_index8 ON msgs (ephemeral_timestamp);
|
||||
CREATE INDEX msgs_index9 ON msgs (pre_rfc724_mid);
|
||||
CREATE INDEX msgs_index10 ON msgs (pinned) WHERE pinned=1;
|
||||
|
||||
CREATE TABLE leftgrps (
|
||||
id INTEGER PRIMARY KEY,
|
||||
grpid TEXT DEFAULT ''
|
||||
);
|
||||
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
|
||||
|
||||
CREATE TABLE msgs_mdns (
|
||||
msg_id INTEGER,
|
||||
contact_id INTEGER,
|
||||
timestamp_sent INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
|
||||
|
||||
CREATE TABLE locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
latitude REAL DEFAULT 0.0,
|
||||
longitude REAL DEFAULT 0.0,
|
||||
accuracy REAL DEFAULT 0.0,
|
||||
timestamp INTEGER DEFAULT 0,
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
from_id INTEGER DEFAULT 0,
|
||||
|
||||
-- If true, the location is an independent POI
|
||||
-- and should not be part of the path.
|
||||
independent INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX locations_index1 ON locations (from_id);
|
||||
CREATE INDEX locations_index2 ON locations (timestamp);
|
||||
|
||||
CREATE TABLE devmsglabels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT,
|
||||
msg_id INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
|
||||
|
||||
-- Table to store QR code tokens for SecureJoin protocol.
|
||||
CREATE TABLE tokens (
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
||||
-- Namespace is one of:
|
||||
-- 0 - unknown
|
||||
-- 100 - invite number
|
||||
-- 110 - auth
|
||||
namespc INTEGER NOT NULL,
|
||||
|
||||
foreign_key TEXT DEFAULT '' NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
timestamp INTEGER DEFAULT 0 NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- State of the scanner of the QR code (Bob) in SecureJoin protocol.
|
||||
CREATE TABLE bobstate (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
invite TEXT NOT NULL,
|
||||
next_step INTEGER NOT NULL,
|
||||
chat_id INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE smtp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
mime TEXT NOT NULL, -- SMTP payload
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
|
||||
recipients TEXT NOT NULL, -- List of recipients separated by space
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
);
|
||||
|
||||
CREATE TABLE smtp_mdns (
|
||||
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
|
||||
from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID header
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN
|
||||
);
|
||||
CREATE TABLE smtp_status_updates (
|
||||
msg_id INTEGER NOT NULL UNIQUE, -- msg_id of the webxdc instance with pending updates
|
||||
first_serial INTEGER NOT NULL, -- id in msgs_status_updates
|
||||
last_serial INTEGER NOT NULL, -- id in msgs_status_updates
|
||||
descr TEXT NOT NULL -- text to send along with the updates
|
||||
);
|
||||
|
||||
-- Table of "sync items" to be grouped into sync messages
|
||||
-- and sent to own devices.
|
||||
CREATE TABLE multi_device_sync (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- JSON of the "sync item".
|
||||
item TEXT DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE reactions (
|
||||
msg_id INTEGER NOT NULL, -- id of the message reacted to
|
||||
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
|
||||
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
|
||||
PRIMARY KEY(msg_id, contact_id),
|
||||
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
|
||||
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
|
||||
);
|
||||
CREATE INDEX reactions_index1 ON reactions (msg_id);
|
||||
|
||||
CREATE TABLE pending_reactions (
|
||||
rfc724_mid TEXT NOT NULL,
|
||||
contact_id INTEGER NOT NULL,
|
||||
reaction TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
PRIMARY KEY(rfc724_mid, contact_id),
|
||||
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
|
||||
-- accumulated reactions received from broadcast owner.
|
||||
-- pairs of reaction and their counts.
|
||||
-- these pairs are given to the UI (for non-broadcasts, the pairs are calculated from the `reactions` table)
|
||||
CREATE TABLE broadcasted_reactions (
|
||||
msg_id INTEGER NOT NULL DEFAULT 0,
|
||||
reaction TEXT NOT NULL DEFAULT '',
|
||||
count INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
|
||||
) STRICT;
|
||||
CREATE INDEX broadcasted_reactions_index1 ON broadcasted_reactions (msg_id);
|
||||
|
||||
-- messages that received reactions from broadcast subscriber to broadcast owner.
|
||||
-- the broadcast owner will send them, accumulated by chat_id,
|
||||
-- to all other subscribers every some minutes, and then remove all entries with the chat_id processed.
|
||||
CREATE TABLE reactions_need_broadcast (
|
||||
chat_id INTEGER NOT NULL DEFAULT 0,
|
||||
msg_id INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (chat_id, msg_id),
|
||||
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
|
||||
) STRICT;
|
||||
CREATE INDEX reactions_need_broadcast_index1 ON reactions_need_broadcast (chat_id);
|
||||
|
||||
|
||||
CREATE TABLE connection_history (
|
||||
host TEXT NOT NULL, -- server hostname
|
||||
port INTEGER NOT NULL, -- server port
|
||||
alpn TEXT NOT NULL, -- ALPN such as smtp or imap
|
||||
addr TEXT NOT NULL, -- IP address
|
||||
timestamp INTEGER NOT NULL, -- timestamp of the most recent successful connection
|
||||
UNIQUE (host, port, alpn, addr)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE dns_cache (
|
||||
hostname TEXT NOT NULL,
|
||||
address TEXT NOT NULL, -- IPv4 or IPv6 address
|
||||
timestamp INTEGER NOT NULL,
|
||||
UNIQUE (hostname, address)
|
||||
);
|
||||
|
||||
CREATE TABLE tls_spki (
|
||||
host TEXT NOT NULL UNIQUE,
|
||||
spki_hash TEXT NOT NULL, -- base64 of SPKI SHA-256 hash
|
||||
timestamp INTEGER NOT NULL -- timestamp of the last time we have seen this key
|
||||
) STRICT;
|
||||
CREATE INDEX tls_spki_index_timestamp ON tls_spki (timestamp);
|
||||
|
||||
CREATE TABLE http_cache (
|
||||
url TEXT PRIMARY KEY,
|
||||
expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
|
||||
stale INTEGER NOT NULL, -- When the cache entry is considered stale, timestamp in seconds.
|
||||
blobname TEXT NOT NULL,
|
||||
mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
|
||||
encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
|
||||
) STRICT;
|
||||
|
||||
|
||||
|
||||
|
||||
-- Webxdc updates.
|
||||
CREATE TABLE msgs_status_updates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
msg_id INTEGER,
|
||||
update_item TEXT DEFAULT '',
|
||||
uid TEXT UNIQUE,
|
||||
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
|
||||
CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
|
||||
-- Webxdc realtime.
|
||||
CREATE TABLE iroh_gossip_peers (
|
||||
msg_id INTEGER not NULL,
|
||||
topic BLOB NOT NULL,
|
||||
public_key BLOB NOT NULL,
|
||||
relay_server TEXT, UNIQUE (topic, public_key),
|
||||
PRIMARY KEY(topic, public_key)
|
||||
) STRICT;
|
||||
|
||||
--
|
||||
-- OpenPGP.
|
||||
--
|
||||
|
||||
-- Storage for own private keys.
|
||||
-- Only one of the keys is used.
|
||||
--
|
||||
-- In the past mulitple keys could be imported,
|
||||
-- so this table can contain multiple keys for existing users.
|
||||
-- Used key is identified by the "key_id" config value.
|
||||
-- Other keys must never be used, even for decryption.
|
||||
CREATE TABLE keypairs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- OpenPGP private key stored as a binary blob.
|
||||
private_key UNIQUE NOT NULL,
|
||||
|
||||
-- Unused public key aka OpenPGP certificate.
|
||||
-- Stored only for compatibility.
|
||||
-- OpenPGP certificate is generated at runtime from the private key.
|
||||
public_key UNIQUE NOT NULL,
|
||||
|
||||
--
|
||||
-- Unused columns.
|
||||
--
|
||||
--
|
||||
-- Columns "addr", "is_default" and "created" were
|
||||
-- dropped in migration 107
|
||||
-- but added back for compatibility in migration 110.
|
||||
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
|
||||
-- Migrated into "key_id" config value in migration 107.
|
||||
is_default INTEGER DEFAULT 0,
|
||||
|
||||
created INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE public_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key.
|
||||
public_key BLOB NOT NULL -- Binary key, not ASCII-armored
|
||||
) STRICT;
|
||||
CREATE INDEX public_key_index ON public_keys (fingerprint);
|
||||
|
||||
CREATE TABLE gossip_timestamp (
|
||||
chat_id INTEGER NOT NULL,
|
||||
fingerprint TEXT NOT NULL, -- Upper-case fingerprint of the key.
|
||||
timestamp INTEGER NOT NULL,
|
||||
UNIQUE (chat_id, fingerprint)
|
||||
) STRICT;
|
||||
CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
|
||||
-- Timestamps of distributing own key in the Autocrypt header of MDNs.
|
||||
CREATE TABLE mdn_autocrypt_timestamp (
|
||||
fingerprint TEXT PRIMARY KEY NOT NULL, -- Upper-case fingerprint of the recipient key.
|
||||
attached_timestamp INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- Passwords used to derive symmetric keys for broadcast lists aka channels.
|
||||
CREATE TABLE broadcast_secrets(
|
||||
chat_id INTEGER PRIMARY KEY NOT NULL,
|
||||
secret TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
|
||||
-- Candidate chatmail relays for automatic relay management.
|
||||
CREATE TABLE relay_candidates(
|
||||
host TEXT PRIMARY KEY NOT NULL,
|
||||
last_tried INTEGER NOT NULL DEFAULT 0 -- Timestamp of the last connection attempt.
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE transports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- Email address associated with this transport.
|
||||
-- It uniquely identifies the transport.
|
||||
addr TEXT NOT NULL,
|
||||
|
||||
-- JSON with the settings entered by the user.
|
||||
-- The settings are not necessary entered manually,
|
||||
-- but can be entered by scanning the QR code.
|
||||
-- These settings are only used during the transport configuration process.
|
||||
entered_param TEXT NOT NULL,
|
||||
|
||||
-- JSON with the settings used to connect to the transport.
|
||||
-- These settings are derived from entered parameters
|
||||
-- and possibly autoconfiguration XML fetched over HTTPS.
|
||||
-- The settings stored here are known to have worked at least once,
|
||||
-- this is ensured during configuration.
|
||||
configured_param TEXT NOT NULL,
|
||||
|
||||
add_timestamp INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- True if the transport address is published
|
||||
-- by sending it in the public key signature.
|
||||
is_published INTEGER DEFAULT 1 NOT NULL,
|
||||
|
||||
-- Time when the transport was last used to receive a message.
|
||||
-- Used to remove the least recently used transport
|
||||
-- when a new transport is added and there are too many relays already.
|
||||
last_rcvd_timestamp INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(addr)
|
||||
);
|
||||
|
||||
CREATE TABLE removed_transports (
|
||||
addr TEXT NOT NULL,
|
||||
remove_timestamp INTEGER NOT NULL,
|
||||
UNIQUE(addr)
|
||||
) STRICT;
|
||||
|
||||
|
||||
CREATE TABLE imap (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID header
|
||||
folder TEXT NOT NULL, -- IMAP folder
|
||||
target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
|
||||
uid INTEGER NOT NULL, -- UID
|
||||
uidvalidity INTEGER NOT NULL,
|
||||
UNIQUE (transport_id, folder, uid, uidvalidity)
|
||||
) STRICT;
|
||||
CREATE INDEX imap_folder ON imap(transport_id, folder);
|
||||
CREATE INDEX imap_rfc724_mid ON imap(transport_id, rfc724_mid);
|
||||
CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid);
|
||||
|
||||
CREATE TABLE imap_markseen (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE imap_sync (
|
||||
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
|
||||
folder TEXT NOT NULL,
|
||||
uidvalidity INTEGER NOT NULL DEFAULT 0,
|
||||
uid_next INTEGER NOT NULL DEFAULT 0,
|
||||
modseq INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (transport_id, folder)
|
||||
) STRICT;
|
||||
CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
|
||||
|
||||
CREATE TABLE download (
|
||||
rfc724_mid TEXT PRIMARY KEY,
|
||||
msg_id INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE available_post_msgs (
|
||||
rfc724_mid TEXT PRIMARY KEY
|
||||
) STRICT;
|
||||
|
||||
--
|
||||
-- Statistics.
|
||||
--
|
||||
|
||||
CREATE TABLE stats_securejoin_sources(
|
||||
source INTEGER PRIMARY KEY,
|
||||
count INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
CREATE TABLE stats_securejoin_uipaths(
|
||||
uipath INTEGER PRIMARY KEY,
|
||||
count INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
CREATE TABLE stats_securejoin_invites(
|
||||
already_existed INTEGER NOT NULL,
|
||||
already_verified INTEGER NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE TABLE stats_msgs(
|
||||
chattype INTEGER PRIMARY KEY,
|
||||
verified INTEGER NOT NULL DEFAULT 0,
|
||||
unverified_encrypted INTEGER NOT NULL DEFAULT 0,
|
||||
unencrypted INTEGER NOT NULL DEFAULT 0,
|
||||
only_to_self INTEGER NOT NULL DEFAULT 0,
|
||||
last_counted_msg_id INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
CREATE TABLE stats_sending_enabled_events(timestamp INTEGER NOT NULL) STRICT;
|
||||
CREATE TABLE stats_sending_disabled_events(timestamp INTEGER NOT NULL) STRICT;
|
||||
|
||||
-- Deprecated and unused tables.
|
||||
--
|
||||
-- We don't immediately drop unused tables,
|
||||
-- because we want users to be able to downgrade.
|
||||
|
||||
-- Deprecated table for Autocrypt peer states.
|
||||
-- It is replaced by the new "public_keys" table in migration 132
|
||||
-- that introduced "key contacts" which have a fixed fingerprint.
|
||||
CREATE TABLE acpeerstates (
|
||||
id INTEGER PRIMARY KEY,
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
last_seen INTEGER DEFAULT 0,
|
||||
last_seen_autocrypt INTEGER DEFAULT 0,
|
||||
public_key,
|
||||
prefer_encrypted INTEGER DEFAULT 0,
|
||||
gossip_timestamp INTEGER DEFAULT 0,
|
||||
gossip_key,
|
||||
public_key_fingerprint TEXT DEFAULT '',
|
||||
gossip_key_fingerprint TEXT DEFAULT '',
|
||||
verified_key,
|
||||
verified_key_fingerprint TEXT DEFAULT '',
|
||||
verifier TEXT DEFAULT '',
|
||||
secondary_verified_key,
|
||||
secondary_verified_key_fingerprint TEXT DEFAULT '',
|
||||
secondary_verifier TEXT DEFAULT '',
|
||||
backward_verified_key_id -- What we think the contact has as our verified key
|
||||
INTEGER,
|
||||
UNIQUE (addr) -- Only one peerstate per address
|
||||
);
|
||||
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
|
||||
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
|
||||
|
||||
-- Deprecated table previously used to upload sync messages using IMAP APPEND.
|
||||
-- Sync messages are sent over SMTP now as they should be sent to multiple transports.
|
||||
CREATE TABLE imap_send (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mime TEXT NOT NULL, -- Message content
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in the `msgs` table
|
||||
attempts INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
);
|
||||
|
||||
-- Deprecated table used for a job system.
|
||||
CREATE TABLE jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
added_timestamp INTEGER,
|
||||
desired_timestamp INTEGER DEFAULT 0,
|
||||
action INTEGER,
|
||||
foreign_id INTEGER,
|
||||
param TEXT DEFAULT '',
|
||||
thread INTEGER DEFAULT 0,
|
||||
tries INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
|
||||
|
||||
-- Backup of the keypairs left after migration 107.
|
||||
-- Not used by any code, it was only for a recovery
|
||||
-- in case migration 107 goes wrong.
|
||||
CREATE TABLE old_keypairs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
private_key,
|
||||
public_key,
|
||||
created INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
-- Unused table, previously introduced for AEAP mechanism
|
||||
-- which is replaced by key contacts.
|
||||
CREATE TABLE sending_domains(
|
||||
domain TEXT PRIMARY KEY,
|
||||
dkim_works INTEGER DEFAULT 0
|
||||
);
|
||||
Generated
+4
-3
@@ -94,15 +94,16 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780914171,
|
||||
"narHash": "sha256-NYoa+CvsCgayY356worC9g6QoZkKJmvrj6nVdEA2Lyk=",
|
||||
"lastModified": 1779912356,
|
||||
"narHash": "sha256-yj5O6vmAj+OfhTQMiUwhmQRP0HAII3BxEI6zuY6h/5k=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "41b6e9efb62fac9c16d5617e0456a715928a2206",
|
||||
"rev": "33eaf5c72a67db15073322d26cd342c443556214",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "pull/391/head",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
fenix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
naersk.url = "github:nix-community/naersk/pull/391/head";
|
||||
naersk.inputs.nixpkgs.follows = "nixpkgs";
|
||||
naersk.inputs.fenix.follows = "fenix";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
@@ -66,15 +66,37 @@
|
||||
];
|
||||
};
|
||||
|
||||
# Map from architecture name to nixpkgs targets.
|
||||
# Map from architecture name to rust targets and nixpkgs targets.
|
||||
arch2targets = {
|
||||
"x86_64-linux" = "x86_64-unknown-linux-musl";
|
||||
"armv7l-linux" = "armv7l-unknown-linux-musleabihf";
|
||||
"armv6l-linux" = "armv6l-unknown-linux-musleabihf";
|
||||
"aarch64-linux" = "aarch64-unknown-linux-musl";
|
||||
"i686-linux" = "i686-unknown-linux-musl";
|
||||
"x86_64-darwin" = "x86_64-darwin";
|
||||
"aarch64-darwin" = "aarch64-darwin";
|
||||
"x86_64-linux" = {
|
||||
rustTarget = "x86_64-unknown-linux-musl";
|
||||
crossTarget = "x86_64-unknown-linux-musl";
|
||||
};
|
||||
"armv7l-linux" = {
|
||||
rustTarget = "armv7-unknown-linux-musleabihf";
|
||||
crossTarget = "armv7l-unknown-linux-musleabihf";
|
||||
};
|
||||
"armv6l-linux" = {
|
||||
rustTarget = "arm-unknown-linux-musleabihf";
|
||||
crossTarget = "armv6l-unknown-linux-musleabihf";
|
||||
};
|
||||
"aarch64-linux" = {
|
||||
rustTarget = "aarch64-unknown-linux-musl";
|
||||
crossTarget = "aarch64-unknown-linux-musl";
|
||||
};
|
||||
"i686-linux" = {
|
||||
rustTarget = "i686-unknown-linux-musl";
|
||||
crossTarget = "i686-unknown-linux-musl";
|
||||
};
|
||||
|
||||
"x86_64-darwin" = {
|
||||
rustTarget = "x86_64-apple-darwin";
|
||||
crossTarget = "x86_64-darwin";
|
||||
};
|
||||
"aarch64-darwin" = {
|
||||
rustTarget = "aarch64-apple-darwin";
|
||||
crossTarget = "aarch64-darwin";
|
||||
};
|
||||
};
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
@@ -91,10 +113,10 @@
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
};
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
|
||||
rustTarget = "x86_64-pc-windows-gnu";
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -125,7 +147,6 @@
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
@@ -137,10 +158,12 @@
|
||||
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
mkWin32RustPackage = packageName:
|
||||
let
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
|
||||
rustTarget = "i686-pc-windows-gnu";
|
||||
in
|
||||
let
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -180,7 +203,6 @@
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.nasm # aws-lc-sys requires it
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
@@ -193,7 +215,6 @@
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
@@ -207,12 +228,14 @@
|
||||
|
||||
mkCrossRustPackage = arch: packageName:
|
||||
let
|
||||
crossTarget = arch2targets."${arch}";
|
||||
rustTarget = arch2targets."${arch}".rustTarget;
|
||||
crossTarget = arch2targets."${arch}".crossTarget;
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
|
||||
in
|
||||
let
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "format-flowed"
|
||||
version = "1.0.0"
|
||||
description = "format=flowed support"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["email"]
|
||||
|
||||
@@ -78,11 +78,11 @@ pub fn format_flowed(text: &str) -> String {
|
||||
|
||||
let mut prefix = prefix.to_string();
|
||||
|
||||
if quote_depth > 0
|
||||
&& let Some(s) = line.strip_prefix(' ')
|
||||
{
|
||||
line = s;
|
||||
prefix += " ";
|
||||
if quote_depth > 0 {
|
||||
if let Some(s) = line.strip_prefix(' ') {
|
||||
line = s;
|
||||
prefix += " ";
|
||||
}
|
||||
}
|
||||
|
||||
result += &format_line_flowed(line, &prefix);
|
||||
@@ -224,7 +224,8 @@ mod tests {
|
||||
fn test_unformat_flowed() {
|
||||
let text = "this is a very long message that should be wrapped using format=flowed and \n\
|
||||
unwrapped on the receiver";
|
||||
let expected = "this is a very long message that should be wrapped using format=flowed and \
|
||||
let expected =
|
||||
"this is a very long message that should be wrapped using format=flowed and \
|
||||
unwrapped on the receiver";
|
||||
assert_eq!(unformat_flowed(text, false), expected);
|
||||
|
||||
@@ -254,7 +255,8 @@ mod tests {
|
||||
assert_eq!(format_flowed_quote(quote), expected);
|
||||
|
||||
let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
|
||||
let expected = "> this is a very long quote that should be wrapped using format=flowed and \r\n\
|
||||
let expected =
|
||||
"> this is a very long quote that should be wrapped using format=flowed and \r\n\
|
||||
> unwrapped on the receiver";
|
||||
assert_eq!(format_flowed_quote(quote), expected);
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
name = "deltachat-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.59.0"
|
||||
version = "2.51.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -397,7 +397,7 @@ class Account:
|
||||
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
|
||||
|
||||
def create_chat(self, obj) -> Chat:
|
||||
"""Create a single chat with Account, Contact or e-mail address."""
|
||||
"""Create a 1:1 chat with Account, Contact or e-mail address."""
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
def create_group_chat(
|
||||
|
||||
@@ -74,7 +74,7 @@ class Chat:
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_single(self) -> bool:
|
||||
"""Return True if this chat is a single chat, False otherwise."""
|
||||
"""Return True if this chat is a single/direct chat, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE
|
||||
|
||||
def is_mailinglist(self) -> bool:
|
||||
@@ -458,7 +458,7 @@ class Chat:
|
||||
"""Get group profile image.
|
||||
|
||||
For groups, this is the image set by any group member using
|
||||
set_chat_profile_image(). For single chats, this is the image
|
||||
set_chat_profile_image(). For normal chats, this is the image
|
||||
set by each remote user on their own using dc_set_config(context,
|
||||
"selfavatar", image).
|
||||
:returns: path to profile image, None if no profile image exists.
|
||||
|
||||
@@ -107,7 +107,7 @@ class Contact:
|
||||
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
|
||||
|
||||
def create_chat(self):
|
||||
"""create or get an existing single chat object for the specified contact or contact id.
|
||||
"""create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
|
||||
:param contact: chat_id (int) or contact object.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
|
||||
@@ -7,7 +7,6 @@ import imaplib
|
||||
import io
|
||||
import pathlib
|
||||
import ssl
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
@@ -43,22 +42,10 @@ class DirectImap:
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
if host.startswith("_"):
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||
self.conn.login(user, pw)
|
||||
|
||||
while True:
|
||||
try:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
self.select_folder("INBOX")
|
||||
return
|
||||
except (OSError, imaplib.IMAP4.abort) as e:
|
||||
# OSError covers ssl.SSLError, "abort" is a dropped connection;
|
||||
# permanent IMAP errors (login rejected etc) must not be retried.
|
||||
print(f"direct_imap connect to {host} failed ({e!r}), retrying")
|
||||
time.sleep(1)
|
||||
self.select_folder("INBOX")
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
|
||||
@@ -226,10 +226,6 @@ class EventThread(threading.Thread):
|
||||
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.event_emitter = ffi.gc(
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.daemon = True
|
||||
self._marked_for_shutdown = False
|
||||
@@ -254,9 +250,13 @@ class EventThread(threading.Thread):
|
||||
def run(self) -> None:
|
||||
"""get and run events until shutdown."""
|
||||
with self.log_execution("EVENT THREAD"):
|
||||
event_emitter = ffi.gc(
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
while not self._marked_for_shutdown:
|
||||
with self.swallow_and_log_exception("Unexpected error in event thread"):
|
||||
event = lib.dc_get_next_event(self.event_emitter)
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
if event == ffi.NULL or self._marked_for_shutdown:
|
||||
break
|
||||
self._process_event(event)
|
||||
|
||||
@@ -283,7 +283,7 @@ class Message:
|
||||
lib.dc_msg_set_override_sender_name(self._dc_msg, as_dc_charpointer(name))
|
||||
|
||||
def get_sender_chat(self):
|
||||
"""return the single chat with the sender of this message.
|
||||
"""return the 1:1 chat with the sender of this message.
|
||||
|
||||
:returns: :class:`deltachat.chat.Chat` instance
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Provider info class."""
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer
|
||||
|
||||
|
||||
class ProviderNotFoundError(Exception):
|
||||
"""The provider information was not found."""
|
||||
|
||||
|
||||
class Provider:
|
||||
"""
|
||||
Provider information.
|
||||
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
def __init__(self, account, addr) -> None:
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
lib.dc_provider_unref,
|
||||
)
|
||||
if provider == ffi.NULL:
|
||||
raise ProviderNotFoundError("Provider not found")
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self) -> int:
|
||||
"""The status of the provider information.
|
||||
|
||||
This is one of the
|
||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_OK`,
|
||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_PREPARATION` or
|
||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_BROKEN` constants.
|
||||
"""
|
||||
return lib.dc_provider_get_status(self._provider)
|
||||
@@ -1,6 +1,7 @@
|
||||
import fnmatch
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -9,7 +10,7 @@ import time
|
||||
import weakref
|
||||
import random
|
||||
from queue import Queue
|
||||
from typing import Callable, Dict, List, Optional
|
||||
from typing import Callable, Dict, List, Optional, Set
|
||||
|
||||
import pytest
|
||||
from _pytest._code import Source
|
||||
@@ -137,6 +138,94 @@ def pytest_report_header(config):
|
||||
return summary
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def testprocess(request):
|
||||
"""Return live account configuration manager.
|
||||
|
||||
The returned object is a :class:`TestProcess` object."""
|
||||
return TestProcess(pytestconfig=request.config)
|
||||
|
||||
|
||||
class TestProcess:
|
||||
"""A pytest session-scoped instance to help with managing "live" account configurations."""
|
||||
|
||||
_addr2files: Dict[str, Dict[pathlib.Path, bytes]]
|
||||
|
||||
def __init__(self, pytestconfig) -> None:
|
||||
self.pytestconfig = pytestconfig
|
||||
self._addr2files = {}
|
||||
self._configlist: List[Dict[str, str]] = []
|
||||
|
||||
def get_liveconfig_producer(self):
|
||||
"""provide live account configs, cached on a per-test-process scope
|
||||
so that test functions can reuse already known live configs.
|
||||
"""
|
||||
chatmail_opt = self.pytestconfig.getoption("--chatmail")
|
||||
if chatmail_opt:
|
||||
# Use a chatmail instance.
|
||||
domain = chatmail_opt
|
||||
MAX_LIVE_CREATED_ACCOUNTS = 10
|
||||
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
|
||||
try:
|
||||
yield self._configlist[index]
|
||||
except IndexError:
|
||||
part = "".join(random.choices("2345789acdefghjkmnpqrstuvwxyz", k=6))
|
||||
username = f"ci-{part}"
|
||||
password = f"{username}${username}"
|
||||
addr = f"{username}@{domain}"
|
||||
config = {"addr": addr, "mail_pw": password}
|
||||
print("newtmpuser {}: addr={}".format(index, config["addr"]))
|
||||
self._configlist.append(config)
|
||||
yield config
|
||||
pytest.fail(f"more than {MAX_LIVE_CREATED_ACCOUNTS} live accounts requested.")
|
||||
else:
|
||||
pytest.skip(
|
||||
"specify CHATMAIL_DOMAIN or --chatmail to provide live accounts",
|
||||
)
|
||||
|
||||
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
|
||||
db_target_path = pathlib.Path(db_target_path)
|
||||
assert not db_target_path.exists()
|
||||
|
||||
try:
|
||||
filescache = self._addr2files[cache_addr]
|
||||
except KeyError:
|
||||
print("CACHE FAIL for", cache_addr)
|
||||
return False
|
||||
else:
|
||||
print("CACHE HIT for", cache_addr)
|
||||
targetdir = db_target_path.parent
|
||||
write_dict_to_dir(filescache, targetdir)
|
||||
return True
|
||||
|
||||
def cache_maybe_store_configured_db_files(self, acc):
|
||||
addr = acc.get_config("addr")
|
||||
assert acc.is_configured()
|
||||
# don't overwrite existing entries
|
||||
if addr not in self._addr2files:
|
||||
print("storing cache for", addr)
|
||||
basedir = pathlib.Path(acc.get_blobdir()).parent
|
||||
self._addr2files[addr] = create_dict_from_files_in_path(basedir)
|
||||
return True
|
||||
|
||||
|
||||
def create_dict_from_files_in_path(base):
|
||||
cachedict = {}
|
||||
for path in base.glob("**/*"):
|
||||
if path.is_file():
|
||||
cachedict[path.relative_to(base)] = path.read_bytes()
|
||||
return cachedict
|
||||
|
||||
|
||||
def write_dict_to_dir(dic, target_dir):
|
||||
assert dic
|
||||
for relpath, content in dic.items():
|
||||
path = target_dir.joinpath(relpath)
|
||||
if not path.parent.exists():
|
||||
os.makedirs(path.parent)
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def data(request):
|
||||
"""Test data."""
|
||||
@@ -189,15 +278,23 @@ class ACSetup:
|
||||
|
||||
_configured_events: Queue
|
||||
|
||||
def __init__(self, init_time) -> None:
|
||||
def __init__(self, testprocess, init_time) -> None:
|
||||
self._configured_events = Queue()
|
||||
self._account2state: Dict[Account, str] = {}
|
||||
self._account2config: Dict[Account, Dict[str, str]] = {}
|
||||
self._imap_cleaned: Set[str] = set()
|
||||
self.testprocess = testprocess
|
||||
self.init_time = init_time
|
||||
|
||||
def log(self, *args):
|
||||
print("[acsetup]", f"{time.time() - self.init_time:.3f}", *args)
|
||||
|
||||
def add_configured(self, account):
|
||||
"""add an already configured account."""
|
||||
assert account.is_configured()
|
||||
self._account2state[account] = self.CONFIGURED
|
||||
self.log("added already configured account", account, account.get_config("addr"))
|
||||
|
||||
def start_configure(self, account):
|
||||
"""add an account and start its configure process."""
|
||||
|
||||
@@ -229,7 +326,7 @@ class ACSetup:
|
||||
for each account which either is CONFIGURED already or which is CONFIGURING
|
||||
and successfully completing the configuration process.
|
||||
"""
|
||||
print("bring_online finds accounts=", self._account2state)
|
||||
print("wait_all_configured finds accounts=", self._account2state)
|
||||
for acc, state in self._account2state.items():
|
||||
if state == self.CONFIGURED:
|
||||
self._onconfigure_start_io(acc)
|
||||
@@ -265,12 +362,23 @@ class ACSetup:
|
||||
acc.add_account_plugin(logger, name="logger-" + acc._logid)
|
||||
|
||||
def init_imap(self, acc):
|
||||
"""initialize direct_imap for the account."""
|
||||
"""initialize direct_imap and cleanup server state."""
|
||||
from deltachat.direct_imap import DirectImap
|
||||
|
||||
assert acc.is_configured()
|
||||
if not hasattr(acc, "direct_imap"):
|
||||
acc.direct_imap = DirectImap(acc)
|
||||
addr = acc.get_config("addr")
|
||||
if addr not in self._imap_cleaned:
|
||||
imap = acc.direct_imap
|
||||
for folder in imap.list_folders():
|
||||
if folder.lower() == "inbox" or folder.lower() == "deltachat":
|
||||
assert imap.select_folder(folder)
|
||||
imap.delete("1:*", expunge=True)
|
||||
else:
|
||||
imap.conn.folder.delete(folder)
|
||||
acc.log(f"imap cleaned for addr {addr}")
|
||||
self._imap_cleaned.add(addr)
|
||||
|
||||
|
||||
class ACFactory:
|
||||
@@ -282,19 +390,24 @@ class ACFactory:
|
||||
_acsetup: ACSetup
|
||||
_preconfigured_keys: List[str]
|
||||
|
||||
def __init__(self, request, tmpdir, data) -> None:
|
||||
def __init__(self, request, testprocess, tmpdir, data) -> None:
|
||||
self.init_time = time.time()
|
||||
self.tmpdir = tmpdir
|
||||
self.pytestconfig = request.config
|
||||
self.data = data
|
||||
self.testprocess = testprocess
|
||||
self._liveconfig_producer = testprocess.get_liveconfig_producer()
|
||||
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self._acsetup = ACSetup(self.init_time)
|
||||
self._acsetup = ACSetup(testprocess, self.init_time)
|
||||
self._preconfigured_keys = ["alice", "bob", "charlie", "dom", "elena", "fiona"]
|
||||
self.set_logging_default(False)
|
||||
request.addfinalizer(self.finalize)
|
||||
|
||||
def log(self, *args):
|
||||
print("[acfactory]", f"{time.time() - self.init_time:.3f}", *args)
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
@@ -311,27 +424,33 @@ class ACFactory:
|
||||
acc.disable_logging()
|
||||
|
||||
def get_next_liveconfig(self):
|
||||
"""Return a fresh account configuration with unique address and password."""
|
||||
chatmail_domain = self.pytestconfig.getoption("--chatmail")
|
||||
if not chatmail_domain:
|
||||
pytest.skip(
|
||||
"specify CHATMAIL_DOMAIN or --chatmail to provide live accounts",
|
||||
)
|
||||
part = "".join(random.choices("2345789acdefghjkmnpqrstuvwxyz", k=6))
|
||||
username = f"ci-{part}"
|
||||
password = f"{username}${username}"
|
||||
addr = f"{username}@{chatmail_domain}"
|
||||
configdict = {"addr": addr, "mail_pw": password}
|
||||
print(f"newtmpuser: addr={addr}")
|
||||
"""
|
||||
Base function to get functional online configurations
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
|
||||
if self.pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
assert "addr" in configdict and "mail_pw" in configdict
|
||||
return configdict
|
||||
|
||||
def _get_cached_account(self, addr) -> Optional[Account]:
|
||||
if addr in self.testprocess._addr2files:
|
||||
return self._getaccount(addr)
|
||||
return None
|
||||
|
||||
def get_unconfigured_account(self, closed=False) -> Account:
|
||||
return self._getaccount(closed=closed)
|
||||
|
||||
def _getaccount(self, try_cache_addr=None, closed=False) -> Account:
|
||||
logid = f"ac{len(self._accounts) + 1}"
|
||||
# we need to use fixed database basename for maybe_cache_* functions to work
|
||||
path = self.tmpdir.mkdir(logid).join("dc.db")
|
||||
if try_cache_addr:
|
||||
self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path)
|
||||
ac = Account(path.strpath, logging=self._logging, closed=closed)
|
||||
ac._logid = logid # later instantiated FFIEventLogger needs this
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
@@ -376,16 +495,23 @@ class ACFactory:
|
||||
self._acsetup.init_logging(ac)
|
||||
return ac
|
||||
|
||||
def new_online_configuring_account(self, cloned_from=None, **kwargs) -> Account:
|
||||
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs) -> Account:
|
||||
if cloned_from is None:
|
||||
configdict = self.get_next_liveconfig()
|
||||
else:
|
||||
# XXX we might want to transfer the key to the new account
|
||||
configdict = {
|
||||
"addr": cloned_from.get_config("addr"),
|
||||
"mail_pw": cloned_from.get_config("mail_pw"),
|
||||
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
|
||||
}
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
if ac is not None:
|
||||
# make sure we consume a preconfig key, as if we had created a fresh account
|
||||
self._preconfigured_keys.pop(0)
|
||||
self._acsetup.add_configured(ac)
|
||||
return ac
|
||||
ac = self.prepare_account_from_liveconfig(configdict)
|
||||
self._acsetup.start_configure(ac)
|
||||
return ac
|
||||
@@ -410,8 +536,11 @@ class ACFactory:
|
||||
print("all accounts online")
|
||||
|
||||
def get_online_accounts(self, num):
|
||||
accounts = [self.new_online_configuring_account() for i in range(num)]
|
||||
accounts = [self.new_online_configuring_account(cache=True) for i in range(num)]
|
||||
self.bring_accounts_online()
|
||||
# we cache fully configured and started accounts
|
||||
for acc in accounts:
|
||||
self.testprocess.cache_maybe_store_configured_db_files(acc)
|
||||
return accounts
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
@@ -486,15 +615,16 @@ class ACFactory:
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def acfactory(request, tmpdir, data):
|
||||
def acfactory(request, tmpdir, testprocess, data):
|
||||
"""Account factory."""
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, data=data)
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
|
||||
yield am
|
||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
if request.config.getoption("--extra-info"):
|
||||
if testprocess.pytestconfig.getoption("--extra-info"):
|
||||
logfile = io.StringIO()
|
||||
am.dump_imap_summary(logfile=logfile)
|
||||
print(logfile.getvalue())
|
||||
# request.node.add_report_section("call", "imap-server-state", s)
|
||||
|
||||
|
||||
class BotProcess:
|
||||
|
||||
@@ -224,7 +224,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
|
||||
lp.sec("ac2: sending message")
|
||||
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
|
||||
# "You were added by <addr>." message.
|
||||
# "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.is_system_message()
|
||||
msg_out = chat2.send_text("hello")
|
||||
@@ -278,7 +278,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
|
||||
lp.sec("ac2_offl: going online, checking the 'member added' message")
|
||||
ac2_offl.start_io()
|
||||
# Receive "You were added by <addr>." message.
|
||||
# Receive "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
contact = msg_in.get_sender_contact()
|
||||
assert msg_in.is_system_message()
|
||||
|
||||
@@ -789,7 +789,7 @@ def test_qr_email_capitalization(acfactory, lp):
|
||||
lp.sec("ac1 joins a group via a QR code")
|
||||
ac1_chat = ac1.qr_join_chat(qr)
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "You were added by {}.".format(ac3.get_config("addr"))
|
||||
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
assert len(ac1_chat.get_contacts()) == 2
|
||||
|
||||
lp.sec("ac2 joins a group via a QR code")
|
||||
|
||||
@@ -7,6 +7,8 @@ from deltachat.capi import ffi, lib
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.testplugin import (
|
||||
ACSetup,
|
||||
create_dict_from_files_in_path,
|
||||
write_dict_to_dir,
|
||||
)
|
||||
from deltachat.cutil import from_optional_dc_charpointer
|
||||
|
||||
@@ -14,8 +16,23 @@ from deltachat.cutil import from_optional_dc_charpointer
|
||||
|
||||
|
||||
class TestACSetup:
|
||||
def test_basic_states(self, acfactory, monkeypatch):
|
||||
pc = ACSetup(init_time=0.0)
|
||||
def test_cache_writing(self, tmp_path):
|
||||
base = tmp_path.joinpath("hello")
|
||||
base.mkdir()
|
||||
d1 = base.joinpath("dir1")
|
||||
d1.mkdir()
|
||||
d1.joinpath("file1").write_bytes(b"content1")
|
||||
d2 = d1.joinpath("dir2")
|
||||
d2.mkdir()
|
||||
d2.joinpath("file2").write_bytes(b"123")
|
||||
d = create_dict_from_files_in_path(base)
|
||||
newbase = tmp_path.joinpath("other")
|
||||
write_dict_to_dir(d, newbase)
|
||||
assert newbase.joinpath("dir1", "dir2", "file2").exists()
|
||||
assert newbase.joinpath("dir1", "file1").exists()
|
||||
|
||||
def test_basic_states(self, acfactory, monkeypatch, testprocess):
|
||||
pc = ACSetup(init_time=0.0, testprocess=testprocess)
|
||||
acc = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(acc, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(acc)
|
||||
@@ -28,8 +45,8 @@ class TestACSetup:
|
||||
pc.bring_online()
|
||||
assert pc._account2state[acc] == pc.IDLEREADY
|
||||
|
||||
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory):
|
||||
pc = ACSetup(init_time=0.0)
|
||||
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory, testprocess):
|
||||
pc = ACSetup(init_time=0.0, testprocess=testprocess)
|
||||
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -49,6 +66,30 @@ class TestACSetup:
|
||||
assert pc._account2state[ac1] == pc.IDLEREADY
|
||||
assert pc._account2state[ac2] == pc.IDLEREADY
|
||||
|
||||
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmp_path):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
holder = acfactory._acsetup.testprocess
|
||||
assert holder.cache_maybe_store_configured_db_files(ac1)
|
||||
assert not holder.cache_maybe_store_configured_db_files(ac1)
|
||||
acdir = tmp_path / "newaccount"
|
||||
acdir.mkdir()
|
||||
addr = ac1.get_config("addr")
|
||||
target_db_path = acdir / "db"
|
||||
assert holder.cache_maybe_retrieve_configured_db_files(addr, str(target_db_path))
|
||||
assert sum(1 for _ in acdir.iterdir()) >= 2
|
||||
|
||||
|
||||
def test_liveconfig_caching(acfactory, monkeypatch):
|
||||
prod = [
|
||||
{"addr": "1@example.org", "mail_pw": "123"},
|
||||
]
|
||||
acfactory._liveconfig_producer = iter(prod)
|
||||
d1 = acfactory.get_next_liveconfig()
|
||||
d1["hello"] = "world"
|
||||
acfactory._liveconfig_producer = iter(prod)
|
||||
d2 = acfactory.get_next_liveconfig()
|
||||
assert "hello" not in d2
|
||||
|
||||
|
||||
def test_empty_context():
|
||||
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||
@@ -130,6 +171,14 @@ def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
assert msg.id == 0
|
||||
|
||||
|
||||
def test_provider_info_none():
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||
|
||||
|
||||
def test_get_info_open(tmp_path):
|
||||
db_fname = tmp_path / "test.db"
|
||||
ctx = ffi.gc(
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026-08-14
|
||||
2026-05-29
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
# if the yaml import fails, run "pip install pyyaml"
|
||||
|
||||
import sys
|
||||
import yaml
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
out_all = ""
|
||||
out_domains = ""
|
||||
out_ids = ""
|
||||
domains_set = set()
|
||||
|
||||
|
||||
def camel(name):
|
||||
words = name.split("_")
|
||||
return "".join(w.capitalize() for i, w in enumerate(words))
|
||||
|
||||
|
||||
def cleanstr(s):
|
||||
s = s.strip()
|
||||
s = s.replace("\n", " ")
|
||||
s = s.replace("\\", "\\\\")
|
||||
s = s.replace('"', '\\"')
|
||||
return s
|
||||
|
||||
|
||||
def file2id(f):
|
||||
return f.stem
|
||||
|
||||
|
||||
def file2varname(f):
|
||||
f = file2id(f)
|
||||
f = f.replace(".", "_")
|
||||
f = f.replace("-", "_")
|
||||
return "P_" + f.upper()
|
||||
|
||||
|
||||
def file2url(f):
|
||||
f = file2id(f)
|
||||
f = f.replace(".", "-")
|
||||
return "https://providers.delta.chat/" + f
|
||||
|
||||
|
||||
def process_opt(data):
|
||||
if not "opt" in data:
|
||||
return "ProviderOptions::new()"
|
||||
opt = "ProviderOptions {\n"
|
||||
opt_data = data.get("opt", "")
|
||||
for key in opt_data:
|
||||
value = str(opt_data[key])
|
||||
if key == "max_smtp_rcpt_to":
|
||||
value = "Some(" + value + ")"
|
||||
if value in {"True", "False"}:
|
||||
value = value.lower()
|
||||
opt += " " + key + ": " + value + ",\n"
|
||||
opt += " ..ProviderOptions::new()\n"
|
||||
opt += " }"
|
||||
return opt
|
||||
|
||||
|
||||
def process_config_defaults(data):
|
||||
if not "config_defaults" in data:
|
||||
return "None"
|
||||
defaults = "Some(&[\n"
|
||||
config_defaults = data.get("config_defaults", "")
|
||||
for key in config_defaults:
|
||||
value = str(config_defaults[key])
|
||||
defaults += (
|
||||
" ConfigDefault { key: Config::"
|
||||
+ camel(key)
|
||||
+ ', value: "'
|
||||
+ value
|
||||
+ '" },\n'
|
||||
)
|
||||
defaults += " ])"
|
||||
return defaults
|
||||
|
||||
|
||||
def process_data(data, file):
|
||||
status = data.get("status", "")
|
||||
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
|
||||
raise TypeError("bad status")
|
||||
|
||||
comment = ""
|
||||
domains = ""
|
||||
if not "domains" in data:
|
||||
raise TypeError("no domains found")
|
||||
for domain in data["domains"]:
|
||||
domain = cleanstr(domain)
|
||||
if domain == "" or domain.lower() != domain:
|
||||
raise TypeError("bad domain: " + domain)
|
||||
|
||||
global domains_set
|
||||
if domain in domains_set:
|
||||
raise TypeError("domain used twice: " + domain)
|
||||
domains_set.add(domain)
|
||||
|
||||
domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
|
||||
comment += domain + ", "
|
||||
|
||||
ids = ""
|
||||
ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
|
||||
|
||||
server = ""
|
||||
has_imap = False
|
||||
has_smtp = False
|
||||
if "server" in data:
|
||||
for s in data["server"]:
|
||||
hostname = cleanstr(s.get("hostname", ""))
|
||||
port = int(s.get("port", ""))
|
||||
if hostname == "" or hostname.lower() != hostname or port <= 0:
|
||||
raise TypeError("bad hostname or port")
|
||||
|
||||
protocol = s.get("type", "").upper()
|
||||
if protocol == "IMAP":
|
||||
has_imap = True
|
||||
elif protocol == "SMTP":
|
||||
has_smtp = True
|
||||
else:
|
||||
raise TypeError("bad protocol")
|
||||
|
||||
socket = s.get("socket", "").upper()
|
||||
if socket != "STARTTLS" and socket != "SSL" and socket != "PLAIN":
|
||||
raise TypeError("bad socket")
|
||||
|
||||
username_pattern = s.get("username_pattern", "EMAIL").upper()
|
||||
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
|
||||
raise TypeError("bad username pattern")
|
||||
|
||||
server += (
|
||||
" Server { protocol: "
|
||||
+ protocol.capitalize()
|
||||
+ ", socket: "
|
||||
+ socket.capitalize()
|
||||
+ ', hostname: "'
|
||||
+ hostname
|
||||
+ '", port: '
|
||||
+ str(port)
|
||||
+ ", username_pattern: "
|
||||
+ username_pattern.capitalize()
|
||||
+ " },\n"
|
||||
)
|
||||
|
||||
opt = process_opt(data)
|
||||
config_defaults = process_config_defaults(data)
|
||||
|
||||
oauth2 = data.get("oauth2", "")
|
||||
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
|
||||
|
||||
provider = ""
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", "") or "")
|
||||
after_login_hint = cleanstr(data.get("after_login_hint", ""))
|
||||
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
|
||||
provider += (
|
||||
"static "
|
||||
+ file2varname(file)
|
||||
+ ": Provider = Provider {\n"
|
||||
)
|
||||
provider += ' id: "' + file2id(file) + '",\n'
|
||||
provider += " status: Status::" + status.capitalize() + ",\n"
|
||||
provider += ' before_login_hint: "' + before_login_hint + '",\n'
|
||||
provider += ' after_login_hint: "' + after_login_hint + '",\n'
|
||||
provider += ' overview_page: "' + file2url(file) + '",\n'
|
||||
provider += " server: &[\n" + server + " ],\n"
|
||||
provider += " opt: " + opt + ",\n"
|
||||
provider += " config_defaults: " + config_defaults + ",\n"
|
||||
provider += " oauth2_authorizer: " + oauth2 + ",\n"
|
||||
provider += "};\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
|
||||
if status != "OK" and before_login_hint == "":
|
||||
raise TypeError(
|
||||
"status PREPARATION or BROKEN requires before_login_hint: " + file
|
||||
)
|
||||
|
||||
# finally, add the provider
|
||||
global out_all, out_domains, out_ids
|
||||
out_all += "// " + file.name + ": " + comment.strip(", ") + "\n"
|
||||
|
||||
# also add provider with no special things to do -
|
||||
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
|
||||
out_all += provider
|
||||
out_domains += domains
|
||||
out_ids += ids
|
||||
|
||||
|
||||
def process_file(file):
|
||||
print("processing file: {}".format(file), file=sys.stderr)
|
||||
with open(file) as f:
|
||||
# load_all() loads "---"-separated yamls -
|
||||
# by coincidence, this is also the frontmatter separator :)
|
||||
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
|
||||
process_data(data, file)
|
||||
|
||||
|
||||
def process_dir(dir):
|
||||
print("processing directory: {}".format(dir), file=sys.stderr)
|
||||
files = sorted(f for f in dir.iterdir() if f.suffix == ".md")
|
||||
for f in files:
|
||||
process_file(f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
|
||||
|
||||
out_all = (
|
||||
"// file generated by src/provider/update.py\n\n"
|
||||
"use crate::provider::Protocol::*;\n"
|
||||
"use crate::provider::Socket::*;\n"
|
||||
"use crate::provider::UsernamePattern::*;\n"
|
||||
"use crate::provider::{\n"
|
||||
" Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,\n"
|
||||
"};\n"
|
||||
"use std::collections::HashMap;\n\n"
|
||||
"use std::sync::LazyLock;\n\n"
|
||||
)
|
||||
|
||||
process_dir(Path(sys.argv[1]))
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_DATA: [(&str, &Provider); " + str(len(domains_set)) + "] = [\n";
|
||||
out_all += out_domains
|
||||
out_all += "];\n\n"
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider>> = LazyLock::new(|| HashMap::from([\n"
|
||||
out_all += out_ids
|
||||
out_all += "]));\n\n"
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
now = datetime.datetime.utcnow()
|
||||
else:
|
||||
now = datetime.datetime.fromisoformat(sys.argv[2])
|
||||
out_all += (
|
||||
"pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> = "
|
||||
"LazyLock::new(|| chrono::NaiveDate::from_ymd_opt("
|
||||
+ str(now.year)
|
||||
+ ", "
|
||||
+ str(now.month)
|
||||
+ ", "
|
||||
+ str(now.day)
|
||||
+ ").unwrap());\n"
|
||||
)
|
||||
|
||||
print(out_all)
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Updates provider database.
|
||||
# Returns 1 if the database is changed, 0 otherwise.
|
||||
set -euo pipefail
|
||||
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
git clone --filter=blob:none https://github.com/chatmail/provider-db.git "$TMP"
|
||||
cd "$TMP"
|
||||
git checkout "$REV"
|
||||
DATE=$(git show -s --format=%cs)
|
||||
"$CORE_ROOT"/scripts/create-provider-data-rs.py "$TMP/_providers" "$DATE" >"$CORE_ROOT/src/provider/data.rs"
|
||||
rustfmt --edition 2024 "$CORE_ROOT/src/provider/data.rs"
|
||||
rm -fr "$TMP"
|
||||
|
||||
cd "$CORE_ROOT"
|
||||
test -z "$(git status --porcelain src/provider/data.rs)"
|
||||
@@ -196,7 +196,7 @@ If the messenger finds a valid but not existent group-id,
|
||||
the messenger MAY create a new group.
|
||||
If no group-id is found,
|
||||
the message MAY be assigned
|
||||
to a single chat with the email-address given in `From`.
|
||||
to a normal single-user chat with the email-address given in `From`.
|
||||
|
||||
|
||||
## Add and remove members
|
||||
|
||||
+2
-2
@@ -534,8 +534,8 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Sets notification token for Apple Push Notification service.
|
||||
pub fn set_push_device_token(&self, token: &str) -> Result<()> {
|
||||
self.push_subscriber.set_device_token(token);
|
||||
pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
//! Get version information of clients.
|
||||
//!
|
||||
//! Used by clients to inform about updates.
|
||||
//! The version information comes in via IMAP METADATA,
|
||||
//! (as JSON) and is parsed to `AppVersionInfo`.
|
||||
use crate::accounts::Accounts;
|
||||
use crate::log::warn;
|
||||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Version information of clients as used on the wire.
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
struct AppVersionInfo {
|
||||
/// Array of clients with version information.
|
||||
clients: Vec<AppClient>,
|
||||
}
|
||||
|
||||
/// Version information of a single client, eg. "deltachat" or "ubuntutouch".
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
struct AppClient {
|
||||
/// ID how the client identifies itself, eg. "deltachat" or "ubuntutouch"
|
||||
client_id: String,
|
||||
|
||||
/// Array of sources for that client.
|
||||
sources: Vec<AppSource>,
|
||||
}
|
||||
|
||||
/// Version information of a single source of a client, eg. "gplay" or "fdroid".
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct AppSource {
|
||||
/// ID how the client identifies a source, eg. "gplay" or "fdroid"
|
||||
source_id: String,
|
||||
|
||||
/// Always increasing version number.
|
||||
pub version_integer: u32,
|
||||
|
||||
/// Any version string.
|
||||
pub version_string: String,
|
||||
|
||||
/// Where to download that version.
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
/// Get version information of a specific client and source.
|
||||
///
|
||||
/// Iterates over all accounts and all transports,
|
||||
/// checking version information set by IMAP METADATA,
|
||||
/// and returns the source with the highest `version_integer`.
|
||||
///
|
||||
/// If no matching version information is available at all, `None` is returned.
|
||||
pub async fn get_app_version(
|
||||
accounts: &Accounts,
|
||||
client_id: &str,
|
||||
source_id: &str,
|
||||
) -> Result<Option<AppSource>> {
|
||||
let mut best: Option<AppSource> = None;
|
||||
|
||||
for account_id in accounts.get_all() {
|
||||
let Some(context) = accounts.get_account(account_id) else {
|
||||
continue;
|
||||
};
|
||||
for metadata in context.metadata.read().await.values() {
|
||||
let Some(json) = &metadata.app_versions else {
|
||||
continue;
|
||||
};
|
||||
let app_versions: AppVersionInfo = match serde_json::from_str(json) {
|
||||
Ok(app_versions) => app_versions,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse appversions: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let candidate = app_versions
|
||||
.clients
|
||||
.into_iter()
|
||||
.find(|c| c.client_id == client_id)
|
||||
.and_then(|c| c.sources.into_iter().find(|s| s.source_id == source_id));
|
||||
|
||||
if let Some(candidate) = candidate
|
||||
&& best
|
||||
.as_ref()
|
||||
.is_none_or(|b| candidate.version_integer > b.version_integer)
|
||||
{
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(best)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_app_version_info_deserialize() -> Result<()> {
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "deltachat",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "gplay",
|
||||
"versionInteger": 754,
|
||||
"versionString": "2.57.0",
|
||||
"downloadUrl": "https://github.com/deltachat/deltachat-android/releases/download/v2.57.0/deltachat-gplay-release-2.57.0.apk"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
let versions: AppVersionInfo = serde_json::from_str(json)?;
|
||||
assert_eq!(versions.clients.len(), 1);
|
||||
assert_eq!(versions.clients[0].client_id, "deltachat");
|
||||
assert_eq!(versions.clients[0].sources.len(), 1);
|
||||
assert_eq!(versions.clients[0].sources[0].source_id, "gplay");
|
||||
assert_eq!(versions.clients[0].sources[0].version_integer, 754);
|
||||
assert_eq!(versions.clients[0].sources[0].version_string, "2.57.0");
|
||||
assert_eq!(
|
||||
versions.clients[0].sources[0].download_url,
|
||||
"https://github.com/deltachat/deltachat-android/releases/download/v2.57.0/deltachat-gplay-release-2.57.0.apk"
|
||||
);
|
||||
|
||||
// missing fields are set to defaults, additional fields are ignored, errors are errors
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "deltachat",
|
||||
"xsource": "bang"
|
||||
}
|
||||
],
|
||||
"foo": "bar"
|
||||
}"##;
|
||||
let versions: AppVersionInfo = serde_json::from_str(json)?;
|
||||
assert_eq!(versions.clients.len(), 1);
|
||||
assert_eq!(versions.clients[0].client_id, "deltachat");
|
||||
assert_eq!(versions.clients[0].sources.len(), 0);
|
||||
|
||||
let json = "{}";
|
||||
let versions: AppVersionInfo = serde_json::from_str(json)?;
|
||||
assert_eq!(versions.clients.len(), 0);
|
||||
|
||||
let json = "";
|
||||
assert!(serde_json::from_str::<AppVersionInfo>(json).is_err());
|
||||
|
||||
let json = "bad json";
|
||||
assert!(serde_json::from_str::<AppVersionInfo>(json).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mockup_app_versions(
|
||||
accounts: &Accounts,
|
||||
account_id: u32,
|
||||
transport_id: u32,
|
||||
json: &str,
|
||||
) {
|
||||
let context = accounts.get_account(account_id).expect("account exists");
|
||||
let mut metadata = context.metadata.write().await;
|
||||
metadata.entry(transport_id).or_default().app_versions = Some(json.to_string());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_app_versions() -> Result<()> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
|
||||
// no accounts configured, means no versions are reported
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// first account reports two clients, with one and two sources
|
||||
let account_id1 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "basta",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "web",
|
||||
"versionInteger": 754,
|
||||
"versionString": "2.57.0",
|
||||
"downloadUrl": "https://example.org/basta-2.57.0.apk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientId": "foo",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "bar",
|
||||
"versionInteger": 42,
|
||||
"versionString": "42.0",
|
||||
"downloadUrl": "https://foo.bar/42.0.prg"
|
||||
},
|
||||
{
|
||||
"sourceId": "baz",
|
||||
"versionInteger": 1337,
|
||||
"versionString": "13.37",
|
||||
"downloadUrl": "https://dl.org/1337.acc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id1, 1, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "basta", "web").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 754);
|
||||
assert_eq!(version.version_string, "2.57.0");
|
||||
assert_eq!(version.download_url, "https://example.org/basta-2.57.0.apk");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 42);
|
||||
assert_eq!(version.version_string, "42.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/42.0.prg");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "baz").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 1337);
|
||||
assert_eq!(version.version_string, "13.37");
|
||||
assert_eq!(version.download_url, "https://dl.org/1337.acc");
|
||||
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// a second account reports a newer version for "bar"
|
||||
let account_id2 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "foo",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "bar",
|
||||
"versionInteger": 43,
|
||||
"versionString": "43.0",
|
||||
"downloadUrl": "https://foo.bar/43.0-is-newer.prg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id2, 1, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "basta", "web").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 754);
|
||||
assert_eq!(version.version_string, "2.57.0");
|
||||
assert_eq!(version.download_url, "https://example.org/basta-2.57.0.apk");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 43);
|
||||
assert_eq!(version.version_string, "43.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/43.0-is-newer.prg");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "baz").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 1337);
|
||||
assert_eq!(version.version_string, "13.37");
|
||||
assert_eq!(version.download_url, "https://dl.org/1337.acc");
|
||||
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// a third account reports a older version for "bar", that is ignored
|
||||
let account_id3 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "foo",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "bar",
|
||||
"versionInteger": 39,
|
||||
"versionString": "39.0",
|
||||
"downloadUrl": "https://foo.bar/39.0-is-too-old.prg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id3, 1, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "basta", "web").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 754);
|
||||
assert_eq!(version.version_string, "2.57.0");
|
||||
assert_eq!(version.download_url, "https://example.org/basta-2.57.0.apk");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 43);
|
||||
assert_eq!(version.version_string, "43.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/43.0-is-newer.prg");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "baz").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 1337);
|
||||
assert_eq!(version.version_string, "13.37");
|
||||
assert_eq!(version.download_url, "https://dl.org/1337.acc");
|
||||
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// second account returns an invalid json, that account is skipped, but app versionss are still gathered from the other
|
||||
let json = r"bad json!";
|
||||
mockup_app_versions(&accounts, account_id2, 1, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 42);
|
||||
assert_eq!(version.version_string, "42.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/42.0.prg");
|
||||
|
||||
// second account now has two transports, returning different version information in different orders
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{ "clientId": "a", "sources": [{"sourceId": "a", "versionInteger": 1, "versionString": "1.0", "downloadUrl": "https://a/1.prg"}] },
|
||||
{ "clientId": "b", "sources": [{"sourceId": "b", "versionInteger": 2, "versionString": "2.0", "downloadUrl": "https://b/2.prg"}] },
|
||||
{ "clientId": "c", "sources": [{"sourceId": "c", "versionInteger": 2, "versionString": "2.0", "downloadUrl": "https://c/2.prg"}] }
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id2, 1, json).await;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{ "clientId": "a", "sources": [{"sourceId": "a", "versionInteger": 2, "versionString": "2.0", "downloadUrl": "https://a/2.prg"}] },
|
||||
{ "clientId": "b", "sources": [{"sourceId": "b", "versionInteger": 1, "versionString": "1.0", "downloadUrl": "https://b/1.prg"}] },
|
||||
{ "clientId": "d", "sources": [{"sourceId": "d", "versionInteger": 2, "versionString": "2.0", "downloadUrl": "https://d/2.prg"}] }
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id2, 2, json).await;
|
||||
let version = get_app_version(&accounts, "a", "a").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 2);
|
||||
let version = get_app_version(&accounts, "b", "b").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 2);
|
||||
let version = get_app_version(&accounts, "c", "c").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 2);
|
||||
let version = get_app_version(&accounts, "d", "d").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::addr_normalize;
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use rand::seq::IndexedRandom;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam};
|
||||
use crate::{configure::EnteredLoginParam, context::Context, tools::time};
|
||||
|
||||
/// The target number of transports.
|
||||
const NUM_TRANSPORTS_TARGET: usize = 3;
|
||||
/// How often we want to try adding new relays.
|
||||
const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour
|
||||
/// How long we ignore a relay candidate after failing to connect to it:
|
||||
const BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY: i64 = 60 * 60 * 24 * 7; // one week
|
||||
|
||||
pub(crate) fn maybe_add_additional_relays(
|
||||
context: Context,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
// We need to Box::pin the future because it wouldn't compile otherwise
|
||||
// because Rust async doesn't support recursion:
|
||||
// `maybe_add_additional_relays_inner()` calls `restart_io_if_running()`,
|
||||
// which (via several other functions) calls `imap_loop()`,
|
||||
// which (via several other functions) calls `maybe_add_additional_relays()`
|
||||
Box::pin(async move {
|
||||
let skip_network = false;
|
||||
let relay_added = maybe_add_additional_relays_inner(&context, skip_network)
|
||||
.await
|
||||
.log_err(&context)
|
||||
.unwrap_or(false);
|
||||
|
||||
if relay_added {
|
||||
info!(context, "Restarting IO after relay addition");
|
||||
context.restart_io_if_running().await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool) -> Result<bool> {
|
||||
let now = time();
|
||||
|
||||
let Ok(_lock) = context.background_task_mutex.try_lock() else {
|
||||
// Housekeeping or automatic relay management is already running in another thread, do nothing.
|
||||
return Ok(false);
|
||||
};
|
||||
let last_timestamp = context
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
if last_timestamp > now {
|
||||
warn!(
|
||||
context,
|
||||
"Clock ran backwards, unclear if automatic relay management should run. Will run it anyways."
|
||||
);
|
||||
} else if last_timestamp > now.saturating_sub(AUTOMATIC_ADDITION_DEBOUNCE_SECONDS) {
|
||||
return Ok(false);
|
||||
}
|
||||
if !context
|
||||
.get_config_bool(Config::AutomaticRelayManagement)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if context
|
||||
.get_config_bool(Config::AutomaticRelayManagementFinished)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
// Set the config at the beginning to avoid endless loops.
|
||||
// Race conditions are not a concern because we locked the mutex.
|
||||
context
|
||||
.set_config_internal(Config::LastAutomaticRelayManagement, Some(&now.to_string()))
|
||||
.await?;
|
||||
|
||||
let mut relay_added = false;
|
||||
// Using `for` instead of `while` to prevent infinite loop
|
||||
for _ in 0..NUM_TRANSPORTS_TARGET {
|
||||
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::AutomaticRelayManagementFinished,
|
||||
config::from_bool(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(relay_added);
|
||||
}
|
||||
|
||||
// First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY` seconds.
|
||||
// Hosts that are already used are excluded.
|
||||
let candidates = load_relay_candidates(context, now).await?;
|
||||
|
||||
let Some(host) = candidates.choose(&mut rand::rng()) else {
|
||||
info!(
|
||||
context,
|
||||
"maybe_add_additional_relays: No suitable candidates"
|
||||
);
|
||||
return Ok(relay_added);
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Trying to automatically add relay {host} (there were {} candidates).",
|
||||
candidates.len(),
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE relay_candidates SET last_tried=? WHERE host=?",
|
||||
(now, host),
|
||||
)
|
||||
.await?;
|
||||
let param = login_param_from_host(host);
|
||||
let res = crate::configure::configure(context, ¶m, skip_network).await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to automatically add a relay {host}: {e:#}."
|
||||
);
|
||||
} else {
|
||||
info!(context, "Successfully automatically added relay {host}.");
|
||||
relay_added = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(relay_added)
|
||||
}
|
||||
|
||||
async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>, anyhow::Error> {
|
||||
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
||||
let candidates: Vec<String> = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
// This also selects candidates which have last_tried in the future,
|
||||
// essentially treating them as never tried,
|
||||
// so if some timestamp far in the future is accidentally stored,
|
||||
// we are not stuck never trying the candidate.
|
||||
// After trying the candidate, last_tried will be corrected to the current time.
|
||||
"SELECT host FROM relay_candidates WHERE (last_tried<? OR last_tried>?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM transports
|
||||
WHERE substr(addr, instr(addr, '@') + 1) = host
|
||||
)",
|
||||
(cutoff_timestamp, now),
|
||||
|row| Ok(row.get::<_, String>(0)?),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str) -> EnteredLoginParam {
|
||||
let rng = &mut rand::rng();
|
||||
let username = Alphanumeric.sample_string(rng, 9);
|
||||
let addr = username + "@" + host;
|
||||
let addr = addr_normalize(&addr);
|
||||
// 22 * log2(26 * 2 + 10) = 130 bits of entropy
|
||||
let password = Alphanumeric.sample_string(rng, 22);
|
||||
|
||||
EnteredLoginParam {
|
||||
addr,
|
||||
imap: EnteredImapLoginParam {
|
||||
password,
|
||||
..Default::default()
|
||||
},
|
||||
smtp: Default::default(),
|
||||
certificate_checks: EnteredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod automatic_relay_management_tests;
|
||||
@@ -1,292 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
|
||||
// This host should be returned by load_relay_candidates():
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("never_tried.example", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This host was recently tried and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("recent.example", now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This host is already in use (alice@example.org) and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("example.org", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let candidates = load_relay_candidates(t, now).await?;
|
||||
|
||||
assert_eq!(candidates, vec!["never_tried.example".to_string()]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_multiple() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut candidates = load_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"a.example".to_string(),
|
||||
"b.example".to_string(),
|
||||
"c.example".to_string()
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_automatic_relay_management_does_nothing(t: &TestContext) {
|
||||
let transports_before = t.count_transports().await.unwrap();
|
||||
let config_before = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let skip_network = false; // No need to skip network, nothing is supposed to happen
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relay_added, false);
|
||||
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await
|
||||
.unwrap();
|
||||
let transports_after = t.count_transports().await.unwrap();
|
||||
|
||||
assert_eq!(config_after, config_before);
|
||||
assert_eq!(transports_before, transports_after);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_mutex_held() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
enable_config(t).await;
|
||||
|
||||
// Hold the housekeeping mutex ourselves, simulating another task
|
||||
// already running housekeeping or relay management.
|
||||
let _lock = t.background_task_mutex.lock().await;
|
||||
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_debounce() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let some_seconds_ago = time() - 10;
|
||||
|
||||
// Pretend automatic relay management just ran.
|
||||
t.set_config_internal(
|
||||
Config::LastAutomaticRelayManagement,
|
||||
Some(&some_seconds_ago.to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_disabled() {
|
||||
// By default, automatic relay management is disabled:
|
||||
let t = &TestContext::new_alice().await;
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
}
|
||||
|
||||
/// Runs maybe_add_additional_relays_inner(), then deletes one of the transports.
|
||||
/// Even after AUTOMATIC_ADDITION_DEBOUNCE_SECONDS,
|
||||
/// running automatic transport management again should not add back a transport.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_does_nothing_after_finishing_once() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let transports = t.list_transports().await?;
|
||||
t.delete_transport(&transports.last().unwrap().param.addr)
|
||||
.await?;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(
|
||||
AUTOMATIC_ADDITION_DEBOUNCE_SECONDS as u64 + 1,
|
||||
));
|
||||
|
||||
let transports_count = t.count_transports().await?;
|
||||
assert_eq!(transports_count, NUM_TRANSPORTS_TARGET - 1);
|
||||
|
||||
assert!(
|
||||
t.get_config_bool(Config::AutomaticRelayManagementFinished)
|
||||
.await?
|
||||
);
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("relay.example", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, transports_before + 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example", "d.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, NUM_TRANSPORTS_TARGET);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for i in 1..10 {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(format!("{i}.invalid.example"), 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
|
||||
// Don't skip network, since we want the relay addition to fail
|
||||
let skip_network = false;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert_eq!(relay_added, false);
|
||||
|
||||
// The config is still updated:
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, transports_before);
|
||||
|
||||
// Some of the candidates should have an updated last_tried:
|
||||
assert!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM relay_candidates WHERE last_tried>=?",
|
||||
(now,)
|
||||
)
|
||||
.await?
|
||||
);
|
||||
|
||||
// ...but not all, because there might be many relay candidates
|
||||
// and we don't want to try all of them in a single call:
|
||||
assert_eq!(load_relay_candidates(t, now).await?.is_empty(), false);
|
||||
|
||||
t.assert_warns_or_errors(&[
|
||||
"DNS lookup with memory cache failure",
|
||||
"Could not find DNS resolutions",
|
||||
])
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn enable_config(context: &Context) {
|
||||
context
|
||||
.set_config_bool(Config::AutomaticRelayManagement, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
+15
-38
@@ -1,6 +1,6 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::cmp::max;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -12,7 +12,7 @@ use futures::StreamExt;
|
||||
use image::ImageReader;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
|
||||
use num_traits::{FromPrimitive, cast};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::{fs, task};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
@@ -68,10 +68,11 @@ impl<'a> BlobObject<'a> {
|
||||
// so we are doing essentially the same here.
|
||||
task::block_in_place(|| {
|
||||
let temp_path;
|
||||
let src_in_blobdir: &Path;
|
||||
let blobdir = context.get_blobdir();
|
||||
|
||||
let src_in_blobdir = if src.starts_with(blobdir) {
|
||||
src
|
||||
if src.starts_with(blobdir) {
|
||||
src_in_blobdir = src;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
@@ -83,8 +84,8 @@ impl<'a> BlobObject<'a> {
|
||||
std::fs::create_dir_all(blobdir).log_err(context).ok();
|
||||
std::fs::copy(src, &temp_path).context("Copying new blobfile failed")?;
|
||||
};
|
||||
&temp_path
|
||||
};
|
||||
src_in_blobdir = &temp_path;
|
||||
}
|
||||
|
||||
let hash = file_hash(src_in_blobdir)?.to_hex();
|
||||
let hash = hash.as_str();
|
||||
@@ -381,9 +382,14 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit,
|
||||
// as set by `Config::MediaQuality`.
|
||||
// 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())
|
||||
};
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
@@ -422,35 +428,6 @@ impl<'a> BlobObject<'a> {
|
||||
});
|
||||
|
||||
if do_scale {
|
||||
let longest_side_len = max(img.width(), img.height());
|
||||
|
||||
// target_wh will be used as the target-resolution for resizing the image,
|
||||
// so that the longest sides of the image match the target-resolution.
|
||||
let mut target_wh = if !is_avatar {
|
||||
let area_sqrt = (f64::from(img.width()) * f64::from(img.height())).sqrt();
|
||||
// Limit resolution to the number of pixels that fit within max_wh * max_wh,
|
||||
// so that the image-quality does not depend on the aspect-ratio.
|
||||
let mut resolution_limit: u32 = cast(
|
||||
(f64::from(longest_side_len) * (f64::from(max_wh) / area_sqrt)).floor(),
|
||||
)
|
||||
.unwrap_or(max_wh);
|
||||
// Align at least one dimension of the resampled image to a multiple of 8 pixels,
|
||||
// to have fewer partially used JPEG-blocks (which represent 8x8 pixels each).
|
||||
if resolution_limit < longest_side_len && resolution_limit > 8 {
|
||||
while !resolution_limit.is_multiple_of(8) {
|
||||
resolution_limit -= 1
|
||||
}
|
||||
}
|
||||
resolution_limit
|
||||
} else {
|
||||
max_wh
|
||||
};
|
||||
|
||||
target_wh = min(target_wh, longest_side_len);
|
||||
|
||||
// For images in JPEG-format, 65535 pixels is the maximum resolution per dimension.
|
||||
target_wh = min(target_wh, 65535);
|
||||
|
||||
loop {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
@@ -572,7 +549,7 @@ fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation {
|
||||
&& let Some(val) = orientation.value.get_uint(0)
|
||||
&& let Ok(val) = TryInto::<u8>::try_into(val)
|
||||
{
|
||||
return Orientation::from_exif(val).unwrap_or_else(|| {
|
||||
return Orientation::from_exif(val).unwrap_or({
|
||||
warn!(context, "Exif orientation value ignored: {val:?}.");
|
||||
Orientation::NoTransforms
|
||||
});
|
||||
|
||||
@@ -406,8 +406,8 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 848,
|
||||
compressed_height: 477,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
@@ -497,8 +497,8 @@ async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 848,
|
||||
compressed_height: 477,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
@@ -517,8 +517,8 @@ async fn test_recode_image_huge_jpg() {
|
||||
has_exif: true,
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1704,
|
||||
compressed_height: 959,
|
||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
|
||||
+12
-18
@@ -201,7 +201,7 @@ impl Context {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Single,
|
||||
"Can only place calls in single chats"
|
||||
"Can only place calls in 1:1 chats"
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
@@ -617,7 +617,7 @@ pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
|
||||
/// ICE server for JSON serialization.
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
pub(crate) struct IceServer {
|
||||
struct IceServer {
|
||||
/// STUN or TURN URLs.
|
||||
pub urls: Vec<String>,
|
||||
|
||||
@@ -655,7 +655,7 @@ pub(crate) async fn create_ice_servers_from_metadata(
|
||||
}
|
||||
|
||||
/// STUN or TURN server with unresolved DNS name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum UnresolvedIceServer {
|
||||
/// STUN server.
|
||||
Stun { hostname: String, port: u16 },
|
||||
@@ -676,7 +676,7 @@ pub(crate) enum UnresolvedIceServer {
|
||||
pub(crate) async fn resolve_ice_servers(
|
||||
context: &Context,
|
||||
unresolved_ice_servers: Vec<UnresolvedIceServer>,
|
||||
) -> Result<Vec<IceServer>> {
|
||||
) -> Result<String> {
|
||||
let mut result: Vec<IceServer> = Vec::new();
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
@@ -733,7 +733,8 @@ pub(crate) async fn resolve_ice_servers(
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
let json = serde_json::to_string(&result)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
@@ -769,19 +770,12 @@ pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
/// because it itself cannot utilize DNS. See
|
||||
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
||||
pub async fn ice_servers(context: &Context) -> Result<String> {
|
||||
let mut unresolved_ice_servers: Vec<UnresolvedIceServer> = context
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.flat_map(|metadata| metadata.ice_servers.clone())
|
||||
.collect();
|
||||
unresolved_ice_servers.sort();
|
||||
unresolved_ice_servers.dedup();
|
||||
|
||||
let ice_servers = resolve_ice_servers(context, unresolved_ice_servers).await?;
|
||||
let json = serde_json::to_string(&ice_servers)?;
|
||||
Ok(json)
|
||||
if let Some(ref metadata) = *context.metadata.read().await {
|
||||
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
|
||||
Ok(ice_servers)
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// "Who can call me" config options.
|
||||
|
||||
@@ -679,7 +679,6 @@ async fn test_end_text_call() -> Result<()> {
|
||||
.unwrap();
|
||||
assert_eq!(received2.msg_ids.len(), 1);
|
||||
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
|
||||
alice.assert_warn("does not refer to a call message").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user