mirror of
https://github.com/chatmail/core.git
synced 2026-04-09 09:02:10 +03:00
Compare commits
7 Commits
link2xt/de
...
simon/UICh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22263953f | ||
|
|
2c3734153a | ||
|
|
7483aa532b | ||
|
|
a76a7ae781 | ||
|
|
f4caf1688b | ||
|
|
6442e13a1a | ||
|
|
a048d6b0d1 |
80
.github/workflows/ci.yml
vendored
80
.github/workflows/ci.yml
vendored
@@ -24,11 +24,9 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.78.0
|
||||
RUSTUP_TOOLCHAIN: 1.75.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
||||
- name: Cache rust cargo artifacts
|
||||
@@ -44,9 +42,7 @@ jobs:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
arguments: --all-features --workspace
|
||||
@@ -57,9 +53,7 @@ jobs:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check provider database
|
||||
run: scripts/update-provider-database.sh
|
||||
|
||||
@@ -69,9 +63,8 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
- name: Rustdoc
|
||||
@@ -83,20 +76,18 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.78.0
|
||||
- os: windows-2019
|
||||
rust: 1.78.0
|
||||
rust: 1.75.0
|
||||
- os: windows-latest
|
||||
rust: 1.75.0
|
||||
- os: macos-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.75.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
# Minimum Supported Rust Version = 1.70.0
|
||||
- os: ubuntu-latest
|
||||
rust: 1.77.0
|
||||
rust: 1.70.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust ${{ matrix.rust }}
|
||||
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||
@@ -105,20 +96,10 @@ jobs:
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --doc
|
||||
run: cargo test --workspace
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
@@ -130,9 +111,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
@@ -141,7 +120,7 @@ jobs:
|
||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
@@ -154,9 +133,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
@@ -165,7 +142,7 @@ jobs:
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
@@ -175,9 +152,8 @@ jobs:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
@@ -217,18 +193,16 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -269,12 +243,10 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -282,7 +254,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
228
.github/workflows/deltachat-rpc-server.yml
vendored
228
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -21,65 +21,69 @@ jobs:
|
||||
# Build a version statically linked against musl libc
|
||||
# to avoid problems with glibc version incompatibility.
|
||||
build_linux:
|
||||
name: Linux
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install ziglang
|
||||
run: pip install wheel ziglang==0.11.0
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
run: sh scripts/zig-rpc-server.sh
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Upload dist directory with Linux binaries
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
path: result/bin/deltachat-rpc-server
|
||||
name: linux
|
||||
path: dist/
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
name: Windows
|
||||
name: Build deltachat-rpc-server for Windows
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact: win32.exe
|
||||
path: deltachat-rpc-server.exe
|
||||
target: i686-pc-windows-msvc
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
- os: windows-latest
|
||||
artifact: win64.exe
|
||||
path: deltachat-rpc-server.exe
|
||||
target: x86_64-pc-windows-msvc
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
name: deltachat-rpc-server-${{ matrix.artifact }}
|
||||
path: target/${{ matrix.target}}/release/${{ matrix.path }}
|
||||
if-no-files-found: error
|
||||
|
||||
build_macos:
|
||||
name: macOS
|
||||
name: Build deltachat-rpc-server for macOS
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x86_64, aarch64]
|
||||
include:
|
||||
- arch: x86_64
|
||||
- arch: aarch64
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||
@@ -88,140 +92,61 @@ jobs:
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_android:
|
||||
name: Android
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Build wheels and upload binaries to the release
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/deltachat-rpc-server
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Download Linux binaries
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
name: linux
|
||||
path: dist/
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Download win32 binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
name: deltachat-rpc-server-win32.exe
|
||||
path: deltachat-rpc-server-win32.exe.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Download win64 binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
name: deltachat-rpc-server-win64.exe
|
||||
path: deltachat-rpc-server-win64.exe.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
|
||||
- name: Create bin/ directory
|
||||
- name: Flatten dist/ directory
|
||||
run: |
|
||||
mkdir -p bin
|
||||
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
|
||||
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
|
||||
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
|
||||
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
|
||||
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
|
||||
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
|
||||
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
|
||||
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
|
||||
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
|
||||
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
|
||||
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
|
||||
|
||||
- name: List binaries
|
||||
run: ls -l bin/
|
||||
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
|
||||
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
|
||||
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
|
||||
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -229,40 +154,15 @@ jobs:
|
||||
run: pip install wheel
|
||||
|
||||
- name: Build deltachat-rpc-server Python wheels and source package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-i686-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win64-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win32-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-source
|
||||
cp result/*.tar.gz dist/
|
||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||
mv *.whl dist/
|
||||
run: scripts/wheel-rpc-server.py
|
||||
|
||||
- name: List artifacts
|
||||
- name: List downloaded artifacts
|
||||
run: ls -l dist/
|
||||
|
||||
- name: Upload binaries to the GitHub release
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} \
|
||||
--repo ${{ github.repository }} \
|
||||
bin/* dist/*
|
||||
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
dist/*
|
||||
|
||||
@@ -13,10 +13,9 @@ jobs:
|
||||
steps:
|
||||
- name: Install tree
|
||||
run: sudo apt install tree
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Get tag
|
||||
@@ -50,7 +49,7 @@ jobs:
|
||||
ls -lah
|
||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-jsonrpc-client.tgz
|
||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
|
||||
6
.github/workflows/jsonrpc.yml
vendored
6
.github/workflows/jsonrpc.yml
vendored
@@ -14,11 +14,9 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
|
||||
6
.github/workflows/node-docs.yml
vendored
6
.github/workflows/node-docs.yml
vendored
@@ -14,12 +14,10 @@ jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
|
||||
49
.github/workflows/node-package.yml
vendored
49
.github/workflows/node-package.yml
vendored
@@ -14,10 +14,9 @@ jobs:
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: System info
|
||||
@@ -29,7 +28,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -37,7 +36,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
@@ -57,7 +56,7 @@ jobs:
|
||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: node/${{ matrix.os }}.tar.gz
|
||||
@@ -75,10 +74,9 @@ jobs:
|
||||
- name: Change working directory owner
|
||||
run: chown root:root .
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- run: apt-get update
|
||||
@@ -99,7 +97,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -107,7 +105,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
@@ -127,7 +125,7 @@ jobs:
|
||||
tar -zcvf "linux.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: linux
|
||||
path: node/linux.tar.gz
|
||||
@@ -139,10 +137,9 @@ jobs:
|
||||
steps:
|
||||
- name: Install tree
|
||||
run: sudo apt install tree
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Get tag
|
||||
@@ -168,25 +165,25 @@ jobs:
|
||||
node --version
|
||||
echo $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Download Linux prebuild
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: linux
|
||||
- name: Download macOS prebuild
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: macos-latest
|
||||
- name: Download Windows prebuild
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: windows-latest
|
||||
- shell: bash
|
||||
run: |
|
||||
mkdir node/prebuilds
|
||||
tar -xvzf linux.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf linux/linux.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||
tree node/prebuilds
|
||||
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
|
||||
rm -rf linux macos-latest windows-latest
|
||||
- name: Install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
@@ -204,7 +201,7 @@ jobs:
|
||||
ls -lah
|
||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Upload prebuild
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-node.tgz
|
||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||
|
||||
11
.github/workflows/node-tests.yml
vendored
11
.github/workflows/node-tests.yml
vendored
@@ -23,10 +23,9 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: System info
|
||||
@@ -38,7 +37,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -46,7 +45,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Publish deltachat-rpc-client to PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build distribution
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Install pypa/build
|
||||
run: python3 -m pip install build
|
||||
- name: Build a binary wheel and a source tarball
|
||||
working-directory: deltachat-rpc-client
|
||||
run: python3 -m build
|
||||
- name: Store the distribution packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: deltachat-rpc-client/dist/
|
||||
|
||||
publish-to-pypi:
|
||||
name: Publish Python distribution to PyPI
|
||||
if: github.event_name == 'release'
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/deltachat-rpc-client
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
16
.github/workflows/repl.yml
vendored
16
.github/workflows/repl.yml
vendored
@@ -10,17 +10,15 @@ on:
|
||||
jobs:
|
||||
build_repl:
|
||||
name: Build REPL example
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
run: cargo build -p deltachat-repl --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: repl.exe
|
||||
path: "result/bin/deltachat-repl.exe"
|
||||
path: "target/debug/deltachat-repl.exe"
|
||||
|
||||
87
.github/workflows/upload-docs.yml
vendored
87
.github/workflows/upload-docs.yml
vendored
@@ -1,91 +1,24 @@
|
||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||
name: Build & Deploy Documentation on rs.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- build_jsonrpc_docs_ci
|
||||
|
||||
jobs:
|
||||
build-rs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat --no-deps --document-private-items
|
||||
- name: Upload to rs.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
||||
|
||||
build-python:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
|
||||
|
||||
build-c:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||
|
||||
build-ts:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
run: npm install
|
||||
- name: npm run build
|
||||
run: npm run build
|
||||
- name: Run docs script
|
||||
run: npm run docs
|
||||
- name: Upload to js.jsonrpc.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: ${{ secrets.USERNAME }}
|
||||
KEY: ${{ secrets.KEY }}
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/rs/"
|
||||
|
||||
18
.github/workflows/upload-ffi-docs.yml
vendored
18
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat_ffi` crate documentation
|
||||
# to build `deltachat_fii` crate documentation
|
||||
# and upload it to <https://cffi.delta.chat/>
|
||||
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
@@ -14,15 +14,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: ${{ secrets.USERNAME }}
|
||||
KEY: ${{ secrets.KEY }}
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/cffi/"
|
||||
|
||||
25
.github/workflows/upload-python-docs.yml
vendored
Normal file
25
.github/workflows/upload-python-docs.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Build & Deploy Documentation on py.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Build Python documentation
|
||||
run: scripts/build-python-docs.sh
|
||||
- name: Upload to py.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: delta
|
||||
KEY: ${{ secrets.CODESPEAK_KEY }}
|
||||
HOST: "lists.codespeak.net"
|
||||
SOURCE: "dist/html/"
|
||||
TARGET: "/home/delta/build/master"
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -44,10 +44,3 @@ node/build/
|
||||
node/dist/
|
||||
node/prebuilds/
|
||||
node/.nyc_output/
|
||||
|
||||
# Nix symlink.
|
||||
result
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
626
CHANGELOG.md
626
CHANGELOG.md
@@ -1,613 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.137.4] - 2024-04-24
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `Stream` implementation for `EventEmitter`.
|
||||
- Experimental Webxdc Integration API, Maps Integration ([#5461](https://github.com/deltachat/deltachat-core-rust/pull/5461)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add progressive backoff for failing IMAP connection attempts ([#5443](https://github.com/deltachat/deltachat-core-rust/pull/5443)).
|
||||
- Replace event channel with broadcast channel.
|
||||
- Mark contact request messages as seen on IMAP.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Convert images to RGB8 (without alpha) before encoding into JPEG to fix sending of large RGBA images.
|
||||
- Don't set `is_bot` for webxdc status updates ([#5445](https://github.com/deltachat/deltachat-core-rust/pull/5445)).
|
||||
- Do not fail if Autocrypt Setup Message has no encryption preference to fix key transfer from K-9 Mail to Delta Chat.
|
||||
- Use only CRLF in Autocrypt Setup Message.
|
||||
- python: Use cached message object if `dc_get_msg()` returns `NULL`.
|
||||
- python: `Message::is_outgoing`: Don't reload message from db.
|
||||
- python: `_map_ffi_event`: Always check if `get_message_by_id()` returned None.
|
||||
- node: Undefine `NAPI_EXPERIMENTAL` to fix build with new clang.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Add `imap-tools` as `deltachat-rpc-client` dependency.
|
||||
- nix: Add `./deltachat-contact-tools` to sources.
|
||||
- nix: Update nix flake.
|
||||
- deps: Update rustls to 0.21.11.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update references to SecureJoin protocols.
|
||||
- Fix broken references in documentation comments.
|
||||
|
||||
### Refactor
|
||||
|
||||
- imap: remove `RwLock` from `ratelimit`.
|
||||
- deltachat-ffi: Remove unused `ResultNullableExt`.
|
||||
- Remove duplicate clippy exceptions.
|
||||
- Group `use` at the top of the test modules.
|
||||
|
||||
## [1.137.3] - 2024-04-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove reactions ffi; all implementations use jsonrpc.
|
||||
- Don't load trashed messages with `Message::load_from_db`.
|
||||
- Add `ChatListChanged` and `ChatListItemChanged` events ([#4476](https://github.com/deltachat/deltachat-core-rust/pull/4476)).
|
||||
- deltachat-rpc-client: Add `check_qr` and `set_config_from_qr` APIs.
|
||||
- deltachat-rpc-client: Add `Account.create_chat()`.
|
||||
- deltachat-rpc-client: Add `Message.wait_until_delivered()`.
|
||||
- deltachat-rpc-client: Add `Chat.send_file()`.
|
||||
- deltachat-rpc-client: Add `Account.wait_for_reactions_changed()`.
|
||||
- deltachat-rpc-client: Return Message from `Message.send_reaction()`.
|
||||
- deltachat-rpc-client: Add `Account.bring_online()`.
|
||||
- deltachat-rpc-client: Add `ACFactory.get_accepted_chat()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Port `direct_imap.py` into deltachat-rpc-client.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not emit `MSGS_CHANGED` event for outgoing hidden messages.
|
||||
- `Message::get_summary()` must not return reaction summary.
|
||||
- Fix emitting `ContactsChanged` events on "recently seen" status change ([#5377](https://github.com/deltachat/deltachat-core-rust/pull/5377)).
|
||||
- deltachat-jsonrpc: block in `inner_get_backup_qr`.
|
||||
- Add tolerance to `MemberListTimestamp` ([#5366](https://github.com/deltachat/deltachat-core-rust/pull/5366)).
|
||||
- Keep webxdc instance for `delete_device_after` period after a status update ([#5365](https://github.com/deltachat/deltachat-core-rust/pull/5365)).
|
||||
- Don't try to do `fetch_move_delete()` if Trash is needed but not yet configured.
|
||||
- Assign messages to chats based on not fully downloaded references.
|
||||
- Do not create ad-hoc groups from partial downloads.
|
||||
- deltachat-rpc-client: construct Thread with `target` keyword argument.
|
||||
- Format error context in `Message::load_from_db`.
|
||||
|
||||
### Build system
|
||||
|
||||
- cmake: adapt target install path if env var `CARGO_BUILD_TARGET` is set.
|
||||
- nix: Use stable Rust in flake.nix devshell.
|
||||
|
||||
### CI
|
||||
|
||||
- Use cargo-nextest instead of cargo-test.
|
||||
- Run doc tests with cargo test --workspace --doc ([#5459](https://github.com/deltachat/deltachat-core-rust/pull/5459)).
|
||||
- Typos in CI files ([#5453](https://github.com/deltachat/deltachat-core-rust/pull/5453)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add <https://deps.rs> badge.
|
||||
- Add 'Ubuntu Touch' to the list of 'frontend projects'
|
||||
|
||||
### Refactor
|
||||
|
||||
- Do not ignore `Contact::get_by_id` errors in `get_encrinfo`.
|
||||
- deltachat-rpc-client: Use `list`, `set` and `tuple` instead of `typing`.
|
||||
- Use `clone_from()` ([#5451](https://github.com/deltachat/deltachat-core-rust/pull/5451)).
|
||||
- Do not check for `is_trash()` in `get_last_reaction_if_newer_than()`.
|
||||
- Split off functional contact tools into its own crate ([#5444](https://github.com/deltachat/deltachat-core-rust/pull/5444))
|
||||
- Fix nightly clippy warnings.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test withdrawing group join QR codes.
|
||||
- `display_chat()`: Don't add day markers.
|
||||
- Move reaction tests to JSON-RPC.
|
||||
- node: Increase 'static tests' timeout to 5 minutes.
|
||||
|
||||
## [1.137.2] - 2024-04-05
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Increase Minimum Supported Rust Version to 1.77.0.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Show reactions in summaries ([#5387](https://github.com/deltachat/deltachat-core-rust/pull/5387)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Test reactions for forwarded messages
|
||||
|
||||
### Refactor
|
||||
|
||||
- `is_probably_private_reply`: Remove reaction-specific code.
|
||||
- Use Rust 1.77.0 support for recursion in async functions.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump rustyline from 13.0.0 to 14.0.0.
|
||||
- Update chrono from 0.4.34 to 0.4.37.
|
||||
- Update from brotli 3.4.0 to brotli 4.0.0.
|
||||
- Upgrade `h2` from 0.4.3 to 0.4.4.
|
||||
- Upgrade `image` from 0.24.9 to 0.25.1.
|
||||
- cargo: Bump fast-socks5 from 0.9.5 to 0.9.6.
|
||||
|
||||
## [1.137.1] - 2024-04-03
|
||||
|
||||
### CI
|
||||
|
||||
- Remove android builds for `x86` and `x86_64`.
|
||||
|
||||
## [1.137.0] - 2024-04-02
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove data from `DC_EVENT_INCOMING_MSG_BUNCH`.
|
||||
- [**breaking**] Remove unused `dc_accounts_all_work_done()` ([#5384](https://github.com/deltachat/deltachat-core-rust/pull/5384)).
|
||||
- deltachat-rpc-client: Add futures.
|
||||
|
||||
### Build system
|
||||
|
||||
- cmake: Build outside the source tree.
|
||||
- nix: Add outputs for Android binaries.
|
||||
- Add `repository` to Cargo.toml.
|
||||
- python: Remove `setuptools_scm` dependency.
|
||||
- Add development shell ([#5390](https://github.com/deltachat/deltachat-core-rust/pull/5390)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Rust 1.77.0.
|
||||
- Build deltachat-rpc-server for Android.
|
||||
- Shorter names for deltachat-rpc-server jobs.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not include provider hostname in `Message-ID`.
|
||||
- Include 3 recent Message-IDs in `References` header.
|
||||
- Include more entries into DNS fallback cache.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Preserve upper-/lowercase of links parsed by `dehtml()` ([#5362](https://github.com/deltachat/deltachat-core-rust/pull/5362)).
|
||||
- Rescan folders after changing `Config::SentboxWatch`.
|
||||
- Do not ignore `Contact::get_by_id()` error in `from_field_to_contact_id()`.
|
||||
- Put overridden sender name into message info.
|
||||
- Don't send selfavatar in `SecureJoin` messages before contact verification ([#5354](https://github.com/deltachat/deltachat-core-rust/pull/5354)).
|
||||
- Always set correct `chat_id` for `DC_EVENT_REACTIONS_CHANGED` ([#5419](https://github.com/deltachat/deltachat-core-rust/pull/5419)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `MessageObject::from_message_id()`.
|
||||
- jsonrpc: Add `msg_id` and `account_id` to `get_message()` errors.
|
||||
- Cleanup `jobs` and `Params` relicts.
|
||||
|
||||
### Tests
|
||||
|
||||
- `Test_mvbox_sentbox_threads`: Check that sentbox gets configured after setting `sentbox_watch` ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
|
||||
- Remove flaky time check from `test_list_from()`.
|
||||
- Add failing test for #5418 (wrong `DC_EVENT_REACTIONS_CHANGED`)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Add `result` to .gitignore.
|
||||
- cargo: Bump thiserror from 1.0.57 to 1.0.58.
|
||||
- cargo: Bump tokio from 1.36.0 to 1.37.0.
|
||||
- cargo: Bump pin-project from 1.1.4 to 1.1.5.
|
||||
- cargo: Bump strum from 0.26.1 to 0.26.2.
|
||||
- cargo: Bump uuid from 1.7.0 to 1.8.0.
|
||||
- cargo: Bump toml from 0.8.10 to 0.8.12.
|
||||
- cargo: Bump tokio-stream from 0.1.14 to 0.1.15.
|
||||
- cargo: Bump smallvec from 1.13.1 to 1.13.2.
|
||||
- cargo: Bump async-smtp from 0.9.0 to 0.9.1.
|
||||
- cargo: Bump strum_macros from 0.26.1 to 0.26.2.
|
||||
- cargo: Bump serde_json from 1.0.114 to 1.0.115.
|
||||
- cargo: Bump anyhow from 1.0.80 to 1.0.81.
|
||||
- cargo: Bump syn from 2.0.52 to 2.0.57.
|
||||
- cargo: Bump futures-lite from 2.2.0 to 2.3.0.
|
||||
- cargo: Bump axum from 0.7.4 to 0.7.5.
|
||||
- cargo: Bump reqwest from 0.11.24 to 0.12.2.
|
||||
- cargo: Bump backtrace from 0.3.69 to 0.3.71.
|
||||
- cargo: Bump regex from 1.10.3 to 1.10.4.
|
||||
- cargo: Update aho-corasick from 1.1.2 to 1.1.3.
|
||||
- Update deny.toml.
|
||||
|
||||
## [1.136.6] - 2024-03-19
|
||||
|
||||
### Build system
|
||||
|
||||
- Add description to deltachat-rpc-server wheels.
|
||||
- Read version from Cargo.toml in wheel-rpc-server.py.
|
||||
|
||||
### CI
|
||||
|
||||
- Update actions/cache from v3 to v4.
|
||||
- Automate publishing of deltachat-rpc-server to PyPI.
|
||||
|
||||
### Documentation
|
||||
|
||||
- deltachat-rpc-server: Update deltachat-rpc-client URL.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Nix flake update.
|
||||
|
||||
## [1.136.5] - 2024-03-18
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Nicer summaries: prefer emoji over names
|
||||
- Add `save_mime_headers` to debug info ([#5350](https://github.com/deltachat/deltachat-core-rust/pull/5350))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Terminate ephemeral and location loop immediately on channel close.
|
||||
- Update MemberListTimestamp when sending a group message.
|
||||
- On iOS, use FILE (default) instead of MEMORY ([#5349](https://github.com/deltachat/deltachat-core-rust/pull/5349)).
|
||||
- Add white background to recoded avatars ([#3787](https://github.com/deltachat/deltachat-core-rust/pull/3787)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Add README to deltachat-rpc-client Python packages.
|
||||
|
||||
### Documentation
|
||||
|
||||
- deltachat-rpc-client: Document that 0 is a special value of `set_ephemeral_timer()`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test that reordering of Member added message results in square bracket error.
|
||||
|
||||
## [1.136.4] - 2024-03-11
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Make .#libdeltachat buildable on macOS.
|
||||
- Build deltachat-rpc-server wheels with nix.
|
||||
|
||||
### CI
|
||||
|
||||
- Add workflow for automatic publishing of deltachat-rpc-client.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove duplicate CHANGELOG entries for 1.135.1.
|
||||
|
||||
## [1.136.3] - 2024-03-09
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Start IMAP loop for sentbox only if it is configured ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove leading whitespace from Subject ([#5106](https://github.com/deltachat/deltachat-core-rust/pull/5106)).
|
||||
- Create new Peerstate for unencrypted message with already known Autocrypt key, but a new address.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Cleanup cross-compilation code.
|
||||
- nix: Include SystemConfiguration framework on darwin systems.
|
||||
|
||||
### CI
|
||||
|
||||
- Wait for `build_windows` task before trying to publish it.
|
||||
- Remove artifacts from npm package.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Don't parse Autocrypt header for outgoing messages ([#5259](https://github.com/deltachat/deltachat-core-rust/pull/5259)).
|
||||
- Remove `deduplicate_peerstates()`.
|
||||
- Fix 2024-03-05 nightly clippy warnings.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump mio from 0.8.8 to 0.8.11 in /fuzz.
|
||||
- RPC client: Add missing constants ([#5110](https://github.com/deltachat/deltachat-core-rust/pull/5110)).
|
||||
|
||||
## [1.136.2] - 2024-03-05
|
||||
|
||||
### Build system
|
||||
|
||||
- Downgrade `cc` to 1.0.83 to fix build for Android.
|
||||
|
||||
### CI
|
||||
|
||||
- Update setup-node action.
|
||||
|
||||
## [1.136.1] - 2024-03-05
|
||||
|
||||
### Build system
|
||||
|
||||
- Revert to OpenSSL 3.1.
|
||||
- Restore MSRV 1.70.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update node constants.
|
||||
|
||||
## [1.136.0] - 2024-03-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Recognise Trash folder by name ([#5275](https://github.com/deltachat/deltachat-core-rust/pull/5275)).
|
||||
- Send Chat-Group-Avatar as inline base64 ([#5253](https://github.com/deltachat/deltachat-core-rust/pull/5253)).
|
||||
- Self-Reporting: Report number of protected/encrypted/unencrypted chats ([#5292](https://github.com/deltachat/deltachat-core-rust/pull/5292)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't send sync messages on self-{status,avatar} update from self-sent messages ([#5289](https://github.com/deltachat/deltachat-core-rust/pull/5289)).
|
||||
- imap: Allow `maybe_network` to interrupt connection ratelimit.
|
||||
- imap: Set connectivity to "connecting" only after ratelimit.
|
||||
- Remove `Group-ID` from `Message-ID`.
|
||||
- Prioritize protected `Message-ID` over `X-Microsoft-Original-Message-ID`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Make `store_self_keypair` private.
|
||||
- Add `ContextBuilder.build()` to build Context without opening.
|
||||
- `dc_accounts_set_push_device_token` and `dc_get_push_state` APIs for iOS push notifications.
|
||||
|
||||
### Build system
|
||||
|
||||
- Tag armv6 wheels with tags accepted by PyPI.
|
||||
- Unpin OpenSSL.
|
||||
- Remove deprecated `unmaintained` field from deny.toml.
|
||||
- Do not vendor OpenSSL when cross-compiling ([#5316](https://github.com/deltachat/deltachat-core-rust/pull/5316)).
|
||||
- Increase MSRV to 1.74.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Upgrade setup-python GitHub Action.
|
||||
- Update to Rust 1.76 and fix clippy warnings.
|
||||
- Build Python docs with Nix.
|
||||
- Upload python docs without GH actions.
|
||||
- Upload cffi docs without GH actions.
|
||||
- Build c.delta.chat docs with nix.
|
||||
|
||||
### Other
|
||||
|
||||
- refactor: move more methods from Imap into Session.
|
||||
- Add deltachat-time to sources.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove Session from Imap structure.
|
||||
- Merge ImapConfig into Imap.
|
||||
- Get rid of ImapActionResult.
|
||||
- Build contexts using ContextBuilder.
|
||||
- Do not send `Secure-Join-Group` in `vg-request`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix `test_verified_oneonone_chat_broken_by_device_change()` ([#5280](https://github.com/deltachat/deltachat-core-rust/pull/5280)).
|
||||
- `get_protected_chat()`: Use FFIEventTracker instead of `dc_wait_next_msgs()` ([#5207](https://github.com/deltachat/deltachat-core-rust/pull/5207)).
|
||||
- Fixup `tests/test_3_offline.py::TestOfflineAccountBasic::test_wrong_db`.
|
||||
- Fix pytest compat ([#5317](https://github.com/deltachat/deltachat-core-rust/pull/5317)).
|
||||
|
||||
## [1.135.1] - 2024-02-20
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Sync self-avatar across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
|
||||
- Sync Config::Selfstatus across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
|
||||
- Remove webxdc sending limit.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Never encrypt `{vc,vg}-request` SecureJoin messages.
|
||||
- Apply Autocrypt headers if timestamp is unchanged.
|
||||
- `Context::get_info`: Report displayname as "displayname" (w/o underscore).
|
||||
|
||||
### Tests
|
||||
|
||||
- Mock `SystemTime::now()` for the tests.
|
||||
- Add a test on protection message sort timestamp ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Add flake.nix.
|
||||
- Add footer template for git-cliff.
|
||||
|
||||
### CI
|
||||
|
||||
- Update GitHub Actions `actions/upload-artifact`, `actions/download-artifact`, `actions/checkout`.
|
||||
- Build deltachat-repl for Windows with nix.
|
||||
- Build deltachat-rpc-server with nix.
|
||||
- Try to upload deltachat-rpc-server only on release.
|
||||
- Fixup node-package.yml after artifact actions upgrade.
|
||||
- Update to actions/checkout@v4.
|
||||
- Replace download-artifact v1 with v4.
|
||||
|
||||
### Refactor
|
||||
|
||||
- `create_keypair`: Remove unnecessary `map_err`.
|
||||
- Return error with a cause when failing to export keys.
|
||||
- Rename incorrectly named variables in `create_keypair`.
|
||||
|
||||
## [1.135.0] - 2024-02-13
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add wildcard pattern support to provider database.
|
||||
- Add device message about outgoing undecryptable messages ([#5164](https://github.com/deltachat/deltachat-core-rust/pull/5164)).
|
||||
- Context::set_config(): Restart IO scheduler if needed ([#5111](https://github.com/deltachat/deltachat-core-rust/pull/5111)).
|
||||
- Server_sent_unsolicited_exists(): Log folder name.
|
||||
- Cache system time instead of looking at the clock several times in a row.
|
||||
- Basic self-reporting ([#5129](https://github.com/deltachat/deltachat-core-rust/pull/5129)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Dehtml: Don't just truncate text when trying to decode ([#5223](https://github.com/deltachat/deltachat-core-rust/pull/5223)).
|
||||
- Mark the gossip keys from the message as verified, not the ones from the db ([#5247](https://github.com/deltachat/deltachat-core-rust/pull/5247)).
|
||||
- Guarantee immediate message deletion if delete_server_after == 0 ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)).
|
||||
- Never allow a message timestamp to be a lot in the future ([#5249](https://github.com/deltachat/deltachat-core-rust/pull/5249)).
|
||||
- Imap::configure_mvbox: Do select_with_uidvalidity() before return.
|
||||
- ImapSession::select_or_create_folder(): Don't fail if folder is created in parallel.
|
||||
- Emit ConfigSynced event on the second device.
|
||||
- Create mvbox on setting mvbox_move.
|
||||
- Use SystemTime instead of Instant everywhere.
|
||||
- Restore database rows removed in previous release; this ensures compatibility when adding second device or importing backup and not all devices run the new core ([#5254](https://github.com/deltachat/deltachat-core-rust/pull/5254))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump image from 0.24.7 to 0.24.8.
|
||||
- cargo: Bump chrono from 0.4.31 to 0.4.33.
|
||||
- cargo: Bump futures-lite from 2.1.0 to 2.2.0.
|
||||
- cargo: Bump pin-project from 1.1.3 to 1.1.4.
|
||||
- cargo: Bump iana-time-zone from yanked 0.1.59 to 0.1.60.
|
||||
- cargo: Bump smallvec from 1.11.2 to 1.13.1.
|
||||
- cargo: Bump base64 from 0.21.5 to 0.21.7.
|
||||
- cargo: Bump regex from 1.10.2 to 1.10.3.
|
||||
- cargo: Bump libc from 0.2.151 to 0.2.153.
|
||||
- cargo: Bump reqwest from 0.11.23 to 0.11.24.
|
||||
- cargo: Bump axum from 0.7.3 to 0.7.4.
|
||||
- cargo: Bump uuid from 1.6.1 to 1.7.0.
|
||||
- cargo: Bump fast-socks5 from 0.9.2 to 0.9.5.
|
||||
- cargo: Bump serde_json from 1.0.111 to 1.0.113.
|
||||
- cargo: Bump syn from 2.0.46 to 2.0.48.
|
||||
- cargo: Bump serde from 1.0.194 to 1.0.196.
|
||||
- cargo: Bump toml from 0.8.8 to 0.8.10.
|
||||
- cargo: Update to strum 0.26.
|
||||
- Cargo update.
|
||||
- scripts: Do not install deltachat-rpc-client twice.
|
||||
|
||||
### Other
|
||||
|
||||
- Update welcome image, thanks @paulaluap
|
||||
- Merge pull request #5243 from deltachat/dependabot/cargo/pin-project-1.1.4
|
||||
- Merge pull request #5241 from deltachat/dependabot/cargo/futures-lite-2.2.0
|
||||
- Merge pull request #5236 from deltachat/dependabot/cargo/chrono-0.4.33
|
||||
- Merge pull request #5235 from deltachat/dependabot/cargo/image-0.24.8
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Resultify token::exists.
|
||||
|
||||
### Tests
|
||||
|
||||
- Delete_server_after="1" should cause immediate message deletion ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)).
|
||||
|
||||
## [1.134.0] - 2024-01-31
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] JSON-RPC: device message api now requires `Option<MessageData>` instead of `String` for the message ([#5211](https://github.com/deltachat/deltachat-core-rust/pull/5211)).
|
||||
- CFFI: add `dc_accounts_background_fetch` and event `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE`.
|
||||
- JSON-RPC: add `accounts_background_fetch`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- `Qr::check_qr()`: Accept i.delta.chat invite links ([#5217](https://github.com/deltachat/deltachat-core-rust/pull/5217)).
|
||||
- Add support for IMAP METADATA, fetching `/shared/comment` and `/shared/admin` and displaying it in account info.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Add tolerance for macOS and iOS changing `#` to `%23`.
|
||||
- Do not drop unknown report attachments, such as TLS reports.
|
||||
- Treat only "Auto-Submitted: auto-generated" messages as bot-sent ([#5213](https://github.com/deltachat/deltachat-core-rust/pull/5213)).
|
||||
- `Chat::resend_msgs`: Guarantee strictly increasing time in the `Date` header.
|
||||
- Delete resent messages on receiver side ([#5155](https://github.com/deltachat/deltachat-core-rust/pull/5155)).
|
||||
- Fix iOS build issue.
|
||||
|
||||
### CI
|
||||
|
||||
- Add/remove necessary newlines to fix Python lint.
|
||||
|
||||
### Tests
|
||||
|
||||
- `test_import_export_online_all`: Send the message to the existing address to avoid errors ([#5220](https://github.com/deltachat/deltachat-core-rust/pull/5220)).
|
||||
|
||||
## [1.133.2] - 2024-01-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Downgrade OpenSSL from 3.2.0 to 3.1.4 ([#5206](https://github.com/deltachat/deltachat-core-rust/issues/5206))
|
||||
- No new chats for MDNs with alias ([#5196](https://github.com/deltachat/deltachat-core-rust/issues/5196)) ([#5199](https://github.com/deltachat/deltachat-core-rust/pull/5199)).
|
||||
|
||||
## [1.133.1] - 2024-01-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `is_bot` to cffi and jsonrpc ([#5197](https://github.com/deltachat/deltachat-core-rust/pull/5197)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add system message when provider does not allow unencrypted messages ([#5195](https://github.com/deltachat/deltachat-core-rust/pull/5195)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `Chat::send_msg`: Remove encryption-related params from already sent message. This allows to send received encrypted `dc_msg_t` object to unencrypted chat, e.g. in a Python bot.
|
||||
- Set message download state to Failure on IMAP errors. This avoids partially downloaded messages getting stuck in "Downloading..." state without actually being in a download queue.
|
||||
- BCC-to-self even if server deletion is set to "at once". This is a workaround for SMTP servers which do not return response in time, BCC-self works as a confirmation that message was sent out successfully and does not need more retries.
|
||||
- node: Run tests with native ESM modules instead of `esm` ([#5194](https://github.com/deltachat/deltachat-core-rust/pull/5194)).
|
||||
- Use Quoted-Printable MIME encoding for the text part ([#3986](https://github.com/deltachat/deltachat-core-rust/pull/3986)).
|
||||
|
||||
### Tests
|
||||
|
||||
- python: Add `get_protected_chat` to testplugin.py.
|
||||
|
||||
## [1.133.0] - 2024-01-14
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Securejoin protocol implementation refinements
|
||||
- Track forward and backward verification separately ([#5089](https://github.com/deltachat/deltachat-core-rust/pull/5089)) to avoid inconsistent states.
|
||||
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
|
||||
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
|
||||
- deltachat-repl: Enable INFO logging by default and add timestamps.
|
||||
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
|
||||
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
|
||||
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
|
||||
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Use released version of iroh 0.4.2 for "setup second device" feature.
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Rust 1.75.0.
|
||||
- Downgrade `chai` from 4.4.0 to 4.3.10.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add a link <https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html> to autoconfig RFC draft.
|
||||
- Update securejoin link in `standards.md` from <https://countermitm.readthedocs.io/> to <https://securejoin.readthedocs.io>.
|
||||
- Restore "Constants" page in Doxygen >=1.9.8
|
||||
|
||||
### Fixes
|
||||
|
||||
- imap: Limit the rate of LOGIN attempts rather than connection attempts. This is to avoid having to wait for rate limiter right after switching from a bad or offline network to a working network while still guarding against reconnection loop.
|
||||
- Do not ignore `peerstate.save_to_db()` errors.
|
||||
- securejoin: Mark 1:1s as protected regardless of the Config::VerifiedOneOnOneChats.
|
||||
- Delete received outgoing messages from SMTP queue ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
|
||||
- imap: Fail fast on `LIST` errors to avoid busy loop when connection is lost.
|
||||
- Split SMTP jobs already in `chat::create_send_msg_jobs()` ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
|
||||
- Do not remove contents from unencrypted [Schleuder](https://schleuder.org/) mailing lists messages.
|
||||
- Reset message error when scheduling resending ([#5119](https://github.com/deltachat/deltachat-core-rust/pull/5119)).
|
||||
- Emit events more reliably when starting and stopping I/O ([#5101](https://github.com/deltachat/deltachat-core-rust/pull/5101)).
|
||||
- Fix timestamp of chat protection info message for correct message ordering after restoring a backup ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- sql: Recreate `config` table with UNIQUE constraint.
|
||||
- sql: Recreate `keypairs` table to remove unused `addr` and `created` fields and move `is_default` flag to `config` table.
|
||||
- Send `Secure-Join-Fingerprint` only in `*-request-with-auth`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test joining non-protected group.
|
||||
- Test that read receipts don't degrade encryption.
|
||||
- Test that changing default private key breaks backward verification.
|
||||
- Test recovery from lost vc-contact-confirm.
|
||||
- Use `wait_for_incoming_msg_event()` more.
|
||||
|
||||
## [1.132.1] - 2023-12-12
|
||||
|
||||
### Features / Changes
|
||||
@@ -3965,21 +3357,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.131.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.8...v1.131.9
|
||||
[1.132.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.9...v1.132.0
|
||||
[1.132.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.0...v1.132.1
|
||||
[1.133.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.1...v1.133.0
|
||||
[1.133.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.0...v1.133.1
|
||||
[1.133.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.1...v1.133.2
|
||||
[1.134.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.2...v1.134.0
|
||||
[1.135.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.134.0...v1.135.0
|
||||
[1.135.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.0...v1.135.1
|
||||
[1.136.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.1...v1.136.0
|
||||
[1.136.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.0...v1.136.1
|
||||
[1.136.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.1...v1.136.2
|
||||
[1.136.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.2...v1.136.3
|
||||
[1.136.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.3...v1.136.4
|
||||
[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5
|
||||
[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6
|
||||
[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0
|
||||
[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1
|
||||
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2
|
||||
[1.137.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.2...v1.137.3
|
||||
[1.137.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.3...v1.137.4
|
||||
|
||||
@@ -12,22 +12,26 @@ else()
|
||||
set(DYNAMIC_EXT "dll")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||
else()
|
||||
set(ARCH_DIR "./")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||
${CARGO} build --release --no-default-features --features jsonrpc
|
||||
|
||||
# Build in `deltachat-ffi` directory instead of using
|
||||
# `--package deltachat_ffi` to avoid feature resolver version
|
||||
# "1" bug which makes `--no-default-features` affect only
|
||||
# `deltachat`, but not `deltachat-ffi` package.
|
||||
#
|
||||
# We can't enable version "2" resolver [1] because it is not
|
||||
# stable yet on rust 1.50.0.
|
||||
#
|
||||
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
@@ -35,12 +39,12 @@ add_custom_target(
|
||||
lib_deltachat
|
||||
ALL
|
||||
DEPENDS
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
1250
Cargo.lock
generated
1250
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
76
Cargo.toml
76
Cargo.toml
@@ -1,10 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.137.4"
|
||||
version = "1.132.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
rust-version = "1.70"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -31,96 +30,88 @@ opt-level = "z"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[patch.crates-io]
|
||||
imap-proto = { git = "https://github.com/djc/tokio-imap.git", rev = "01ff256a7e42a9f7d2732706f8b71a16ce93427e" }
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.0"
|
||||
async-channel = "2.2.1"
|
||||
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||
anyhow = "1"
|
||||
async-channel = "2.0.0"
|
||||
async-imap = { version = "0.9.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
brotli = { version = "5", default-features=false, features = ["std"] }
|
||||
chrono = { workspace = true }
|
||||
base64 = "0.21"
|
||||
brotli = { version = "3.4", default-features=false, features = ["std"] }
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.9"
|
||||
fd-lock = "4"
|
||||
futures = "0.3"
|
||||
futures-lite = "2.3.0"
|
||||
futures-lite = "2.0.0"
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
humansize = "2"
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh = { version = "0.4.2", default-features = false }
|
||||
kamadak-exif = "0.5.3"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
mailparse = "0.15"
|
||||
mailparse = "0.14"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
once_cell = { workspace = true }
|
||||
once_cell = "1.18.0"
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.11", default-features = false }
|
||||
pgp = { version = "0.10", default-features = false }
|
||||
pin-project = "1"
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quoted_printable = "0.5"
|
||||
rand = "0.8"
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.12.2", features = ["json"] }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
regex = "1.9"
|
||||
reqwest = { version = "0.11.23", features = ["json"] }
|
||||
rusqlite = { version = "0.30", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
smallvec = "1"
|
||||
strum = "0.25"
|
||||
strum_macros = "0.25"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.1"
|
||||
textwrap = "0.16.0"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
# Pin OpenSSL to 3.1 releases.
|
||||
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
|
||||
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
|
||||
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
|
||||
# According to <https://www.openssl.org/policies/releasestrat.html>
|
||||
# 3.1 branch will be supported until 2025-03-14.
|
||||
openssl-src = "~300.1"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "2.3.0"
|
||||
futures-lite = "2.0.0"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
testdir = "0.9.0"
|
||||
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[workspace]
|
||||
@@ -131,9 +122,7 @@ members = [
|
||||
"deltachat-rpc-server",
|
||||
"deltachat-ratelimit",
|
||||
"deltachat-repl",
|
||||
"deltachat-time",
|
||||
"format-flowed",
|
||||
"deltachat-contact-tools",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
@@ -164,13 +153,6 @@ harness = false
|
||||
name = "send_events"
|
||||
harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
once_cell = "1.18.0"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.31"
|
||||
chrono = { version = "0.4.38", default-features=false, features = ["alloc", "clock", "std"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -195,7 +192,6 @@ or its language bindings:
|
||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||
- several **Bots**
|
||||
|
||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||
|
||||
13
RELEASE.md
13
RELEASE.md
@@ -9,16 +9,13 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
4. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
5. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
6. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
7. Tag the release: `git tag -a v1.116.0`.
|
||||
6. Tag the release: `git tag -a v1.116.0`.
|
||||
|
||||
8. Push the release tag: `git push origin v1.116.0`.
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 112 KiB |
14
cliff.toml
14
cliff.toml
@@ -77,17 +77,3 @@ body = """
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
footer = """
|
||||
{% for release in releases -%}
|
||||
{% if release.version -%}
|
||||
{% if release.previous.version -%}
|
||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||
https://github.com/deltachat/deltachat-core-rust\
|
||||
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
||||
/compare/{{ release.previous.version }}..HEAD
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.0.0" # No semver-stable versioning
|
||||
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"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||
chrono = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -1,615 +0,0 @@
|
||||
//! Contact-related tools, like parsing vcards and sanitizing name and address
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
unused,
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
missing_docs,
|
||||
clippy::all,
|
||||
clippy::wildcard_imports,
|
||||
clippy::needless_borrow,
|
||||
clippy::cast_lossless,
|
||||
clippy::unused_async,
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once,
|
||||
clippy::format_push_string,
|
||||
clippy::bool_to_int_with_if
|
||||
)]
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
// TODOs to clean up:
|
||||
// - Check if sanitizing is done correctly everywhere
|
||||
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A Contact, as represented in a VCard.
|
||||
pub struct VcardContact {
|
||||
/// The email address, vcard property `email`
|
||||
pub addr: String,
|
||||
/// The contact's display name, vcard property `fn`
|
||||
pub display_name: String,
|
||||
/// The contact's public PGP key in Base64, vcard property `key`
|
||||
pub key: Option<String>,
|
||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||
pub profile_image: Option<String>,
|
||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||
pub timestamp: Result<u64>,
|
||||
}
|
||||
|
||||
/// Returns a vCard containing given contacts.
|
||||
///
|
||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||
pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
fn format_timestamp(c: &VcardContact) -> Option<String> {
|
||||
let timestamp = *c.timestamp.as_ref().ok()?;
|
||||
let timestamp: i64 = timestamp.try_into().ok()?;
|
||||
let datetime = DateTime::from_timestamp(timestamp, 0)?;
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
let addr = &c.addr;
|
||||
let display_name = match c.display_name.is_empty() {
|
||||
false => &c.display_name,
|
||||
true => &c.addr,
|
||||
};
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:{addr}\n\
|
||||
FN:{display_name}\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\n");
|
||||
}
|
||||
res += "END:VCARD\n";
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Parses `VcardContact`s from a given `&str`.
|
||||
pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
||||
let start_of_s = s.get(..prefix.len())?;
|
||||
|
||||
if start_of_s.eq_ignore_ascii_case(prefix) {
|
||||
s.get(prefix.len()..)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
|
||||
let remainder = remove_prefix(s, property)?;
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
|
||||
// TODO this doesn't handle the case where there are quotes around a colon
|
||||
let (params, value) = remainder.split_once(':')?;
|
||||
// In the example from above, `params` is now `;TYPE=work`
|
||||
// and `value` is now `alice@example.com`
|
||||
|
||||
if params
|
||||
.chars()
|
||||
.next()
|
||||
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
|
||||
.is_some()
|
||||
{
|
||||
// `s` started with `property`, but the next character after it was not punctuation,
|
||||
// so this line's property is actually something else
|
||||
return None;
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
fn parse_datetime(datetime: &str) -> Result<u64> {
|
||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
|
||||
// ISO.8601, but fails to parse any of the examples given.
|
||||
// So, instead just parse using a format string.
|
||||
|
||||
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
|
||||
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
|
||||
Ok(datetime) => datetime.timestamp(),
|
||||
// Parses 19961022T140000.
|
||||
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
|
||||
Ok(datetime) => datetime
|
||||
.and_local_timezone(chrono::offset::Local)
|
||||
.single()
|
||||
.context("Could not apply local timezone to parsed date and time")?
|
||||
.timestamp(),
|
||||
Err(_) => return Err(e.into()),
|
||||
},
|
||||
};
|
||||
Ok(timestamp.try_into()?)
|
||||
}
|
||||
|
||||
let mut lines = vcard.lines().peekable();
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
while lines.peek().is_some() {
|
||||
// Skip to the start of the vcard:
|
||||
for line in lines.by_ref() {
|
||||
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut display_name = None;
|
||||
let mut addr = None;
|
||||
let mut key = None;
|
||||
let mut photo = None;
|
||||
let mut datetime = None;
|
||||
|
||||
for line in lines.by_ref() {
|
||||
if let Some(email) = vcard_property(line, "email") {
|
||||
addr.get_or_insert(email);
|
||||
} else if let Some(name) = vcard_property(line, "fn") {
|
||||
display_name.get_or_insert(name);
|
||||
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
|
||||
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
|
||||
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
|
||||
{
|
||||
key.get_or_insert(k);
|
||||
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
|
||||
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
|
||||
{
|
||||
photo.get_or_insert(p);
|
||||
} else if let Some(rev) = vcard_property(line, "rev") {
|
||||
datetime.get_or_insert(rev);
|
||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (display_name, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
|
||||
contacts.push(VcardContact {
|
||||
display_name,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
timestamp: datetime
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContactAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactAddress {
|
||||
/// Constructs a new contact address from string,
|
||||
/// normalizing and validating it.
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the name and address
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
strip_rtlo_characters(&normalize_name(name)),
|
||||
addr.to_string(),
|
||||
)
|
||||
};
|
||||
let mut name = normalize_name(&name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||
if name == addr {
|
||||
name = "".to_string();
|
||||
}
|
||||
|
||||
(name, addr)
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
||||
/// This method strips all occurrences of the RTLO Unicode character.
|
||||
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
|
||||
pub fn strip_rtlo_characters(input_str: &str) -> String {
|
||||
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
|
||||
}
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
let res = EmailAddress::new(addr);
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
/// Returns address lowercased,
|
||||
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||
pub fn addr_normalize(addr: &str) -> String {
|
||||
let norm = addr.trim().to_lowercase();
|
||||
|
||||
if norm.starts_with("mailto:") {
|
||||
norm.get(7..).unwrap_or(&norm).to_string()
|
||||
} else {
|
||||
norm
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two email addresses, normalizing them beforehand.
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1);
|
||||
let norm2 = addr_normalize(addr2);
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
///
|
||||
/// Represents an email address, right now just the `name@domain` portion.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use deltachat_contact_tools::EmailAddress;
|
||||
/// let email = match EmailAddress::new("someone@example.com") {
|
||||
/// Ok(addr) => addr,
|
||||
/// Err(e) => panic!("Error parsing address, error was {}", e),
|
||||
/// };
|
||||
/// assert_eq!(&email.local, "someone");
|
||||
/// assert_eq!(&email.domain, "example.com");
|
||||
/// assert_eq!(email.to_string(), "someone@example.com");
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct EmailAddress {
|
||||
/// Local part of the email address.
|
||||
pub local: String,
|
||||
|
||||
/// Email address domain.
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}@{}", self.local, self.domain)
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailAddress {
|
||||
/// Performs a dead-simple parse of an email address.
|
||||
pub fn new(input: &str) -> Result<EmailAddress> {
|
||||
if input.is_empty() {
|
||||
bail!("empty string is not valid");
|
||||
}
|
||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||
|
||||
if input
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||
{
|
||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
}
|
||||
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
if local.is_empty() {
|
||||
bail!("empty string is not valid for local part in {:?}", input);
|
||||
}
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vcard_thunderbird() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'Alice Mueller'
|
||||
EMAIL;PREF=1:alice.mueller@posteo.de
|
||||
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'bobzzz@freenet.de'
|
||||
EMAIL;PREF=1:bobzzz@freenet.de
|
||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||
END:VCARD
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Mueller".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
|
||||
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
||||
assert_eq!(contacts[1].display_name, "".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
assert!(contacts[1].timestamp.is_err());
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_simple_example() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Alice Wonderland
|
||||
N:Wonderland;Alice;;;Ms.
|
||||
GENDER:W
|
||||
EMAIL;TYPE=work:alice@example.com
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
||||
REV:20240418T184242Z
|
||||
|
||||
END:VCARD",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_and_parse_vcard() {
|
||||
let contacts = [
|
||||
VcardContact {
|
||||
addr: "alice@example.org".to_string(),
|
||||
display_name: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
addr: "bob@example.com".to_string(),
|
||||
display_name: "".to_string(),
|
||||
key: None,
|
||||
profile_image: None,
|
||||
timestamp: Ok(0),
|
||||
},
|
||||
];
|
||||
for len in 0..=contacts.len() {
|
||||
let contacts = &contacts[0..len];
|
||||
let vcard = make_vcard(contacts);
|
||||
let parsed = parse_vcard(&vcard).unwrap();
|
||||
assert_eq!(parsed.len(), contacts.len());
|
||||
for i in 0..parsed.len() {
|
||||
assert_eq!(parsed[i].addr, contacts[i].addr);
|
||||
assert_eq!(parsed[i].display_name, contacts[i].display_name);
|
||||
assert_eq!(parsed[i].key, contacts[i].key);
|
||||
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
|
||||
assert_eq!(
|
||||
parsed[i].timestamp.as_ref().unwrap(),
|
||||
contacts[i].timestamp.as_ref().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
let contact_address = ContactAddress::new(alice_addr)?;
|
||||
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||
|
||||
let invalid_addr = "<> foobar";
|
||||
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emailaddress_parse() {
|
||||
assert_eq!(EmailAddress::new("").is_ok(), false);
|
||||
assert_eq!(
|
||||
EmailAddress::new("user@domain.tld").unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "domain.tld".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
EmailAddress::new("user@localhost").unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "localhost".into()
|
||||
}
|
||||
);
|
||||
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
||||
assert!(EmailAddress::new("u@d").is_ok());
|
||||
assert!(EmailAddress::new("u@d.").is_err());
|
||||
assert!(EmailAddress::new("u@d.t").is_ok());
|
||||
assert_eq!(
|
||||
EmailAddress::new("u@d.tt").unwrap(),
|
||||
EmailAddress {
|
||||
local: "u".into(),
|
||||
domain: "d.tt".into(),
|
||||
}
|
||||
);
|
||||
assert!(EmailAddress::new("u@tt").is_ok());
|
||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_android() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
TEL;CELL:+1-234-567-890
|
||||
EMAIL;HOME:bob@example.org
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Alice;;;
|
||||
FN:Alice
|
||||
EMAIL;HOME:alice@example.org
|
||||
END:VCARD
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
|
||||
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[1].display_name, "Alice".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_local_datetime() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
FN:Alice Wonderland\n\
|
||||
EMAIL;TYPE=work:alice@example.org\n\
|
||||
REV:20240418T184242\n\
|
||||
END:VCARD",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||
assert_eq!(
|
||||
*contacts[0].timestamp.as_ref().unwrap(),
|
||||
chrono::offset::Local
|
||||
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
|
||||
.unwrap()
|
||||
.timestamp()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.137.4"
|
||||
version = "1.132.1"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -20,7 +20,7 @@ libc = "0.2"
|
||||
human-panic = { version = "1", default-features = false }
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="namespaces" visible="yes" title="">
|
||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||
|
||||
@@ -17,6 +17,7 @@ typedef struct _dc_array dc_array_t;
|
||||
typedef struct _dc_chatlist dc_chatlist_t;
|
||||
typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
typedef struct _dc_reactions dc_reactions_t;
|
||||
typedef struct _dc_contact dc_contact_t;
|
||||
typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
@@ -362,12 +363,8 @@ uint32_t dc_get_id (dc_context_t* context);
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per context.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* may or may not be available to event emitter.
|
||||
* Having more than one event emitter running at the same time on the same context
|
||||
* will result in events being randomly delivered to one of the emitters.
|
||||
*/
|
||||
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
||||
|
||||
@@ -426,16 +423,19 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
||||
* 0=do not watch the `Sent`-folder (default).
|
||||
* 0=do not watch the `Sent`-folder (default),
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `mvbox_move` = 1=detect chat messages,
|
||||
* move them to the `DeltaChat` folder,
|
||||
* and watch the `DeltaChat` folder for updates (default),
|
||||
* 0=do not move chat-messages
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
|
||||
* `DeltaChat` folder. Messages will still be fetched from the
|
||||
* spam folder and `sendbox_watch` will also still be respected
|
||||
* if enabled.
|
||||
* 0=watch all folders normally (default)
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only,
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
@@ -689,25 +689,8 @@ 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);
|
||||
|
||||
|
||||
/**
|
||||
* Standalone version of dc_accounts_all_work_done().
|
||||
* Only used by the python tests.
|
||||
*/
|
||||
int dc_all_work_done (dc_context_t* context);
|
||||
@@ -1120,6 +1103,36 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Send a reaction to message.
|
||||
*
|
||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
* single message can be sent multiple times. The last reaction
|
||||
* received overrides all previously received reactions. It is
|
||||
* possible to remove all reactions by sending an empty string.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `send_reaction` instead
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id ID of the message you react to.
|
||||
* @param reaction A string consisting of emojis separated by spaces.
|
||||
* @return The ID of the message sent out or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
|
||||
|
||||
|
||||
/**
|
||||
* Get a structure with reactions to the message.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID to get reactions for.
|
||||
* @return A structure with all reactions to the message.
|
||||
*/
|
||||
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc instance sends a status update to its other members.
|
||||
*
|
||||
@@ -1182,65 +1195,6 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
|
||||
*/
|
||||
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
|
||||
|
||||
|
||||
/**
|
||||
* Set Webxdc file as integration.
|
||||
* see dc_init_webxdc_integration() for more details about Webxdc integrations.
|
||||
*
|
||||
* @warning This is an experimental API which may change in the future
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param file The .xdc file to use as Webxdc integration.
|
||||
*/
|
||||
void dc_set_webxdc_integration (dc_context_t* context, const char* file);
|
||||
|
||||
|
||||
/**
|
||||
* Init a Webxdc integration.
|
||||
*
|
||||
* A Webxdc integration is
|
||||
* a Webxdc showing a map, getting locations via setUpdateListener(), setting POIs via sendUpdate();
|
||||
* core takes eg. care of feeding locations to the Webxdc or sending the data out.
|
||||
*
|
||||
* @warning This is an experimental API, esp. support of integration types (eg. image editor, tools) is left out for simplicity
|
||||
*
|
||||
* Currently, Webxdc integrations are .xdc files shipped together with the main app.
|
||||
* Before dc_init_webxdc_integration() can be called,
|
||||
* UI has to call dc_set_webxdc_integration() to define a .xdc file to be used as integration.
|
||||
*
|
||||
* dc_init_webxdc_integration() returns a Webxdc message ID that
|
||||
* UI can open and use mostly as usual.
|
||||
*
|
||||
* Concrete behaviour and status updates depend on the integration, driven by UI needs.
|
||||
*
|
||||
* There is no need to de-initialize the integration,
|
||||
* however, unless documented otherwise,
|
||||
* the integration is valid only as long as not re-initialized
|
||||
* In other words, UI must not have a Webxdc with the same integration open twice.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ~~~
|
||||
* // Define a .xdc file to be used as maps integration
|
||||
* dc_set_webxdc_integration(context, path_to_maps_xdc);
|
||||
*
|
||||
* // Integrate the map to a chat, the map will show locations for this chat then:
|
||||
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, any_chat_id);
|
||||
*
|
||||
* // Or use the Webxdc as a global map, showing locations of all chats:
|
||||
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, 0);
|
||||
* ~~~
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to get the integration for.
|
||||
* @return ID of the message that refers to the Webxdc instance.
|
||||
* UI can open a Webxdc as usual with this instance.
|
||||
*/
|
||||
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -2608,7 +2562,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://securejoin.delta.chat/
|
||||
* See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
* @return The text that should go to the QR code,
|
||||
* On errors, an empty QR code is returned, NULL is never returned.
|
||||
@@ -2644,7 +2598,8 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
|
||||
*
|
||||
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
||||
*
|
||||
* See https://securejoin.delta.chat/ for details about both protocols.
|
||||
* See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -3129,6 +3084,23 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
|
||||
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
|
||||
|
||||
|
||||
/**
|
||||
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
*
|
||||
* iOS can:
|
||||
* - call dc_start_io() (in case IO was not running)
|
||||
* - call dc_maybe_network()
|
||||
* - while dc_accounts_all_work_done() returns false:
|
||||
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @return Whether all accounts finished their background work.
|
||||
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
*/
|
||||
int dc_accounts_all_work_done (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
|
||||
* If IO is already running, nothing happens.
|
||||
@@ -3179,33 +3151,6 @@ void dc_accounts_maybe_network (dc_accounts_t* accounts);
|
||||
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Perform a background fetch for all accounts in parallel with a timeout.
|
||||
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
|
||||
*
|
||||
* dc_accounts_background_fetch() was created for the iOS Background fetch.
|
||||
*
|
||||
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
|
||||
* even in case of timeout, unless the function fails and returns 0.
|
||||
* Process all events until you get this one and you can safely return to the background
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param timeout The timeout in seconds
|
||||
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
|
||||
*/
|
||||
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||
|
||||
|
||||
/**
|
||||
* Sets device token for Apple Push Notification service.
|
||||
* Returns immediately.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param token Hexadecimal device token
|
||||
*/
|
||||
void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const char *token);
|
||||
|
||||
/**
|
||||
* Create the event emitter that is used to receive events.
|
||||
*
|
||||
@@ -4105,19 +4050,6 @@ char* dc_msg_get_subject (const dc_msg_t* msg);
|
||||
char* dc_msg_get_file (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Save file copy at the user-provided path.
|
||||
*
|
||||
* Fails if file already exists at the provided path.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @param path Destination file path with filename and extension.
|
||||
* @return 0 on failure, 1 on success.
|
||||
*/
|
||||
int dc_msg_save_file (const dc_msg_t* msg, const char* path);
|
||||
|
||||
|
||||
/**
|
||||
* Get an original attachment filename, with extension but without the path. To get the full path,
|
||||
* use dc_msg_get_file().
|
||||
@@ -4182,6 +4114,7 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* true if the Webxdc should get full internet access, including Webrtc.
|
||||
* currently, this is only true for encrypted Webxdc's in the self chat
|
||||
* that have requested internet access in the manifest.
|
||||
* this is useful for development and maybe for internal integrations at some point.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
@@ -4390,9 +4323,9 @@ int dc_msg_has_deviating_timestamp(const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a message has a POI location bound to it.
|
||||
* These locations are also returned by dc_get_locations()
|
||||
* The UI may decide to display a special icon beside such messages.
|
||||
* Check if a message has a location bound to it.
|
||||
* These messages are also returned by dc_get_locations()
|
||||
* and the UI may decide to display a special icon beside such messages,
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
@@ -4464,9 +4397,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* Currently, the following types are defined:
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
*
|
||||
* Even when you display an icon,
|
||||
* you should still display the text of the informational message using dc_msg_get_text()
|
||||
@@ -4493,7 +4423,6 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
|
||||
/**
|
||||
@@ -5139,15 +5068,6 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Returns whether contact is a bot.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0 if the contact is not a bot, 1 otherwise.
|
||||
*/
|
||||
int dc_contact_is_bot (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
@@ -5363,6 +5283,52 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
|
||||
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_reactions_t
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
*
|
||||
* An object representing all reactions for a single message.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns array of contacts which reacted to the given message.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
|
||||
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
|
||||
*/
|
||||
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string containing space-separated reactions of a single contact.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @param contact_id ID of the contact.
|
||||
* @return Space-separated list of emoji sequences, which could be empty.
|
||||
* Returned string should not be modified and should be freed
|
||||
* with dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
|
||||
|
||||
|
||||
/**
|
||||
* Frees an object containing message reactions.
|
||||
*
|
||||
* Reactions objects are created by dc_get_msg_reactions().
|
||||
*
|
||||
* @deprecated 2023-11-27
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object to free.
|
||||
* If NULL is given, nothing is done.
|
||||
*/
|
||||
void dc_reactions_unref (dc_reactions_t* reactions);
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_MSG DC_MSG
|
||||
*
|
||||
@@ -6053,12 +6019,10 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Downloading a bunch of messages just finished. This is an
|
||||
* event to allow the UI to only show one notification per message bunch,
|
||||
* instead of cluttering the user with many notifications.
|
||||
* UI may store #DC_EVENT_INCOMING_MSG events
|
||||
* and display notifications for all messages at once
|
||||
* when this event arrives.
|
||||
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 0
|
||||
* @param data2 (char*) msg_ids, a json object with the message ids.
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
|
||||
|
||||
@@ -6242,18 +6206,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_SELFAVATAR_CHANGED 2110
|
||||
|
||||
|
||||
/**
|
||||
* A multi-device synced config value changed. Maybe the app needs to refresh smth. For uniformity
|
||||
* this is emitted on the source device too. The value isn't reported, otherwise it would be logged
|
||||
* which might not be good for privacy. You can get the new value with
|
||||
* `dc_get_config(context, data2)`.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) Configuration key.
|
||||
*/
|
||||
#define DC_EVENT_CONFIG_SYNCED 2111
|
||||
|
||||
|
||||
/**
|
||||
* webxdc status update received.
|
||||
* To get the received status update, use dc_get_webxdc_status_updates() with
|
||||
@@ -6279,32 +6231,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
* Inform UI that Order (and content as in chat ids) of the chatlist changed.
|
||||
*
|
||||
* This event acts as a marker, when you reach this event you can be sure
|
||||
* that all events emitted during the background fetch were processed.
|
||||
*
|
||||
* This event is only emitted by the account manager
|
||||
* Sometimes this is emitted together with `DC_EVENT_UI_CHATLIST_ITEM_CHANGED` such as on `DC_EVENT_INCOMING_MSG`.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
|
||||
#define DC_EVENT_UI_CHATLIST_CHANGED 2200
|
||||
|
||||
/**
|
||||
* Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
*
|
||||
* Sometimes this is emitted together with `DC_EVENT_CHATLIST_ITEM_CHANGED`.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_CHATLIST_CHANGED 2300
|
||||
|
||||
/**
|
||||
* Inform that all or a single chat list item changed and needs to be rerendered
|
||||
* Inform UI that all or a single chat list item changed and needs to be rerendered
|
||||
* If `chat_id` is set to 0, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
*
|
||||
* @param data1 (int) chat_id chat id of chatlist item to be rerendered, if chat_id = 0 all (cached & visible) items need to be rerendered
|
||||
*/
|
||||
|
||||
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
|
||||
#define DC_EVENT_UI_CHATLIST_ITEM_CHANGED 2201
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -7073,8 +7014,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "You added member %1$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the added member's name.
|
||||
#define DC_STR_ADD_MEMBER_BY_YOU 128
|
||||
|
||||
/// "Member %1$s added by %2$s."
|
||||
@@ -7296,37 +7235,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as the first info messages in newly created groups.
|
||||
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
|
||||
|
||||
/// "Member %1$s added."
|
||||
///
|
||||
/// Used as info messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the added member's name.
|
||||
#define DC_STR_MESSAGE_ADD_MEMBER 173
|
||||
|
||||
/// "Your email provider %1$s requires end-to-end encryption which is not setup yet."
|
||||
///
|
||||
/// Used as info messages when a message cannot be sent because it cannot be encrypted.
|
||||
///
|
||||
/// `%1$s` will be replaced by the provider's domain.
|
||||
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
|
||||
|
||||
/// "You reacted %1$s to '%2$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the reaction, usually an emoji
|
||||
/// `%2$s` will be replaced by the summary of the message the reaction refers to
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_YOU_REACTED 176
|
||||
|
||||
/// "%1$s reacted %2$s to '%3$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the name the contact who reacted
|
||||
/// `%2$s` will be replaced by the reaction, usually an emoji
|
||||
/// `%3$s` will be replaced by the summary of the message the reaction refers to
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_REACTED_BY 177
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
non_upper_case_globals,
|
||||
non_camel_case_types,
|
||||
clippy::missing_safety_doc,
|
||||
clippy::expect_fun_call
|
||||
)]
|
||||
@@ -24,13 +26,15 @@ use anyhow::Context as _;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::{Context, ContextBuilder};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
@@ -65,6 +69,8 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
/// Struct representing the deltachat context.
|
||||
pub type dc_context_t = Context;
|
||||
|
||||
pub type dc_reactions_t = Reactions;
|
||||
|
||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||
|
||||
fn block_on<T>(fut: T) -> T::Output
|
||||
@@ -98,11 +104,12 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::thread_rng().gen();
|
||||
block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
.open(),
|
||||
)
|
||||
block_on(Context::new(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
))
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
@@ -126,11 +133,12 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
||||
}
|
||||
|
||||
let id = rand::thread_rng().gen();
|
||||
match block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
.build(),
|
||||
) {
|
||||
match block_on(Context::new_closed(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
)) {
|
||||
Ok(context) => Box::into_raw(Box::new(context)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {err:#}");
|
||||
@@ -379,7 +387,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
||||
block_on(async move { ctx.get_connectivity().await as u32 as libc::c_int })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -402,16 +410,6 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_push_state()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.push_state()) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -558,12 +556,10 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::SecurejoinJoinerProgress { .. } => 2061,
|
||||
EventType::ConnectivityChanged => 2100,
|
||||
EventType::SelfavatarChanged => 2110,
|
||||
EventType::ConfigSynced { .. } => 2111,
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::UIChatListChanged => 2200,
|
||||
EventType::UIChatListItemChanged { .. } => 2201,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,11 +585,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::Error(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
| EventType::UIChatListChanged => 0,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -618,7 +612,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
}
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
EventType::UIChatListItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
}
|
||||
@@ -656,10 +650,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. } => 0,
|
||||
| EventType::UIChatListChanged
|
||||
| EventType::UIChatListItemChanged { .. } => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
@@ -721,11 +713,9 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged => ptr::null_mut(),
|
||||
| EventType::UIChatListItemChanged { .. }
|
||||
| EventType::UIChatListChanged => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -737,10 +727,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::ConfigSynced { key } => {
|
||||
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
|
||||
.unwrap_or_default()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,6 +1011,49 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_reaction(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
reaction: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_reaction()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to send reaction")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_reactions(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> *mut dc_reactions_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_msg_reactions()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
|
||||
.context("failed dc_get_msg_reactions() call")
|
||||
.log_err(ctx)
|
||||
{
|
||||
reactions
|
||||
} else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
Box::into_raw(Box::new(reactions))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1063,43 +1097,6 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_webxdc_integration(
|
||||
context: *mut dc_context_t,
|
||||
file: *const libc::c_char,
|
||||
) {
|
||||
if context.is_null() || file.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_webxdc_integration()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.set_webxdc_integration(&to_string_lossy(file)))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_init_webxdc_integration(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_init_webxdc_integration()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(ctx.init_webxdc_integration(chat_id))
|
||||
.log_err(ctx)
|
||||
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2045,7 +2042,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
|
||||
);
|
||||
message::Message::default()
|
||||
} else {
|
||||
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
||||
error!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
}
|
||||
@@ -3368,34 +3365,6 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
.unwrap_or_else(|| "".strdup())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_save_file(
|
||||
msg: *mut dc_msg_t,
|
||||
path: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if msg.is_null() || path.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_save_file()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
let path = to_string_lossy(path);
|
||||
let r = block_on(
|
||||
ffi_msg
|
||||
.message
|
||||
.save_file(ctx, &std::path::PathBuf::from(path)),
|
||||
);
|
||||
match r {
|
||||
Ok(()) => 1,
|
||||
Err(_) => {
|
||||
r.context("Failed to save file from message")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
@@ -4166,15 +4135,6 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_is_bot()");
|
||||
return 0;
|
||||
}
|
||||
(*contact).contact.is_bot() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
|
||||
if contact.is_null() {
|
||||
@@ -4271,6 +4231,45 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
|
||||
lot.get_timestamp()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_contacts(
|
||||
reactions: *mut dc_reactions_t,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
let array: dc_array_t = reactions.contacts().into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
|
||||
reactions: *mut dc_reactions_t,
|
||||
contact_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
reactions.get(ContactId::new(contact_id)).as_str().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_unref()");
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(reactions));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||
libc::free(s as *mut _)
|
||||
@@ -4471,6 +4470,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultNullableExt<T> {
|
||||
fn into_raw(self) -> *mut T;
|
||||
}
|
||||
|
||||
impl<T, E> ResultNullableExt<T> for Result<T, E> {
|
||||
fn into_raw(self) -> *mut T {
|
||||
match self {
|
||||
Ok(t) => Box::into_raw(Box::new(t)),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> Vec<MsgId> {
|
||||
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
|
||||
let msg_ids: Vec<MsgId> = ids
|
||||
@@ -4826,6 +4838,16 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_all_work_done()");
|
||||
return 0;
|
||||
}
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
@@ -4870,49 +4892,6 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
accounts: *mut dc_accounts_t,
|
||||
timeout_in_seconds: u64,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() || timeout_in_seconds <= 2 {
|
||||
eprintln!("ignoring careless call to dc_accounts_background_fetch()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move {
|
||||
let accounts = accounts.read().await;
|
||||
accounts
|
||||
.background_fetch(Duration::from_secs(timeout_in_seconds))
|
||||
.await;
|
||||
});
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
accounts: *mut dc_accounts_t,
|
||||
token: *const libc::c_char,
|
||||
) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_set_push_device_token()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let token = to_string_lossy(token);
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
if let Err(err) = accounts.set_push_device_token(&token).await {
|
||||
accounts.emit_event(EventType::Error(format!(
|
||||
"Failed to set notify token: {err:#}."
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
accounts: *mut dc_accounts_t,
|
||||
@@ -4950,9 +4929,7 @@ mod jsonrpc {
|
||||
}
|
||||
|
||||
let account_manager = &*account_manager;
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||
account_manager.inner.clone(),
|
||||
));
|
||||
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.137.4"
|
||||
version = "1.132.1"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-jsonrpc-server"
|
||||
@@ -16,26 +15,26 @@ required-features = ["webserver"]
|
||||
anyhow = "1"
|
||||
deltachat = { path = ".." }
|
||||
num-traits = "0.2"
|
||||
schemars = "0.8.19"
|
||||
schemars = "0.8.13"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.10.1"
|
||||
tempfile = "3.9.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "2.2.1" }
|
||||
async-channel = { version = "2.0.0" }
|
||||
futures = { version = "0.3.30" }
|
||||
serde_json = "1"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||
tokio = { version = "1.37.0" }
|
||||
tokio = { version = "1.33.0" }
|
||||
sanitize-filename = "0.5"
|
||||
walkdir = "2.5.0"
|
||||
base64 = "0.22"
|
||||
walkdir = "2.3.3"
|
||||
base64 = "0.21"
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.11.3", optional = true }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.33.0", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
7048
deltachat-jsonrpc/openrpc/openrpc.json
Normal file
7048
deltachat-jsonrpc/openrpc/openrpc.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
@@ -30,7 +28,6 @@ use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
@@ -65,14 +62,14 @@ use crate::api::types::qr::QrObject;
|
||||
struct AccountState {
|
||||
/// The Qr code for current [`CommandApi::provide_backup`] call.
|
||||
///
|
||||
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
|
||||
/// `Some`, otherwise `None`.
|
||||
backup_provider_qr: watch::Sender<Option<Qr>>,
|
||||
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
|
||||
/// `Pending` or `Ready`, otherwise `NoProvider`.
|
||||
backup_provider_qr: watch::Sender<ProviderQr>,
|
||||
}
|
||||
|
||||
impl Default for AccountState {
|
||||
fn default() -> Self {
|
||||
let tx = watch::Sender::new(None);
|
||||
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
|
||||
Self {
|
||||
backup_provider_qr: tx,
|
||||
}
|
||||
@@ -83,30 +80,21 @@ impl Default for AccountState {
|
||||
pub struct CommandApi {
|
||||
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
||||
|
||||
/// Receiver side of the event channel.
|
||||
///
|
||||
/// Events from it can be received by calling `get_next_event` method.
|
||||
event_emitter: Arc<EventEmitter>,
|
||||
|
||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||
}
|
||||
|
||||
impl CommandApi {
|
||||
pub fn new(accounts: Accounts) -> Self {
|
||||
let event_emitter = Arc::new(accounts.get_event_emitter());
|
||||
CommandApi {
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
event_emitter,
|
||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
let event_emitter = Arc::new(accounts.read().await.get_event_emitter());
|
||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
CommandApi {
|
||||
accounts,
|
||||
event_emitter,
|
||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||
}
|
||||
}
|
||||
@@ -135,13 +123,21 @@ impl CommandApi {
|
||||
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
|
||||
.await;
|
||||
|
||||
loop {
|
||||
if let Some(qr) = receiver.borrow_and_update().clone() {
|
||||
return Ok(qr);
|
||||
}
|
||||
if receiver.changed().await.is_err() {
|
||||
bail!("No backup being provided (account state dropped)");
|
||||
}
|
||||
let val: ProviderQr = receiver.borrow_and_update().clone();
|
||||
match val {
|
||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
||||
ProviderQr::Pending => loop {
|
||||
if receiver.changed().await.is_err() {
|
||||
bail!("No backup being provided (account state dropped)");
|
||||
}
|
||||
let val: ProviderQr = receiver.borrow().clone();
|
||||
match val {
|
||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
||||
ProviderQr::Pending => continue,
|
||||
ProviderQr::Ready(qr) => break Ok(qr),
|
||||
};
|
||||
},
|
||||
ProviderQr::Ready(qr) => Ok(qr),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +165,8 @@ impl CommandApi {
|
||||
|
||||
/// Get the next event.
|
||||
async fn get_next_event(&self) -> Result<Event> {
|
||||
self.event_emitter
|
||||
let event_emitter = self.accounts.read().await.get_event_emitter();
|
||||
event_emitter
|
||||
.recv()
|
||||
.await
|
||||
.map(|event| event.into())
|
||||
@@ -234,27 +231,13 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
self.accounts
|
||||
.write()
|
||||
.await
|
||||
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Methods that work on individual accounts
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Starts background tasks for a single account.
|
||||
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
ctx.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -328,11 +311,6 @@ impl CommandApi {
|
||||
ctx.get_info().await
|
||||
}
|
||||
|
||||
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.draft_self_report().await?.to_u32())
|
||||
}
|
||||
|
||||
/// Sets the given configuration key.
|
||||
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -405,7 +383,7 @@ impl CommandApi {
|
||||
/// Configures this account with the currently set parameters.
|
||||
/// Setup the credential config before calling this.
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
let result = ctx.configure().await;
|
||||
if result.is_err() {
|
||||
@@ -700,7 +678,8 @@ impl CommandApi {
|
||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||
/// works for protected groups as well as for normal groups.
|
||||
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||
/// See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
/// for details about both protocols.
|
||||
///
|
||||
/// return format: `[code, svg]`
|
||||
async fn get_chat_securejoin_qr_code_svg(
|
||||
@@ -728,7 +707,8 @@ impl CommandApi {
|
||||
///
|
||||
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
///
|
||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||
/// See https://securejoin.readthedocs.io/en/latest/new.html
|
||||
/// for details about both protocols.
|
||||
///
|
||||
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||
/// to `check_qr()`.
|
||||
@@ -916,35 +896,19 @@ impl CommandApi {
|
||||
.to_u32())
|
||||
}
|
||||
|
||||
/// Add a message to the device-chat.
|
||||
/// Device-messages usually contain update information
|
||||
/// and some hints that are added during the program runs, multi-device etc.
|
||||
/// The device-message may be defined by a label;
|
||||
/// if a message with the same label was added or skipped before,
|
||||
/// the message is not added again, even if the message was deleted in between.
|
||||
/// If needed, the device-chat is created before.
|
||||
///
|
||||
/// Sends the `MsgsChanged` event on success.
|
||||
///
|
||||
/// Setting msg to None will prevent the device message with this label from being added in the future.
|
||||
// for now only text messages, because we only used text messages in desktop thusfar
|
||||
async fn add_device_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
label: String,
|
||||
msg: Option<MessageData>,
|
||||
) -> Result<Option<u32>> {
|
||||
text: String,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
if let Some(msg) = msg {
|
||||
let mut message = msg.create_message(&ctx).await?;
|
||||
let message_id =
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?;
|
||||
if !message_id.is_unset() {
|
||||
return Ok(Some(message_id.to_u32()));
|
||||
}
|
||||
} else {
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?;
|
||||
}
|
||||
Ok(None)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(text);
|
||||
let message_id =
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
||||
Ok(message_id.to_u32())
|
||||
}
|
||||
|
||||
/// Mark all messages in a chat as _noticed_.
|
||||
@@ -1098,12 +1062,9 @@ impl CommandApi {
|
||||
.collect::<Vec<JSONRPCMessageListItem>>())
|
||||
}
|
||||
|
||||
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
MessageObject::from_msg_id(&ctx, msg_id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
|
||||
MessageObject::from_message_id(&ctx, message_id).await
|
||||
}
|
||||
|
||||
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
||||
@@ -1123,7 +1084,7 @@ impl CommandApi {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
|
||||
for message_id in message_ids {
|
||||
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
|
||||
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
|
||||
messages.insert(
|
||||
message_id,
|
||||
match message_result {
|
||||
@@ -1570,21 +1531,20 @@ impl CommandApi {
|
||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
self.with_state(account_id, |state| {
|
||||
state.backup_provider_qr.send_replace(ProviderQr::Pending);
|
||||
})
|
||||
.await;
|
||||
|
||||
let provider = imex::BackupProvider::prepare(&ctx).await?;
|
||||
self.with_state(account_id, |state| {
|
||||
state.backup_provider_qr.send_replace(Some(provider.qr()));
|
||||
state
|
||||
.backup_provider_qr
|
||||
.send_replace(ProviderQr::Ready(provider.qr()));
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = provider.await;
|
||||
|
||||
self.with_state(account_id, |state| {
|
||||
state.backup_provider_qr.send_replace(None);
|
||||
})
|
||||
.await;
|
||||
|
||||
res
|
||||
provider.await
|
||||
}
|
||||
|
||||
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
||||
@@ -1592,17 +1552,11 @@ impl CommandApi {
|
||||
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
|
||||
/// retrieve the backup and setup this second device.
|
||||
///
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
/// This call will fail if there is currently no concurrent call to
|
||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
||||
/// ready.
|
||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
.context("Backup provider did not start in time")?
|
||||
.context("Failed to get backup QR code")?;
|
||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
||||
qr::format_backup(&qr)
|
||||
}
|
||||
|
||||
@@ -1611,20 +1565,14 @@ impl CommandApi {
|
||||
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
|
||||
/// retrieve the backup and setup this second device.
|
||||
///
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
/// This call will fail if there is currently no concurrent call to
|
||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
||||
/// ready.
|
||||
///
|
||||
/// Returns the QR code rendered as an SVG image.
|
||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
.context("Backup provider did not start in time")?
|
||||
.context("Failed to get backup QR code")?;
|
||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
||||
generate_backup_qr(&ctx, &qr).await
|
||||
}
|
||||
|
||||
@@ -1634,9 +1582,6 @@ impl CommandApi {
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
///
|
||||
/// Do not forget to call start_io on the account after a successful import,
|
||||
/// otherwise it will not connect to the email server.
|
||||
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = qr::check_qr(&ctx, &qr_text).await?;
|
||||
@@ -1771,29 +1716,6 @@ impl CommandApi {
|
||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||
}
|
||||
|
||||
/// Sets Webxdc file as integration.
|
||||
/// `file` is the .xdc to use as Webxdc integration.
|
||||
async fn set_webxdc_integration(&self, account_id: u32, file_path: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.set_webxdc_integration(&file_path).await
|
||||
}
|
||||
|
||||
/// Returns Webxdc instance used for optional integrations.
|
||||
/// UI can open the Webxdc as usual.
|
||||
/// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
|
||||
/// `integrate_for` is the chat to get the integration for.
|
||||
async fn init_webxdc_integration(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: Option<u32>,
|
||||
) -> Result<Option<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx
|
||||
.init_webxdc_integration(chat_id.map(ChatId::new))
|
||||
.await?
|
||||
.map(|msg_id| msg_id.to_u32()))
|
||||
}
|
||||
|
||||
/// Makes an HTTP GET request and returns a response.
|
||||
///
|
||||
/// `url` is the HTTP or HTTPS URL.
|
||||
@@ -1886,7 +1808,38 @@ impl CommandApi {
|
||||
|
||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = data.create_message(&ctx).await?;
|
||||
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
|
||||
viewtype.into()
|
||||
} else if data.file.is_some() {
|
||||
Viewtype::File
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
message.set_text(data.text.unwrap_or_default());
|
||||
if data.html.is_some() {
|
||||
message.set_html(data.html);
|
||||
}
|
||||
if data.override_sender_name.is_some() {
|
||||
message.set_override_sender_name(data.override_sender_name);
|
||||
}
|
||||
if let Some(file) = data.file {
|
||||
message.set_file(file, None);
|
||||
}
|
||||
if let Some((latitude, longitude)) = data.location {
|
||||
message.set_location(latitude, longitude);
|
||||
}
|
||||
if let Some(id) = data.quoted_message_id {
|
||||
message
|
||||
.set_quote(
|
||||
&ctx,
|
||||
Some(
|
||||
&Message::load_from_db(&ctx, MsgId::new(id))
|
||||
.await
|
||||
.context("message to quote could not be loaded")?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.to_u32();
|
||||
@@ -1902,15 +1855,6 @@ impl CommandApi {
|
||||
Ok(can_send)
|
||||
}
|
||||
|
||||
/// Saves a file copy at the user-provided path.
|
||||
///
|
||||
/// Fails if file already exists at the provided path.
|
||||
async fn save_msg_file(&self, account_id: u32, msg_id: u32, path: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
message.save_file(&ctx, Path::new(&path)).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// functions for the composer
|
||||
// the composer is the message input field
|
||||
@@ -1983,21 +1927,19 @@ impl CommandApi {
|
||||
);
|
||||
let destination_path = account_folder.join("stickers").join(collection);
|
||||
fs::create_dir_all(&destination_path).await?;
|
||||
let file = message.get_filename().context("no file?")?;
|
||||
message
|
||||
.save_file(
|
||||
&ctx,
|
||||
&destination_path.join(format!(
|
||||
"{}.{}",
|
||||
msg_id,
|
||||
Path::new(&file)
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
let file = message.get_file(&ctx).context("no file")?;
|
||||
fs::copy(
|
||||
&file,
|
||||
destination_path.join(format!(
|
||||
"{}.{}",
|
||||
msg_id,
|
||||
file.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2096,9 +2038,11 @@ impl CommandApi {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
|
||||
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
|
||||
Ok((msg_id.to_u32(), message))
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.to_u32();
|
||||
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
|
||||
Ok((msg_id, message))
|
||||
}
|
||||
|
||||
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
||||
@@ -2177,6 +2121,13 @@ async fn set_config(
|
||||
value,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match key {
|
||||
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
|
||||
ctx.restart_io_if_running().await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2192,3 +2143,15 @@ async fn get_config(
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a QR code for a BackupProvider is currently available.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug)]
|
||||
enum ProviderQr {
|
||||
/// There is no provider, asking for a QR is an error.
|
||||
NoProvider,
|
||||
/// There is a provider, the QR code is pending.
|
||||
Pending,
|
||||
/// There is a provider and QR code.
|
||||
Ready(Qr),
|
||||
}
|
||||
|
||||
@@ -45,9 +45,6 @@ pub struct ContactObject {
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
|
||||
/// If the contact is a bot.
|
||||
is_bot: bool,
|
||||
}
|
||||
|
||||
impl ContactObject {
|
||||
@@ -83,7 +80,6 @@ impl ContactObject {
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
is_bot: contact.is_bot(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,37 +28,55 @@ pub enum EventType {
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Info { msg: String },
|
||||
Info {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
SmtpConnected { msg: String },
|
||||
SmtpConnected {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
ImapConnected { msg: String },
|
||||
ImapConnected {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
SmtpMessageSent { msg: String },
|
||||
SmtpMessageSent {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted { msg: String },
|
||||
ImapMessageDeleted {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved { msg: String },
|
||||
ImapMessageMoved {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
NewBlobFile { file: String },
|
||||
NewBlobFile {
|
||||
file: String,
|
||||
},
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
DeletedBlobFile { file: String },
|
||||
DeletedBlobFile {
|
||||
file: String,
|
||||
},
|
||||
|
||||
/// The library-user should write a warning string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Warning { msg: String },
|
||||
Warning {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
///
|
||||
@@ -70,14 +88,18 @@ pub enum EventType {
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a messasge box then.
|
||||
Error { msg: String },
|
||||
Error {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
/// Reported eg. after a call to
|
||||
/// setChatName(), setChatProfileImage(),
|
||||
/// addContactToChat(), removeContactFromChat(),
|
||||
/// and messages sending functions.
|
||||
ErrorSelfNotInGroup { msg: String },
|
||||
ErrorSelfNotInGroup {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
/// reasons in the database:
|
||||
@@ -88,7 +110,10 @@ pub enum EventType {
|
||||
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgsChanged { chat_id: u32, msg_id: u32 },
|
||||
MsgsChanged {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Reactions for the message changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -101,39 +126,60 @@ pub enum EventType {
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsg { chat_id: u32, msg_id: u32 },
|
||||
IncomingMsg {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Downloading a bunch of messages just finished. This is an
|
||||
/// Downloading a bunch of messages just finished. This is an experimental
|
||||
/// event to allow the UI to only show one notification per message bunch,
|
||||
/// instead of cluttering the user with many notifications.
|
||||
///
|
||||
/// msg_ids contains the message ids.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsgBunch,
|
||||
IncomingMsgBunch {
|
||||
msg_ids: Vec<u32>,
|
||||
},
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgsNoticed { chat_id: u32 },
|
||||
MsgsNoticed {
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgDelivered { chat_id: u32, msg_id: u32 },
|
||||
MsgDelivered {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgFailed { chat_id: u32, msg_id: u32 },
|
||||
MsgFailed {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgRead { chat_id: u32, msg_id: u32 },
|
||||
MsgRead {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message is deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgDeleted { chat_id: u32, msg_id: u32 },
|
||||
MsgDeleted {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
@@ -143,17 +189,24 @@ pub enum EventType {
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatModified { chat_id: u32 },
|
||||
ChatModified {
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: u32,
|
||||
timer: u32,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged { contact_id: Option<u32> },
|
||||
ContactsChanged {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
@@ -161,7 +214,9 @@ pub enum EventType {
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// this parameter is set to `None`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
LocationChanged { contact_id: Option<u32> },
|
||||
LocationChanged {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
ConfigureProgress {
|
||||
@@ -179,7 +234,9 @@ pub enum EventType {
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexProgress { progress: usize },
|
||||
ImexProgress {
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
/// This event may be sent multiple times by a single call to imex().
|
||||
@@ -189,7 +246,9 @@ pub enum EventType {
|
||||
///
|
||||
/// @param data2 0
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexFileWritten { path: String },
|
||||
ImexFileWritten {
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
@@ -204,7 +263,10 @@ pub enum EventType {
|
||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
||||
SecurejoinInviterProgress {
|
||||
contact_id: u32,
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
/// (Bob, the person who scans the QR code).
|
||||
@@ -215,7 +277,10 @@ pub enum EventType {
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||
SecurejoinJoinerProgress {
|
||||
contact_id: u32,
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
/// This means that you should refresh the connectivity view
|
||||
@@ -223,17 +288,8 @@ pub enum EventType {
|
||||
/// getConnectivityHtml() for details.
|
||||
ConnectivityChanged,
|
||||
|
||||
/// Deprecated by `ConfigSynced`.
|
||||
SelfavatarChanged,
|
||||
|
||||
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
|
||||
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
|
||||
/// would be logged which might not be good for privacy.
|
||||
ConfigSynced {
|
||||
/// Configuration key.
|
||||
key: String,
|
||||
},
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcStatusUpdate {
|
||||
msg_id: u32,
|
||||
@@ -242,23 +298,21 @@ pub enum EventType {
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcInstanceDeleted { msg_id: u32 },
|
||||
WebxdcInstanceDeleted {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Tells that the Background fetch was completed (or timed out).
|
||||
/// This event acts as a marker, when you reach this event you can be sure
|
||||
/// that all events emitted during the background fetch were processed.
|
||||
/// Inform UI that Order (and content as in chat ids) of the chatlist changed.
|
||||
///
|
||||
/// This event is only emitted by the account manager
|
||||
AccountsBackgroundFetchDone,
|
||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
///
|
||||
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
|
||||
ChatlistChanged,
|
||||
/// Sometimes this is emitted together with `UIChatListItemChanged` such as on IncomingMessage.
|
||||
UIChatListChanged,
|
||||
|
||||
/// Inform that a single chat list item changed and needs to be rerendered.
|
||||
/// Inform UI that a single chat list item changed and needs to be rerendered
|
||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatlistItemChanged { chat_id: Option<u32> },
|
||||
UIChatListItemChanged {
|
||||
chat_id: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -294,7 +348,9 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
|
||||
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
||||
},
|
||||
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
@@ -352,9 +408,6 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::ConnectivityChanged => ConnectivityChanged,
|
||||
CoreEventType::SelfavatarChanged => SelfavatarChanged,
|
||||
CoreEventType::ConfigSynced { key } => ConfigSynced {
|
||||
key: key.to_string(),
|
||||
},
|
||||
CoreEventType::WebxdcStatusUpdate {
|
||||
msg_id,
|
||||
status_update_serial,
|
||||
@@ -365,11 +418,10 @@ impl From<CoreEventType> for EventType {
|
||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
|
||||
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
|
||||
CoreEventType::UIChatListItemChanged { chat_id } => UIChatListItemChanged {
|
||||
chat_id: chat_id.map(|id| id.to_u32()),
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::UIChatListChanged => UIChatListChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,6 @@ pub struct MessageObject {
|
||||
parent_id: Option<u32>,
|
||||
|
||||
text: String,
|
||||
|
||||
/// Check if a message has a POI location bound to it.
|
||||
/// These locations are also returned by `get_locations` method.
|
||||
/// The UI may decide to display a special icon beside such messages.
|
||||
has_location: bool,
|
||||
has_html: bool,
|
||||
view_type: MessageViewtype,
|
||||
@@ -109,6 +105,11 @@ enum MessageQuote {
|
||||
}
|
||||
|
||||
impl MessageObject {
|
||||
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
|
||||
let msg_id = MsgId::new(message_id);
|
||||
Self::from_msg_id(context, msg_id).await
|
||||
}
|
||||
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
@@ -344,7 +345,6 @@ pub enum SystemMessageType {
|
||||
SecurejoinMessage,
|
||||
LocationStreamingEnabled,
|
||||
LocationOnly,
|
||||
InvalidUnencryptedMail,
|
||||
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
@@ -385,7 +385,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
||||
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
||||
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,44 +546,6 @@ pub struct MessageData {
|
||||
pub quoted_message_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl MessageData {
|
||||
pub(crate) async fn create_message(self, context: &Context) -> Result<Message> {
|
||||
let mut message = Message::new(if let Some(viewtype) = self.viewtype {
|
||||
viewtype.into()
|
||||
} else if self.file.is_some() {
|
||||
Viewtype::File
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
message.set_text(self.text.unwrap_or_default());
|
||||
if self.html.is_some() {
|
||||
message.set_html(self.html);
|
||||
}
|
||||
if self.override_sender_name.is_some() {
|
||||
message.set_override_sender_name(self.override_sender_name);
|
||||
}
|
||||
if let Some(file) = self.file {
|
||||
message.set_file(file, None);
|
||||
}
|
||||
if let Some((latitude, longitude)) = self.location {
|
||||
message.set_location(latitude, longitude);
|
||||
}
|
||||
if let Some(id) = self.quoted_message_id {
|
||||
message
|
||||
.set_quote(
|
||||
context,
|
||||
Some(
|
||||
&Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("message to quote could not be loaded")?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageReadReceipt {
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.137.4"
|
||||
"version": "1.132.1"
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.137.4"
|
||||
version = "1.132.1"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.21"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.31"
|
||||
rustyline = "14"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
rusqlite = "0.30"
|
||||
rustyline = "12"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -3,7 +3,7 @@ extern crate dirs;
|
||||
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use deltachat::chat::{
|
||||
@@ -33,6 +33,14 @@ use tokio::fs;
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM jobs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
}
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
|
||||
@@ -9,6 +9,7 @@ extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use ansi_term::Color;
|
||||
@@ -19,7 +20,8 @@ use deltachat::context::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::EventType;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{EventType, Events};
|
||||
use log::{error, info, warn};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
@@ -297,8 +299,8 @@ impl Highlighter for DcHelper {
|
||||
self.highlighter.highlight(line, pos)
|
||||
}
|
||||
|
||||
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
||||
self.highlighter.highlight_char(line, pos, forced)
|
||||
fn highlight_char(&self, line: &str, pos: usize) -> bool {
|
||||
self.highlighter.highlight_char(line, pos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,10 +312,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = ContextBuilder::new(args[1].clone().into())
|
||||
.with_id(1)
|
||||
.open()
|
||||
.await?;
|
||||
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
tokio::task::spawn(async move {
|
||||
@@ -402,7 +401,7 @@ enum ExitResult {
|
||||
|
||||
async fn handle_cmd(
|
||||
line: &str,
|
||||
ctx: Context,
|
||||
mut ctx: Context,
|
||||
selected_chat: &mut ChatId,
|
||||
) -> Result<ExitResult, Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
||||
Pass --help to the CLI to see available options.
|
||||
"""
|
||||
|
||||
from deltachat_rpc_client import events, run_bot_cli
|
||||
|
||||
hooks = events.HookCollection()
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
it will echo back any message that has non-empty text and also supports the /help command.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from threading import Thread
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"""
|
||||
Example echo bot without using hooks
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45"]
|
||||
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.137.4"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -20,9 +19,8 @@ classifiers = [
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"imap-tools",
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
@@ -33,11 +31,14 @@ deltachat_rpc_client = [
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = [
|
||||
select = [
|
||||
"E", "W", # pycodestyle
|
||||
"F", # Pyflakes
|
||||
"N", # pep8-naming
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Delta Chat JSON-RPC high-level API"""
|
||||
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
|
||||
@@ -104,11 +104,7 @@ def _run_cli(
|
||||
if not client.is_configured():
|
||||
assert args.email, "Account is not configured and email must be provided"
|
||||
assert args.password, "Account is not configured and password must be provided"
|
||||
configure_thread = Thread(
|
||||
target=client.configure,
|
||||
daemon=True,
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
@@ -172,33 +168,3 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||
return "removed", addr, addr
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class futuremethod: # noqa: N801
|
||||
"""Decorator for async methods."""
|
||||
|
||||
def __init__(self, func):
|
||||
self._func = func
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
def future(*args):
|
||||
generator = self._func(instance, *args)
|
||||
res = next(generator)
|
||||
|
||||
def f():
|
||||
try:
|
||||
generator.send(res())
|
||||
except StopIteration as e:
|
||||
return e.value
|
||||
|
||||
return f
|
||||
|
||||
def wrapper(*args):
|
||||
f = future(*args)
|
||||
return f()
|
||||
|
||||
wrapper.future = future
|
||||
return wrapper
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
from warnings import warn
|
||||
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from ._utils import AttrDict
|
||||
from .chat import Chat
|
||||
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
@@ -30,10 +28,6 @@ class Account:
|
||||
"""Wait until the next event and return it."""
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
self._rpc.clear_all_events(self.id)
|
||||
|
||||
def remove(self) -> None:
|
||||
"""Remove the account."""
|
||||
self._rpc.remove_account(self.id)
|
||||
@@ -82,24 +76,9 @@ class Account:
|
||||
"""Get self avatar."""
|
||||
return self.get_config("selfavatar")
|
||||
|
||||
def check_qr(self, qr):
|
||||
return self._rpc.check_qr(self.id, qr)
|
||||
|
||||
def set_config_from_qr(self, qr: str):
|
||||
self._rpc.set_config_from_qr(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def configure(self):
|
||||
def configure(self) -> None:
|
||||
"""Configure an account."""
|
||||
yield self._rpc.configure.future(self.id)
|
||||
|
||||
def bring_online(self):
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
self.start_io()
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
self._rpc.configure(self.id)
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
@@ -118,11 +97,6 @@ class Account:
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
addr = account.get_config("addr")
|
||||
contact = self.create_contact(addr)
|
||||
return contact.create_chat()
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
return Contact(self, contact_id)
|
||||
@@ -132,7 +106,7 @@ class Account:
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self) -> list[AttrDict]:
|
||||
def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
@@ -157,7 +131,7 @@ class Account:
|
||||
with_self: bool = False,
|
||||
verified_only: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[list[Contact], list[AttrDict]]:
|
||||
) -> Union[List[Contact], List[AttrDict]]:
|
||||
"""Get a filtered list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
@@ -192,7 +166,7 @@ class Account:
|
||||
no_specials: bool = False,
|
||||
alldone_hint: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[list[Chat], list[AttrDict]]:
|
||||
) -> Union[List[Chat], List[AttrDict]]:
|
||||
"""Return list of chats.
|
||||
|
||||
:param query: if a string is specified only chats matching this query are returned.
|
||||
@@ -244,13 +218,13 @@ class Account:
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
See https://securejoin.delta.chat/ for protocol details.
|
||||
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
|
||||
|
||||
:param qrdata: The text of the scanned QR code.
|
||||
"""
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
@@ -262,15 +236,15 @@ class Account:
|
||||
"""Return the Message instance with the given ID."""
|
||||
return Message(self, msg_id)
|
||||
|
||||
def mark_seen_messages(self, messages: list[Message]) -> None:
|
||||
def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
"""Mark the given set of messages as seen."""
|
||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
|
||||
def delete_messages(self, messages: list[Message]) -> None:
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
"""Delete messages (local and remote)."""
|
||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
|
||||
def get_fresh_messages(self) -> list[Message]:
|
||||
def get_fresh_messages(self) -> List[Message]:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
@@ -280,12 +254,12 @@ class Account:
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def get_next_messages(self) -> list[Message]:
|
||||
def get_next_messages(self) -> List[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_next_messages(self) -> list[Message]:
|
||||
def wait_next_messages(self) -> List[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
@@ -309,13 +283,7 @@ class Account:
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def wait_for_reactions_changed(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.REACTIONS_CHANGED:
|
||||
return event
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import ChatVisibility, ViewType
|
||||
@@ -86,16 +84,14 @@ class Chat:
|
||||
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
|
||||
def set_ephemeral_timer(self, timer: int) -> None:
|
||||
"""Set ephemeral timer of this chat in seconds.
|
||||
|
||||
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||
"""Set ephemeral timer of this chat."""
|
||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
|
||||
def get_encryption_info(self) -> str:
|
||||
"""Return encryption info for this chat."""
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
@@ -119,7 +115,7 @@ class Chat:
|
||||
html: Optional[str] = None,
|
||||
viewtype: Optional[ViewType] = None,
|
||||
file: Optional[str] = None,
|
||||
location: Optional[tuple[float, float]] = None,
|
||||
location: Optional[Tuple[float, float]] = None,
|
||||
override_sender_name: Optional[str] = None,
|
||||
quoted_msg: Optional[Union[int, Message]] = None,
|
||||
) -> Message:
|
||||
@@ -144,10 +140,6 @@ class Chat:
|
||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_file(self, path):
|
||||
"""Send a file and return the resulting Message instance."""
|
||||
return self.send_message(file=path)
|
||||
|
||||
def send_videochat_invitation(self) -> Message:
|
||||
"""Send a videochat invitation and return the resulting Message instance."""
|
||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
@@ -158,7 +150,7 @@ class Chat:
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def forward_messages(self, messages: list[Message]) -> None:
|
||||
def forward_messages(self, messages: List[Message]) -> None:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
@@ -190,7 +182,7 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
"""get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
@@ -225,7 +217,7 @@ class Chat:
|
||||
contact_id = cnt
|
||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
def get_contacts(self) -> list[Contact]:
|
||||
def get_contacts(self) -> List[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single/direct chats self-address is not included.
|
||||
@@ -249,7 +241,7 @@ class Chat:
|
||||
contact: Optional[Contact] = None,
|
||||
timestamp_from: Optional["datetime"] = None,
|
||||
timestamp_to: Optional["datetime"] = None,
|
||||
) -> list[AttrDict]:
|
||||
) -> List[AttrDict]:
|
||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
@@ -257,7 +249,7 @@ class Chat:
|
||||
|
||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: dict[int, Contact] = {}
|
||||
contacts: Dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
location = AttrDict(loc)
|
||||
location["chat"] = self
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Event loop implementations offering high level event handling/hooking."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
@@ -38,16 +38,16 @@ class Client:
|
||||
def __init__(
|
||||
self,
|
||||
account: "Account",
|
||||
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
) -> None:
|
||||
self.account = account
|
||||
self.logger = logger or logging
|
||||
self._hooks: dict[type, set[tuple]] = {}
|
||||
self._hooks: Dict[type, Set[tuple]] = {}
|
||||
self._should_process_messages = 0
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
for hook, event in hooks:
|
||||
self.add_hook(hook, event)
|
||||
|
||||
|
||||
@@ -59,17 +59,6 @@ class EventType(str, Enum):
|
||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
"""Special chat ids"""
|
||||
|
||||
TRASH = 3
|
||||
ARCHIVED_LINK = 6
|
||||
ALLDONE_HINT = 7
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class ChatType(IntEnum):
|
||||
@@ -133,107 +122,3 @@ class SystemMessageType(str, Enum):
|
||||
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
|
||||
MULTI_DEVICE_SYNC = "MultiDeviceSync"
|
||||
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
|
||||
|
||||
|
||||
class MessageState(IntEnum):
|
||||
"""State of the message."""
|
||||
|
||||
UNDEFINED = 0
|
||||
IN_FRESH = 10
|
||||
IN_NOTICED = 13
|
||||
IN_SEEN = 16
|
||||
OUT_PREPARING = 18
|
||||
OUT_DRAFT = 19
|
||||
OUT_PENDING = 20
|
||||
OUT_FAILED = 24
|
||||
OUT_DELIVERED = 26
|
||||
OUT_MDN_RCVD = 28
|
||||
|
||||
|
||||
class MessageId(IntEnum):
|
||||
"""Special message ids"""
|
||||
|
||||
DAYMARKER = 9
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class CertificateChecks(IntEnum):
|
||||
"""Certificate checks mode"""
|
||||
|
||||
AUTOMATIC = 0
|
||||
STRICT = 1
|
||||
ACCEPT_INVALID_CERTIFICATES = 3
|
||||
|
||||
|
||||
class Connectivity(IntEnum):
|
||||
"""Connectivity states"""
|
||||
|
||||
NOT_CONNECTED = 1000
|
||||
CONNECTING = 2000
|
||||
WORKING = 3000
|
||||
CONNECTED = 4000
|
||||
|
||||
|
||||
class KeyGenType(IntEnum):
|
||||
"""Type of the key to generate"""
|
||||
|
||||
DEFAULT = 0
|
||||
RSA2048 = 1
|
||||
ED25519 = 2
|
||||
RSA4096 = 3
|
||||
|
||||
|
||||
# "Lp" means "login parameters"
|
||||
class LpAuthFlag(IntEnum):
|
||||
"""Authorization flags"""
|
||||
|
||||
OAUTH2 = 0x2
|
||||
NORMAL = 0x4
|
||||
|
||||
|
||||
class MediaQuality(IntEnum):
|
||||
"""Media quality setting"""
|
||||
|
||||
BALANCED = 0
|
||||
WORSE = 1
|
||||
|
||||
|
||||
class ProviderStatus(IntEnum):
|
||||
"""Provider status according to manual testing"""
|
||||
|
||||
OK = 1
|
||||
PREPARATION = 2
|
||||
BROKEN = 3
|
||||
|
||||
|
||||
class PushNotifyState(IntEnum):
|
||||
"""Push notifications state"""
|
||||
|
||||
NOT_CONNECTED = 0
|
||||
HEARTBEAT = 1
|
||||
CONNECTED = 2
|
||||
|
||||
|
||||
class ShowEmails(IntEnum):
|
||||
"""Show emails mode"""
|
||||
|
||||
OFF = 0
|
||||
ACCEPTED_CONTACTS = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class SocketSecurity(IntEnum):
|
||||
"""Socket security"""
|
||||
|
||||
AUTOMATIC = 0
|
||||
SSL = 1
|
||||
STARTTLS = 2
|
||||
PLAIN = 3
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type"""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
JITSI = 2
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .account import Account
|
||||
@@ -23,7 +21,7 @@ class DeltaChat:
|
||||
account_id = self.rpc.add_account()
|
||||
return Account(self, account_id)
|
||||
|
||||
def get_all_accounts(self) -> list[Account]:
|
||||
def get_all_accounts(self) -> List[Account]:
|
||||
"""Return a list of all available accounts."""
|
||||
account_ids = self.rpc.get_all_account_ids()
|
||||
return [Account(self, account_id) for account_id in account_ids]
|
||||
@@ -46,6 +44,6 @@ class DeltaChat:
|
||||
"""Get information about the Delta Chat core in this system."""
|
||||
return AttrDict(self.rpc.get_system_info())
|
||||
|
||||
def set_translations(self, translations: dict[str, str]) -> None:
|
||||
def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
"""
|
||||
Internal Python-level IMAP handling used by the tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
import io
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from . import Account, const
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
ALL = "1:*"
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.SocketSecurity.PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.SocketSecurity.STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, imaplib.IMAP4.abort):
|
||||
print("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.folder.create(foldername)
|
||||
except errors.MailboxFolderCreateError as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
|
||||
def select_folder(self, foldername: str) -> tuple:
|
||||
assert not self._idling
|
||||
return self.conn.folder.set(foldername)
|
||||
|
||||
def select_config_folder(self, config_name: str):
|
||||
"""Return info about selected folder if it is
|
||||
configured, otherwise None.
|
||||
"""
|
||||
if "_" not in config_name:
|
||||
config_name = f"configured_{config_name}_folder"
|
||||
foldername = self.account.get_config(config_name)
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
return None
|
||||
|
||||
def list_folders(self) -> list[str]:
|
||||
"""return list of all existing folder names."""
|
||||
assert not self._idling
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
|
||||
def delete(self, uid_list: str, expunge=True):
|
||||
"""delete a range of messages (imap-syntax).
|
||||
If expunge is true, perform the expunge-operation
|
||||
to make sure the messages are really gone and not
|
||||
just flagged as deleted.
|
||||
"""
|
||||
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
|
||||
if expunge:
|
||||
self.conn.expunge()
|
||||
|
||||
def get_all_messages(self) -> list[MailMessage]:
|
||||
assert not self._idling
|
||||
return list(self.conn.fetch())
|
||||
|
||||
def get_unread_messages(self) -> list[str]:
|
||||
assert not self._idling
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
if messages:
|
||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||
print("marked seen:", messages, res)
|
||||
|
||||
def get_unread_cnt(self) -> int:
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
def dump_imap_structures(self, dir, logfile):
|
||||
assert not self._idling
|
||||
stream = io.StringIO()
|
||||
|
||||
def log(*args, **kwargs):
|
||||
kwargs["file"] = stream
|
||||
print(*args, **kwargs)
|
||||
|
||||
empty_folders = []
|
||||
for imapfolder in self.list_folders():
|
||||
self.select_folder(imapfolder)
|
||||
messages = list(self.get_all_messages())
|
||||
if not messages:
|
||||
empty_folders.append(imapfolder)
|
||||
continue
|
||||
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
for msg in self.conn.fetch(mark_seen=False):
|
||||
body = getattr(msg.obj, "text", None)
|
||||
if not body:
|
||||
body = getattr(msg.obj, "html", None)
|
||||
if not body:
|
||||
log("Message", msg.uid, "has empty body")
|
||||
continue
|
||||
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
fn = path.joinpath(str(msg.uid))
|
||||
fn.write_bytes(body)
|
||||
log("Message", msg.uid, fn)
|
||||
log(
|
||||
"Message",
|
||||
msg.uid,
|
||||
msg.flags,
|
||||
"Message-Id:",
|
||||
msg.obj.get("Message-Id"),
|
||||
)
|
||||
|
||||
if empty_folders:
|
||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||
|
||||
print(stream.getvalue(), file=logfile)
|
||||
|
||||
@contextmanager
|
||||
def idle(self):
|
||||
"""return Idle ContextManager."""
|
||||
idle_manager = IdleManager(self)
|
||||
try:
|
||||
yield idle_manager
|
||||
finally:
|
||||
idle_manager.done()
|
||||
|
||||
def append(self, folder: str, msg: str):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap) -> None:
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> list[bytes]:
|
||||
"""(blocking) wait for next idle message from server."""
|
||||
self.log("imap-direct: calling idle_check")
|
||||
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
self.log(f"imap-direct: idle_check returned {res!r}")
|
||||
return res
|
||||
|
||||
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
if b"EXISTS" in item or b"RECENT" in item:
|
||||
return item
|
||||
|
||||
def wait_for_seen(self, timeout=None) -> int:
|
||||
"""Return first message with SEEN flag from a running idle-stream."""
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
if FETCH in item:
|
||||
self.log(str(item))
|
||||
if FLAGS in item and rb"\Seen" in item:
|
||||
return int(item.split(b" ")[1])
|
||||
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
return self.direct_imap.conn.idle.stop()
|
||||
@@ -1,10 +1,7 @@
|
||||
"""High-level classes for event processing and filtering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||
|
||||
from .const import EventType
|
||||
|
||||
@@ -265,9 +262,9 @@ class HookCollection:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
|
||||
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
|
||||
return iter(self._hooks)
|
||||
|
||||
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
||||
|
||||
@@ -3,7 +3,6 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import EventType
|
||||
from .contact import Contact
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -22,10 +21,9 @@ class Message:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
def send_reaction(self, *reaction: str) -> "Message":
|
||||
def send_reaction(self, *reaction: str):
|
||||
"""Send a reaction to this message."""
|
||||
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
return Message(self.account, msg_id)
|
||||
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
"""Get a snapshot with the properties of this message."""
|
||||
@@ -63,10 +61,3 @@ class Message:
|
||||
|
||||
def get_webxdc_info(self) -> dict:
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
def wait_until_delivered(self) -> None:
|
||||
"""Consume events until the message is delivered."""
|
||||
while True:
|
||||
event = self.account.wait_for_event()
|
||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||
break
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
from typing import AsyncGenerator, Optional
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
from ._utils import futuremethod
|
||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
@@ -40,10 +37,9 @@ class ACFactory:
|
||||
assert not account.is_configured()
|
||||
return account
|
||||
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
def new_configured_account(self) -> Account:
|
||||
account = self.new_preconfigured_account()
|
||||
yield account.configure.future()
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
@@ -53,15 +49,17 @@ class ACFactory:
|
||||
bot.configure(credentials["email"], credentials["password"])
|
||||
return bot
|
||||
|
||||
@futuremethod
|
||||
def get_online_account(self):
|
||||
account = yield self.new_configured_account.future()
|
||||
account.bring_online()
|
||||
def get_online_account(self) -> Account:
|
||||
account = self.new_configured_account()
|
||||
account.start_io()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
return account
|
||||
|
||||
def get_online_accounts(self, num: int) -> list[Account]:
|
||||
futures = [self.get_online_account.future() for _ in range(num)]
|
||||
return [f() for f in futures]
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
@@ -73,10 +71,6 @@ class ACFactory:
|
||||
ac_clone.configure()
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
|
||||
@@ -1,62 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Empty, Queue
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Iterator, Optional
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RpcFuture:
|
||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||
self.rpc = rpc
|
||||
self.request_id = request_id
|
||||
self.event = event
|
||||
|
||||
def __call__(self):
|
||||
self.event.wait()
|
||||
response = self.rpc.request_results.pop(self.request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
|
||||
class RpcMethod:
|
||||
def __init__(self, rpc: "Rpc", name: str):
|
||||
self.rpc = rpc
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *args) -> Any:
|
||||
"""Synchronously calls JSON-RPC method."""
|
||||
future = self.future(*args)
|
||||
return future()
|
||||
|
||||
def future(self, *args) -> Any:
|
||||
"""Asynchronously calls JSON-RPC method."""
|
||||
request_id = next(self.rpc.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": self.name,
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.rpc.request_events[request_id] = event
|
||||
self.rpc.request_queue.put(request)
|
||||
|
||||
return RpcFuture(self.rpc, request_id, event)
|
||||
|
||||
|
||||
class Rpc:
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
@@ -69,11 +25,11 @@ class Rpc:
|
||||
self._kwargs = kwargs
|
||||
self.process: subprocess.Popen
|
||||
self.id_iterator: Iterator[int]
|
||||
self.event_queues: dict[int, Queue]
|
||||
self.event_queues: Dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: dict[int, Event]
|
||||
self.request_events: Dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: dict[int, Any]
|
||||
self.request_results: Dict[int, Any]
|
||||
self.request_queue: Queue[Any]
|
||||
self.closing: bool
|
||||
self.reader_thread: Thread
|
||||
@@ -188,14 +144,25 @@ class Rpc:
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
|
||||
def clear_all_events(self, account_id: int):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
queue = self.get_queue(account_id)
|
||||
try:
|
||||
while True:
|
||||
queue.get_nowait()
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
return RpcMethod(self, attr)
|
||||
def method(*args) -> Any:
|
||||
request_id = next(self.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
self.request_queue.put(request)
|
||||
event.wait()
|
||||
|
||||
response = self.request_results.pop(request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
return method
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
|
||||
def wait_for_chatlist_and_specific_item(account, chat_id):
|
||||
first_event = ""
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED:
|
||||
first_event = "change"
|
||||
break
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||
first_event = "item_change"
|
||||
break
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
|
||||
break
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
|
||||
break
|
||||
|
||||
|
||||
def wait_for_chatlist_specific_item(account, chat_id):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||
break
|
||||
|
||||
|
||||
def wait_for_chatlist(account):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED:
|
||||
break
|
||||
|
||||
|
||||
def test_delivery_status(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes (delivered, read)
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
alice.clear_all_events()
|
||||
bob.stop_io()
|
||||
alice.stop_io()
|
||||
alice_chat_bob.send_text("hi")
|
||||
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
|
||||
alice.clear_all_events()
|
||||
alice.start_io()
|
||||
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
|
||||
bob.clear_all_events()
|
||||
bob.start_io()
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg.get_snapshot().chat.accept()
|
||||
msg.mark_seen()
|
||||
|
||||
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSG_READ:
|
||||
break
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
||||
|
||||
|
||||
def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes failed
|
||||
"""
|
||||
(alice,) = acfactory.get_online_accounts(1)
|
||||
|
||||
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
||||
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
failing_message = invalid_chat.send_text("test")
|
||||
|
||||
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
|
||||
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSG_FAILED:
|
||||
break
|
||||
|
||||
wait_for_chatlist_specific_item(alice, invalid_chat.id)
|
||||
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test if download on demand emits chatlist update events.
|
||||
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
)
|
||||
|
||||
msg_id = alice.wait_for_incoming_msg_event().msg_id
|
||||
|
||||
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
|
||||
|
||||
alice.clear_all_events()
|
||||
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
|
||||
alice._rpc.download_full_message(alice.id, msg_id)
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
|
||||
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
bob.wait_for_incoming_msg_event()
|
||||
|
||||
alice_second_device: Account = acfactory.get_unconfigured_account()
|
||||
|
||||
alice._rpc.provide_backup.future(alice.id)
|
||||
backup_code = alice._rpc.get_backup_qr(alice.id)
|
||||
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
|
||||
alice_second_device.start_io()
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
bob.clear_all_events()
|
||||
return [alice, alice_second_device, bob, alice_chat_bob]
|
||||
|
||||
|
||||
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test that chatlist changed events are emitted for the second device
|
||||
when the message is marked as read on the first device
|
||||
"""
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||
|
||||
alice_chat_bob.send_text("hello")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
bob_chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
bob.get_chat_by_id(bob_chat_id).send_text("hello")
|
||||
|
||||
# make sure alice_second_device already received the message
|
||||
alice_second_device.wait_for_incoming_msg_event()
|
||||
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg = alice.get_message_by_id(event.msg_id)
|
||||
alice_second_device.clear_all_events()
|
||||
msg.mark_seen()
|
||||
|
||||
wait_for_chatlist_specific_item(bob, bob_chat_id)
|
||||
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
||||
|
||||
|
||||
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test multidevice sync: syncing chat visibility and muting across multiple devices
|
||||
"""
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||
|
||||
alice_chat_bob.archive()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().archived
|
||||
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.pin()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.mute()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().is_muted
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
from deltachat_rpc_client import Chat, SpecialContactId
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
@@ -164,21 +164,6 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
|
||||
def test_setup_contact_resetup(acfactory) -> None:
|
||||
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice = acfactory.resetup_account(alice)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_verified_group_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
@@ -190,7 +175,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
@@ -198,8 +183,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
@@ -207,7 +191,8 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
@@ -254,7 +239,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
@@ -262,8 +247,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
@@ -271,7 +255,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
@@ -579,40 +564,3 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
|
||||
def test_withdraw_securejoin_qr(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
assert alice_chat.get_basic_snapshot().is_protected
|
||||
logging.info("Bob joins verified group")
|
||||
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||
|
||||
logging.info("Alice withdraws QR code.")
|
||||
qr = alice.check_qr(qr_code)
|
||||
assert qr["kind"] == "withdrawVerifyGroup"
|
||||
alice.set_config_from_qr(qr_code)
|
||||
|
||||
logging.info("Bob scans withdrawn QR code.")
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
|
||||
logging.info("Bob scanned withdrawn QR code")
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
|
||||
break
|
||||
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
|
||||
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.direct_imap import DirectImap
|
||||
from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -444,172 +439,3 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
message = alice.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.show_padlock
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
m.wait_until_delivered()
|
||||
|
||||
logging.info("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
msgs[-1].wait_until_delivered()
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
logging.info("wait for ac2 to receive a reaction")
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
|
||||
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1_addr
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_preconfigured_account()
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.configure()
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
msg1.wait_until_delivered()
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = DirectImap(ac2)
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
logging.info("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
download_limit = 300000
|
||||
|
||||
alice, *others = acfactory.get_online_accounts(n_accounts)
|
||||
bob = others[0]
|
||||
|
||||
alice_group = alice.create_group("test group")
|
||||
for account in others:
|
||||
chat = account.create_chat(alice)
|
||||
chat.send_text("Hello Alice!")
|
||||
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||
|
||||
contact_addr = account.get_config("addr")
|
||||
contact = alice.create_contact(contact_addr, "")
|
||||
|
||||
alice_group.add_contact(contact)
|
||||
|
||||
if n_accounts == 2:
|
||||
bob_chat_alice = bob.create_chat(alice)
|
||||
bob.set_config("download_limit", str(download_limit))
|
||||
|
||||
alice_group.send_text("hi")
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "hi"
|
||||
bob_group = snapshot.chat
|
||||
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
|
||||
for i in range(10):
|
||||
logging.info("Sending message %s", i)
|
||||
alice_group.send_file(str(path))
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
if n_accounts > 2:
|
||||
assert snapshot.chat == bob_group
|
||||
else:
|
||||
# Group contains only Alice and Bob,
|
||||
# so partially downloaded messages are
|
||||
# hard to distinguish from private replies to group messages.
|
||||
#
|
||||
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
|
||||
assert snapshot.chat == bob_chat_alice
|
||||
|
||||
|
||||
def test_markseen_contact_request(acfactory, tmp_path):
|
||||
"""
|
||||
Test that seen status is synchronized for contact request messages
|
||||
even though read receipt is not sent.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets up a second device.
|
||||
bob.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
bob2 = acfactory.get_unconfigured_account()
|
||||
bob2.import_backup(files[0])
|
||||
bob2.start_io()
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
|
||||
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
message.mark_seen()
|
||||
while True:
|
||||
event = bob2.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
break
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
@@ -22,9 +22,10 @@ skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
ruff
|
||||
black
|
||||
commands =
|
||||
ruff format --quiet --diff src/ examples/ tests/
|
||||
ruff check src/ examples/ tests/
|
||||
black --quiet --check --diff src/ examples/ tests/
|
||||
ruff src/ examples/ tests/
|
||||
|
||||
[pytest]
|
||||
timeout = 300
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.137.4"
|
||||
version = "1.132.1"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -14,12 +14,12 @@ deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = "..", default-features = false }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.11.3" }
|
||||
futures-lite = "2.3.0"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "2.0.0"
|
||||
log = "0.4"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||
tokio = { version = "1.33.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.9"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ deltachat-rpc-server
|
||||
The common use case for this program is to create bindings to use Delta Chat core from programming
|
||||
languages other than Rust, for example:
|
||||
|
||||
1. Python: https://pypi.org/project/deltachat-rpc-client/
|
||||
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
|
||||
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
||||
|
||||
Run `deltachat-rpc-server --version` to check the version of the server.
|
||||
|
||||
@@ -68,7 +68,7 @@ async fn main_impl() -> Result<()> {
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let accounts = Arc::new(RwLock::new(accounts));
|
||||
let state = CommandApi::from_arc(accounts.clone()).await;
|
||||
let state = CommandApi::from_arc(accounts.clone());
|
||||
|
||||
let (client, mut out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), state.clone());
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[package]
|
||||
name = "deltachat-time"
|
||||
version = "1.0.0"
|
||||
description = "Time-related tools"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,35 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::RwLock;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
static SYSTEM_TIME_SHIFT: RwLock<Duration> = RwLock::new(Duration::new(0, 0));
|
||||
|
||||
/// Fake struct for mocking `SystemTime::now()` for test purposes. You still need to use
|
||||
/// `SystemTime` as a struct representing a system time.
|
||||
pub struct SystemTimeTools();
|
||||
|
||||
impl SystemTimeTools {
|
||||
pub const UNIX_EPOCH: SystemTime = SystemTime::UNIX_EPOCH;
|
||||
|
||||
pub fn now() -> SystemTime {
|
||||
return SystemTime::now() + *SYSTEM_TIME_SHIFT.read().unwrap();
|
||||
}
|
||||
|
||||
/// Simulates a system clock forward adjustment by `duration`.
|
||||
pub fn shift(duration: Duration) {
|
||||
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
SystemTimeTools::shift(Duration::from_secs(60));
|
||||
let t = SystemTimeTools::now();
|
||||
assert!(t > SystemTime::now());
|
||||
}
|
||||
}
|
||||
18
deny.toml
18
deny.toml
@@ -1,4 +1,5 @@
|
||||
[advisories]
|
||||
unmaintained = "allow"
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2022-0093",
|
||||
@@ -9,12 +10,6 @@ ignore = [
|
||||
# There is no fix at the time of writing this (2023-11-28).
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
|
||||
"RUSTSEC-2023-0071",
|
||||
|
||||
# Unmaintained ansi_term
|
||||
"RUSTSEC-2021-0139",
|
||||
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -26,7 +21,6 @@ skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
@@ -38,9 +32,13 @@ skip = [
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "0.10.2" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "fd-lock", version = "3.0.13" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "h2", version = "0.3.22" },
|
||||
{ name = "http-body", version = "0.4.5" },
|
||||
{ name = "http", version = "0.2.11" },
|
||||
{ name = "hyper", version = "0.14.27" },
|
||||
{ name = "idna", version = "0.4.0" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
@@ -57,10 +55,8 @@ skip = [
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "toml_edit", version = "0.21.1" },
|
||||
{ name = "untrusted", version = "0.7.1" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
@@ -73,7 +69,6 @@ skip = [
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
]
|
||||
|
||||
|
||||
@@ -105,4 +100,5 @@ license-files = [
|
||||
github = [
|
||||
"async-email",
|
||||
"deltachat",
|
||||
"djc",
|
||||
]
|
||||
|
||||
288
flake.lock
generated
288
flake.lock
generated
@@ -1,288 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"android": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712088936,
|
||||
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"android",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711099426,
|
||||
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713421495,
|
||||
"narHash": "sha256-5vVF9W1tJT+WdfpWAEG76KywktKDAW/71mVmNHEHjac=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "fd47b1f9404fae02a4f38bd9f4b12bad7833c96b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713520724,
|
||||
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1710156097,
|
||||
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1711703276,
|
||||
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1713248628,
|
||||
"narHash": "sha256-NLznXB5AOnniUtZsyy/aPWOk8ussTuePp2acb9U+ISA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5672bc9dbf9d88246ddab5ac454e82318d094bb8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1713562564,
|
||||
"narHash": "sha256-NQpYhgoy0M89g9whRixSwsHb8RFIbwlxeYiVSDwSXJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "92d295f588631b0db2da509f381b4fb1e74173c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1713537308,
|
||||
"narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"android": "android",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"naersk": "naersk",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1713373173,
|
||||
"narHash": "sha256-octd9BFY9G/Gbr4KfwK4itZp4Lx+qvJeRRcYnN+dEH8=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "46702ffc1a02a2ac153f1d1ce619ec917af8f3a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
541
flake.nix
541
flake.nix
@@ -1,541 +0,0 @@
|
||||
{
|
||||
description = "Delta Chat core";
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
android.url = "github:tadfisher/android-nixpkgs";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs.stdenv) isDarwin;
|
||||
fenixPkgs = fenix.packages.${system};
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
|
||||
# Include only necessary files
|
||||
# to avoid rebuilds e.g. when README.md or flake.nix changes.
|
||||
include = [
|
||||
./benches
|
||||
./assets
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
./CMakeLists.txt
|
||||
./CONTRIBUTING.md
|
||||
./deltachat_derive
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
./deltachat-jsonrpc
|
||||
./deltachat-ratelimit
|
||||
./deltachat-repl
|
||||
./deltachat-rpc-client
|
||||
./deltachat-time
|
||||
./deltachat-rpc-server
|
||||
./format-flowed
|
||||
./release-date.in
|
||||
./src
|
||||
];
|
||||
exclude = [
|
||||
(nix-filter.lib.matchExt "nix")
|
||||
"flake.lock"
|
||||
];
|
||||
};
|
||||
|
||||
# Map from architecture name to rust targets and nixpkgs targets.
|
||||
arch2targets = {
|
||||
"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;
|
||||
outputHashes = {
|
||||
"email-0.0.20" = "sha256-rV4Uzqt2Qdrfi5Ti1r+Si1c2iW1kKyWLwOgLkQ5JGGw=";
|
||||
"encoded-words-0.2.0" = "sha256-KK9st0hLFh4dsrnLd6D8lC6pRFFs8W+WpZSGMGJcosk=";
|
||||
"lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
|
||||
};
|
||||
};
|
||||
mkRustPackage = packageName:
|
||||
naersk'.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
buildInputs = pkgs.lib.optionals isDarwin [
|
||||
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
};
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
rustTarget = "x86_64-pc-windows-gnu";
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
mkWin32RustPackage = packageName:
|
||||
let
|
||||
rustTarget = "i686-pc-windows-gnu";
|
||||
in
|
||||
let
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
|
||||
# Get rid of MCF Gthread library.
|
||||
# See <https://github.com/NixOS/nixpkgs/issues/156343>
|
||||
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
|
||||
# for details.
|
||||
#
|
||||
# Use DWARF-2 instead of SJLJ for exception handling.
|
||||
winCC = pkgsWin32.buildPackages.wrapCC (
|
||||
(pkgsWin32.buildPackages.gcc-unwrapped.override
|
||||
({
|
||||
threadsCross = {
|
||||
model = "win32";
|
||||
package = null;
|
||||
};
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions --with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
};
|
||||
|
||||
mkCrossRustPackage = arch: packageName:
|
||||
let
|
||||
rustTarget = arch2targets."${arch}".rustTarget;
|
||||
crossTarget = arch2targets."${arch}".crossTarget;
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
in
|
||||
let
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
androidAttrs = {
|
||||
armeabi-v7a = {
|
||||
cc = "armv7a-linux-androideabi19-clang";
|
||||
rustTarget = "armv7-linux-androideabi";
|
||||
};
|
||||
arm64-v8a = {
|
||||
cc = "aarch64-linux-android21-clang";
|
||||
rustTarget = "aarch64-linux-android";
|
||||
};
|
||||
};
|
||||
|
||||
mkAndroidRustPackage = arch: packageName:
|
||||
let
|
||||
rustTarget = androidAttrs.${arch}.rustTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
|
||||
targetCcName = androidAttrs.${arch}.cc;
|
||||
targetCc = "${targetToolchain}/bin/${targetCcName}";
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch: {
|
||||
"deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
let
|
||||
rpc-server = mkCrossRustPackage arch "deltachat-rpc-server";
|
||||
in
|
||||
{
|
||||
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}" = rpc-server;
|
||||
"deltachat-rpc-server-${arch}-wheel" =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixpkgs-fmt;
|
||||
|
||||
packages =
|
||||
mkRustPackages "aarch64-linux" //
|
||||
mkRustPackages "i686-linux" //
|
||||
mkRustPackages "x86_64-linux" //
|
||||
mkRustPackages "armv7l-linux" //
|
||||
mkRustPackages "armv6l-linux" //
|
||||
mkAndroidPackages "armeabi-v7a" //
|
||||
mkAndroidPackages "arm64-v8a" //
|
||||
mkAndroidPackages "x86" //
|
||||
mkAndroidPackages "x86_64" // rec {
|
||||
# Run with `nix run .#deltachat-repl foo.db`.
|
||||
deltachat-repl = mkRustPackage "deltachat-repl";
|
||||
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
|
||||
|
||||
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win64-wheel =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win64-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win64
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
|
||||
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win32-wheel =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win32-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win32
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
# Run `nix build .#docs` to get C docs generated in `./result/`.
|
||||
docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [ pkgs.doxygen ];
|
||||
buildPhase = ''scripts/run-doxygen.sh'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
|
||||
};
|
||||
|
||||
libdeltachat =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
version = manifest.version;
|
||||
src = rustSrc;
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
pkgs.rustPlatform.cargoSetupHook
|
||||
pkgs.cargo
|
||||
];
|
||||
buildInputs = pkgs.lib.optionals isDarwin [
|
||||
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
|
||||
pkgs.darwin.apple_sdk.frameworks.Security
|
||||
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
|
||||
pkgs.libiconv
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
substituteInPlace $out/include/deltachat.h \
|
||||
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
|
||||
'';
|
||||
};
|
||||
|
||||
# Source package for deltachat-rpc-server.
|
||||
# Fake package that downloads Linux version,
|
||||
# needed to install deltachat-rpc-server on Android with `pip`.
|
||||
deltachat-rpc-server-source =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-source";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
|
||||
};
|
||||
|
||||
deltachat-rpc-client =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-rpc-client";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.imap-tools
|
||||
];
|
||||
};
|
||||
|
||||
deltachat-python =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-python";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./python;
|
||||
format = "pyproject";
|
||||
buildInputs = [
|
||||
libdeltachat
|
||||
];
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
];
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.pkgconfig
|
||||
pkgs.python3Packages.cffi
|
||||
pkgs.python3Packages.imap-tools
|
||||
pkgs.python3Packages.pluggy
|
||||
pkgs.python3Packages.requests
|
||||
];
|
||||
};
|
||||
python-docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
buildInputs = [
|
||||
deltachat-python
|
||||
deltachat-rpc-client
|
||||
pkgs.python3Packages.breathe
|
||||
pkgs.python3Packages.sphinx_rtd_theme
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.sphinx ];
|
||||
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
|
||||
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
clippy
|
||||
rustc
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
cargo-deny
|
||||
perl # needed to build vendored OpenSSL
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
608
fuzz/Cargo.lock
generated
608
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -29,13 +29,9 @@ module.exports = {
|
||||
DC_DOWNLOAD_FAILURE: 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
|
||||
DC_EVENT_CHATLIST_CHANGED: 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
DC_EVENT_CHAT_MODIFIED: 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS: 2041,
|
||||
DC_EVENT_CONFIG_SYNCED: 2111,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED: 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED: 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE: 151,
|
||||
@@ -64,6 +60,8 @@ module.exports = {
|
||||
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
||||
DC_EVENT_SMTP_CONNECTED: 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_UI_CHATLIST_CHANGED: 2200,
|
||||
DC_EVENT_UI_CHATLIST_ITEM_CHANGED: 2201,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
@@ -83,7 +81,6 @@ module.exports = {
|
||||
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
|
||||
DC_INFO_GROUP_IMAGE_CHANGED: 3,
|
||||
DC_INFO_GROUP_NAME_CHANGED: 2,
|
||||
DC_INFO_INVALID_UNENCRYPTED_MAIL: 13,
|
||||
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
|
||||
DC_INFO_LOCATION_ONLY: 9,
|
||||
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
|
||||
@@ -117,9 +114,6 @@ module.exports = {
|
||||
DC_PROVIDER_STATUS_BROKEN: 3,
|
||||
DC_PROVIDER_STATUS_OK: 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION: 2,
|
||||
DC_PUSH_CONNECTED: 2,
|
||||
DC_PUSH_HEARTBEAT: 1,
|
||||
DC_PUSH_NOT_CONNECTED: 0,
|
||||
DC_QR_ACCOUNT: 250,
|
||||
DC_QR_ADDR: 320,
|
||||
DC_QR_ASK_VERIFYCONTACT: 200,
|
||||
@@ -233,13 +227,11 @@ module.exports = {
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU: 124,
|
||||
DC_STR_IMAGE: 9,
|
||||
DC_STR_INCOMING_MESSAGES: 103,
|
||||
DC_STR_INVALID_UNENCRYPTED_MAIL: 174,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
|
||||
DC_STR_LOCATION: 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER: 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU: 136,
|
||||
DC_STR_MESSAGES: 114,
|
||||
DC_STR_MESSAGE_ADD_MEMBER: 173,
|
||||
DC_STR_MSGACTIONBYME: 63,
|
||||
DC_STR_MSGACTIONBYUSER: 62,
|
||||
DC_STR_MSGADDMEMBER: 17,
|
||||
@@ -259,7 +251,6 @@ module.exports = {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||
DC_STR_REACTED_BY: 177,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
|
||||
@@ -287,7 +278,6 @@ module.exports = {
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
|
||||
DC_STR_VOICEMESSAGE: 7,
|
||||
DC_STR_WELCOME_MESSAGE: 71,
|
||||
DC_STR_YOU_REACTED: 176,
|
||||
DC_TEXT1_DRAFT: 1,
|
||||
DC_TEXT1_SELF: 3,
|
||||
DC_TEXT1_USERNAME: 2,
|
||||
|
||||
@@ -34,10 +34,8 @@ module.exports = {
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
|
||||
2200: 'DC_EVENT_UI_CHATLIST_CHANGED',
|
||||
2201: 'DC_EVENT_UI_CHATLIST_ITEM_CHANGED'
|
||||
}
|
||||
|
||||
@@ -29,13 +29,9 @@ export enum C {
|
||||
DC_DOWNLOAD_FAILURE = 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
|
||||
DC_EVENT_CHATLIST_CHANGED = 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
DC_EVENT_CHAT_MODIFIED = 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041,
|
||||
DC_EVENT_CONFIG_SYNCED = 2111,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED = 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED = 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE = 151,
|
||||
@@ -64,6 +60,8 @@ export enum C {
|
||||
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
||||
DC_EVENT_SMTP_CONNECTED = 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_UI_CHATLIST_CHANGED = 2200,
|
||||
DC_EVENT_UI_CHATLIST_ITEM_CHANGED = 2201,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
@@ -83,7 +81,6 @@ export enum C {
|
||||
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
|
||||
DC_INFO_GROUP_IMAGE_CHANGED = 3,
|
||||
DC_INFO_GROUP_NAME_CHANGED = 2,
|
||||
DC_INFO_INVALID_UNENCRYPTED_MAIL = 13,
|
||||
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
|
||||
DC_INFO_LOCATION_ONLY = 9,
|
||||
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
|
||||
@@ -117,9 +114,6 @@ export enum C {
|
||||
DC_PROVIDER_STATUS_BROKEN = 3,
|
||||
DC_PROVIDER_STATUS_OK = 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2,
|
||||
DC_PUSH_CONNECTED = 2,
|
||||
DC_PUSH_HEARTBEAT = 1,
|
||||
DC_PUSH_NOT_CONNECTED = 0,
|
||||
DC_QR_ACCOUNT = 250,
|
||||
DC_QR_ADDR = 320,
|
||||
DC_QR_ASK_VERIFYCONTACT = 200,
|
||||
@@ -233,13 +227,11 @@ export enum C {
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
|
||||
DC_STR_IMAGE = 9,
|
||||
DC_STR_INCOMING_MESSAGES = 103,
|
||||
DC_STR_INVALID_UNENCRYPTED_MAIL = 174,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||
DC_STR_LOCATION = 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
|
||||
DC_STR_MESSAGES = 114,
|
||||
DC_STR_MESSAGE_ADD_MEMBER = 173,
|
||||
DC_STR_MSGACTIONBYME = 63,
|
||||
DC_STR_MSGACTIONBYUSER = 62,
|
||||
DC_STR_MSGADDMEMBER = 17,
|
||||
@@ -259,7 +251,6 @@ export enum C {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_REACTED_BY = 177,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||
@@ -287,7 +278,6 @@ export enum C {
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||
DC_STR_VOICEMESSAGE = 7,
|
||||
DC_STR_WELCOME_MESSAGE = 71,
|
||||
DC_STR_YOU_REACTED = 176,
|
||||
DC_TEXT1_DRAFT = 1,
|
||||
DC_TEXT1_SELF = 3,
|
||||
DC_TEXT1_USERNAME = 2,
|
||||
@@ -331,10 +321,8 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2200: 'DC_EVENT_UI_CHATLIST_CHANGED',
|
||||
2201: 'DC_EVENT_UI_CHATLIST_ITEM_CHANGED',
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export class AccountManager extends EventEmitter {
|
||||
static newTemporary() {
|
||||
let directory = null
|
||||
while (true) {
|
||||
const randomString = Math.random().toString(36).substring(2, 5)
|
||||
const randomString = Math.random().toString(36).substr(2, 5)
|
||||
directory = join(tmpdir(), 'deltachat-' + randomString)
|
||||
if (!existsSync(directory)) break
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#define NAPI_VERSION 4
|
||||
#define NAPI_EXPERIMENTAL
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@@ -3047,6 +3048,14 @@ NAPI_METHOD(dcn_accounts_select_account) {
|
||||
NAPI_RETURN_UINT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_all_work_done) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
|
||||
int result = dc_accounts_all_work_done(dcn_accounts->dc_accounts);
|
||||
NAPI_RETURN_INT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_start_io) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
@@ -3373,6 +3382,7 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_get_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_get_selected_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_select_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_all_work_done);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_start_io);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_stop_io);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
// @ts-check
|
||||
import { DeltaChat } from '../dist/index.js'
|
||||
import DeltaChat from '../dist'
|
||||
|
||||
import { deepStrictEqual, strictEqual } from 'assert'
|
||||
import chai, { expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import { EventId2EventName, C } from '../dist/constants.js'
|
||||
import { EventId2EventName, C } from '../dist/constants'
|
||||
import { join } from 'path'
|
||||
import { statSync } from 'fs'
|
||||
import { Context } from '../dist/context.js'
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
import { Context } from '../dist/context'
|
||||
chai.use(chaiAsPromised)
|
||||
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
|
||||
|
||||
@@ -26,8 +22,6 @@ function createTempUser(chatmailDomain) {
|
||||
}
|
||||
|
||||
describe('static tests', function () {
|
||||
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
|
||||
|
||||
it('reverse lookup of events', function () {
|
||||
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
|
||||
const eventValues = Object.values(EventId2EventName)
|
||||
@@ -241,7 +235,7 @@ describe('Basic offline Tests', function () {
|
||||
'delete_device_after',
|
||||
'delete_server_after',
|
||||
'deltachat_core_version',
|
||||
'displayname',
|
||||
'display_name',
|
||||
'download_limit',
|
||||
'e2ee_enabled',
|
||||
'entered_account_settings',
|
||||
@@ -252,7 +246,6 @@ describe('Basic offline Tests', function () {
|
||||
'journal_mode',
|
||||
'key_gen_type',
|
||||
'last_housekeeping',
|
||||
'last_cant_decrypt_outgoing_msgs',
|
||||
'level',
|
||||
'mdns_enabled',
|
||||
'media_quality',
|
||||
@@ -703,7 +696,7 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
})
|
||||
|
||||
describe('Integration tests', function () {
|
||||
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
|
||||
this.timeout(60 * 3000) // increase timeout to 1min
|
||||
|
||||
let [dc, context, accountId, directory, account] = [
|
||||
null,
|
||||
@@ -10,6 +10,7 @@
|
||||
"@types/node": "^20.8.10",
|
||||
"chai": "~4.3.10",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"mocha": "^8.2.1",
|
||||
"node-gyp": "^10.0.0",
|
||||
"prebuildify": "^5.0.1",
|
||||
@@ -52,8 +53,8 @@
|
||||
"prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.137.4"
|
||||
"version": "1.132.1"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.137.4"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
@@ -27,6 +26,9 @@ dependencies = [
|
||||
"pluggy",
|
||||
"requests",
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
@@ -42,11 +44,16 @@ deltachat = [
|
||||
"py.typed"
|
||||
]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match v*.*"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
|
||||
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Account class implementation."""
|
||||
|
||||
|
||||
import os
|
||||
from array import array
|
||||
from contextlib import contextmanager
|
||||
@@ -375,6 +376,22 @@ class Account:
|
||||
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
|
||||
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
|
||||
|
||||
def _wait_next_message_ids(self) -> List[int]:
|
||||
"""Return IDs of all next messages from all chats."""
|
||||
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
|
||||
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
|
||||
|
||||
def wait_next_incoming_message(self) -> Message:
|
||||
"""Waits until the next incoming message
|
||||
with ID higher than given is received and returns it."""
|
||||
while True:
|
||||
message_ids = self._wait_next_message_ids()
|
||||
for msg_id in message_ids:
|
||||
message = Message.from_db(self, msg_id)
|
||||
if message and not message.is_from_self() and not message.is_from_device():
|
||||
self.set_config("last_msg_id", str(msg_id))
|
||||
return message
|
||||
|
||||
def create_chat(self, obj) -> Chat:
|
||||
"""Create a 1:1 chat with Account, Contact or e-mail address."""
|
||||
return self.create_contact(obj).create_chat()
|
||||
@@ -461,16 +478,6 @@ class Account:
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||
|
||||
def resend_messages(self, messages: List[Message]) -> None:
|
||||
"""Resend list of messages.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
:returns: None
|
||||
"""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
if lib.dc_resend_msgs(self._dc_context, msg_ids, len(msg_ids)) != 1:
|
||||
raise ValueError(f"could not resend messages {msg_ids}")
|
||||
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
"""delete messages (local and remote).
|
||||
|
||||
|
||||
@@ -182,12 +182,6 @@ class FFIEventTracker:
|
||||
print(f"** SECUREJOINT-INVITER PROGRESS {target}", self.account)
|
||||
break
|
||||
|
||||
def wait_securejoin_joiner_progress(self, target):
|
||||
while True:
|
||||
event = self.get_matching("DC_EVENT_SECUREJOIN_JOINER_PROGRESS")
|
||||
if event.data2 >= target:
|
||||
break
|
||||
|
||||
def wait_idle_inbox_ready(self):
|
||||
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
|
||||
so that new messages are not mistaken for old ones:
|
||||
@@ -328,12 +322,10 @@ class EventThread(threading.Thread):
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||
chat = account.get_chat_by_id(ffi_event.data1)
|
||||
yield "ac_chat_modified", {"chat": chat}
|
||||
|
||||
@@ -364,9 +364,6 @@ class Message:
|
||||
else:
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
# Message could be trashed, use the cached object if so.
|
||||
if dc_msg == ffi.NULL:
|
||||
dc_msg = self._dc_msg
|
||||
return lib.dc_msg_get_state(dc_msg)
|
||||
|
||||
def is_in_fresh(self):
|
||||
@@ -395,7 +392,7 @@ class Message:
|
||||
|
||||
def is_outgoing(self):
|
||||
"""Return True if Message is outgoing."""
|
||||
return lib.dc_msg_get_state(self._dc_msg) in (
|
||||
return self._msgstate in (
|
||||
const.DC_STATE_OUT_PREPARING,
|
||||
const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED,
|
||||
@@ -487,9 +484,6 @@ class Message:
|
||||
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
# Message could be trashed, use the cached object if so.
|
||||
if dc_msg == ffi.NULL:
|
||||
dc_msg = self._dc_msg
|
||||
return lib.dc_msg_get_download_state(dc_msg)
|
||||
|
||||
def download_full(self) -> None:
|
||||
|
||||
@@ -115,7 +115,7 @@ def pytest_configure(config):
|
||||
deltachat.register_global_plugin(la)
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
def pytest_report_header(config, startdir):
|
||||
info = get_core_info()
|
||||
summary = [
|
||||
"Deltachat core={} sqlite={} journal_mode={}".format(
|
||||
@@ -590,20 +590,6 @@ class ACFactory:
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def get_protected_chat(self, ac1: Account, ac2: Account):
|
||||
chat = ac1.create_group_chat("Protected Group", verified=True)
|
||||
qr = chat.get_join_qr()
|
||||
ac2.qr_join_chat(qr)
|
||||
ac2._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg is not None
|
||||
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
return chat
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
|
||||
@@ -18,14 +18,14 @@ def test_db_busy_error(acfactory):
|
||||
|
||||
# make a number of accounts
|
||||
accounts = acfactory.get_many_online_accounts(3)
|
||||
log(f"created {len(accounts)} accounts")
|
||||
log("created %s accounts" % len(accounts))
|
||||
|
||||
# put a bigfile into each account
|
||||
for acc in accounts:
|
||||
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
|
||||
with open(acc.bigfile, "wb") as f:
|
||||
f.write(b"01234567890" * 1000_000)
|
||||
log(f"created {len(accounts)} bigfiles")
|
||||
log("created %s bigfiles" % len(accounts))
|
||||
|
||||
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
|
||||
chat = accounts[0].create_group_chat("stress-group")
|
||||
|
||||
@@ -547,14 +547,13 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
|
||||
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
"""Another test for the bug #3836:
|
||||
- Bob has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Bob joins the group.
|
||||
- Bob's second devices goes online, but sees a contact request instead of the verified group.
|
||||
- The "member added" message is not a system message but a plain text message.
|
||||
- Bob's second device doesn't display the Alice's avatar (bug #5354).
|
||||
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
|
||||
missing, cannot encrypt".
|
||||
"""
|
||||
@@ -569,10 +568,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: set avatar")
|
||||
avatar_path = data.get_path("d.png")
|
||||
ac1.set_avatar(avatar_path)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
@@ -585,13 +580,11 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
ac2_offl.start_io()
|
||||
# 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()
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -43,21 +44,21 @@ def test_configure_generate_key(acfactory, lp):
|
||||
lp.sec("ac1: send unencrypted message to ac2")
|
||||
chat.send_text("message1")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
msg_in = ac2.wait_next_incoming_message()
|
||||
assert msg_in.text == "message1"
|
||||
assert not msg_in.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send encrypted message to ac1")
|
||||
msg_in.chat.send_text("message2")
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
msg2_in = ac1._evtracker.wait_next_incoming_message()
|
||||
msg2_in = ac1.wait_next_incoming_message()
|
||||
assert msg2_in.text == "message2"
|
||||
assert msg2_in.is_encrypted()
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
msg2_in.chat.send_text("message3")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg3_in = ac2._evtracker.wait_next_incoming_message()
|
||||
msg3_in = ac2.wait_next_incoming_message()
|
||||
assert msg3_in.text == "message3"
|
||||
assert msg3_in.is_encrypted()
|
||||
|
||||
@@ -381,24 +382,9 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
assert msgs_changed_event.data2 == 0
|
||||
|
||||
|
||||
def test_enable_mvbox_move(acfactory, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("ac2: start without mvbox thread")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac2: configuring mvbox")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
|
||||
@@ -406,18 +392,10 @@ def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac2 and ac1: waiting for configuration")
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac1: create and configure sentbox")
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.set_config("sentbox_watch", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
|
||||
while ac1.get_config("configured_sentbox_folder") != "Sent":
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
|
||||
def test_move_works(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
@@ -520,26 +498,6 @@ def test_forward_messages(acfactory, lp):
|
||||
assert not chat3.get_messages()
|
||||
|
||||
|
||||
def test_forward_encrypted_to_unencrypted(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
chat = acfactory.get_protected_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
txt = "This should be encrypted"
|
||||
chat.send_text(txt)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == txt
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: forward message to ac3 unencrypted")
|
||||
unencrypted_chat = ac2.create_chat(ac3)
|
||||
msg_id = msg.id
|
||||
msg2 = unencrypted_chat.send_msg(msg)
|
||||
assert msg2 == msg
|
||||
assert msg.id != msg_id
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
|
||||
def test_forward_own_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -565,27 +523,6 @@ def test_forward_own_message(acfactory, lp):
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
|
||||
def test_resend_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("ac1: send message to ac2")
|
||||
chat1.send_text("message")
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message"
|
||||
chat2 = msg_in.chat
|
||||
chat2_msg_cnt = len(chat2.get_messages())
|
||||
|
||||
lp.sec("ac1: resend message")
|
||||
ac1.resend_messages([msg_in])
|
||||
|
||||
lp.sec("ac2: check that message is deleted")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
assert len(chat2.get_messages()) == chat2_msg_cnt
|
||||
|
||||
|
||||
def test_long_group_name(acfactory, lp):
|
||||
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
|
||||
group names after MIME serialization/deserialization".
|
||||
@@ -1343,6 +1280,7 @@ def test_quote_encrypted(acfactory, lp):
|
||||
|
||||
for quoted_msg in msg1, msg3:
|
||||
# Save the draft with a quote.
|
||||
# It should be encrypted if quoted message is encrypted.
|
||||
msg_draft = Message.new_empty(ac1, "text")
|
||||
msg_draft.set_text("message reply")
|
||||
msg_draft.quote = quoted_msg
|
||||
@@ -1356,14 +1294,10 @@ def test_quote_encrypted(acfactory, lp):
|
||||
chat.set_draft(None)
|
||||
assert chat.get_draft() is None
|
||||
|
||||
# Quote should be replaced with "..." if quoted message is encrypted.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message reply"
|
||||
assert not msg_in.is_encrypted()
|
||||
if quoted_msg.is_encrypted():
|
||||
assert msg_in.quoted_text == "..."
|
||||
else:
|
||||
assert msg_in.quoted_text == quoted_msg.text
|
||||
assert msg_in.quoted_text == quoted_msg.text
|
||||
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
|
||||
|
||||
|
||||
def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
@@ -1495,12 +1429,112 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
reactions_queue = queue.Queue()
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_reactions_changed(self, message):
|
||||
reactions_queue.put(message)
|
||||
|
||||
ac2.add_account_plugin(InPlugin())
|
||||
|
||||
lp.sec("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
ac1._evtracker.wait_msg_delivered(m)
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
ac1._evtracker.wait_msg_delivered(msgs[-1])
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.get_sender_contact().addr == ac1_addr
|
||||
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert reactions_queue.get() == msg2
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1_addr
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
lp.sec("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
|
||||
|
||||
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2.direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2.direct_imap.get_all_messages()], reverse=True):
|
||||
ac2.direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
lp.sec("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.text == msg1.text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1.get_config("addr")
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
|
||||
chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts(query="some1")) == 1
|
||||
|
||||
@@ -1517,7 +1551,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
contacts = ac.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == some1_addr
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3
|
||||
@@ -2036,15 +2070,14 @@ def test_send_receive_locations(acfactory, lp):
|
||||
assert chat1.is_sending_locations()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
# Wait for "enabled location streaming" message.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
# First location is sent immediately as a location-only message.
|
||||
ac1.set_location(latitude=2.0, longitude=3.0, accuracy=0.5)
|
||||
ac1._evtracker.get_matching("DC_EVENT_LOCATION_CHANGED")
|
||||
chat1.send_text("🍞")
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
lp.sec("ac2: wait for incoming location message")
|
||||
|
||||
# currently core emits location changed before event_incoming message
|
||||
ac2._evtracker.get_matching("DC_EVENT_LOCATION_CHANGED")
|
||||
|
||||
locations = chat2.get_locations()
|
||||
@@ -2053,7 +2086,7 @@ def test_send_receive_locations(acfactory, lp):
|
||||
assert locations[0].longitude == 3.0
|
||||
assert locations[0].accuracy == 0.5
|
||||
assert locations[0].timestamp > now
|
||||
assert locations[0].marker is None
|
||||
assert locations[0].marker == "🍞"
|
||||
|
||||
contact = ac2.create_contact(ac1)
|
||||
locations2 = chat2.get_locations(contact=contact)
|
||||
@@ -2132,21 +2165,17 @@ def test_delete_multiple_messages(acfactory, lp):
|
||||
|
||||
def test_trash_multiple_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
|
||||
# Trash wasn't configured initially, it can't be configured later, let's check this.
|
||||
# Create the Trash folder on IMAP server
|
||||
# and recreate the account so Trash folder is configured.
|
||||
lp.sec("Creating trash folder")
|
||||
ac2.direct_imap.create_folder("Trash")
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
|
||||
lp.sec("Check that Trash can be configured initially as well")
|
||||
ac3 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
lp.sec("Creating new accounts")
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
acfactory.bring_accounts_online()
|
||||
assert ac3.get_config("configured_trash_folder")
|
||||
ac3.stop_io()
|
||||
|
||||
ac2.start_io()
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
assert ac2.get_config("configured_trash_folder")
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending 3 messages")
|
||||
@@ -2161,9 +2190,6 @@ def test_trash_multiple_messages(acfactory, lp):
|
||||
assert msg.text in texts
|
||||
if text != "second":
|
||||
to_delete.append(msg)
|
||||
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
|
||||
# check the configuration.
|
||||
assert ac2.get_config("configured_trash_folder") == "Trash"
|
||||
|
||||
lp.sec("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
|
||||
@@ -52,8 +52,8 @@ class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
p.write_text("123")
|
||||
with pytest.raises(ValueError):
|
||||
_account = Account(str(p))
|
||||
account = Account(str(p))
|
||||
assert not account.is_open()
|
||||
|
||||
def test_os_name(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
|
||||
@@ -41,12 +41,13 @@ skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
ruff
|
||||
black
|
||||
# pygments required by rst-lint
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
ruff format --quiet --diff setup.py src/deltachat examples/ tests/
|
||||
ruff check src/deltachat tests/ examples/
|
||||
black --quiet --check --diff setup.py src/deltachat examples/ tests/
|
||||
ruff src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
@@ -61,8 +62,7 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
# Pinned version, workaround for <https://github.com/breathe-doc/breathe/issues/981>
|
||||
sphinx<7.3
|
||||
sphinx
|
||||
breathe
|
||||
sphinx_rtd_theme
|
||||
commands =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-04-24
|
||||
2023-12-12
|
||||
@@ -35,8 +35,12 @@ and an own build machine.
|
||||
|
||||
- `run_all.sh` builds Python wheels
|
||||
|
||||
- `zig-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Zig toolchain statically linked against musl libc.
|
||||
|
||||
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
|
||||
|
||||
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
|
||||
|
||||
## Triggering runs on the build machine locally (fast!)
|
||||
|
||||
There is experimental support for triggering a remote Python or Rust test run
|
||||
|
||||
13
scripts/build-python-docs.sh
Executable file
13
scripts/build-python-docs.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV="$PWD"
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install ./python
|
||||
venv/bin/pip install ./deltachat-rpc-client
|
||||
venv/bin/pip install sphinx breathe sphinx_rtd_theme
|
||||
venv/bin/pip install ./deltachat-rpc-client
|
||||
venv/bin/sphinx-build -b html -a python/doc/ dist/html
|
||||
@@ -15,6 +15,55 @@ resources:
|
||||
tag_filter: "v*"
|
||||
|
||||
jobs:
|
||||
- name: doxygen
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
trigger: true
|
||||
|
||||
# Build Doxygen documentation
|
||||
- task: build-doxygen
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust
|
||||
outputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
source:
|
||||
repository: alpine
|
||||
type: registry-image
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache doxygen git
|
||||
cd deltachat-core-rust
|
||||
scripts/run-doxygen.sh
|
||||
cd ..
|
||||
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
|
||||
|
||||
- task: upload-c-docs
|
||||
config:
|
||||
inputs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: alpine
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache rsync openssh-client
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
|
||||
|
||||
- name: python-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.78.0
|
||||
RUST_VERSION=1.72.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -220,9 +220,9 @@ if __name__ == "__main__":
|
||||
|
||||
process_dir(Path(sys.argv[1]))
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_DATA: [(&str, &Provider); " + str(len(domains_set)) + "] = [\n";
|
||||
out_all += "pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
|
||||
out_all += out_domains
|
||||
out_all += "];\n\n"
|
||||
out_all += "]));\n\n"
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
|
||||
out_all += out_ids
|
||||
|
||||
@@ -73,8 +73,6 @@ def main():
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
"deltachat-repl/Cargo.toml",
|
||||
"python/pyproject.toml",
|
||||
"deltachat-rpc-client/pyproject.toml",
|
||||
]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
@@ -104,7 +102,7 @@ def main():
|
||||
found = True
|
||||
if not found:
|
||||
raise SystemExit(
|
||||
f"CHANGELOG.md contains no entry for version: {newversion}"
|
||||
f"{changelog_name} contains no entry for version: {newversion}"
|
||||
)
|
||||
|
||||
for toml_filename in toml_list:
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=2f3db24107e4802c2df0aa0a40f0e144006c0a9b
|
||||
REV=18f714cf73d0bdfb8b013fa344494ab80c92b477
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Python wheels for deltachat-rpc-server."""
|
||||
"""Build Python wheels for deltachat-rpc-server.
|
||||
Run scripts/zig-rpc-server.sh first."""
|
||||
from pathlib import Path
|
||||
from wheel.wheelfile import WheelFile
|
||||
import tomllib
|
||||
import tarfile
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def metadata_contents(version):
|
||||
readme_text = (Path("deltachat-rpc-server") / "README.md").read_text()
|
||||
return f"""Metadata-Version: 2.1
|
||||
Name: deltachat-rpc-server
|
||||
Version: {version}
|
||||
Summary: Delta Chat JSON-RPC server
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
{readme_text}
|
||||
"""
|
||||
|
||||
|
||||
def build_source_package(version, filename):
|
||||
def build_source_package(version):
|
||||
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
|
||||
|
||||
with tarfile.open(filename, "w:gz") as pkg:
|
||||
|
||||
def pack(name, contents):
|
||||
@@ -101,7 +99,7 @@ setup(
|
||||
|
||||
|
||||
def build_wheel(version, binary, tag, windows=False):
|
||||
filename = f"deltachat_rpc_server-{version}-{tag}.whl"
|
||||
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
|
||||
|
||||
with WheelFile(filename, "w") as wheel:
|
||||
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
|
||||
@@ -128,11 +126,9 @@ def main():
|
||||
Path(binary).chmod(0o755)
|
||||
wheel.write(
|
||||
binary,
|
||||
(
|
||||
"deltachat_rpc_server/deltachat-rpc-server.exe"
|
||||
if windows
|
||||
else "deltachat_rpc_server/deltachat-rpc-server"
|
||||
),
|
||||
"deltachat_rpc_server/deltachat-rpc-server.exe"
|
||||
if windows
|
||||
else "deltachat_rpc_server/deltachat-rpc-server",
|
||||
)
|
||||
wheel.writestr(
|
||||
f"deltachat_rpc_server-{version}.dist-info/METADATA",
|
||||
@@ -148,41 +144,54 @@ def main():
|
||||
)
|
||||
|
||||
|
||||
arch2tags = {
|
||||
"x86_64-linux": "manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
|
||||
"armv7l-linux": "linux_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
|
||||
"armv6l-linux": "linux_armv6l",
|
||||
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
|
||||
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
|
||||
"win64": "win_amd64",
|
||||
"win32": "win32",
|
||||
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
|
||||
"x86_64-darwin": "macosx_10_7_x86_64",
|
||||
"aarch64-darwin": "macosx_11_0_arm64",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
with Path("Cargo.toml").open("rb") as fp:
|
||||
cargo_manifest = tomllib.load(fp)
|
||||
version = cargo_manifest["package"]["version"]
|
||||
if sys.argv[1] == "source":
|
||||
filename = f"deltachat-rpc-server-{version}.tar.gz"
|
||||
build_source_package(version, filename)
|
||||
else:
|
||||
arch = sys.argv[1]
|
||||
executable = sys.argv[2]
|
||||
tags = arch2tags[arch]
|
||||
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
|
||||
cargo_toml = tomllib.load(f)
|
||||
version = cargo_toml["package"]["version"]
|
||||
Path("dist").mkdir(exist_ok=True)
|
||||
build_source_package(version)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-x86_64-linux",
|
||||
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-armv7-linux",
|
||||
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-aarch64-linux",
|
||||
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-i686-linux",
|
||||
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
|
||||
)
|
||||
|
||||
if arch in ["win32", "win64"]:
|
||||
build_wheel(
|
||||
version,
|
||||
executable,
|
||||
f"py3-none-{tags}",
|
||||
windows=True,
|
||||
)
|
||||
else:
|
||||
build_wheel(version, executable, f"py3-none-{tags}")
|
||||
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-x86_64-macos",
|
||||
"py3-none-macosx_10_7_x86_64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-aarch64-macos",
|
||||
"py3-none-macosx_11_0_arm64",
|
||||
)
|
||||
|
||||
build_wheel(
|
||||
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-win64.exe",
|
||||
"py3-none-win_amd64",
|
||||
windows=True,
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
|
||||
57
scripts/zig-cc
Executable file
57
scripts/zig-cc
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
# /// pyproject
|
||||
# [run]
|
||||
# dependencies = [
|
||||
# "ziglang==0.11.0"
|
||||
# ]
|
||||
# ///
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def flag_filter(flag: str) -> bool:
|
||||
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
|
||||
if flag == "-latomic":
|
||||
return False
|
||||
|
||||
if flag == "-lc":
|
||||
return False
|
||||
if flag == "-Wl,-melf_i386":
|
||||
return False
|
||||
if "self-contained" in flag and "crt" in flag:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
args = [flag for flag in sys.argv[1:] if flag_filter(flag)]
|
||||
zig_target = os.environ["ZIG_TARGET"]
|
||||
zig_cpu = os.environ.get("ZIG_CPU")
|
||||
if zig_cpu:
|
||||
zig_cpu_args = ["-mcpu=" + zig_cpu]
|
||||
args = [x for x in args if not x.startswith("-march")]
|
||||
else:
|
||||
zig_cpu_args = []
|
||||
|
||||
# Disable atomics and use locks instead in OpenSSL.
|
||||
# Zig toolchains do not provide atomics.
|
||||
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
|
||||
args += ["-DBROKEN_CLANG_ATOMICS"]
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"ziglang",
|
||||
"cc",
|
||||
"-target",
|
||||
zig_target,
|
||||
*zig_cpu_args,
|
||||
*args,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
50
scripts/zig-rpc-server.sh
Executable file
50
scripts/zig-rpc-server.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Build statically linked deltachat-rpc-server using zig.
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
unset RUSTFLAGS
|
||||
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
rustup target add i686-unknown-linux-musl
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="x86-linux-musl" \
|
||||
cargo build --release --target i686-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="arm-linux-musleabihf" \
|
||||
ZIG_CPU="generic+v7a+vfp3-d32+thumb2-neon" \
|
||||
cargo build --release --target armv7-unknown-linux-musleabihf -p deltachat-rpc-server --features vendored
|
||||
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="x86_64-linux-musl" \
|
||||
cargo build --release --target x86_64-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="aarch64-linux-musl" \
|
||||
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
|
||||
|
||||
mkdir -p dist
|
||||
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
|
||||
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
|
||||
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
|
||||
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux
|
||||
5
spec.md
5
spec.md
@@ -119,9 +119,8 @@ All group members form the member list.
|
||||
To allow different groups with the same members,
|
||||
groups are identified by a group-id.
|
||||
The group-id MUST be created only from the characters
|
||||
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`,
|
||||
MUST have a length of at least 11 characters
|
||||
and no more than 32 characters.
|
||||
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`
|
||||
and MUST have a length of at least 11 characters.
|
||||
|
||||
Groups MUST have a group-name.
|
||||
The group-name is any non-zero-length UTF-8 string.
|
||||
|
||||
131
src/accounts.rs
131
src/accounts.rs
@@ -5,7 +5,6 @@ use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use futures::future::join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -17,9 +16,8 @@ use tokio::sync::oneshot;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::context::Context;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
@@ -38,9 +36,6 @@ pub struct Accounts {
|
||||
/// This way changing a translation for one context automatically
|
||||
/// changes it for all other contexts.
|
||||
pub(crate) stockstrings: StockStrings,
|
||||
|
||||
/// Push notification subscriber shared between accounts.
|
||||
push_subscriber: PushSubscriber,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@@ -77,9 +72,8 @@ impl Accounts {
|
||||
.context("failed to load accounts config")?;
|
||||
let events = Events::new();
|
||||
let stockstrings = StockStrings::new();
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
let accounts = config
|
||||
.load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
|
||||
.load_accounts(&events, &stockstrings, &dir)
|
||||
.await
|
||||
.context("failed to load accounts")?;
|
||||
|
||||
@@ -89,7 +83,6 @@ impl Accounts {
|
||||
accounts,
|
||||
events,
|
||||
stockstrings,
|
||||
push_subscriber,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -126,17 +119,13 @@ impl Accounts {
|
||||
let account_config = self.config.new_account().await?;
|
||||
let dbfile = account_config.dbfile(&self.dir);
|
||||
|
||||
let ctx = ContextBuilder::new(dbfile)
|
||||
.with_id(account_config.id)
|
||||
.with_events(self.events.clone())
|
||||
.with_stock_strings(self.stockstrings.clone())
|
||||
.with_push_subscriber(self.push_subscriber.clone())
|
||||
.build()
|
||||
.await?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
let ctx = Context::new(
|
||||
&dbfile,
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
@@ -145,15 +134,14 @@ impl Accounts {
|
||||
/// Adds a new closed account.
|
||||
pub async fn add_closed_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account().await?;
|
||||
let dbfile = account_config.dbfile(&self.dir);
|
||||
|
||||
let ctx = ContextBuilder::new(dbfile)
|
||||
.with_id(account_config.id)
|
||||
.with_events(self.events.clone())
|
||||
.with_stock_strings(self.stockstrings.clone())
|
||||
.with_push_subscriber(self.push_subscriber.clone())
|
||||
.build()
|
||||
.await?;
|
||||
let ctx = Context::new_closed(
|
||||
&account_config.dbfile(&self.dir),
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
@@ -253,6 +241,25 @@ impl Accounts {
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
///
|
||||
/// Returns whether all accounts finished their background work.
|
||||
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
///
|
||||
/// iOS can:
|
||||
/// - call dc_start_io() (in case IO was not running)
|
||||
/// - call dc_maybe_network()
|
||||
/// - while dc_accounts_all_work_done() returns false:
|
||||
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
for account in self.accounts.values() {
|
||||
if !account.all_work_done().await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
pub async fn start_io(&mut self) {
|
||||
for account in self.accounts.values_mut() {
|
||||
@@ -284,42 +291,6 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel.
|
||||
///
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_without_timeout(&self) {
|
||||
async fn background_fetch_and_log_error(account: Context) {
|
||||
if let Err(error) = account.background_fetch().await {
|
||||
warn!(account, "{error:#}");
|
||||
}
|
||||
}
|
||||
|
||||
join_all(
|
||||
self.accounts
|
||||
.values()
|
||||
.cloned()
|
||||
.map(background_fetch_and_log_error),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
pub async fn background_fetch(&self, timeout: std::time::Duration) {
|
||||
if let Err(_err) =
|
||||
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
|
||||
{
|
||||
self.emit_event(EventType::Warning(
|
||||
"Background fetch timed out.".to_string(),
|
||||
));
|
||||
}
|
||||
self.emit_event(EventType::AccountsBackgroundFetchDone);
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
pub fn emit_event(&self, event: EventType) {
|
||||
self.events.emit(Event { id: 0, typ: event })
|
||||
@@ -329,12 +300,6 @@ impl Accounts {
|
||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
|
||||
/// Sets notification token for Apple Push Notification service.
|
||||
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration file name.
|
||||
@@ -520,24 +485,24 @@ impl Config {
|
||||
&self,
|
||||
events: &Events,
|
||||
stockstrings: &StockStrings,
|
||||
push_subscriber: PushSubscriber,
|
||||
dir: &Path,
|
||||
) -> Result<BTreeMap<u32, Context>> {
|
||||
let mut accounts = BTreeMap::new();
|
||||
|
||||
for account_config in &self.inner.accounts {
|
||||
let dbfile = account_config.dbfile(dir);
|
||||
let ctx = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(account_config.id)
|
||||
.with_events(events.clone())
|
||||
.with_stock_strings(stockstrings.clone())
|
||||
.with_push_subscriber(push_subscriber.clone())
|
||||
.build()
|
||||
.await
|
||||
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
let ctx = Context::new(
|
||||
&account_config.dbfile(dir),
|
||||
account_config.id,
|
||||
events.clone(),
|
||||
stockstrings.clone(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create context from file {:?}",
|
||||
account_config.dbfile(dir)
|
||||
)
|
||||
})?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
255
src/authres.rs
255
src/authres.rs
@@ -6,7 +6,6 @@ use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -14,6 +13,8 @@ use once_cell::sync::Lazy;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::tools::time;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
/// `authres` is short for the Authentication-Results header, defined in
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
|
||||
@@ -28,28 +29,45 @@ pub(crate) async fn handle_authres(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
message_time: i64,
|
||||
) -> Result<DkimResults> {
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
// This email is invalid, but don't return an error, we still want to
|
||||
// add a stub to the database so that it's not downloaded again
|
||||
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
|
||||
}
|
||||
};
|
||||
|
||||
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
|
||||
update_authservid_candidates(context, &authres).await?;
|
||||
compute_dkim_results(context, authres).await
|
||||
compute_dkim_results(context, authres, &from_domain, message_time).await
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DkimResults {
|
||||
/// Whether DKIM passed for this particular e-mail.
|
||||
pub dkim_passed: bool,
|
||||
/// Whether DKIM is known to work for e-mails coming from the sender's domain,
|
||||
/// i.e. whether we expect DKIM to work.
|
||||
pub dkim_should_work: bool,
|
||||
/// Whether changing the public Autocrypt key should be allowed.
|
||||
/// This is false if we expected DKIM to work (dkim_works=true),
|
||||
/// but it failed now (dkim_passed=false).
|
||||
pub allow_keychange: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for DkimResults {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "DKIM Results: Passed={}", self.dkim_passed)?;
|
||||
write!(
|
||||
fmt,
|
||||
"DKIM Results: Passed={}, Works={}, Allow_Keychange={}",
|
||||
self.dkim_passed, self.dkim_should_work, self.allow_keychange
|
||||
)?;
|
||||
if !self.allow_keychange {
|
||||
write!(fmt, " KEYCHANGES NOT ALLOWED!!!!")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -198,8 +216,12 @@ async fn update_authservid_candidates(
|
||||
if old_ids != new_ids {
|
||||
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
||||
context
|
||||
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.set_config(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.await?;
|
||||
// Updating the authservid candidates may mean that we now consider
|
||||
// emails as "failed" which "passed" previously, so we need to
|
||||
// reset our expectation which DKIMs work.
|
||||
clear_dkim_works(context).await?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -216,6 +238,8 @@ async fn update_authservid_candidates(
|
||||
async fn compute_dkim_results(
|
||||
context: &Context,
|
||||
mut authres: ParsedAuthresHeaders,
|
||||
from_domain: &str,
|
||||
message_time: i64,
|
||||
) -> Result<DkimResults> {
|
||||
let mut dkim_passed = false;
|
||||
|
||||
@@ -248,7 +272,71 @@ async fn compute_dkim_results(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DkimResults { dkim_passed })
|
||||
let last_working_timestamp = dkim_works_timestamp(context, from_domain).await?;
|
||||
let mut dkim_should_work = dkim_should_work(last_working_timestamp)?;
|
||||
if message_time > last_working_timestamp && dkim_passed {
|
||||
set_dkim_works_timestamp(context, from_domain, message_time).await?;
|
||||
dkim_should_work = true;
|
||||
}
|
||||
|
||||
Ok(DkimResults {
|
||||
dkim_passed,
|
||||
dkim_should_work,
|
||||
allow_keychange: dkim_passed || !dkim_should_work,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether DKIM in emails from this domain should be considered to work.
|
||||
fn dkim_should_work(last_working_timestamp: i64) -> Result<bool> {
|
||||
// When we get an email with valid DKIM-Authentication-Results,
|
||||
// then we assume that DKIM works for 30 days from this time on.
|
||||
let should_work_until = last_working_timestamp + 3600 * 24 * 30;
|
||||
|
||||
let dkim_ever_worked = last_working_timestamp > 0;
|
||||
|
||||
// We're using time() here and not the time when the message
|
||||
// claims to have been sent (passed around as `message_time`)
|
||||
// because otherwise an attacker could just put a time way
|
||||
// in the future into the `Date` header and then we would
|
||||
// assume that DKIM doesn't have to be valid anymore.
|
||||
let dkim_should_work_now = should_work_until > time();
|
||||
Ok(dkim_ever_worked && dkim_should_work_now)
|
||||
}
|
||||
|
||||
async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i64, anyhow::Error> {
|
||||
let last_working_timestamp: i64 = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT dkim_works FROM sending_domains WHERE domain=?",
|
||||
(from_domain,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
Ok(last_working_timestamp)
|
||||
}
|
||||
|
||||
async fn set_dkim_works_timestamp(
|
||||
context: &Context,
|
||||
from_domain: &str,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
|
||||
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
|
||||
(from_domain, timestamp),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_dkim_works(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM sending_domains", ())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
|
||||
@@ -261,12 +349,19 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::e2ee;
|
||||
use crate::mimeparser;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::securejoin::join_securejoin;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
@@ -479,8 +574,33 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from).await?;
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
assert!(res.allow_keychange);
|
||||
}
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
bytes.clear();
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
if !res.allow_keychange {
|
||||
println!(
|
||||
"!!!!!! FAILURE Receiving {:?}, keychange is not allowed !!!!!!",
|
||||
entry.path()
|
||||
);
|
||||
test_failed = true;
|
||||
}
|
||||
|
||||
let from_domain = EmailAddress::new(from).unwrap().domain;
|
||||
assert_eq!(
|
||||
res.dkim_should_work,
|
||||
dkim_should_work(dkim_works_timestamp(&t, &from_domain).await?)?
|
||||
);
|
||||
assert_eq!(res.dkim_passed, res.dkim_should_work);
|
||||
|
||||
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
|
||||
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
|
||||
@@ -493,8 +613,9 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
if res.dkim_passed != expected_result {
|
||||
if authres_parsing_works {
|
||||
println!(
|
||||
"!!!!!! FAILURE Receiving {:?} wrong result: !!!!!!",
|
||||
"!!!!!! FAILURE Receiving {:?}, order {:#?} wrong result: !!!!!!",
|
||||
entry.path(),
|
||||
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
|
||||
);
|
||||
test_failed = true;
|
||||
}
|
||||
@@ -517,7 +638,116 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
let bytes = b"From: invalid@from.com
|
||||
Authentication-Results: dkim=";
|
||||
let mail = mailparse::parse_mail(bytes).unwrap();
|
||||
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
|
||||
handle_authres(&t, &mail, "invalid@rom.com", time())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[ignore = "Disallowing keychanges is disabled for now"]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_authres_fails() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Bob sends Alice a message, so she gets his key
|
||||
tcm.send_recv_accept(&bob, &alice, "Hi").await;
|
||||
|
||||
// We don't need bob anymore, let's make sure it's not accidentally used
|
||||
drop(bob);
|
||||
|
||||
// Assume Alice receives an email from bob@example.net with
|
||||
// correct DKIM -> `set_dkim_works()` was called
|
||||
set_dkim_works_timestamp(&alice, "example.net", time()).await?;
|
||||
// And Alice knows her server's authserv-id
|
||||
alice
|
||||
.set_config(Config::AuthservIdCandidates, Some("example.org"))
|
||||
.await?;
|
||||
|
||||
tcm.section("An attacker, bob2, sends a from-forged email to Alice!");
|
||||
|
||||
// Sleep to make sure key reset is ignored because of DKIM failure
|
||||
// and not because reordering is suspected.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
let bob2 = tcm.unconfigured().await;
|
||||
bob2.configure_addr("bob@example.net").await;
|
||||
e2ee::ensure_secret_key_exists(&bob2).await?;
|
||||
|
||||
let chat = bob2.create_chat(&alice).await;
|
||||
let mut sent = bob2
|
||||
.send_text(chat.id, "Please send me lots of money")
|
||||
.await;
|
||||
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
|
||||
|
||||
let received = alice.recv_msg(&sent).await;
|
||||
|
||||
// Assert that the error tells the user about the problem
|
||||
assert!(received.error.unwrap().contains("DKIM failed"));
|
||||
|
||||
let bob_state = Peerstate::from_addr(&alice, "bob@example.net")
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
// Encryption preference is still mutual.
|
||||
assert_eq!(bob_state.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Also check that the keypair was not changed
|
||||
assert_eq!(
|
||||
bob_state.public_key.unwrap(),
|
||||
test_utils::bob_keypair().public
|
||||
);
|
||||
|
||||
// Since Alice didn't change the key, Bob can't read her message
|
||||
let received = tcm
|
||||
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
|
||||
.await;
|
||||
assert!(!received.text.contains("1234"));
|
||||
assert!(received.error.is_some());
|
||||
|
||||
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
|
||||
tcm.section("To fix the key problems, Bob scans Alice's QR code.");
|
||||
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
join_securejoin(&bob2.ctx, &qr).await.unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
|
||||
alice.recv_msg(&sent).await;
|
||||
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
bob2.recv_msg(&sent).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately, securejoin currently doesn't work with authres-checking,
|
||||
// so these checks would fail:
|
||||
|
||||
// let contact_bob = alice.add_or_lookup_contact(&bob2).await;
|
||||
// assert_eq!(
|
||||
// contact_bob.is_verified(&alice.ctx).await.unwrap(),
|
||||
// VerifiedStatus::BidirectVerified
|
||||
// );
|
||||
|
||||
// let contact_alice = bob2.add_or_lookup_contact(&alice).await;
|
||||
// assert_eq!(
|
||||
// contact_alice.is_verified(&bob2.ctx).await.unwrap(),
|
||||
// VerifiedStatus::BidirectVerified
|
||||
// );
|
||||
|
||||
// // Bob can read Alice's messages again
|
||||
// let received = tcm
|
||||
// .try_send_recv(&alice, &bob2, "Can you read this again?")
|
||||
// .await;
|
||||
// assert_eq!(received.text.as_ref().unwrap(), "Can you read this again?");
|
||||
// assert!(received.error.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -566,7 +796,10 @@ Authentication-Results: dkim=";
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Bob knows his server's authserv-id
|
||||
// Assume Bob received an email from something@example.net with
|
||||
// correct DKIM -> `set_dkim_works()` was called
|
||||
set_dkim_works_timestamp(&bob, "example.org", time()).await?;
|
||||
// And Bob knows his server's authserv-id
|
||||
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
|
||||
.await?;
|
||||
|
||||
@@ -588,13 +821,15 @@ Authentication-Results: dkim=";
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
|
||||
// Disallowing keychanges is disabled for now:
|
||||
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
|
||||
// The message info should contain a warning:
|
||||
assert!(rcvd
|
||||
.id
|
||||
.get_info(&bob)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("DKIM Results: Passed=false"));
|
||||
.contains("KEYCHANGES NOT ALLOWED"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
202
src/blob.rs
202
src/blob.rs
@@ -5,14 +5,11 @@ use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::{fs, io};
|
||||
@@ -36,12 +33,6 @@ pub struct BlobObject<'a> {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ImageOutputFormat {
|
||||
Png,
|
||||
Jpeg { quality: u8 },
|
||||
}
|
||||
|
||||
impl<'a> BlobObject<'a> {
|
||||
/// Creates a new blob object with a unique name.
|
||||
///
|
||||
@@ -321,30 +312,6 @@ impl<'a> BlobObject<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns path to the stored Base64-decoded blob.
|
||||
///
|
||||
/// If `data` represents an image of known format, this adds the corresponding extension to
|
||||
/// `suggested_file_stem`.
|
||||
pub(crate) async fn store_from_base64(
|
||||
context: &Context,
|
||||
data: &str,
|
||||
suggested_file_stem: &str,
|
||||
) -> Result<String> {
|
||||
let buf = base64::engine::general_purpose::STANDARD.decode(data)?;
|
||||
let ext = if let Ok(format) = image::guess_format(&buf) {
|
||||
if let Some(ext) = format.extensions_str().first() {
|
||||
format!(".{ext}")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let blob =
|
||||
BlobObject::create(context, &format!("{suggested_file_stem}{ext}"), &buf).await?;
|
||||
Ok(blob.as_name().to_string())
|
||||
}
|
||||
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
|
||||
@@ -421,8 +388,6 @@ impl<'a> BlobObject<'a> {
|
||||
max_bytes: usize,
|
||||
strict_limits: bool,
|
||||
) -> Result<Option<String>> {
|
||||
// Add white background only to avatars to spare the CPU.
|
||||
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let res = tokio::task::block_in_place(move || {
|
||||
@@ -456,19 +421,13 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
let fmt = ImageFormat::from_path(&blob_abs);
|
||||
let ofmt = match fmt {
|
||||
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
Ok(ImageFormat::Jpeg) => {
|
||||
add_white_bg = false;
|
||||
ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
}
|
||||
_ => {
|
||||
let jpeg_quality = 75;
|
||||
ImageOutputFormat::Jpeg(jpeg_quality)
|
||||
}
|
||||
_ => ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
},
|
||||
};
|
||||
// We need to rewrite images with Exif to remove metadata such as location,
|
||||
// camera model, etc.
|
||||
@@ -479,18 +438,14 @@ impl<'a> BlobObject<'a> {
|
||||
let do_scale = exceeds_max_bytes
|
||||
|| strict_limits
|
||||
&& (exceeds_wh
|
||||
|| exif.is_some() && {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
}
|
||||
encoded_img_exceeds_bytes(
|
||||
|| exif.is_some()
|
||||
&& encoded_img_exceeds_bytes(
|
||||
context,
|
||||
&img,
|
||||
ofmt.clone(),
|
||||
max_bytes,
|
||||
&mut encoded,
|
||||
)?
|
||||
});
|
||||
)?);
|
||||
|
||||
if do_scale {
|
||||
if !exceeds_wh {
|
||||
@@ -503,9 +458,6 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
loop {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
}
|
||||
let new_img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
if encoded_img_exceeds_bytes(
|
||||
@@ -539,7 +491,7 @@ impl<'a> BlobObject<'a> {
|
||||
if do_scale || exif.is_some() {
|
||||
// The file format is JPEG/PNG now, we may have to change the file extension
|
||||
if !matches!(fmt, Ok(ImageFormat::Jpeg))
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
|
||||
{
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
let file_name = blob_abs.file_name().context("No image file name (???)")?;
|
||||
@@ -548,9 +500,6 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
if encoded.is_empty() {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
}
|
||||
encode_img(&img, ofmt, &mut encoded)?;
|
||||
}
|
||||
|
||||
@@ -694,16 +643,7 @@ fn encode_img(
|
||||
) -> anyhow::Result<()> {
|
||||
encoded.clear();
|
||||
let mut buf = Cursor::new(encoded);
|
||||
match fmt {
|
||||
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
|
||||
ImageOutputFormat::Jpeg { quality } => {
|
||||
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
|
||||
// Convert image into RGB8 to avoid the error
|
||||
// "The encoder or decoder for Jpeg does not support the color type Rgba8"
|
||||
// (<https://github.com/image-rs/image/issues/2211>).
|
||||
img.clone().into_rgb8().write_with_encoder(encoder)?;
|
||||
}
|
||||
}
|
||||
img.write_to(&mut buf, fmt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -729,20 +669,11 @@ fn encoded_img_exceeds_bytes(
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Removes transparency from an image using a white background.
|
||||
fn add_white_bg(img: &mut DynamicImage) {
|
||||
for y in 0..img.height() {
|
||||
for x in 0..img.width() {
|
||||
let mut p = Rgba([255u8, 255, 255, 255]);
|
||||
p.blend(&img.get_pixel(x, y));
|
||||
img.put_pixel(x, y, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use fs::File;
|
||||
use image::{GenericImageView, Pixel};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
@@ -954,40 +885,6 @@ mod tests {
|
||||
assert!(!stem.contains('?'));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_white_bg() {
|
||||
let t = TestContext::new().await;
|
||||
let bytes0 = include_bytes!("../test-data/image/logo.png").as_slice();
|
||||
let bytes1 = include_bytes!("../test-data/image/avatar900x900.png").as_slice();
|
||||
for (bytes, color) in [
|
||||
(bytes0, [255u8, 255, 255, 255]),
|
||||
(bytes1, [253u8, 198, 0, 255]),
|
||||
] {
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
fs::write(&avatar_src, bytes).await.unwrap();
|
||||
|
||||
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
|
||||
let img_wh = 128;
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(
|
||||
&t,
|
||||
blob.to_abs_path(),
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
20_000,
|
||||
strict_limits,
|
||||
)
|
||||
.unwrap();
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = image::open(blob.to_abs_path()).unwrap();
|
||||
assert!(img.width() == img_wh);
|
||||
assert!(img.height() == img_wh);
|
||||
assert_eq!(img.get_pixel(0, 0), Rgba(color));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_outside_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -1208,28 +1105,6 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Tests that RGBA PNG can be recoded into JPEG
|
||||
/// by dropping alpha channel.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::WORSE_IMAGE_SIZE,
|
||||
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_huge_jpg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||
@@ -1307,65 +1182,26 @@ mod tests {
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
alice_msg.save_file(&alice, &file_saved).await?;
|
||||
check_image_size(file_saved, compressed_width, compressed_height);
|
||||
check_image_size(
|
||||
alice_msg.get_file(&alice).unwrap(),
|
||||
compressed_width,
|
||||
compressed_height,
|
||||
);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let blob = BlobObject::new_from_path(&bob, &file).await?;
|
||||
let (_, exif) = blob.metadata()?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
let img = check_image_size(file, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_big_gif_as_image() -> Result<()> {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.gif");
|
||||
let (width, height) = (1920u32, 1080u32);
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(
|
||||
Config::MediaQuality,
|
||||
Some(&(MediaQuality::Worse as i32).to_string()),
|
||||
)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension("gif");
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
// DC must detect the image as GIF and send it w/o reencoding.
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
|
||||
assert_eq!(bob_msg.get_width() as u32, width);
|
||||
assert_eq!(bob_msg.get_height() as u32, height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let (file_size, _) = blob.metadata()?;
|
||||
assert_eq!(file_size, bytes.len() as u64);
|
||||
check_image_size(file_saved, width, height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_increation_in_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
476
src/chat.rs
476
src/chat.rs
@@ -2,13 +2,13 @@
|
||||
|
||||
use std::cmp;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
use deltachat_contact_tools::{strip_rtlo_characters, ContactAddress};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
@@ -16,14 +16,13 @@ use strum_macros::EnumIter;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::chatlist_events;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
|
||||
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
@@ -45,8 +44,9 @@ use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
smeared_time, time, IsNoneOrEmpty, SystemTime,
|
||||
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty,
|
||||
};
|
||||
use crate::ui_events;
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
@@ -211,11 +211,24 @@ impl ChatId {
|
||||
}
|
||||
|
||||
/// Returns [`ChatId`] of a chat that `msg` belongs to.
|
||||
///
|
||||
/// Checks that `msg` is assigned to the right chat.
|
||||
pub(crate) fn lookup_by_message(msg: &Message) -> Option<Self> {
|
||||
if msg.chat_id == DC_CHAT_ID_TRASH {
|
||||
return None;
|
||||
}
|
||||
if msg.download_state == DownloadState::Undecipherable {
|
||||
if msg.download_state != DownloadState::Done
|
||||
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
|
||||
// `DownloadState::Undecipherable`. Remove eventually with the comment in
|
||||
// `MimeMessage::from_bytes()`.
|
||||
|| msg
|
||||
.error
|
||||
.as_ref()
|
||||
.filter(|e| e.starts_with("Decrypting failed:"))
|
||||
.is_some()
|
||||
{
|
||||
// If `msg` is not fully downloaded or undecipherable, it may have been assigned to the
|
||||
// wrong chat (they often get assigned to the 1:1 chat with the sender).
|
||||
return None;
|
||||
}
|
||||
Some(msg.chat_id)
|
||||
@@ -297,8 +310,8 @@ impl ChatId {
|
||||
}
|
||||
};
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
@@ -415,7 +428,7 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
||||
@@ -438,7 +451,7 @@ impl ChatId {
|
||||
pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
self.set_blocked(context, Blocked::Not).await?;
|
||||
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
@@ -494,7 +507,7 @@ impl ChatId {
|
||||
|
||||
if self.set_blocked(context, Blocked::Not).await? {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
ui_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
@@ -537,7 +550,7 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
ui_events::emit_chatlist_item_changed(context, self);
|
||||
|
||||
// make sure, the receivers will get all keys
|
||||
self.reset_gossiped_timestamp(context).await?;
|
||||
@@ -586,7 +599,7 @@ impl ChatId {
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
ui_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -673,8 +686,8 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_item_changed(context, self);
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
@@ -781,11 +794,9 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
context.set_config(Config::LastHousekeeping, None).await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
@@ -793,7 +804,7 @@ impl ChatId {
|
||||
msg.text = stock_str::self_deleted_msg_body(context).await;
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1174,15 +1185,6 @@ impl ChatId {
|
||||
Ok(self.get_param(context).await?.exists(Param::Devicetalk))
|
||||
}
|
||||
|
||||
/// Returns chat member list timestamp.
|
||||
pub(crate) async fn get_member_list_timestamp(self, context: &Context) -> Result<i64> {
|
||||
Ok(self
|
||||
.get_param(context)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn parent_query<T, F>(
|
||||
self,
|
||||
context: &Context,
|
||||
@@ -1441,7 +1443,7 @@ impl rusqlite::types::ToSql for ChatId {
|
||||
impl rusqlite::types::FromSql for ChatId {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|val| {
|
||||
if 0 <= val && val <= i64::from(u32::MAX) {
|
||||
if 0 <= val && val <= i64::from(std::u32::MAX) {
|
||||
Ok(ChatId::new(val as u32))
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(val))
|
||||
@@ -1529,7 +1531,7 @@ impl Chat {
|
||||
Ok(contacts) => {
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
contact.get_display_name().clone_into(&mut chat_name);
|
||||
chat_name = contact.get_display_name().to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1800,10 +1802,18 @@ impl Chat {
|
||||
update_msg_id: Option<MsgId>,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
let mut new_references = "".into();
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
let new_rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let new_rfc724_mid = {
|
||||
let grpid = match self.typ {
|
||||
Chattype::Group => Some(self.grpid.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
create_outgoing_rfc724_mid(grpid, &from)
|
||||
};
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
if let Some(id) = context
|
||||
@@ -1841,72 +1851,58 @@ impl Chat {
|
||||
// reset encrypt error state eg. for forwarding
|
||||
msg.param.remove(Param::ErroneousE2ee);
|
||||
|
||||
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
|
||||
// Set "References:" to identify the "thread" of the conversation.
|
||||
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
|
||||
let new_references;
|
||||
if self.is_self_talk() {
|
||||
// As self-talks are mainly used to transfer data between devices,
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
new_references = String::new();
|
||||
} else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message for
|
||||
// which `parent_query()` is done may assume that it will be received in a context
|
||||
// affected by those messages, e.g. they could add new members to a group and the
|
||||
// new message will contain them in "To:". Anyway recipients must be prepared to
|
||||
// orphaned references.
|
||||
self
|
||||
.id
|
||||
.get_parent_mime_headers(context, MessageState::OutPending)
|
||||
.await?
|
||||
{
|
||||
// "In-Reply-To:" is not changed if it is set manually.
|
||||
// This does not affect "References:" header, it will contain "default parent" (the
|
||||
// latest message in the thread) anyway.
|
||||
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
|
||||
msg.in_reply_to = Some(parent_rfc724_mid.clone());
|
||||
}
|
||||
|
||||
// Use parent `In-Reply-To` as a fallback
|
||||
// in case parent message has no `References` header
|
||||
// as specified in RFC 5322:
|
||||
// > If the parent message does not contain
|
||||
// > a "References:" field but does have an "In-Reply-To:" field
|
||||
// > containing a single message identifier, then the "References:" field
|
||||
// > will contain the contents of the parent's "In-Reply-To:" field
|
||||
// > followed by the contents of the parent's "Message-ID:" field (if
|
||||
// > any).
|
||||
let parent_references = if parent_references.is_empty() {
|
||||
parent_in_reply_to
|
||||
} else {
|
||||
parent_references
|
||||
};
|
||||
|
||||
// The whole list of messages referenced may be huge.
|
||||
// Only take 2 recent references and add third from `In-Reply-To`.
|
||||
let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
|
||||
references_vec.reverse();
|
||||
|
||||
if !parent_rfc724_mid.is_empty()
|
||||
&& !references_vec.contains(&parent_rfc724_mid.as_str())
|
||||
// set "In-Reply-To:" to identify the message to which the composed message is a reply;
|
||||
// set "References:" to identify the "thread" of the conversation;
|
||||
// both according to RFC 5322 3.6.4, page 25
|
||||
//
|
||||
// as self-talks are mainly used to transfer data between devices,
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
if !self.is_self_talk() {
|
||||
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message for
|
||||
// which `parent_query()` is done may assume that it will be received in a context
|
||||
// affected by those messages, e.g. they could add new members to a group and the
|
||||
// new message will contain them in "To:". Anyway recipients must be prepared to
|
||||
// orphaned references.
|
||||
self
|
||||
.id
|
||||
.get_parent_mime_headers(context, MessageState::OutPending)
|
||||
.await?
|
||||
{
|
||||
references_vec.push(&parent_rfc724_mid)
|
||||
}
|
||||
// "In-Reply-To:" is not changed if it is set manually.
|
||||
// This does not affect "References:" header, it will contain "default parent" (the
|
||||
// latest message in the thread) anyway.
|
||||
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
|
||||
msg.in_reply_to = Some(parent_rfc724_mid.clone());
|
||||
}
|
||||
|
||||
if references_vec.is_empty() {
|
||||
// As a fallback, use our Message-ID,
|
||||
// same as in the case of top-level message.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
// the whole list of messages referenced may be huge;
|
||||
// only use the oldest and the parent message
|
||||
let parent_references = parent_references
|
||||
.find(' ')
|
||||
.and_then(|n| parent_references.get(..n))
|
||||
.unwrap_or(&parent_references);
|
||||
|
||||
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
|
||||
// angle brackets are added by the mimefactory later
|
||||
new_references = format!("{parent_references} {parent_rfc724_mid}");
|
||||
} else if !parent_references.is_empty() {
|
||||
new_references = parent_references.to_string();
|
||||
} else if !parent_in_reply_to.is_empty() && !parent_rfc724_mid.is_empty() {
|
||||
new_references = format!("{parent_in_reply_to} {parent_rfc724_mid}");
|
||||
} else if !parent_in_reply_to.is_empty() {
|
||||
new_references = parent_in_reply_to;
|
||||
} else {
|
||||
// as a fallback, use our Message-ID, see reasoning below.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
}
|
||||
} else {
|
||||
new_references = references_vec.join(" ");
|
||||
// this is a top-level message, add our Message-ID as first reference.
|
||||
// as we always try to extract the grpid also from `References:`-header,
|
||||
// this allows group conversations also if smtp-server as outlook change `Message-ID:`-header
|
||||
// (MUAs usually keep the first Message-ID in `References:`-header unchanged).
|
||||
new_references = new_rfc724_mid.clone();
|
||||
}
|
||||
} else {
|
||||
// This is a top-level message.
|
||||
// Add our Message-ID as first references.
|
||||
// This allows us to identify replies to our message even if
|
||||
// email server such as Outlook changes `Message-ID:` header.
|
||||
// MUAs usually keep the first Message-ID in `References:` header unchanged.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
}
|
||||
|
||||
// add independent location to database
|
||||
@@ -2049,7 +2045,6 @@ impl Chat {
|
||||
msg.id = MsgId::new(u32::try_from(raw_id)?);
|
||||
|
||||
maybe_set_logging_xdc(context, msg, self.id).await?;
|
||||
context.update_webxdc_integration_database(msg).await?;
|
||||
}
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
Ok(msg.id)
|
||||
@@ -2505,30 +2500,6 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.await?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
//
|
||||
// Typical conversions:
|
||||
// - from FILE to AUDIO/VIDEO/IMAGE
|
||||
// - from FILE/IMAGE to GIF */
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(&blob.to_abs_path())
|
||||
{
|
||||
if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
}
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
||||
if msg.viewtype == Viewtype::Image
|
||||
|| maybe_sticker && !msg.param.exists(Param::ForceSticker)
|
||||
@@ -2550,6 +2521,34 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.set(Param::Filename, stem.to_string() + "." + blob_ext);
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
//
|
||||
// Typical conversions:
|
||||
// - from FILE to AUDIO/VIDEO/IMAGE
|
||||
// - from FILE/IMAGE to GIF */
|
||||
if let Some((better_type, better_mime)) =
|
||||
message::guess_msgtype_from_suffix(&blob.to_abs_path())
|
||||
{
|
||||
if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
if !msg.param.exists(Param::MimeType) {
|
||||
msg.param.set(Param::MimeType, better_mime);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !msg.param.exists(Param::MimeType) {
|
||||
if let Some((_, mime)) = message::guess_msgtype_from_suffix(&blob.to_abs_path()) {
|
||||
msg.param.set(Param::MimeType, mime);
|
||||
@@ -2593,18 +2592,6 @@ async fn prepare_msg_common(
|
||||
}
|
||||
}
|
||||
|
||||
// Check a quote reply is not leaking data from other chats.
|
||||
// This is meant as a last line of defence, the UI should check that before as well.
|
||||
// (We allow Chattype::Single in general for "Reply Privately";
|
||||
// checking for exact contact_id will produce false positives when ppl just left the group)
|
||||
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
|
||||
if let Some(quoted_message) = msg.quoted_message(context).await? {
|
||||
if quoted_message.chat_id != chat_id {
|
||||
bail!("Bad quote reply");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check current MessageState for drafts (to keep msg_id) ...
|
||||
let update_msg_id = if msg.state == MessageState::OutDraft {
|
||||
msg.hidden = false;
|
||||
@@ -2684,11 +2671,6 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
return send_msg_inner(context, chat_id, msg).await;
|
||||
}
|
||||
|
||||
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
send_msg_inner(context, chat_id, msg).await
|
||||
}
|
||||
|
||||
@@ -2718,12 +2700,10 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
|
||||
}
|
||||
|
||||
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
|
||||
if !msg.hidden {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
}
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
context.emit_location_changed(Some(ContactId::SELF)).await?;
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
}
|
||||
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
@@ -2764,25 +2744,26 @@ async fn prepare_send_msg(
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
let mimefactory = MimeFactory::from_msg(context, msg).await?;
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(attach_selfavatar) => attach_selfavatar,
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, msg, attach_selfavatar).await?;
|
||||
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled.
|
||||
//
|
||||
// Previous versions of Delta Chat did not send BCC self
|
||||
// if DeleteServerAfter was set to immediately delete messages
|
||||
// from the server. This is not the case anymore
|
||||
// because BCC-self messages are also used to detect
|
||||
// that message was sent if SMTP server is slow to respond
|
||||
// and connection is frequently lost
|
||||
// before receiving status line.
|
||||
//
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
// delete it immediately. `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
&& context.get_config_delete_server_after().await? != Some(0)
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
@@ -2790,11 +2771,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
// Webxdc integrations are messages, however, shipped with main app and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() {
|
||||
recipients.clear();
|
||||
}
|
||||
|
||||
if recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
@@ -2829,28 +2805,21 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
);
|
||||
}
|
||||
|
||||
let now = smeared_time(context);
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
msg.chat_id.set_gossiped_timestamp(context, now).await?;
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
}
|
||||
|
||||
if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
// Reject member list synchronisation from older messages. See also
|
||||
// `receive_imf::apply_group_changes()`.
|
||||
msg.chat_id
|
||||
.update_timestamp(
|
||||
context,
|
||||
Param::MemberListTimestamp,
|
||||
now.saturating_add(constants::TIMESTAMP_SENT_TOLERANCE),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if rendered_msg.last_added_location_id.is_some() {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
|
||||
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
|
||||
}
|
||||
if !msg.hidden {
|
||||
if let Err(err) =
|
||||
location::set_msg_location_id(context, msg.id, last_added_location_id).await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
@@ -2860,7 +2829,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
}
|
||||
}
|
||||
@@ -2870,7 +2839,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
msg.subject = rendered_msg.subject.clone();
|
||||
msg.update_subject(context).await?;
|
||||
let chunk_size = context
|
||||
.get_configured_provider()
|
||||
@@ -3125,9 +3094,9 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
.await?;
|
||||
for chat_id_in_archive in chat_ids_in_archive {
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
|
||||
ui_events::emit_chatlist_item_changed(context, chat_id_in_archive);
|
||||
}
|
||||
chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK);
|
||||
ui_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK);
|
||||
} else {
|
||||
let exists = context
|
||||
.sql
|
||||
@@ -3154,7 +3123,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
ui_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3222,7 +3191,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
|
||||
for c in changed_chats {
|
||||
context.emit_event(EventType::MsgsNoticed(c));
|
||||
chatlist_events::emit_chatlist_item_changed(context, c);
|
||||
ui_events::emit_chatlist_item_changed(context, c);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3385,8 +3354,8 @@ pub async fn create_group_chat(
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
if protect == ProtectionStatus::Protected {
|
||||
chat_id
|
||||
@@ -3474,7 +3443,7 @@ pub(crate) async fn create_broadcast_list_ex(
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
ui_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
let id = SyncId::Grpid(grpid);
|
||||
@@ -3693,7 +3662,7 @@ pub enum MuteDuration {
|
||||
Forever,
|
||||
|
||||
/// Chat is muted for a limited period of time.
|
||||
Until(std::time::SystemTime),
|
||||
Until(SystemTime),
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for MuteDuration {
|
||||
@@ -3752,7 +3721,7 @@ pub(crate) async fn set_muted_ex(
|
||||
.await
|
||||
.context(format!("Failed to set mute duration for {chat_id}"))?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
ui_events::emit_chatlist_item_changed(context, chat_id);
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.sync(context, SyncAction::SetMuted(duration))
|
||||
@@ -3913,7 +3882,7 @@ async fn rename_ex(
|
||||
sync = Nosync;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
ui_events::emit_chatlist_item_changed(context, chat_id);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
@@ -3974,7 +3943,7 @@ pub async fn set_chat_profile_image(
|
||||
context.emit_msgs_changed(chat_id, msg.id);
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
ui_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4120,9 +4089,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
msg.timestamp_sort = create_smeared_timestamp(context);
|
||||
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
ui_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
@@ -4193,7 +4161,7 @@ pub async fn add_device_msg_with_importance(
|
||||
if let Some(msg) = msg {
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context);
|
||||
@@ -4312,9 +4280,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::QuotaExceeding, None)
|
||||
.await?;
|
||||
context.set_config(Config::QuotaExceeding, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4333,7 +4299,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
parent: Option<&Message>,
|
||||
from_id: Option<ContactId>,
|
||||
) -> Result<MsgId> {
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
|
||||
let mut param = Params::new();
|
||||
@@ -4516,8 +4482,9 @@ impl Context {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::chatlist::{get_archived_cnt, Chatlist};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::contact::{Contact, ContactAddress};
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager};
|
||||
@@ -4725,59 +4692,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote_replies() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?;
|
||||
let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?;
|
||||
|
||||
let one2one_chat_id = alice.create_chat(&bob).await.id;
|
||||
let one2one_msg_id = send_text_msg(&alice, one2one_chat_id, "foo".to_string()).await?;
|
||||
let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?;
|
||||
|
||||
// quoting messages in same chat is okay
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&grp_msg)).await?;
|
||||
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&one2one_msg)).await?;
|
||||
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
let one2one_quote_reply_msg_id = result.unwrap();
|
||||
|
||||
// quoting messages from groups to one-to-ones is okay ("reply privately")
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&grp_msg)).await?;
|
||||
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ...
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&one2one_msg)).await?;
|
||||
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// ... but forwarding messages with quotes is allowed
|
||||
let result = forward_msgs(&alice, &[one2one_quote_reply_msg_id], grp_chat_id).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// ... and bots are not restricted
|
||||
alice.set_config(Config::Bot, Some("1")).await?;
|
||||
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_contact_to_chat_ex_add_self() {
|
||||
// Adding self to a contact should succeed, even though it's pointless.
|
||||
@@ -4868,9 +4782,9 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test parallel removal of user from the chat and leaving the group.
|
||||
/// Test simultaneous removal of user from the chat and leaving the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parallel_member_remove() -> Result<()> {
|
||||
async fn test_simultaneous_member_remove() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
@@ -4901,25 +4815,20 @@ mod tests {
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
|
||||
let alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob leaves the chat.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
// Bob receives a msg about Alice adding Claire to the group.
|
||||
bob.recv_msg(&alice_sent_add_msg).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
// This adds Bob because they left quite long ago.
|
||||
let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await;
|
||||
bob.recv_msg(&alice_sent_msg).await;
|
||||
|
||||
// Test that add message is rewritten.
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove")
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_simultaneous_member_remove")
|
||||
.await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives a msg about Alice removing him from the group.
|
||||
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
|
||||
|
||||
@@ -4956,13 +4865,8 @@ mod tests {
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
|
||||
|
||||
// This doesn't add Fiona back because Bob just removed them.
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome back, Fiona!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_msg_with_implicit_member_add")
|
||||
.await;
|
||||
Ok(())
|
||||
@@ -5092,32 +4996,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if member added message is completely lost,
|
||||
/// member is eventually added.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lost_member_added() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
|
||||
.await;
|
||||
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
|
||||
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
|
||||
// Attempt to add member, but message is lost.
|
||||
let claire_id = Contact::create(alice, "", "claire@foo.de").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, claire_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await;
|
||||
bob.recv_msg(&alice_sent).await;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that group updates are robust to lost messages and eventual out of order arrival.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_modify_chat_lost() -> Result<()> {
|
||||
@@ -6062,11 +5940,11 @@ mod tests {
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
|
||||
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 0);
|
||||
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
|
||||
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
|
||||
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
|
||||
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
|
||||
|
||||
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
|
||||
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
@@ -6083,7 +5961,7 @@ mod tests {
|
||||
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
|
||||
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
|
||||
let msg = msg.replace("Chat-", "XXXX-");
|
||||
assert_eq!(msg.match_indices("Chat-").count(), 0);
|
||||
|
||||
@@ -6639,7 +6517,6 @@ mod tests {
|
||||
// Bob receives all messages
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
let sent1_ts_sent = msg.timestamp_sent;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
|
||||
@@ -6662,7 +6539,6 @@ mod tests {
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
assert!(sent1_ts_sent < msg.timestamp_sent);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ impl Chatlist {
|
||||
if chat.id.is_archived_link() {
|
||||
Ok(Default::default())
|
||||
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
|
||||
Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
|
||||
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
|
||||
} else {
|
||||
Ok(Summary {
|
||||
text: stock_str::no_messages(context).await,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user