mirror of
https://github.com/chatmail/core.git
synced 2026-10-17 18:33:01 +03:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be07f4687 | ||
|
|
7ea50ca8e0 | ||
|
|
1d163fe0e0 | ||
|
|
572cd51f1e | ||
|
|
4188c2e4f5 |
@@ -29,7 +29,6 @@ jobs:
|
||||
lint_rust:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -53,7 +52,6 @@ jobs:
|
||||
cargo_deny:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -68,7 +66,6 @@ jobs:
|
||||
provider_database:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -82,7 +79,6 @@ jobs:
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
@@ -111,7 +107,6 @@ jobs:
|
||||
- os: ubuntu-latest
|
||||
rust: minimum
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- run:
|
||||
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
||||
@@ -160,7 +155,6 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -186,7 +180,6 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -209,7 +202,6 @@ jobs:
|
||||
python_lint:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -227,38 +219,6 @@ jobs:
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e lint
|
||||
|
||||
# mypy does not work with PyPy since mypy 1.19
|
||||
# as it introduced native `librt` dependency
|
||||
# that uses CPython internals.
|
||||
# We only run mypy with CPython because of this.
|
||||
cffi_python_mypy:
|
||||
name: CFFI Python mypy
|
||||
needs: ["c_library", "python_lint"]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: ubuntu-latest-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
|
||||
- name: Run mypy
|
||||
env:
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e mypy
|
||||
|
||||
|
||||
cffi_python_tests:
|
||||
name: CFFI Python tests
|
||||
needs: ["c_library", "python_lint"]
|
||||
@@ -278,14 +238,13 @@ jobs:
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.10
|
||||
# Minimum Supported Python Version = 3.8
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built. Test it with minimum supported Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: "3.10"
|
||||
python: 3.8
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -312,7 +271,7 @@ jobs:
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e doc,py
|
||||
run: tox -e mypy,doc,py
|
||||
|
||||
rpc_python_tests:
|
||||
name: JSON-RPC Python tests
|
||||
@@ -334,12 +293,11 @@ jobs:
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.10
|
||||
# Minimum Supported Python Version = 3.8
|
||||
- os: ubuntu-latest
|
||||
python: "3.10"
|
||||
python: 3.8
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -46,30 +46,6 @@ jobs:
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_linux_wheel:
|
||||
name: Linux wheel
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
path: result/*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
name: Windows
|
||||
strategy:
|
||||
@@ -82,7 +58,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -94,30 +70,6 @@ jobs:
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows_wheel:
|
||||
name: Windows wheel
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
path: result/*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
build_macos:
|
||||
name: macOS
|
||||
strategy:
|
||||
@@ -157,7 +109,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -169,33 +121,9 @@ jobs:
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_android_wheel:
|
||||
name: Android wheel
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
path: result/*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Build wheels and upload binaries to the release
|
||||
needs: ["build_linux", "build_linux_wheel", "build_windows", "build_windows_wheel", "build_macos", "build_android", "build_android_wheel"]
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/deltachat-rpc-server
|
||||
@@ -208,7 +136,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
@@ -216,84 +144,42 @@ jobs:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux aarch64 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux-wheel
|
||||
path: deltachat-rpc-server-aarch64-linux-wheel.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv7l wheel
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux-wheel
|
||||
path: deltachat-rpc-server-armv7l-linux-wheel.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux armv6l wheel
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux-wheel
|
||||
path: deltachat-rpc-server-armv6l-linux-wheel.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux i686 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux-wheel
|
||||
path: deltachat-rpc-server-i686-linux-wheel.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Linux x86_64 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux-wheel
|
||||
path: deltachat-rpc-server-x86_64-linux-wheel.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win32 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win32-wheel
|
||||
path: deltachat-rpc-server-win32-wheel.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download Win64 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win64-wheel
|
||||
path: deltachat-rpc-server-win64-wheel.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
@@ -312,24 +198,12 @@ jobs:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android wheel for arm64-v8a
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
|
||||
- name: Download Android wheel for armeabi-v7a
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
|
||||
|
||||
- name: Create bin/ directory
|
||||
run: |
|
||||
mkdir -p bin
|
||||
@@ -348,21 +222,38 @@ jobs:
|
||||
- name: List binaries
|
||||
run: ls -l bin/
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install wheel
|
||||
run: pip install wheel
|
||||
|
||||
- name: Build deltachat-rpc-server Python wheels
|
||||
- name: Build deltachat-rpc-server Python wheels and source package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
mv deltachat-rpc-server-aarch64-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-armv7l-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-armv6l-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-i686-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-x86_64-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-win64-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-win32-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-arm64-v8a-android-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-armeabi-v7a-android-wheel.d/*.whl 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-arm64-v8a-android-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armeabi-v7a-android-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/
|
||||
@@ -380,7 +271,7 @@ jobs:
|
||||
--repo ${{ github.repository }} \
|
||||
bin/* dist/*
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -105,5 +105,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
|
||||
@@ -1,78 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.29.0] - 2025-12-01
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add Message.exists().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- [**breaking**] Increase backup version from 3 to 4.
|
||||
- Hide `To` header in encrypted messages.
|
||||
- `deltachat_rpc_client.Rpc` accepts `rpc_server_path` for using a particular deltachat-rpc-server ([#7493](https://github.com/chatmail/core/pull/7493)).
|
||||
- Don't send `Chat-Group-Avatar` header in unencrypted groups.
|
||||
- Don't update `self-{avatar,status}` from received messages ([#7002](https://github.com/chatmail/core/pull/7002)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid)` ([#7490](https://github.com/chatmail/core/pull/7490)).
|
||||
- Use the same webxdc ratelimit for all email servers.
|
||||
- Handle the case when account does not exist in `get_existing_msg_ids()`.
|
||||
- Don't send self-avatar in unencrypted messages ([#7136](https://github.com/chatmail/core/pull/7136)).
|
||||
- Do not configure folders during transport configuration.
|
||||
- Upload sync messages only with the primary transport.
|
||||
- Do not use deprecated ConfiguredProvider in get_configured_provider.
|
||||
|
||||
### Build system
|
||||
|
||||
- Make scripts for remote testing usable.
|
||||
- Increase minimum supported Python version to 3.10.
|
||||
- Use SPDX license expression in Python package metadata.
|
||||
|
||||
### CI
|
||||
|
||||
- Set timeout-minutes for all jobs in ci.yaml workflow.
|
||||
- Do not install Python manually to bulid RPC server wheels.
|
||||
- Do not build fake RPC server source packages.
|
||||
- Build Python wheels in separate jobs.
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Remove some unneeded stock strings ([#7496](https://github.com/chatmail/core/pull/7496)).
|
||||
- Strike events in rpc-client request handling, get result from queue.
|
||||
- Use ConfiguredProvider config directly when loading legacy settings.
|
||||
- Remove update_icons and disable_server_delete migrations.
|
||||
- Use `SYMMETRIC_KEY_ALGORITHM` constant in `symm_encrypt_message()`.
|
||||
- Make signing key non-optional for `pk_encrypt`.
|
||||
|
||||
### Tests
|
||||
|
||||
- `test_remove_member_bcc`: Test unencrypted group as it was initially.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump cachix/install-nix-action from 31.8.1 to 31.8.4.
|
||||
- cargo: Bump hyper from 1.7.0 to 1.8.1.
|
||||
- cargo: Bump human-panic from 2.0.3 to 2.0.4.
|
||||
- cargo: Bump hyper-util from 0.1.17 to 0.1.18.
|
||||
- cargo: Bump rusqlite from 0.36.0 to 0.37.0.
|
||||
- cargo: Bump tokio-util from 0.7.16 to 0.7.17.
|
||||
- cargo: Bump toml from 0.9.7 to 0.9.8.
|
||||
- cargo: Bump proptest from 1.8.0 to 1.9.0.
|
||||
- cargo: Bump parking_lot from 0.12.4 to 0.12.5.
|
||||
- cargo: Bump syn from 2.0.106 to 2.0.110.
|
||||
- cargo: Bump quick-xml from 0.38.3 to 0.38.4.
|
||||
- cargo: Bump rustls-pki-types from 1.12.0 to 1.13.0.
|
||||
- cargo: Bump nu-ansi-term from 0.50.1 to 0.50.3.
|
||||
- cargo: Bump sanitize-filename from 0.5.0 to 0.6.0.
|
||||
- cargo: Bump quote from 1.0.41 to 1.0.42.
|
||||
- cargo: Bump libc from 0.2.176 to 0.2.177.
|
||||
- cargo: Bump bytes from 1.10.1 to 1.11.0.
|
||||
- cargo: Bump image from 0.25.8 to 0.25.9.
|
||||
- cargo: Bump rand from 0.9.0 to 0.9.2 ([#7501](https://github.com/chatmail/core/pull/7501)).
|
||||
- cargo: Bump tokio from 1.45.1 to 1.48.0.
|
||||
|
||||
## [2.28.0] - 2025-11-23
|
||||
|
||||
### API-Changes
|
||||
@@ -7309,4 +7236,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.26.0]: https://github.com/chatmail/core/compare/v2.25.0..v2.26.0
|
||||
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
|
||||
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0
|
||||
[2.29.0]: https://github.com/chatmail/core/compare/v2.28.0..v2.29.0
|
||||
|
||||
Generated
+182
-176
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.29.0"
|
||||
version = "2.28.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
@@ -194,14 +194,14 @@ nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.9"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.37"
|
||||
sanitize-filename = "0.6"
|
||||
rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.17"
|
||||
tokio-util = "0.7.16"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
pk_encrypt(
|
||||
plain.clone(),
|
||||
vec![black_box(key_pair.public.clone())],
|
||||
key_pair.secret.clone(),
|
||||
Some(key_pair.secret.clone()),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.29.0"
|
||||
version = "2.28.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -7215,6 +7215,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as device message text.
|
||||
#define DC_STR_SELF_DELETED_MSG_BODY 91
|
||||
|
||||
/// "'Delete messages from server' turned off as now all folders are affected."
|
||||
///
|
||||
/// Used as device message text.
|
||||
#define DC_STR_SERVER_TURNED_OFF 92
|
||||
|
||||
/// "Message deletion timer is set to %1$s minutes."
|
||||
///
|
||||
/// Used in status messages.
|
||||
@@ -7411,6 +7416,19 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_AEAP_ADDR_CHANGED 122
|
||||
|
||||
/// "You changed your email address from %1$s to %2$s.
|
||||
/// If you now send a message to a group, contacts there will automatically
|
||||
/// replace the old with your new address.\n\n It's highly advised to set up
|
||||
/// your old email provider to forward all emails to your new email address.
|
||||
/// Otherwise you might miss messages of contacts who did not get your new
|
||||
/// address yet." + the link to the AEAP blog post
|
||||
///
|
||||
/// As soon as there is a post about AEAP, the UIs should add it:
|
||||
/// set_stock_translation(123, getString(aeap_explanation) + "\n\n" + AEAP_BLOG_LINK)
|
||||
///
|
||||
/// Used in a device message that explains AEAP.
|
||||
#define DC_STR_AEAP_EXPLANATION_AND_LINK 123
|
||||
|
||||
/// "You changed group name from \"%1$s\" to \"%2$s\"."
|
||||
///
|
||||
/// `%1$s` will be replaced by the old group name.
|
||||
@@ -7659,6 +7677,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
|
||||
/// "%1$s sent a message from another device."
|
||||
///
|
||||
/// Used in info messages.
|
||||
/// @deprecated 2025-07
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
///
|
||||
/// Used as the first info messages in newly created groups.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.29.0"
|
||||
version = "2.28.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -121,14 +121,14 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_context_opt(&self, id: u32) -> Option<deltachat::context::Context> {
|
||||
self.accounts.read().await.get_account(id)
|
||||
}
|
||||
|
||||
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
|
||||
self.get_context_opt(id)
|
||||
let sc = self
|
||||
.accounts
|
||||
.read()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))
|
||||
.get_account(id)
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
|
||||
Ok(sc)
|
||||
}
|
||||
|
||||
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
|
||||
@@ -1307,18 +1307,13 @@ impl CommandApi {
|
||||
///
|
||||
/// Returns IDs of existing messages.
|
||||
async fn get_existing_msg_ids(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<Vec<u32>> {
|
||||
if let Some(context) = self.get_context_opt(account_id).await {
|
||||
let msg_ids: Vec<MsgId> = msg_ids.into_iter().map(MsgId::new).collect();
|
||||
let existing_msg_ids = get_existing_msg_ids(&context, &msg_ids).await?;
|
||||
Ok(existing_msg_ids
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect())
|
||||
} else {
|
||||
// Account does not exist, so messages do not exist either,
|
||||
// but this is not an error.
|
||||
Ok(Vec::new())
|
||||
}
|
||||
let context = self.get_context(account_id).await?;
|
||||
let msg_ids: Vec<MsgId> = msg_ids.into_iter().map(MsgId::new).collect();
|
||||
let existing_msg_ids = get_existing_msg_ids(&context, &msg_ids).await?;
|
||||
Ok(existing_msg_ids
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_message_list_items(
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.29.0"
|
||||
"version": "2.28.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.29.0"
|
||||
version = "2.28.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=77"]
|
||||
requires = ["setuptools>=45"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.29.0"
|
||||
license = "MPL-2.0"
|
||||
version = "2.28.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
@@ -22,7 +24,7 @@ classifiers = [
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
|
||||
@@ -60,10 +60,6 @@ class Message:
|
||||
"""Mark the message as seen."""
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Return True if the message exists."""
|
||||
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
"""Continue the Autocrypt Setup Message key transfer.
|
||||
|
||||
|
||||
@@ -4,8 +4,11 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import execnet
|
||||
import py
|
||||
import pytest
|
||||
|
||||
@@ -197,3 +200,102 @@ def log():
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
|
||||
#
|
||||
# support for testing against different deltachat-rpc-server/clients
|
||||
# installed into a temporary virtualenv and connected via 'execnet' channels
|
||||
#
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def get_core_python_env(tmp_path_factory):
|
||||
envs = {}
|
||||
|
||||
def get_core_python(core_version):
|
||||
venv = envs.get(core_version)
|
||||
if venv:
|
||||
return venv
|
||||
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
|
||||
python = sys.executable
|
||||
subprocess.check_call([python, "-m", "venv", venv])
|
||||
pip = venv.joinpath("bin", "pip")
|
||||
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}"]
|
||||
subprocess.check_call([pip, "install", "pytest"] + pkgs)
|
||||
|
||||
envs[core_version] = venv
|
||||
return venv
|
||||
|
||||
return get_core_python
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
|
||||
|
||||
The 'eval' function allows to remote-execute arbitrary expressions
|
||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||
"""
|
||||
|
||||
def factory(core_version):
|
||||
venv = get_core_python_env(core_version)
|
||||
python = venv.joinpath("bin", "python")
|
||||
gw = execnet.makegateway(f"popen//python={python}")
|
||||
|
||||
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
||||
channel = gw.remote_exec(remote_bob_loop)
|
||||
cm = os.environ.get("CHATMAIL_DOMAIN")
|
||||
|
||||
# trigger getting an online account on bob's side
|
||||
channel.send((accounts_dir, str(venv.joinpath("bin", "deltachat-rpc-server")), cm))
|
||||
|
||||
# meanwhile get a local alice account
|
||||
alice = acfactory.get_online_account()
|
||||
channel.send(alice.self_contact.make_vcard())
|
||||
|
||||
# wait for bob to have started
|
||||
sysinfo = channel.receive()
|
||||
assert sysinfo == f"v{core_version}"
|
||||
bob_vcard = channel.receive()
|
||||
[alice_contact_bob] = alice.import_vcard(bob_vcard)
|
||||
|
||||
def eval(eval_str):
|
||||
channel.send(eval_str)
|
||||
return channel.receive()
|
||||
|
||||
return alice, alice_contact_bob, eval
|
||||
|
||||
return factory
|
||||
|
||||
|
||||
def remote_bob_loop(channel):
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
|
||||
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
|
||||
bin_path = str(pathlib.Path(rpc_server_path).parent)
|
||||
os.environ["PATH"] = bin_path + ":" + os.environ["PATH"]
|
||||
|
||||
rpc = Rpc(accounts_dir=accounts_dir)
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
||||
acfactory = ACFactory(dc)
|
||||
bob = acfactory.get_online_account()
|
||||
alice_vcard = channel.receive()
|
||||
[alice_contact] = bob.import_vcard(alice_vcard)
|
||||
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
||||
channel.send(bob.self_contact.make_vcard())
|
||||
|
||||
while 1:
|
||||
eval_str = channel.receive()
|
||||
res = eval(eval_str, ns)
|
||||
try:
|
||||
channel.send(res)
|
||||
except Exception:
|
||||
# some unserializable result
|
||||
channel.send(None)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.20.0", "2.10.0"])
|
||||
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
|
||||
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||
|
||||
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
assert msg.get_snapshot().text == "hello"
|
||||
@@ -1,18 +1,14 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from platform import system # noqa
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
|
||||
|
||||
@pytest.mark.skipif("system() == 'Windows'")
|
||||
def test_install_venv_and_use_other_core(tmp_path):
|
||||
venv = tmp_path.joinpath("venv1")
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv])
|
||||
python = venv / "bin" / "python"
|
||||
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"])
|
||||
python = sys.executable
|
||||
subprocess.check_call([python, "-m", "venv", venv])
|
||||
subprocess.check_call([venv / "bin" / "pip", "install", "deltachat-rpc-server==2.20.0"])
|
||||
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server"))
|
||||
|
||||
with rpc:
|
||||
|
||||
@@ -661,6 +661,8 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
contact = alice.create_contact(account)
|
||||
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")
|
||||
@@ -676,7 +678,15 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
alice_group.send_file(str(path))
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
assert snapshot.chat == bob_group
|
||||
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):
|
||||
@@ -993,22 +1003,3 @@ def test_background_fetch(acfactory, dc):
|
||||
snapshot = messages[-1].get_snapshot()
|
||||
if snapshot.text == "Hello again!":
|
||||
break
|
||||
|
||||
|
||||
def test_message_exists(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
message1 = chat.send_text("Hello!")
|
||||
message2 = chat.send_text("Hello again!")
|
||||
assert message1.exists()
|
||||
assert message2.exists()
|
||||
|
||||
ac1.delete_messages([message1])
|
||||
assert not message1.exists()
|
||||
assert message2.exists()
|
||||
|
||||
# There is no error when checking if
|
||||
# the message exists for deleted account.
|
||||
ac1.remove()
|
||||
assert not message1.exists()
|
||||
assert not message2.exists()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.29.0"
|
||||
version = "2.28.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.29.0"
|
||||
"version": "2.28.0"
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ skip = [
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
@@ -63,6 +62,7 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "zerocopy", version = "0.7.32" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=77", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.29.0"
|
||||
license = "MPL-2.0"
|
||||
version = "2.28.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.8"
|
||||
authors = [
|
||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email",
|
||||
@@ -23,6 +23,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"cffi>=1.0.0",
|
||||
"imap-tools",
|
||||
"importlib_metadata;python_version<'3.8'",
|
||||
"pluggy",
|
||||
"requests",
|
||||
]
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2025-12-01
|
||||
2025-11-23
|
||||
+2
-2
@@ -26,10 +26,10 @@ and an own build machine.
|
||||
i.e. `deltachat-rpc-client` and `deltachat-rpc-server`.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
JSON-RPC Python tests remotely on the build machine.
|
||||
`run-python-test.sh` remotely on the build machine.
|
||||
|
||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
||||
Rust tests remotely on the build machine.
|
||||
`run-rust-test.sh` remotely on the build machine.
|
||||
|
||||
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
|
||||
|
||||
|
||||
@@ -1,32 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
if ! test -v SSHTARGET; then
|
||||
echo >&2 SSHTARGET is not set
|
||||
exit 1
|
||||
fi
|
||||
BUILDDIR=ci_builds/chatmailcore
|
||||
BUILD_ID=${1:?specify build ID}
|
||||
|
||||
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILDDIR=ci_builds/$BUILD_ID
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
|
||||
set -xe
|
||||
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
# we seem to need .git for setuptools_scm versioning
|
||||
find .git >>.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
set +x
|
||||
|
||||
echo "--- Running Python tests remotely"
|
||||
|
||||
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
|
||||
export RUSTC_WRAPPER=\`command -v sccache\`
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
export TARGET=release
|
||||
export CHATMAIL_DOMAIN=$CHATMAIL_DOMAIN
|
||||
|
||||
scripts/make-rpc-testenv.sh
|
||||
. venv/bin/activate
|
||||
#we rely on tox/virtualenv being available in the host
|
||||
#rm -rf virtualenv venv
|
||||
#virtualenv -q -p python3.7 venv
|
||||
#source venv/bin/activate
|
||||
#pip install -q tox virtualenv
|
||||
|
||||
cd deltachat-rpc-client
|
||||
pytest -n6 $@
|
||||
set -x
|
||||
which python
|
||||
source \$HOME/venv/bin/activate
|
||||
which python
|
||||
|
||||
bash scripts/run-python-test.sh
|
||||
_HERE
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#!/bin/bash
|
||||
|
||||
if ! test -v SSHTARGET; then
|
||||
echo >&2 SSHTARGET is not set
|
||||
exit 1
|
||||
fi
|
||||
BUILDDIR=ci_builds/chatmailcore
|
||||
BUILD_ID=${1:?specify build ID}
|
||||
|
||||
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILDDIR=ci_builds/$BUILD_ID
|
||||
|
||||
set -e
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
echo "--- Running Rust tests remotely"
|
||||
|
||||
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
export RUSTC_WRAPPER=\`command -v sccache\`
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
export TARGET=x86_64-unknown-linux-gnu
|
||||
export RUSTC_WRAPPER=sccache
|
||||
|
||||
cargo nextest run
|
||||
bash scripts/run-rust-test.sh
|
||||
_HERE
|
||||
|
||||
|
||||
+1
-1
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py310,py311,py312,py313,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py38,py39,py310,py311,py312,py313,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
@@ -173,8 +173,11 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
.unwrap();
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
let avatar_path = Path::new(&avatar_blob);
|
||||
assert!(
|
||||
avatar_blob.ends_with("7dde69e06b5ae6c27520a436bbfd65b.jpg"),
|
||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||
);
|
||||
let scaled_avatar_size = file_size(avatar_path).await;
|
||||
info!(&t, "Scaled avatar size: {scaled_avatar_size}.");
|
||||
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
|
||||
|
||||
check_image_size(avatar_src, 1000, 1000);
|
||||
@@ -184,11 +187,6 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
|
||||
assert!(
|
||||
avatar_blob.ends_with("2a048b6fcd86448032b854ea1ad7608.jpg"),
|
||||
"The avatar filename should be its hash, but instead it's {avatar_blob}"
|
||||
);
|
||||
|
||||
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let strict_limits = true;
|
||||
|
||||
+4
-10
@@ -3730,16 +3730,10 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
chat.typ != Chattype::OutBroadcast || contact_id != ContactId::SELF,
|
||||
"Cannot add SELF to broadcast channel."
|
||||
);
|
||||
match chat.is_encrypted(context).await? {
|
||||
true => ensure!(
|
||||
contact.is_key_contact(),
|
||||
"Only key-contacts can be added to encrypted chats"
|
||||
),
|
||||
false => ensure!(
|
||||
!contact.is_key_contact(),
|
||||
"Only address-contacts can be added to unencrypted chats"
|
||||
),
|
||||
}
|
||||
ensure!(
|
||||
chat.is_encrypted(context).await? == contact.is_key_contact(),
|
||||
"Only key-contacts can be added to encrypted chats"
|
||||
);
|
||||
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
|
||||
+13
-1
@@ -815,6 +815,15 @@ async fn test_self_talk() -> Result<()> {
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
let payload = sent_msg.payload();
|
||||
// Make sure the `To` field contains the address and not
|
||||
// "undisclosed recipients".
|
||||
// Otherwise Delta Chat core <1.153.0 assigns the message
|
||||
// to the trash chat.
|
||||
assert_eq!(
|
||||
payload.match_indices("To: <alice@example.org>\r\n").count(),
|
||||
1
|
||||
);
|
||||
|
||||
let t2 = TestContext::new_alice().await;
|
||||
t2.recv_msg(&sent_msg).await;
|
||||
@@ -3312,7 +3321,10 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
|
||||
let leave_msg = bob0.pop_sent_msg().await;
|
||||
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?;
|
||||
assert_eq!(parsed.parts[0].msg, "I left the group.");
|
||||
assert_eq!(
|
||||
parsed.parts[0].msg,
|
||||
stock_str::msg_group_left_remote(bob0).await
|
||||
);
|
||||
|
||||
let rcvd = bob1.recv_msg(&leave_msg).await;
|
||||
|
||||
|
||||
+8
-7
@@ -18,7 +18,7 @@ use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::Provider;
|
||||
use crate::provider::{Provider, get_provider_by_id};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
@@ -646,14 +646,15 @@ impl Context {
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Gets the configured provider.
|
||||
/// Gets the configured provider, as saved in the `configured_provider` value.
|
||||
///
|
||||
/// The provider is determined by the current primary transport.
|
||||
/// The provider is determined by `get_provider_info()` during configuration and then saved
|
||||
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
|
||||
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
|
||||
let provider = ConfiguredLoginParam::load(self)
|
||||
.await?
|
||||
.and_then(|(_transport_id, param)| param.provider);
|
||||
Ok(provider)
|
||||
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
|
||||
return Ok(get_provider_by_id(&cfg));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
|
||||
@@ -278,6 +278,7 @@ async fn test_sync() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -287,7 +288,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
let status = "Sent via usual message";
|
||||
let status = "Synced via usual message";
|
||||
alice0.set_config(Config::Selfstatus, Some(status)).await?;
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_sync_msg().await;
|
||||
@@ -296,7 +297,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
tcm.send_recv(alice0, alice1, "hi Alice!").await;
|
||||
assert_eq!(
|
||||
alice1.get_config(Config::Selfstatus).await?,
|
||||
Some(status1.to_string())
|
||||
Some(status.to_string())
|
||||
);
|
||||
sync(alice1, alice0).await;
|
||||
assert_eq!(
|
||||
@@ -327,7 +328,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".jpg"))
|
||||
.filter(|path| path.ends_with(".png"))
|
||||
.is_some()
|
||||
);
|
||||
sync(alice1, alice0).await;
|
||||
|
||||
+26
-5
@@ -27,7 +27,7 @@ use crate::config::{self, Config};
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::warn;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::message::Message;
|
||||
@@ -44,6 +44,7 @@ use crate::transport::{
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
@@ -266,7 +267,7 @@ impl Context {
|
||||
let provider = configure(self, param).await?;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, provider).await?;
|
||||
on_configure_completed(self, provider, old_addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -274,6 +275,7 @@ impl Context {
|
||||
async fn on_configure_completed(
|
||||
context: &Context,
|
||||
provider: Option<&'static Provider>,
|
||||
old_addr: Option<String>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = provider {
|
||||
if let Some(config_defaults) = provider.config_defaults {
|
||||
@@ -303,6 +305,20 @@ async fn on_configure_completed(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await?
|
||||
&& let Some(old_addr) = old_addr
|
||||
&& !addr_cmp(&new_addr, &old_addr)
|
||||
{
|
||||
let mut msg = Message::new_text(
|
||||
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
|
||||
);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.context("Cannot add AEAP explanation")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -538,11 +554,12 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||
let configuring = true;
|
||||
if let Err(err) = imap.connect(ctx, configuring).await {
|
||||
bail!(
|
||||
let mut imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => bail!(
|
||||
"{}",
|
||||
nicer_configuration_error(ctx, format!("{err:#}")).await
|
||||
);
|
||||
),
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
@@ -557,6 +574,10 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
|
||||
}
|
||||
|
||||
let create_mvbox = false;
|
||||
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
|
||||
.await?;
|
||||
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
+51
-19
@@ -383,16 +383,20 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
if let Some(path) = path
|
||||
&& let Err(e) = set_profile_image(context, id, &AvatarAction::Change(path)).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
|
||||
);
|
||||
if let Some(path) = path {
|
||||
// Currently this value doesn't matter as we don't import the contact of self.
|
||||
let was_encrypted = false;
|
||||
if let Err(e) =
|
||||
set_profile_image(context, id, &AvatarAction::Change(path), was_encrypted).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(biography) = &contact.biography
|
||||
&& let Err(e) = set_status(context, id, biography.to_owned()).await
|
||||
&& let Err(e) = set_status(context, id, biography.to_owned(), false, false).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
@@ -1505,6 +1509,18 @@ impl Contact {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Get authorized name or address.
|
||||
///
|
||||
/// This string is suitable for sending over email
|
||||
/// as it does not leak the locally set name.
|
||||
pub(crate) fn get_authname_or_addr(&self) -> String {
|
||||
if !self.authname.is_empty() {
|
||||
(&self.authname).into()
|
||||
} else {
|
||||
(&self.addr).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a summary of name and address.
|
||||
///
|
||||
/// The returned string is either "Name (email@domain.com)" or just
|
||||
@@ -1820,19 +1836,25 @@ WHERE type=? AND id IN (
|
||||
/// The given profile image is expected to be already in the blob directory
|
||||
/// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
///
|
||||
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar.
|
||||
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
|
||||
/// this typically happens if we see message with our own profile image.
|
||||
pub(crate) async fn set_profile_image(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
profile_image: &AvatarAction,
|
||||
was_encrypted: bool,
|
||||
) -> Result<()> {
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let changed = match profile_image {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
if contact_id == ContactId::SELF {
|
||||
context
|
||||
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
|
||||
.await?;
|
||||
if was_encrypted {
|
||||
context
|
||||
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
|
||||
.await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar.");
|
||||
}
|
||||
} else {
|
||||
contact.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
@@ -1840,9 +1862,13 @@ pub(crate) async fn set_profile_image(
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
if contact_id == ContactId::SELF {
|
||||
context
|
||||
.set_config_ex(Nosync, Config::Selfavatar, None)
|
||||
.await?;
|
||||
if was_encrypted {
|
||||
context
|
||||
.set_config_ex(Nosync, Config::Selfavatar, None)
|
||||
.await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar deletion.");
|
||||
}
|
||||
} else {
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
}
|
||||
@@ -1859,16 +1885,22 @@ pub(crate) async fn set_profile_image(
|
||||
|
||||
/// Sets contact status.
|
||||
///
|
||||
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus.
|
||||
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus. This
|
||||
/// is only done if message is sent from Delta Chat and it is encrypted, to synchronize signature
|
||||
/// between Delta Chat devices.
|
||||
pub(crate) async fn set_status(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
status: String,
|
||||
encrypted: bool,
|
||||
has_chat_version: bool,
|
||||
) -> Result<()> {
|
||||
if contact_id == ContactId::SELF {
|
||||
context
|
||||
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
|
||||
.await?;
|
||||
if encrypted && has_chat_version {
|
||||
context
|
||||
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::*;
|
||||
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync};
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
|
||||
#[test]
|
||||
fn test_contact_id_values() {
|
||||
@@ -850,7 +850,8 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that self-status is not synchronized from outgoing messages.
|
||||
/// Tests that status is synchronized when sending encrypted BCC-self messages and not
|
||||
/// synchronized when the message is not encrypted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_synchronize_status() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -869,12 +870,21 @@ async fn test_synchronize_status() -> Result<()> {
|
||||
.await?;
|
||||
let chat = alice1.create_email_chat(bob).await;
|
||||
|
||||
// Alice sends an unencrypted message to Bob from the first device.
|
||||
// Alice sends a message to Bob from the first device.
|
||||
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// Message is not encrypted.
|
||||
let message = sent_msg.load_from_db().await;
|
||||
assert!(!message.get_showpadlock());
|
||||
|
||||
// Alice's second devices receives a copy of outgoing message.
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Bob receives message.
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
// Message was not encrypted, so status is not copied.
|
||||
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
|
||||
|
||||
// Alice sends encrypted message.
|
||||
@@ -882,9 +892,17 @@ async fn test_synchronize_status() -> Result<()> {
|
||||
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// Second message is encrypted.
|
||||
let message = sent_msg.load_from_db().await;
|
||||
assert!(message.get_showpadlock());
|
||||
|
||||
// Alice's second devices receives a copy of second outgoing message.
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
|
||||
|
||||
assert_eq!(
|
||||
alice2.get_config(Config::Selfstatus).await?,
|
||||
Some("New status".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -897,9 +915,9 @@ async fn test_selfavatar_changed_event() -> Result<()> {
|
||||
// Alice has two devices.
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
// Bob has one device.
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
|
||||
|
||||
@@ -915,7 +933,17 @@ async fn test_selfavatar_changed_event() -> Result<()> {
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.await;
|
||||
|
||||
sync(alice1, alice2).await;
|
||||
// Alice sends a message.
|
||||
let alice1_chat_id = alice1.create_chat(bob).await.id;
|
||||
send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// The message is encrypted.
|
||||
let message = sent_msg.load_from_db().await;
|
||||
assert!(message.get_showpadlock());
|
||||
|
||||
// Alice's second device receives a copy of the outgoing message.
|
||||
alice2.recv_msg(&sent_msg).await;
|
||||
|
||||
// Alice's second device applies the selfavatar.
|
||||
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());
|
||||
|
||||
+7
-1
@@ -453,7 +453,7 @@ impl Context {
|
||||
translated_stockstrings: stockstrings,
|
||||
events,
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3.
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
|
||||
quota: RwLock::new(None),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
@@ -485,6 +485,12 @@ impl Context {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_chatmail().await.unwrap_or_default() {
|
||||
let mut lock = self.ratelimit.write().await;
|
||||
// Allow at least 1 message every second + a burst of 3.
|
||||
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
|
||||
}
|
||||
|
||||
// The next line is mainly for iOS:
|
||||
// iOS starts a separate process for receiving notifications and if the user concurrently
|
||||
// starts the app, the UI process opens the database but waits with calling start_io()
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ impl EncryptHelper {
|
||||
let ctext = pgp::pk_encrypt(
|
||||
raw_message,
|
||||
keyring,
|
||||
sign_key,
|
||||
Some(sign_key),
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
)
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ pub(crate) struct Imap {
|
||||
pub(crate) idle_interrupt_receiver: Receiver<()>,
|
||||
|
||||
/// Email address.
|
||||
pub(crate) addr: String,
|
||||
addr: String,
|
||||
|
||||
/// Login parameters.
|
||||
lp: Vec<ConfiguredServerLoginParam>,
|
||||
|
||||
+27
-14
@@ -9,7 +9,7 @@ use data_encoding::BASE32_NOPAD;
|
||||
use deltachat_contact_tools::sanitize_bidi_characters;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mail_builder::headers::HeaderType;
|
||||
use mail_builder::headers::address::Address;
|
||||
use mail_builder::headers::address::{Address, EmailAddress};
|
||||
use mail_builder::mime::MimePart;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -464,11 +464,7 @@ impl MimeFactory {
|
||||
.unwrap_or_default(),
|
||||
false => "".to_string(),
|
||||
};
|
||||
// We don't display avatars for address-contacts, so sending avatars w/o encryption is not
|
||||
// useful and causes e.g. Outlook to reject a message with a big header, see
|
||||
// https://support.delta.chat/t/invalid-mime-content-single-text-value-size-32822-exceeded-allowed-maximum-32768-for-the-chat-user-avatar-header/4067.
|
||||
let attach_selfavatar =
|
||||
Self::should_attach_selfavatar(context, &msg).await && encryption_pubkeys.is_some();
|
||||
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
member_timestamps.is_empty()
|
||||
@@ -997,7 +993,24 @@ impl MimeFactory {
|
||||
} else if header_name == "to" {
|
||||
protected_headers.push(header.clone());
|
||||
if is_encrypted {
|
||||
unprotected_headers.push(("To", hidden_recipients().into()));
|
||||
let mut to_without_names = to
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|header| match header {
|
||||
Address::Address(mb) => Some(Address::Address(EmailAddress {
|
||||
name: None,
|
||||
email: mb.email,
|
||||
})),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if to_without_names.is_empty() {
|
||||
to_without_names.push(hidden_recipients());
|
||||
}
|
||||
unprotected_headers.push((
|
||||
original_header_name,
|
||||
Address::new_list(to_without_names).into(),
|
||||
));
|
||||
} else {
|
||||
unprotected_headers.push(header.clone());
|
||||
}
|
||||
@@ -1525,9 +1538,10 @@ impl MimeFactory {
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
placeholdertext = Some("I left the group.".to_string());
|
||||
placeholdertext = Some(stock_str::msg_group_left_remote(context).await);
|
||||
} else {
|
||||
placeholdertext = Some(format!("I removed member {email_to_remove}."));
|
||||
placeholdertext =
|
||||
Some(stock_str::msg_del_member_remote(context, email_to_remove).await);
|
||||
};
|
||||
|
||||
if !email_to_remove.is_empty() {
|
||||
@@ -1550,7 +1564,8 @@ impl MimeFactory {
|
||||
let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
let fingerprint_to_add = msg.param.get(Param::Arg4).unwrap_or_default();
|
||||
|
||||
placeholdertext = Some(format!("I added member {email_to_add}."));
|
||||
placeholdertext =
|
||||
Some(stock_str::msg_add_member_remote(context, email_to_add).await);
|
||||
|
||||
if !email_to_add.is_empty() {
|
||||
headers.push((
|
||||
@@ -1586,7 +1601,7 @@ impl MimeFactory {
|
||||
"Chat-Content",
|
||||
mail_builder::headers::text::Text::new("group-avatar-changed").into(),
|
||||
));
|
||||
if grpimage.is_none() && is_encrypted {
|
||||
if grpimage.is_none() {
|
||||
headers.push((
|
||||
"Chat-Group-Avatar",
|
||||
mail_builder::headers::raw::Raw::new("0").into(),
|
||||
@@ -1713,9 +1728,7 @@ impl MimeFactory {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(grpimage) = grpimage
|
||||
&& is_encrypted
|
||||
{
|
||||
if let Some(grpimage) = grpimage {
|
||||
info!(context, "setting group image '{}'", grpimage);
|
||||
let avatar = build_avatar_file(context, grpimage)
|
||||
.await
|
||||
|
||||
@@ -6,8 +6,7 @@ use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
self, ChatId, add_contact_to_chat, create_group, create_group_unencrypted,
|
||||
remove_contact_from_chat, send_text_msg,
|
||||
self, ChatId, add_contact_to_chat, create_group, remove_contact_from_chat, send_text_msg,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants;
|
||||
@@ -352,7 +351,7 @@ async fn test_subject_in_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let group_id = create_group(&t, "groupname").await.unwrap();
|
||||
let group_id = chat::create_group(&t, "groupname").await.unwrap();
|
||||
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
|
||||
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
|
||||
|
||||
@@ -593,6 +592,26 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
|
||||
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
assert_eq!(inner.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1);
|
||||
assert_eq!(inner.match_indices("Subject:").count(), 0);
|
||||
|
||||
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||
|
||||
// if another message is sent, that one must not contain the avatar
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
|
||||
let outer = payload.next().unwrap();
|
||||
let inner = payload.next().unwrap();
|
||||
let body = payload.next().unwrap();
|
||||
|
||||
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
|
||||
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
assert_eq!(inner.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
@@ -651,7 +670,7 @@ async fn test_selfavatar_unencrypted_signed() {
|
||||
assert_eq!(part.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 0);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 0);
|
||||
|
||||
let body = payload.next().unwrap();
|
||||
@@ -665,6 +684,58 @@ async fn test_selfavatar_unencrypted_signed() {
|
||||
.unwrap();
|
||||
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
|
||||
assert_eq!(alice_contact.is_key_contact(), false);
|
||||
assert!(
|
||||
alice_contact
|
||||
.get_profile_image(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// if another message is sent, that one must not contain the avatar
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(part.match_indices("multipart/signed").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(
|
||||
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(part.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(body.match_indices("From:").count(), 0);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 0);
|
||||
|
||||
let body = payload.next().unwrap();
|
||||
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
|
||||
assert!(
|
||||
alice_contact
|
||||
.get_profile_image(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that removed member address does not go into the `To:` field.
|
||||
@@ -672,20 +743,17 @@ async fn test_selfavatar_unencrypted_signed() {
|
||||
async fn test_remove_member_bcc() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Alice creates a group with Bob and Charlie and then removes Charlie.
|
||||
|
||||
// Alice creates a group with Bob and Claire and then removes Bob.
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
let charlie_contact = Contact::get_by_id(alice, charlie_id).await?;
|
||||
let charlie_addr = charlie_contact.get_addr();
|
||||
|
||||
let bob_id = alice.add_or_lookup_address_contact_id(bob).await;
|
||||
let charlie_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
|
||||
let alice_chat_id = create_group_unencrypted(alice, "foo").await?;
|
||||
let alice_chat_id = create_group(alice, "foo").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
|
||||
send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?;
|
||||
@@ -702,9 +770,8 @@ async fn test_remove_member_bcc() -> Result<()> {
|
||||
for to_addr in to.iter() {
|
||||
match to_addr {
|
||||
mailparse::MailAddr::Single(info) => {
|
||||
// Addresses should be of existing members and not Charlie.
|
||||
// Addresses should be of existing members (Alice and Bob) and not Charlie.
|
||||
assert_ne!(info.addr, charlie_addr);
|
||||
assert!(info.addr == alice_addr || info.addr == bob_addr);
|
||||
}
|
||||
mailparse::MailAddr::Group(_) => {
|
||||
panic!("Group addresses are not expected here");
|
||||
|
||||
+43
-16
@@ -165,7 +165,7 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey
|
||||
pub async fn pk_encrypt(
|
||||
plain: Vec<u8>,
|
||||
public_keys_for_encryption: Vec<SignedPublicKey>,
|
||||
private_key_for_signing: SignedSecretKey,
|
||||
private_key_for_signing: Option<SignedSecretKey>,
|
||||
compress: bool,
|
||||
anonymous_recipients: bool,
|
||||
) -> Result<String> {
|
||||
@@ -187,9 +187,11 @@ pub async fn pk_encrypt(
|
||||
}
|
||||
}
|
||||
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
if let Some(ref skey) = private_key_for_signing {
|
||||
msg.sign(&**skey, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
}
|
||||
|
||||
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
|
||||
@@ -404,7 +406,7 @@ pub async fn symm_encrypt_message(
|
||||
};
|
||||
let mut msg = msg.seipd_v2(
|
||||
&mut rng,
|
||||
SYMMETRIC_KEY_ALGORITHM,
|
||||
SymmetricKeyAlgorithm::AES128,
|
||||
AeadAlgorithm::Ocb,
|
||||
ChunkSize::C8KiB,
|
||||
);
|
||||
@@ -532,6 +534,7 @@ mod tests {
|
||||
static KEYS: LazyLock<TestKeys> = LazyLock::new(TestKeys::new);
|
||||
|
||||
static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
|
||||
static CTEXT_UNSIGNED: OnceCell<String> = OnceCell::const_new();
|
||||
|
||||
/// A ciphertext encrypted to Alice & Bob, signed by Alice.
|
||||
async fn ctext_signed() -> &'static String {
|
||||
@@ -544,7 +547,28 @@ mod tests {
|
||||
pk_encrypt(
|
||||
CLEARTEXT.to_vec(),
|
||||
keyring,
|
||||
KEYS.alice_secret.clone(),
|
||||
Some(KEYS.alice_secret.clone()),
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// A ciphertext encrypted to Alice & Bob, not signed.
|
||||
async fn ctext_unsigned() -> &'static String {
|
||||
let anonymous_recipients = true;
|
||||
CTEXT_UNSIGNED
|
||||
.get_or_init(|| async {
|
||||
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
|
||||
let compress = true;
|
||||
|
||||
pk_encrypt(
|
||||
CLEARTEXT.to_vec(),
|
||||
keyring,
|
||||
None,
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
)
|
||||
@@ -564,6 +588,16 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encrypt_unsigned() {
|
||||
assert!(!ctext_unsigned().await.is_empty());
|
||||
assert!(
|
||||
ctext_unsigned()
|
||||
.await
|
||||
.starts_with("-----BEGIN PGP MESSAGE-----")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_singed() {
|
||||
// Check decrypting as Alice
|
||||
@@ -618,9 +652,9 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_unsigned() {
|
||||
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
let ctext_unsigned = include_bytes!("../test-data/message/ctext_unsigned.asc");
|
||||
let (_msg, valid_signatures, content) =
|
||||
pk_decrypt_and_validate(ctext_unsigned, &decrypt_keyring, &[]).unwrap();
|
||||
pk_decrypt_and_validate(ctext_unsigned().await.as_bytes(), &decrypt_keyring, &[])
|
||||
.unwrap();
|
||||
assert_eq!(content, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
@@ -710,14 +744,7 @@ mod tests {
|
||||
let pk_for_encryption = load_self_public_key(alice).await?;
|
||||
|
||||
// Encrypt a message, but only to self, not to Bob:
|
||||
let ctext = pk_encrypt(
|
||||
plain,
|
||||
vec![pk_for_encryption],
|
||||
KEYS.alice_secret.clone(),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let ctext = pk_encrypt(plain, vec![pk_for_encryption], None, true, true).await?;
|
||||
|
||||
// Trying to decrypt it should fail with an OK error message:
|
||||
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
|
||||
|
||||
/// Version written to Backups and Backup-QR-Codes.
|
||||
/// Imports will fail when they have a larger version.
|
||||
pub(crate) const DCBACKUP_VERSION: i32 = 4;
|
||||
pub(crate) const DCBACKUP_VERSION: i32 = 3;
|
||||
|
||||
/// Scanned QR code.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -991,6 +991,42 @@ Content-Disposition: reaction\n\
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for reaction resetting self-status.
|
||||
///
|
||||
/// Reactions do not contain the status,
|
||||
/// but should not result in self-status being reset on other devices.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_status_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = tcm.alice().await;
|
||||
let alice2 = tcm.alice().await;
|
||||
|
||||
alice1
|
||||
.set_config(Config::Selfstatus, Some("New status"))
|
||||
.await?;
|
||||
|
||||
let alice2_msg = tcm.send_recv(&alice1, &alice2, "Hi!").await;
|
||||
assert_eq!(
|
||||
alice2.get_config(Config::Selfstatus).await?.as_deref(),
|
||||
Some("New status")
|
||||
);
|
||||
|
||||
// Alice reacts to own message from second device,
|
||||
// first device receives rection.
|
||||
{
|
||||
send_reaction(&alice2, alice2_msg.id, "👍").await?;
|
||||
let msg = alice2.pop_sent_msg().await;
|
||||
alice1.recv_msg_hidden(&msg).await;
|
||||
}
|
||||
|
||||
// Check that the status is still the same.
|
||||
assert_eq!(
|
||||
alice1.get_config(Config::Selfstatus).await?.as_deref(),
|
||||
Some("New status")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_reaction_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
+13
-4
@@ -885,11 +885,13 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar
|
||||
&& !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
|
||||
&& from_id != ContactId::UNDEFINED
|
||||
&& context
|
||||
.update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
|
||||
.await?
|
||||
&& let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
|
||||
&& let Err(err) =
|
||||
contact::set_profile_image(context, from_id, avatar_action, mime_parser.was_encrypted())
|
||||
.await
|
||||
{
|
||||
warn!(context, "receive_imf cannot update profile image: {err:#}.");
|
||||
};
|
||||
@@ -897,11 +899,18 @@ pub(crate) async fn receive_imf_inner(
|
||||
// Ignore footers from mailinglists as they are often created or modified by the mailinglist software.
|
||||
if let Some(footer) = &mime_parser.footer
|
||||
&& !mime_parser.is_mailinglist_message()
|
||||
&& !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
|
||||
&& from_id != ContactId::UNDEFINED
|
||||
&& context
|
||||
.update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
|
||||
.await?
|
||||
&& let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
|
||||
&& let Err(err) = contact::set_status(
|
||||
context,
|
||||
from_id,
|
||||
footer.to_string(),
|
||||
mime_parser.was_encrypted(),
|
||||
mime_parser.has_chat_version(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(context, "Cannot update contact status: {err:#}.");
|
||||
}
|
||||
|
||||
+6
-13
@@ -578,19 +578,12 @@ async fn fetch_idle(
|
||||
mvbox.as_deref().unwrap_or(&watch_folder)
|
||||
}
|
||||
};
|
||||
if ctx
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
== connection.addr
|
||||
{
|
||||
session
|
||||
.send_sync_msgs(ctx, syncbox)
|
||||
.await
|
||||
.context("fetch_idle: send_sync_msgs")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
session
|
||||
.send_sync_msgs(ctx, syncbox)
|
||||
.await
|
||||
.context("fetch_idle: send_sync_msgs")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
|
||||
session
|
||||
.store_seen_flags_on_imap(ctx)
|
||||
|
||||
+17
-2
@@ -9,6 +9,7 @@ use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::context::Context;
|
||||
@@ -17,11 +18,12 @@ use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::net::dns::prune_dns_cache;
|
||||
use crate::net::http::http_cache_cleanup;
|
||||
use crate::net::prune_connection_history;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::stock_str;
|
||||
use crate::tools::{SystemTime, Time, delete_file, time, time_elapsed};
|
||||
|
||||
/// Extension to [`rusqlite::ToSql`] trait
|
||||
@@ -214,13 +216,26 @@ impl Sql {
|
||||
// this should be done before updates that use high-level objects that
|
||||
// rely themselves on the low-level structure.
|
||||
|
||||
let recode_avatar = migrations::run(context, self)
|
||||
// `update_icons` is not used anymore, since it's not necessary anymore to "update" icons:
|
||||
let (_update_icons, disable_server_delete, recode_avatar) = migrations::run(context, self)
|
||||
.await
|
||||
.context("failed to run migrations")?;
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// the structure is complete now and all objects are usable
|
||||
|
||||
if disable_server_delete {
|
||||
// We now always watch all folders and delete messages there if delete_server is enabled.
|
||||
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
|
||||
if context.get_config_delete_server_after().await?.is_some() {
|
||||
let mut msg = Message::new_text(stock_str::delete_server_turned_off(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
context
|
||||
.set_config_internal(Config::DeleteServerAfter, Some("0"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if recode_avatar && let Some(avatar) = context.get_config(Config::Selfavatar).await? {
|
||||
let mut blob = BlobObject::from_path(context, Path::new(&avatar))?;
|
||||
match blob.recode_to_avatar_size(context).await {
|
||||
|
||||
+17
-11
@@ -31,7 +31,7 @@ tokio::task_local! {
|
||||
static STOP_MIGRATIONS_AT: i32;
|
||||
}
|
||||
|
||||
pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
|
||||
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> {
|
||||
let mut exists_before_update = false;
|
||||
let mut dbversion_before_update = DBVERSION;
|
||||
|
||||
@@ -68,6 +68,8 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
|
||||
}
|
||||
|
||||
let dbversion = dbversion_before_update;
|
||||
let mut update_icons = !exists_before_update;
|
||||
let mut disable_server_delete = false;
|
||||
let mut recode_avatar = false;
|
||||
|
||||
if dbversion < 1 {
|
||||
@@ -297,6 +299,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
|
||||
61,
|
||||
)
|
||||
.await?;
|
||||
update_icons = true;
|
||||
}
|
||||
if dbversion < 62 {
|
||||
sql.execute_migration(
|
||||
@@ -324,6 +327,7 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 66 {
|
||||
update_icons = true;
|
||||
sql.set_db_version(66).await?;
|
||||
}
|
||||
if dbversion < 67 {
|
||||
@@ -441,6 +445,17 @@ CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
if exists_before_update {
|
||||
disable_server_delete = true;
|
||||
|
||||
// Don't disable server delete if it was on by default (Nauta):
|
||||
if let Some(provider) = context.get_configured_provider().await?
|
||||
&& let Some(defaults) = &provider.config_defaults
|
||||
&& defaults.iter().any(|d| d.key == Config::DeleteServerAfter)
|
||||
{
|
||||
disable_server_delete = false;
|
||||
}
|
||||
}
|
||||
sql.set_db_version(73).await?;
|
||||
}
|
||||
if dbversion < 74 {
|
||||
@@ -1430,15 +1445,6 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 141)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid)",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
@@ -1453,7 +1459,7 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
|
||||
}
|
||||
info!(context, "Database version: v{new_version}.");
|
||||
|
||||
Ok(recode_avatar)
|
||||
Ok((update_icons, disable_server_delete, recode_avatar))
|
||||
}
|
||||
|
||||
fn migrate_key_contacts(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use crate::message::Message;
|
||||
use crate::{EventType, test_utils::TestContext};
|
||||
|
||||
#[test]
|
||||
@@ -180,7 +179,9 @@ async fn test_migration_flags() -> Result<()> {
|
||||
// as migrations::run() was already executed on context creation,
|
||||
// another call should not result in any action needed.
|
||||
// this test catches some bugs where dbversion was forgotten to be persisted.
|
||||
let recode_avatar = migrations::run(&t, &t.sql).await?;
|
||||
let (update_icons, disable_server_delete, recode_avatar) = migrations::run(&t, &t.sql).await?;
|
||||
assert!(!update_icons);
|
||||
assert!(!disable_server_delete);
|
||||
assert!(!recode_avatar);
|
||||
|
||||
info!(&t, "test_migration_flags: XXX END MARKER");
|
||||
|
||||
+84
-1
@@ -13,7 +13,7 @@ use crate::accounts::Accounts;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
@@ -155,6 +155,13 @@ pub enum StockMessage {
|
||||
To use the \"Saved messages\" feature again, create a new chat with yourself."))]
|
||||
SelfDeletedMsgBody = 91,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "⚠️ The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\
|
||||
ℹ️ To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \
|
||||
Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it."
|
||||
))]
|
||||
DeleteServerTurnedOff = 92,
|
||||
|
||||
#[strum(props(fallback = "Forwarded"))]
|
||||
Forwarded = 97,
|
||||
|
||||
@@ -234,6 +241,11 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Not connected"))]
|
||||
NotConnected = 121,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
|
||||
))]
|
||||
AeapExplanationAndLink = 123,
|
||||
|
||||
#[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
|
||||
MsgYouChangedGrpName = 124,
|
||||
|
||||
@@ -344,9 +356,22 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
|
||||
BackupTransferMsgBody = 163,
|
||||
|
||||
#[strum(props(fallback = "I added member %1$s."))]
|
||||
MsgIAddMember = 164,
|
||||
|
||||
#[strum(props(fallback = "I removed member %1$s."))]
|
||||
MsgIDelMember = 165,
|
||||
|
||||
#[strum(props(fallback = "I left the group."))]
|
||||
MsgILeftGroup = 166,
|
||||
|
||||
#[strum(props(fallback = "Messages are end-to-end encrypted."))]
|
||||
ChatProtectionEnabled = 170,
|
||||
|
||||
// deprecated 2025-07
|
||||
#[strum(props(fallback = "%1$s sent a message from another device."))]
|
||||
ChatProtectionDisabled = 171,
|
||||
|
||||
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
|
||||
NewGroupSendFirstMessage = 172,
|
||||
|
||||
@@ -601,6 +626,25 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `I added member %1$s.`.
|
||||
/// This one is for sending in group chats.
|
||||
///
|
||||
/// The `added_member_addr` parameter should be an email address and is looked up in the
|
||||
/// contacts to combine with the authorized display name.
|
||||
pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
|
||||
let addr = added_member_addr;
|
||||
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_authname_or_addr())
|
||||
.unwrap_or_else(|_| addr.to_string()),
|
||||
_ => addr.to_string(),
|
||||
};
|
||||
translated(context, StockMessage::MsgIAddMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
}
|
||||
|
||||
/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
|
||||
///
|
||||
/// The `added_member_addr` parameter should be an email address and is looked up in the
|
||||
@@ -627,6 +671,24 @@ pub(crate) async fn msg_add_member_local(
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `I removed member %1$s.`.
|
||||
///
|
||||
/// The `removed_member_addr` parameter should be an email address and is looked up in
|
||||
/// the contacts to combine with the display name.
|
||||
pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
|
||||
let addr = removed_member_addr;
|
||||
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_authname_or_addr())
|
||||
.unwrap_or_else(|_| addr.to_string()),
|
||||
_ => addr.to_string(),
|
||||
};
|
||||
translated(context, StockMessage::MsgIDelMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
}
|
||||
|
||||
/// Stock string: `I added member %1$s.` or `Member %1$s removed by %2$s.`.
|
||||
///
|
||||
/// The `removed_member_addr` parameter should be an email address and is looked up in
|
||||
@@ -653,6 +715,11 @@ pub(crate) async fn msg_del_member_local(
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `I left the group.`.
|
||||
pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgILeftGroup).await
|
||||
}
|
||||
|
||||
/// Stock string: `You left the group.` or `Group left by %1$s.`.
|
||||
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -1034,6 +1101,11 @@ pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::SelfDeletedMsgBody).await
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ The "Delete messages from server" feature now also...`.
|
||||
pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeleteServerTurnedOff).await
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to %1$s minutes.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_minutes(
|
||||
context: &Context,
|
||||
@@ -1221,6 +1293,17 @@ pub(crate) async fn stats_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::StatsMsgBody).await
|
||||
}
|
||||
|
||||
pub(crate) async fn aeap_explanation_and_link(
|
||||
context: &Context,
|
||||
old_addr: &str,
|
||||
new_addr: &str,
|
||||
) -> String {
|
||||
translated(context, StockMessage::AeapExplanationAndLink)
|
||||
.await
|
||||
.replace1(old_addr)
|
||||
.replace2(new_addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Others will only see this group after you sent a first message.`.
|
||||
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::NewGroupSendFirstMessage).await
|
||||
|
||||
@@ -75,6 +75,10 @@ async fn test_stock_system_msg_add_member_by_me() {
|
||||
let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await,
|
||||
"You added member Alice."
|
||||
@@ -87,6 +91,10 @@ async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await,
|
||||
"You added member Alice."
|
||||
|
||||
@@ -33,7 +33,6 @@ use crate::context::Context;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{self, DcKey, DcSecretKey, self_fingerprint};
|
||||
use crate::log::warn;
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::message::{Message, MessageState, MsgId, update_msg_state};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::pgp::KeyPair;
|
||||
@@ -201,18 +200,6 @@ impl TestContextManager {
|
||||
"{} changes her self address and reconfigures",
|
||||
test_context.name()
|
||||
));
|
||||
|
||||
// Insert a transport for the new address.
|
||||
test_context.sql
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||
(
|
||||
new_addr,
|
||||
serde_json::to_string(&EnteredLoginParam::default()).unwrap(),
|
||||
format!(r#"{{"addr":"{new_addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
||||
),
|
||||
).await.unwrap();
|
||||
|
||||
test_context.set_primary_self_addr(new_addr).await.unwrap();
|
||||
// ensure_secret_key_exists() is called during configure
|
||||
crate::e2ee::ensure_secret_key_exists(test_context)
|
||||
|
||||
+1
-4
@@ -323,10 +323,7 @@ impl ConfiguredLoginParam {
|
||||
.unwrap_or_default();
|
||||
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
|
||||
|
||||
let provider = context
|
||||
.get_config(Config::ConfiguredProvider)
|
||||
.await?
|
||||
.and_then(|cfg| get_provider_by_id(&cfg));
|
||||
let provider = context.get_configured_provider().await?;
|
||||
|
||||
let imap;
|
||||
let smtp;
|
||||
|
||||
@@ -1289,7 +1289,7 @@ async fn test_get_webxdc_info() -> Result<()> {
|
||||
let info = instance.get_webxdc_info(&t).await?;
|
||||
assert_eq!(info.name, "minimal.xdc");
|
||||
assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
|
||||
assert_eq!(info.send_update_interval, 1000);
|
||||
assert_eq!(info.send_update_interval, 10000);
|
||||
assert_eq!(info.send_update_max_size, RECOMMENDED_FILE_SIZE as usize);
|
||||
|
||||
let mut instance = create_webxdc_instance(
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wU4DAAAAAAAAAAASAQdA4GtzIk21tMpcktf+k47Gn8iSt8JMJgu/hEf7wJLUGgsg
|
||||
/dQ8eBSe4/PoEoNQ2mHnHWSdPH4zvS142R86iiyGe3jBwEwDAAAAAAAAAAABB/9l
|
||||
dteYjCvGpJxjgm8dGcb3lY9jVj76HLu72KWLZy2p/Ch/mxGW2GtgWNipFA2plCLQ
|
||||
Dzutak20lbYXkBL+zEnioHc5aAixruQQa2yZSRqePxPmkC/eTiedNeVJCOcrJBbj
|
||||
k8kQ+CPXLxx6mWS28s0tQkbpH9oJ2Esy0J6FNl0DMTWG50FWuo+Mh/gQ7Su3S1Il
|
||||
MYa7n58LhMjNGwD0ckp+Ig1XPNnnyauA+0kU/yDy7aA9NH4CtdqeobitEnn2aK4c
|
||||
sge0008GzZ/p1UWAyIVCqXuB1cNQY0UMVLohV3v343Xf9vS6ZQ8Bi8L+DiS+WpSt
|
||||
2fYGJ7Iqq6joatv6wPgS0j8BTbOcCygy/V1NvwzZpXnOawiGYJaCFU33vr2p1SrK
|
||||
YB5YCwFI4499aXUGhokyAW0xy6+N7o2Ps1qS4L3cqu0=
|
||||
=Ytf8
|
||||
-----END PGP MESSAGE-----
|
||||
Reference in New Issue
Block a user