mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
Compare commits
2 Commits
v2.29.0
...
hoc/timest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c64e4c7f8c | ||
|
|
d7ada8affa |
54
.github/workflows/ci.yml
vendored
54
.github/workflows/ci.yml
vendored
@@ -23,13 +23,12 @@ env:
|
||||
RUST_VERSION: 1.91.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.88.0
|
||||
MSRV: 1.85.0
|
||||
|
||||
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:
|
||||
|
||||
175
.github/workflows/deltachat-rpc-server.yml
vendored
175
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -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
|
||||
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@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 }}
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
167
CHANGELOG.md
167
CHANGELOG.md
@@ -1,169 +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
|
||||
|
||||
- New API `get_existing_msg_ids()` to check if the messages with given IDs exist.
|
||||
- Add API to get storage usage information. (JSON-RPC method: `get_storage_usage_report_string`) ([#7486](https://github.com/chatmail/core/pull/7486)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Experimentaly allow adding second transport.
|
||||
There is no synchronization yet, so UIs should not allow the user to change the address manually and only expose the ability to add transports if `bcc_self` is disabled.
|
||||
- Default `bcc_self` to 0 for all new accounts.
|
||||
- Rephrase "Establishing end-to-end encryption" -> "Establishing connection".
|
||||
- Stock string for joining a channel ([#7480](https://github.com/chatmail/core/pull/7480)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Limit the range of `Date` to up to 6 days in the past.
|
||||
- `ContactId::set_name_ex()`: Emit ContactsChanged when transaction is completed.
|
||||
- Set SQLite busy timeout to 1 minute on iOS.
|
||||
- Sort system messages to the bottom of the chat.
|
||||
- Assign outgoing self-sent unencrypted messages to ad-hoc groups with only SELF ([#7409](https://github.com/chatmail/core/pull/7409)).
|
||||
- Add missing stock strings.
|
||||
- Look up or create ad-hoc group if there are duplicate addresses in "To".
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add missing RFC 9788, link 'Header Protection for Cryptographically Protected Email' as other RFC.
|
||||
- Remove unsupported RFC 3503 (`$MDNSent` flag) from the list of standards.
|
||||
- Mark database encryption support as deprecated ([#7403](https://github.com/chatmail/core/pull/7403)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Increase Minimum Supported Rust Version to 1.88.0.
|
||||
- Update rPGP from 0.17.0 to 0.18.0.
|
||||
- nix: Update `fenix` and use it for all Rust builds.
|
||||
|
||||
### CI
|
||||
|
||||
- Do not use --encoding option for rst-lint.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use `HashMap::extract_if()` stabilized in Rust 1.88.0.
|
||||
- Remove some easy to remove unwrap() calls.
|
||||
|
||||
### Tests
|
||||
|
||||
- Contact shalln't be verified by another having unknown verifier.
|
||||
|
||||
## [2.27.0] - 2025-11-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add APIs to stop background fetch.
|
||||
- [**breaking**]: rename JSON-RPC method accounts_background_fetch() into background_fetch()
|
||||
- rpc-client: Add APIs for background fetch.
|
||||
- rpc-client: Add Account.wait_for_msg().
|
||||
- Deprecate deletion timer string for '1 Minute'.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Implement RFC 9788 (Header Protection for Cryptographically Protected Email) ([#7130](https://github.com/chatmail/core/pull/7130)).
|
||||
- Tweak initial info-message for unencrypted chats ([#7427](https://github.com/chatmail/core/pull/7427)).
|
||||
- Add Contact::get_or_gen_color. Use it in CFFI and JSON-RPC to avoid gray self-color ([#7374](https://github.com/chatmail/core/pull/7374)).
|
||||
- [**breaking**] Withdraw broadcast invites. Add Qr::WithdrawJoinBroadcast and Qr::ReviveJoinBroadcast QR code types. ([#7439](https://github.com/chatmail/core/pull/7439)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set `get_max_smtp_rcpt_to` for chatmail to the actual limit of 1000 instead of unlimited. ([#7432](https://github.com/chatmail/core/pull/7432)).
|
||||
- Always set bcc_self on backup import/export.
|
||||
- Escape connectivity HTML.
|
||||
- Send webm as file, it is not supported by all UI.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Exclude CONTRIBUTING.md from the source files.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use wait_for_incoming_msg() in more tests.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix flaky test_send_receive_locations.
|
||||
- Port folder-related CFFI tests to JSON-RPC.
|
||||
- HP-Outer headers are added to messages with standard Header Protection ([#7130](https://github.com/chatmail/core/pull/7130)).
|
||||
- rpc-client: Test_qr_securejoin_broadcast: Wait for incoming message before getting chatlist ([#7442](https://github.com/chatmail/core/pull/7442)).
|
||||
- Add pytest fixture for account manager.
|
||||
- Test background_fetch() and stop_background_fetch().
|
||||
|
||||
## [2.26.0] - 2025-11-11
|
||||
|
||||
### API-Changes
|
||||
@@ -7307,6 +7143,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.24.0]: https://github.com/chatmail/core/compare/v2.23.0..v2.24.0
|
||||
[2.25.0]: https://github.com/chatmail/core/compare/v2.24.0..v2.25.0
|
||||
[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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Contributing to chatmail core
|
||||
# Contributing to Delta Chat
|
||||
|
||||
## Bug reports
|
||||
|
||||
|
||||
366
Cargo.lock
generated
366
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.29.0"
|
||||
version = "2.26.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.85"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -78,7 +78,7 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.18.0", default-features = false }
|
||||
pgp = { version = "0.17.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
@@ -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.26.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -247,7 +247,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
// create/open/config/information
|
||||
|
||||
/**
|
||||
* Create a new context object and try to open it. If
|
||||
* Create a new context object and try to open it without passphrase. If
|
||||
* database is encrypted, the result is the same as using
|
||||
* dc_context_new_closed() and the database should be opened with
|
||||
* dc_context_open() before using.
|
||||
@@ -283,13 +283,8 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
|
||||
|
||||
|
||||
/**
|
||||
* Opens the database with the given passphrase.
|
||||
* NB: Nonempty passphrase (db encryption) is deprecated 2025-11:
|
||||
* - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
|
||||
* - Isolation from other apps is needed anyway.
|
||||
*
|
||||
* This can only be used on closed context, such as
|
||||
* created by dc_context_new_closed(). If the database
|
||||
* Opens the database with the given passphrase. This can only be used on
|
||||
* closed context, such as created by dc_context_new_closed(). If the database
|
||||
* is new, this operation sets the database passphrase. For existing databases
|
||||
* the passphrase should be the one used to encrypt the database the first
|
||||
* time.
|
||||
@@ -306,8 +301,6 @@ int dc_context_open (dc_context_t *context, const char*
|
||||
|
||||
/**
|
||||
* Changes the passphrase on the open database.
|
||||
* Deprecated 2025-11, see `dc_context_open()` for reasoning.
|
||||
*
|
||||
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
|
||||
* It is impossible to encrypt unencrypted database with this method and vice versa.
|
||||
*
|
||||
@@ -2585,10 +2578,8 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_ERROR 400 // text1=error string
|
||||
#define DC_QR_WITHDRAW_VERIFYCONTACT 500
|
||||
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
|
||||
#define DC_QR_WITHDRAW_JOINBROADCAST 504 // text1=broadcast name
|
||||
#define DC_QR_REVIVE_VERIFYCONTACT 510
|
||||
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
|
||||
#define DC_QR_REVIVE_JOINBROADCAST 514 // text1=broadcast name
|
||||
#define DC_QR_LOGIN 520 // text1=email_address
|
||||
|
||||
/**
|
||||
@@ -3305,30 +3296,12 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @param timeout The timeout in seconds
|
||||
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
|
||||
*/
|
||||
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||
|
||||
|
||||
/**
|
||||
* Stop ongoing background fetch.
|
||||
*
|
||||
* Calling this function allows to stop dc_accounts_background_fetch() early.
|
||||
* dc_accounts_background_fetch() will then return immediately
|
||||
* and emit DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE unless
|
||||
* if it has failed and returned 0.
|
||||
*
|
||||
* If there is no ongoing dc_accounts_background_fetch() call,
|
||||
* calling this function does nothing.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
*/
|
||||
void dc_accounts_stop_background_fetch (dc_accounts_t *accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Sets device token for Apple Push Notification service.
|
||||
* Returns immediately.
|
||||
@@ -7215,6 +7188,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 +7389,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.
|
||||
@@ -7527,13 +7518,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "You set message deletion timer to 1 minute."
|
||||
///
|
||||
/// @deprecated 2025-11-14, this string is no longer needed
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU 142
|
||||
|
||||
/// "Message deletion timer is set to 1 minute by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// @deprecated 2025-11-14, this string is no longer needed
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
|
||||
|
||||
/// "You set message deletion timer to 1 hour."
|
||||
@@ -7659,6 +7651,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.
|
||||
@@ -7695,12 +7693,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_REACTED_BY 177
|
||||
|
||||
/// "Member %1$s removed."
|
||||
///
|
||||
/// `%1$s` will be replaced by name of the removed contact.
|
||||
#define DC_STR_REMOVE_MEMBER 178
|
||||
|
||||
/// "Establishing connection, please wait…"
|
||||
/// "Establishing guaranteed end-to-end encryption, please wait…"
|
||||
///
|
||||
/// Used as info message.
|
||||
#define DC_STR_SECUREJOIN_WAIT 190
|
||||
@@ -7744,22 +7737,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Subtitle for channel join qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced with the channel name.
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
|
||||
|
||||
/// "You joined the channel."
|
||||
#define DC_STR_MSG_YOU_JOINED_CHANNEL 202
|
||||
|
||||
/// "%1$s invited you to join this channel. Waiting for the device of %2$s to reply…"
|
||||
///
|
||||
/// Added as an info-message directly after scanning a QR code for joining a broadcast channel.
|
||||
///
|
||||
/// `%1$s` and `%2$s` will both be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_STARTED 203
|
||||
|
||||
/// "The attachment contains anonymous usage statistics, which help us improve Delta Chat. Thank you!"
|
||||
///
|
||||
/// Used as the message body for statistics sent out.
|
||||
#define DC_STR_STATS_MSG_BODY 210
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
|
||||
|
||||
/// "Proxy Enabled"
|
||||
///
|
||||
|
||||
@@ -5027,17 +5027,6 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_stop_background_fetch()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.read()).stop_background_fetch();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
accounts: *mut dc_accounts_t,
|
||||
|
||||
@@ -58,10 +58,8 @@ impl Lot {
|
||||
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::WithdrawJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::ReviveJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
||||
},
|
||||
Self::Error(err) => Some(Cow::Borrowed(err)),
|
||||
@@ -114,10 +112,8 @@ impl Lot {
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::WithdrawJoinBroadcast { .. } => LotState::QrWithdrawJoinBroadcast,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
Qr::ReviveJoinBroadcast { .. } => LotState::QrReviveJoinBroadcast,
|
||||
Qr::Login { .. } => LotState::QrLogin,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
@@ -142,11 +138,9 @@ impl Lot {
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::WithdrawVerifyGroup { .. } | Qr::WithdrawJoinBroadcast { .. } => {
|
||||
Default::default()
|
||||
}
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::ReviveVerifyGroup { .. } | Qr::ReviveJoinBroadcast { .. } => Default::default(),
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
Qr::Login { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
@@ -213,15 +207,11 @@ pub enum LotState {
|
||||
|
||||
/// text1=groupname
|
||||
QrWithdrawVerifyGroup = 502,
|
||||
/// text1=broadcast channel name
|
||||
QrWithdrawJoinBroadcast = 504,
|
||||
|
||||
QrReviveVerifyContact = 510,
|
||||
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
/// text1=groupname
|
||||
QrReviveJoinBroadcast = 514,
|
||||
|
||||
/// text1=email_address
|
||||
QrLogin = 520,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.29.0"
|
||||
version = "2.26.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -21,9 +21,9 @@ use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
use deltachat::message::{
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipts, markseen_msgs, Message,
|
||||
MessageState, MsgId, Viewtype,
|
||||
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
@@ -34,7 +34,6 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::storage_usage::get_storage_usage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
@@ -121,14 +120,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
|
||||
@@ -274,7 +273,7 @@ impl CommandApi {
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||
@@ -284,11 +283,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_background_fetch(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_background_fetch();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Methods that work on individual accounts
|
||||
// ---------------------------------------------
|
||||
@@ -367,13 +361,6 @@ impl CommandApi {
|
||||
ctx.get_info().await
|
||||
}
|
||||
|
||||
/// Get storage usage report as formatted string
|
||||
async fn get_storage_usage_report_string(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let storage_usage = get_storage_usage(&ctx).await?;
|
||||
Ok(storage_usage.to_string())
|
||||
}
|
||||
|
||||
/// Get the blob dir.
|
||||
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1303,24 +1290,6 @@ impl CommandApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Checks if the messages with given IDs exist.
|
||||
///
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_message_list_items(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -157,21 +157,6 @@ pub enum QrObject {
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to withdraw their own broadcast channel invite QR code.
|
||||
WithdrawJoinBroadcast {
|
||||
/// Broadcast name.
|
||||
name: String,
|
||||
/// ID, uniquely identifying this chat. Called grpid for historic reasons.
|
||||
grpid: String,
|
||||
/// Contact ID. Always `ContactId::SELF`.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own QR code.
|
||||
ReviveVerifyContact {
|
||||
/// Contact ID.
|
||||
@@ -198,21 +183,6 @@ pub enum QrObject {
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own broadcast channel invite QR code.
|
||||
ReviveJoinBroadcast {
|
||||
/// Broadcast name.
|
||||
name: String,
|
||||
/// Globally unique chat ID. Called grpid for historic reasons.
|
||||
grpid: String,
|
||||
/// Contact ID. Always `ContactId::SELF`.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// `dclogin:` scheme parameters.
|
||||
///
|
||||
/// Ask the user if they want to login with the email address.
|
||||
@@ -336,25 +306,6 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::WithdrawJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
@@ -389,25 +340,6 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::Login { address, .. } => QrObject::Login { address },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.29.0"
|
||||
"version": "2.26.0"
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ describe("online tests", function () {
|
||||
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
||||
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
||||
await dc.rpc.configure(accountId1);
|
||||
await waitForEvent(dc, "ImapInboxIdle", accountId1);
|
||||
|
||||
accountId2 = await dc.rpc.addAccount();
|
||||
await dc.rpc.batchSetConfig(accountId2, {
|
||||
@@ -72,7 +71,6 @@ describe("online tests", function () {
|
||||
mail_pw: account2.password,
|
||||
});
|
||||
await dc.rpc.configure(accountId2);
|
||||
await waitForEvent(dc, "ImapInboxIdle", accountId2);
|
||||
accountsConfigured = true;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.29.0"
|
||||
version = "2.26.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.26.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 = [
|
||||
|
||||
@@ -130,10 +130,6 @@ class Account:
|
||||
"""Add a new transport using a QR code."""
|
||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||
|
||||
def delete_transport(self, addr: str):
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from ._utils import AttrDict
|
||||
from .account import Account
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -39,15 +39,6 @@ class DeltaChat:
|
||||
"""Stop the I/O of all accounts."""
|
||||
self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
@futuremethod
|
||||
def background_fetch(self, timeout_in_seconds: int) -> None:
|
||||
"""Run background fetch for all accounts."""
|
||||
yield self.rpc.background_fetch.future(timeout_in_seconds)
|
||||
|
||||
def stop_background_fetch(self) -> None:
|
||||
"""Stop ongoing background fetch."""
|
||||
self.rpc.stop_background_fetch()
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network conditions might have changed."""
|
||||
self.rpc.maybe_network()
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -40,17 +40,12 @@ class ACFactory:
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
return f"{username}@{domain}", f"{username}${username}"
|
||||
|
||||
def get_account_qr(self):
|
||||
"""Return "dcaccount:" QR code for testing chatmail relay."""
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
return f"dcaccount:{domain}"
|
||||
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
"""Create a new configured account."""
|
||||
account = self.get_unconfigured_account()
|
||||
qr = self.get_account_qr()
|
||||
yield account.add_transport_from_qr.future(qr)
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
|
||||
|
||||
assert account.is_configured()
|
||||
return account
|
||||
@@ -82,7 +77,6 @@ class ACFactory:
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for transport in transports:
|
||||
ac_clone.add_or_update_transport(transport)
|
||||
ac_clone.bring_online()
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
@@ -141,15 +135,9 @@ def rpc(tmp_path) -> AsyncGenerator:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dc(rpc) -> DeltaChat:
|
||||
"""Return account manager."""
|
||||
return DeltaChat(rpc)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(dc) -> AsyncGenerator:
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
"""Return account factory fixture."""
|
||||
return ACFactory(dc)
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Empty, Queue
|
||||
from threading import Thread
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Iterator, Optional
|
||||
|
||||
|
||||
@@ -17,6 +17,25 @@ class JsonRpcError(Exception):
|
||||
"""JSON-RPC error."""
|
||||
|
||||
|
||||
class RpcFuture:
|
||||
"""RPC future waiting for RPC call result."""
|
||||
|
||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||
self.rpc = rpc
|
||||
self.request_id = request_id
|
||||
self.event = event
|
||||
|
||||
def __call__(self):
|
||||
"""Wait for the future to return the result."""
|
||||
self.event.wait()
|
||||
response = self.rpc.request_results.pop(self.request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
|
||||
class RpcMethod:
|
||||
"""RPC method."""
|
||||
|
||||
@@ -38,23 +57,17 @@ class RpcMethod:
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
self.rpc.request_results[request_id] = queue = Queue()
|
||||
event = Event()
|
||||
self.rpc.request_events[request_id] = event
|
||||
self.rpc.request_queue.put(request)
|
||||
|
||||
def rpc_future():
|
||||
"""Wait for the request to receive a result."""
|
||||
response = queue.get()
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
return response.get("result", None)
|
||||
|
||||
return rpc_future
|
||||
return RpcFuture(self.rpc, request_id, event)
|
||||
|
||||
|
||||
class Rpc:
|
||||
"""RPC client."""
|
||||
|
||||
def __init__(self, accounts_dir: Optional[str] = None, rpc_server_path="deltachat-rpc-server", **kwargs):
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""Initialize RPC client.
|
||||
|
||||
The given arguments will be passed to subprocess.Popen().
|
||||
@@ -66,12 +79,13 @@ class Rpc:
|
||||
}
|
||||
|
||||
self._kwargs = kwargs
|
||||
self.rpc_server_path = rpc_server_path
|
||||
self.process: subprocess.Popen
|
||||
self.id_iterator: Iterator[int]
|
||||
self.event_queues: dict[int, Queue]
|
||||
# Map from request ID to a Queue which provides a single result
|
||||
self.request_results: dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: dict[int, Any]
|
||||
self.request_queue: Queue[Any]
|
||||
self.closing: bool
|
||||
self.reader_thread: Thread
|
||||
@@ -80,18 +94,27 @@ class Rpc:
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start RPC server subprocess."""
|
||||
popen_kwargs = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE}
|
||||
if sys.version_info >= (3, 11):
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
popen_kwargs["process_group"] = 0
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
process_group=0,
|
||||
**self._kwargs,
|
||||
)
|
||||
else:
|
||||
# `process_group` is not supported before Python 3.11.
|
||||
popen_kwargs["preexec_fn"] = os.setpgrp # noqa: PLW1509
|
||||
|
||||
popen_kwargs.update(self._kwargs)
|
||||
self.process = subprocess.Popen(self.rpc_server_path, **popen_kwargs)
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# `process_group` is not supported before Python 3.11.
|
||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id_iterator = itertools.count(start=1)
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.request_results = {}
|
||||
self.request_queue = Queue()
|
||||
self.closing = False
|
||||
@@ -126,7 +149,9 @@ class Rpc:
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
response_id = response["id"]
|
||||
self.request_results.pop(response_id).put(response)
|
||||
event = self.request_events.pop(response_id)
|
||||
self.request_results[response_id] = response
|
||||
event.set()
|
||||
else:
|
||||
logging.warning("Got a response without ID: %s", response)
|
||||
except Exception:
|
||||
|
||||
@@ -143,7 +143,7 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
|
||||
# Wait until new folder is created and UIDVALIDITY is updated.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.INFO and "transport 1: UID validity for folder DeltaChat changed from " in event.msg:
|
||||
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
|
||||
break
|
||||
|
||||
ac2 = acfactory.get_online_account()
|
||||
|
||||
@@ -4,41 +4,6 @@ from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import MessageState
|
||||
|
||||
|
||||
def test_bcc_self_delete_server_after_defaults(acfactory):
|
||||
"""Test default values for bcc_self and delete_server_after."""
|
||||
ac = acfactory.get_online_account()
|
||||
|
||||
# Initially after getting online
|
||||
# the setting bcc_self is set to 0 because there is only one device
|
||||
# and delete_server_after is "1", meaning immediate deletion.
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Setup a second device.
|
||||
ac_clone = ac.clone()
|
||||
ac_clone.bring_online()
|
||||
|
||||
# Second device setup
|
||||
# enables bcc_self and changes default delete_server_after.
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
assert ac_clone.get_config("bcc_self") == "1"
|
||||
assert ac_clone.get_config("delete_server_after") == "0"
|
||||
|
||||
# Manually disabling bcc_self
|
||||
# also restores the default for delete_server_after.
|
||||
ac.set_config("bcc_self", "0")
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Cloning the account again enables bcc_self
|
||||
# even though it was manually disabled.
|
||||
ac_clone = ac.clone()
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_add_second_address(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
# When the first transport is created,
|
||||
# mvbox_move and only_fetch_mvbox should be disabled.
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("only_fetch_mvbox") == "0"
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 3
|
||||
|
||||
first_addr = account.list_transports()[0]["addr"]
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
|
||||
# Cannot delete the first address.
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.delete_transport(first_addr)
|
||||
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
# Enabling mvbox_move or only_fetch_mvbox
|
||||
# is not allowed when multi-transport is enabled.
|
||||
for option in ["mvbox_move", "only_fetch_mvbox"]:
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.set_config(option, "1")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
|
||||
def test_no_second_transport_with_mvbox(acfactory, key) -> None:
|
||||
"""Test that second transport cannot be configured if mvbox is used."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("only_fetch_mvbox") == "0"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config(key, "1")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_no_second_transport_without_classic_emails(acfactory) -> None:
|
||||
"""Test that second transport cannot be configured if classic emails are not fetched."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_change_address(acfactory) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
msg1 = bob.wait_for_incoming_msg().get_snapshot()
|
||||
sender_addr1 = msg1.sender.get_snapshot().address
|
||||
|
||||
alice.stop_io()
|
||||
old_alice_addr = alice.get_config("configured_addr")
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr in alice_vcard
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
with pytest.raises(JsonRpcError):
|
||||
# Cannot use the address that is not
|
||||
# configured for any transport.
|
||||
alice.set_config("configured_addr", bob_addr)
|
||||
|
||||
# Load old address so it is cached.
|
||||
assert alice.get_config("configured_addr") == old_alice_addr
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
# Make sure that setting `configured_addr` invalidated the cache.
|
||||
assert alice.get_config("configured_addr") == new_alice_addr
|
||||
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr not in alice_vcard
|
||||
assert new_alice_addr in alice_vcard
|
||||
with pytest.raises(JsonRpcError):
|
||||
alice.delete_transport(new_alice_addr)
|
||||
alice.start_io()
|
||||
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
|
||||
msg2 = bob.wait_for_incoming_msg().get_snapshot()
|
||||
sender_addr2 = msg2.sender.get_snapshot().address
|
||||
|
||||
assert msg1.sender == msg2.sender
|
||||
assert sender_addr1 != sender_addr2
|
||||
assert sender_addr1 == old_alice_addr
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
|
||||
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
|
||||
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
|
||||
Disabling mvbox_move is required to be able to setup a second transport.
|
||||
"""
|
||||
account = acfactory.get_unconfigured_account()
|
||||
|
||||
account.set_config("fix_is_chatmail", "1")
|
||||
account.set_config("is_chatmail", is_chatmail)
|
||||
|
||||
# The default value when the setting is unset is "1".
|
||||
# This is not changed for compatibility with old databases
|
||||
# imported from backups.
|
||||
assert account.get_config("mvbox_move") == "1"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
# Once the first transport is set up,
|
||||
# mvbox_move is disabled.
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("is_chatmail") == is_chatmail
|
||||
|
||||
|
||||
def test_reconfigure_transport(acfactory) -> None:
|
||||
"""Test that reconfiguring the transport works
|
||||
even if settings not supported for multi-transport
|
||||
like mvbox_move are enabled."""
|
||||
account = acfactory.get_online_account()
|
||||
account.set_config("mvbox_move", "1")
|
||||
|
||||
[transport] = account.list_transports()
|
||||
account.add_or_update_transport(transport)
|
||||
|
||||
# Reconfiguring the transport should not reset
|
||||
# the settings as if when configuring the first transport.
|
||||
assert account.get_config("mvbox_move") == "1"
|
||||
@@ -1,20 +0,0 @@
|
||||
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"])
|
||||
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server"))
|
||||
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.20.0"
|
||||
@@ -140,15 +140,15 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||
return chat
|
||||
|
||||
def wait_for_broadcast_messages(ac):
|
||||
snapshot1 = ac.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot1.text == "You joined the channel."
|
||||
|
||||
snapshot2 = ac.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot2.text == "Hello everyone!"
|
||||
|
||||
chat = get_broadcast(ac)
|
||||
assert snapshot1.chat_id == chat.id
|
||||
assert snapshot2.chat_id == chat.id
|
||||
|
||||
snapshot = ac.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You joined the channel."
|
||||
assert snapshot.chat_id == chat.id
|
||||
|
||||
snapshot = ac.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello everyone!"
|
||||
assert snapshot.chat_id == chat.id
|
||||
|
||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||
# Check that the chat partner is verified.
|
||||
@@ -158,29 +158,29 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||
chat = get_broadcast(ac)
|
||||
chat_msgs = chat.get_messages()
|
||||
|
||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||
if please_wait_info_msg:
|
||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
|
||||
assert first_msg.is_info
|
||||
|
||||
encrypted_msg = chat_msgs[0].get_snapshot()
|
||||
assert encrypted_msg.text == "Messages are end-to-end encrypted."
|
||||
assert encrypted_msg.is_info
|
||||
|
||||
if please_wait_info_msg:
|
||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
member_added_msg = chat_msgs[1].get_snapshot()
|
||||
if inviter_side:
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
else:
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
hello_msg = chat_msgs.pop(0).get_snapshot()
|
||||
hello_msg = chat_msgs[2].get_snapshot()
|
||||
assert hello_msg.text == "Hello everyone!"
|
||||
assert not hello_msg.is_info
|
||||
assert hello_msg.show_padlock
|
||||
assert hello_msg.error is None
|
||||
|
||||
assert len(chat_msgs) == 0
|
||||
assert len(chat_msgs) == 3
|
||||
|
||||
chat_snapshot = chat.get_full_snapshot()
|
||||
assert chat_snapshot.is_encrypted
|
||||
|
||||
@@ -467,7 +467,7 @@ def test_bot(acfactory) -> None:
|
||||
|
||||
|
||||
def test_wait_next_messages(acfactory) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
addr, password = acfactory.get_credentials()
|
||||
@@ -475,7 +475,6 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
bot.set_config("bot", "1")
|
||||
bot.add_or_update_transport({"addr": addr, "password": password})
|
||||
assert bot.is_configured()
|
||||
bot.bring_online()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not bot.wait_next_messages()
|
||||
@@ -661,6 +660,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 +677,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):
|
||||
@@ -858,15 +867,15 @@ def test_leave_broadcast(acfactory, all_devices_online):
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
chat_msgs = chat.get_messages()
|
||||
|
||||
if please_wait_info_msg:
|
||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
|
||||
assert first_msg.is_info
|
||||
|
||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert encrypted_msg.text == "Messages are end-to-end encrypted."
|
||||
assert encrypted_msg.is_info
|
||||
|
||||
if please_wait_info_msg:
|
||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
if inviter_side:
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
@@ -956,59 +965,3 @@ def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
ev = ac1.wait_for_event(EventType.MSG_READ)
|
||||
assert ev.chat_id == chat1.id
|
||||
assert ev.msg_id == sent_msg.id
|
||||
|
||||
|
||||
def test_background_fetch(acfactory, dc):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.stop_io()
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
ac2_chat.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
dc.background_fetch(300)
|
||||
messages = ac1_chat.get_messages()
|
||||
snapshot = messages[-1].get_snapshot()
|
||||
if snapshot.text == "Hello!":
|
||||
break
|
||||
|
||||
# Stopping background fetch immediately after starting
|
||||
# does not result in any errors.
|
||||
background_fetch_future = dc.background_fetch.future(300)
|
||||
dc.stop_background_fetch()
|
||||
background_fetch_future()
|
||||
|
||||
# Starting background fetch with zero timeout is ok,
|
||||
# it should terminate immediately.
|
||||
dc.background_fetch(0)
|
||||
|
||||
# Background fetch can still be used to send and receive messages.
|
||||
ac2_chat.send_text("Hello again!")
|
||||
|
||||
while True:
|
||||
dc.background_fetch(300)
|
||||
messages = ac1_chat.get_messages()
|
||||
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.26.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.26.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" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -47,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1763361733,
|
||||
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=",
|
||||
"lastModified": 1747291057,
|
||||
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193",
|
||||
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -147,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1762977756,
|
||||
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -202,11 +202,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1762860488,
|
||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
||||
"lastModified": 1746889290,
|
||||
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
||||
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
24
flake.nix
24
flake.nix
@@ -1,5 +1,5 @@
|
||||
{
|
||||
description = "Chatmail core";
|
||||
description = "Delta Chat core";
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
@@ -14,15 +14,7 @@
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs.stdenv) isDarwin;
|
||||
fenixPkgs = fenix.packages.${system};
|
||||
fenixToolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.stable.rust-std
|
||||
];
|
||||
naersk' = pkgs.callPackage naersk {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
};
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
@@ -42,6 +34,7 @@
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
./CMakeLists.txt
|
||||
./CONTRIBUTING.md
|
||||
./deltachat_derive
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
@@ -478,12 +471,6 @@
|
||||
};
|
||||
|
||||
libdeltachat =
|
||||
let
|
||||
rustPlatform = (pkgs.makeRustPlatform {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
});
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
version = manifest.version;
|
||||
@@ -493,9 +480,8 @@
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
rustPlatform.cargoSetupHook
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
pkgs.rustPlatform.cargoSetupHook
|
||||
pkgs.cargo
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
|
||||
@@ -14,7 +14,6 @@ def datadir():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.skip("The test is flaky in CI and crashes the interpreter as of 2025-11-12")
|
||||
def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("creating one echo_and_quit bot")
|
||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||
|
||||
@@ -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.26.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",
|
||||
]
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestOfflineAccountBasic:
|
||||
d = ac1.get_info()
|
||||
assert d["arch"]
|
||||
assert d["number_of_chats"] == "0"
|
||||
assert d["bcc_self"] == "0"
|
||||
assert d["bcc_self"] == "1"
|
||||
|
||||
def test_is_not_configured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -69,7 +69,7 @@ class TestOfflineAccountBasic:
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
@@ -46,7 +46,7 @@ deps =
|
||||
commands =
|
||||
ruff format --diff setup.py src/deltachat examples/ tests/
|
||||
ruff check src/deltachat tests/ examples/
|
||||
rst-lint README.rst
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
deps =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-12-01
|
||||
2025-11-11
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use futures::FutureExt as _;
|
||||
use futures_lite::FutureExt as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -45,13 +41,6 @@ pub struct Accounts {
|
||||
|
||||
/// Push notification subscriber shared between accounts.
|
||||
push_subscriber: PushSubscriber,
|
||||
|
||||
/// Channel sender to cancel ongoing background_fetch().
|
||||
///
|
||||
/// If background_fetch() is not running, this is `None`.
|
||||
/// New background_fetch() should not be started if this
|
||||
/// contains `Some`.
|
||||
background_fetch_interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@@ -107,7 +96,6 @@ impl Accounts {
|
||||
events,
|
||||
stockstrings,
|
||||
push_subscriber,
|
||||
background_fetch_interrupt_sender: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -364,11 +352,6 @@ impl Accounts {
|
||||
///
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
///
|
||||
/// This function is cancellation-safe.
|
||||
/// It is intended to be cancellable,
|
||||
/// either because of the timeout or because background
|
||||
/// fetch was explicitly cancelled.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
let n_accounts = accounts.len();
|
||||
events.emit(Event {
|
||||
@@ -395,33 +378,14 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
///
|
||||
/// Runs `background_fetch` until it finishes
|
||||
/// or until the timeout.
|
||||
///
|
||||
/// Produces `AccountsBackgroundFetchDone` event in every case
|
||||
/// and clears [`Self::background_fetch_interrupt_sender`]
|
||||
/// so a new background fetch can be started.
|
||||
///
|
||||
/// This function is not cancellation-safe.
|
||||
/// Cancelling it before it returns may result
|
||||
/// in not being able to run any new background fetch
|
||||
/// if interrupt sender was not cleared.
|
||||
async fn background_fetch_with_timeout(
|
||||
accounts: Vec<Context>,
|
||||
events: Events,
|
||||
timeout: std::time::Duration,
|
||||
interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
|
||||
interrupt_receiver: Option<Receiver<()>>,
|
||||
) {
|
||||
let Some(interrupt_receiver) = interrupt_receiver else {
|
||||
// Nothing to do if we got no interrupt receiver.
|
||||
return;
|
||||
};
|
||||
if let Err(_err) = tokio::time::timeout(
|
||||
timeout,
|
||||
Self::background_fetch_no_timeout(accounts, events.clone())
|
||||
.race(interrupt_receiver.recv().map(|_| ())),
|
||||
Self::background_fetch_no_timeout(accounts, events.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -434,16 +398,10 @@ impl Accounts {
|
||||
id: 0,
|
||||
typ: EventType::AccountsBackgroundFetchDone,
|
||||
});
|
||||
(*interrupt_sender.lock()) = None;
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// Ongoing background fetch can also be cancelled manually
|
||||
/// by calling `stop_background_fetch()`, in which case it will
|
||||
/// return immediately even before the timeout expiration
|
||||
/// or finishing fetching.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
@@ -456,39 +414,7 @@ impl Accounts {
|
||||
) -> impl Future<Output = ()> + use<> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
let events = self.events.clone();
|
||||
let (sender, receiver) = async_channel::bounded(1);
|
||||
let receiver = {
|
||||
let mut lock = self.background_fetch_interrupt_sender.lock();
|
||||
if (*lock).is_some() {
|
||||
// Another background_fetch() is already running,
|
||||
// return immeidately.
|
||||
None
|
||||
} else {
|
||||
*lock = Some(sender);
|
||||
Some(receiver)
|
||||
}
|
||||
};
|
||||
Self::background_fetch_with_timeout(
|
||||
accounts,
|
||||
events,
|
||||
timeout,
|
||||
self.background_fetch_interrupt_sender.clone(),
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
/// Interrupts ongoing background_fetch() call,
|
||||
/// making it return early.
|
||||
///
|
||||
/// This method allows to cancel background_fetch() early,
|
||||
/// e.g. on Android, when `Service.onTimeout` is called.
|
||||
///
|
||||
/// If there is no ongoing background_fetch(), does nothing.
|
||||
pub fn stop_background_fetch(&self) {
|
||||
let mut lock = self.background_fetch_interrupt_sender.lock();
|
||||
if let Some(sender) = lock.take() {
|
||||
sender.try_send(()).ok();
|
||||
}
|
||||
Self::background_fetch_with_timeout(accounts, events, timeout)
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
@@ -678,12 +604,13 @@ impl Config {
|
||||
// Convert them to relative paths.
|
||||
let mut modified = false;
|
||||
for account in &mut config.inner.accounts {
|
||||
if account.dir.is_absolute()
|
||||
&& let Some(old_path_parent) = account.dir.parent()
|
||||
&& let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
|
||||
{
|
||||
account.dir = new_path.to_path_buf();
|
||||
modified = true;
|
||||
if account.dir.is_absolute() {
|
||||
if let Some(old_path_parent) = account.dir.parent() {
|
||||
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
|
||||
account.dir = new_path.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if modified && writable {
|
||||
|
||||
@@ -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;
|
||||
|
||||
216
src/chat.rs
216
src/chat.rs
@@ -301,7 +301,7 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
if chat.is_encrypted(context).await? {
|
||||
chat_id.add_e2ee_notice(context, timestamp).await?;
|
||||
chat_id.add_encrypted_msg(context, timestamp).await?;
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -462,15 +462,19 @@ impl ChatId {
|
||||
}
|
||||
|
||||
/// Adds message "Messages are end-to-end encrypted".
|
||||
pub(crate) async fn add_e2ee_notice(self, context: &Context, timestamp: i64) -> Result<()> {
|
||||
pub(crate) async fn add_encrypted_msg(
|
||||
self,
|
||||
context: &Context,
|
||||
timestamp_sort: i64,
|
||||
) -> Result<()> {
|
||||
let text = stock_str::messages_e2e_encrypted(context).await;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
SystemMessage::ChatE2ee,
|
||||
Some(timestamp),
|
||||
timestamp,
|
||||
timestamp_sort,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -736,15 +740,16 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if msg.viewtype == Viewtype::File
|
||||
&& let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
|
||||
if msg.viewtype == Viewtype::File {
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
|
||||
// We do not do an automatic conversion to other viewtypes here so that
|
||||
// users can send images as "files" to preserve the original quality
|
||||
// (usually we compress images). The remaining conversions are done by
|
||||
// `prepare_msg_blob()` later.
|
||||
.filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
}
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
let blob = msg
|
||||
@@ -762,13 +767,13 @@ impl ChatId {
|
||||
msg.chat_id = self;
|
||||
|
||||
// if possible, replace existing draft and keep id
|
||||
if !msg.id.is_special()
|
||||
&& let Some(old_draft) = self.get_draft(context).await?
|
||||
&& old_draft.id == msg.id
|
||||
&& old_draft.chat_id == self
|
||||
&& old_draft.state == MessageState::OutDraft
|
||||
{
|
||||
let affected_rows = context
|
||||
if !msg.id.is_special() {
|
||||
if let Some(old_draft) = self.get_draft(context).await? {
|
||||
if old_draft.id == msg.id
|
||||
&& old_draft.chat_id == self
|
||||
&& old_draft.state == MessageState::OutDraft
|
||||
{
|
||||
let affected_rows = context
|
||||
.sql.execute(
|
||||
"UPDATE msgs
|
||||
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
|
||||
@@ -788,7 +793,9 @@ impl ChatId {
|
||||
msg.id,
|
||||
),
|
||||
).await?;
|
||||
return Ok(affected_rows > 0);
|
||||
return Ok(affected_rows > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let row_id = context
|
||||
@@ -986,11 +993,11 @@ impl ChatId {
|
||||
let mut res = Vec::new();
|
||||
let now = time();
|
||||
for (chat_id, metric) in chats_with_metrics {
|
||||
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await?
|
||||
&& now > chat_timestamp + 42 * 24 * 3600
|
||||
{
|
||||
// Chat was inactive for 42 days, skip.
|
||||
continue;
|
||||
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
|
||||
if now > chat_timestamp + 42 * 24 * 3600 {
|
||||
// Chat was inactive for 42 days, skip.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if metric < 0.1 {
|
||||
@@ -1245,10 +1252,10 @@ impl ChatId {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(last_msg_time) = last_msg_time
|
||||
&& last_msg_time > sort_timestamp
|
||||
{
|
||||
sort_timestamp = last_msg_time;
|
||||
if let Some(last_msg_time) = last_msg_time {
|
||||
if last_msg_time > sort_timestamp {
|
||||
sort_timestamp = last_msg_time;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sort_timestamp)
|
||||
@@ -1369,10 +1376,10 @@ impl Chat {
|
||||
let mut chat_name = "Err [Name not found]".to_owned();
|
||||
match get_chat_contacts(context, chat.id).await {
|
||||
Ok(contacts) => {
|
||||
if let Some(contact_id) = contacts.first()
|
||||
&& let Ok(contact) = Contact::get_by_id(context, *contact_id).await
|
||||
{
|
||||
contact.get_display_name().clone_into(&mut chat_name);
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
contact.get_display_name().clone_into(&mut chat_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1569,10 +1576,10 @@ impl Chat {
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = contacts.first()
|
||||
&& let Ok(contact) = Contact::get_by_id(context, *contact_id).await
|
||||
{
|
||||
color = contact.get_color();
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
color = contact.get_color();
|
||||
}
|
||||
}
|
||||
} else if !self.grpid.is_empty() {
|
||||
color = str_to_color(&self.grpid);
|
||||
@@ -1834,8 +1841,8 @@ impl Chat {
|
||||
}
|
||||
|
||||
// add independent location to database
|
||||
if msg.param.exists(Param::SetLatitude)
|
||||
&& let Ok(row_id) = context
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
if let Ok(row_id) = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO locations \
|
||||
@@ -1850,8 +1857,9 @@ impl Chat {
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
location_id = row_id;
|
||||
{
|
||||
location_id = row_id;
|
||||
}
|
||||
}
|
||||
|
||||
let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
|
||||
@@ -2421,7 +2429,7 @@ impl ChatIdBlocked {
|
||||
&& !chat.param.exists(Param::Devicetalk)
|
||||
&& !chat.param.exists(Param::Selftalk)
|
||||
{
|
||||
chat_id.add_e2ee_notice(context, smeared_time).await?;
|
||||
chat_id.add_encrypted_msg(context, smeared_time).await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -2489,18 +2497,18 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
}
|
||||
|
||||
if !msg.param.exists(Param::MimeType)
|
||||
&& let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg)
|
||||
{
|
||||
// If we unexpectedly didn't recognize the file as image, don't send it as such,
|
||||
// either the format is unsupported or the image is corrupted.
|
||||
let mime = match viewtype != Viewtype::Image
|
||||
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
|
||||
{
|
||||
true => mime,
|
||||
false => "application/octet-stream",
|
||||
};
|
||||
msg.param.set(Param::MimeType, mime);
|
||||
if !msg.param.exists(Param::MimeType) {
|
||||
if let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg) {
|
||||
// If we unexpectedly didn't recognize the file as image, don't send it as such,
|
||||
// either the format is unsupported or the image is corrupted.
|
||||
let mime = match viewtype != Viewtype::Image
|
||||
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
|
||||
{
|
||||
true => mime,
|
||||
false => "application/octet-stream",
|
||||
};
|
||||
msg.param.set(Param::MimeType, mime);
|
||||
}
|
||||
}
|
||||
|
||||
msg.try_calc_and_set_dimensions(context).await?;
|
||||
@@ -2684,15 +2692,15 @@ async fn prepare_send_msg(
|
||||
// This is meant as a last line of defence, the UI should check that before as well.
|
||||
// (We allow Chattype::Single in general for "Reply Privately";
|
||||
// checking for exact contact_id will produce false positives when ppl just left the group)
|
||||
if chat.typ != Chattype::Single
|
||||
&& !context.get_config_bool(Config::Bot).await?
|
||||
&& let Some(quoted_message) = msg.quoted_message(context).await?
|
||||
&& quoted_message.chat_id != chat_id
|
||||
{
|
||||
bail!(
|
||||
"Quote of message from {} cannot be sent to {chat_id}",
|
||||
quoted_message.chat_id
|
||||
);
|
||||
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
|
||||
if let Some(quoted_message) = msg.quoted_message(context).await? {
|
||||
if quoted_message.chat_id != chat_id {
|
||||
bail!(
|
||||
"Quote of message from {} cannot be sent to {chat_id}",
|
||||
quoted_message.chat_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check current MessageState for drafts (to keep msg_id) ...
|
||||
@@ -2822,15 +2830,16 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
|
||||
let now = smeared_time(context);
|
||||
|
||||
if rendered_msg.last_added_location_id.is_some()
|
||||
&& let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await
|
||||
{
|
||||
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
|
||||
if rendered_msg.last_added_location_id.is_some() {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar && let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await
|
||||
{
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
}
|
||||
}
|
||||
|
||||
if rendered_msg.is_encrypted {
|
||||
@@ -3448,7 +3457,7 @@ pub(crate) async fn create_group_ex(
|
||||
|
||||
if !grpid.is_empty() {
|
||||
// Add "Messages are end-to-end encrypted." message.
|
||||
chat_id.add_e2ee_notice(context, timestamp).await?;
|
||||
chat_id.add_encrypted_msg(context, timestamp).await?;
|
||||
}
|
||||
|
||||
if !context.get_config_bool(Config::Bot).await?
|
||||
@@ -3461,7 +3470,7 @@ pub(crate) async fn create_group_ex(
|
||||
// Add "Messages in this chat use classic email and are not encrypted." message.
|
||||
stock_str::chat_unencrypted_explanation(context).await
|
||||
};
|
||||
add_info_msg(context, chat_id, &text).await?;
|
||||
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
|
||||
}
|
||||
if let (true, true) = (sync.into(), !grpid.is_empty()) {
|
||||
let id = SyncId::Grpid(grpid);
|
||||
@@ -3529,7 +3538,7 @@ pub(crate) async fn create_out_broadcast_ex(
|
||||
Ok(chat_id)
|
||||
};
|
||||
let chat_id = context.sql.transaction(trans_fn).await?;
|
||||
chat_id.add_e2ee_notice(context, timestamp).await?;
|
||||
chat_id.add_encrypted_msg(context, timestamp).await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
@@ -3730,16 +3739,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(
|
||||
@@ -4480,11 +4483,11 @@ pub async fn add_device_msg_with_importance(
|
||||
let mut chat_id = ChatId::new(0);
|
||||
let mut msg_id = MsgId::new_unset();
|
||||
|
||||
if let Some(label) = label
|
||||
&& was_device_msg_ever_added(context, label).await?
|
||||
{
|
||||
info!(context, "Device-message {label} already added.");
|
||||
return Ok(msg_id);
|
||||
if let Some(label) = label {
|
||||
if was_device_msg_ever_added(context, label).await? {
|
||||
info!(context, "Device-message {label} already added.");
|
||||
return Ok(msg_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(msg) = msg {
|
||||
@@ -4496,10 +4499,10 @@ pub async fn add_device_msg_with_importance(
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
msg.timestamp_sort = timestamp_sent;
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
|
||||
&& msg.timestamp_sort <= last_msg_time
|
||||
{
|
||||
msg.timestamp_sort = last_msg_time + 1;
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
|
||||
if msg.timestamp_sort <= last_msg_time {
|
||||
msg.timestamp_sort = last_msg_time + 1;
|
||||
}
|
||||
}
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
let state = MessageState::InFresh;
|
||||
@@ -4618,11 +4621,9 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
chat_id: ChatId,
|
||||
text: &str,
|
||||
cmd: SystemMessage,
|
||||
// Timestamp where in the chat the message will be sorted.
|
||||
// If this is None, the message will be sorted to the bottom.
|
||||
timestamp_sort: Option<i64>,
|
||||
// Timestamp to show to the user
|
||||
timestamp_sent_rcvd: i64,
|
||||
timestamp_sort: i64,
|
||||
// Timestamp to show to the user (if this is None, `timestamp_sort` will be shown to the user)
|
||||
timestamp_sent_rcvd: Option<i64>,
|
||||
parent: Option<&Message>,
|
||||
from_id: Option<ContactId>,
|
||||
added_removed_id: Option<ContactId>,
|
||||
@@ -4638,22 +4639,6 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
let timestamp_sort = if let Some(ts) = timestamp_sort {
|
||||
ts
|
||||
} else {
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
chat_id
|
||||
.calc_sort_timestamp(
|
||||
context,
|
||||
smeared_time(context),
|
||||
sort_to_bottom,
|
||||
received,
|
||||
incoming,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,txt_normalized,rfc724_mid,ephemeral_timer,param,mime_in_reply_to)
|
||||
@@ -4663,8 +4648,8 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
from_id.unwrap_or(ContactId::INFO),
|
||||
ContactId::INFO,
|
||||
timestamp_sort,
|
||||
timestamp_sent_rcvd,
|
||||
timestamp_sent_rcvd,
|
||||
timestamp_sent_rcvd.unwrap_or(0),
|
||||
timestamp_sent_rcvd.unwrap_or(0),
|
||||
Viewtype::Text,
|
||||
MessageState::InNoticed,
|
||||
text,
|
||||
@@ -4684,14 +4669,19 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
}
|
||||
|
||||
/// Adds info message with a given text and `timestamp` to the chat.
|
||||
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: &str) -> Result<MsgId> {
|
||||
pub(crate) async fn add_info_msg(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
text: &str,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
text,
|
||||
SystemMessage::Unknown,
|
||||
timestamp,
|
||||
None,
|
||||
time(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -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;
|
||||
@@ -1229,7 +1238,7 @@ async fn test_unarchive_if_muted() -> Result<()> {
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
set_muted(&t, chat_id, MuteDuration::Forever).await?;
|
||||
send_text_msg(&t, chat_id, "out".to_string()).await?;
|
||||
add_info_msg(&t, chat_id, "info").await?;
|
||||
add_info_msg(&t, chat_id, "info", time()).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// finally, unarchive on sending to not muted chat
|
||||
@@ -1628,7 +1637,7 @@ async fn test_set_mute_duration() {
|
||||
async fn test_add_info_msg() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group(&t, "foo").await?;
|
||||
add_info_msg(&t, chat_id, "foo info").await?;
|
||||
add_info_msg(&t, chat_id, "foo info", time()).await?;
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_chat_id(), chat_id);
|
||||
@@ -1650,11 +1659,11 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
|
||||
chat_id,
|
||||
"foo bar info",
|
||||
SystemMessage::EphemeralTimerChanged,
|
||||
None,
|
||||
time(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2623,6 +2632,12 @@ async fn test_can_send_group() -> Result<()> {
|
||||
/// the recipients can't see the identity of their fellow recipients.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
fn contains(parsed: &MimeMessage, s: &str) -> bool {
|
||||
assert_eq!(parsed.decrypting_failed, false);
|
||||
let decoded_str = std::str::from_utf8(&parsed.decoded_data).unwrap();
|
||||
decoded_str.contains(s)
|
||||
}
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
@@ -2654,8 +2669,8 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
);
|
||||
let parsed = charlie.parse_msg(&auth_required).await;
|
||||
assert!(parsed.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
assert!(parsed.decoded_data_contains("charlie@example.net"));
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
assert!(contains(&parsed, "charlie@example.net"));
|
||||
assert_eq!(contains(&parsed, "bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&auth_required).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
@@ -2683,8 +2698,8 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
);
|
||||
let parsed = charlie.parse_msg(&member_added).await;
|
||||
assert!(parsed.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
assert!(parsed.decoded_data_contains("charlie@example.net"));
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
assert!(contains(&parsed, "charlie@example.net"));
|
||||
assert_eq!(contains(&parsed, "bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&member_added).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
@@ -2698,8 +2713,8 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
let hi_msg = alice.send_text(alice_broadcast_id, "hi").await;
|
||||
let parsed = charlie.parse_msg(&hi_msg).await;
|
||||
assert_eq!(parsed.header_exists(HeaderDef::AutocryptGossip), false);
|
||||
assert_eq!(parsed.decoded_data_contains("charlie@example.net"), false);
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
assert_eq!(contains(&parsed, "charlie@example.net"), false);
|
||||
assert_eq!(contains(&parsed, "bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&hi_msg).await;
|
||||
assert_eq!(parsed_by_bob.decrypting_failed, false);
|
||||
@@ -2715,8 +2730,8 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
"charlie@example.net alice@example.org"
|
||||
);
|
||||
let parsed = charlie.parse_msg(&member_removed).await;
|
||||
assert!(parsed.decoded_data_contains("charlie@example.net"));
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
assert!(contains(&parsed, "charlie@example.net"));
|
||||
assert_eq!(contains(&parsed, "bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&member_removed).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
@@ -3312,7 +3327,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;
|
||||
|
||||
@@ -4495,7 +4513,7 @@ async fn test_info_not_referenced() -> Result<()> {
|
||||
|
||||
let bob_received_message = tcm.send_recv_accept(alice, bob, "Hi!").await;
|
||||
let bob_chat_id = bob_received_message.chat_id;
|
||||
add_info_msg(bob, bob_chat_id, "Some info").await?;
|
||||
add_info_msg(bob, bob_chat_id, "Some info", create_smeared_timestamp(bob)).await?;
|
||||
|
||||
// Bob sends a message.
|
||||
// This message should reference Alice's "Hi!" message and not the info message.
|
||||
|
||||
@@ -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;
|
||||
@@ -144,11 +144,11 @@ pub enum Config {
|
||||
|
||||
/// Send BCC copy to self.
|
||||
///
|
||||
/// Should be enabled for multi-device setups.
|
||||
/// Should be enabled for multidevice setups.
|
||||
/// Default is 0 for chatmail accounts, 1 otherwise.
|
||||
///
|
||||
/// This is automatically enabled when importing/exporting a backup,
|
||||
/// setting up a second device, or receiving a sync message.
|
||||
#[strum(props(default = "0"))]
|
||||
BccSelf,
|
||||
|
||||
/// True if Message Delivery Notifications (read receipts) should
|
||||
@@ -438,19 +438,8 @@ pub enum Config {
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
|
||||
/// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`.
|
||||
/// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not
|
||||
/// using this still run unmodified code.
|
||||
TestHooks,
|
||||
|
||||
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
|
||||
FailOnReceivingFullMsg,
|
||||
|
||||
/// Enable composing emails with Header Protection as defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
|
||||
/// Protected Email".
|
||||
#[strum(props(default = "1"))]
|
||||
StdHeaderProtectionComposing,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -477,10 +466,7 @@ impl Config {
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
|
||||
)
|
||||
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +512,10 @@ impl Context {
|
||||
|
||||
// Default values
|
||||
let val = match key {
|
||||
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
|
||||
false => Some("1".to_string()),
|
||||
true => Some("0".to_string()),
|
||||
},
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
||||
Config::DeleteServerAfter => {
|
||||
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
|
||||
@@ -646,14 +636,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.
|
||||
@@ -715,16 +706,6 @@ impl Context {
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let n_transports = self.count_transports().await?;
|
||||
if n_transports > 1
|
||||
&& matches!(
|
||||
key,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails
|
||||
)
|
||||
{
|
||||
bail!("Cannot reconfigure {key} when multiple transports are configured");
|
||||
}
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self).await?,
|
||||
_ => Default::default(),
|
||||
@@ -810,11 +791,10 @@ impl Context {
|
||||
.await?;
|
||||
}
|
||||
Config::ConfiguredAddr => {
|
||||
let Some(addr) = value else {
|
||||
bail!("Cannot unset configured_addr");
|
||||
};
|
||||
|
||||
if !self.is_configured().await? {
|
||||
if self.is_configured().await? {
|
||||
bail!("Cannot change ConfiguredAddr");
|
||||
}
|
||||
if let Some(addr) = value {
|
||||
info!(
|
||||
self,
|
||||
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
|
||||
@@ -825,36 +805,6 @@ impl Context {
|
||||
.save_to_transports_table(self, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
}
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
// Clean up SMTP and IMAP APPEND queue.
|
||||
//
|
||||
// The messages in the queue have a different
|
||||
// From address so we cannot send them over
|
||||
// the new SMTP transport.
|
||||
transaction.execute("DELETE FROM smtp", ())?;
|
||||
transaction.execute("DELETE FROM imap_send", ())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
142
src/configure.rs
142
src/configure.rs
@@ -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) => {
|
||||
@@ -129,6 +130,12 @@ impl Context {
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
param.addr = addr_normalize(¶m.addr);
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) {
|
||||
let error_msg = "Changing your email address is not supported right now. Check back in a few months!";
|
||||
progress!(self, 0, Some(error_msg.to_string()));
|
||||
bail!(error_msg);
|
||||
}
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
|
||||
let res = self
|
||||
@@ -197,76 +204,23 @@ impl Context {
|
||||
Ok(transports)
|
||||
}
|
||||
|
||||
/// Returns the number of configured transports.
|
||||
pub async fn count_transports(&self) -> Result<usize> {
|
||||
self.sql.count("SELECT COUNT(*) FROM transports", ()).await
|
||||
}
|
||||
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
let primary_addr = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)?;
|
||||
|
||||
if primary_addr == addr {
|
||||
bail!("Cannot delete primary transport");
|
||||
}
|
||||
let transport_id = transaction.query_row(
|
||||
"DELETE FROM transports WHERE addr=? RETURNING id",
|
||||
(addr,),
|
||||
|row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
Ok(id)
|
||||
},
|
||||
)?;
|
||||
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
|
||||
transaction.execute(
|
||||
"DELETE FROM imap_sync WHERE transport_id=?",
|
||||
(transport_id,),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
#[expect(clippy::unused_async)]
|
||||
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
|
||||
bail!(
|
||||
"Adding and removing additional transports is not supported yet. Check back in a few months!"
|
||||
)
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if old_addr.is_some()
|
||||
&& !self
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(¶m.addr,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
|
||||
bail!("Cannot use multi-transport with mvbox_move enabled.");
|
||||
}
|
||||
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
|
||||
bail!("Cannot use multi-transport with only_fetch_mvbox enabled.");
|
||||
}
|
||||
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
|
||||
bail!("Cannot use multi-transport with disabled fetching of classic emails.");
|
||||
}
|
||||
}
|
||||
|
||||
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 +228,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 +258,21 @@ async fn on_configure_completed(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if let Some(old_addr) = old_addr {
|
||||
if !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(())
|
||||
}
|
||||
|
||||
@@ -534,29 +504,63 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
// Configure IMAP
|
||||
|
||||
let transport_id = 0;
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||
let mut imap = Imap::new(
|
||||
configured_param.imap.clone(),
|
||||
configured_param.imap_password.clone(),
|
||||
proxy_config,
|
||||
&configured_param.addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
r,
|
||||
);
|
||||
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);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
smtp_config_task.await??;
|
||||
smtp_config_task.await.unwrap()?;
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
if !ctx.is_configured().await? {
|
||||
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
|
||||
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
|
||||
let is_chatmail = match ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
false => {
|
||||
let is_chatmail = imap_session.is_chatmail();
|
||||
ctx.set_config(
|
||||
Config::IsChatmail,
|
||||
Some(match is_chatmail {
|
||||
false => "0",
|
||||
true => "1",
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
is_chatmail
|
||||
}
|
||||
true => ctx.get_config_bool(Config::IsChatmail).await?,
|
||||
};
|
||||
if is_chatmail {
|
||||
ctx.set_config(Config::MvboxMove, Some("0")).await?;
|
||||
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
|
||||
ctx.set_config(Config::ShowEmails, None).await?;
|
||||
}
|
||||
|
||||
let create_mvbox = !is_chatmail;
|
||||
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
|
||||
.await?;
|
||||
|
||||
let create = true;
|
||||
imap_session
|
||||
.select_with_uidvalidity(ctx, "INBOX", create)
|
||||
.await
|
||||
.context("could not read INBOX status")?;
|
||||
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
@@ -154,10 +154,10 @@ fn parse_xml_reader<B: BufRead>(
|
||||
if let Some(incoming_server) = parse_server(reader, event)? {
|
||||
incoming_servers.push(incoming_server);
|
||||
}
|
||||
} else if tag == "outgoingserver"
|
||||
&& let Some(outgoing_server) = parse_server(reader, event)?
|
||||
{
|
||||
outgoing_servers.push(outgoing_server);
|
||||
} else if tag == "outgoingserver" {
|
||||
if let Some(outgoing_server) = parse_server(reader, event)? {
|
||||
outgoing_servers.push(outgoing_server);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
|
||||
145
src/contact.rs
145
src/contact.rs
@@ -130,37 +130,35 @@ impl ContactId {
|
||||
Ok((addr, fingerprint))
|
||||
},
|
||||
)?;
|
||||
context.emit_event(EventType::ContactsChanged(Some(self)));
|
||||
Ok(Some((addr, fingerprint)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
if row.is_some() {
|
||||
context.emit_event(EventType::ContactsChanged(Some(self)));
|
||||
}
|
||||
|
||||
if sync.into()
|
||||
&& let Some((addr, fingerprint)) = row
|
||||
{
|
||||
if fingerprint.is_empty() {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
} else {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactFingerprint(fingerprint),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
if sync.into() {
|
||||
if let Some((addr, fingerprint)) = row {
|
||||
if fingerprint.is_empty() {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
} else {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactFingerprint(fingerprint),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -383,21 +381,25 @@ 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
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
|
||||
);
|
||||
if let Some(biography) = &contact.biography {
|
||||
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
@@ -1505,6 +1507,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
|
||||
@@ -1550,10 +1564,10 @@ impl Contact {
|
||||
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
|
||||
return Ok(Some(chat::get_unencrypted_icon(context).await?));
|
||||
}
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage)
|
||||
&& !image_rel.is_empty()
|
||||
{
|
||||
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1786,11 +1800,10 @@ WHERE type=? AND id IN (
|
||||
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking
|
||||
&& contact.origin == Origin::MailinglistAddress
|
||||
&& let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
@@ -1820,19 +1833,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 +1859,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 +1882,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());
|
||||
|
||||
@@ -46,7 +46,7 @@ use crate::{chatlist_events, stats};
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Creating a new database:
|
||||
/// Creating a new unencrypted database:
|
||||
///
|
||||
/// ```
|
||||
/// # let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -61,6 +61,24 @@ use crate::{chatlist_events, stats};
|
||||
/// drop(context);
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// To use an encrypted database provide a password. If the database does not yet exist it
|
||||
/// will be created:
|
||||
///
|
||||
/// ```
|
||||
/// # let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// # rt.block_on(async move {
|
||||
/// use deltachat::context::ContextBuilder;
|
||||
///
|
||||
/// let dir = tempfile::tempdir().unwrap();
|
||||
/// let context = ContextBuilder::new(dir.path().join("db"))
|
||||
/// .with_password("secret".into())
|
||||
/// .open()
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// drop(context);
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ContextBuilder {
|
||||
dbfile: PathBuf,
|
||||
@@ -132,13 +150,9 @@ impl ContextBuilder {
|
||||
}
|
||||
|
||||
/// Sets the password to unlock the database.
|
||||
/// Deprecated 2025-11:
|
||||
/// - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
|
||||
/// - Isolation from other apps is needed anyway.
|
||||
///
|
||||
/// If an encrypted database is used it must be opened with a password. Setting a
|
||||
/// password on a new database will enable encryption.
|
||||
#[deprecated(since = "TBD")]
|
||||
pub fn with_password(mut self, password: String) -> Self {
|
||||
self.password = Some(password);
|
||||
self
|
||||
@@ -166,7 +180,7 @@ impl ContextBuilder {
|
||||
|
||||
/// Builds the [`Context`] and opens it.
|
||||
///
|
||||
/// Returns error if context cannot be opened.
|
||||
/// Returns error if context cannot be opened with the given passphrase.
|
||||
pub async fn open(self) -> Result<Context> {
|
||||
let password = self.password.clone().unwrap_or_default();
|
||||
let context = self.build().await?;
|
||||
@@ -289,17 +303,6 @@ pub struct InnerContext {
|
||||
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
|
||||
/// see [`Context::get_connectivity()`].
|
||||
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
|
||||
|
||||
#[expect(clippy::type_complexity)]
|
||||
/// Transforms the root of the cryptographic payload before encryption.
|
||||
pub(crate) pre_encrypt_mime_hook: parking_lot::Mutex<
|
||||
Option<
|
||||
for<'a> fn(
|
||||
&Context,
|
||||
mail_builder::mime::MimePart<'a>,
|
||||
) -> mail_builder::mime::MimePart<'a>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -386,12 +389,9 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Opens the database with the given passphrase.
|
||||
/// NB: Db encryption is deprecated, so `passphrase` should be empty normally. See
|
||||
/// [`ContextBuilder::with_password()`] for reasoning.
|
||||
///
|
||||
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
|
||||
/// errors.
|
||||
#[deprecated(since = "TBD")]
|
||||
pub async fn open(&self, passphrase: String) -> Result<bool> {
|
||||
if self.sql.check_passphrase(passphrase.clone()).await? {
|
||||
self.sql.open(self, passphrase).await?;
|
||||
@@ -402,7 +402,6 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Changes encrypted database passphrase.
|
||||
/// Deprecated 2025-11, see [`ContextBuilder::with_password()`] for reasoning.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
self.sql.change_passphrase(passphrase).await?;
|
||||
Ok(())
|
||||
@@ -453,7 +452,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),
|
||||
@@ -468,7 +467,6 @@ impl Context {
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
self_fingerprint: OnceLock::new(),
|
||||
connectivities: parking_lot::Mutex::new(Vec::new()),
|
||||
pre_encrypt_mime_hook: None.into(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -485,6 +483,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()
|
||||
@@ -592,9 +596,10 @@ impl Context {
|
||||
if self
|
||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
.await
|
||||
&& let Err(err) = self.update_recent_quota(&mut session).await
|
||||
{
|
||||
warn!(self, "Failed to update quota: {err:#}.");
|
||||
if let Err(err) = self.update_recent_quota(&mut session).await {
|
||||
warn!(self, "Failed to update quota: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,10 +796,9 @@ impl Context {
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let l = EnteredLoginParam::load(self).await?;
|
||||
let l2 = ConfiguredLoginParam::load(self).await?.map_or_else(
|
||||
|| "Not configured".to_string(),
|
||||
|(_transport_id, param)| param.to_string(),
|
||||
);
|
||||
let l2 = ConfiguredLoginParam::load(self)
|
||||
.await?
|
||||
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let chats = get_chat_cnt(self).await?;
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
|
||||
@@ -1047,13 +1051,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"test_hooks",
|
||||
self.sql
|
||||
.get_raw_config("test_hooks")
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"fail_on_receiving_full_msg",
|
||||
self.sql
|
||||
@@ -1061,13 +1058,6 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"std_header_protection_composing",
|
||||
self.sql
|
||||
.get_raw_config("std_header_protection_composing")
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
|
||||
@@ -115,13 +115,15 @@ pub async fn maybe_set_logging_xdc_inner(
|
||||
filename: Option<&str>,
|
||||
msg_id: MsgId,
|
||||
) -> anyhow::Result<()> {
|
||||
if viewtype == Viewtype::Webxdc
|
||||
&& let Some(filename) = filename
|
||||
&& filename.starts_with("debug_logging")
|
||||
&& filename.ends_with(".xdc")
|
||||
&& chat_id.is_self_talk(context).await?
|
||||
{
|
||||
set_debug_logging_xdc(context, Some(msg_id)).await?;
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
if let Some(filename) = filename {
|
||||
if filename.starts_with("debug_logging")
|
||||
&& filename.ends_with(".xdc")
|
||||
&& chat_id.is_self_talk(context).await?
|
||||
{
|
||||
set_debug_logging_xdc(context, Some(msg_id)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ use quick_xml::{
|
||||
|
||||
use crate::simplify::{SimplifiedText, simplify_quote};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Dehtml {
|
||||
strbuilder: String,
|
||||
quote: String,
|
||||
@@ -26,9 +25,6 @@ struct Dehtml {
|
||||
/// Everything between `<div name="quote">` and `<div name="quoted-content">` is usually metadata
|
||||
/// If this is > `0`, then we are inside a `<div name="quoted-content">`.
|
||||
divs_since_quoted_content_div: u32,
|
||||
/// `<div class="header-protection-legacy-display">` elements should be omitted, see
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html#section-4.5.3.3>.
|
||||
divs_since_hp_legacy_display: u32,
|
||||
/// All-Inkl just puts the quote into `<blockquote> </blockquote>`. This count is
|
||||
/// increased at each `<blockquote>` and decreased at each `</blockquote>`.
|
||||
blockquotes_since_blockquote: u32,
|
||||
@@ -52,25 +48,20 @@ impl Dehtml {
|
||||
}
|
||||
|
||||
fn get_add_text(&self) -> AddText {
|
||||
// Everything between `<div name="quoted">` and `<div name="quoted_content">` is
|
||||
// metadata which we don't want.
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0
|
||||
|| self.divs_since_hp_legacy_display > 0
|
||||
{
|
||||
AddText::No
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
|
||||
AddText::No // Everything between `<div name="quoted">` and `<div name="quoted_content">` is metadata which we don't want
|
||||
} else {
|
||||
self.add_text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum AddText {
|
||||
/// Inside `<script>`, `<style>` and similar tags
|
||||
/// which contents should not be displayed.
|
||||
No,
|
||||
|
||||
#[default]
|
||||
YesRemoveLineEnds,
|
||||
|
||||
/// Inside `<pre>`.
|
||||
@@ -130,7 +121,12 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
|
||||
let mut dehtml = Dehtml {
|
||||
strbuilder: String::with_capacity(buf.len()),
|
||||
..Default::default()
|
||||
quote: String::new(),
|
||||
add_text: AddText::YesRemoveLineEnds,
|
||||
last_href: None,
|
||||
divs_since_quote_div: 0,
|
||||
divs_since_quoted_content_div: 0,
|
||||
blockquotes_since_blockquote: 0,
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(buf);
|
||||
@@ -248,7 +244,6 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
"div" => {
|
||||
pop_tag(&mut dehtml.divs_since_quote_div);
|
||||
pop_tag(&mut dehtml.divs_since_quoted_content_div);
|
||||
pop_tag(&mut dehtml.divs_since_hp_legacy_display);
|
||||
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
@@ -300,8 +295,6 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
"div" => {
|
||||
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
|
||||
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
|
||||
maybe_push_tag(event, reader, "header-protection-legacy-display",
|
||||
&mut dehtml.divs_since_hp_legacy_display);
|
||||
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
@@ -546,27 +539,6 @@ mod tests {
|
||||
assert_eq!(txt.text.trim(), "two\nlines");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hp_legacy_display() {
|
||||
let input = r#"
|
||||
<html><head><title></title></head><body>
|
||||
<div class="header-protection-legacy-display">
|
||||
<pre>Subject: Dinner plans</pre>
|
||||
</div>
|
||||
<p>
|
||||
Let's meet at Rama's Roti Shop at 8pm and go to the park
|
||||
from there.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(
|
||||
txt.text.trim(),
|
||||
"Let's meet at Rama's Roti Shop at 8pm and go to the park from there."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote_div() {
|
||||
let input = include_str!("../test-data/message/gmx-quote-body.eml");
|
||||
|
||||
@@ -57,7 +57,7 @@ impl EncryptHelper {
|
||||
let ctext = pgp::pk_encrypt(
|
||||
raw_message,
|
||||
keyring,
|
||||
sign_key,
|
||||
Some(sign_key),
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
)
|
||||
|
||||
@@ -241,9 +241,10 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
match timer {
|
||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
0..=60 => {
|
||||
0..=59 => {
|
||||
stock_str::msg_ephemeral_timer_enabled(context, &timer.to_string(), from_id).await
|
||||
}
|
||||
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
|
||||
61..=3599 => {
|
||||
stock_str::msg_ephemeral_timer_minutes(
|
||||
context,
|
||||
|
||||
@@ -38,7 +38,7 @@ async fn test_stock_ephemeral_messages() {
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 60 s."
|
||||
"You set message deletion timer to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 90 }, ContactId::SELF)
|
||||
@@ -142,7 +142,7 @@ async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
bob_received_message.text,
|
||||
"Message deletion timer is set to 60 s by alice@example.org."
|
||||
"Message deletion timer is set to 1 minute by alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(bob).await?,
|
||||
@@ -451,8 +451,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
let transport_id = 1;
|
||||
let uidvalidity = 12345;
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
@@ -472,8 +470,8 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (?, ?,'INBOX',?, 'INBOX', ?);",
|
||||
(transport_id, &message_id, id, uidvalidity),
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
(&message_id, id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -138,9 +138,6 @@ pub enum HeaderDef {
|
||||
/// Advertised gossip topic for one webxdc.
|
||||
IrohGossipTopic,
|
||||
|
||||
/// See <https://www.rfc-editor.org/rfc/rfc9788.html#name-hp-outer-header-field>.
|
||||
HpOuter,
|
||||
|
||||
#[cfg(test)]
|
||||
TestHeader,
|
||||
}
|
||||
|
||||
87
src/html.rs
87
src/html.rs
@@ -169,28 +169,27 @@ impl HtmlMsgParser {
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype == mime::TEXT_HTML {
|
||||
if self.html.is_empty()
|
||||
&& let Ok(decoded_data) = mail.get_body()
|
||||
{
|
||||
self.html = decoded_data;
|
||||
if self.html.is_empty() {
|
||||
if let Ok(decoded_data) = mail.get_body() {
|
||||
self.html = decoded_data;
|
||||
}
|
||||
}
|
||||
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
|
||||
if let Ok(decoded_data) = mail.get_body() {
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().eq_ignore_ascii_case("flowed")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if mimetype == mime::TEXT_PLAIN
|
||||
&& self.plain.is_none()
|
||||
&& let Ok(decoded_data) = mail.get_body()
|
||||
{
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().eq_ignore_ascii_case("flowed")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -214,29 +213,31 @@ impl HtmlMsgParser {
|
||||
MimeMultipartType::Message => Ok(()),
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype.type_() == mime::IMAGE
|
||||
&& let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId)
|
||||
&& let Ok(cid) = parse_message_id(&cid)
|
||||
&& let Ok(replacement) = mimepart_to_data_url(mail)
|
||||
{
|
||||
let re_string = format!(
|
||||
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
|
||||
regex::escape(&cid)
|
||||
);
|
||||
match regex::Regex::new(&re_string) {
|
||||
Ok(re) => {
|
||||
self.html = re
|
||||
.replace_all(
|
||||
&self.html,
|
||||
format!("${{1}}{replacement}${{3}}").as_str(),
|
||||
)
|
||||
.as_ref()
|
||||
.to_string()
|
||||
if mimetype.type_() == mime::IMAGE {
|
||||
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
|
||||
if let Ok(cid) = parse_message_id(&cid) {
|
||||
if let Ok(replacement) = mimepart_to_data_url(mail) {
|
||||
let re_string = format!(
|
||||
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
|
||||
regex::escape(&cid)
|
||||
);
|
||||
match regex::Regex::new(&re_string) {
|
||||
Ok(re) => {
|
||||
self.html = re
|
||||
.replace_all(
|
||||
&self.html,
|
||||
format!("${{1}}{replacement}${{3}}").as_str(),
|
||||
)
|
||||
.as_ref()
|
||||
.to_string()
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"Cannot create regex for cid: {} throws {}", re_string, e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"Cannot create regex for cid: {} throws {}", re_string, e
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
236
src/imap.rs
236
src/imap.rs
@@ -71,15 +71,10 @@ const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Imap {
|
||||
/// ID of the transport configuration in the `transports` table.
|
||||
///
|
||||
/// This ID is used to namespace records in the `imap` table.
|
||||
transport_id: u32,
|
||||
|
||||
pub(crate) idle_interrupt_receiver: Receiver<()>,
|
||||
|
||||
/// Email address.
|
||||
pub(crate) addr: String,
|
||||
addr: String,
|
||||
|
||||
/// Login parameters.
|
||||
lp: Vec<ConfiguredServerLoginParam>,
|
||||
@@ -254,21 +249,19 @@ impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
|
||||
|
||||
impl Imap {
|
||||
/// Creates new disconnected IMAP client using the specific login parameters.
|
||||
pub async fn new(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
param: ConfiguredLoginParam,
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
pub fn new(
|
||||
lp: Vec<ConfiguredServerLoginParam>,
|
||||
password: String,
|
||||
proxy_config: Option<ProxyConfig>,
|
||||
addr: &str,
|
||||
strict_tls: bool,
|
||||
oauth2: bool,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
) -> Result<Self> {
|
||||
let lp = param.imap.clone();
|
||||
let password = param.imap_password.clone();
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
let addr = ¶m.addr;
|
||||
let strict_tls = param.strict_tls(proxy_config.is_some());
|
||||
let oauth2 = param.oauth2;
|
||||
) -> Self {
|
||||
let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
|
||||
Ok(Imap {
|
||||
transport_id,
|
||||
Imap {
|
||||
idle_interrupt_receiver,
|
||||
addr: addr.to_string(),
|
||||
lp,
|
||||
@@ -284,7 +277,7 @@ impl Imap {
|
||||
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
|
||||
resync_request_sender,
|
||||
resync_request_receiver,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates new disconnected IMAP client using configured parameters.
|
||||
@@ -292,10 +285,20 @@ impl Imap {
|
||||
context: &Context,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
) -> Result<Self> {
|
||||
let (transport_id, param) = ConfiguredLoginParam::load(context)
|
||||
let param = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
let strict_tls = param.strict_tls(proxy_config.is_some());
|
||||
let imap = Self::new(
|
||||
param.imap.clone(),
|
||||
param.imap_password.clone(),
|
||||
proxy_config,
|
||||
¶m.addr,
|
||||
strict_tls,
|
||||
param.oauth2,
|
||||
idle_interrupt_receiver,
|
||||
);
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
@@ -409,19 +412,9 @@ impl Imap {
|
||||
})
|
||||
.await
|
||||
.context("Failed to enable IMAP compression")?;
|
||||
Session::new(
|
||||
compressed_session,
|
||||
capabilities,
|
||||
resync_request_sender,
|
||||
self.transport_id,
|
||||
)
|
||||
Session::new(compressed_session, capabilities, resync_request_sender)
|
||||
} else {
|
||||
Session::new(
|
||||
session,
|
||||
capabilities,
|
||||
resync_request_sender,
|
||||
self.transport_id,
|
||||
)
|
||||
Session::new(session, capabilities, resync_request_sender)
|
||||
};
|
||||
|
||||
// Store server ID in the context to display in account info.
|
||||
@@ -600,9 +593,8 @@ impl Imap {
|
||||
folder: &str,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<(usize, bool)> {
|
||||
let transport_id = self.transport_id;
|
||||
let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
|
||||
let old_uid_next = get_uid_next(context, transport_id, folder).await?;
|
||||
let uid_validity = get_uidvalidity(context, folder).await?;
|
||||
let old_uid_next = get_uid_next(context, folder).await?;
|
||||
info!(
|
||||
context,
|
||||
"fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
|
||||
@@ -670,19 +662,12 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(transport_id, folder, uid, uidvalidity)
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target",
|
||||
(
|
||||
self.transport_id,
|
||||
&message_id,
|
||||
&folder,
|
||||
uid,
|
||||
uid_validity,
|
||||
target,
|
||||
),
|
||||
(&message_id, &folder, uid, uid_validity, target),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -793,7 +778,7 @@ impl Imap {
|
||||
prefetch_uid_next < mailbox_uid_next
|
||||
};
|
||||
if new_uid_next > old_uid_next {
|
||||
set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
}
|
||||
|
||||
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
|
||||
@@ -873,7 +858,6 @@ impl Session {
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
let transport_id = self.transport_id();
|
||||
if folder_exists {
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", RFC724MID_UID)
|
||||
@@ -906,7 +890,7 @@ impl Session {
|
||||
msgs.len(),
|
||||
);
|
||||
|
||||
uid_validity = get_uidvalidity(context, transport_id, folder).await?;
|
||||
uid_validity = get_uidvalidity(context, folder).await?;
|
||||
} else {
|
||||
warn!(context, "resync_folder_uids: No folder {folder}.");
|
||||
uid_validity = 0;
|
||||
@@ -921,12 +905,12 @@ impl Session {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
transaction.execute(
|
||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(transport_id, folder, uid, uidvalidity)
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target",
|
||||
(transport_id, rfc724_mid, folder, uid, uid_validity, target),
|
||||
(rfc724_mid, folder, uid, uid_validity, target),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1248,12 +1232,11 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
let mut updated_chat_ids = BTreeSet::new();
|
||||
let uid_validity = get_uidvalidity(context, transport_id, folder)
|
||||
let uid_validity = get_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get UID validity for folder {folder}"))?;
|
||||
let mut highest_modseq = get_modseq(context, transport_id, folder)
|
||||
let mut highest_modseq = get_modseq(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
|
||||
let mut list = self
|
||||
@@ -1276,14 +1259,15 @@ impl Session {
|
||||
continue;
|
||||
};
|
||||
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
|
||||
if is_seen
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
|
||||
if is_seen {
|
||||
if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to update seen status for msg {folder}/{uid}")
|
||||
})?
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(modseq) = fetch.modseq {
|
||||
@@ -1304,7 +1288,7 @@ impl Session {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
set_modseq(context, transport_id, folder, highest_modseq)
|
||||
set_modseq(context, folder, highest_modseq)
|
||||
.await
|
||||
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
|
||||
if !updated_chat_ids.is_empty() {
|
||||
@@ -1337,10 +1321,10 @@ impl Session {
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
match get_fetch_headers(&msg) {
|
||||
Ok(headers) => {
|
||||
if let Some(from) = mimeparser::get_from(&headers)
|
||||
&& context.is_self_addr(&from.addr).await?
|
||||
{
|
||||
result.extend(mimeparser::get_recipients(&headers));
|
||||
if let Some(from) = mimeparser::get_from(&headers) {
|
||||
if context.is_self_addr(&from.addr).await? {
|
||||
result.extend(mimeparser::get_recipients(&headers));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1573,17 +1557,17 @@ impl Session {
|
||||
.await?;
|
||||
let mut got_turn_server = false;
|
||||
for m in metadata {
|
||||
if m.entry == "/shared/vendor/deltachat/turn"
|
||||
&& let Some(value) = m.value
|
||||
{
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
got_turn_server = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
if m.entry == "/shared/vendor/deltachat/turn" {
|
||||
if let Some(value) = m.value {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
got_turn_server = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1868,13 +1852,11 @@ impl Imap {
|
||||
info!(context, "Scanning folder: {:?}", folder);
|
||||
|
||||
// Update the delimiter iff there is a different one, but only once.
|
||||
if let Some(d) = folder.delimiter()
|
||||
&& delimiter_is_default
|
||||
&& !d.is_empty()
|
||||
&& delimiter != d
|
||||
{
|
||||
delimiter = d.to_string();
|
||||
delimiter_is_default = false;
|
||||
if let Some(d) = folder.delimiter() {
|
||||
if delimiter_is_default && !d.is_empty() && delimiter != d {
|
||||
delimiter = d.to_string();
|
||||
delimiter_is_default = false;
|
||||
}
|
||||
}
|
||||
|
||||
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
|
||||
@@ -2115,10 +2097,12 @@ async fn needs_move_to_mvbox(
|
||||
.get_header_value(HeaderDef::AutoSubmitted)
|
||||
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
|
||||
.is_some()
|
||||
&& let Some(from) = mimeparser::get_from(headers)
|
||||
&& context.is_self_addr(&from.addr).await?
|
||||
{
|
||||
return Ok(true);
|
||||
if let Some(from) = mimeparser::get_from(headers) {
|
||||
if context.is_self_addr(&from.addr).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||
return Ok(false);
|
||||
@@ -2288,13 +2272,12 @@ pub(crate) async fn prefetch_should_download(
|
||||
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
|
||||
// the further process).
|
||||
|
||||
if let Some(chat) = prefetch_get_chat(context, headers).await?
|
||||
&& chat.typ == Chattype::Group
|
||||
&& !chat.id.is_special()
|
||||
{
|
||||
// This might be a group command, like removing a group member.
|
||||
// We really need to fetch this to avoid inconsistent group state.
|
||||
return Ok(true);
|
||||
if let Some(chat) = prefetch_get_chat(context, headers).await? {
|
||||
if chat.typ == Chattype::Group && !chat.id.is_special() {
|
||||
// This might be a group command, like removing a group member.
|
||||
// We really need to fetch this to avoid inconsistent group state.
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
|
||||
@@ -2434,18 +2417,13 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
|
||||
/// uid_next is the next unique identifier value from the last time we fetched a folder
|
||||
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
|
||||
/// This function is used to update our uid_next after fetching messages.
|
||||
pub(crate) async fn set_uid_next(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
folder: &str,
|
||||
uid_next: u32,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
|
||||
ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
|
||||
(transport_id, folder, uid_next),
|
||||
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
|
||||
(folder, uid_next),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2456,69 +2434,57 @@ pub(crate) async fn set_uid_next(
|
||||
/// This method returns the uid_next from the last time we fetched messages.
|
||||
/// We can compare this to the current uid_next to find out whether there are new messages
|
||||
/// and fetch from this value on to get all new messages.
|
||||
async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
|
||||
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
)
|
||||
.query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_uidvalidity(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
folder: &str,
|
||||
uidvalidity: u32,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
|
||||
ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
|
||||
(transport_id, folder, uidvalidity),
|
||||
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
|
||||
(folder, uidvalidity),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
|
||||
async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
|
||||
(folder,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub(crate) async fn set_modseq(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
folder: &str,
|
||||
modseq: u64,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
|
||||
ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
|
||||
(transport_id, folder, modseq),
|
||||
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
|
||||
(folder, modseq),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
|
||||
async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
)
|
||||
.query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
@@ -2558,11 +2524,11 @@ fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
|
||||
let mut ranges: Vec<UidRange> = vec![];
|
||||
|
||||
for ¤t in uids {
|
||||
if let Some(last) = ranges.last_mut()
|
||||
&& last.end + 1 == current
|
||||
{
|
||||
last.end = current;
|
||||
continue;
|
||||
if let Some(last) = ranges.last_mut() {
|
||||
if last.end + 1 == current {
|
||||
last.end = current;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ranges.push(UidRange {
|
||||
|
||||
@@ -11,23 +11,17 @@ fn test_get_folder_meaning_by_name() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_uid_next_validity() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uidvalidity(&t.ctx, 1, "Inbox", 7).await.unwrap();
|
||||
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 7);
|
||||
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 0);
|
||||
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
// For another transport there is still no UIDVALIDITY set.
|
||||
assert_eq!(get_uidvalidity(&t.ctx, 2, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uid_next(&t.ctx, 1, "Inbox", 5).await.unwrap();
|
||||
set_uidvalidity(&t.ctx, 1, "Inbox", 6).await.unwrap();
|
||||
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 5);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 6);
|
||||
|
||||
assert_eq!(get_uid_next(&t.ctx, 2, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, 2, "Inbox").await.unwrap(), 0);
|
||||
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
|
||||
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,7 +5,6 @@ use anyhow::Context as _;
|
||||
use super::session::Session as ImapSession;
|
||||
use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity};
|
||||
use crate::context::Context;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::log::warn;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -35,16 +34,16 @@ impl ImapSession {
|
||||
/// because no EXPUNGE responses are sent, see
|
||||
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
|
||||
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Some(folder) = &self.selected_folder
|
||||
&& self.selected_folder_needs_expunge
|
||||
{
|
||||
info!(context, "Expunge messages in {folder:?}.");
|
||||
if let Some(folder) = &self.selected_folder {
|
||||
if self.selected_folder_needs_expunge {
|
||||
info!(context, "Expunge messages in {folder:?}.");
|
||||
|
||||
self.close().await.context("IMAP close/expunge failed")?;
|
||||
info!(context, "Close/expunge succeeded.");
|
||||
self.selected_folder = None;
|
||||
self.selected_folder_needs_expunge = false;
|
||||
self.new_mail = false;
|
||||
self.close().await.context("IMAP close/expunge failed")?;
|
||||
info!(context, "Close/expunge succeeded.");
|
||||
self.selected_folder = None;
|
||||
self.selected_folder_needs_expunge = false;
|
||||
self.new_mail = false;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -55,10 +54,10 @@ impl ImapSession {
|
||||
async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> {
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||
if let Some(selected_folder) = &self.selected_folder
|
||||
&& folder == selected_folder
|
||||
{
|
||||
return Ok(NewlySelected::No);
|
||||
if let Some(selected_folder) = &self.selected_folder {
|
||||
if folder == selected_folder {
|
||||
return Ok(NewlySelected::No);
|
||||
}
|
||||
}
|
||||
|
||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||
@@ -130,7 +129,7 @@ impl ImapSession {
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
create: bool,
|
||||
) -> anyhow::Result<bool> {
|
||||
) -> Result<bool> {
|
||||
let newly_selected = if create {
|
||||
self.select_or_create_folder(context, folder)
|
||||
.await
|
||||
@@ -147,24 +146,15 @@ impl ImapSession {
|
||||
},
|
||||
}
|
||||
};
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
// Folders should not be selected when transport_id is not assigned yet
|
||||
// because we cannot save UID validity then.
|
||||
ensure_and_debug_assert!(
|
||||
transport_id > 0,
|
||||
"Cannot select folder when transport ID is unknown"
|
||||
);
|
||||
|
||||
let mailbox = self
|
||||
.selected_mailbox
|
||||
.as_mut()
|
||||
.with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
|
||||
|
||||
let old_uid_validity = get_uidvalidity(context, transport_id, folder)
|
||||
let old_uid_validity = get_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
|
||||
let old_uid_next = get_uid_next(context, transport_id, folder)
|
||||
let old_uid_next = get_uid_next(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
|
||||
|
||||
@@ -215,7 +205,7 @@ impl ImapSession {
|
||||
context,
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
);
|
||||
set_uid_next(context, transport_id, folder, new_uid_next).await?;
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
self.resync_request_sender.try_send(()).ok();
|
||||
}
|
||||
|
||||
@@ -234,21 +224,21 @@ impl ImapSession {
|
||||
}
|
||||
|
||||
// UIDVALIDITY is modified, reset highest seen MODSEQ.
|
||||
set_modseq(context, transport_id, folder, 0).await?;
|
||||
set_modseq(context, folder, 0).await?;
|
||||
|
||||
// ============== uid_validity has changed or is being set the first time. ==============
|
||||
|
||||
let new_uid_next = new_uid_next.unwrap_or_default();
|
||||
set_uid_next(context, transport_id, folder, new_uid_next).await?;
|
||||
set_uidvalidity(context, transport_id, folder, new_uid_validity).await?;
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
set_uidvalidity(context, folder, new_uid_validity).await?;
|
||||
self.new_mail = true;
|
||||
|
||||
// Collect garbage entries in `imap` table.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE transport_id=? AND folder=? AND uidvalidity!=?",
|
||||
(transport_id, &folder, new_uid_validity),
|
||||
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
|
||||
(&folder, new_uid_validity),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -257,7 +247,12 @@ impl ImapSession {
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"transport {transport_id}: UID validity for folder {folder} changed from {old_uid_validity}/{old_uid_next} to {new_uid_validity}/{new_uid_next}.",
|
||||
"uid/validity change folder {}: new {}/{} previous {}/{}.",
|
||||
folder,
|
||||
new_uid_next,
|
||||
new_uid_validity,
|
||||
old_uid_next,
|
||||
old_uid_validity,
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIE
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Session {
|
||||
transport_id: u32,
|
||||
|
||||
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
|
||||
|
||||
pub capabilities: Capabilities,
|
||||
@@ -73,10 +71,8 @@ impl Session {
|
||||
inner: ImapSession<Box<dyn SessionStream>>,
|
||||
capabilities: Capabilities,
|
||||
resync_request_sender: async_channel::Sender<()>,
|
||||
transport_id: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
transport_id,
|
||||
inner,
|
||||
capabilities,
|
||||
selected_folder: None,
|
||||
@@ -88,11 +84,6 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns ID of the transport for which this session was created.
|
||||
pub(crate) fn transport_id(&self) -> u32 {
|
||||
self.transport_id
|
||||
}
|
||||
|
||||
pub fn can_idle(&self) -> bool {
|
||||
self.capabilities.can_idle
|
||||
}
|
||||
|
||||
31
src/imex.rs
31
src/imex.rs
@@ -377,15 +377,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
res = check_backup_version(context).await;
|
||||
}
|
||||
if res.is_ok() {
|
||||
// All recent backups have `bcc_self` set to "1" before export.
|
||||
//
|
||||
// Setting `bcc_self` to "1" on export was introduced on 2024-12-17
|
||||
// in commit 21664125d798021be75f47d5b0d5006d338b4531
|
||||
//
|
||||
// We additionally try to set `bcc_self` to "1" after import here
|
||||
// for compatibility with older backups,
|
||||
// but eventually this code can be removed.
|
||||
res = context.set_config(Config::BccSelf, Some("1")).await;
|
||||
res = adjust_bcc_self(context).await;
|
||||
}
|
||||
fs::remove_file(unpacked_database)
|
||||
.await
|
||||
@@ -759,7 +751,7 @@ async fn export_database(
|
||||
.to_str()
|
||||
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
|
||||
|
||||
context.set_config(Config::BccSelf, Some("1")).await?;
|
||||
adjust_bcc_self(context).await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("backup_time", timestamp)
|
||||
@@ -793,6 +785,18 @@ async fn export_database(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sets `Config::BccSelf` (and `DeleteServerAfter` to "never" in effect) if needed so that new
|
||||
/// messages are present on the server after a backup restoration or available for all devices in
|
||||
/// multi-device case. NB: Calling this after a backup import isn't reliable as we can crash in
|
||||
/// between, but this is a problem only for old backups, new backups already have `BccSelf` set if
|
||||
/// necessary.
|
||||
async fn adjust_bcc_self(context: &Context) -> Result<()> {
|
||||
if context.is_chatmail().await? && !context.config_exists(Config::BccSelf).await? {
|
||||
context.set_config(Config::BccSelf, Some("1")).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_backup_version(context: &Context) -> Result<()> {
|
||||
let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
|
||||
ensure!(
|
||||
@@ -979,10 +983,11 @@ mod tests {
|
||||
|
||||
let context1 = &TestContext::new_alice().await;
|
||||
|
||||
// `bcc_self` is enabled by default for test contexts. Unset it.
|
||||
context1.set_config(Config::BccSelf, None).await?;
|
||||
|
||||
// Check that the settings are displayed correctly.
|
||||
assert_eq!(
|
||||
context1.get_config(Config::BccSelf).await?,
|
||||
Some("1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
context1.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
|
||||
@@ -89,7 +89,6 @@ pub mod securejoin;
|
||||
mod simplify;
|
||||
mod smtp;
|
||||
pub mod stock_str;
|
||||
pub mod storage_usage;
|
||||
mod sync;
|
||||
mod timesmearing;
|
||||
mod token;
|
||||
|
||||
@@ -294,7 +294,7 @@ pub async fn send_locations_to_chat(
|
||||
.unwrap_or_default();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, &stock_str).await?;
|
||||
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
@@ -849,7 +849,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
.context("failed to disable location streaming")?;
|
||||
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, &stock_str).await?;
|
||||
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
|
||||
132
src/message.rs
132
src/message.rs
@@ -638,33 +638,33 @@ impl Message {
|
||||
pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
|
||||
if self.viewtype.has_file() {
|
||||
let file_param = self.param.get_file_path(context)?;
|
||||
if let Some(path_and_filename) = file_param
|
||||
&& matches!(
|
||||
if let Some(path_and_filename) = file_param {
|
||||
if matches!(
|
||||
self.viewtype,
|
||||
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
|
||||
)
|
||||
&& !self.param.exists(Param::Width)
|
||||
{
|
||||
let buf = read_file(context, &path_and_filename).await?;
|
||||
) && !self.param.exists(Param::Width)
|
||||
{
|
||||
let buf = read_file(context, &path_and_filename).await?;
|
||||
|
||||
match get_filemeta(&buf) {
|
||||
Ok((width, height)) => {
|
||||
self.param.set_int(Param::Width, width as i32);
|
||||
self.param.set_int(Param::Height, height as i32);
|
||||
match get_filemeta(&buf) {
|
||||
Ok((width, height)) => {
|
||||
self.param.set_int(Param::Width, width as i32);
|
||||
self.param.set_int(Param::Height, height as i32);
|
||||
}
|
||||
Err(err) => {
|
||||
self.param.set_int(Param::Width, 0);
|
||||
self.param.set_int(Param::Height, 0);
|
||||
warn!(
|
||||
context,
|
||||
"Failed to get width and height for {}: {err:#}.",
|
||||
path_and_filename.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.param.set_int(Param::Width, 0);
|
||||
self.param.set_int(Param::Height, 0);
|
||||
warn!(
|
||||
context,
|
||||
"Failed to get width and height for {}: {err:#}.",
|
||||
path_and_filename.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.id.is_unset() {
|
||||
self.update_param(context).await?;
|
||||
if !self.id.is_unset() {
|
||||
self.update_param(context).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -992,12 +992,14 @@ impl Message {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(filename) = self.get_file(context)
|
||||
&& let Ok(ref buf) = read_file(context, &filename).await
|
||||
&& let Ok((typ, headers, _)) = split_armored_data(buf)
|
||||
&& typ == pgp::armor::BlockType::Message
|
||||
{
|
||||
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
|
||||
if let Some(filename) = self.get_file(context) {
|
||||
if let Ok(ref buf) = read_file(context, &filename).await {
|
||||
if let Ok((typ, headers, _)) = split_armored_data(buf) {
|
||||
if typ == pgp::armor::BlockType::Message {
|
||||
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
@@ -1222,25 +1224,26 @@ impl Message {
|
||||
///
|
||||
/// `References` header is not taken into account.
|
||||
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
|
||||
if let Some(in_reply_to) = &self.in_reply_to
|
||||
&& let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await?
|
||||
{
|
||||
let msg = Message::load_from_db_optional(context, msg_id).await?;
|
||||
return Ok(msg);
|
||||
if let Some(in_reply_to) = &self.in_reply_to {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
let msg = Message::load_from_db_optional(context, msg_id).await?;
|
||||
return Ok(msg);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns original message ID for message from "Saved Messages".
|
||||
pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
|
||||
if !self.original_msg_id.is_special()
|
||||
&& let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
|
||||
{
|
||||
return if msg.chat_id.is_trash() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(msg.id))
|
||||
};
|
||||
if !self.original_msg_id.is_special() {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
|
||||
{
|
||||
return if msg.chat_id.is_trash() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(msg.id))
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1437,15 +1440,7 @@ pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &
|
||||
let extension: &str = &path.extension()?.to_str()?.to_lowercase();
|
||||
let info = match extension {
|
||||
// before using viewtype other than Viewtype::File,
|
||||
// make sure, all target UIs support that type.
|
||||
//
|
||||
// it is a non-goal to support as many formats as possible in-app.
|
||||
// additional parser come at security and maintainance costs and
|
||||
// should only be added when strictly neccessary,
|
||||
// eg. when a format comes from the camera app on a significant number of devices.
|
||||
// it is okay, when eg. dragging some video from a browser results in a "File"
|
||||
// for everyone, sender as well as all receivers.
|
||||
//
|
||||
// make sure, all target UIs support that type in the context of the used viewer/player.
|
||||
// if in doubt, it is better to default to Viewtype::File that passes handing to an external app.
|
||||
// (cmp. <https://developer.android.com/guide/topics/media/media-formats>)
|
||||
"3gp" => (Viewtype::Video, "video/3gpp"),
|
||||
@@ -1508,7 +1503,7 @@ pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &
|
||||
"vcf" => (Viewtype::Vcard, "text/vcard"),
|
||||
"wav" => (Viewtype::Audio, "audio/wav"),
|
||||
"weba" => (Viewtype::File, "audio/webm"),
|
||||
"webm" => (Viewtype::File, "video/webm"), // not supported natively by iOS nor by SDWebImage
|
||||
"webm" => (Viewtype::Video, "video/webm"),
|
||||
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
|
||||
"wmv" => (Viewtype::Video, "video/x-ms-wmv"),
|
||||
"xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
|
||||
@@ -1610,10 +1605,10 @@ pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Resu
|
||||
.expect("RwLock is poisoned")
|
||||
.as_ref()
|
||||
.map(|dl| dl.msg_id);
|
||||
if let Some(id) = logging_xdc_id
|
||||
&& id == msg.id
|
||||
{
|
||||
set_debug_logging_xdc(context, None).await?;
|
||||
if let Some(id) = logging_xdc_id {
|
||||
if id == msg.id {
|
||||
set_debug_logging_xdc(context, None).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1868,33 +1863,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the messages with given IDs exist.
|
||||
///
|
||||
/// Returns IDs of existing messages.
|
||||
pub async fn get_existing_msg_ids(context: &Context, ids: &[MsgId]) -> Result<Vec<MsgId>> {
|
||||
let query_only = true;
|
||||
let res = context
|
||||
.sql
|
||||
.transaction_ex(query_only, |transaction| {
|
||||
let mut res: Vec<MsgId> = Vec::new();
|
||||
for id in ids {
|
||||
if transaction.query_one(
|
||||
"SELECT COUNT(*) > 0 FROM msgs WHERE id=? AND chat_id!=3",
|
||||
(id,),
|
||||
|row| {
|
||||
let exists: bool = row.get(0)?;
|
||||
Ok(exists)
|
||||
},
|
||||
)? {
|
||||
res.push(*id);
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
})
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_msg_state(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
|
||||
@@ -39,16 +39,17 @@ async fn test_get_width_height() {
|
||||
let mut has_image = false;
|
||||
let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
|
||||
for chatitem in chatitems {
|
||||
if let ChatItem::Message { msg_id } = chatitem
|
||||
&& let Ok(msg) = Message::load_from_db(&t, msg_id).await
|
||||
&& msg.get_viewtype() == Viewtype::Image
|
||||
{
|
||||
has_image = true;
|
||||
// just check that width/height are inside some reasonable ranges
|
||||
assert!(msg.get_width() > 100);
|
||||
assert!(msg.get_height() > 100);
|
||||
assert!(msg.get_width() < 4000);
|
||||
assert!(msg.get_height() < 4000);
|
||||
if let ChatItem::Message { msg_id } = chatitem {
|
||||
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
|
||||
if msg.get_viewtype() == Viewtype::Image {
|
||||
has_image = true;
|
||||
// just check that width/height are inside some reasonable ranges
|
||||
assert!(msg.get_width() > 100);
|
||||
assert!(msg.get_height() > 100);
|
||||
assert!(msg.get_width() < 4000);
|
||||
assert!(msg.get_height() < 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(has_image);
|
||||
@@ -764,27 +765,3 @@ async fn test_load_unknown_viewtype() -> Result<()> {
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Unknown);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_existing_msg_ids() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let msg1_id = tcm.send_recv(alice, bob, "Hello 1!").await.id;
|
||||
let msg2_id = tcm.send_recv(alice, bob, "Hello 2!").await.id;
|
||||
let msg3_id = tcm.send_recv(alice, bob, "Hello 3!").await.id;
|
||||
let msg4_id = tcm.send_recv(alice, bob, "Hello 4!").await.id;
|
||||
|
||||
assert_eq!(
|
||||
get_existing_msg_ids(bob, &[msg1_id, msg2_id, msg3_id, msg4_id]).await?,
|
||||
vec![msg1_id, msg2_id, msg3_id, msg4_id]
|
||||
);
|
||||
delete_msgs(bob, &[msg1_id, msg3_id]).await?;
|
||||
assert_eq!(
|
||||
get_existing_msg_ids(bob, &[msg1_id, msg2_id, msg3_id, msg4_id]).await?,
|
||||
vec![msg2_id, msg4_id]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -292,10 +292,11 @@ impl MimeFactory {
|
||||
|
||||
// In a broadcast channel, only send member-added/removed messages
|
||||
// to the affected member:
|
||||
if let Some(fp) = must_have_only_one_recipient(&msg, &chat)
|
||||
&& fp? != fingerprint {
|
||||
if let Some(fp) = must_have_only_one_recipient(&msg, &chat) {
|
||||
if fp? != fingerprint {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
|
||||
Some(SignedPublicKey::from_slice(public_key_bytes)?)
|
||||
@@ -346,8 +347,8 @@ impl MimeFactory {
|
||||
// Row is a tombstone,
|
||||
// member is not actually part of the group.
|
||||
if !recipients_contain_addr(&past_members, &addr) {
|
||||
if let Some(email_to_remove) = email_to_remove
|
||||
&& email_to_remove == addr {
|
||||
if let Some(email_to_remove) = email_to_remove {
|
||||
if email_to_remove == addr {
|
||||
// This is a "member removed" message,
|
||||
// we need to notify removed member
|
||||
// that it was removed.
|
||||
@@ -364,6 +365,7 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !undisclosed_recipients {
|
||||
past_members.push((name, addr.clone()));
|
||||
past_member_timestamps.push(remove_timestamp);
|
||||
@@ -393,14 +395,15 @@ impl MimeFactory {
|
||||
"member_fingerprints.len() ({}) < to.len() ({})",
|
||||
member_fingerprints.len(), to.len());
|
||||
|
||||
if to.len() > 1
|
||||
&& let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
|
||||
if to.len() > 1 {
|
||||
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
|
||||
to.remove(position);
|
||||
member_timestamps.remove(position);
|
||||
if is_encrypted {
|
||||
member_fingerprints.remove(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
member_timestamps.extend(past_member_timestamps);
|
||||
if is_encrypted {
|
||||
@@ -464,11 +467,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()
|
||||
@@ -734,35 +733,36 @@ impl MimeFactory {
|
||||
));
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded
|
||||
&& chat.typ == Chattype::Group
|
||||
{
|
||||
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? {
|
||||
headers.push((
|
||||
"Chat-Group-Member-Timestamps",
|
||||
mail_builder::headers::raw::Raw::new(
|
||||
self.member_timestamps
|
||||
.iter()
|
||||
.map(|ts| ts.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if chat.typ == Chattype::Group {
|
||||
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await?
|
||||
{
|
||||
headers.push((
|
||||
"Chat-Group-Member-Timestamps",
|
||||
mail_builder::headers::raw::Raw::new(
|
||||
self.member_timestamps
|
||||
.iter()
|
||||
.map(|ts| ts.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
if !self.member_fingerprints.is_empty() {
|
||||
headers.push((
|
||||
"Chat-Group-Member-Fpr",
|
||||
mail_builder::headers::raw::Raw::new(
|
||||
self.member_fingerprints
|
||||
.iter()
|
||||
.map(|fp| fp.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
if !self.member_fingerprints.is_empty() {
|
||||
headers.push((
|
||||
"Chat-Group-Member-Fpr",
|
||||
mail_builder::headers::raw::Raw::new(
|
||||
self.member_fingerprints
|
||||
.iter()
|
||||
.map(|fp| fp.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,34 +814,37 @@ impl MimeFactory {
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
|
||||
));
|
||||
} else if let Loaded::Message { msg, .. } = &self.loaded
|
||||
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
{
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
if step != "vg-request" && step != "vc-request" {
|
||||
headers.push((
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
|
||||
));
|
||||
} else if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
if step != "vg-request" && step != "vc-request" {
|
||||
headers.push((
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Loaded::Message { msg, chat } = &self.loaded
|
||||
&& (chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast)
|
||||
{
|
||||
headers.push((
|
||||
"Chat-List-ID",
|
||||
mail_builder::headers::text::Text::new(format!("{} <{}>", chat.name, chat.grpid))
|
||||
.into(),
|
||||
));
|
||||
|
||||
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
|
||||
&& let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET)
|
||||
{
|
||||
if let Loaded::Message { msg, chat } = &self.loaded {
|
||||
if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast {
|
||||
headers.push((
|
||||
"Chat-Broadcast-Secret",
|
||||
mail_builder::headers::text::Text::new(secret.to_string()).into(),
|
||||
"Chat-List-ID",
|
||||
mail_builder::headers::text::Text::new(format!(
|
||||
"{} <{}>",
|
||||
chat.name, chat.grpid
|
||||
))
|
||||
.into(),
|
||||
));
|
||||
|
||||
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup {
|
||||
if let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET) {
|
||||
headers.push((
|
||||
"Chat-Broadcast-Secret",
|
||||
mail_builder::headers::text::Text::new(secret.to_string()).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,7 +1000,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());
|
||||
}
|
||||
@@ -1023,15 +1043,7 @@ impl MimeFactory {
|
||||
//
|
||||
// and the explanation page says
|
||||
// "The time information deviates too much from the actual time".
|
||||
//
|
||||
// We also limit the range to 6 days (518400 seconds)
|
||||
// because with a larger range we got
|
||||
// error "500 Date header far in the past/future"
|
||||
// which apparently originates from Symantec Messaging Gateway
|
||||
// and means the message has a Date that is more
|
||||
// than 7 days in the past:
|
||||
// <https://github.com/chatmail/core/issues/7466>
|
||||
let timestamp_offset = rand::random_range(0..518400);
|
||||
let timestamp_offset = rand::random_range(0..1000000);
|
||||
let protected_timestamp = self.timestamp.saturating_sub(timestamp_offset);
|
||||
let unprotected_date =
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(protected_timestamp, 0)
|
||||
@@ -1071,9 +1083,6 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
let use_std_header_protection = context
|
||||
.get_config_bool(Config::StdHeaderProtectionComposing)
|
||||
.await?;
|
||||
let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys {
|
||||
// Store protected headers in the inner message.
|
||||
let message = protected_headers
|
||||
@@ -1089,22 +1098,6 @@ impl MimeFactory {
|
||||
message.header(header, value)
|
||||
});
|
||||
|
||||
if use_std_header_protection {
|
||||
message = unprotected_headers
|
||||
.iter()
|
||||
// Structural headers shouldn't be added as "HP-Outer". They are defined in
|
||||
// <https://www.rfc-editor.org/rfc/rfc9787.html#structural-header-fields>.
|
||||
.filter(|(name, _)| {
|
||||
!(name.eq_ignore_ascii_case("mime-version")
|
||||
|| name.eq_ignore_ascii_case("content-type")
|
||||
|| name.eq_ignore_ascii_case("content-transfer-encoding")
|
||||
|| name.eq_ignore_ascii_case("content-disposition"))
|
||||
})
|
||||
.fold(message, |message, (name, value)| {
|
||||
message.header(format!("HP-Outer: {name}"), value.clone())
|
||||
});
|
||||
}
|
||||
|
||||
// Add gossip headers in chats with multiple recipients
|
||||
let multiple_recipients =
|
||||
encryption_pubkeys.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
|
||||
@@ -1192,16 +1185,10 @@ impl MimeFactory {
|
||||
|
||||
// Set the appropriate Content-Type for the inner message.
|
||||
for (h, v) in &mut message.headers {
|
||||
if h == "Content-Type"
|
||||
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
|
||||
{
|
||||
let mut ct_new = ct.clone();
|
||||
ct_new = ct_new.attribute("protected-headers", "v1");
|
||||
if use_std_header_protection {
|
||||
ct_new = ct_new.attribute("hp", "cipher");
|
||||
if h == "Content-Type" {
|
||||
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
|
||||
*ct = ct.clone().attribute("protected-headers", "v1");
|
||||
}
|
||||
*ct = ct_new;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1226,7 +1213,7 @@ impl MimeFactory {
|
||||
// created before we had symmetric encryption,
|
||||
// we show an error message.
|
||||
let text = BROADCAST_INCOMPATIBILITY_MSG;
|
||||
chat::add_info_msg(context, chat.id, text).await?;
|
||||
chat::add_info_msg(context, chat.id, text, time()).await?;
|
||||
bail!(text);
|
||||
}
|
||||
secret
|
||||
@@ -1245,12 +1232,6 @@ impl MimeFactory {
|
||||
// once new core versions are sufficiently deployed.
|
||||
let anonymous_recipients = false;
|
||||
|
||||
if context.get_config_bool(Config::TestHooks).await?
|
||||
&& let Some(hook) = &*context.pre_encrypt_mime_hook.lock()
|
||||
{
|
||||
message = hook(context, message);
|
||||
}
|
||||
|
||||
let encrypted = if let Some(shared_secret) = shared_secret {
|
||||
encrypt_helper
|
||||
.encrypt_symmetrically(context, &shared_secret, message, compress)
|
||||
@@ -1336,16 +1317,10 @@ impl MimeFactory {
|
||||
message
|
||||
} else {
|
||||
for (h, v) in &mut message.headers {
|
||||
if h == "Content-Type"
|
||||
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
|
||||
{
|
||||
let mut ct_new = ct.clone();
|
||||
ct_new = ct_new.attribute("protected-headers", "v1");
|
||||
if use_std_header_protection {
|
||||
ct_new = ct_new.attribute("hp", "clear");
|
||||
if h == "Content-Type" {
|
||||
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
|
||||
*ct = ct.clone().attribute("protected-headers", "v1");
|
||||
}
|
||||
*ct = ct_new;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1525,9 +1500,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 +1526,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 +1563,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 +1690,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
|
||||
@@ -1870,10 +1845,10 @@ impl MimeFactory {
|
||||
parts.push(msg_kml_part);
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
|
||||
&& let Some(part) = self.get_location_kml_part(context).await?
|
||||
{
|
||||
parts.push(part);
|
||||
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await? {
|
||||
if let Some(part) = self.get_location_kml_part(context).await? {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
// we do not piggyback sync-files to other self-sent-messages
|
||||
|
||||
@@ -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");
|
||||
@@ -765,32 +832,8 @@ async fn test_protected_headers_directive() -> Result<()> {
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 2);
|
||||
assert_eq!(part.match_indices("HP-Outer: Subject:").count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_hp_outer_headers() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let chat_id = t.get_self_chat().await.id;
|
||||
|
||||
for std_hp_composing in [false, true] {
|
||||
t.set_config_bool(Config::StdHeaderProtectionComposing, std_hp_composing)
|
||||
.await?;
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
let msg = MimeMessage::from_bytes(t, sent_msg.payload.as_bytes(), None).await?;
|
||||
assert_eq!(msg.header_exists(HeaderDef::HpOuter), std_hp_composing);
|
||||
for hdr in ["Date", "From", "Message-ID"] {
|
||||
assert_eq!(
|
||||
msg.decoded_data_contains(&format!("HP-Outer: {hdr}:")),
|
||||
std_hp_composing,
|
||||
);
|
||||
}
|
||||
assert!(!msg.decoded_data_contains("HP-Outer: Content-Type"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -271,13 +271,14 @@ impl MimeMessage {
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&mail,
|
||||
);
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| is_hidden(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
&mail.headers,
|
||||
);
|
||||
headers.retain(|k, _| {
|
||||
!is_hidden(k) || {
|
||||
headers_removed.insert(k.to_string());
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -298,7 +299,7 @@ impl MimeMessage {
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
part,
|
||||
&part.headers,
|
||||
);
|
||||
(part, part.ctype.mimetype.parse::<Mime>()?)
|
||||
} else {
|
||||
@@ -309,14 +310,13 @@ impl MimeMessage {
|
||||
// Currently we do not sign unencrypted messages by default.
|
||||
(&mail, mimetype)
|
||||
};
|
||||
if mimetype.type_() == mime::MULTIPART
|
||||
&& mimetype.subtype().as_str() == "mixed"
|
||||
&& let Some(part) = part.subparts.first()
|
||||
{
|
||||
for field in &part.headers {
|
||||
let key = field.get_key().to_lowercase();
|
||||
if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
|
||||
headers.insert(key.to_string(), field.get_value());
|
||||
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
|
||||
if let Some(part) = part.subparts.first() {
|
||||
for field in &part.headers {
|
||||
let key = field.get_key().to_lowercase();
|
||||
if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
|
||||
headers.insert(key.to_string(), field.get_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,7 +536,7 @@ impl MimeMessage {
|
||||
&mut inner_from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
mail,
|
||||
&mail.headers,
|
||||
);
|
||||
|
||||
if !signatures.is_empty() {
|
||||
@@ -679,9 +679,8 @@ impl MimeMessage {
|
||||
fn parse_system_message_headers(&mut self, context: &Context) {
|
||||
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
|
||||
self.parts.retain(|part| {
|
||||
part.mimetype
|
||||
.as_ref()
|
||||
.is_none_or(|mimetype| mimetype.as_ref() == MIME_AC_SETUP_FILE)
|
||||
part.mimetype.is_none()
|
||||
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
|
||||
});
|
||||
|
||||
if self.parts.len() == 1 {
|
||||
@@ -806,20 +805,22 @@ impl MimeMessage {
|
||||
{
|
||||
part.typ = Viewtype::Voice;
|
||||
}
|
||||
if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
|
||||
&& let Some(value) = self.get_header(HeaderDef::ChatContent)
|
||||
&& value == "sticker"
|
||||
{
|
||||
part.typ = Viewtype::Sticker;
|
||||
if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
|
||||
if let Some(value) = self.get_header(HeaderDef::ChatContent) {
|
||||
if value == "sticker" {
|
||||
part.typ = Viewtype::Sticker;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (part.typ == Viewtype::Audio
|
||||
if part.typ == Viewtype::Audio
|
||||
|| part.typ == Viewtype::Voice
|
||||
|| part.typ == Viewtype::Video)
|
||||
&& let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
|
||||
|| part.typ == Viewtype::Video
|
||||
{
|
||||
let duration_ms = field_0.parse().unwrap_or_default();
|
||||
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
|
||||
part.param.set_int(Param::Duration, duration_ms);
|
||||
if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
|
||||
let duration_ms = field_0.parse().unwrap_or_default();
|
||||
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
|
||||
part.param.set_int(Param::Duration, duration_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,38 +836,38 @@ impl MimeMessage {
|
||||
self.squash_attachment_parts();
|
||||
}
|
||||
|
||||
if !context.get_config_bool(Config::Bot).await?
|
||||
&& let Some(ref subject) = self.get_subject()
|
||||
{
|
||||
let mut prepend_subject = true;
|
||||
if !self.decrypting_failed {
|
||||
let colon = subject.find(':');
|
||||
if colon == Some(2)
|
||||
|| colon == Some(3)
|
||||
|| self.has_chat_version()
|
||||
|| subject.contains("Chat:")
|
||||
{
|
||||
prepend_subject = false
|
||||
if !context.get_config_bool(Config::Bot).await? {
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
let mut prepend_subject = true;
|
||||
if !self.decrypting_failed {
|
||||
let colon = subject.find(':');
|
||||
if colon == Some(2)
|
||||
|| colon == Some(3)
|
||||
|| self.has_chat_version()
|
||||
|| subject.contains("Chat:")
|
||||
{
|
||||
prepend_subject = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For mailing lists, always add the subject because sometimes there are different topics
|
||||
// and otherwise it might be hard to keep track:
|
||||
if self.is_mailinglist_message() && !self.has_chat_version() {
|
||||
prepend_subject = true;
|
||||
}
|
||||
// For mailing lists, always add the subject because sometimes there are different topics
|
||||
// and otherwise it might be hard to keep track:
|
||||
if self.is_mailinglist_message() && !self.has_chat_version() {
|
||||
prepend_subject = true;
|
||||
}
|
||||
|
||||
if prepend_subject && !subject.is_empty() {
|
||||
let part_with_text = self
|
||||
.parts
|
||||
.iter_mut()
|
||||
.find(|part| !part.msg.is_empty() && !part.is_reaction);
|
||||
if let Some(part) = part_with_text {
|
||||
// Message bubbles are small, so we use en dash to save space. In some
|
||||
// languages there may be em dashes in the message text added by the author,
|
||||
// they may look stronger than Subject separation, this is a known thing.
|
||||
// Anyway, classic email support isn't a priority as of 2025.
|
||||
part.msg = format!("{} – {}", subject, part.msg);
|
||||
if prepend_subject && !subject.is_empty() {
|
||||
let part_with_text = self
|
||||
.parts
|
||||
.iter_mut()
|
||||
.find(|part| !part.msg.is_empty() && !part.is_reaction);
|
||||
if let Some(part) = part_with_text {
|
||||
// Message bubbles are small, so we use en dash to save space. In some
|
||||
// languages there may be em dashes in the message text added by the author,
|
||||
// they may look stronger than Subject separation, this is a known thing.
|
||||
// Anyway, classic email support isn't a priority as of 2025.
|
||||
part.msg = format!("{} – {}", subject, part.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -880,22 +881,21 @@ impl MimeMessage {
|
||||
self.parse_attachments();
|
||||
|
||||
// See if an MDN is requested from the other side
|
||||
if !self.decrypting_failed
|
||||
&& !self.parts.is_empty()
|
||||
&& let Some(ref dn_to) = self.chat_disposition_notification_to
|
||||
{
|
||||
// Check that the message is not outgoing.
|
||||
let from = &self.from.addr;
|
||||
if !context.is_self_addr(from).await? {
|
||||
if from.to_lowercase() == dn_to.addr.to_lowercase() {
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
if !self.decrypting_failed && !self.parts.is_empty() {
|
||||
if let Some(ref dn_to) = self.chat_disposition_notification_to {
|
||||
// Check that the message is not outgoing.
|
||||
let from = &self.from.addr;
|
||||
if !context.is_self_addr(from).await? {
|
||||
if from.to_lowercase() == dn_to.addr.to_lowercase() {
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -910,11 +910,10 @@ impl MimeMessage {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(ref subject) = self.get_subject()
|
||||
&& !self.has_chat_version()
|
||||
&& self.webxdc_status_update.is_none()
|
||||
{
|
||||
part.msg = subject.to_string();
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
if !self.has_chat_version() && self.webxdc_status_update.is_none() {
|
||||
part.msg = subject.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
self.do_add_single_part(part);
|
||||
@@ -953,15 +952,15 @@ impl MimeMessage {
|
||||
|
||||
let mut i = 0;
|
||||
while let Some(part) = self.parts.get_mut(i) {
|
||||
if let Some(part_filename) = &part.org_filename
|
||||
&& part_filename == &header_value
|
||||
{
|
||||
if let Some(blob) = part.param.get(Param::File) {
|
||||
let res = Some(AvatarAction::Change(blob.to_string()));
|
||||
self.parts.remove(i);
|
||||
return Ok(res);
|
||||
if let Some(part_filename) = &part.org_filename {
|
||||
if part_filename == &header_value {
|
||||
if let Some(blob) = part.param.get(Param::File) {
|
||||
let res = Some(AvatarAction::Change(blob.to_string()));
|
||||
self.parts.remove(i);
|
||||
return Ok(res);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
@@ -1008,14 +1007,6 @@ impl MimeMessage {
|
||||
self.headers.contains_key(hname) || self.headers_removed.contains(hname)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Returns whether the decrypted data contains the given `&str`.
|
||||
pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
|
||||
assert!(!self.decrypting_failed);
|
||||
let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
|
||||
decoded_str.contains(s)
|
||||
}
|
||||
|
||||
/// Returns `Chat-Group-ID` header value if it is a valid group ID.
|
||||
pub fn get_chat_group_id(&self) -> Option<&str> {
|
||||
self.get_header(HeaderDef::ChatGroupId)
|
||||
@@ -1332,10 +1323,6 @@ impl MimeMessage {
|
||||
let is_html = mime_type == mime::TEXT_HTML;
|
||||
if is_html {
|
||||
self.is_mime_modified = true;
|
||||
// NB: This unconditionally removes Legacy Display Elements (see
|
||||
// <https://www.rfc-editor.org/rfc/rfc9788.html#section-4.5.3.3>). We
|
||||
// don't check for the "hp-legacy-display" Content-Type parameter
|
||||
// for simplicity.
|
||||
if let Some(text) = dehtml(&decoded_data) {
|
||||
text
|
||||
} else {
|
||||
@@ -1363,30 +1350,16 @@ impl MimeMessage {
|
||||
|
||||
let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
|
||||
&& mime_type.subtype() == mime::PLAIN
|
||||
&& is_format_flowed
|
||||
{
|
||||
// Don't check that we're inside an encrypted or signed part for
|
||||
// simplicity.
|
||||
let simplified_txt = match mail
|
||||
.ctype
|
||||
.params
|
||||
.get("hp-legacy-display")
|
||||
.is_some_and(|v| v == "1")
|
||||
{
|
||||
false => simplified_txt,
|
||||
true => rm_legacy_display_elements(&simplified_txt),
|
||||
};
|
||||
if is_format_flowed {
|
||||
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
|
||||
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
|
||||
(unflowed_text, unflowed_quote)
|
||||
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
(simplified_txt, top_quote)
|
||||
}
|
||||
false
|
||||
};
|
||||
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
|
||||
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
|
||||
(unflowed_text, unflowed_quote)
|
||||
} else {
|
||||
(simplified_txt, top_quote)
|
||||
};
|
||||
@@ -1605,13 +1578,13 @@ impl MimeMessage {
|
||||
} else if let Some(sender) = self.get_header(HeaderDef::Sender) {
|
||||
// the `Sender:`-header alone is no indicator for mailing list
|
||||
// as also used for bot-impersonation via `set_override_sender_name()`
|
||||
if let Some(precedence) = self.get_header(HeaderDef::Precedence)
|
||||
&& (precedence == "list" || precedence == "bulk")
|
||||
{
|
||||
// The message belongs to a mailing list, but there is no `ListId:`-header;
|
||||
// `Sender:`-header is be used to get a unique id.
|
||||
// This method is used by implementations as Majordomo.
|
||||
return Some(sender);
|
||||
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
|
||||
if precedence == "list" || precedence == "bulk" {
|
||||
// The message belongs to a mailing list, but there is no `ListId:`-header;
|
||||
// `Sender:`-header is be used to get a unique id.
|
||||
// This method is used by implementations as Majordomo.
|
||||
return Some(sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -1652,18 +1625,13 @@ impl MimeMessage {
|
||||
remove_header(headers, "autocrypt-gossip", removed);
|
||||
|
||||
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
|
||||
if let Some(secure_join) = remove_header(headers, "secure-join", removed)
|
||||
&& (secure_join == "vc-request" || secure_join == "vg-request")
|
||||
{
|
||||
headers.insert("secure-join".to_string(), secure_join);
|
||||
if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
|
||||
if secure_join == "vc-request" || secure_join == "vg-request" {
|
||||
headers.insert("secure-join".to_string(), secure_join);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges headers from the email `part` into `headers` respecting header protection.
|
||||
/// Should only be called with nonempty `headers` if `part` is a root of the Cryptographic
|
||||
/// Payload as defined in <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for
|
||||
/// Cryptographically Protected Email", otherwise this may unnecessarily discard headers from
|
||||
/// outer parts.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn merge_headers(
|
||||
context: &Context,
|
||||
@@ -1674,17 +1642,14 @@ impl MimeMessage {
|
||||
from: &mut Option<SingleInfo>,
|
||||
list_post: &mut Option<String>,
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
part: &mailparse::ParsedMail,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
) {
|
||||
let fields = &part.headers;
|
||||
// See <https://www.rfc-editor.org/rfc/rfc9788.html>.
|
||||
let has_header_protection = part.ctype.params.contains_key("hp");
|
||||
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| has_header_protection || is_protected(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
);
|
||||
headers.retain(|k, _| {
|
||||
!is_protected(k) || {
|
||||
headers_removed.insert(k.to_string());
|
||||
false
|
||||
}
|
||||
});
|
||||
for field in fields {
|
||||
// lowercasing all headers is technically not correct, but makes things work better
|
||||
let key = field.get_key().to_lowercase();
|
||||
@@ -1889,11 +1854,12 @@ impl MimeMessage {
|
||||
.iter()
|
||||
.filter(|p| p.typ == Viewtype::Text)
|
||||
.count();
|
||||
if text_part_cnt == 2
|
||||
&& let Some(last_part) = self.parts.last()
|
||||
&& last_part.typ == Viewtype::Text
|
||||
{
|
||||
self.parts.pop();
|
||||
if text_part_cnt == 2 {
|
||||
if let Some(last_part) = self.parts.last() {
|
||||
if last_part.typ == Viewtype::Text {
|
||||
self.parts.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1946,15 +1912,15 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(delivery_report) = &self.delivery_report
|
||||
&& delivery_report.failure
|
||||
{
|
||||
let error = parts
|
||||
.iter()
|
||||
.find(|p| p.typ == Viewtype::Text)
|
||||
.map(|p| p.msg.clone());
|
||||
if let Err(err) = handle_ndn(context, delivery_report, error).await {
|
||||
warn!(context, "Could not handle NDN: {err:#}.");
|
||||
if let Some(delivery_report) = &self.delivery_report {
|
||||
if delivery_report.failure {
|
||||
let error = parts
|
||||
.iter()
|
||||
.find(|p| p.typ == Viewtype::Text)
|
||||
.map(|p| p.msg.clone());
|
||||
if let Err(err) = handle_ndn(context, delivery_report, error).await {
|
||||
warn!(context, "Could not handle NDN: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2006,20 +1972,6 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
fn rm_legacy_display_elements(text: &str) -> String {
|
||||
let mut res = None;
|
||||
for l in text.lines() {
|
||||
res = res.map(|r: String| match r.is_empty() {
|
||||
true => l.to_string(),
|
||||
false => r + "\r\n" + l,
|
||||
});
|
||||
if l.is_empty() {
|
||||
res = Some(String::new());
|
||||
}
|
||||
}
|
||||
res.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn remove_header(
|
||||
headers: &mut HashMap<String, String>,
|
||||
key: &str,
|
||||
@@ -2144,8 +2096,7 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
/// Returns whether the outer header value must be ignored if the message contains a signed (and
|
||||
/// optionally encrypted) part. This is independent from the modern Header Protection defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html>.
|
||||
/// optionally encrypted) part.
|
||||
///
|
||||
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
|
||||
/// signed-only messages. Such messages are shown as unencrypted anyway.
|
||||
@@ -2302,14 +2253,14 @@ fn get_attachment_filename(
|
||||
// `Content-Disposition: ... filename=...`
|
||||
let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
|
||||
|
||||
if desired_filename.is_none()
|
||||
&& let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
|
||||
{
|
||||
// be graceful and just use the original name.
|
||||
// some MUA, including Delta Chat up to core1.50,
|
||||
// use `filename*` mistakenly for simple encoded-words without following rfc2231
|
||||
warn!(context, "apostrophed encoding invalid: {}", name);
|
||||
desired_filename = Some(name);
|
||||
if desired_filename.is_none() {
|
||||
if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
|
||||
// be graceful and just use the original name.
|
||||
// some MUA, including Delta Chat up to core1.50,
|
||||
// use `filename*` mistakenly for simple encoded-words without following rfc2231
|
||||
warn!(context, "apostrophed encoding invalid: {}", name);
|
||||
desired_filename = Some(name);
|
||||
}
|
||||
}
|
||||
|
||||
// if no filename is set, try `Content-Disposition: ... name=...`
|
||||
@@ -2382,23 +2333,24 @@ fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<Si
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|h| h.get_key().to_lowercase() == header)
|
||||
&& let Ok(addrs) = mailparse::addrparse_header(header)
|
||||
{
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(info) => {
|
||||
result.push(SingleInfo {
|
||||
addr: addr_normalize(&info.addr).to_lowercase(),
|
||||
display_name: info.display_name.clone(),
|
||||
});
|
||||
}
|
||||
mailparse::MailAddr::Group(infos) => {
|
||||
for info in &infos.addrs {
|
||||
if let Ok(addrs) = mailparse::addrparse_header(header) {
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(info) => {
|
||||
result.push(SingleInfo {
|
||||
addr: addr_normalize(&info.addr).to_lowercase(),
|
||||
display_name: info.display_name.clone(),
|
||||
});
|
||||
}
|
||||
mailparse::MailAddr::Group(infos) => {
|
||||
for info in &infos.addrs {
|
||||
result.push(SingleInfo {
|
||||
addr: addr_normalize(&info.addr).to_lowercase(),
|
||||
display_name: info.display_name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1401,28 +1401,22 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_extra_imf_headers() -> Result<()> {
|
||||
async fn test_extra_imf_chat_header() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let chat_id = t.get_self_chat().await.id;
|
||||
|
||||
for std_hp_composing in [false, true] {
|
||||
t.set_config_bool(Config::StdHeaderProtectionComposing, std_hp_composing)
|
||||
.await?;
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
// Check removal of some nonexistent "Chat-*" header to protect the code from future
|
||||
// breakages. But headers not prefixed with "Chat-" remain unless a message has standard
|
||||
// Header Protection.
|
||||
let payload = sent_msg.payload.replace(
|
||||
"Message-ID:",
|
||||
"Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:",
|
||||
);
|
||||
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None).await?;
|
||||
assert!(msg.headers.contains_key("chat-version"));
|
||||
assert!(!msg.headers.contains_key("chat-forty-two"));
|
||||
assert_ne!(msg.headers.contains_key("forty-two"), std_hp_composing);
|
||||
}
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
|
||||
let payload = sent_msg
|
||||
.payload
|
||||
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
|
||||
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(msg.headers.contains_key("chat-version"));
|
||||
assert!(!msg.headers.contains_key("chat-forty-two"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1757,39 +1751,6 @@ async fn test_time_in_future() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_hp_legacy_display() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let mut msg = Message::new_text(
|
||||
"Subject: Dinner plans\n\
|
||||
\n\
|
||||
Let's eat"
|
||||
.to_string(),
|
||||
);
|
||||
msg.set_subject("Dinner plans".to_string());
|
||||
let chat_id = alice.create_chat(bob).await.id;
|
||||
alice.set_config_bool(Config::TestHooks, true).await?;
|
||||
*alice.pre_encrypt_mime_hook.lock() = Some(|_, mut mime| {
|
||||
for (h, v) in &mut mime.headers {
|
||||
if h == "Content-Type"
|
||||
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
|
||||
{
|
||||
*ct = ct.clone().attribute("hp-legacy-display", "1");
|
||||
}
|
||||
}
|
||||
mime
|
||||
});
|
||||
let sent_msg = alice.send_msg(chat_id, &mut msg).await;
|
||||
|
||||
let msg_bob = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg_bob.subject, "Dinner plans");
|
||||
assert_eq!(msg_bob.text, "Let's eat");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that subject is not prepended to the message
|
||||
/// when bot receives it.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
393
src/net/dns.rs
393
src/net/dns.rs
@@ -229,6 +229,64 @@ pub(crate) async fn update_connect_timestamp(
|
||||
/// Preloaded DNS results that can be used in case of DNS server failures.
|
||||
static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"mail.sangham.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"nine.testrun.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
|
||||
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
|
||||
IpAddr::V4(Ipv4Addr::new(49, 12, 116, 128)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"disroot.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
|
||||
),
|
||||
(
|
||||
"imap.gmail.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 109)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.gmail.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.autistici.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
|
||||
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.autistici.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
|
||||
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"daleth.cafe",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
|
||||
),
|
||||
(
|
||||
"imap.163.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(111, 124, 203, 45))],
|
||||
@@ -237,21 +295,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"smtp.163.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(103, 129, 252, 45))],
|
||||
),
|
||||
(
|
||||
"newyear.aktivix.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(209, 51, 180, 245))],
|
||||
),
|
||||
(
|
||||
"smtp.aliyun.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(47, 246, 136, 232))],
|
||||
),
|
||||
(
|
||||
"imap.aliyun.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(59, 82, 43, 123)),
|
||||
IpAddr::V4(Ipv4Addr::new(59, 82, 9, 176)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.aol.com",
|
||||
vec![
|
||||
@@ -259,67 +302,17 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
IpAddr::V4(Ipv4Addr::new(87, 248, 98, 69)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.arcor.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 210)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.aol.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(87, 248, 97, 31))],
|
||||
),
|
||||
(
|
||||
"mail.arcor.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
|
||||
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 206)),
|
||||
],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
|
||||
),
|
||||
(
|
||||
"mail.autistici.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
|
||||
IpAddr::V4(Ipv4Addr::new(185, 218, 207, 228)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.autistici.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
|
||||
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imaps.bluewin.ch",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(16, 62, 253, 42)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 63, 141, 244)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 63, 146, 183)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtpauths.bluewin.ch",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(16, 62, 176, 232)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 62, 15, 25)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 63, 183, 216)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.buzon.uy",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(200, 40, 115, 74))],
|
||||
),
|
||||
(
|
||||
"daleth.cafe",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
|
||||
),
|
||||
(
|
||||
"disroot.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
|
||||
"imap.arcor.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
|
||||
),
|
||||
(
|
||||
"imap.fastmail.com",
|
||||
@@ -335,39 +328,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 60)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.gmail.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 109)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.ecloud.global",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 246, 96))],
|
||||
),
|
||||
(
|
||||
"mail.ende.in.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 5, 72))],
|
||||
),
|
||||
(
|
||||
"smtp.gmail.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.gmx.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 190)),
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 168)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.gmx.net",
|
||||
vec![
|
||||
@@ -375,13 +335,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 186)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.sangham.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.mail.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 16))],
|
||||
@@ -396,7 +349,7 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
),
|
||||
(
|
||||
"imap.naver.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 233, 34))],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 238, 153))],
|
||||
),
|
||||
(
|
||||
"imap.ouvaton.coop",
|
||||
@@ -406,57 +359,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"imap.purelymail.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(18, 204, 123, 63))],
|
||||
),
|
||||
(
|
||||
"mail.systemausfall.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(51, 75, 71, 249)),
|
||||
IpAddr::V4(Ipv4Addr::new(80, 153, 252, 42)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.systemli.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(93, 190, 126, 36))],
|
||||
),
|
||||
("testrun.org", vec![IpAddr::V4(Ipv4Addr::new(5, 1, 76, 52))]),
|
||||
(
|
||||
"nine.testrun.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
|
||||
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
|
||||
IpAddr::V4(Ipv4Addr::new(216, 144, 228, 100)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
|
||||
IpAddr::V6(Ipv6Addr::new(
|
||||
0x2001, 0x41d0, 0x701, 0x1100, 0, 0, 0, 0x8ab1,
|
||||
)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"secureimap.t-online.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 114)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 115)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 50)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 51)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"securesmtp.t-online.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 46)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 110)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.riseup.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 171)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 170)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"pimap.schulon.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
|
||||
),
|
||||
(
|
||||
"imap.tiscali.it",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 10))],
|
||||
@@ -465,14 +367,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"smtp.tiscali.it",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 13))],
|
||||
),
|
||||
(
|
||||
"imap.ukr.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(212, 42, 75, 240))],
|
||||
),
|
||||
(
|
||||
"smtp.ukr.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(212, 42, 75, 250))],
|
||||
),
|
||||
(
|
||||
"imap.web.de",
|
||||
vec![
|
||||
@@ -486,9 +380,33 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
),
|
||||
(
|
||||
"imap.zoho.eu",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 214, 25))],
|
||||
),
|
||||
(
|
||||
"imaps.bluewin.ch",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 25)),
|
||||
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 206)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 62, 253, 42)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 63, 141, 244)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 63, 146, 183)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.buzon.uy",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 101, 93, 79))],
|
||||
),
|
||||
(
|
||||
"mail.ecloud.global",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 246, 96))],
|
||||
),
|
||||
(
|
||||
"mail.ende.in.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 5, 72))],
|
||||
),
|
||||
(
|
||||
"mail.gmx.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 168)),
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 190)),
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -506,6 +424,24 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"mail.nubo.coop",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
|
||||
),
|
||||
(
|
||||
"mail.riseup.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.systemausfall.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(51, 75, 71, 249)),
|
||||
IpAddr::V4(Ipv4Addr::new(80, 153, 252, 42)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.systemli.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(93, 190, 126, 36))],
|
||||
),
|
||||
(
|
||||
"mehl.cloud",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
|
||||
@@ -513,14 +449,20 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
(
|
||||
"mx.freenet.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 36)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 34)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 35)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 39)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 37)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 38)),
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 210)),
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 211)),
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 212)),
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 213)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"newyear.aktivix.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(162, 247, 75, 192))],
|
||||
),
|
||||
(
|
||||
"pimap.schulon.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
|
||||
),
|
||||
(
|
||||
"posteo.de",
|
||||
vec![
|
||||
@@ -532,6 +474,26 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"psmtp.schulon.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
|
||||
),
|
||||
(
|
||||
"secureimap.t-online.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 114)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 115)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 50)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 51)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"securesmtp.t-online.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 110)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 46)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.aliyun.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(47, 246, 136, 232))],
|
||||
),
|
||||
(
|
||||
"smtp.mail.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 21))],
|
||||
@@ -539,8 +501,8 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
(
|
||||
"smtp.mail.ru",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(94, 100, 180, 160)),
|
||||
IpAddr::V4(Ipv4Addr::new(217, 69, 139, 160)),
|
||||
IpAddr::V4(Ipv4Addr::new(94, 100, 180, 160)),
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -558,20 +520,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"imap.mailo.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
|
||||
),
|
||||
(
|
||||
"imap.migadu.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(51, 210, 3, 23)),
|
||||
IpAddr::V4(Ipv4Addr::new(51, 210, 3, 20)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.migadu.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(37, 59, 57, 117)),
|
||||
IpAddr::V4(Ipv4Addr::new(51, 255, 82, 75)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.mailo.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
|
||||
@@ -590,48 +538,30 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
),
|
||||
(
|
||||
"imap.qq.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(43, 163, 178, 76)),
|
||||
IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54)),
|
||||
],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
|
||||
),
|
||||
(
|
||||
"smtp.qq.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54)),
|
||||
IpAddr::V4(Ipv4Addr::new(43, 163, 178, 76)),
|
||||
],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
|
||||
),
|
||||
(
|
||||
"imap.rambler.ru",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 168)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 169)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 170)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 171)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 168)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 170)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.rambler.ru",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 164)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 165)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 166)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 167)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 166)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 164)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"stinpriza.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(5, 9, 122, 184))],
|
||||
),
|
||||
(
|
||||
"webbox222.server-home.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(91, 203, 111, 88))],
|
||||
),
|
||||
(
|
||||
"undernet.uy",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(200, 40, 115, 74))],
|
||||
),
|
||||
(
|
||||
"imap.vivaldi.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(31, 209, 137, 15))],
|
||||
@@ -642,23 +572,17 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
),
|
||||
(
|
||||
"imap.vodafonemail.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 210)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
|
||||
],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
|
||||
),
|
||||
(
|
||||
"smtp.vodafonemail.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
|
||||
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 206)),
|
||||
],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
|
||||
),
|
||||
(
|
||||
"smtp.web.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 124)),
|
||||
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 124)),
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -675,10 +599,23 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
),
|
||||
(
|
||||
"smtp.zoho.eu",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(185, 230, 212, 164)),
|
||||
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 164)),
|
||||
],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 212, 164))],
|
||||
),
|
||||
(
|
||||
"smtpauths.bluewin.ch",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(195, 186, 120, 54))],
|
||||
),
|
||||
(
|
||||
"stinpriza.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(5, 9, 122, 184))],
|
||||
),
|
||||
(
|
||||
"undernet.uy",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(167, 62, 254, 153))],
|
||||
),
|
||||
(
|
||||
"webbox222.server-home.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(91, 203, 111, 88))],
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
@@ -139,10 +139,10 @@ pub(crate) async fn get_oauth2_access_token(
|
||||
value = &redirect_uri;
|
||||
} else if value == "$CODE" {
|
||||
value = code;
|
||||
} else if value == "$REFRESH_TOKEN"
|
||||
&& let Some(refresh_token) = refresh_token.as_ref()
|
||||
{
|
||||
value = refresh_token;
|
||||
} else if value == "$REFRESH_TOKEN" {
|
||||
if let Some(refresh_token) = refresh_token.as_ref() {
|
||||
value = refresh_token;
|
||||
}
|
||||
}
|
||||
|
||||
post_param.insert(key, value);
|
||||
@@ -261,12 +261,14 @@ impl Oauth2 {
|
||||
if let Some(domain) = addr_normalized
|
||||
.find('@')
|
||||
.map(|index| addr_normalized.split_at(index + 1).1)
|
||||
&& let Some(oauth2_authorizer) = provider::get_provider_info(domain)
|
||||
.and_then(|provider| provider.oauth2_authorizer.as_ref())
|
||||
{
|
||||
return Some(match oauth2_authorizer {
|
||||
Oauth2Authorizer::Yandex => OAUTH2_YANDEX,
|
||||
});
|
||||
if let Some(oauth2_authorizer) = provider::get_provider_info(domain)
|
||||
.and_then(|provider| provider.oauth2_authorizer.as_ref())
|
||||
{
|
||||
return Some(match oauth2_authorizer {
|
||||
Oauth2Authorizer::Yandex => OAUTH2_YANDEX,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
59
src/pgp.rs
59
src/pgp.rs
@@ -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?;
|
||||
|
||||
106
src/qr.rs
106
src/qr.rs
@@ -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)]
|
||||
@@ -233,31 +233,6 @@ pub enum Qr {
|
||||
authcode: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to withdraw their own broadcast channel invite QR code.
|
||||
WithdrawJoinBroadcast {
|
||||
/// The user-visible name of this broadcast channel
|
||||
name: String,
|
||||
|
||||
/// A string of random characters,
|
||||
/// uniquely identifying this broadcast channel across all databases/clients.
|
||||
/// Called `grpid` for historic reasons:
|
||||
/// The id of multi-user chats is always called `grpid` in the database
|
||||
/// because groups were once the only multi-user chats.
|
||||
grpid: String,
|
||||
|
||||
/// Contact ID. Always `ContactId::SELF`.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Fingerprint of the contact's key as scanned from the QR code.
|
||||
fingerprint: Fingerprint,
|
||||
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to revive their own QR code.
|
||||
ReviveVerifyContact {
|
||||
/// Contact ID.
|
||||
@@ -294,31 +269,6 @@ pub enum Qr {
|
||||
authcode: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to revive their own broadcast channel invite QR code.
|
||||
ReviveJoinBroadcast {
|
||||
/// The user-visible name of this broadcast channel
|
||||
name: String,
|
||||
|
||||
/// A string of random characters,
|
||||
/// uniquely identifying this broadcast channel across all databases/clients.
|
||||
/// Called `grpid` for historic reasons:
|
||||
/// The id of multi-user chats is always called `grpid` in the database
|
||||
/// because groups were once the only multi-user chats.
|
||||
grpid: String,
|
||||
|
||||
/// Contact ID. Always `ContactId::SELF`.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Fingerprint of the contact's key as scanned from the QR code.
|
||||
fingerprint: Fingerprint,
|
||||
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
|
||||
/// `dclogin:` scheme parameters.
|
||||
///
|
||||
/// Ask the user if they want to login with the email address.
|
||||
@@ -550,40 +500,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
}
|
||||
} else if let (Some(grpid), Some(name)) = (grpid, broadcast_name) {
|
||||
if context
|
||||
.is_self_addr(&addr)
|
||||
.await
|
||||
.with_context(|| format!("Can't check if {addr:?} is our address"))?
|
||||
{
|
||||
if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? {
|
||||
Ok(Qr::WithdrawJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
})
|
||||
} else {
|
||||
Ok(Qr::ReviveJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(Qr::AskJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
})
|
||||
}
|
||||
Ok(Qr::AskJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
})
|
||||
} else if context.is_self_addr(&addr).await? {
|
||||
if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? {
|
||||
Ok(Qr::WithdrawVerifyContact {
|
||||
@@ -876,12 +800,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
invitenumber,
|
||||
authcode,
|
||||
..
|
||||
}
|
||||
| Qr::WithdrawJoinBroadcast {
|
||||
grpid,
|
||||
invitenumber,
|
||||
authcode,
|
||||
..
|
||||
} => {
|
||||
token::delete(context, &grpid).await?;
|
||||
context
|
||||
@@ -911,12 +829,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
authcode,
|
||||
grpid,
|
||||
..
|
||||
}
|
||||
| Qr::ReviveJoinBroadcast {
|
||||
invitenumber,
|
||||
authcode,
|
||||
grpid,
|
||||
..
|
||||
} => {
|
||||
let timestamp = time();
|
||||
token::save(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::chat::{Chat, create_broadcast, create_group, get_chat_contacts};
|
||||
use crate::chat::create_group;
|
||||
use crate::config::Config;
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
use crate::provider::Socket;
|
||||
@@ -511,56 +511,6 @@ async fn test_withdraw_verifygroup() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_withdraw_joinbroadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(chat_id)).await?;
|
||||
|
||||
// scanning own verify-group code offers withdrawing
|
||||
if let Qr::WithdrawJoinBroadcast { name, .. } = check_qr(alice, &qr).await? {
|
||||
assert_eq!(name, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected WithdrawJoinBroadcast");
|
||||
}
|
||||
set_config_from_qr(alice, &qr).await?;
|
||||
|
||||
// scanning withdrawn verify-group code offers reviving
|
||||
if let Qr::ReviveJoinBroadcast { name, .. } = check_qr(alice, &qr).await? {
|
||||
assert_eq!(name, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected ReviveJoinBroadcast");
|
||||
}
|
||||
|
||||
// someone else always scans as ask-verify-group
|
||||
if let Qr::AskJoinBroadcast { name, .. } = check_qr(bob, &qr).await? {
|
||||
assert_eq!(name, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected AskJoinBroadcast");
|
||||
}
|
||||
assert!(set_config_from_qr(bob, &qr).await.is_err());
|
||||
|
||||
// Bob can't join using this QR code, since it's still withdrawn
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.is_self_in_chat(bob).await?, false);
|
||||
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 0);
|
||||
|
||||
// Revive
|
||||
set_config_from_qr(alice, &qr).await?;
|
||||
|
||||
// Now Bob can join
|
||||
let bob_chat_id2 = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
assert_eq!(bob_chat_id, bob_chat_id2);
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.is_self_in_chat(bob).await?, true);
|
||||
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_withdraw_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Internet Message Format reception pipeline.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::iter;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -884,26 +884,53 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar
|
||||
&& !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
|
||||
&& 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
|
||||
{
|
||||
warn!(context, "receive_imf cannot update profile image: {err:#}.");
|
||||
};
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||
if from_id != ContactId::UNDEFINED
|
||||
&& context
|
||||
.update_contacts_timestamp(
|
||||
from_id,
|
||||
Param::AvatarTimestamp,
|
||||
mime_parser.timestamp_sent,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if 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:#}.");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
&& 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
|
||||
{
|
||||
warn!(context, "Cannot update contact status: {err:#}.");
|
||||
if let Some(footer) = &mime_parser.footer {
|
||||
if !mime_parser.is_mailinglist_message()
|
||||
&& from_id != ContactId::UNDEFINED
|
||||
&& context
|
||||
.update_contacts_timestamp(
|
||||
from_id,
|
||||
Param::StatusTimestamp,
|
||||
mime_parser.timestamp_sent,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if 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:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get user-configured server deletion
|
||||
@@ -1178,19 +1205,19 @@ async fn decide_chat_assignment(
|
||||
let mut num_recipients = 0;
|
||||
let mut has_self_addr = false;
|
||||
for recipient in &mime_parser.recipients {
|
||||
has_self_addr |= context.is_self_addr(&recipient.addr).await?;
|
||||
if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if context.is_self_addr(&recipient.addr).await? {
|
||||
has_self_addr = true;
|
||||
}
|
||||
|
||||
num_recipients += 1;
|
||||
}
|
||||
if from_id != ContactId::SELF && !has_self_addr {
|
||||
num_recipients += 1;
|
||||
}
|
||||
let can_be_11_chat = num_recipients <= 1
|
||||
&& (from_id != ContactId::SELF
|
||||
|| !(mime_parser.recipients.is_empty() || has_self_addr)
|
||||
|| mime_parser.was_encrypted());
|
||||
|
||||
let chat_assignment = if should_trash {
|
||||
ChatAssignment::Trash
|
||||
@@ -1234,14 +1261,14 @@ async fn decide_chat_assignment(
|
||||
}
|
||||
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
|
||||
ChatAssignment::AdHocGroup
|
||||
} else if can_be_11_chat {
|
||||
} else if num_recipients <= 1 {
|
||||
ChatAssignment::OneOneChat
|
||||
} else {
|
||||
ChatAssignment::AdHocGroup
|
||||
}
|
||||
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
|
||||
ChatAssignment::AdHocGroup
|
||||
} else if can_be_11_chat {
|
||||
} else if num_recipients <= 1 {
|
||||
ChatAssignment::OneOneChat
|
||||
} else {
|
||||
ChatAssignment::AdHocGroup
|
||||
@@ -1314,8 +1341,8 @@ async fn do_chat_assignment(
|
||||
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
chat_id = Some(id);
|
||||
chat_id_blocked = blocked;
|
||||
} else if (allow_creation || test_normal_chat.is_some())
|
||||
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group(
|
||||
} else if allow_creation || test_normal_chat.is_some() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
@@ -1326,15 +1353,16 @@ async fn do_chat_assignment(
|
||||
grpid,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
chat_created = true;
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
chat_created = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatAssignment::MailingListOrBroadcast => {
|
||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
|
||||
&& let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
|
||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
|
||||
create_or_lookup_mailinglist_or_broadcast(
|
||||
context,
|
||||
allow_creation,
|
||||
@@ -1344,12 +1372,13 @@ async fn do_chat_assignment(
|
||||
mime_parser,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
chat_created = new_chat_created;
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
chat_created = new_chat_created;
|
||||
|
||||
apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
|
||||
apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatAssignment::ExistingChat {
|
||||
@@ -1384,10 +1413,11 @@ async fn do_chat_assignment(
|
||||
if chat_id_blocked != Blocked::Not
|
||||
&& create_blocked != Blocked::Yes
|
||||
&& !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
|
||||
&& let Some(chat_id) = chat_id
|
||||
{
|
||||
chat_id.set_blocked(context, create_blocked).await?;
|
||||
chat_id_blocked = create_blocked;
|
||||
if let Some(chat_id) = chat_id {
|
||||
chat_id.set_blocked(context, create_blocked).await?;
|
||||
chat_id_blocked = create_blocked;
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
@@ -1411,20 +1441,21 @@ async fn do_chat_assignment(
|
||||
chat_created = true;
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id
|
||||
&& chat_id_blocked != Blocked::Not
|
||||
{
|
||||
if chat_id_blocked != create_blocked {
|
||||
chat_id.set_blocked(context, create_blocked).await?;
|
||||
}
|
||||
if create_blocked == Blocked::Request && parent_message.is_some() {
|
||||
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
|
||||
// the contact requests will pop up and this should be just fine.
|
||||
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
|
||||
info!(
|
||||
context,
|
||||
"Message is a reply to a known message, mark sender as known.",
|
||||
);
|
||||
if let Some(chat_id) = chat_id {
|
||||
if chat_id_blocked != Blocked::Not {
|
||||
if chat_id_blocked != create_blocked {
|
||||
chat_id.set_blocked(context, create_blocked).await?;
|
||||
}
|
||||
if create_blocked == Blocked::Request && parent_message.is_some() {
|
||||
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
|
||||
// the contact requests will pop up and this should be just fine.
|
||||
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"Message is a reply to a known message, mark sender as known.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1445,8 +1476,8 @@ async fn do_chat_assignment(
|
||||
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
chat_id = Some(id);
|
||||
chat_id_blocked = blocked;
|
||||
} else if allow_creation
|
||||
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group(
|
||||
} else if allow_creation {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
@@ -1457,10 +1488,11 @@ async fn do_chat_assignment(
|
||||
grpid,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
chat_created = true;
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
chat_created = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatAssignment::ExistingChat {
|
||||
@@ -1542,12 +1574,11 @@ async fn do_chat_assignment(
|
||||
chat_created = true;
|
||||
}
|
||||
}
|
||||
if chat_id.is_none()
|
||||
&& mime_parser.has_chat_version()
|
||||
&& let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
|
||||
{
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
if chat_id.is_none() && mime_parser.has_chat_version() {
|
||||
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1567,11 +1598,11 @@ async fn do_chat_assignment(
|
||||
}
|
||||
|
||||
// automatically unblock chat when the user sends a message
|
||||
if chat_id_blocked != Blocked::Not
|
||||
&& let Some(chat_id) = chat_id
|
||||
{
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id_blocked = Blocked::Not;
|
||||
if chat_id_blocked != Blocked::Not {
|
||||
if let Some(chat_id) = chat_id {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
}
|
||||
let chat_id = chat_id.unwrap_or_else(|| {
|
||||
@@ -1609,9 +1640,11 @@ async fn add_parts(
|
||||
|
||||
// if contact renaming is prevented (for mailinglists and bots),
|
||||
// we use name from From:-header as override name
|
||||
if prevent_rename && let Some(name) = &mime_parser.from.display_name {
|
||||
for part in &mut mime_parser.parts {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
if prevent_rename {
|
||||
if let Some(name) = &mime_parser.from.display_name {
|
||||
for part in &mut mime_parser.parts {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1768,16 +1801,11 @@ async fn add_parts(
|
||||
"Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
|
||||
);
|
||||
if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
|
||||
chat::add_info_msg_with_cmd(
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat_id,
|
||||
&stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
SystemMessage::Unknown,
|
||||
Some(sort_timestamp),
|
||||
mime_parser.timestamp_sent,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
sort_timestamp,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1885,8 +1913,8 @@ async fn add_parts(
|
||||
chat_id,
|
||||
&group_changes_msg,
|
||||
cmd,
|
||||
Some(sort_timestamp),
|
||||
mime_parser.timestamp_sent,
|
||||
sort_timestamp,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
added_removed_id,
|
||||
@@ -2242,36 +2270,36 @@ async fn handle_edit_delete(
|
||||
"Edit message: rfc724_mid {rfc724_mid:?} not found."
|
||||
);
|
||||
}
|
||||
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
|
||||
&& let Some(part) = mime_parser.parts.first()
|
||||
{
|
||||
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
|
||||
// deletion requests, so there's no need to support them.
|
||||
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
|
||||
if let Some(part) = mime_parser.parts.first() {
|
||||
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
|
||||
// deletion requests, so there's no need to support them.
|
||||
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
for rfc724_mid in rfc724_mid_vec {
|
||||
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
if msg.from_id == from_id {
|
||||
message::delete_msg_locally(context, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
for rfc724_mid in rfc724_mid_vec {
|
||||
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
if msg.from_id == from_id {
|
||||
message::delete_msg_locally(context, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
} else {
|
||||
warn!(context, "Delete message: Bad sender.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Delete message: Bad sender.");
|
||||
warn!(context, "Delete message: Database entry does not exist.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Delete message: Database entry does not exist.");
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
} else {
|
||||
warn!(context, "Delete message: Not encrypted.");
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
} else {
|
||||
warn!(context, "Delete message: Not encrypted.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -2328,32 +2356,33 @@ async fn save_locations(
|
||||
|
||||
let mut send_event = false;
|
||||
|
||||
if let Some(message_kml) = &mime_parser.message_kml
|
||||
&& let Some(newest_location_id) =
|
||||
if let Some(message_kml) = &mime_parser.message_kml {
|
||||
if let Some(newest_location_id) =
|
||||
location::save(context, chat_id, from_id, &message_kml.locations, true).await?
|
||||
{
|
||||
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
|
||||
send_event = true;
|
||||
{
|
||||
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
|
||||
send_event = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(location_kml) = &mime_parser.location_kml
|
||||
&& let Some(addr) = &location_kml.addr
|
||||
{
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
|
||||
if location::save(context, chat_id, from_id, &location_kml.locations, false)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
send_event = true;
|
||||
if let Some(location_kml) = &mime_parser.location_kml {
|
||||
if let Some(addr) = &location_kml.addr {
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
|
||||
if location::save(context, chat_id, from_id, &location_kml.locations, false)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
send_event = true;
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Address in location.kml {:?} is not the same as the sender address {:?}.",
|
||||
addr,
|
||||
contact.get_addr()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Address in location.kml {:?} is not the same as the sender address {:?}.",
|
||||
addr,
|
||||
contact.get_addr()
|
||||
);
|
||||
}
|
||||
}
|
||||
if send_event {
|
||||
@@ -2462,8 +2491,11 @@ async fn lookup_or_create_adhoc_group(
|
||||
.unwrap_or_else(|| "👥📧".to_string())
|
||||
});
|
||||
let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
|
||||
let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
|
||||
contact_ids.insert(from_id);
|
||||
let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
|
||||
contact_ids.extend(&to_ids);
|
||||
if !contact_ids.contains(&from_id) {
|
||||
contact_ids.push(from_id);
|
||||
}
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
t.pragma_update(None, "query_only", "0")?;
|
||||
t.execute(
|
||||
@@ -2702,13 +2734,13 @@ async fn update_chats_contacts_timestamps(
|
||||
to_ids.iter(),
|
||||
chat_group_member_timestamps.iter().take(to_ids.len()),
|
||||
) {
|
||||
if let Some(contact_id) = contact_id
|
||||
&& Some(*contact_id) != ignored_id
|
||||
{
|
||||
// It could be that member was already added,
|
||||
// but updated addition timestamp
|
||||
// is also a modification worth notifying about.
|
||||
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
|
||||
if let Some(contact_id) = contact_id {
|
||||
if Some(*contact_id) != ignored_id {
|
||||
// It could be that member was already added,
|
||||
// but updated addition timestamp
|
||||
// is also a modification worth notifying about.
|
||||
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2971,14 +3003,13 @@ async fn apply_group_changes(
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
if let Some(added_id) = added_id
|
||||
&& !added_ids.remove(&added_id)
|
||||
&& added_id != ContactId::SELF
|
||||
{
|
||||
// No-op "Member added" message. An exception is self-addition messages because they at
|
||||
// least must be shown when a chat is created on our side.
|
||||
info!(context, "No-op 'Member added' message (TRASH)");
|
||||
better_msg = Some(String::new());
|
||||
if let Some(added_id) = added_id {
|
||||
if !added_ids.remove(&added_id) && added_id != ContactId::SELF {
|
||||
// No-op "Member added" message. An exception is self-addition messages because they at
|
||||
// least must be shown when a chat is created on our side.
|
||||
info!(context, "No-op 'Member added' message (TRASH)");
|
||||
better_msg = Some(String::new());
|
||||
}
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
removed_ids.remove(&removed_id);
|
||||
@@ -3033,52 +3064,59 @@ async fn apply_chat_name_and_avatar_changes(
|
||||
Some(_) => Some(chat.name.as_str()),
|
||||
None => None,
|
||||
})
|
||||
&& let Some(grpname) = mime_parser
|
||||
{
|
||||
if let Some(grpname) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.map(|grpname| grpname.trim())
|
||||
.filter(|grpname| grpname.len() < 200)
|
||||
{
|
||||
let grpname = &sanitize_single_line(grpname);
|
||||
{
|
||||
let grpname = &sanitize_single_line(grpname);
|
||||
|
||||
let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
|
||||
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
|
||||
// To provide group name consistency, compare names if timestamps are equal.
|
||||
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
|
||||
&& chat
|
||||
.id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
|
||||
.await?
|
||||
&& grpname != &chat.name
|
||||
{
|
||||
info!(context, "Updating grpname for chat {}.", chat.id);
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
|
||||
.await?;
|
||||
*send_event_chat_modified = true;
|
||||
}
|
||||
if mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.is_some()
|
||||
{
|
||||
let old_name = &sanitize_single_line(old_name);
|
||||
better_msg
|
||||
.get_or_insert(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
let chat_group_name_timestamp =
|
||||
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
|
||||
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
|
||||
// To provide group name consistency, compare names if timestamps are equal.
|
||||
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
|
||||
&& chat
|
||||
.id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
|
||||
.await?
|
||||
&& grpname != &chat.name
|
||||
{
|
||||
info!(context, "Updating grpname for chat {}.", chat.id);
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
|
||||
.await?;
|
||||
*send_event_chat_modified = true;
|
||||
}
|
||||
if mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.is_some()
|
||||
{
|
||||
let old_name = &sanitize_single_line(old_name);
|
||||
better_msg.get_or_insert(
|
||||
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Apply chat avatar changes ==========
|
||||
|
||||
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
|
||||
&& value == "group-avatar-changed"
|
||||
&& let Some(avatar_action) = &mime_parser.group_avatar
|
||||
{
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
better_msg.get_or_insert(match avatar_action {
|
||||
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
|
||||
AvatarAction::Change(_) => stock_str::msg_grp_img_changed(context, from_id).await,
|
||||
});
|
||||
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
|
||||
if value == "group-avatar-changed" {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
better_msg.get_or_insert(match avatar_action {
|
||||
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
|
||||
AvatarAction::Change(_) => {
|
||||
stock_str::msg_grp_img_changed(context, from_id).await
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
@@ -3259,10 +3297,10 @@ fn compute_mailinglist_name(
|
||||
// for mailchimp lists, the name in `ListId` is just a long number.
|
||||
// a usable name for these lists is in the `From` header
|
||||
// and we can detect these lists by a unique `ListId`-suffix.
|
||||
if listid.ends_with(".list-id.mcsv.net")
|
||||
&& let Some(display_name) = &mime_parser.from.display_name
|
||||
{
|
||||
name.clone_from(display_name);
|
||||
if listid.ends_with(".list-id.mcsv.net") {
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name.clone_from(display_name);
|
||||
}
|
||||
}
|
||||
|
||||
// additional names in square brackets in the subject are preferred
|
||||
@@ -3287,9 +3325,10 @@ fn compute_mailinglist_name(
|
||||
|| mime_parser.from.addr.starts_with("notifications@")
|
||||
|| mime_parser.from.addr.starts_with("newsletter@")
|
||||
|| listid.ends_with(".xt.local"))
|
||||
&& let Some(display_name) = &mime_parser.from.display_name
|
||||
{
|
||||
name.clone_from(display_name);
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name.clone_from(display_name);
|
||||
}
|
||||
}
|
||||
|
||||
// as a last resort, use the ListId as the name
|
||||
@@ -3437,15 +3476,15 @@ async fn apply_out_broadcast_changes(
|
||||
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
|
||||
info!(context, "Broadcast leave message (TRASH)");
|
||||
better_msg = Some("".to_string());
|
||||
} else if from_id == ContactId::SELF
|
||||
&& let Some(removed_id) = removed_id
|
||||
{
|
||||
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
|
||||
.await?;
|
||||
} else if from_id == ContactId::SELF {
|
||||
if let Some(removed_id) = removed_id {
|
||||
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
|
||||
.await?;
|
||||
|
||||
better_msg.get_or_insert(
|
||||
stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
|
||||
);
|
||||
better_msg.get_or_insert(
|
||||
stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3469,14 +3508,14 @@ async fn apply_in_broadcast_changes(
|
||||
) -> Result<GroupChangesInfo> {
|
||||
ensure!(chat.typ == Chattype::InBroadcast);
|
||||
|
||||
if let Some(part) = mime_parser.parts.first()
|
||||
&& let Some(error) = &part.error
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Not applying broadcast changes from message with error: {error}"
|
||||
);
|
||||
return Ok(GroupChangesInfo::default());
|
||||
if let Some(part) = mime_parser.parts.first() {
|
||||
if let Some(error) = &part.error {
|
||||
warn!(
|
||||
context,
|
||||
"Not applying broadcast changes from message with error: {error}"
|
||||
);
|
||||
return Ok(GroupChangesInfo::default());
|
||||
}
|
||||
}
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
@@ -3492,21 +3531,21 @@ async fn apply_in_broadcast_changes(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
|
||||
&& context.is_self_addr(added_addr).await?
|
||||
{
|
||||
let msg = if chat.is_self_in_chat(context).await? {
|
||||
// Self is already in the chat.
|
||||
// Probably Alice has two devices and her second device added us again;
|
||||
// just hide the message.
|
||||
info!(context, "No-op broadcast 'Member added' message (TRASH)");
|
||||
"".to_string()
|
||||
} else {
|
||||
stock_str::msg_you_joined_broadcast(context).await
|
||||
};
|
||||
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
if context.is_self_addr(added_addr).await? {
|
||||
let msg = if chat.is_self_in_chat(context).await? {
|
||||
// Self is already in the chat.
|
||||
// Probably Alice has two devices and her second device added us again;
|
||||
// just hide the message.
|
||||
info!(context, "No-op broadcast 'Member added' message (TRASH)");
|
||||
"".to_string()
|
||||
} else {
|
||||
stock_str::msg_you_joined_broadcast(context).await
|
||||
};
|
||||
|
||||
better_msg.get_or_insert(msg);
|
||||
send_event_chat_modified = true;
|
||||
better_msg.get_or_insert(msg);
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
|
||||
@@ -3589,6 +3628,13 @@ async fn create_adhoc_group(
|
||||
);
|
||||
return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
|
||||
}
|
||||
if member_ids.len() < 2 {
|
||||
info!(
|
||||
context,
|
||||
"Not creating ad hoc group with less than 2 members."
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let new_chat_id: ChatId = ChatId::create_multiuser_record(
|
||||
context,
|
||||
@@ -3710,11 +3756,12 @@ async fn get_previous_message(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
) -> Result<Option<Message>> {
|
||||
if let Some(field) = mime_parser.get_header(HeaderDef::References)
|
||||
&& let Some(rfc724mid) = parse_message_ids(field).last()
|
||||
&& let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
|
||||
{
|
||||
return Message::load_from_db_optional(context, msg_id).await;
|
||||
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
|
||||
if let Some(rfc724mid) = parse_message_ids(field).last() {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await? {
|
||||
return Message::load_from_db_optional(context, msg_id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -1964,53 +1964,28 @@ Message content",
|
||||
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
|
||||
}
|
||||
|
||||
/// Tests that an outgoing self-sent unencrypted message doesn't go to the self-chat, but to a
|
||||
/// proper unencrypted chat instead.
|
||||
/// Tests that message with hidden recipients is assigned to Saved Messages chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unencrypted_doesnt_goto_self_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let mut chat_id = None;
|
||||
async fn test_hidden_recipients_self_chat() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
for (i, to) in [
|
||||
"<alice@example.org>",
|
||||
"<alice@example.org>",
|
||||
"alice@example.org, alice@example.org",
|
||||
"hidden-recipients:;",
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
receive_imf(
|
||||
t,
|
||||
format!(
|
||||
"Subject: s
|
||||
receive_imf(
|
||||
&t,
|
||||
b"Subject: s
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <foobar{i}@localhost>
|
||||
To: {to}
|
||||
Message-ID: <foobar@localhost>
|
||||
To: hidden-recipients:;
|
||||
From: <alice@example.org>
|
||||
|
||||
Your server is hacked. Have a nice day!"
|
||||
)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Message content",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
assert_eq!(msg.to_id, ContactId::SELF);
|
||||
if let Some(chat_id) = chat_id {
|
||||
assert_eq!(msg.chat_id, chat_id);
|
||||
} else {
|
||||
chat_id = Some(msg.chat_id);
|
||||
let chat = Chat::load_from_db(t, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert!(!chat.is_encrypted(t).await?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, t.get_self_chat().await.id);
|
||||
assert_eq!(msg.to_id, ContactId::SELF);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -5011,7 +4986,7 @@ async fn test_make_n_send_vcard() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that an ad-hoc group is created if the message
|
||||
/// Tests that group is not created if the message
|
||||
/// has no recipients even if it has unencrypted Chat-Group-ID.
|
||||
///
|
||||
/// Chat-Group-ID in unencrypted messages should be ignored.
|
||||
@@ -5030,12 +5005,8 @@ Hello!"
|
||||
.as_bytes();
|
||||
let received = receive_imf(t, raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
assert_eq!(msg.to_id, ContactId::SELF);
|
||||
let chat = Chat::load_from_db(t, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert!(!chat.is_encrypted(t).await?);
|
||||
assert!(chat.grpid.is_empty());
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
|
||||
// Check that the weird group name is sanitzied correctly:
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
@@ -5046,7 +5017,7 @@ Hello!"
|
||||
.get_value_raw(),
|
||||
"Group\n name\u{202B}".as_bytes()
|
||||
);
|
||||
assert_eq!(chat.name, "Group name");
|
||||
assert_eq!(chat.name, "Saved messages");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5183,58 +5154,6 @@ async fn test_dont_reverify_by_self_on_outgoing_msg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let a0 = &tcm.alice().await;
|
||||
let a1 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_chat_id = chat::create_group(bob, "Group").await?;
|
||||
bob.set_chat_protected(bob_chat_id).await;
|
||||
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(a0, bob, &qr).await;
|
||||
|
||||
let qr = get_securejoin_qr(bob, None).await?;
|
||||
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
|
||||
|
||||
// Bob verifies Fiona for Alice#0.
|
||||
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
add_contact_to_chat(bob, bob_chat_id, bob_fiona_id).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
a0.recv_msg(&sent_msg).await;
|
||||
fiona.recv_msg(&sent_msg).await;
|
||||
let a0_bob = a0.add_or_lookup_contact(bob).await;
|
||||
let a0_fiona = a0.add_or_lookup_contact(fiona).await;
|
||||
assert_eq!(a0_fiona.get_verifier_id(a0).await?, Some(Some(a0_bob.id)));
|
||||
|
||||
let chat_id = a0.create_group_with_members("", &[fiona]).await;
|
||||
a0.set_chat_protected(chat_id).await;
|
||||
a1.recv_msg(&a0.send_text(chat_id, "Hi").await).await;
|
||||
let a1_fiona = a1.add_or_lookup_contact(fiona).await;
|
||||
assert_eq!(a1_fiona.get_verifier_id(a1).await?, Some(None));
|
||||
|
||||
let some_time_to_regossip = Duration::from_secs(20 * 24 * 3600);
|
||||
SystemTime::shift(some_time_to_regossip);
|
||||
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
|
||||
fiona.set_chat_protected(fiona_chat_id).await;
|
||||
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
|
||||
.await;
|
||||
let a1_bob = a1.add_or_lookup_contact(bob).await;
|
||||
// There was a bug that Bob is verified by Fiona on Alice's other device.
|
||||
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(None));
|
||||
|
||||
SystemTime::shift(some_time_to_regossip);
|
||||
tcm.execute_securejoin(a1, fiona).await;
|
||||
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
|
||||
.await;
|
||||
// But now Bob's verifier id must be updated because Fiona is verified by a known verifier
|
||||
// (moreover, directly), so Alice has reverse verification chains on her devices.
|
||||
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(Some(a1_fiona.id)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sanitize_filename_in_received() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
|
||||
133
src/scheduler.rs
133
src/scheduler.rs
@@ -1,4 +1,5 @@
|
||||
use std::cmp;
|
||||
use std::iter::{self, once};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, bail};
|
||||
@@ -25,7 +26,6 @@ use crate::smtp::{Smtp, send_smtp_messages};
|
||||
use crate::sql;
|
||||
use crate::stats::maybe_send_stats;
|
||||
use crate::tools::{self, duration_to_str, maybe_add_time_based_warnings, time, time_elapsed};
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
use crate::{constants, stats};
|
||||
|
||||
pub(crate) mod connectivity;
|
||||
@@ -145,14 +145,14 @@ impl SchedulerState {
|
||||
InnerSchedulerState::Started(_) => {
|
||||
let new_state = InnerSchedulerState::Paused {
|
||||
started: true,
|
||||
pause_guards_count: NonZeroUsize::MIN,
|
||||
pause_guards_count: NonZeroUsize::new(1).unwrap(),
|
||||
};
|
||||
Self::do_stop(&mut inner, context, new_state).await;
|
||||
}
|
||||
InnerSchedulerState::Stopped => {
|
||||
*inner = InnerSchedulerState::Paused {
|
||||
started: false,
|
||||
pause_guards_count: NonZeroUsize::MIN,
|
||||
pause_guards_count: NonZeroUsize::new(1).unwrap(),
|
||||
};
|
||||
}
|
||||
InnerSchedulerState::Paused {
|
||||
@@ -183,7 +183,7 @@ impl SchedulerState {
|
||||
ref started,
|
||||
ref mut pause_guards_count,
|
||||
} => {
|
||||
if *pause_guards_count == NonZeroUsize::MIN {
|
||||
if *pause_guards_count == NonZeroUsize::new(1).unwrap() {
|
||||
match *started {
|
||||
true => SchedulerState::do_start(&mut inner, &context).await,
|
||||
false => *inner = InnerSchedulerState::Stopped,
|
||||
@@ -212,25 +212,21 @@ impl SchedulerState {
|
||||
/// Indicate that the network likely has come back.
|
||||
pub(crate) async fn maybe_network(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
let (inboxes, oboxes) = match *inner {
|
||||
let (inbox, oboxes) = match *inner {
|
||||
InnerSchedulerState::Started(ref scheduler) => {
|
||||
scheduler.maybe_network();
|
||||
let inboxes = scheduler
|
||||
.inboxes
|
||||
.iter()
|
||||
.map(|b| b.conn_state.state.connectivity.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let inbox = scheduler.inbox.conn_state.state.connectivity.clone();
|
||||
let oboxes = scheduler
|
||||
.oboxes
|
||||
.iter()
|
||||
.map(|b| b.conn_state.state.connectivity.clone())
|
||||
.collect::<Vec<_>>();
|
||||
(inboxes, oboxes)
|
||||
(inbox, oboxes)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
drop(inner);
|
||||
connectivity::idle_interrupted(inboxes, oboxes);
|
||||
connectivity::idle_interrupted(inbox, oboxes);
|
||||
}
|
||||
|
||||
/// Indicate that the network likely is lost.
|
||||
@@ -335,8 +331,7 @@ struct SchedBox {
|
||||
/// Job and connection scheduler.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Scheduler {
|
||||
/// Inboxes, one per transport.
|
||||
inboxes: Vec<SchedBox>,
|
||||
inbox: SchedBox,
|
||||
/// Optional boxes -- mvbox.
|
||||
oboxes: Vec<SchedBox>,
|
||||
smtp: SmtpConnectionState,
|
||||
@@ -479,17 +474,17 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
}
|
||||
|
||||
// Update quota no more than once a minute.
|
||||
if ctx.quota_needs_update(60).await
|
||||
&& let Err(err) = ctx.update_recent_quota(&mut session).await
|
||||
{
|
||||
warn!(ctx, "Failed to update quota: {:#}.", err);
|
||||
if ctx.quota_needs_update(60).await {
|
||||
if let Err(err) = ctx.update_recent_quota(&mut session).await {
|
||||
warn!(ctx, "Failed to update quota: {:#}.", err);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(()) = imap.resync_request_receiver.try_recv()
|
||||
&& let Err(err) = session.resync_folders(ctx).await
|
||||
{
|
||||
warn!(ctx, "Failed to resync folders: {:#}.", err);
|
||||
imap.resync_request_sender.try_send(()).ok();
|
||||
if let Ok(()) = imap.resync_request_receiver.try_recv() {
|
||||
if let Err(err) = session.resync_folders(ctx).await {
|
||||
warn!(ctx, "Failed to resync folders: {:#}.", err);
|
||||
imap.resync_request_sender.try_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
maybe_add_time_based_warnings(ctx).await;
|
||||
@@ -578,19 +573,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)
|
||||
@@ -869,40 +857,34 @@ impl Scheduler {
|
||||
let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1);
|
||||
let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1);
|
||||
|
||||
let mut inboxes = Vec::new();
|
||||
let mut oboxes = Vec::new();
|
||||
let mut start_recvs = Vec::new();
|
||||
|
||||
for (transport_id, configured_login_param) in ConfiguredLoginParam::load_all(ctx).await? {
|
||||
let (conn_state, inbox_handlers) =
|
||||
ImapConnectionState::new(ctx, transport_id, configured_login_param.clone()).await?;
|
||||
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
|
||||
let handle = {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
|
||||
};
|
||||
let inbox = SchedBox {
|
||||
meaning: FolderMeaning::Inbox,
|
||||
let (conn_state, inbox_handlers) = ImapConnectionState::new(ctx).await?;
|
||||
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
|
||||
let handle = {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
|
||||
};
|
||||
let inbox = SchedBox {
|
||||
meaning: FolderMeaning::Inbox,
|
||||
conn_state,
|
||||
handle,
|
||||
};
|
||||
start_recvs.push(inbox_start_recv);
|
||||
|
||||
if ctx.should_watch_mvbox().await? {
|
||||
let (conn_state, handlers) = ImapConnectionState::new(ctx).await?;
|
||||
let (start_send, start_recv) = oneshot::channel();
|
||||
let ctx = ctx.clone();
|
||||
let meaning = FolderMeaning::Mvbox;
|
||||
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
|
||||
oboxes.push(SchedBox {
|
||||
meaning,
|
||||
conn_state,
|
||||
handle,
|
||||
};
|
||||
inboxes.push(inbox);
|
||||
start_recvs.push(inbox_start_recv);
|
||||
|
||||
if ctx.should_watch_mvbox().await? {
|
||||
let (conn_state, handlers) =
|
||||
ImapConnectionState::new(ctx, transport_id, configured_login_param).await?;
|
||||
let (start_send, start_recv) = oneshot::channel();
|
||||
let ctx = ctx.clone();
|
||||
let meaning = FolderMeaning::Mvbox;
|
||||
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
|
||||
oboxes.push(SchedBox {
|
||||
meaning,
|
||||
conn_state,
|
||||
handle,
|
||||
});
|
||||
start_recvs.push(start_recv);
|
||||
}
|
||||
});
|
||||
start_recvs.push(start_recv);
|
||||
}
|
||||
|
||||
let smtp_handle = {
|
||||
@@ -928,7 +910,7 @@ impl Scheduler {
|
||||
let recently_seen_loop = RecentlySeenLoop::new(ctx.clone());
|
||||
|
||||
let res = Self {
|
||||
inboxes,
|
||||
inbox,
|
||||
oboxes,
|
||||
smtp,
|
||||
smtp_handle,
|
||||
@@ -948,8 +930,8 @@ impl Scheduler {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn boxes(&self) -> impl Iterator<Item = &SchedBox> {
|
||||
self.inboxes.iter().chain(self.oboxes.iter())
|
||||
fn boxes(&self) -> iter::Chain<iter::Once<&SchedBox>, std::slice::Iter<'_, SchedBox>> {
|
||||
once(&self.inbox).chain(self.oboxes.iter())
|
||||
}
|
||||
|
||||
fn maybe_network(&self) {
|
||||
@@ -967,9 +949,7 @@ impl Scheduler {
|
||||
}
|
||||
|
||||
fn interrupt_inbox(&self) {
|
||||
for b in &self.inboxes {
|
||||
b.conn_state.interrupt();
|
||||
}
|
||||
self.inbox.conn_state.interrupt();
|
||||
}
|
||||
|
||||
fn interrupt_oboxes(&self) {
|
||||
@@ -1009,7 +989,7 @@ impl Scheduler {
|
||||
let timeout_duration = std::time::Duration::from_secs(30);
|
||||
|
||||
let tracker = TaskTracker::new();
|
||||
for b in self.inboxes.into_iter().chain(self.oboxes.into_iter()) {
|
||||
for b in once(self.inbox).chain(self.oboxes) {
|
||||
let context = context.clone();
|
||||
tracker.spawn(async move {
|
||||
tokio::time::timeout(timeout_duration, b.handle)
|
||||
@@ -1115,17 +1095,12 @@ pub(crate) struct ImapConnectionState {
|
||||
|
||||
impl ImapConnectionState {
|
||||
/// Construct a new connection.
|
||||
async fn new(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
login_param: ConfiguredLoginParam,
|
||||
) -> Result<(Self, ImapConnectionHandlers)> {
|
||||
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
|
||||
let stop_token = CancellationToken::new();
|
||||
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
|
||||
|
||||
let handlers = ImapConnectionHandlers {
|
||||
connection: Imap::new(context, transport_id, login_param, idle_interrupt_receiver)
|
||||
.await?,
|
||||
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
|
||||
stop_token: stop_token.clone(),
|
||||
};
|
||||
|
||||
|
||||
@@ -201,20 +201,19 @@ impl ConnectivityStore {
|
||||
/// Set all folder states to InterruptingIdle in case they were `Idle` before.
|
||||
/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
|
||||
/// returns false immediately after `dc_maybe_network()`.
|
||||
pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>, oboxes: Vec<ConnectivityStore>) {
|
||||
for inbox in inboxes {
|
||||
let mut connectivity_lock = inbox.0.lock();
|
||||
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
|
||||
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
|
||||
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
|
||||
// return Connected until DC is completely done with fetching folders; this also
|
||||
// includes scan_folders() which happens on the inbox thread.
|
||||
if *connectivity_lock == DetailedConnectivity::Idle
|
||||
|| *connectivity_lock == DetailedConnectivity::NotConfigured
|
||||
{
|
||||
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
|
||||
}
|
||||
pub(crate) fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
|
||||
let mut connectivity_lock = inbox.0.lock();
|
||||
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
|
||||
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
|
||||
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
|
||||
// return Connected until DC is completely done with fetching folders; this also
|
||||
// includes scan_folders() which happens on the inbox thread.
|
||||
if *connectivity_lock == DetailedConnectivity::Idle
|
||||
|| *connectivity_lock == DetailedConnectivity::NotConfigured
|
||||
{
|
||||
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
|
||||
}
|
||||
drop(connectivity_lock);
|
||||
|
||||
for state in oboxes {
|
||||
let mut connectivity_lock = state.0.lock();
|
||||
@@ -455,8 +454,7 @@ impl Context {
|
||||
let domain =
|
||||
&deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
|
||||
.domain;
|
||||
let storage_on_domain =
|
||||
escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await);
|
||||
let storage_on_domain = stock_str::storage_on_domain(self, domain).await;
|
||||
ret += &format!("<h3>{storage_on_domain}</h3><ul>");
|
||||
let quota = self.quota.read().await;
|
||||
if let Some(quota) = &*quota {
|
||||
@@ -530,15 +528,11 @@ impl Context {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let domain_escaped = escaper::encode_minimal(domain);
|
||||
ret += &format!(
|
||||
"<li>Warning: {domain_escaped} claims to support quota but gives no information</li>"
|
||||
);
|
||||
ret += format!("<li>Warning: {domain} claims to support quota but gives no information</li>").as_str();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_escaped = escaper::encode_minimal(&e.to_string());
|
||||
ret += &format!("<li>{error_escaped}</li>");
|
||||
ret += format!("<li>{e}</li>").as_str();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -118,7 +118,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
chat.id,
|
||||
);
|
||||
let text = BROADCAST_INCOMPATIBILITY_MSG;
|
||||
add_info_msg(context, chat.id, text).await?;
|
||||
add_info_msg(context, chat.id, text, time()).await?;
|
||||
bail!(text.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,20 +123,23 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
}
|
||||
}
|
||||
|
||||
let timestamp = smeared_time(context);
|
||||
match invite {
|
||||
QrInvite::Group { .. } => {
|
||||
let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
|
||||
let joining_chat_id =
|
||||
joining_chat_id(context, &invite, private_chat_id, timestamp).await?;
|
||||
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
|
||||
chat::add_info_msg(context, joining_chat_id, &msg).await?;
|
||||
chat::add_info_msg(context, joining_chat_id, &msg, timestamp).await?;
|
||||
Ok(joining_chat_id)
|
||||
}
|
||||
QrInvite::Broadcast { .. } => {
|
||||
let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
|
||||
let joining_chat_id =
|
||||
joining_chat_id(context, &invite, private_chat_id, timestamp).await?;
|
||||
// We created the broadcast channel already, now we need to add Alice to it.
|
||||
if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
time(),
|
||||
timestamp,
|
||||
joining_chat_id,
|
||||
&[invite.contact_id()],
|
||||
)
|
||||
@@ -144,23 +147,30 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
}
|
||||
|
||||
// If we were not in the broadcast channel before, show a 'please wait' info message.
|
||||
// Since we don't have any specific stock string for this,
|
||||
// use the generic `Establishing guaranteed end-to-end encryption, please wait…`
|
||||
if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
|
||||
let msg =
|
||||
stock_str::secure_join_broadcast_started(context, invite.contact_id()).await;
|
||||
chat::add_info_msg(context, joining_chat_id, &msg).await?;
|
||||
let msg = stock_str::securejoin_wait(context).await;
|
||||
chat::add_info_msg(context, joining_chat_id, &msg, timestamp).await?;
|
||||
}
|
||||
Ok(joining_chat_id)
|
||||
}
|
||||
QrInvite::Contact { .. } => {
|
||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
||||
// uses it to send the handshake messages.
|
||||
// Use calc_sort_timestamp() to make sure that the new message is the last message in the chat.
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
let ts_sort = private_chat_id
|
||||
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
|
||||
.await?;
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
private_chat_id,
|
||||
&stock_str::securejoin_wait(context).await,
|
||||
SystemMessage::SecurejoinWait,
|
||||
None,
|
||||
time(),
|
||||
ts_sort,
|
||||
Some(timestamp),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -233,8 +243,9 @@ pub(super) async fn handle_auth_required(
|
||||
// so only show it when joining a group and not for a 1:1 chat or broadcast channel.
|
||||
let contact_id = invite.contact_id();
|
||||
let msg = stock_str::secure_join_replies(context, contact_id).await;
|
||||
let chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg).await?;
|
||||
let timestamp = smeared_time(context);
|
||||
let chat_id = joining_chat_id(context, &invite, chat_id, timestamp).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, timestamp).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +370,7 @@ async fn joining_chat_id(
|
||||
context: &Context,
|
||||
invite: &QrInvite,
|
||||
alice_chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<ChatId> {
|
||||
match invite {
|
||||
QrInvite::Contact { .. } => Ok(alice_chat_id),
|
||||
@@ -382,7 +394,7 @@ async fn joining_chat_id(
|
||||
name,
|
||||
Blocked::Not,
|
||||
None,
|
||||
smeared_time(context),
|
||||
timestamp,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
@@ -199,17 +199,19 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
if l_last > 1
|
||||
&& let Some(line) = lines.get(l_last - 1)
|
||||
&& is_empty_line(line)
|
||||
{
|
||||
l_last -= 1
|
||||
if l_last > 1 {
|
||||
if let Some(line) = lines.get(l_last - 1) {
|
||||
if is_empty_line(line) {
|
||||
l_last -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if l_last > 1
|
||||
&& let Some(line) = lines.get(l_last - 1)
|
||||
&& is_quoted_headline(line)
|
||||
{
|
||||
l_last -= 1
|
||||
if l_last > 1 {
|
||||
if let Some(line) = lines.get(l_last - 1) {
|
||||
if is_quoted_headline(line) {
|
||||
l_last -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
(lines.get(..l_last).unwrap_or(lines), Some(quoted_text))
|
||||
} else {
|
||||
|
||||
35
src/smtp.rs
35
src/smtp.rs
@@ -89,7 +89,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
self.connectivity.set_connecting(context);
|
||||
let (_transport_id, lp) = ConfiguredLoginParam::load(context)
|
||||
let lp = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
@@ -307,22 +307,23 @@ pub(crate) async fn smtp_send(
|
||||
Ok(()) => SendResult::Success,
|
||||
};
|
||||
|
||||
if let SendResult::Failure(err) = &status
|
||||
&& let Some(msg_id) = msg_id
|
||||
{
|
||||
// We couldn't send the message, so mark it as failed
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(mut msg) => {
|
||||
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
|
||||
{
|
||||
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
|
||||
if let SendResult::Failure(err) = &status {
|
||||
if let Some(msg_id) = msg_id {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(mut msg) => {
|
||||
if let Err(err) =
|
||||
message::set_msg_failed(context, &mut msg, &err.to_string()).await
|
||||
{
|
||||
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to load {msg_id} to mark it as failed: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to load {msg_id} to mark it as failed: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,11 +444,11 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
chat_id,
|
||||
&text,
|
||||
crate::mimeparser::SystemMessage::InvalidUnencryptedMail,
|
||||
Some(timestamp_sort),
|
||||
timestamp_sort,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
71
src/sql.rs
71
src/sql.rs
@@ -2,13 +2,13 @@
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
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 +17,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,30 +215,45 @@ 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 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 {
|
||||
Ok(()) => {
|
||||
if let Some(path) = blob.to_abs_path().to_str() {
|
||||
context
|
||||
.set_config_internal(Config::Selfavatar, Some(path))
|
||||
.await?;
|
||||
} else {
|
||||
warn!(context, "Setting selfavatar failed: non-UTF-8 filename");
|
||||
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 {
|
||||
if 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 {
|
||||
Ok(()) => {
|
||||
if let Some(path) = blob.to_abs_path().to_str() {
|
||||
context
|
||||
.set_config_internal(Config::Selfavatar, Some(path))
|
||||
.await?;
|
||||
} else {
|
||||
warn!(context, "Setting selfavatar failed: non-UTF-8 filename");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
|
||||
context
|
||||
.set_config_internal(Config::Selfavatar, None)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
|
||||
context
|
||||
.set_config_internal(Config::Selfavatar, None)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -746,6 +762,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
||||
conn.execute_batch(
|
||||
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
|
||||
PRAGMA secure_delete=on;
|
||||
PRAGMA busy_timeout = 0; -- fail immediately
|
||||
PRAGMA soft_heap_limit = 8388608; -- 8 MiB limit, same as set in Android SQLiteDatabase.
|
||||
PRAGMA foreign_keys=on;
|
||||
",
|
||||
@@ -758,22 +775,6 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
||||
conn.pragma_update(None, "temp_store", "memory")?;
|
||||
}
|
||||
|
||||
// Fail immediately when the database is busy,
|
||||
// except for iOS. On iOS we don't have
|
||||
// `accounts.lock` lockfile and the database
|
||||
// is used by two processes:
|
||||
// main process and the notification extension.
|
||||
// Due to a bug they both may run at the same time
|
||||
// and try to write to the database.
|
||||
// As a workaround, we wait up to 1 minute and retry
|
||||
// instead of failing immediately and
|
||||
// possibly missing a message.
|
||||
if cfg!(target_os = "ios") {
|
||||
conn.busy_timeout(Duration::new(60, 0))?;
|
||||
} else {
|
||||
conn.busy_timeout(Duration::ZERO)?;
|
||||
}
|
||||
|
||||
if !passphrase.is_empty() {
|
||||
conn.pragma_update(None, "key", passphrase)?;
|
||||
}
|
||||
|
||||
@@ -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,18 @@ 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? {
|
||||
if let Some(defaults) = &provider.config_defaults {
|
||||
if defaults.iter().any(|d| d.key == Config::DeleteServerAfter) {
|
||||
disable_server_delete = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sql.set_db_version(73).await?;
|
||||
}
|
||||
if dbversion < 74 {
|
||||
@@ -1347,98 +1363,6 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 139)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration_transaction(
|
||||
|transaction| {
|
||||
if exists_before_update {
|
||||
let is_chatmail = transaction
|
||||
.query_row(
|
||||
"SELECT value FROM config WHERE keyname='is_chatmail'",
|
||||
(),
|
||||
|row| {
|
||||
let value: String = row.get(0)?;
|
||||
Ok(value)
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
.as_deref()
|
||||
== Some("1");
|
||||
|
||||
// For non-chatmail accounts
|
||||
// default "bcc_self" was "1".
|
||||
// If it is not in the database,
|
||||
// save the old default explicity
|
||||
// as the new default is "0"
|
||||
// for all accounts.
|
||||
if !is_chatmail {
|
||||
transaction.execute(
|
||||
"INSERT OR IGNORE
|
||||
INTO config (keyname, value)
|
||||
VALUES (?, ?)",
|
||||
("bcc_self", "1"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 140)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"
|
||||
CREATE TABLE new_imap (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID header
|
||||
folder TEXT NOT NULL, -- IMAP folder
|
||||
target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
|
||||
uid INTEGER NOT NULL, -- UID
|
||||
uidvalidity INTEGER NOT NULL,
|
||||
UNIQUE (transport_id, folder, uid, uidvalidity)
|
||||
) STRICT;
|
||||
|
||||
INSERT OR IGNORE INTO new_imap SELECT
|
||||
id, 1, rfc724_mid, folder, target, uid, uidvalidity
|
||||
FROM imap;
|
||||
DROP TABLE imap;
|
||||
ALTER TABLE new_imap RENAME TO imap;
|
||||
CREATE INDEX imap_folder ON imap(transport_id, folder);
|
||||
CREATE INDEX imap_rfc724_mid ON imap(transport_id, rfc724_mid);
|
||||
|
||||
CREATE TABLE new_imap_sync (
|
||||
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
|
||||
folder TEXT NOT NULL,
|
||||
uidvalidity INTEGER NOT NULL DEFAULT 0,
|
||||
uid_next INTEGER NOT NULL DEFAULT 0,
|
||||
modseq INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (transport_id, folder)
|
||||
) STRICT;
|
||||
INSERT OR IGNORE INTO new_imap_sync SELECT
|
||||
1, folder, uidvalidity, uid_next, modseq
|
||||
FROM imap_sync;
|
||||
DROP TABLE imap_sync;
|
||||
ALTER TABLE new_imap_sync RENAME TO imap_sync;
|
||||
CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.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 +1377,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(
|
||||
|
||||
@@ -151,10 +151,10 @@ pub struct PooledConnection {
|
||||
impl Drop for PooledConnection {
|
||||
fn drop(&mut self) {
|
||||
// Put the connection back unless the pool is already dropped.
|
||||
if let Some(pool) = self.pool.upgrade()
|
||||
&& let Some(conn) = self.conn.take()
|
||||
{
|
||||
pool.put(conn);
|
||||
if let Some(pool) = self.pool.upgrade() {
|
||||
if let Some(conn) = self.conn.take() {
|
||||
pool.put(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
125
src/stock_str.rs
125
src/stock_str.rs
@@ -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,
|
||||
|
||||
@@ -290,6 +302,12 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
|
||||
MsgEphemeralTimerEnabledBy = 141,
|
||||
|
||||
#[strum(props(fallback = "You set message deletion timer to 1 minute."))]
|
||||
MsgYouEphemeralTimerMinute = 142,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 minute by %1$s."))]
|
||||
MsgEphemeralTimerMinuteBy = 143,
|
||||
|
||||
#[strum(props(fallback = "You set message deletion timer to 1 hour."))]
|
||||
MsgYouEphemeralTimerHour = 144,
|
||||
|
||||
@@ -344,9 +362,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,
|
||||
|
||||
@@ -367,7 +398,7 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Member %1$s removed."))]
|
||||
MsgDelMember = 178,
|
||||
|
||||
#[strum(props(fallback = "Establishing connection, please wait…"))]
|
||||
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
|
||||
SecurejoinWait = 190,
|
||||
|
||||
#[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
|
||||
@@ -404,10 +435,6 @@ https://delta.chat/donate"))]
|
||||
#[strum(props(fallback = "You joined the channel."))]
|
||||
MsgYouJoinedBroadcast = 202,
|
||||
|
||||
#[strum(props(fallback = "%1$s invited you to join this channel.\n\n\
|
||||
Waiting for the device of %2$s to reply…"))]
|
||||
SecureJoinBroadcastStarted = 203,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
|
||||
))]
|
||||
@@ -601,6 +628,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 +673,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 +717,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 {
|
||||
@@ -674,21 +743,6 @@ pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgYouJoinedBroadcast).await
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s invited you to join this channel. Waiting for the device of %2$s to reply…`.
|
||||
pub(crate) async fn secure_join_broadcast_started(
|
||||
context: &Context,
|
||||
inviter_contact_id: ContactId,
|
||||
) -> String {
|
||||
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinBroadcastStarted)
|
||||
.await
|
||||
.replace1(contact.get_display_name())
|
||||
.replace2(contact.get_display_name())
|
||||
} else {
|
||||
format!("secure_join_started: unknown contact {inviter_contact_id}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
|
||||
pub(crate) async fn msg_reacted(
|
||||
context: &Context,
|
||||
@@ -763,7 +817,7 @@ pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId
|
||||
.replace1(&contact_id.get_stock_name(context).await)
|
||||
}
|
||||
|
||||
/// Stock string: `Establishing connection, please wait…`.
|
||||
/// Stock string: `Establishing guaranteed end-to-end encryption, please wait…`.
|
||||
pub(crate) async fn securejoin_wait(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinWait).await
|
||||
}
|
||||
@@ -950,6 +1004,17 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to 1 minute.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to 1 hour.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -1034,6 +1099,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 +1291,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."
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
//! Module to collect and display Disk Space Usage of a Profile.
|
||||
use crate::{context::Context, message::MsgId};
|
||||
use anyhow::Result;
|
||||
use humansize::{BINARY, format_size};
|
||||
|
||||
/// Storage Usage Report
|
||||
/// Useful for debugging space usage problems in the deltachat database.
|
||||
#[derive(Debug)]
|
||||
pub struct StorageUsage {
|
||||
/// Total database size, subtract this from the backup size to estimate size of all blobs
|
||||
pub db_size: usize,
|
||||
/// size and row count of the 10 biggest tables
|
||||
pub largest_tables: Vec<(String, usize, Option<usize>)>,
|
||||
/// count and total size of status updates
|
||||
/// for the 10 webxdc apps with the most size usage in status updates
|
||||
pub largest_webxdc_data: Vec<(MsgId, usize, usize)>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StorageUsage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Storage Usage:")?;
|
||||
let human_db_size = format_size(self.db_size, BINARY);
|
||||
writeln!(f, "[Database Size]: {human_db_size}")?;
|
||||
writeln!(f, "[Largest Tables]:")?;
|
||||
for (name, size, row_count) in &self.largest_tables {
|
||||
let human_table_size = format_size(*size, BINARY);
|
||||
writeln!(
|
||||
f,
|
||||
" {name:<20} {human_table_size:>10}, {row_count:>6} rows",
|
||||
name = format!("{name}:"),
|
||||
row_count = row_count.map(|c| c.to_string()).unwrap_or("?".to_owned())
|
||||
)?;
|
||||
}
|
||||
writeln!(f, "[Webxdc With Biggest Status Update Space Usage]:")?;
|
||||
for (msg_id, size, update_count) in &self.largest_webxdc_data {
|
||||
let human_size = format_size(*size, BINARY);
|
||||
writeln!(
|
||||
f,
|
||||
" {msg_id:<8} {human_size:>10} across {update_count:>5} updates",
|
||||
msg_id = format!("{msg_id}:")
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage usage information for the Context's database
|
||||
pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
let page_size: usize = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_size", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let page_count: usize = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_count", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut largest_tables = ctx
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT name,
|
||||
SUM(pgsize) AS size
|
||||
FROM dbstat
|
||||
WHERE name IN (SELECT name FROM sqlite_master WHERE type='table')
|
||||
GROUP BY name ORDER BY size DESC LIMIT 10",
|
||||
(),
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let size: usize = row.get(1)?;
|
||||
Ok((name, size, None))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
for row in &mut largest_tables {
|
||||
let name = &row.0;
|
||||
let row_count: Result<Option<usize>> = ctx
|
||||
.sql
|
||||
// SECURITY: the table name comes from the db, not from the user
|
||||
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
|
||||
.await;
|
||||
row.2 = row_count.unwrap_or_default();
|
||||
}
|
||||
|
||||
let largest_webxdc_data = ctx
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT msg_id, SUM(length(update_item)) as size, COUNT(*) as update_count
|
||||
FROM msgs_status_updates
|
||||
GROUP BY msg_id ORDER BY size DESC LIMIT 10",
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let size: usize = row.get(1)?;
|
||||
let count: usize = row.get(2)?;
|
||||
|
||||
Ok((msg_id, size, count))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(StorageUsage {
|
||||
db_size: page_size * page_count,
|
||||
largest_tables,
|
||||
largest_webxdc_data,
|
||||
})
|
||||
}
|
||||
@@ -657,11 +657,14 @@ mod tests {
|
||||
alice1.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
alice1.set_config_bool(Config::IsChatmail, chatmail).await?;
|
||||
alice2.set_config_bool(Config::IsChatmail, chatmail).await?;
|
||||
if chatmail {
|
||||
alice1.set_config_bool(Config::IsChatmail, true).await?;
|
||||
alice2.set_config_bool(Config::IsChatmail, true).await?;
|
||||
} else {
|
||||
alice2.set_config_bool(Config::BccSelf, false).await?;
|
||||
}
|
||||
|
||||
alice1.set_config_bool(Config::BccSelf, true).await?;
|
||||
alice2.set_config_bool(Config::BccSelf, false).await?;
|
||||
|
||||
let sent_msg = if sync_message_sent {
|
||||
alice1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user