Compare commits

..

1 Commits

Author SHA1 Message Date
gerryfrancis
335680c05e Correct a date typo 2024-02-14 15:12:28 +01:00
119 changed files with 2489 additions and 4824 deletions

View File

@@ -24,11 +24,9 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.77.0
RUSTUP_TOOLCHAIN: 1.75.0
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
- name: Cache rust cargo artifacts
@@ -44,9 +42,7 @@ jobs:
name: cargo deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: --all-features --workspace
@@ -57,9 +53,7 @@ jobs:
name: Check provider database
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Check provider database
run: scripts/update-provider-database.sh
@@ -69,9 +63,8 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Checkout sources
uses: actions/checkout@v3
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Rustdoc
@@ -83,20 +76,18 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.77.0
rust: 1.75.0
- os: windows-latest
rust: 1.77.0
rust: 1.75.0
- os: macos-latest
rust: 1.77.0
rust: 1.75.0
# Minimum Supported Rust Version = 1.70.0
- os: ubuntu-latest
rust: 1.70.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
@@ -120,9 +111,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
@@ -131,7 +120,7 @@ jobs:
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Upload C library
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
@@ -144,9 +133,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
@@ -155,7 +142,7 @@ jobs:
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
@@ -165,9 +152,8 @@ jobs:
name: Python lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Checkout sources
uses: actions/checkout@v3
- name: Install tox
run: pip install tox
@@ -207,18 +193,16 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Download libdeltachat.a
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
- name: Install python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
@@ -259,12 +243,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Install python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
@@ -272,7 +254,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -21,27 +21,22 @@ jobs:
# Build a version statically linked against musl libc
# to avoid problems with glibc version incompatibility.
build_linux:
name: Build deltachat-rpc-server for Linux
strategy:
fail-fast: false
matrix:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: actions/checkout@v3
- name: Install ziglang
run: pip install wheel ziglang==0.11.0
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
run: sh scripts/zig-rpc-server.sh
- name: Upload binary
uses: actions/upload-artifact@v4
- name: Upload dist directory with Linux binaries
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux
path: result/bin/deltachat-rpc-server
name: linux
path: dist/
if-no-files-found: error
build_windows:
@@ -49,23 +44,32 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
include:
- os: windows-latest
artifact: win32.exe
path: deltachat-rpc-server.exe
target: i686-pc-windows-msvc
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- os: windows-latest
artifact: win64.exe
path: deltachat-rpc-server.exe
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
- name: Build
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-${{ matrix.arch }}
path: result/bin/deltachat-rpc-server.exe
name: deltachat-rpc-server-${{ matrix.artifact }}
path: target/${{ matrix.target}}/release/${{ matrix.path }}
if-no-files-found: error
build_macos:
@@ -73,13 +77,13 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
include:
- arch: x86_64
- arch: aarch64
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Setup rust target
run: rustup target add ${{ matrix.arch }}-apple-darwin
@@ -88,7 +92,7 @@ jobs:
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
@@ -97,93 +101,52 @@ jobs:
publish:
name: Build wheels and upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"]
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-server
permissions:
id-token: write
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: actions/checkout@v3
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
- name: Download Linux binaries
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
name: linux
path: dist/
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
- name: Download win32 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
name: deltachat-rpc-server-win32.exe
path: deltachat-rpc-server-win32.exe.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
- name: Download win64 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
name: deltachat-rpc-server-win64.exe
path: deltachat-rpc-server-win64.exe.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Create bin/ directory
- name: Flatten dist/ directory
run: |
mkdir -p bin
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
- name: List binaries
run: ls -l bin/
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: 3.12
@@ -191,40 +154,15 @@ jobs:
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels and source package
run: |
mkdir -p dist
nix build .#deltachat-rpc-server-x86_64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv7l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv6l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-aarch64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-i686-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win64-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win32-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-source
cp result/*.tar.gz dist/
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
mv *.whl dist/
run: scripts/wheel-rpc-server.py
- name: List artifacts
- name: List downloaded artifacts
run: ls -l dist/
- name: Upload binaries to the GitHub release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
bin/* dist/*
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
dist/*

View File

@@ -13,10 +13,9 @@ jobs:
steps:
- name: Install tree
run: sudo apt install tree
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
- name: Get tag
@@ -50,7 +49,7 @@ jobs:
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}

View File

@@ -14,11 +14,9 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Add Rust cache

View File

@@ -14,12 +14,10 @@ jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18.x

View File

@@ -14,10 +14,9 @@ jobs:
matrix:
os: [macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
- name: System info
@@ -29,7 +28,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -37,7 +36,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
@@ -57,7 +56,7 @@ jobs:
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz
@@ -75,10 +74,9 @@ jobs:
- name: Change working directory owner
run: chown root:root .
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
- run: apt-get update
@@ -99,7 +97,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -107,7 +105,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
@@ -127,7 +125,7 @@ jobs:
tar -zcvf "linux.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: linux
path: node/linux.tar.gz
@@ -139,10 +137,9 @@ jobs:
steps:
- name: Install tree
run: sudo apt install tree
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v2
with:
node-version: "18"
- name: Get tag
@@ -168,25 +165,25 @@ jobs:
node --version
echo $DELTACHAT_NODE_TAR_GZ
- name: Download Linux prebuild
uses: actions/download-artifact@v4
uses: actions/download-artifact@v1
with:
name: linux
- name: Download macOS prebuild
uses: actions/download-artifact@v4
uses: actions/download-artifact@v1
with:
name: macos-latest
- name: Download Windows prebuild
uses: actions/download-artifact@v4
uses: actions/download-artifact@v1
with:
name: windows-latest
- shell: bash
run: |
mkdir node/prebuilds
tar -xvzf linux.tar.gz -C node/prebuilds
tar -xvzf macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest.tar.gz -C node/prebuilds
tar -xvzf linux/linux.tar.gz -C node/prebuilds
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
rm -rf linux macos-latest windows-latest
- name: Install dependencies without running scripts
run: |
npm install --ignore-scripts
@@ -204,7 +201,7 @@ jobs:
ls -lah
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
- name: Upload prebuild
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: deltachat-node.tgz
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}

View File

@@ -23,10 +23,9 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
- name: System info
@@ -38,7 +37,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -46,7 +45,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/

View File

@@ -1,47 +0,0 @@
name: Publish deltachat-rpc-client to PyPI
on:
workflow_dispatch:
release:
types: [published]
jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install pypa/build
run: python3 -m pip install build
- name: Build a binary wheel and a source tarball
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
publish-to-pypi:
name: Publish Python distribution to PyPI
if: github.event_name == 'release'
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-client
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -10,17 +10,15 @@ on:
jobs:
build_repl:
name: Build REPL example
runs-on: ubuntu-latest
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: actions/checkout@v3
- name: Build
run: nix build .#deltachat-repl-win64
run: cargo build -p deltachat-repl --features vendored
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: repl.exe
path: "result/bin/deltachat-repl.exe"
path: "target/debug/deltachat-repl.exe"

View File

@@ -1,4 +1,4 @@
name: Build & Deploy Documentation on rs.delta.chat, c.delta.chat, py.delta.chat
name: Build & Deploy Documentation on rs.delta.chat
on:
push:
@@ -6,13 +6,11 @@ on:
- main
jobs:
build-rs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat --no-deps --document-private-items
@@ -24,41 +22,3 @@ jobs:
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"
build-python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
build-c:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build C documentation
run: nix build .#docs
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"

View File

@@ -14,15 +14,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/checkout@v3
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"

View File

@@ -0,0 +1,25 @@
name: Build & Deploy Documentation on py.delta.chat
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Build Python documentation
run: scripts/build-python-docs.sh
- name: Upload to py.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: delta
KEY: ${{ secrets.CODESPEAK_KEY }}
HOST: "lists.codespeak.net"
SOURCE: "dist/html/"
TARGET: "/home/delta/build/master"

3
.gitignore vendored
View File

@@ -44,6 +44,3 @@ node/build/
node/dist/
node/prebuilds/
node/.nyc_output/
# Nix symlink.
result

View File

@@ -1,219 +1,6 @@
# Changelog
## [1.136.6] - 2024-03-19
### Build system
- Add description to deltachat-rpc-server wheels.
- Read version from Cargo.toml in wheel-rpc-server.py.
### CI
- Update actions/cache from v3 to v4.
- Automate publishing of deltachat-rpc-server to PyPI.
### Documentation
- deltachat-rpc-server: Update deltachat-rpc-client URL.
### Miscellaneous Tasks
- Nix flake update.
## [1.136.5] - 2024-03-18
### Features / Changes
- Nicer summaries: prefer emoji over names
- Add `save_mime_headers` to debug info ([#5350](https://github.com/deltachat/deltachat-core-rust/pull/5350))
### Fixes
- Terminate ephemeral and location loop immediately on channel close.
- Update MemberListTimestamp when sending a group message.
- On iOS, use FILE (default) instead of MEMORY ([#5349](https://github.com/deltachat/deltachat-core-rust/pull/5349)).
- Add white background to recoded avatars ([#3787](https://github.com/deltachat/deltachat-core-rust/pull/3787)).
### Build system
- Add README to deltachat-rpc-client Python packages.
### Documentation
- deltachat-rpc-client: Document that 0 is a special value of `set_ephemeral_timer()`.
### Tests
- Test that reordering of Member added message results in square bracket error.
## [1.136.4] - 2024-03-11
### Build system
- nix: Make .#libdeltachat buildable on macOS.
- Build deltachat-rpc-server wheels with nix.
### CI
- Add workflow for automatic publishing of deltachat-rpc-client.
### Fixes
- Remove duplicate CHANGELOG entries for 1.135.1.
## [1.136.3] - 2024-03-09
### Features / Changes
- Start IMAP loop for sentbox only if it is configured ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
### Fixes
- Remove leading whitespace from Subject ([#5106](https://github.com/deltachat/deltachat-core-rust/pull/5106)).
- Create new Peerstate for unencrypted message with already known Autocrypt key, but a new address.
### Build system
- nix: Cleanup cross-compilation code.
- nix: Include SystemConfiguration framework on darwin systems.
### CI
- Wait for `build_windows` task before trying to publish it.
- Remove artifacts from npm package.
### Refactor
- Don't parse Autocrypt header for outgoing messages ([#5259](https://github.com/deltachat/deltachat-core-rust/pull/5259)).
- Remove `deduplicate_peerstates()`.
- Fix 2024-03-05 nightly clippy warnings.
### Miscellaneous Tasks
- deps: Bump mio from 0.8.8 to 0.8.11 in /fuzz.
- RPC client: Add missing constants ([#5110](https://github.com/deltachat/deltachat-core-rust/pull/5110)).
## [1.136.2] - 2024-03-05
### Build system
- Downgrade `cc` to 1.0.83 to fix build for Android.
### CI
- Update setup-node action.
## [1.136.1] - 2024-03-05
### Build system
- Revert to OpenSSL 3.1.
- Restore MSRV 1.70.0.
### Miscellaneous Tasks
- Update node constants.
## [1.136.0] - 2024-03-04
### Features / Changes
- Recognise Trash folder by name ([#5275](https://github.com/deltachat/deltachat-core-rust/pull/5275)).
- Send Chat-Group-Avatar as inline base64 ([#5253](https://github.com/deltachat/deltachat-core-rust/pull/5253)).
- Self-Reporting: Report number of protected/encrypted/unencrypted chats ([#5292](https://github.com/deltachat/deltachat-core-rust/pull/5292)).
### Fixes
- Don't send sync messages on self-{status,avatar} update from self-sent messages ([#5289](https://github.com/deltachat/deltachat-core-rust/pull/5289)).
- imap: Allow `maybe_network` to interrupt connection ratelimit.
- imap: Set connectivity to "connecting" only after ratelimit.
- Remove `Group-ID` from `Message-ID`.
- Prioritize protected `Message-ID` over `X-Microsoft-Original-Message-ID`.
### API-Changes
- Make `store_self_keypair` private.
- Add `ContextBuilder.build()` to build Context without opening.
- `dc_accounts_set_push_device_token` and `dc_get_push_state` APIs for iOS push notifications.
### Build system
- Tag armv6 wheels with tags accepted by PyPI.
- Unpin OpenSSL.
- Remove deprecated `unmaintained` field from deny.toml.
- Do not vendor OpenSSL when cross-compiling ([#5316](https://github.com/deltachat/deltachat-core-rust/pull/5316)).
- Increase MSRV to 1.74.0.
### CI
- Upgrade setup-python GitHub Action.
- Update to Rust 1.76 and fix clippy warnings.
- Build Python docs with Nix.
- Upload python docs without GH actions.
- Upload cffi docs without GH actions.
- Build c.delta.chat docs with nix.
### Other
- refactor: move more methods from Imap into Session.
- Add deltachat-time to sources.
### Refactor
- Remove Session from Imap structure.
- Merge ImapConfig into Imap.
- Get rid of ImapActionResult.
- Build contexts using ContextBuilder.
- Do not send `Secure-Join-Group` in `vg-request`.
### Tests
- Fix `test_verified_oneonone_chat_broken_by_device_change()` ([#5280](https://github.com/deltachat/deltachat-core-rust/pull/5280)).
- `get_protected_chat()`: Use FFIEventTracker instead of `dc_wait_next_msgs()` ([#5207](https://github.com/deltachat/deltachat-core-rust/pull/5207)).
- Fixup `tests/test_3_offline.py::TestOfflineAccountBasic::test_wrong_db`.
- Fix pytest compat ([#5317](https://github.com/deltachat/deltachat-core-rust/pull/5317)).
## [1.135.1] - 2024-02-20
### Features / Changes
- Sync self-avatar across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
- Sync Config::Selfstatus across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
- Remove webxdc sending limit.
### Fixes
- Never encrypt `{vc,vg}-request` SecureJoin messages.
- Apply Autocrypt headers if timestamp is unchanged.
- `Context::get_info`: Report displayname as "displayname" (w/o underscore).
### Tests
- Mock `SystemTime::now()` for the tests.
- Add a test on protection message sort timestamp ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
### Build system
- Add flake.nix.
- Add footer template for git-cliff.
### CI
- Update GitHub Actions `actions/upload-artifact`, `actions/download-artifact`, `actions/checkout`.
- Build deltachat-repl for Windows with nix.
- Build deltachat-rpc-server with nix.
- Try to upload deltachat-rpc-server only on release.
- Fixup node-package.yml after artifact actions upgrade.
- Update to actions/checkout@v4.
- Replace download-artifact v1 with v4.
### Refactor
- `create_keypair`: Remove unnecessary `map_err`.
- Return error with a cause when failing to export keys.
- Rename incorrectly named variables in `create_keypair`.
## [1.135.0] - 2024-02-13
## [1.135.0] - 2024-02-14
### Features / Changes
@@ -222,7 +9,6 @@
- Context::set_config(): Restart IO scheduler if needed ([#5111](https://github.com/deltachat/deltachat-core-rust/pull/5111)).
- Server_sent_unsolicited_exists(): Log folder name.
- Cache system time instead of looking at the clock several times in a row.
- Basic self-reporting ([#5129](https://github.com/deltachat/deltachat-core-rust/pull/5129)).
### Fixes
@@ -263,11 +49,47 @@
### Other
- Update welcome image, thanks @paulaluap
.
- Merge pull request #5243 from deltachat/dependabot/cargo/pin-project-1.1.4
.
- Merge pull request #5241 from deltachat/dependabot/cargo/futures-lite-2.2.0
.
- Merge pull request #5236 from deltachat/dependabot/cargo/chrono-0.4.33
.
- Merge pull request #5235 from deltachat/dependabot/cargo/image-0.24.8
.
- Basic self-reporting, core part ([#5129](https://github.com/deltachat/deltachat-core-rust/pull/5129))
Part of https://github.com/deltachat/deltachat-android/issues/2909
For now, this is only sending a few basic metrics..
- Do not change db schema in an incompatible way ([#5254](https://github.com/deltachat/deltachat-core-rust/pull/5254))
PR #5099 removed some columns in the database that were actually in use.
usually, to not worsen UX unnecessarily
(releases take time - in between, "Add Second Device", "Backup" etc.
would fail), we try to avoid such schema changes (checking for
db-version would avoid import etc. but would still worse UX),
see discussion at #2294.
these are the errors, the user will be confronted with otherwise:
<img width=400
src=https://github.com/deltachat/deltachat-core-rust/assets/9800740/e3f0fd6e-a7a9-43f6-9023-0ae003985425>
it is not great to maintain the old columns, but well :)
as no official releases with newer cores are rolled out yet, i think, it
is fine to change the "107" migration
and not copy things a second time in a newer migration.
(this issue happens to me during testing, and is probably also the issue
reported by @lk108 for ubuntu-touch).
### Refactor
@@ -3754,11 +3576,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.133.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.1...v1.133.2
[1.134.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.2...v1.134.0
[1.135.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.134.0...v1.135.0
[1.135.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.0...v1.135.1
[1.136.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.1...v1.136.0
[1.136.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.0...v1.136.1
[1.136.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.1...v1.136.2
[1.136.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.2...v1.136.3
[1.136.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.3...v1.136.4
[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5
[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6

View File

@@ -14,14 +14,24 @@ endif()
add_custom_command(
OUTPUT
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
${CARGO} build --release --no-default-features --features jsonrpc
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version
# "1" bug which makes `--no-default-features` affect only
# `deltachat`, but not `deltachat-ffi` package.
#
# We can't enable version "2" resolver [1] because it is not
# stable yet on rust 1.50.0.
#
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)
@@ -29,12 +39,12 @@ add_custom_target(
lib_deltachat
ALL
DEPENDS
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

488
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.136.6"
version = "1.135.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
@@ -32,7 +32,6 @@ strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
@@ -56,7 +55,7 @@ futures-lite = "2.2.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.24.9", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.8", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.2", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
@@ -69,7 +68,7 @@ num-traits = "0.2"
once_cell = "1.18.0"
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.11", default-features = false }
pgp = { version = "0.10", default-features = false }
pin-project = "1"
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
@@ -78,7 +77,7 @@ quoted_printable = "0.5"
rand = "0.8"
regex = "1.10"
reqwest = { version = "0.11.24", features = ["json"] }
rusqlite = { version = "0.31", features = ["sqlcipher"] }
rusqlite = { version = "0.30", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.5"
serde_json = "1"
@@ -89,7 +88,7 @@ smallvec = "1"
strum = "0.26"
strum_macros = "0.26"
tagger = "4.3.4"
textwrap = "0.16.1"
textwrap = "0.16.0"
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
@@ -129,7 +128,6 @@ members = [
"deltachat-rpc-server",
"deltachat-ratelimit",
"deltachat-repl",
"deltachat-time",
"format-flowed",
]

View File

@@ -77,17 +77,3 @@ body = """
"""
# remove the leading and trailing whitespace from the template
trim = true
footer = """
{% for release in releases -%}
{% if release.version -%}
{% if release.previous.version -%}
[{{ release.version | trim_start_matches(pat="v") }}]: \
https://github.com/deltachat/deltachat-core-rust\
/compare/{{ release.previous.version }}..{{ release.version }}
{% endif -%}
{% else -%}
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
/compare/{{ release.previous.version }}..HEAD
{% endif -%}
{% endfor %}
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.136.6"
version = "1.135.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -686,24 +686,6 @@ int dc_get_connectivity (dc_context_t* context);
char* dc_get_connectivity_html (dc_context_t* context);
#define DC_PUSH_NOT_CONNECTED 0
#define DC_PUSH_HEARTBEAT 1
#define DC_PUSH_CONNECTED 2
/**
* Get the current push notification state.
* One of:
* - DC_PUSH_NOT_CONNECTED
* - DC_PUSH_HEARTBEAT
* - DC_PUSH_CONNECTED
*
* @memberof dc_context_t
* @param context The context object.
* @return Push notification state.
*/
int dc_get_push_state (dc_context_t* context);
/**
* Standalone version of dc_accounts_all_work_done().
* Only used by the python tests.
@@ -3183,16 +3165,6 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
*/
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
/**
* Sets device token for Apple Push Notification service.
* Returns immediately.
*
* @memberof dc_accounts_t
* @param token Hexadecimal device token
*/
void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const char *token);
/**
* Create the event emitter that is used to receive events.
*
@@ -6074,12 +6046,10 @@ void dc_event_unref(dc_event_t* event);
* Downloading a bunch of messages just finished. This is an
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
* UI may store #DC_EVENT_INCOMING_MSG events
* and display notifications for all messages at once
* when this event arrives.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
*
* @param data1 0
* @param data2 0
* @param data2 (char*) msg_ids, a json object with the message ids.
*/
#define DC_EVENT_INCOMING_MSG_BUNCH 2006

View File

@@ -26,7 +26,7 @@ use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::{Context, ContextBuilder};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
@@ -34,6 +34,7 @@ use deltachat::message::MsgId;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -103,11 +104,12 @@ pub unsafe extern "C" fn dc_context_new(
let ctx = if blobdir.is_null() || *blobdir == 0 {
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::thread_rng().gen();
block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
.open(),
)
block_on(Context::new(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
))
} else {
eprintln!("blobdir can not be defined explicitly anymore");
return ptr::null_mut();
@@ -131,11 +133,12 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
let id = rand::thread_rng().gen();
match block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
.build(),
) {
match block_on(Context::new_closed(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
)) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
eprintln!("failed to create context: {err:#}");
@@ -384,7 +387,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
return 0;
}
let ctx = &*context;
block_on(ctx.get_connectivity()) as u32 as libc::c_int
block_on(async move { ctx.get_connectivity().await as u32 as libc::c_int })
}
#[no_mangle]
@@ -407,16 +410,6 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_push_state()");
return 0;
}
let ctx = &*context;
block_on(ctx.push_state()) as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -719,8 +712,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::WebxdcStatusUpdate { .. }
| EventType::WebxdcInstanceDeleted { .. }
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::IncomingMsgBunch { .. } => ptr::null_mut(),
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -732,6 +724,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
.unwrap_or_default()
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::ConfigSynced { key } => {
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
@@ -4925,29 +4922,6 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
1
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
accounts: *mut dc_accounts_t,
token: *const libc::c_char,
) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_set_push_device_token()");
return;
}
let accounts = &*accounts;
let token = to_string_lossy(token);
block_on(async move {
let mut accounts = accounts.write().await;
if let Err(err) = accounts.set_push_device_token(&token).await {
accounts.emit_event(EventType::Error(format!(
"Failed to set notify token: {err:#}."
)));
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *mut dc_accounts_t,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.136.6"
version = "1.135.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -17,7 +17,7 @@ deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.10.1"
tempfile = "3.9.0"
log = "0.4"
async-channel = { version = "2.0.0" }
futures = { version = "0.3.30" }
@@ -26,7 +26,7 @@ yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.33.0" }
sanitize-filename = "0.5"
walkdir = "2.5.0"
walkdir = "2.3.3"
base64 = "0.21"
# optional dependencies

View File

@@ -1097,12 +1097,9 @@ impl CommandApi {
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
MessageObject::from_message_id(&ctx, message_id).await
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1122,7 +1119,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
for message_id in message_ids {
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
messages.insert(
message_id,
match message_result {
@@ -2045,9 +2042,11 @@ impl CommandApi {
)
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
Ok((msg_id, message))
}
// mimics the old desktop call, will get replaced with something better in the composer rewrite,

View File

@@ -101,15 +101,17 @@ pub enum EventType {
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
#[serde(rename_all = "camelCase")]
IncomingMsg { chat_id: u32, msg_id: u32 },
/// Downloading a bunch of messages just finished. This is an
/// Downloading a bunch of messages just finished. This is an experimental
/// event to allow the UI to only show one notification per message bunch,
/// instead of cluttering the user with many notifications.
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")]
IncomingMsgBunch,
IncomingMsgBunch { msg_ids: Vec<u32> },
/// Messages were seen or noticed.
/// chat id is always set.
@@ -285,7 +287,9 @@ impl From<CoreEventType> for EventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},

View File

@@ -105,6 +105,11 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
Self::from_msg_id(context, msg_id).await
}
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;

View File

@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.136.6"
"version": "1.135.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.136.6"
version = "1.135.0"
license = "MPL-2.0"
edition = "2021"
@@ -9,9 +9,9 @@ ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.21"
log = "0.4.20"
pretty_env_logger = "0.5"
rusqlite = "0.31"
rusqlite = "0.30"
rustyline = "13"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }

View File

@@ -3,7 +3,7 @@ extern crate dirs;
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Result};
use deltachat::chat::{

View File

@@ -9,6 +9,7 @@ extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use ansi_term::Color;
@@ -19,7 +20,8 @@ use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::EventType;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
@@ -310,10 +312,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = ContextBuilder::new(args[1].clone().into())
.with_id(1)
.open()
.await?;
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
let events = context.get_event_emitter();
tokio::task::spawn(async move {

View File

@@ -22,7 +22,6 @@ classifiers = [
dynamic = [
"version"
]
readme = "README.md"
[tool.setuptools.package-data]
deltachat_rpc_client = [

View File

@@ -84,9 +84,7 @@ class Chat:
self._rpc.set_chat_name(self.account.id, self.id, name)
def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat in seconds.
0 means the timer is disabled, use 1 for immediate deletion."""
"""Set ephemeral timer of this chat."""
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:

View File

@@ -61,15 +61,6 @@ class EventType(str, Enum):
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
class ChatId(IntEnum):
"""Special chat ids"""
TRASH = 3
ARCHIVED_LINK = 6
ALLDONE_HINT = 7
LAST_SPECIAL = 9
class ChatType(IntEnum):
"""Chat types"""
@@ -131,107 +122,3 @@ class SystemMessageType(str, Enum):
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
MULTI_DEVICE_SYNC = "MultiDeviceSync"
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
class MessageState(IntEnum):
"""State of the message."""
UNDEFINED = 0
IN_FRESH = 10
IN_NOTICED = 13
IN_SEEN = 16
OUT_PREPARING = 18
OUT_DRAFT = 19
OUT_PENDING = 20
OUT_FAILED = 24
OUT_DELIVERED = 26
OUT_MDN_RCVD = 28
class MessageId(IntEnum):
"""Special message ids"""
DAYMARKER = 9
LAST_SPECIAL = 9
class CertificateChecks(IntEnum):
"""Certificate checks mode"""
AUTOMATIC = 0
STRICT = 1
ACCEPT_INVALID_CERTIFICATES = 3
class Connectivity(IntEnum):
"""Connectivity states"""
NOT_CONNECTED = 1000
CONNECTING = 2000
WORKING = 3000
CONNECTED = 4000
class KeyGenType(IntEnum):
"""Type of the key to generate"""
DEFAULT = 0
RSA2048 = 1
ED25519 = 2
RSA4096 = 3
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
"""Authorization flags"""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
"""Media quality setting"""
BALANCED = 0
WORSE = 1
class ProviderStatus(IntEnum):
"""Provider status according to manual testing"""
OK = 1
PREPARATION = 2
BROKEN = 3
class PushNotifyState(IntEnum):
"""Push notifications state"""
NOT_CONNECTED = 0
HEARTBEAT = 1
CONNECTED = 2
class ShowEmails(IntEnum):
"""Show emails mode"""
OFF = 0
ACCEPTED_CONTACTS = 1
ALL = 2
class SocketSecurity(IntEnum):
"""Socket security"""
AUTOMATIC = 0
SSL = 1
STARTTLS = 2
PLAIN = 3
class VideochatType(IntEnum):
"""Video chat URL type"""
UNKNOWN = 0
BASICWEBRTC = 1
JITSI = 2

View File

@@ -164,21 +164,6 @@ def test_qr_readreceipt(acfactory) -> None:
assert not bob.get_chat_by_contact(bob_contact_charlie)
def test_setup_contact_resetup(acfactory) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acfactory.resetup_account(alice)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
def test_verified_group_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.136.6"
version = "1.135.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -30,7 +30,7 @@ deltachat-rpc-server
The common use case for this program is to create bindings to use Delta Chat core from programming
languages other than Rust, for example:
1. Python: https://pypi.org/project/deltachat-rpc-client/
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
Run `deltachat-rpc-server --version` to check the version of the server.

View File

@@ -1,8 +0,0 @@
[package]
name = "deltachat-time"
version = "1.0.0"
description = "Time-related tools"
edition = "2021"
license = "MPL-2.0"
[dependencies]

View File

@@ -1,35 +0,0 @@
#![allow(missing_docs)]
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
static SYSTEM_TIME_SHIFT: RwLock<Duration> = RwLock::new(Duration::new(0, 0));
/// Fake struct for mocking `SystemTime::now()` for test purposes. You still need to use
/// `SystemTime` as a struct representing a system time.
pub struct SystemTimeTools();
impl SystemTimeTools {
pub const UNIX_EPOCH: SystemTime = SystemTime::UNIX_EPOCH;
pub fn now() -> SystemTime {
return SystemTime::now() + *SYSTEM_TIME_SHIFT.read().unwrap();
}
/// Simulates a system clock forward adjustment by `duration`.
pub fn shift(duration: Duration) {
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
SystemTimeTools::shift(Duration::from_secs(60));
let t = SystemTimeTools::now();
assert!(t > SystemTime::now());
}
}

View File

@@ -1,4 +1,5 @@
[advisories]
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
@@ -9,12 +10,6 @@ ignore = [
# There is no fix at the time of writing this (2023-11-28).
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained ansi_term
"RUSTSEC-2021-0139",
# Unmaintained encoding
"RUSTSEC-2021-0153",
]
[bans]
@@ -61,7 +56,6 @@ skip = [
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "toml_edit", version = "0.21.1" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
@@ -74,7 +68,6 @@ skip = [
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
]

163
flake.lock generated
View File

@@ -1,163 +0,0 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1710742993,
"narHash": "sha256-W0PQCe0bW3hKF5lHawXrKynBcdSP18Qa4sb8DcUfOqI=",
"owner": "nix-community",
"repo": "fenix",
"rev": "6f2fec850f569d61562d3a47dc263f19e9c7d825",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1710765496,
"narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e367f7a1fb93137af22a3908f00b9a35e2d286a7",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"naersk": "naersk",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_3"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1710708100,
"narHash": "sha256-Jd6pmXlwKk5uYcjyO/8BfbUVmx8g31Qfk7auI2IG66A=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "b6d1887bc4f9543b6c6bf098179a62446f34a6c3",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

467
flake.nix
View File

@@ -1,467 +0,0 @@
{
description = "Delta Chat core";
inputs = {
fenix.url = "github:nix-community/fenix";
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.stdenv) isDarwin;
fenixPkgs = fenix.packages.${system};
naersk' = pkgs.callPackage naersk { };
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
rustSrc = nix-filter.lib {
root = ./.;
# Include only necessary files
# to avoid rebuilds e.g. when README.md or flake.nix changes.
include = [
./benches
./assets
./Cargo.lock
./Cargo.toml
./CMakeLists.txt
./CONTRIBUTING.md
./deltachat_derive
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-ratelimit
./deltachat-repl
./deltachat-rpc-client
./deltachat-time
./deltachat-rpc-server
./format-flowed
./release-date.in
./src
];
exclude = [
(nix-filter.lib.matchExt "nix")
"flake.lock"
];
};
# Map from architecture name to rust targets and nixpkgs targets.
arch2targets = {
"x86_64-linux" = {
rustTarget = "x86_64-unknown-linux-musl";
crossTarget = "x86_64-unknown-linux-musl";
};
"armv7l-linux" = {
rustTarget = "armv7-unknown-linux-musleabihf";
crossTarget = "armv7l-unknown-linux-musleabihf";
};
"armv6l-linux" = {
rustTarget = "arm-unknown-linux-musleabihf";
crossTarget = "armv6l-unknown-linux-musleabihf";
};
"aarch64-linux" = {
rustTarget = "aarch64-unknown-linux-musl";
crossTarget = "aarch64-unknown-linux-musl";
};
"i686-linux" = {
rustTarget = "i686-unknown-linux-musl";
crossTarget = "i686-unknown-linux-musl";
};
"x86_64-darwin" = {
rustTarget = "x86_64-apple-darwin";
crossTarget = "x86_64-darwin";
};
"aarch64-darwin" = {
rustTarget = "aarch64-apple-darwin";
crossTarget = "aarch64-darwin";
};
};
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"email-0.0.20" = "sha256-rV4Uzqt2Qdrfi5Ti1r+Si1c2iW1kKyWLwOgLkQ5JGGw=";
"encoded-words-0.2.0" = "sha256-KK9st0hLFh4dsrnLd6D8lC6pRFFs8W+WpZSGMGJcosk=";
"lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
};
};
mkRustPackage = packageName:
naersk'.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
pkgsWin64 = pkgs.pkgsCross.mingwW64;
mkWin64RustPackage = packageName:
let
rustTarget = "x86_64-pc-windows-gnu";
in
let
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
in
naerskWin.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
depsBuildBuild = [
pkgsWin64.stdenv.cc
pkgsWin64.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
};
pkgsWin32 = pkgs.pkgsCross.mingw32;
mkWin32RustPackage = packageName:
let
rustTarget = "i686-pc-windows-gnu";
in
let
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
# Get rid of MCF Gthread library.
# See <https://github.com/NixOS/nixpkgs/issues/156343>
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
# for details.
#
# Use DWARF-2 instead of SJLJ for exception handling.
winCC = pkgsWin32.buildPackages.wrapCC (
(pkgsWin32.buildPackages.gcc-unwrapped.override
({
threadsCross = {
model = "win32";
package = null;
};
})).overrideAttrs (oldAttr: rec{
configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions --with-dwarf2"
];
})
);
winStdenv = pkgsWin32.buildPackages.overrideCC pkgsWin32.stdenv winCC;
in
naerskWin.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
depsBuildBuild = [
winCC
pkgsWin32.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
};
mkCrossRustPackage = arch: packageName:
let
rustTarget = arch2targets."${arch}".rustTarget;
crossTarget = arch2targets."${arch}".crossTarget;
pkgsCross = import nixpkgs {
system = system;
crossSystem.config = crossTarget;
};
in
let
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
in
naersk-lib.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
};
mkRustPackages = arch:
let
rpc-server = mkCrossRustPackage arch "deltachat-rpc-server";
in
{
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
"deltachat-rpc-server-${arch}" = rpc-server;
"deltachat-rpc-server-${arch}-wheel" =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
};
in
{
formatter = pkgs.nixpkgs-fmt;
packages =
mkRustPackages "aarch64-linux" //
mkRustPackages "i686-linux" //
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" // rec {
# Run with `nix run .#deltachat-repl foo.db`.
deltachat-repl = mkRustPackage "deltachat-repl";
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win64-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win32-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
# Run `nix build .#docs` to get C docs generated in `./result/`.
docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
libdeltachat =
pkgs.stdenv.mkDerivation rec {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
# Source package for deltachat-rpc-server.
# Fake package that downloads Linux version,
# needed to install deltachat-rpc-server on Android with `pip`.
deltachat-rpc-server-source =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage rec {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.setuptools_scm
];
};
deltachat-python =
pkgs.python3Packages.buildPythonPackage rec {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.setuptools_scm
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
};
}
);
}

338
fuzz/Cargo.lock generated
View File

@@ -52,18 +52,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.0.2"
@@ -88,12 +76,6 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -208,9 +190,9 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.7"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98892ebee4c05fc66757e600a7466f0d9bfcde338f645d64add323789f26cb36"
checksum = "a9d69fc1499878158750f644c4eb46aff55bb9d32d77e3dc4aecf8308d5c3ba6"
dependencies = [
"async-channel 2.1.1",
"base64 0.21.0",
@@ -743,9 +725,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.12"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
dependencies = [
"libc",
]
@@ -823,32 +805,19 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "4.1.2"
version = "4.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.6",
"fiat-crypto",
"packed_simd_2",
"platforms",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "cxx"
version = "1.0.85"
@@ -953,7 +922,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.136.0"
version = "1.133.0"
dependencies = [
"anyhow",
"async-channel 2.1.1",
@@ -965,7 +934,6 @@ dependencies = [
"base64 0.21.0",
"brotli",
"chrono",
"deltachat-time",
"deltachat_derive",
"email",
"encoded-words",
@@ -985,7 +953,7 @@ dependencies = [
"libc",
"mailparse 0.14.0",
"mime",
"num-derive",
"num-derive 0.4.0",
"num-traits",
"num_cpus",
"once_cell",
@@ -1033,16 +1001,12 @@ dependencies = [
"mailparse 0.13.8",
]
[[package]]
name = "deltachat-time"
version = "1.0.0"
[[package]]
name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 2.0.52",
"syn 2.0.15",
]
[[package]]
@@ -1223,22 +1187,6 @@ dependencies = [
"syn 1.0.107",
]
[[package]]
name = "dsa"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689"
dependencies = [
"digest 0.10.6",
"num-bigint-dig",
"num-traits",
"pkcs8 0.10.2",
"rfc6979 0.4.0",
"sha2 0.10.6",
"signature 2.1.0",
"zeroize",
]
[[package]]
name = "ecdsa"
version = "0.14.8"
@@ -1301,15 +1249,14 @@ dependencies = [
[[package]]
name = "ed25519-dalek"
version = "2.1.1"
version = "2.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a"
dependencies = [
"curve25519-dalek 4.1.2",
"curve25519-dalek 4.0.0-rc.2",
"ed25519 2.2.1",
"serde",
"sha2 0.10.6",
"subtle",
"zeroize",
]
@@ -1488,7 +1435,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.15",
]
[[package]]
@@ -1662,9 +1609,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.6"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382"
checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
[[package]]
name = "filetime"
@@ -1785,13 +1732,14 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
version = "2.2.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb"
dependencies = [
"fastrand 2.0.1",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
]
@@ -1874,9 +1822,9 @@ dependencies = [
[[package]]
name = "gif"
version = "0.13.1"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
dependencies = [
"color_quant",
"weezl",
@@ -1935,7 +1883,7 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.6",
"ahash",
]
[[package]]
@@ -1943,18 +1891,14 @@ name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
dependencies = [
"ahash 0.8.11",
"allocator-api2",
]
[[package]]
name = "hashlink"
version = "0.9.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
"hashbrown 0.14.3",
"hashbrown 0.12.3",
]
[[package]]
@@ -2089,7 +2033,7 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756"
dependencies = [
"libm",
"libm 0.2.6",
]
[[package]]
@@ -2180,24 +2124,25 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.9"
version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"jpeg-decoder",
"num-rational",
"num-traits",
"png",
]
[[package]]
name = "imap-proto"
version = "0.16.4"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e70cd66882c8cb1c9802096ba75212822153c51478dc61621e1a22f6c92361"
checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0"
dependencies = [
"nom",
]
@@ -2302,12 +2247,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "iter-read"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b"
[[package]]
name = "itoa"
version = "1.0.5"
@@ -2329,20 +2268,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "k256"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc"
dependencies = [
"cfg-if",
"ecdsa 0.16.6",
"elliptic-curve 0.13.4",
"once_cell",
"sha2 0.10.6",
"signature 2.1.0",
]
[[package]]
name = "kamadak-exif"
version = "0.5.5"
@@ -2400,6 +2325,12 @@ version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "libm"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a"
[[package]]
name = "libm"
version = "0.2.6"
@@ -2408,9 +2339,9 @@ checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
dependencies = [
"cc",
"openssl-sys",
@@ -2554,9 +2485,9 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.11"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
@@ -2684,7 +2615,7 @@ checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"libm 0.2.6",
"num-integer",
"num-iter",
"num-traits",
@@ -2694,6 +2625,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.107",
]
[[package]]
name = "num-derive"
version = "0.4.0"
@@ -2702,7 +2644,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.15",
]
[[package]]
@@ -2726,6 +2668,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
@@ -2733,7 +2686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
"libm",
"libm 0.2.6",
]
[[package]]
@@ -2746,27 +2699,6 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "object"
version = "0.30.0"
@@ -2903,6 +2835,16 @@ dependencies = [
"sha2 0.10.6",
]
[[package]]
name = "packed_simd_2"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282"
dependencies = [
"cfg-if",
"libm 0.1.4",
]
[[package]]
name = "parking"
version = "2.2.0"
@@ -2973,9 +2915,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pgp"
version = "0.11.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "031fa1e28c4cb54c90502ef0642a44ef10ec8349349ebe6372089f1b1ef4f297"
checksum = "37a79d6411154d1a9908e7a2c4bac60a5742f6125823c2c30780c7039aef02f0"
dependencies = [
"aes",
"base64 0.21.0",
@@ -2990,32 +2932,27 @@ dependencies = [
"cfb-mode",
"chrono",
"cipher",
"const-oid",
"crc24",
"curve25519-dalek 4.1.2",
"derive_builder",
"des",
"digest 0.10.6",
"dsa",
"ed25519-dalek 2.1.1",
"ed25519-dalek 2.0.0-rc.2",
"elliptic-curve 0.13.4",
"flate2",
"generic-array",
"hex",
"idea",
"iter-read",
"k256",
"log",
"md-5",
"nom",
"num-bigint-dig",
"num-derive 0.3.3",
"num-traits",
"num_enum",
"p256 0.13.1",
"p384 0.13.0",
"rand 0.8.5",
"ripemd",
"rsa 0.9.1",
"rsa 0.9.0-pre.1",
"sha1",
"sha2 0.10.6",
"sha3",
@@ -3073,13 +3010,14 @@ dependencies = [
[[package]]
name = "pkcs1"
version = "0.7.5"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
checksum = "575fd6eebed721a2929faa1ee1383a49788378083bbbd7f299af75dd84195cee"
dependencies = [
"der 0.7.3",
"pkcs8 0.10.2",
"spki 0.7.1",
"zeroize",
]
[[package]]
@@ -3176,15 +3114,6 @@ dependencies = [
"elliptic-curve 0.13.4",
]
[[package]]
name = "proc-macro-crate"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -3211,9 +3140,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.78"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
@@ -3308,9 +3237,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.35"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [
"proc-macro2",
]
@@ -3436,14 +3365,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.3"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.6",
"regex-syntax 0.8.2",
"regex-automata 0.3.8",
"regex-syntax 0.7.5",
]
[[package]]
@@ -3457,13 +3386,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.6"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.2",
"regex-syntax 0.7.5",
]
[[package]]
@@ -3474,15 +3403,15 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "regex-syntax"
version = "0.8.2"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "reqwest"
version = "0.11.24"
version = "0.11.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41"
dependencies = [
"base64 0.21.0",
"bytes",
@@ -3502,11 +3431,9 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
@@ -3610,9 +3537,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.1"
version = "0.9.0-pre.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f1471dbb4be5de45050e8ef7040625298ccb9efe941419ac2697088715925f"
checksum = "f16504cc31b04d2a5ec729f0c7e1b62e76634a9537f089df0ca1981dc8208a89"
dependencies = [
"byteorder",
"const-oid",
@@ -3621,7 +3548,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"pkcs1 0.7.5",
"pkcs1 0.7.2",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"signature 2.1.0",
@@ -3631,9 +3558,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.31.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
dependencies = [
"bitflags 2.4.0",
"fallible-iterator",
@@ -4169,21 +4096,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.26.1"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
version = "0.26.1"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.52",
"syn 2.0.15",
]
[[package]]
@@ -4205,21 +4132,15 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.52"
version = "2.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synstructure"
version = "0.12.6"
@@ -4283,9 +4204,9 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.16.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"smawk",
"unicode-linebreak",
@@ -4412,7 +4333,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
"syn 2.0.15",
]
[[package]]
@@ -4833,9 +4754,9 @@ dependencies = [
[[package]]
name = "weezl"
version = "0.1.8"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "widestring"
@@ -5179,13 +5100,12 @@ dependencies = [
[[package]]
name = "x25519-dalek"
version = "2.0.1"
version = "2.0.0-pre.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df"
dependencies = [
"curve25519-dalek 4.1.2",
"curve25519-dalek 3.2.0",
"rand_core 0.6.4",
"serde",
"zeroize",
]
@@ -5225,26 +5145,6 @@ dependencies = [
"time 0.3.20",
]
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "zeroize"
version = "1.5.7"

View File

@@ -115,9 +115,6 @@ module.exports = {
DC_PROVIDER_STATUS_BROKEN: 3,
DC_PROVIDER_STATUS_OK: 1,
DC_PROVIDER_STATUS_PREPARATION: 2,
DC_PUSH_CONNECTED: 2,
DC_PUSH_HEARTBEAT: 1,
DC_PUSH_NOT_CONNECTED: 0,
DC_QR_ACCOUNT: 250,
DC_QR_ADDR: 320,
DC_QR_ASK_VERIFYCONTACT: 200,

View File

@@ -115,9 +115,6 @@ export enum C {
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_PUSH_CONNECTED = 2,
DC_PUSH_HEARTBEAT = 1,
DC_PUSH_NOT_CONNECTED = 0,
DC_QR_ACCOUNT = 250,
DC_QR_ADDR = 320,
DC_QR_ASK_VERIFYCONTACT = 200,

View File

@@ -239,7 +239,7 @@ describe('Basic offline Tests', function () {
'delete_device_after',
'delete_server_after',
'deltachat_core_version',
'displayname',
'display_name',
'download_limit',
'e2ee_enabled',
'entered_account_settings',

View File

@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.136.6"
"version": "1.135.0"
}

View File

@@ -375,6 +375,22 @@ class Account:
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
def _wait_next_message_ids(self) -> List[int]:
"""Return IDs of all next messages from all chats."""
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
def wait_next_incoming_message(self) -> Message:
"""Waits until the next incoming message
with ID higher than given is received and returns it."""
while True:
message_ids = self._wait_next_message_ids()
for msg_id in message_ids:
message = Message.from_db(self, msg_id)
if message and not message.is_from_self() and not message.is_from_device():
self.set_config("last_msg_id", str(msg_id))
return message
def create_chat(self, obj) -> Chat:
"""Create a 1:1 chat with Account, Contact or e-mail address."""
return self.create_contact(obj).create_chat()

View File

@@ -182,12 +182,6 @@ class FFIEventTracker:
print(f"** SECUREJOINT-INVITER PROGRESS {target}", self.account)
break
def wait_securejoin_joiner_progress(self, target):
while True:
event = self.get_matching("DC_EVENT_SECUREJOIN_JOINER_PROGRESS")
if event.data2 >= target:
break
def wait_idle_inbox_ready(self):
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
so that new messages are not mistaken for old ones:

View File

@@ -10,6 +10,7 @@ import time
import weakref
import random
from queue import Queue
from threading import Event
from typing import Callable, Dict, List, Optional, Set
import pytest
@@ -115,7 +116,7 @@ def pytest_configure(config):
deltachat.register_global_plugin(la)
def pytest_report_header(config):
def pytest_report_header(config, startdir):
info = get_core_info()
summary = [
"Deltachat core={} sqlite={} journal_mode={}".format(
@@ -591,16 +592,23 @@ class ACFactory:
return ac1.create_chat(ac2)
def get_protected_chat(self, ac1: Account, ac2: Account):
class SetupPlugin:
def __init__(self) -> None:
self.member_added = Event()
@account_hookimpl
def ac_member_added(self, chat: deltachat.Chat, contact, actor, message):
self.member_added.set()
setupplugin = SetupPlugin()
ac1.add_account_plugin(setupplugin)
chat = ac1.create_group_chat("Protected Group", verified=True)
qr = chat.get_join_qr()
ac2.qr_join_chat(qr)
ac2._evtracker.wait_securejoin_joiner_progress(1000)
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
setupplugin.member_added.wait()
msg = ac2.wait_next_incoming_message()
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
msg = ac2.wait_next_incoming_message()
assert "Member Me " in msg.text and " added by " in msg.text
return chat

View File

@@ -547,14 +547,13 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac2_addr
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group.
- Bob's second devices goes online, but sees a contact request instead of the verified group.
- The "member added" message is not a system message but a plain text message.
- Bob's second device doesn't display the Alice's avatar (bug #5354).
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
missing, cannot encrypt".
"""
@@ -569,10 +568,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: set avatar")
avatar_path = data.get_path("d.png")
ac1.set_avatar(avatar_path)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
@@ -585,13 +580,11 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
ac2_offl.start_io()
# Receive "Member Me (<addr>) added by <addr>." message.
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
contact = msg_in.get_sender_contact()
assert msg_in.is_system_message()
assert contact.addr == ac1.get_config("addr")
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
msg_out = chat2.send_text("hello")

View File

@@ -44,21 +44,21 @@ def test_configure_generate_key(acfactory, lp):
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
msg_in = ac2._evtracker.wait_next_incoming_message()
msg_in = ac2.wait_next_incoming_message()
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
msg2_in = ac1._evtracker.wait_next_incoming_message()
msg2_in = ac1.wait_next_incoming_message()
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
msg3_in = ac2._evtracker.wait_next_incoming_message()
msg3_in = ac2.wait_next_incoming_message()
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
@@ -399,7 +399,7 @@ def test_enable_mvbox_move(acfactory, lp):
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
lp.sec("ac2: start without mvbox/sentbox threads")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
@@ -407,18 +407,10 @@ def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac2 and ac1: waiting for configuration")
acfactory.bring_accounts_online()
lp.sec("ac1: create and configure sentbox")
ac1.direct_imap.create_folder("Sent")
ac1.set_config("sentbox_watch", "1")
lp.sec("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
while ac1.get_config("configured_sentbox_folder") != "Sent":
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def test_move_works(acfactory):
ac1 = acfactory.new_online_configuring_account()
@@ -528,7 +520,7 @@ def test_forward_encrypted_to_unencrypted(acfactory, lp):
lp.sec("ac1: send encrypted message to ac2")
txt = "This should be encrypted"
chat.send_text(txt)
msg = ac2._evtracker.wait_next_incoming_message()
msg = ac2.wait_next_incoming_message()
assert msg.text == txt
assert msg.is_encrypted()

View File

@@ -52,8 +52,8 @@ class TestOfflineAccountBasic:
def test_wrong_db(self, tmp_path):
p = tmp_path / "hello.db"
p.write_text("123")
with pytest.raises(ValueError):
_account = Account(str(p))
account = Account(str(p))
assert not account.is_open()
def test_os_name(self, tmp_path):
p = tmp_path / "hello.db"

View File

@@ -1 +1 @@
2024-03-19
2024-02-13

View File

@@ -35,8 +35,12 @@ and an own build machine.
- `run_all.sh` builds Python wheels
- `zig-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Zig toolchain statically linked against musl libc.
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
## Triggering runs on the build machine locally (fast!)
There is experimental support for triggering a remote Python or Rust test run

12
scripts/build-python-docs.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
python3 -m venv venv
venv/bin/pip install ./python
venv/bin/pip install ./deltachat-rpc-client
venv/bin/pip install sphinx breathe sphinx_rtd_theme
venv/bin/sphinx-build -b html -a python/doc/ dist/html

View File

@@ -15,6 +15,55 @@ resources:
tag_filter: "v*"
jobs:
- name: doxygen
plan:
- get: deltachat-core-rust
trigger: true
# Build Doxygen documentation
- task: build-doxygen
config:
inputs:
- name: deltachat-core-rust
outputs:
- name: c-docs
image_resource:
source:
repository: alpine
type: registry-image
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache doxygen git
cd deltachat-core-rust
scripts/run-doxygen.sh
cd ..
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
- task: upload-c-docs
config:
inputs:
- name: c-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
- name: python-x86_64
plan:
- get: deltachat-core-rust

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.77.0
RUST_VERSION=1.72.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -1,26 +1,24 @@
#!/usr/bin/env python3
"""Build Python wheels for deltachat-rpc-server."""
"""Build Python wheels for deltachat-rpc-server.
Run scripts/zig-rpc-server.sh first."""
from pathlib import Path
from wheel.wheelfile import WheelFile
import tomllib
import tarfile
import sys
from io import BytesIO
def metadata_contents(version):
readme_text = (Path("deltachat-rpc-server") / "README.md").read_text()
return f"""Metadata-Version: 2.1
Name: deltachat-rpc-server
Version: {version}
Summary: Delta Chat JSON-RPC server
Description-Content-Type: text/markdown
{readme_text}
"""
def build_source_package(version, filename):
def build_source_package(version):
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
with tarfile.open(filename, "w:gz") as pkg:
def pack(name, contents):
@@ -101,7 +99,7 @@ setup(
def build_wheel(version, binary, tag, windows=False):
filename = f"deltachat_rpc_server-{version}-{tag}.whl"
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
with WheelFile(filename, "w") as wheel:
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
@@ -128,11 +126,9 @@ def main():
Path(binary).chmod(0o755)
wheel.write(
binary,
(
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server"
),
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/METADATA",
@@ -148,41 +144,54 @@ def main():
)
arch2tags = {
"x86_64-linux": "manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
"armv7l-linux": "linux_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
"armv6l-linux": "linux_armv6l",
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
"win64": "win_amd64",
"win32": "win32",
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
"x86_64-darwin": "macosx_10_7_x86_64",
"aarch64-darwin": "macosx_11_0_arm64",
}
def main():
with Path("Cargo.toml").open("rb") as fp:
cargo_manifest = tomllib.load(fp)
version = cargo_manifest["package"]["version"]
if sys.argv[1] == "source":
filename = f"deltachat-rpc-server-{version}.tar.gz"
build_source_package(version, filename)
else:
arch = sys.argv[1]
executable = sys.argv[2]
tags = arch2tags[arch]
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
cargo_toml = tomllib.load(f)
version = cargo_toml["package"]["version"]
Path("dist").mkdir(exist_ok=True)
build_source_package(version)
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-linux",
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-armv7-linux",
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-linux",
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-i686-linux",
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
)
if arch in ["win32", "win64"]:
build_wheel(
version,
executable,
f"py3-none-{tags}",
windows=True,
)
else:
build_wheel(version, executable, f"py3-none-{tags}")
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-macos",
"py3-none-macosx_10_7_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-macos",
"py3-none-macosx_11_0_arm64",
)
build_wheel(
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
)
build_wheel(
version,
"dist/deltachat-rpc-server-win64.exe",
"py3-none-win_amd64",
windows=True,
)
main()

57
scripts/zig-cc Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python
# /// pyproject
# [run]
# dependencies = [
# "ziglang==0.11.0"
# ]
# ///
import os
import subprocess
import sys
def flag_filter(flag: str) -> bool:
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
if flag == "-latomic":
return False
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
return False
if "self-contained" in flag and "crt" in flag:
return False
return True
def main():
args = [flag for flag in sys.argv[1:] if flag_filter(flag)]
zig_target = os.environ["ZIG_TARGET"]
zig_cpu = os.environ.get("ZIG_CPU")
if zig_cpu:
zig_cpu_args = ["-mcpu=" + zig_cpu]
args = [x for x in args if not x.startswith("-march")]
else:
zig_cpu_args = []
# Disable atomics and use locks instead in OpenSSL.
# Zig toolchains do not provide atomics.
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
args += ["-DBROKEN_CLANG_ATOMICS"]
subprocess.run(
[
sys.executable,
"-m",
"ziglang",
"cc",
"-target",
zig_target,
*zig_cpu_args,
*args,
],
check=True,
)
main()

50
scripts/zig-rpc-server.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/sh
#
# Build statically linked deltachat-rpc-server using zig.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86-linux-musl" \
cargo build --release --target i686-unknown-linux-musl -p deltachat-rpc-server --features vendored
rustup target add armv7-unknown-linux-musleabihf
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="arm-linux-musleabihf" \
ZIG_CPU="generic+v7a+vfp3-d32+thumb2-neon" \
cargo build --release --target armv7-unknown-linux-musleabihf -p deltachat-rpc-server --features vendored
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo build --release --target x86_64-unknown-linux-musl -p deltachat-rpc-server --features vendored
rustup target add aarch64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
mkdir -p dist
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux

View File

@@ -119,9 +119,8 @@ All group members form the member list.
To allow different groups with the same members,
groups are identified by a group-id.
The group-id MUST be created only from the characters
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`,
MUST have a length of at least 11 characters
and no more than 32 characters.
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`
and MUST have a length of at least 11 characters.
Groups MUST have a group-name.
The group-name is any non-zero-length UTF-8 string.

View File

@@ -17,9 +17,8 @@ use tokio::sync::oneshot;
#[cfg(not(target_os = "ios"))]
use tokio::time::{sleep, Duration};
use crate::context::{Context, ContextBuilder};
use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;
/// Account manager, that can handle multiple accounts in a single place.
@@ -38,9 +37,6 @@ pub struct Accounts {
/// This way changing a translation for one context automatically
/// changes it for all other contexts.
pub(crate) stockstrings: StockStrings,
/// Push notification subscriber shared between accounts.
push_subscriber: PushSubscriber,
}
impl Accounts {
@@ -77,9 +73,8 @@ impl Accounts {
.context("failed to load accounts config")?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
let accounts = config
.load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
.load_accounts(&events, &stockstrings, &dir)
.await
.context("failed to load accounts")?;
@@ -89,7 +84,6 @@ impl Accounts {
accounts,
events,
stockstrings,
push_subscriber,
})
}
@@ -126,17 +120,13 @@ impl Accounts {
let account_config = self.config.new_account().await?;
let dbfile = account_config.dbfile(&self.dir);
let ctx = ContextBuilder::new(dbfile)
.with_id(account_config.id)
.with_events(self.events.clone())
.with_stock_strings(self.stockstrings.clone())
.with_push_subscriber(self.push_subscriber.clone())
.build()
.await?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
let ctx = Context::new(
&dbfile,
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -145,15 +135,14 @@ impl Accounts {
/// Adds a new closed account.
pub async fn add_closed_account(&mut self) -> Result<u32> {
let account_config = self.config.new_account().await?;
let dbfile = account_config.dbfile(&self.dir);
let ctx = ContextBuilder::new(dbfile)
.with_id(account_config.id)
.with_events(self.events.clone())
.with_stock_strings(self.stockstrings.clone())
.with_push_subscriber(self.push_subscriber.clone())
.build()
.await?;
let ctx = Context::new_closed(
&account_config.dbfile(&self.dir),
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -348,12 +337,6 @@ impl Accounts {
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
/// Sets notification token for Apple Push Notification service.
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
self.push_subscriber.set_device_token(token).await;
Ok(())
}
}
/// Configuration file name.
@@ -539,24 +522,24 @@ impl Config {
&self,
events: &Events,
stockstrings: &StockStrings,
push_subscriber: PushSubscriber,
dir: &Path,
) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let dbfile = account_config.dbfile(dir);
let ctx = ContextBuilder::new(dbfile.clone())
.with_id(account_config.id)
.with_events(events.clone())
.with_stock_strings(stockstrings.clone())
.with_push_subscriber(push_subscriber.clone())
.build()
.await
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
let ctx = Context::new(
&account_config.dbfile(dir),
account_config.id,
events.clone(),
stockstrings.clone(),
)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile(dir)
)
})?;
accounts.insert(account_config.id, ctx);
}

View File

@@ -5,15 +5,11 @@ use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use std::iter::FusedIterator;
use std::mem;
use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::{
DynamicImage, GenericImage, GenericImageView, ImageFormat, ImageOutputFormat, Pixel, Rgba,
};
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
@@ -316,30 +312,6 @@ impl<'a> BlobObject<'a> {
true
}
/// Returns path to the stored Base64-decoded blob.
///
/// If `data` represents an image of known format, this adds the corresponding extension to
/// `suggested_file_stem`.
pub(crate) async fn store_from_base64(
context: &Context,
data: &str,
suggested_file_stem: &str,
) -> Result<String> {
let buf = base64::engine::general_purpose::STANDARD.decode(data)?;
let ext = if let Ok(format) = image::guess_format(&buf) {
if let Some(ext) = format.extensions_str().first() {
format!(".{ext}")
} else {
String::new()
}
} else {
String::new()
};
let blob =
BlobObject::create(context, &format!("{suggested_file_stem}{ext}"), &buf).await?;
Ok(blob.as_name().to_string())
}
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
let blob_abs = self.to_abs_path();
@@ -416,8 +388,6 @@ impl<'a> BlobObject<'a> {
max_bytes: usize,
strict_limits: bool,
) -> Result<Option<String>> {
// Add white background only to avatars to spare the CPU.
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
@@ -451,15 +421,13 @@ impl<'a> BlobObject<'a> {
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let jpeg_quality = 75;
let fmt = ImageFormat::from_path(&blob_abs);
let ofmt = match fmt {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
Ok(ImageFormat::Jpeg) => {
add_white_bg = false;
_ => {
let jpeg_quality = 75;
ImageOutputFormat::Jpeg(jpeg_quality)
}
_ => ImageOutputFormat::Jpeg(jpeg_quality),
};
// We need to rewrite images with Exif to remove metadata such as location,
// camera model, etc.
@@ -470,18 +438,14 @@ impl<'a> BlobObject<'a> {
let do_scale = exceeds_max_bytes
|| strict_limits
&& (exceeds_wh
|| exif.is_some() && {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
encoded_img_exceeds_bytes(
|| exif.is_some()
&& encoded_img_exceeds_bytes(
context,
&img,
ofmt.clone(),
max_bytes,
&mut encoded,
)?
});
)?);
if do_scale {
if !exceeds_wh {
@@ -494,9 +458,6 @@ impl<'a> BlobObject<'a> {
}
loop {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(
@@ -539,9 +500,6 @@ impl<'a> BlobObject<'a> {
}
if encoded.is_empty() {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
encode_img(&img, ofmt, &mut encoded)?;
}
@@ -711,21 +669,11 @@ fn encoded_img_exceeds_bytes(
Ok(false)
}
/// Removes transparency from an image using a white background.
fn add_white_bg(img: &mut DynamicImage) {
for y in 0..img.height() {
for x in 0..img.width() {
let mut p = Rgba([255u8, 255, 255, 255]);
p.blend(&img.get_pixel(x, y));
img.put_pixel(x, y, p);
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use fs::File;
use image::Pixel;
use image::{GenericImageView, Pixel};
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
@@ -937,40 +885,6 @@ mod tests {
assert!(!stem.contains('?'));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_white_bg() {
let t = TestContext::new().await;
let bytes0 = include_bytes!("../test-data/image/logo.png").as_slice();
let bytes1 = include_bytes!("../test-data/image/avatar900x900.png").as_slice();
for (bytes, color) in [
(bytes0, [255u8, 255, 255, 255]),
(bytes1, [253u8, 198, 0, 255]),
] {
let avatar_src = t.dir.path().join("avatar.png");
fs::write(&avatar_src, bytes).await.unwrap();
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
let img_wh = 128;
let maybe_sticker = &mut false;
let strict_limits = true;
blob.recode_to_size(
&t,
blob.to_abs_path(),
maybe_sticker,
img_wh,
20_000,
strict_limits,
)
.unwrap();
tokio::task::block_in_place(move || {
let img = image::open(blob.to_abs_path()).unwrap();
assert!(img.width() == img_wh);
assert!(img.height() == img_wh);
assert_eq!(img.get_pixel(0, 0), Rgba(color));
});
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;

View File

@@ -2,10 +2,11 @@
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
@@ -43,7 +44,7 @@ use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty, SystemTime,
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
@@ -1789,10 +1790,18 @@ impl Chat {
update_msg_id: Option<MsgId>,
timestamp: i64,
) -> Result<MsgId> {
let mut new_references = "".into();
let mut to_id = 0;
let mut location_id = 0;
let new_rfc724_mid = create_outgoing_rfc724_mid();
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
Chattype::Group => Some(self.grpid.as_str()),
_ => None,
};
create_outgoing_rfc724_mid(grpid, &from)
};
if self.typ == Chattype::Single {
if let Some(id) = context
@@ -1830,72 +1839,58 @@ impl Chat {
// reset encrypt error state eg. for forwarding
msg.param.remove(Param::ErroneousE2ee);
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
// Set "References:" to identify the "thread" of the conversation.
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
let new_references;
if self.is_self_talk() {
// As self-talks are mainly used to transfer data between devices,
// we do not set In-Reply-To/References in this case.
new_references = String::new();
} else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
// We don't filter `OutPending` and `OutFailed` messages because the new message for
// which `parent_query()` is done may assume that it will be received in a context
// affected by those messages, e.g. they could add new members to a group and the
// new message will contain them in "To:". Anyway recipients must be prepared to
// orphaned references.
self
.id
.get_parent_mime_headers(context, MessageState::OutPending)
.await?
{
// "In-Reply-To:" is not changed if it is set manually.
// This does not affect "References:" header, it will contain "default parent" (the
// latest message in the thread) anyway.
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
msg.in_reply_to = Some(parent_rfc724_mid.clone());
}
// Use parent `In-Reply-To` as a fallback
// in case parent message has no `References` header
// as specified in RFC 5322:
// > If the parent message does not contain
// > a "References:" field but does have an "In-Reply-To:" field
// > containing a single message identifier, then the "References:" field
// > will contain the contents of the parent's "In-Reply-To:" field
// > followed by the contents of the parent's "Message-ID:" field (if
// > any).
let parent_references = if parent_references.is_empty() {
parent_in_reply_to
} else {
parent_references
};
// The whole list of messages referenced may be huge.
// Only take 2 recent references and add third from `In-Reply-To`.
let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
references_vec.reverse();
if !parent_rfc724_mid.is_empty()
&& !references_vec.contains(&parent_rfc724_mid.as_str())
// set "In-Reply-To:" to identify the message to which the composed message is a reply;
// set "References:" to identify the "thread" of the conversation;
// both according to RFC 5322 3.6.4, page 25
//
// as self-talks are mainly used to transfer data between devices,
// we do not set In-Reply-To/References in this case.
if !self.is_self_talk() {
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
// We don't filter `OutPending` and `OutFailed` messages because the new message for
// which `parent_query()` is done may assume that it will be received in a context
// affected by those messages, e.g. they could add new members to a group and the
// new message will contain them in "To:". Anyway recipients must be prepared to
// orphaned references.
self
.id
.get_parent_mime_headers(context, MessageState::OutPending)
.await?
{
references_vec.push(&parent_rfc724_mid)
}
// "In-Reply-To:" is not changed if it is set manually.
// This does not affect "References:" header, it will contain "default parent" (the
// latest message in the thread) anyway.
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
msg.in_reply_to = Some(parent_rfc724_mid.clone());
}
if references_vec.is_empty() {
// As a fallback, use our Message-ID,
// same as in the case of top-level message.
new_references = new_rfc724_mid.clone();
// the whole list of messages referenced may be huge;
// only use the oldest and the parent message
let parent_references = parent_references
.find(' ')
.and_then(|n| parent_references.get(..n))
.unwrap_or(&parent_references);
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
// angle brackets are added by the mimefactory later
new_references = format!("{parent_references} {parent_rfc724_mid}");
} else if !parent_references.is_empty() {
new_references = parent_references.to_string();
} else if !parent_in_reply_to.is_empty() && !parent_rfc724_mid.is_empty() {
new_references = format!("{parent_in_reply_to} {parent_rfc724_mid}");
} else if !parent_in_reply_to.is_empty() {
new_references = parent_in_reply_to;
} else {
// as a fallback, use our Message-ID, see reasoning below.
new_references = new_rfc724_mid.clone();
}
} else {
new_references = references_vec.join(" ");
// this is a top-level message, add our Message-ID as first reference.
// as we always try to extract the grpid also from `References:`-header,
// this allows group conversations also if smtp-server as outlook change `Message-ID:`-header
// (MUAs usually keep the first Message-ID in `References:`-header unchanged).
new_references = new_rfc724_mid.clone();
}
} else {
// This is a top-level message.
// Add our Message-ID as first references.
// This allows us to identify replies to our message even if
// email server such as Outlook changes `Message-ID:` header.
// MUAs usually keep the first Message-ID in `References:` header unchanged.
new_references = new_rfc724_mid.clone();
}
// add independent location to database
@@ -2817,12 +2812,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.chat_id.set_gossiped_timestamp(context, now).await?;
}
if rendered_msg.is_group {
msg.chat_id
.update_timestamp(context, Param::MemberListTimestamp, now)
.await?;
}
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
@@ -3667,7 +3656,7 @@ pub enum MuteDuration {
Forever,
/// Chat is muted for a limited period of time.
Until(std::time::SystemTime),
Until(SystemTime),
}
impl rusqlite::types::ToSql for MuteDuration {
@@ -4162,7 +4151,7 @@ pub async fn add_device_msg_with_importance(
if let Some(msg) = msg {
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
let rfc724_mid = create_outgoing_rfc724_mid();
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
prepare_msg_blob(context, msg).await?;
let timestamp_sent = create_smeared_timestamp(context);
@@ -4302,7 +4291,7 @@ pub(crate) async fn add_info_msg_with_cmd(
parent: Option<&Message>,
from_id: Option<ContactId>,
) -> Result<MsgId> {
let rfc724_mid = create_outgoing_rfc724_mid();
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let mut param = Params::new();
@@ -4485,8 +4474,9 @@ impl Context {
#[cfg(test)]
mod tests {
use super::*;
use crate::chatlist::get_archived_cnt;
use crate::chatlist::{get_archived_cnt, Chatlist};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{sync, TestContext, TestContextManager};
@@ -5942,11 +5932,11 @@ mod tests {
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 2);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 0);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 2);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
@@ -5963,7 +5953,7 @@ mod tests {
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let msg = sent_msg.payload();
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
let msg = msg.replace("Chat-", "XXXX-");
assert_eq!(msg.match_indices("Chat-").count(), 0);

View File

@@ -5,11 +5,9 @@ use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::constants::{self, DC_VERSION_STR};
@@ -363,8 +361,8 @@ impl Config {
///
/// This must be checked on both sides so that if there are different client versions, the
/// synchronisation of a particular option is either done or not done in both directions.
/// Moreover, receivers of a config value need to check if a key can be synced because if it is
/// a file path, it could otherwise lead to exfiltration of files from a receiver's
/// Moreover, receivers of a config value need to check if a key can be synced because some
/// settings (e.g. Avatar path) could otherwise lead to exfiltration of files from a receiver's
/// device if we assume an attacker to have control of a device in a multi-device setting or if
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
/// mustn't be controlled by other devices.
@@ -375,11 +373,7 @@ impl Config {
}
matches!(
self,
Self::Displayname
| Self::MdnsEnabled
| Self::ShowEmails
| Self::Selfavatar
| Self::Selfstatus,
Self::Displayname | Self::MdnsEnabled | Self::ShowEmails
)
}
@@ -475,15 +469,6 @@ impl Context {
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Returns true if sentbox ("Sent" folder) should be watched.
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SentboxWatch).await?
&& self
.get_config(Config::ConfiguredSentboxFolder)
.await?
.is_some())
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -518,23 +503,6 @@ impl Context {
}
}
/// Executes [`SyncData::Config`] item sent by other device.
pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> {
let config_value;
let value = match key {
Config::Selfavatar if value.is_empty() => None,
Config::Selfavatar => {
config_value = BlobObject::store_from_base64(self, value, "avatar").await?;
Some(config_value.as_str())
}
_ => Some(value),
};
match key.is_synced() {
true => self.set_config_ex(Nosync, *key, value).await,
false => Ok(()),
}
}
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
match key {
Config::Socks5Enabled
@@ -574,9 +542,6 @@ impl Context {
_ => Default::default(),
};
self.set_config_internal(key, value).await?;
if key == Config::SentboxWatch {
self.last_full_folder_scan.lock().await.take();
}
Ok(())
}
@@ -600,24 +565,15 @@ impl Context {
.execute("UPDATE contacts SET selfavatar_sent=0;", ())
.await?;
match value {
Some(path) => {
let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?;
Some(value) => {
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
blob.recode_to_avatar_size(self).await?;
self.sql
.set_raw_config(key.as_ref(), Some(blob.as_name()))
.await?;
if sync {
let buf = fs::read(blob.to_abs_path()).await?;
better_value = base64::engine::general_purpose::STANDARD.encode(buf);
value = Some(&better_value);
}
}
None => {
self.sql.set_raw_config(key.as_ref(), None).await?;
if sync {
better_value = String::new();
value = Some(&better_value);
}
}
}
self.emit_event(EventType::SelfavatarChanged);
@@ -785,10 +741,13 @@ fn get_config_keys_string() -> String {
#[cfg(test)]
mod tests {
use std::string::ToString;
use num_traits::FromPrimitive;
use super::*;
use crate::test_utils::{sync, TestContext, TestContextManager};
use crate::constants;
use crate::test_utils::{sync, TestContext};
#[test]
fn test_to_string() {
@@ -982,95 +941,14 @@ mod tests {
assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
// Usual sync scenario.
async fn test_config_str(
alice0: &TestContext,
alice1: &TestContext,
key: Config,
val: &str,
) -> Result<()> {
alice0.set_config(key, Some(val)).await?;
sync(alice0, alice1).await;
assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
Ok(())
}
test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.set_config(Config::Displayname, Some("Alice Sync"))
.await?;
sync(&alice0, &alice1).await;
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
alice0.set_config(Config::Selfavatar, None).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
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();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let status = "Synced via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.pop_sent_msg().await; // Sync message
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
assert_eq!(
alice1.get_config(Config::Selfstatus).await?,
Some(status.to_string())
alice1.get_config(Config::Displayname).await?,
Some("Alice Sync".to_string())
);
sync(alice1, alice0).await;
assert_eq!(
alice0.get_config(Config::Selfstatus).await?,
Some(status1.to_string())
);
// Need a chat with another contact to send self-avatar.
let bob = &tcm.bob().await;
let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.pop_sent_msg().await; // Sync message
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;
alice1
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
alice1.recv_msg(&sent_msg).await;
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
sync(alice1, alice0).await;
assert!(alice0
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".jpg"))
.is_some());
Ok(())
}

View File

@@ -25,7 +25,7 @@ use tokio::task;
use crate::config::{self, Config};
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::imap::{session::Session as ImapSession, Imap};
use crate::imap::Imap;
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
@@ -395,7 +395,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Configure IMAP
let mut imap: Option<(Imap, ImapSession)> = None;
let mut imap: Option<Imap> = None;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
@@ -433,7 +433,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
let (mut imap, mut imap_session) = match imap {
let mut imap = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
};
@@ -454,11 +454,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
imap.configure_folders(ctx, create_mvbox).await?;
imap_session
.select_with_uidvalidity(ctx, "INBOX")
imap.select_with_uidvalidity(ctx, "INBOX")
.await
.context("could not read INBOX status")?;
@@ -578,7 +576,7 @@ async fn try_imap_one_param(
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
) -> Result<(Imap, ImapSession), ConfigurationError> {
) -> Result<Imap, ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
@@ -616,9 +614,9 @@ async fn try_imap_one_param(
msg: format!("{err:#}"),
})
}
Ok(session) => {
Ok(()) => {
info!(context, "success: {}", inf);
Ok((imap, session))
Ok(imap)
}
}
}

View File

@@ -2,10 +2,11 @@
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
@@ -35,7 +36,7 @@ use crate::sql::{self, params_iter};
use crate::sync::{self, Sync::*};
use crate::tools::{
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
EmailAddress, SystemTime,
EmailAddress,
};
use crate::{chat, stock_str};
@@ -1536,7 +1537,7 @@ WHERE type=? AND id IN (
/// 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;
/// this typically happens if we see message with our own profile image.
/// this typically happens if we see message with our own profile image, sent from another device.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: ContactId,
@@ -1549,7 +1550,7 @@ pub(crate) async fn set_profile_image(
if contact_id == ContactId::SELF {
if was_encrypted {
context
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
.set_config_internal(Config::Selfavatar, Some(profile_image))
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar.");
@@ -1563,7 +1564,7 @@ pub(crate) async fn set_profile_image(
if contact_id == ContactId::SELF {
if was_encrypted {
context
.set_config_ex(Nosync, Config::Selfavatar, None)
.set_config_internal(Config::Selfavatar, None)
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar deletion.");
@@ -1596,7 +1597,7 @@ pub(crate) async fn set_status(
if contact_id == ContactId::SELF {
if encrypted && has_chat_version {
context
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
.set_config_internal(Config::Selfstatus, Some(&status))
.await?;
}
} else {

View File

@@ -20,17 +20,14 @@ use crate::config::Config;
use crate::constants::{
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
};
use crate::contact::{Contact, ContactId};
use crate::contact::Contact;
use crate::debug_logging::DebugLogging;
use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{convert_folder_meaning, SchedulerState};
use crate::sql::Sql;
@@ -87,8 +84,6 @@ pub struct ContextBuilder {
events: Events,
stock_strings: StockStrings,
password: Option<String>,
push_subscriber: Option<PushSubscriber>,
}
impl ContextBuilder {
@@ -104,7 +99,6 @@ impl ContextBuilder {
events: Events::new(),
stock_strings: StockStrings::new(),
password: None,
push_subscriber: None,
}
}
@@ -159,32 +153,11 @@ impl ContextBuilder {
self
}
/// Sets push subscriber.
pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self {
self.push_subscriber = Some(push_subscriber);
self
}
/// Builds the [`Context`] without opening it.
pub async fn build(self) -> Result<Context> {
let push_subscriber = self.push_subscriber.unwrap_or_default();
let context = Context::new_closed(
&self.dbfile,
self.id,
self.events,
self.stock_strings,
push_subscriber,
)
.await?;
Ok(context)
}
/// Builds the [`Context`] and opens it.
///
/// Returns error if context cannot be opened with the given passphrase.
/// Opens the [`Context`].
pub async fn open(self) -> Result<Context> {
let password = self.password.clone().unwrap_or_default();
let context = self.build().await?;
let context =
Context::new_closed(&self.dbfile, self.id, self.events, self.stock_strings).await?;
let password = self.password.unwrap_or_default();
match context.open(password).await? {
true => Ok(context),
false => bail!("database could not be decrypted, incorrect or missing password"),
@@ -280,13 +253,6 @@ pub struct InnerContext {
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
/// because the lock is used from synchronous [`Context::emit_event`].
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
/// Push subscriber to store device token
/// and register for heartbeat notifications.
pub(crate) push_subscriber: PushSubscriber,
/// True if account has subscribed to push notifications via IMAP.
pub(crate) push_subscribed: AtomicBool,
}
/// The state of ongoing process.
@@ -332,8 +298,7 @@ impl Context {
events: Events,
stock_strings: StockStrings,
) -> Result<Context> {
let context =
Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
let context = Self::new_closed(dbfile, id, events, stock_strings).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
@@ -348,7 +313,6 @@ impl Context {
id: u32,
events: Events,
stockstrings: StockStrings,
push_subscriber: PushSubscriber,
) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
@@ -357,14 +321,7 @@ impl Context {
if !blobdir.exists() {
tokio::fs::create_dir_all(&blobdir).await?;
}
let context = Context::with_blobdir(
dbfile.into(),
blobdir,
id,
events,
stockstrings,
push_subscriber,
)?;
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events, stockstrings)?;
Ok(context)
}
@@ -407,7 +364,6 @@ impl Context {
id: u32,
events: Events,
stockstrings: StockStrings,
push_subscriber: PushSubscriber,
) -> Result<Context> {
ensure!(
blobdir.is_dir(),
@@ -442,8 +398,6 @@ impl Context {
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
};
let ctx = Context {
@@ -507,13 +461,13 @@ impl Context {
// connection
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
let mut session = connection.prepare(self).await?;
connection.prepare(self).await?;
// fetch imap folders
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
let (_, watch_folder) = convert_folder_meaning(self, folder_meaning).await?;
connection
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
.fetch_move_delete(self, &watch_folder, folder_meaning)
.await?;
}
@@ -530,7 +484,7 @@ impl Context {
};
if quota_needs_update {
if let Err(err) = self.update_recent_quota(&mut session).await {
if let Err(err) = self.update_recent_quota(&mut connection).await {
warn!(self, "Failed to update quota: {err:#}.");
}
}
@@ -761,7 +715,7 @@ impl Context {
);
res.insert("journal_mode", journal_mode);
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("displayname", displayname.unwrap_or_else(|| unset.into()));
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
@@ -804,12 +758,6 @@ impl Context {
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert(
"save_mime_headers",
self.get_config_bool(Config::SaveMimeHeaders)
.await?
.to_string(),
);
res.insert(
"download_limit",
self.get_config_int(Config::DownloadLimit)
@@ -929,16 +877,6 @@ impl Context {
}
async fn get_self_report(&self) -> Result<String> {
#[derive(Default)]
struct ChatNumbers {
protected: u32,
protection_broken: u32,
opportunistic_dc: u32,
opportunistic_mua: u32,
unencrypted_dc: u32,
unencrypted_mua: u32,
}
let mut res = String::new();
res += &format!("core_version {}\n", get_version_str());
@@ -966,76 +904,6 @@ impl Context {
let key_created = secret_key.created_at().timestamp();
res += &format!("key_created {}\n", key_created);
// how many of the chats active in the last months are:
// - protected
// - protection-broken
// - opportunistic-encrypted and the contact uses Delta Chat
// - opportunistic-encrypted and the contact uses a classical MUA
// - unencrypted and the contact uses Delta Chat
// - unencrypted and the contact uses a classical MUA
let three_months_ago = time().saturating_sub(3600 * 24 * 30 * 3);
let chats = self
.sql
.query_map(
"SELECT c.protected, m.param, m.msgrmsg
FROM chats c
JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND hidden=0
AND download_state=?
AND to_id!=?
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND (c.blocked=0 OR c.blocked=2)
AND IFNULL(m.timestamp,c.created_timestamp) > ?
GROUP BY c.id",
(DownloadState::Done, ContactId::INFO, three_months_ago),
|row| {
let protected: ProtectionStatus = row.get(0)?;
let message_param: Params =
row.get::<_, String>(1)?.parse().unwrap_or_default();
let is_dc_message: bool = row.get(2)?;
Ok((protected, message_param, is_dc_message))
},
|rows| {
let mut chats = ChatNumbers::default();
for row in rows {
let (protected, message_param, is_dc_message) = row?;
let encrypted = message_param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or(false);
if protected == ProtectionStatus::Protected {
chats.protected += 1;
} else if protected == ProtectionStatus::ProtectionBroken {
chats.protection_broken += 1;
} else if encrypted {
if is_dc_message {
chats.opportunistic_dc += 1;
} else {
chats.opportunistic_mua += 1;
}
} else if is_dc_message {
chats.unencrypted_dc += 1;
} else {
chats.unencrypted_mua += 1;
}
}
Ok(chats)
},
)
.await?;
res += &format!("chats_protected {}\n", chats.protected);
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
res += &format!("chats_unencrypted_mua {}\n", chats.unencrypted_mua);
let self_reporting_id = match self.get_config(Config::SelfReportingId).await? {
Some(id) => id,
None => {
@@ -1349,18 +1217,24 @@ pub fn get_version_str() -> &'static str {
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use strum::IntoEnumIterator;
use tempfile::tempdir;
use super::*;
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::message::{Message, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext};
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
use crate::tools::create_outgoing_rfc724_mid;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_wrong_db() -> Result<()> {
@@ -1395,7 +1269,7 @@ mod tests {
\n\
hello\n",
contact.get_addr(),
create_outgoing_rfc724_mid()
create_outgoing_rfc724_mid(None, contact.get_addr())
);
println!("{msg}");
receive_imf(t, msg.as_bytes(), false).await.unwrap();
@@ -1545,14 +1419,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(
dbfile,
blobdir,
1,
Events::new(),
StockStrings::new(),
Default::default(),
);
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
assert!(res.is_err());
}
@@ -1561,14 +1428,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(
dbfile,
blobdir,
1,
Events::new(),
StockStrings::new(),
Default::default(),
);
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
assert!(res.is_err());
}
@@ -1583,14 +1443,14 @@ mod tests {
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();
assert!(info.contains_key("database_dir"));
assert!(info.get("database_dir").is_some());
}
#[test]
fn test_get_info_no_context() {
let info = get_info();
assert!(info.contains_key("deltachat_core_version"));
assert!(!info.contains_key("database_dir"));
assert!(info.get("deltachat_core_version").is_some());
assert!(info.get("database_dir").is_none());
assert_eq!(info.get("level").unwrap(), "awesome");
}
@@ -1611,6 +1471,7 @@ mod tests {
"mail_port",
"mail_security",
"notify_about_wrong_pw",
"save_mime_headers",
"self_reporting_id",
"selfstatus",
"send_server",
@@ -1790,18 +1651,16 @@ mod tests {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let context = ContextBuilder::new(dbfile.clone())
.with_id(1)
.build()
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
drop(context);
let context = ContextBuilder::new(dbfile)
.with_id(2)
.build()
let id = 2;
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);
@@ -1817,9 +1676,8 @@ mod tests {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let context = ContextBuilder::new(dbfile)
.with_id(1)
.build()
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);

View File

@@ -27,8 +27,12 @@ pub fn try_decrypt(
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => return Ok(None),
Some(res) => res,
};
decrypt_part(
@@ -65,29 +69,28 @@ pub(crate) async fn prepare_decryption(
});
}
let autocrypt_header = if context.is_self_addr(from).await? {
None
} else if let Some(aheader_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
let autocrypt_header =
if let Some(autocrypt_header_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&autocrypt_header_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
} else {
None
};
let dkim_results = handle_authres(context, mail, from, message_time).await?;
let allow_aeap = get_encrypted_mime(mail).is_some();
let peerstate = get_autocrypt_peerstate(
context,
from,
@@ -95,7 +98,6 @@ pub(crate) async fn prepare_decryption(
message_time,
// Disallowing keychanges is disabled for now:
true, // dkim_results.allow_keychange,
allow_aeap,
)
.await?;
@@ -124,13 +126,6 @@ pub struct DecryptionInfo {
pub(crate) dkim_results: authres::DkimResults,
}
/// Returns a reference to the encrypted payload of a message.
fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
}
/// Returns a reference to the encrypted payload of a ["Mixed
/// Up"][pgpmime-message-mangling] message.
///
@@ -269,7 +264,6 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
}
}
/// Returns public keyring for `peerstate`.
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
let mut public_keyring_for_validate = Vec::new();
if let Some(peerstate) = peerstate {
@@ -297,28 +291,23 @@ pub(crate) async fn get_autocrypt_peerstate(
autocrypt_header: Option<&Aheader>,
message_time: i64,
allow_change: bool,
allow_aeap: bool,
) -> Result<Option<Peerstate>> {
let allow_change = allow_change && !context.is_self_addr(from).await?;
let mut peerstate;
// Apply Autocrypt header
if let Some(header) = autocrypt_header {
if allow_aeap {
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
// For security reasons, for now, we only do a transition
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
} else {
peerstate = Peerstate::from_addr(context, from).await?;
}
// The "from_verified_fingerprint" part is for AEAP:
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
// For security reasons, for now, we only do a transition
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {

View File

@@ -301,7 +301,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
let href = href
.decode_and_unescape_value(reader)
.unwrap_or_default()
.to_string();
.to_lowercase();
if !href.is_empty() {
dehtml.last_href = Some(href);
@@ -463,13 +463,6 @@ mod tests {
assert_eq!(plain, "[text](url)");
}
#[test]
fn test_dehtml_case_sensitive_link() {
let html = "<html><A HrEf=\"https://foo.bar/Data\">case in URLs matter</A></html>";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "[case in URLs matter](https://foo.bar/Data)");
}
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";

View File

@@ -3,13 +3,13 @@
use std::cmp::max;
use std::collections::BTreeMap;
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context;
use crate::imap::session::Session;
use crate::imap::{Imap, ImapActionResult};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::tools::time;
@@ -129,11 +129,9 @@ impl Message {
/// Actually download a message partially downloaded before.
///
/// Most messages are downloaded automatically on fetch instead.
pub(crate) async fn download_msg(
context: &Context,
msg_id: MsgId,
session: &mut Session,
) -> Result<()> {
pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Imap) -> Result<()> {
imap.prepare(context).await?;
let msg = Message::load_from_db(context, msg_id).await?;
let row = context
.sql
@@ -154,7 +152,7 @@ pub(crate) async fn download_msg(
return Err(anyhow!("Call download_full() again to try over."));
};
session
match imap
.fetch_single_msg(
context,
&server_folder,
@@ -162,11 +160,16 @@ pub(crate) async fn download_msg(
server_uid,
msg.rfc724_mid.clone(),
)
.await?;
Ok(())
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
Err(anyhow!("Call download_full() again to try over."))
}
ImapActionResult::Success => Ok(()),
}
}
impl Session {
impl Imap {
/// Download a single message and pipe it to receive_imf().
///
/// receive_imf() is not directly aware that this is a result of a call to download_msg(),
@@ -178,19 +181,20 @@ impl Session {
uidvalidity: u32,
uid: u32,
rfc724_mid: String,
) -> Result<()> {
if uid == 0 {
bail!("Attempt to fetch UID 0");
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
.await
{
return imapresult;
}
self.select_folder(context, Some(folder)).await?;
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (last_uid, _received) = self
let (last_uid, _received) = match self
.fetch_many_msgs(
context,
folder,
@@ -200,11 +204,16 @@ impl Session {
false,
false,
)
.await?;
.await
{
Ok(res) => res,
Err(_) => return ImapActionResult::Failed,
};
if last_uid.is_none() {
bail!("Failed to fetch UID {uid}");
ImapActionResult::Failed
} else {
ImapActionResult::Success
}
Ok(())
}
}
@@ -253,6 +262,7 @@ mod tests {
use super::*;
use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::TestContext;

View File

@@ -95,7 +95,6 @@ impl EncryptHelper {
verified: bool,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, String)>,
compress: bool,
) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new();
@@ -136,7 +135,7 @@ impl EncryptHelper {
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?;
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key)).await?;
Ok(ctext)
}

View File

@@ -64,10 +64,10 @@
use std::cmp::max;
use std::collections::BTreeSet;
use std::fmt;
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Result};
use async_channel::Receiver;
@@ -85,7 +85,7 @@ use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::sql::{self, params_iter};
use crate::stock_str;
use crate::tools::{duration_to_str, time, SystemTime};
use crate::tools::{duration_to_str, time};
/// Ephemeral timer value.
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
@@ -131,9 +131,9 @@ impl Default for Timer {
}
}
impl fmt::Display for Timer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_u32())
impl ToString for Timer {
fn to_string(&self) -> String {
self.to_u32().to_string()
}
}
@@ -569,21 +569,9 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
"Ephemeral loop waiting for deletion in {} or interrupt",
duration_to_str(duration)
);
match timeout(duration, interrupt_receiver.recv()).await {
Ok(Ok(())) => {
// received an interruption signal, recompute waiting time (if any)
continue;
}
Ok(Err(err)) => {
warn!(
context,
"Interrupt channel closed, ephemeral loop exits now: {err:#}."
);
return;
}
Err(_err) => {
// Timeout.
}
if timeout(duration, interrupt_receiver.recv()).await.is_ok() {
// received an interruption signal, recompute waiting time (if any)
continue;
}
}

View File

@@ -107,7 +107,10 @@ pub enum EventType {
},
/// Downloading a bunch of messages just finished.
IncomingMsgBunch,
IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.

View File

@@ -74,11 +74,6 @@ pub enum HeaderDef {
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
/// Deprecated header containing Group-ID in `vg-request-with-auth` message.
///
/// It is not used by Alice as Alice knows the group corresponding to the AUTH token.
/// Bob still sends it for backwards compatibility.
SecureJoinGroup,
SecureJoinFingerprint,
SecureJoinInvitenumber,

View File

@@ -13,7 +13,7 @@ use std::pin::Pin;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use futures::future::FutureExt;
use lettre_email::mime::Mime;
use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
@@ -67,7 +67,7 @@ enum MimeMultipartType {
/// and checks and returns the rough mime-type.
fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
let mimetype = ctype.mimetype.to_lowercase();
if mimetype.starts_with("multipart") && ctype.params.contains_key("boundary") {
if mimetype.starts_with("multipart") && ctype.params.get("boundary").is_some() {
MimeMultipartType::Multiple
} else if mimetype == "message/rfc822" {
MimeMultipartType::Message

File diff suppressed because it is too large Load Diff

View File

@@ -25,13 +25,6 @@ pub(crate) struct Capabilities {
/// <https://tools.ietf.org/html/rfc5464>
pub can_metadata: bool,
/// True if the server supports XDELTAPUSH capability.
/// This capability means setting /private/devicetoken IMAP METADATA
/// on the INBOX results in new mail notifications
/// via notifications.delta.chat service.
/// This is supported by <https://github.com/deltachat/chatmail>
pub can_push: bool,
/// Server ID if the server supports ID capability.
pub server_id: Option<HashMap<String, String>>,
}

View File

@@ -60,7 +60,6 @@ async fn determine_capabilities(
can_check_quota: caps.has_str("QUOTA"),
can_condstore: caps.has_str("CONDSTORE"),
can_metadata: caps.has_str("METADATA"),
can_push: caps.has_str("XDELTAPUSH"),
server_id,
};
Ok(capabilities)

View File

@@ -4,12 +4,13 @@ use anyhow::{bail, Context as _, Result};
use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use tokio::time::timeout;
use super::session::Session;
use super::Imap;
use crate::config::Config;
use crate::context::Context;
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::log::LogExt;
use crate::tools::{self, time_elapsed};
/// Timeout after which IDLE is finished
@@ -97,38 +98,97 @@ impl Session {
}
impl Imap {
/// Idle using polling.
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
session: &mut Session,
watch_folder: String,
watch_folder: Option<String>,
folder_meaning: FolderMeaning,
) -> Result<()> {
) {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
let fake_idle_start_time = tools::Time::now();
// Do not poll, just wait for an interrupt when no folder is passed in.
let watch_folder = if let Some(watch_folder) = watch_folder {
watch_folder
} else {
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
self.idle_interrupt_receiver.recv().await.ok();
return;
};
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// Loop until we are interrupted or until we fetch something.
const TIMEOUT_INIT_MS: u64 = 60_000;
let mut timeout_ms: u64 = TIMEOUT_INIT_MS;
enum Event {
Tick,
Interrupt,
}
// loop until we are interrupted or if we fetched something
loop {
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => {
// Let's see if fetching messages results
use futures::future::FutureExt;
use rand::Rng;
let mut interval = tokio::time::interval(Duration::from_millis(timeout_ms));
timeout_ms = timeout_ms
.saturating_add(rand::thread_rng().gen_range((timeout_ms / 2)..=timeout_ms));
interval.tick().await; // The first tick completes immediately.
match interval
.tick()
.map(|_| Event::Tick)
.race(
self.idle_interrupt_receiver
.recv()
.map(|_| Event::Interrupt),
)
.await
{
Event::Tick => {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.prepare(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if let Some(session) = &self.session {
if session.can_idle()
&& !context
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(context)
.unwrap_or_default()
{
// we only fake-idled because network was gone during IDLE, probably
break;
}
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
let res = self
.fetch_new_messages(context, session, &watch_folder, folder_meaning, false)
.await?;
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break;
match self
.fetch_new_messages(context, &watch_folder, folder_meaning, false)
.await
{
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
timeout_ms = TIMEOUT_INIT_MS;
if res {
break;
}
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect(context);
}
}
}
Ok(_) => {
info!(context, "Fake IDLE interrupted.");
Event::Interrupt => {
info!(context, "Fake IDLE interrupted");
break;
}
}
@@ -139,6 +199,5 @@ impl Imap {
"IMAP-fake-IDLE done after {:.4}s",
time_elapsed(&fake_idle_start_time).as_millis() as f64 / 1000.,
);
Ok(())
}
}

View File

@@ -1,21 +1,18 @@
use std::collections::BTreeMap;
use anyhow::{Context as _, Result};
use futures::TryStreamExt;
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
use crate::config::Config;
use crate::imap::{session::Session, Imap};
use crate::imap::Imap;
use crate::log::LogExt;
use crate::tools::{self, time_elapsed};
use crate::{context::Context, imap::FolderMeaning};
impl Imap {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(
&mut self,
context: &Context,
session: &mut Session,
) -> Result<bool> {
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
@@ -30,7 +27,8 @@ impl Imap {
}
info!(context, "Starting full folder scan");
let folders = session.list_folders().await?;
self.prepare(context).await?;
let folders = self.list_folders().await?;
let watched_folders = get_watched_folders(context).await?;
let mut folder_configs = BTreeMap::new();
@@ -66,16 +64,18 @@ impl Imap {
&& folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash
{
let session = self.session.as_mut().context("no session")?;
// Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
self.fetch_move_delete(context, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
let session = self.session.as_mut().context("no session")?;
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists(context)? {
break;
@@ -97,6 +97,18 @@ impl Imap {
last_scan.replace(tools::Time::now());
Ok(true)
}
/// Returns the names of all folders on the IMAP server.
pub async fn list_folders(self: &mut Imap) -> Result<Vec<async_imap::types::Name>> {
let session = self.session.as_mut();
let session = session.context("No IMAP connection")?;
let list = session
.list(Some(""), Some("*"))
.await?
.try_collect()
.await?;
Ok(list)
}
}
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {

View File

@@ -3,7 +3,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;
type Result<T> = std::result::Result<T, Error>;
@@ -52,7 +51,7 @@ impl ImapSession {
/// Selects a folder, possibly updating uid_validity and, if needed,
/// expunging the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(crate) async fn select_folder(
pub(super) async fn select_folder(
&mut self,
context: &Context,
folder: Option<&str>,
@@ -128,135 +127,10 @@ impl ImapSession {
},
}
}
/// Selects a folder and takes care of UIDVALIDITY changes.
///
/// When selecting a folder for the first time, sets the uid_next to the current
/// mailbox.uid_next so that no old emails are fetched.
///
/// Returns Result<new_emails> (i.e. whether new emails arrived),
/// if in doubt, returns new_emails=true so emails are fetched.
pub(crate) async fn select_with_uidvalidity(
&mut self,
context: &Context,
folder: &str,
) -> Result<bool> {
let newly_selected = self
.select_or_create_folder(context, folder)
.await
.with_context(|| format!("failed to select or create folder {folder}"))?;
let mailbox = self
.selected_mailbox
.as_mut()
.with_context(|| format!("No mailbox selected, folder: {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, folder)
.await
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
let new_uid_validity = mailbox
.uid_validity
.with_context(|| format!("No UIDVALIDITY for folder {folder}"))?;
let new_uid_next = if let Some(uid_next) = mailbox.uid_next {
Some(uid_next)
} else {
warn!(
context,
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
);
// RFC 3501 says STATUS command SHOULD NOT be used
// on the currently selected mailbox because the same
// information can be obtained by other means,
// such as reading SELECT response.
//
// However, it also says that UIDNEXT is REQUIRED
// in the SELECT response and if we are here,
// it is actually not returned.
//
// In particular, Winmail Pro Mail Server 5.1.0616
// never returns UIDNEXT in SELECT response,
// but responds to "STATUS INBOX (UIDNEXT)" command.
let status = self
.inner
.status(folder, "(UIDNEXT)")
.await
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
if status.uid_next.is_none() {
// This happens with mail.163.com as of 2023-11-26.
// It does not return UIDNEXT on SELECT and returns invalid
// `* STATUS "INBOX" ()` response on explicit request for UIDNEXT.
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT.");
}
status.uid_next
};
mailbox.uid_next = new_uid_next;
if new_uid_validity == old_uid_validity {
let new_emails = if newly_selected == NewlySelected::No {
// The folder was not newly selected i.e. no SELECT command was run. This means that mailbox.uid_next
// was not updated and may contain an incorrect value. So, just return true so that
// the caller tries to fetch new messages (we could of course run a SELECT command now, but trying to fetch
// new messages is only one command, just as a SELECT command)
true
} else if let Some(new_uid_next) = new_uid_next {
if new_uid_next < old_uid_next {
warn!(
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, folder, new_uid_next).await?;
context.schedule_resync().await?;
}
new_uid_next != old_uid_next // If UIDNEXT changed, there are new emails
} else {
// We have no UIDNEXT and if in doubt, return true.
true
};
return Ok(new_emails);
}
// UIDVALIDITY is modified, reset highest seen MODSEQ.
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, folder, new_uid_next).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
// Collect garbage entries in `imap` table.
context
.sql
.execute(
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
(&folder, new_uid_validity),
)
.await?;
if old_uid_validity != 0 || old_uid_next != 0 {
context.schedule_resync().await?;
}
info!(
context,
"uid/validity change folder {}: new {}/{} previous {}/{}.",
folder,
new_uid_next,
new_uid_validity,
old_uid_next,
old_uid_validity,
);
Ok(false)
}
}
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(crate) enum NewlySelected {
pub(super) enum NewlySelected {
/// The folder was newly selected during this call to select_folder().
Yes,
/// No SELECT command was run because the folder already was selected

View File

@@ -1,32 +1,11 @@
use std::cmp;
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use anyhow::{Context as _, Result};
use async_imap::types::Mailbox;
use async_imap::Session as ImapSession;
use futures::TryStreamExt;
use crate::constants::DC_FETCH_EXISTING_MSGS_COUNT;
use crate::imap::capabilities::Capabilities;
use crate::net::session::SessionStream;
/// Prefetch:
/// - Message-ID to check if we already have the message.
/// - In-Reply-To and References to check if message is a reply to chat message.
/// - Chat-Version to check if a message is a chat message
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
DATE \
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTOCRYPT-SETUP-MESSAGE\
)])";
#[derive(Debug)]
pub(crate) struct Session {
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
@@ -89,75 +68,4 @@ impl Session {
pub fn can_metadata(&self) -> bool {
self.capabilities.can_metadata
}
pub fn can_push(&self) -> bool {
self.capabilities.can_push
}
/// Returns the names of all folders on the IMAP server.
pub async fn list_folders(&mut self) -> Result<Vec<async_imap::types::Name>> {
let list = self.list(Some(""), Some("*")).await?.try_collect().await?;
Ok(list)
}
/// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results
/// in the order of ascending delivery time to the server (INTERNALDATE).
pub(crate) async fn prefetch(
&mut self,
uid_next: u32,
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
// fetch messages with larger UID than the last one seen
let set = format!("{uid_next}:*");
let mut list = self
.uid_fetch(set, PREFETCH_FLAGS)
.await
.context("IMAP could not fetch")?;
let mut msgs = BTreeMap::new();
while let Some(msg) = list.try_next().await? {
if let Some(msg_uid) = msg.uid {
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
// the last UID in the mailbox. It happens because
// uid:* is interpreted the same way as *:uid.
// See <https://tools.ietf.org/html/rfc3501#page-61> for
// standard reference. Therefore, sometimes we receive
// already seen messages and have to filter them out.
if msg_uid >= uid_next {
msgs.insert((msg.internal_date(), msg_uid), msg);
}
}
}
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
}
/// Like prefetch(), but not for new messages but existing ones (the DC_FETCH_EXISTING_MSGS_COUNT newest messages)
pub(crate) async fn prefetch_existing_msgs(
&mut self,
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
let exists: i64 = {
let mailbox = self.selected_mailbox.as_ref().context("no mailbox")?;
mailbox.exists.into()
};
// Fetch last DC_FETCH_EXISTING_MSGS_COUNT (100) messages.
// Sequence numbers are sequential. If there are 1000 messages in the inbox,
// we can fetch the sequence numbers 900-1000 and get the last 100 messages.
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT + 1);
let set = format!("{first}:{exists}");
let mut list = self
.fetch(&set, PREFETCH_FLAGS)
.await
.context("IMAP Could not fetch")?;
let mut msgs = BTreeMap::new();
while let Some(msg) = list.try_next().await? {
if let Some(msg_uid) = msg.uid {
msgs.insert((msg.internal_date(), msg_uid), msg);
}
}
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
}
}

View File

@@ -389,11 +389,11 @@ async fn imex_inner(
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
e2ee::ensure_secret_key_exists(context)
.await
.context("Cannot create private key or private key not available")?;
create_folder(context, &path).await?;
if e2ee::ensure_secret_key_exists(context).await.is_err() {
bail!("Cannot create private key or private key not available.");
} else {
create_folder(context, &path).await?;
}
}
match what {
@@ -824,9 +824,10 @@ mod tests {
use tokio::task;
use super::*;
use crate::key;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext, TestContextManager};
use crate::test_utils::{alice_keypair, TestContext};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file() {
@@ -1094,17 +1095,13 @@ mod tests {
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt");
// Autocrypt Setup Message payload "encrypted" with plaintext algorithm.
const S_PLAINTEXT_SETUPFILE: &str =
include_str!("../test-data/message/plaintext-autocrypt-setup.txt");
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_split_and_decrypt() {
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
assert_eq!(typ, BlockType::Message);
assert!(S_EM_SETUPCODE.starts_with(headers.get(HEADER_SETUPCODE).unwrap()));
assert!(!headers.contains_key(HEADER_AUTOCRYPT));
assert!(headers.get(HEADER_AUTOCRYPT).is_none());
assert!(!base64.is_empty());
@@ -1118,24 +1115,7 @@ mod tests {
assert_eq!(typ, BlockType::PrivateKey);
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
assert!(!headers.contains_key(HEADER_SETUPCODE));
}
/// Tests that Autocrypt Setup Message encrypted with "plaintext" algorithm cannot be
/// decrypted.
///
/// According to <https://datatracker.ietf.org/doc/html/rfc4880#section-13.4>
/// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_plaintext_autocrypt_setup_message() {
let setup_file = S_PLAINTEXT_SETUPFILE.to_string();
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
assert!(decrypt_setup_file(
incorrect_setupcode,
std::io::Cursor::new(setup_file.as_bytes()),
)
.await
.is_err());
assert!(headers.get(HEADER_SETUPCODE).is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1153,7 +1133,6 @@ mod tests {
alice2.configure_addr("alice@example.org").await;
alice2.recv_msg(&sent).await;
let msg = alice2.get_last_msg().await;
assert!(msg.is_setupmessage());
// Send a message that cannot be decrypted because the keys are
// not synchronized yet.
@@ -1171,25 +1150,4 @@ mod tests {
Ok(())
}
/// Tests that Autocrypt Setup Messages is only clickable if it is self-sent.
/// This prevents Bob from tricking Alice into changing the key
/// by sending her an Autocrypt Setup Message as long as Alice's server
/// does not allow to forge the `From:` header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer_non_self_sent() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let _setup_code = initiate_key_transfer(&alice).await?;
// Get Autocrypt Setup Message.
let sent = alice.pop_sent_msg().await;
let rcvd = bob.recv_msg(&sent).await;
assert!(!rcvd.is_setupmessage());
Ok(())
}
}

View File

@@ -597,6 +597,7 @@ mod tests {
use std::time::Duration;
use crate::chat::{get_chat_msgs, send_msg, ChatItem};
use crate::message::{Message, Viewtype};
use crate::test_utils::TestContextManager;
use super::*;

View File

@@ -79,7 +79,7 @@ pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
}
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
let public_key = context
match context
.sql
.query_row_optional(
"SELECT public_key
@@ -91,8 +91,8 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPubl
Ok(bytes)
},
)
.await?;
match public_key {
.await?
{
Some(bytes) => SignedPublicKey::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
@@ -101,27 +101,8 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPubl
}
}
/// Returns our own public keyring.
pub(crate) async fn load_self_public_keyring(context: &Context) -> Result<Vec<SignedPublicKey>> {
let keys = context
.sql
.query_map(
r#"SELECT public_key
FROM keypairs
ORDER BY id=(SELECT value FROM config WHERE keyname='key_id') DESC"#,
(),
|row| row.get::<_, Vec<u8>>(0),
|keys| keys.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?
.into_iter()
.filter_map(|bytes| SignedPublicKey::from_slice(&bytes).log_err(context).ok())
.collect();
Ok(keys)
}
pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecretKey> {
let private_key = context
match context
.sql
.query_row_optional(
"SELECT private_key
@@ -133,8 +114,8 @@ pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecr
Ok(bytes)
},
)
.await?;
match private_key {
.await?
{
Some(bytes) => SignedSecretKey::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
@@ -274,7 +255,7 @@ pub(crate) async fn load_keypair(
/// Use of a key pair for encryption or decryption.
///
/// This is used by `store_self_keypair` to know what kind of key is
/// This is used by [store_self_keypair] to know what kind of key is
/// being saved.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum KeyPairUse {
@@ -296,7 +277,7 @@ pub enum KeyPairUse {
/// same key again overwrites it.
///
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
pub(crate) async fn store_self_keypair(
pub async fn store_self_keypair(
context: &Context,
keypair: &KeyPair,
default: KeyPairUse,

View File

@@ -98,7 +98,6 @@ mod color;
pub mod html;
pub mod net;
pub mod plaintext;
mod push;
pub mod summary;
mod debug_logging;

View File

@@ -1,5 +1,6 @@
//! Location handling.
use std::convert::TryFrom;
use std::time::Duration;
use anyhow::{ensure, Context as _, Result};
@@ -679,21 +680,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
"Location loop is waiting for {} or interrupt",
duration_to_str(duration)
);
match timeout(duration, interrupt_receiver.recv()).await {
Err(_err) => {
info!(context, "Location loop timeout.");
}
Ok(Err(err)) => {
warn!(
context,
"Interrupt channel closed, location loop exits now: {err:#}."
);
return;
}
Ok(Ok(())) => {
info!(context, "Location loop received interrupt.");
}
}
timeout(duration, interrupt_receiver.recv()).await.ok();
}
}

View File

@@ -201,14 +201,12 @@ WHERE id=?;
let fts = timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {fts}");
let from_contact = Contact::get_by_id(context, msg.from_id).await?;
let name = from_contact.get_name_n_addr();
if let Some(override_sender_name) = msg.get_override_sender_name() {
let addr = from_contact.get_addr();
ret += &format!(" by ~{override_sender_name} ({addr})");
} else {
ret += &format!(" by {name}");
}
let name = Contact::get_by_id(context, msg.from_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
ret += &format!(" by {name}");
ret += "\n";
if msg.from_id != ContactId::SELF {
@@ -2018,7 +2016,7 @@ mod tests {
assert_eq!(_msg2.get_filemime(), None);
}
/// Tests that message can be prepared even if account has no configured address.
/// Tests that message cannot be prepared if account has no configured address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prepare_not_configured() {
let d = test::TestContext::new().await;
@@ -2028,7 +2026,7 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_ok());
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -1,12 +1,13 @@
//! # MIME message production.
use std::collections::HashSet;
use std::convert::TryInto;
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use chrono::TimeZone;
use format_flowed::{format_flowed, format_flowed_quote};
use lettre_email::{Address, Header, MimeMultipartType, PartBuilder};
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use tokio::fs;
use crate::blob::BlobObject;
@@ -64,15 +65,7 @@ pub struct MimeFactory<'a> {
loaded: Loaded,
msg: &'a Message,
in_reply_to: String,
/// Space-separated list of Message-IDs for `References` header.
///
/// Each Message-ID in the list
/// may or may not be enclosed in angle brackets,
/// angle brackets must be added during message rendering
/// as needed.
references: String,
req_mdn: bool,
last_added_location_id: Option<u32>,
@@ -93,7 +86,6 @@ pub struct RenderedEmail {
// pub envelope: Envelope,
pub is_encrypted: bool,
pub is_gossiped: bool,
pub is_group: bool,
pub last_added_location_id: Option<u32>,
/// A comma-separated string of sync-IDs that are used by the rendered email
@@ -346,11 +338,18 @@ impl<'a> MimeFactory<'a> {
fn should_force_plaintext(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
self.msg
.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default()
|| chat.typ == Chattype::Broadcast
if chat.is_protected() {
false
} else if chat.typ == Chattype::Broadcast {
// encryption may disclose recipients;
// this is probably a worse issue than not opportunistically (!) encrypting
true
} else {
self.msg
.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default()
}
}
Loaded::Mdn { .. } => false,
}
@@ -367,25 +366,20 @@ impl<'a> MimeFactory<'a> {
}
}
async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result<bool> {
async fn should_do_gossip(&self, context: &Context) -> Result<bool> {
match &self.loaded {
Loaded::Message { chat } => {
let cmd = self.msg.param.get_cmd();
if cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
{
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
if time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else if multiple_recipients {
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
if time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
let cmd = self.msg.param.get_cmd();
// Do gossip in all Securejoin messages not to complicate the code. There's no
// need in gossips in "vg-auth-required" messages f.e., but let them be.
Ok(cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage)
}
}
Loaded::Mdn { .. } => Ok(false),
@@ -564,7 +558,7 @@ impl<'a> MimeFactory<'a> {
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr),
};
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue);
@@ -596,8 +590,6 @@ impl<'a> MimeFactory<'a> {
));
}
let mut is_group = false;
if let Loaded::Message { chat } = &self.loaded {
if chat.typ == Chattype::Broadcast {
let encoded_chat_name = encode_words(&chat.name);
@@ -605,8 +597,6 @@ impl<'a> MimeFactory<'a> {
"List-ID".into(),
format!("{encoded_chat_name} <{}>", chat.grpid),
));
} else if chat.typ == Chattype::Group {
is_group = true;
}
}
@@ -715,9 +705,9 @@ impl<'a> MimeFactory<'a> {
.fold(message, |message, header| message.header(header));
// Add gossip headers in chats with multiple recipients
let multiple_recipients =
peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
if self.should_do_gossip(context, multiple_recipients).await? {
if (peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?)
&& self.should_do_gossip(context).await?
{
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if let Some(header) = peerstate.render_gossip_header(verified) {
message = message.header(Header::new("Autocrypt-Gossip".into(), header));
@@ -751,12 +741,8 @@ impl<'a> MimeFactory<'a> {
);
}
// Disable compression for SecureJoin to ensure
// there are no compression side channels
// leaking information about the tokens.
let compress = self.msg.param.get_cmd() != SystemMessage::SecurejoinMessage;
let encrypted = encrypt_helper
.encrypt(context, verified, message, peerstates, compress)
.encrypt(context, verified, message, peerstates)
.await?;
outer_message
@@ -878,7 +864,6 @@ impl<'a> MimeFactory<'a> {
// envelope: Envelope::new,
is_encrypted,
is_gossiped,
is_group,
last_added_location_id,
sync_ids_to_delete: self.sync_ids_to_delete,
rfc724_mid,
@@ -955,6 +940,7 @@ impl<'a> MimeFactory<'a> {
};
let command = self.msg.param.get_cmd();
let mut placeholdertext = None;
let mut meta_part = None;
let send_verified_headers = match chat.typ {
Chattype::Single => true,
@@ -1140,13 +1126,17 @@ impl<'a> MimeFactory<'a> {
if let Some(grpimage) = grpimage {
info!(context, "setting group image '{}'", grpimage);
let avatar = build_avatar_file(context, grpimage)
.await
.context("Cannot attach group image")?;
headers.hidden.push(Header::new(
"Chat-Group-Avatar".into(),
format!("base64:{avatar}"),
));
let mut meta = Message {
viewtype: Viewtype::Image,
..Default::default()
};
meta.param.set(Param::File, grpimage);
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image").await?;
meta_part = Some(mail);
headers
.protected
.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
}
if self.msg.viewtype == Viewtype::Sticker {
@@ -1270,6 +1260,10 @@ impl<'a> MimeFactory<'a> {
parts.push(file_part);
}
if let Some(meta_part) = meta_part {
parts.push(meta_part);
}
if let Some(msg_kml_part) = self.get_message_kml_part() {
parts.push(msg_kml_part);
}
@@ -1301,7 +1295,7 @@ impl<'a> MimeFactory<'a> {
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar).await? {
Some(path) => match build_avatar_file(context, &path).await {
Some(path) => match build_selfavatar_file(context, &path).await {
Ok(avatar) => headers.hidden.push(Header::new(
"Chat-User-Avatar".into(),
format!("base64:{avatar}"),
@@ -1510,11 +1504,8 @@ async fn build_body_file(
Ok((mail, filename_to_send))
}
async fn build_avatar_file(context: &Context, path: &str) -> Result<String> {
let blob = match path.starts_with("$BLOBDIR/") {
true => BlobObject::from_name(context, path.to_string())?,
false => BlobObject::from_path(context, path.as_ref())?,
};
async fn build_selfavatar_file(context: &Context, path: &str) -> Result<String> {
let blob = BlobObject::from_path(context, path.as_ref())?;
let body = fs::read(blob.to_abs_path()).await?;
let encoded_body = wrapped_base64_encode(&body);
Ok(encoded_body)

View File

@@ -8,9 +8,10 @@ use std::pin::Pin;
use std::str;
use anyhow::{bail, Context as _, Result};
use base64::Engine as _;
use deltachat_derive::{FromSql, ToSql};
use format_flowed::unformat_flowed;
use lettre_email::mime::Mime;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use crate::aheader::{Aheader, EncryptPreference};
@@ -27,7 +28,7 @@ use crate::decrypt::{
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
use crate::key::{load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
use crate::message::{
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
};
@@ -265,28 +266,14 @@ impl MimeMessage {
for field in &part.headers {
let key = field.get_key().to_lowercase();
// For now only avatar headers can be hidden.
if !headers.contains_key(&key)
&& (key == "chat-user-avatar" || key == "chat-group-avatar")
{
// For now only Chat-User-Avatar can be hidden.
if !headers.contains_key(&key) && key == "chat-user-avatar" {
headers.insert(key.to_string(), field.get_value());
}
}
}
}
// Overwrite Message-ID with X-Microsoft-Original-Message-ID.
// However if we later find Message-ID in the protected part,
// it will overwrite both.
if let Some(microsoft_message_id) =
headers.remove(HeaderDef::XMicrosoftOriginalMessageId.get_headername())
{
headers.insert(
HeaderDef::MessageId.get_headername().to_string(),
microsoft_message_id,
);
}
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
// them in signed-only emails, but has no value currently.
Self::remove_secured_headers(&mut headers);
@@ -304,11 +291,7 @@ impl MimeMessage {
hop_info += "\n\n";
hop_info += &decryption_info.dkim_results.to_string();
let incoming = !context.is_self_addr(&from.addr).await?;
let public_keyring = match decryption_info.peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(decryption_info.peerstate.as_ref()),
};
let public_keyring = keyring_from_peerstate(decryption_info.peerstate.as_ref());
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &public_keyring)
}) {
@@ -351,20 +334,9 @@ impl MimeMessage {
gossip_headers,
)
.await?;
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
for h in [
HeaderDef::Subject,
HeaderDef::ChatGroupId,
HeaderDef::ChatGroupName,
HeaderDef::ChatGroupNameChanged,
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
] {
headers.remove(h.get_headername());
}
// Remove unsigned subject from messages displayed with padlock.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
}
// let known protected headers from the decrypted
@@ -392,20 +364,13 @@ impl MimeMessage {
// signed part, but it doesn't match the outer one.
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack.
// Resending encrypted messages via mailing lists
// without reencrypting is not useful anyway,
// so we return an error below.
// Or it's because someone is doing some replay attack
// - OTOH, I can't come up with an attack scenario
// where this would be useful.
warn!(
context,
"From header in signed part doesn't match the outer one",
);
// Return an error from the parser.
// This will result in creating a tombstone
// and no further message processing
// as if the MIME structure is broken.
bail!("From header is forged");
}
}
}
@@ -433,6 +398,7 @@ impl MimeMessage {
}
}
let incoming = !context.is_self_addr(&from.addr).await?;
let mut parser = MimeMessage {
parts: Vec::new(),
headers,
@@ -523,7 +489,7 @@ impl MimeMessage {
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts.retain(|part| {
part.mimetype.is_none()
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
@@ -756,20 +722,37 @@ impl MimeMessage {
) -> Option<AvatarAction> {
if header_value == "0" {
Some(AvatarAction::Delete)
} else if let Some(base64) = header_value
} else if let Some(avatar) = header_value
.split_ascii_whitespace()
.collect::<String>()
.strip_prefix("base64:")
.map(|x| base64::engine::general_purpose::STANDARD.decode(x))
{
match BlobObject::store_from_base64(context, base64, "avatar").await {
Ok(path) => Some(AvatarAction::Change(path)),
Err(err) => {
warn!(
context,
"Could not decode and save avatar to blob file: {:#}", err,
);
None
// Avatar sent directly in the header as base64.
if let Ok(decoded_data) = avatar {
let extension = if let Ok(format) = image::guess_format(&decoded_data) {
if let Some(ext) = format.extensions_str().first() {
format!(".{ext}")
} else {
String::new()
}
} else {
String::new()
};
match BlobObject::create(context, &format!("avatar{extension}"), &decoded_data)
.await
{
Ok(blob) => Some(AvatarAction::Change(blob.as_name().to_string())),
Err(err) => {
warn!(
context,
"Could not save decoded avatar to blob file: {:#}", err
);
None
}
}
} else {
None
}
} else {
// Avatar sent in attachment, as previous versions of Delta Chat did.
@@ -809,7 +792,6 @@ impl MimeMessage {
pub(crate) fn get_subject(&self) -> Option<String> {
self.get_header(HeaderDef::Subject)
.map(|s| s.trim_start())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
}
@@ -837,7 +819,7 @@ impl MimeMessage {
let mimetype = mail.ctype.mimetype.to_lowercase();
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.contains_key("boundary") {
if mail.ctype.params.get("boundary").is_some() {
MimeS::Multiple
} else {
MimeS::Single
@@ -1413,22 +1395,14 @@ impl MimeMessage {
}
pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
self.get_header(HeaderDef::MessageId)
self.get_header(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get_header(HeaderDef::MessageId))
.and_then(|msgid| parse_message_id(msgid).ok())
}
fn remove_secured_headers(headers: &mut HashMap<String, String>) {
headers.remove("secure-join-fingerprint");
headers.remove("secure-join-auth");
headers.remove("chat-verified");
headers.remove("autocrypt-gossip");
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
if let Some(secure_join) = headers.remove("secure-join") {
if secure_join == "vc-request" || secure_join == "vg-request" {
headers.insert("secure-join".to_string(), secure_join);
}
}
}
fn merge_headers(
@@ -1853,8 +1827,6 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
}
}
/// Returns true if the header overwrites outer header
/// when it comes from protected headers.
fn is_known(key: &str) -> bool {
matches!(
key,
@@ -1870,7 +1842,6 @@ fn is_known(key: &str) -> bool {
| "in-reply-to"
| "references"
| "subject"
| "secure-join"
)
}
@@ -2260,12 +2231,11 @@ mod tests {
use super::*;
use crate::{
chat,
chatlist::Chatlist,
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
message::MessengerMessage,
message::{Message, MessageState, MessengerMessage},
receive_imf::receive_imf,
test_utils::{TestContext, TestContextManager},
test_utils::TestContext,
tools::time,
};
@@ -3584,29 +3554,6 @@ On 2020-10-25, Bob wrote:
);
}
/// Tests that X-Microsoft-Original-Message-ID does not overwrite encrypted Message-ID.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
// Insert X-Microsoft-Original-Message-ID.
// It should be ignored because there is a Message-ID in the encrypted part.
sent_msg.payload = sent_msg.payload.replace(
"Message-ID:",
"X-Microsoft-Original-Message-ID: <fake-message-id@example.net>\r\nMessage-ID:",
);
let msg = alice.recv_msg(&sent_msg).await;
assert!(!msg.rfc724_mid.contains("fake-message-id"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await;

View File

@@ -1,6 +1,6 @@
//! # Common network utilities.
use std::net::Ipv4Addr;
use std::net::{IpAddr, SocketAddr};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::pin::Pin;
use std::str::FromStr;
use std::time::Duration;
@@ -41,8 +41,7 @@ async fn lookup_host_with_timeout(
/// Looks up hostname and port using DNS and updates the address resolution cache.
///
/// If `load_cache` is true, appends cached results not older than 30 days to the end
/// or entries from fallback cache if there are no cached addresses.
/// If `load_cache` is true, appends cached results not older than 30 days to the end.
async fn lookup_host_with_cache(
context: &Context,
hostname: &str,
@@ -130,72 +129,11 @@ async fn lookup_host_with_cache(
//
// In the future we may pre-resolve all provider database addresses
// and build them in.
match hostname {
"mail.sangham.net" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
port,
));
}
"nine.testrun.org" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
port,
));
}
"disroot.org" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139)),
port,
));
}
"mail.riseup.net" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
port,
));
}
"imap.gmail.com" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
port,
));
}
"smtp.gmail.com" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
port,
));
}
_ => {}
if hostname == "mail.sangham.net" {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
port,
));
}
}
}

View File

@@ -87,7 +87,7 @@ pub enum Param {
/// `Secure-Join-Fingerprint` header for `{vc,vg}-request-with-auth` messages.
Arg3 = b'G',
/// Deprecated `Secure-Join-Group` header for messages.
/// For Messages
Arg4 = b'H',
/// For Messages
@@ -455,6 +455,7 @@ mod tests {
use std::path::Path;
use std::str::FromStr;
use anyhow::Result;
use tokio::fs;
use super::*;

View File

@@ -1,7 +1,5 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module.
use std::mem;
use anyhow::{Context as _, Error, Result};
use num_traits::FromPrimitive;
@@ -167,9 +165,6 @@ impl Peerstate {
/// Loads peerstate corresponding to the given address from the database.
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
if context.is_self_addr(addr).await? {
return Ok(None);
}
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, \
@@ -187,7 +182,6 @@ impl Peerstate {
context: &Context,
fingerprint: &Fingerprint,
) -> Result<Option<Peerstate>> {
// NOTE: If it's our key fingerprint, this returns None currently.
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, \
@@ -212,9 +206,6 @@ impl Peerstate {
fingerprint: &Fingerprint,
addr: &str,
) -> Result<Option<Peerstate>> {
if context.is_self_addr(addr).await? {
return Ok(None);
}
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, \
@@ -225,10 +216,9 @@ impl Peerstate {
FROM acpeerstates \
WHERE verified_key_fingerprint=? \
OR addr=? COLLATE NOCASE \
ORDER BY verified_key_fingerprint=? DESC, addr=? COLLATE NOCASE DESC, \
last_seen DESC LIMIT 1;";
ORDER BY verified_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, (&fp, addr, &fp, addr)).await
Self::from_stmt(context, query, (&fp, &addr, &fp)).await
}
async fn from_stmt(
@@ -347,7 +337,7 @@ impl Peerstate {
return;
}
if message_time >= self.last_seen {
if message_time > self.last_seen {
self.last_seen = message_time;
self.last_seen_autocrypt = message_time;
if (header.prefer_encrypt == EncryptPreference::Mutual
@@ -370,7 +360,7 @@ impl Peerstate {
return;
}
if message_time >= self.gossip_timestamp {
if message_time > self.gossip_timestamp {
self.gossip_timestamp = message_time;
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
self.gossip_key = Some(gossip_header.public_key.clone());
@@ -526,82 +516,65 @@ impl Peerstate {
/// Saves the peerstate to the database.
pub async fn save_to_db(&self, sql: &Sql) -> Result<()> {
self.save_to_db_ex(sql, None).await
}
/// Saves the peerstate to the database.
///
/// * `old_addr`: Old address of the peerstate in case of an AEAP transition.
pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> {
let trans_fn = |t: &mut rusqlite::Transaction| {
if let Some(old_addr) = old_addr {
// We are doing an AEAP transition to the new address and the SQL INSERT below will
// save the existing peerstate as belonging to this new address. We now need to
// delete the peerstate that belongs to the current address in case if the contact
// later wants to move back to the current address. Otherwise the old entry will be
// just found and updated instead of doing AEAP.
t.execute("DELETE FROM acpeerstates WHERE addr=?", (old_addr,))?;
}
t.execute(
"INSERT INTO acpeerstates (
last_seen,
last_seen_autocrypt,
prefer_encrypted,
public_key,
gossip_timestamp,
gossip_key,
public_key_fingerprint,
gossip_key_fingerprint,
verified_key,
verified_key_fingerprint,
verifier,
secondary_verified_key,
secondary_verified_key_fingerprint,
secondary_verifier,
backward_verified_key_id,
addr)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT (addr)
DO UPDATE SET
last_seen = excluded.last_seen,
last_seen_autocrypt = excluded.last_seen_autocrypt,
prefer_encrypted = excluded.prefer_encrypted,
public_key = excluded.public_key,
gossip_timestamp = excluded.gossip_timestamp,
gossip_key = excluded.gossip_key,
public_key_fingerprint = excluded.public_key_fingerprint,
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
verified_key = excluded.verified_key,
verified_key_fingerprint = excluded.verified_key_fingerprint,
verifier = excluded.verifier,
secondary_verified_key = excluded.secondary_verified_key,
secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint,
secondary_verifier = excluded.secondary_verifier,
backward_verified_key_id = excluded.backward_verified_key_id",
(
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verifier.as_deref().unwrap_or(""),
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
self.secondary_verified_key_fingerprint
.as_ref()
.map(|fp| fp.hex()),
self.secondary_verifier.as_deref().unwrap_or(""),
self.backward_verified_key_id,
&self.addr,
),
)?;
Ok(())
};
sql.transaction(trans_fn).await
sql.execute(
"INSERT INTO acpeerstates (
last_seen,
last_seen_autocrypt,
prefer_encrypted,
public_key,
gossip_timestamp,
gossip_key,
public_key_fingerprint,
gossip_key_fingerprint,
verified_key,
verified_key_fingerprint,
verifier,
secondary_verified_key,
secondary_verified_key_fingerprint,
secondary_verifier,
backward_verified_key_id,
addr)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT (addr)
DO UPDATE SET
last_seen = excluded.last_seen,
last_seen_autocrypt = excluded.last_seen_autocrypt,
prefer_encrypted = excluded.prefer_encrypted,
public_key = excluded.public_key,
gossip_timestamp = excluded.gossip_timestamp,
gossip_key = excluded.gossip_key,
public_key_fingerprint = excluded.public_key_fingerprint,
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
verified_key = excluded.verified_key,
verified_key_fingerprint = excluded.verified_key_fingerprint,
verifier = excluded.verifier,
secondary_verified_key = excluded.secondary_verified_key,
secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint,
secondary_verifier = excluded.secondary_verifier,
backward_verified_key_id = excluded.backward_verified_key_id",
(
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verifier.as_deref().unwrap_or(""),
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
self.secondary_verified_key_fingerprint
.as_ref()
.map(|fp| fp.hex()),
self.secondary_verifier.as_deref().unwrap_or(""),
self.backward_verified_key_id,
&self.addr,
),
)
.await?;
Ok(())
}
/// Returns the address that verified the contact
@@ -759,16 +732,14 @@ pub(crate) async fn maybe_do_aeap_transition(
// some accidental transitions if someone writes from multiple
// addresses with an MUA.
&& mime_parser.has_chat_version()
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
// probably from a new contact sharing the same key.
// Check if the message is signed correctly.
// Although checking `from_is_signed` below is sufficient, let's play it safe.
&& !mime_parser.signatures.is_empty()
// Check if the From: address was also in the signed part of the email.
// Without this check, an attacker could replay a message from Alice
// to Bob. Then Bob's device would do an AEAP transition from Alice's
// to the attacker's address, allowing for easier phishing.
&& mime_parser.from_is_signed
// DC avoids sending messages with the same timestamp, that's why `>` is here unlike in
// `Peerstate::apply_header()`.
&& info.message_time > peerstate.last_seen
{
let info = &mut mime_parser.decryption_info;
@@ -783,16 +754,13 @@ pub(crate) async fn maybe_do_aeap_transition(
)
.await?;
let old_addr = mem::take(&mut peerstate.addr);
peerstate.addr = info.from.clone();
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(header, info.message_time);
peerstate
.save_to_db_ex(&context.sql, Some(&old_addr))
.await?;
peerstate.save_to_db(&context.sql).await?;
}
Ok(())
@@ -811,6 +779,30 @@ enum PeerstateChange {
Aeap(String),
}
/// Removes duplicate peerstates from `acpeerstates` database table.
///
/// Normally there should be no more than one peerstate per address.
/// However, the database does not enforce this condition.
///
/// Previously there were bugs that caused creation of additional
/// peerstates when existing peerstate could not be read due to a
/// temporary database error or a failure to parse stored data. This
/// procedure fixes the problem by removing duplicate records.
pub(crate) async fn deduplicate_peerstates(sql: &Sql) -> Result<()> {
sql.execute(
"DELETE FROM acpeerstates
WHERE id NOT IN (
SELECT MIN(id)
FROM acpeerstates
GROUP BY addr
)",
(),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1003,7 +995,7 @@ mod tests {
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
// Same header will be applied in the future.
peerstate.apply_header(&header, 300);
peerstate.apply_header(&header, 400);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
}
}

View File

@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashSet};
use std::io;
use std::io::Cursor;
use anyhow::{bail, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use pgp::armor::BlockType;
use pgp::composed::{
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
@@ -138,11 +138,8 @@ pub struct KeyPair {
}
/// Create a new key pair.
///
/// Both secret and public key consist of signing primary key and encryption subkey
/// as [described in the Autocrypt standard](https://autocrypt.org/level1.html#openpgp-based-key-data).
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
let (signing_key_type, encryption_key_type) = match keygen_type {
let (secret_key_type, public_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
@@ -150,8 +147,8 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
let user_id = format!("<{addr}>");
let key_params = SecretKeyParamsBuilder::default()
.key_type(signing_key_type)
.can_certify(true)
.key_type(secret_key_type)
.can_create_certificates(true)
.can_sign(true)
.primary_user_id(user_id)
.passphrase(None)
@@ -173,36 +170,41 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
])
.subkey(
SubkeyParamsBuilder::default()
.key_type(encryption_key_type)
.key_type(public_key_type)
.can_encrypt(true)
.passphrase(None)
.build()
.map_err(|e| format_err!("{}", e))
.context("failed to build subkey parameters")?,
)
.build()
.context("failed to build key parameters")?;
let secret_key = key_params
.generate()
.context("failed to generate the key")?
.map_err(|e| format_err!("{}", e))
.context("invalid key params")?;
let key = key_params.generate().context("invalid params")?;
let private_key = key
.sign(|| "".into())
.map_err(|e| format_err!("{}", e))
.context("failed to sign secret key")?;
secret_key
.verify()
.context("invalid secret key generated")?;
let public_key = secret_key
.public_key()
.sign(&secret_key, || "".into())
let public_key = private_key.public_key();
let public_key = public_key
.sign(&private_key, || "".into())
.map_err(|e| format_err!("{}", e))
.context("failed to sign public key")?;
private_key
.verify()
.map_err(|e| format_err!("{}", e))
.context("invalid private key generated")?;
public_key
.verify()
.map_err(|e| format_err!("{}", e))
.context("invalid public key generated")?;
Ok(KeyPair {
addr,
public: public_key,
secret: secret_key,
secret: private_key,
})
}
@@ -236,7 +238,6 @@ pub async fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: Vec<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
compress: bool,
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
@@ -250,19 +251,20 @@ pub async fn pk_encrypt(
let mut rng = thread_rng();
// TODO: measure time
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
let signed_msg = lit_msg.sign(skey, || "".into(), HASH_ALGORITHM)?;
let compressed_msg = if compress {
signed_msg.compress(CompressionAlgorithm::ZLIB)?
} else {
signed_msg
};
compressed_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
lit_msg
.sign(skey, || "".into(), HASH_ALGORITHM)
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
.and_then(|msg| {
msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
})
} else {
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
};
let encoded_msg = encrypted_msg.to_armored_string(None)?;
let msg = encrypted_msg?;
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
})
@@ -484,16 +486,10 @@ mod tests {
CTEXT_SIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let compress = true;
pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
compress,
)
.await
.unwrap()
pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))
.await
.unwrap()
})
.await
}
@@ -503,11 +499,7 @@ mod tests {
CTEXT_UNSIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let compress = true;
pk_encrypt(CLEARTEXT, keyring, None, compress)
.await
.unwrap()
pk_encrypt(CLEARTEXT, keyring, None).await.unwrap()
})
.await
}

View File

@@ -1,115 +0,0 @@
use std::sync::atomic::Ordering;
use std::sync::Arc;
use anyhow::Result;
use tokio::sync::RwLock;
use crate::context::Context;
use crate::net::http;
/// Manages subscription to Apple Push Notification services.
///
/// This structure is created by account manager and is shared between accounts.
/// To enable notifications, application should request the device token as described in
/// <https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns>
/// and give it to the account manager, which will forward the token in this structure.
///
/// Each account (context) can then retrieve device token
/// from this structure and give it to the email server.
/// If email server does not support push notifications,
/// account can call `subscribe` method
/// to register device token with the heartbeat
/// notification provider server as a fallback.
#[derive(Debug, Clone, Default)]
pub struct PushSubscriber {
inner: Arc<RwLock<PushSubscriberState>>,
}
impl PushSubscriber {
/// Creates new push notification subscriber.
pub(crate) fn new() -> Self {
Default::default()
}
/// Sets device token for Apple Push Notification service.
pub(crate) async fn set_device_token(&mut self, token: &str) {
self.inner.write().await.device_token = Some(token.to_string());
}
/// Retrieves device token.
///
/// Token may be not available if application is not running on Apple platform,
/// failed to register for remote notifications or is in the process of registering.
///
/// IMAP loop should periodically check if device token is available
/// and send the token to the email server if it supports push notifications.
pub(crate) async fn device_token(&self) -> Option<String> {
self.inner.read().await.device_token.clone()
}
/// Subscribes for heartbeat notifications with previously set device token.
pub(crate) async fn subscribe(&self) -> Result<()> {
let mut state = self.inner.write().await;
if state.heartbeat_subscribed {
return Ok(());
}
let Some(ref token) = state.device_token else {
return Ok(());
};
let socks5_config = None;
let response = http::get_client(socks5_config)?
.post("https://notifications.delta.chat/register")
.body(format!("{{\"token\":\"{token}\"}}"))
.send()
.await?;
let response_status = response.status();
if response_status.is_success() {
state.heartbeat_subscribed = true;
}
Ok(())
}
pub(crate) async fn heartbeat_subscribed(&self) -> bool {
self.inner.read().await.heartbeat_subscribed
}
}
#[derive(Debug, Default)]
pub(crate) struct PushSubscriberState {
/// Device token.
device_token: Option<String>,
/// If subscribed to heartbeat push notifications.
heartbeat_subscribed: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i8)]
pub enum NotifyState {
/// Not subscribed to push notifications.
#[default]
NotConnected = 0,
/// Subscribed to heartbeat push notifications.
Heartbeat = 1,
/// Subscribed to push notifications for new messages.
Connected = 2,
}
impl Context {
/// Returns push notification subscriber state.
pub async fn push_state(&self) -> NotifyState {
if self.push_subscribed.load(Ordering::Relaxed) {
NotifyState::Connected
} else if self.push_subscriber.heartbeat_subscribed().await {
NotifyState::Heartbeat
} else {
NotifyState::NotConnected
}
}
}

View File

@@ -23,7 +23,6 @@ use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::socks::Socks5Config;
use crate::token;
use crate::tools::validate_id;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
@@ -250,6 +249,8 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
/// The function should be called after a QR code is scanned.
/// The function takes the raw text scanned and checks what can be done with it.
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
info!(context, "Scanned QR code: {}", qr);
let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
.await
@@ -346,18 +347,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
"".to_string()
};
let invitenumber = param
.get("i")
.filter(|&s| validate_id(s))
.map(|s| s.to_string());
let authcode = param
.get("s")
.filter(|&s| validate_id(s))
.map(|s| s.to_string());
let grpid = param
.get("x")
.filter(|&s| validate_id(s))
.map(|s| s.to_string());
let invitenumber = param.get("i").map(|s| s.to_string());
let authcode = param.get("s").map(|s| s.to_string());
let grpid = param.get("x").map(|s| s.to_string());
let grpname = if grpid.is_some() {
if let Some(encoded_name) = param.get("g") {
@@ -482,7 +474,8 @@ fn decode_account(qr: &str) -> Result<Qr> {
let payload = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("invalid DCACCOUNT payload")?;
let url = url::Url::parse(payload).context("Invalid account URL")?;
let url =
url::Url::parse(payload).with_context(|| format!("Invalid account URL: {payload:?}"))?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::Account {
domain: url
@@ -491,7 +484,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
.to_string(),
})
} else {
bail!("Bad scheme for account URL: {:?}.", url.scheme());
bail!("Bad scheme for account URL: {:?}.", payload);
}
}
@@ -502,7 +495,8 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
.context("invalid DCWEBRTC payload")?;
let (_type, url) = Message::parse_webrtc_instance(payload);
let url = url::Url::parse(&url).context("Invalid WebRTC instance")?;
let url =
url::Url::parse(&url).with_context(|| format!("Invalid WebRTC instance: {payload:?}"))?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::WebrtcInstance {
@@ -513,7 +507,7 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
instance_pattern: payload.to_string(),
})
} else {
bail!("Bad URL scheme for WebRTC instance: {:?}", url.scheme());
bail!("Bad URL scheme for WebRTC instance: {:?}", payload);
}
}
@@ -555,15 +549,16 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
.send()
.await?;
let response_status = response.status();
let response_text = response
.text()
.await
.context("Cannot create account, request failed: empty response")?;
let response_text = response.text().await.with_context(|| {
format!("Cannot create account, request to {url_str:?} failed: empty response")
})?;
if response_status.is_success() {
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
.with_context(|| {
format!("Cannot create account, response is malformed:\n{response_text:?}")
format!(
"Cannot create account, response from {url_str:?} is malformed:\n{response_text:?}"
)
})?;
context
.set_config_internal(Config::Addr, Some(&email))
@@ -658,7 +653,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
Qr::Login { address, options } => {
configure_from_login_qr(context, &address, options).await?
}
_ => bail!("QR code does not contain config"),
_ => bail!("qr code {:?} does not contain config", qr),
}
Ok(())
@@ -821,6 +816,8 @@ fn normalize_address(addr: &str) -> Result<String> {
#[cfg(test)]
mod tests {
use anyhow::Result;
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
@@ -1043,21 +1040,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_invalid_token() -> Result<()> {
let ctx = TestContext::new().await;
// Token cannot contain "/"
let qr = check_qr(
&ctx.ctx,
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
).await?;
assert!(matches!(qr, Qr::FprMismatch { .. }));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_secure_join() -> Result<()> {
let ctx = TestContext::new().await;

View File

@@ -268,7 +268,7 @@ pub(crate) async fn configure_from_login_qr(
#[cfg(test)]
mod test {
use anyhow::bail;
use anyhow::{self, bail};
use super::{decode_login, LoginOptions};
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};

View File

@@ -10,6 +10,7 @@ use crate::config::Config;
use crate::context::Context;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::session::Session as ImapSession;
use crate::imap::Imap;
use crate::message::{Message, Viewtype};
use crate::tools;
use crate::{stock_str, EventType};
@@ -110,7 +111,13 @@ impl Context {
/// As the message is added only once, the user is not spammed
/// in case for some providers the quota is always at ~100%
/// and new space is allocated as needed.
pub(crate) async fn update_recent_quota(&self, session: &mut ImapSession) -> Result<()> {
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result<()> {
if let Err(err) = imap.prepare(self).await {
warn!(self, "could not connect: {:#}", err);
return Ok(());
}
let session = imap.session.as_mut().context("no session")?;
let quota = if session.can_check_quota() {
let folders = get_watched_folders(self).await?;
get_unique_quota_roots_and_usage(session, folders).await
@@ -155,6 +162,10 @@ impl Context {
#[cfg(test)]
mod tests {
use super::*;
use crate::quota::{
QUOTA_ALLCLEAR_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE,
QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_needs_quota_warning() -> Result<()> {

View File

@@ -1,6 +1,7 @@
//! Internet Message Format reception pipeline.
use std::collections::HashSet;
use std::convert::TryFrom;
use anyhow::{Context as _, Result};
use mailparse::{parse_mail, SingleInfo};
@@ -37,9 +38,7 @@ use crate::simplify;
use crate::sql;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{
self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters, validate_id,
};
use crate::tools::{self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters};
use crate::{contact, imap};
/// This is the struct that is returned after receiving one email (aka MIME message).
@@ -316,9 +315,6 @@ pub(crate) async fn receive_imf_inner(
//
// If this is a mailing list email (i.e. list_id_header is some), don't change the displayname because in
// a mailing list the sender displayname sometimes does not belong to the sender email address.
// For example, GitHub sends messages from `notifications@github.com`,
// but uses display name of the user whose action generated the notification
// as the display name.
let (from_id, _from_id_blocked, incoming_origin) =
match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? {
Some(contact_id_res) => contact_id_res,
@@ -631,9 +627,12 @@ pub async fn from_field_to_contact_id(
if from_id == ContactId::SELF {
Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
} else {
let contact = Contact::get_by_id(context, from_id).await?;
let from_id_blocked = contact.blocked;
let incoming_origin = contact.origin;
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::get_by_id(context, from_id).await {
from_id_blocked = contact.blocked;
incoming_origin = contact.origin;
}
Ok(Some((from_id, from_id_blocked, incoming_origin)))
}
}
@@ -2357,10 +2356,7 @@ async fn apply_mailinglist_changes(
}
fn try_getting_grpid(mime_parser: &MimeMessage) -> Option<String> {
if let Some(optional_field) = mime_parser
.get_header(HeaderDef::ChatGroupId)
.filter(|s| validate_id(s))
{
if let Some(optional_field) = mime_parser.get_header(HeaderDef::ChatGroupId) {
return Some(optional_field.clone());
}
@@ -2683,6 +2679,7 @@ async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Opt
///
/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
/// References: header.
// TODO also save first entry of References and look for this?
async fn get_parent_message(
context: &Context,
mime_parser: &MimeMessage,

View File

@@ -1,20 +1,19 @@
use std::time::Duration;
use tokio::fs;
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{self, get_chat_msgs, ChatItem, ChatVisibility};
use crate::chat::{
add_contact_to_chat, add_to_chat_contacts_table, create_group_chat, get_chat_contacts,
get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg, ChatItem,
ChatVisibility,
is_contact_in_chat, remove_contact_from_chat, send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::download::{DownloadState, MIN_DOWNLOAD_LIMIT};
use crate::imap::prefetch_should_download;
use crate::imex::{imex, ImexMode};
use crate::message::{self, Message};
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
use crate::tools::SystemTime;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_grpid_simple() {
@@ -1693,7 +1692,7 @@ async fn test_in_reply_to_two_member_group() {
Subject: foo\n\
Message-ID: <message@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: foobarbaz12\n\
Chat-Group-ID: foo\n\
Chat-Group-Name: foo\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
@@ -1738,7 +1737,7 @@ async fn test_in_reply_to_two_member_group() {
Message-ID: <chatreply@example.org>\n\
In-Reply-To: <message@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: foobarbaz12\n\
Chat-Group-ID: foo\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
chat reply\n",
@@ -3547,32 +3546,6 @@ async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> {
Ok(())
}
/// If a message is Autocrypt-encrypted, unsigned Chat-Group-* headers have no effect.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unsigned_chat_group_hdr() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let bob_id = Contact::create(alice, "Bob", &bob_addr).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let sent_msg = alice.pop_sent_msg().await;
let bob_chat_id = bob.recv_msg(&sent_msg).await.chat_id;
bob_chat_id.accept(bob).await?;
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg.payload.replace(
"Chat-Version:",
&format!("Chat-Group-Member-Removed: {bob_addr}\r\nChat-Version:"),
);
let chat_id = alice.recv_msg(&sent_msg).await.chat_id;
assert_eq!(chat_id, alice_chat_id);
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 2);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -3611,52 +3584,6 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
Ok(())
}
/// Test for the bug when remote group membership changes from outdated messages overrode local
/// ones. Especially that was a problem when a message is sent offline so that it doesn't
/// incorporate recent group membership changes.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_outdated_membership_changes() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_id =
Contact::create(alice, "", &bob.get_config(Config::Addr).await?.unwrap()).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
// Alice creates a group chat. Bob accepts it.
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let msg = &alice.pop_sent_msg().await;
bob.recv_msg(msg).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
bob_chat_id.accept(bob).await?;
// Bob replies.
send_text_msg(bob, bob_chat_id, "i'm bob".to_string()).await?;
let msg = &bob.pop_sent_msg().await;
SystemTime::shift(Duration::from_secs(3600));
// Alice leaves.
remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?;
alice.pop_sent_msg().await;
assert!(!is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
// Alice receives Bob's message, but it's outdated to add Alice back.
alice.recv_msg(msg).await;
assert!(!is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
SystemTime::shift(Duration::from_secs(3600));
// Bob replies again adding Alice back.
send_text_msg(bob, bob_chat_id, "i'm bob".to_string()).await?;
let msg = &bob.pop_sent_msg().await;
alice.recv_msg(msg).await;
assert!(is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -3940,6 +3867,7 @@ async fn test_mua_can_readd() -> Result<()> {
// And leaves it.
remove_contact_from_chat(&alice, alice_chat.id, ContactId::SELF).await?;
let alice_chat = Chat::load_from_db(&alice, alice_chat.id).await?;
assert!(!is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
// Bob uses a classical MUA to answer, adding Alice back.
@@ -3948,7 +3876,7 @@ async fn test_mua_can_readd() -> Result<()> {
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>, <claire@example.org>, <fiona@example.org>\r\n\
Date: Mon, 12 Dec 3000 14:32:39 +0000\r\n\
Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
\r\n\
@@ -3957,6 +3885,8 @@ async fn test_mua_can_readd() -> Result<()> {
)
.await?
.unwrap();
let alice_chat = Chat::load_from_db(&alice, alice_chat.id).await?;
assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
Ok(())
}
@@ -4086,37 +4016,6 @@ async fn test_download_later() -> Result<()> {
Ok(())
}
/// Malice can pretend they have the same address as Alice and sends a message encrypted to Alice's
/// key but signed with another one. Alice must detect that this message is wrongly signed and not
/// treat it as Autocrypt-encrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_msg_forgery() -> Result<()> {
let mut tcm = TestContextManager::new();
let export_dir = tempfile::tempdir().unwrap();
let alice = &tcm.alice().await;
let alice_addr = &alice.get_config(Config::Addr).await?.unwrap();
imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
// We need Bob only to encrypt the forged message to Alice's key, actually Bob doesn't
// participate in the scenario.
let bob = &TestContext::new().await;
bob.configure_addr("bob@example.net").await;
imex(bob, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
let malice = &TestContext::new().await;
malice.configure_addr(alice_addr).await;
let malice_chat_id = tcm
.send_recv_accept(bob, malice, "hi from bob")
.await
.chat_id;
let sent_msg = malice.send_text(malice_chat_id, "hi from malice").await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.state, MessageState::OutDelivered);
assert!(!msg.get_showpadlock());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group_with_big_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -4310,105 +4209,3 @@ Chat-Group-Member-Added: charlie@example.com",
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg
.payload
.replace("bob@example.net", "notbob@example.net");
let msg = alice.recv_msg(&sent_msg).await;
assert!(msg.chat_id.is_trash());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_multiline_iso_8859_1_subject() -> Result<()> {
let t = &TestContext::new_alice().await;
let mail = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: bob@example.com\n\
To: alice@example.org, claire@example.com\n\
Subject:\n \
=?iso-8859-1?Q?Weihnachten_&_Silvester:_Feiern,_genie=DFen_&_entspannen_i?=\n \
=?iso-8859-1?Q?nmitten_der_Weing=E4rten?=\n\
Message-ID: <3333@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n";
receive_imf(t, mail, false).await?;
let msg = t.get_last_msg().await;
assert_eq!(
msg.get_subject(),
"Weihnachten & Silvester: Feiern, genießen & entspannen inmitten der Weingärten"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_references() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice.set_config_bool(Config::BccSelf, true).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let _sent = alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
let bob_received_msg = bob.recv_msg(&sent).await;
let bob_chat_id = bob_received_msg.chat_id;
// Alice sends another three messages, but two of them are lost.
let _sent = alice.send_text(alice_chat_id, "Second message").await;
let _sent = alice.send_text(alice_chat_id, "Third message").await;
// Message can still be assigned based on the `References` header.
let sent = alice.send_text(alice_chat_id, "Fourth message").await;
let bob_parsed_message = bob.parse_msg(&sent).await;
let bob_parent_message = get_parent_message(bob, &bob_parsed_message).await?.unwrap();
assert_eq!(bob_chat_id, bob_parent_message.chat_id);
// If more messages are lost, message cannot be assigned to the correct chat
// without `Chat-Group-ID` header, e.g. if the message is partially downloaded.
let sent = alice.send_text(alice_chat_id, "Fifth message").await;
let bob_parsed_message = bob.parse_msg(&sent).await;
let bob_parent_message = get_parent_message(bob, &bob_parsed_message).await?;
assert!(bob_parent_message.is_none());
// When the message is received, it is assigned correctly because of `Chat-Group-ID` header.
let bob_received_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_chat_id, bob_received_msg.chat_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_list_from() -> Result<()> {
let t = &TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/list-from.eml");
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.get_override_sender_name().unwrap(), "ÖAMTC");
let sender_contact = Contact::get_by_id(t, msg.from_id).await?;
assert_eq!(
sender_contact.get_display_name(),
"clubinfo@donotreply.oeamtc.at"
);
let info = msg.id.get_info(t).await?;
assert!(info.contains("Sent: 2024.03.20 09:00:01 by ~ÖAMTC (clubinfo@donotreply.oeamtc.at)"));
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More